1. 程式人生 > >詳解C# 網路程式設計系列:實現類似QQ的即時通訊程式

詳解C# 網路程式設計系列:實現類似QQ的即時通訊程式

https://www.jb51.net/article/101289.htm

 

引言:

前面專題中介紹了UDP、TCP和P2P程式設計,並且通過一些小的示例來讓大家更好的理解它們的工作原理以及怎樣.Net類庫去實現它們的。為了讓大家更好的理解我們平常中常見的軟體QQ的工作原理,所以在本專題中將利用前面專題介紹的知識來實現一個類似QQ的聊天程式。

 一、即時通訊系統

在我們的生活中經常使用即時通訊的軟體,我們經常接觸到的有:QQ、阿里旺旺、MSN等等。這些都是屬於即時通訊(Instant Messenger,IM)軟體,IM是指所有能夠即時傳送和接收網際網路訊息的軟體。

在前面專題P2P程式設計中介紹過P2P系統分兩種型別——單純型P2P和混合型P2P(QQ就是屬於混合型的應用),混合型P2P系統中的伺服器(也叫索引伺服器)起到協調的作用。在檔案共享類應用中,如果採用混合型P2P技術的話,索引伺服器就儲存著檔案資訊,這樣就可能會造成版權的問題,然而在即時通訊類的軟體中, 因為客戶端傳遞的都是簡單的聊天文字而不是網路媒體資源,這樣就不存在版權問題了,在這種情況下,就可以採用混合型P2P技術來實現我們的即時通訊軟體。前面已經講了,騰訊的QQ就是屬於混合型P2P的軟體。

因此本專題要實現一個類似QQ的聊天程式,其中用到的P2P技術是屬於混合型P2P,而不是前一專題中的採用的單純型P2P技術,同時本程式的實現也會用到TCP、UDP程式設計技術。

二、程式實現的詳細設計

本程式採用P2P方式,各個客戶端之間直接發訊息進行聊天,伺服器在其中只是起到協調的作用,下面先理清下程式的流程:

2.1 程式流程設計

當一個新使用者通過客戶端登陸系統後,從伺服器獲取當線上的使用者資訊列表,列表資訊包括系統中每個使用者的地址,然後使用者就可以單獨向其他發訊息。如果有使用者加入或者線上使用者退出時,伺服器就會及時發訊息通知系統中的所有其他客戶端,達到它們即時地更新使用者資訊列表。

根據上面大致的描述,我們可以把系統的流程分為下面幾步來更好的理解(大家可以參考QQ程式將會更好的理解本程式的流程):

1.使用者通過客戶端進入系統,向伺服器發出訊息,請求登陸

2.伺服器收到請求後,向客戶端返回迴應訊息,表示同意接受該使用者加入,並把自己(指的是伺服器)所在監聽的埠傳送給客戶端

3.客戶端根據伺服器傳送過來的埠號和伺服器建立連線

4.伺服器通過該連線 把線上使用者的列表資訊傳送給新加入的客戶端。

5.客戶端獲得了線上使用者列表後就可以自己選擇線上使用者聊天。(程式中另外設計一個類似QQ的聊天視窗來進行聊天)

6.當用戶退出系統時也要及時通知伺服器,伺服器再把這個訊息轉發給每個線上的使用者,使客戶端及時更新本地的使用者資訊列表。 

2.2 通訊協議設計

所謂協議就是約定,即伺服器和客戶端之間會話資訊的內容格式進行約定,使雙方都可以識別,達到更好的通訊。

下面就具體介紹下協議的設計:

1. 客戶端和伺服器之間的對話

(1)登陸過程

① 客戶端用匿名UDP的方式向伺服器發出下面的資訊:

login, username, localIPEndPoint

 訊息內容包括三個欄位,每個欄位用 “,”分割,login表示的是請求登陸;username表示使用者名稱;localIPEndPint表示客戶端本地地址。

② 伺服器收到後以匿名UDP返回下面的迴應:

Accept, port

其中Accept表示伺服器接受請求,port表示伺服器所在的埠號,伺服器監聽著這個埠的客戶端連線

③ 連線伺服器,獲取使用者列表

客戶端從上一步獲得了埠號,然後向該埠發起TCP連線,向伺服器索取線上使用者列表,伺服器接受連線後將使用者列表傳輸到客戶端。使用者列表資訊格式如下:

 username1,IPEndPoint1;username2,IPEndPoint2;...;end

username1、username2表示使用者名稱,IPEndPoint1,IPEndPoint2表示對應的端點,每個使用者資訊都是由"使用者名稱+端點"組成,使用者資訊以“;”隔開,整個使用者列表以“end”結尾。

(2)登出過程

使用者退出時,向伺服器傳送如下訊息:

logout,username,localIPEndPoint

這條訊息看字面意思大家都知道就是告訴伺服器 username+localIPEndPoint這個使用者要退出了。

2. 伺服器管理使用者

(1)新使用者加入通知

  因為系統中線上的每個使用者都有一份當前線上使用者表,因此當有新使用者登入時,伺服器不需要重複地給系統中的每個使用者再發送所有使用者資訊,只需要將新加入使用者的資訊通知其他使用者,其他使用者再更新自己的使用者列表。

伺服器向系統中每個使用者廣播如下資訊:login,username,remoteIPEndPoint

在這個過程中伺服器只是負責將收到的"login"資訊轉發出去。

(2)使用者退出

  與新使用者加入一樣,伺服器將使用者退出的訊息進行廣播轉發:logout,username,remoteIPEndPoint

3. 客戶端之間聊天

使用者進行聊天時,各自的客戶端之間是以P2P方式進行工作的,不與伺服器有直接聯絡,這也是P2P技術的特點。

聊天傳送的訊息格式如下:talk, longtime, selfUserName, message

其中,talk表明這是聊天內容的訊息;longtime是長時間格式的當前系統時間;selfUserName為傳送發的使用者名稱;message表示訊息的內容。

協議設計介紹完後,下面就進入本程式的具體實現的介紹的。

注:協議是本程式的核心,也是所有軟體的核心,每個軟體產品的協議都是不一樣的,QQ有自己的一套協議,MSN又有另一套協議,所以使用的QQ的使用者無法和用MSN的朋友進行聊天。 

三、程式的實現

 伺服器端核心程式碼:

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 // 啟動伺服器      // 根據部落格中協議的設計部分      // 客戶端先向伺服器傳送登入請求,然後通過伺服器返回的埠號      // 再與伺服器建立連線      // 所以啟動服務按鈕事件中有兩個套接字:一個是接收客戶端資訊套接字和      // 監聽客戶端連線套接字      private void btnStart_Click( object sender, EventArgs e)      {        // 建立接收套接字        serverIp = IPAddress.Parse(txbServerIP.Text);        serverIPEndPoint = new IPEndPoint(serverIp, int .Parse(txbServerport.Text));        receiveUdpClient = new UdpClient(serverIPEndPoint);        // 啟動接收執行緒        Thread receiveThread = new Thread(ReceiveMessage);        receiveThread.Start();        btnStart.Enabled = false ;        btnStop.Enabled = true ;          // 隨機指定監聽埠        Random random = new Random();        tcpPort = random.Next(port + 1, 65536);          // 建立監聽套接字        tcpListener = new TcpListener(serverIp, tcpPort);        tcpListener.Start();          // 啟動監聽執行緒        Thread listenThread = new Thread(ListenClientConnect);        listenThread.Start();        AddItemToListBox( string .Format( "伺服器執行緒{0}啟動,監聽埠{1}" ,serverIPEndPoint,tcpPort));      }        // 接收客戶端發來的資訊      private void ReceiveMessage()      {        IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);        while ( true )        {          try          {            // 關閉receiveUdpClient時下面一行程式碼會產生異常            byte [] receiveBytes = receiveUdpClient.Receive( ref remoteIPEndPoint);            string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);              // 顯示訊息內容            AddItemToListBox( string .Format( "{0}:{1}" ,remoteIPEndPoint,message));              // 處理訊息資料            // 根據協議的設計部分,從客戶端傳送來的訊息是具有一定格式的            // 伺服器接收訊息後要對訊息做處理            string [] splitstring = message.Split( ',' );            // 解析使用者端地址            string [] splitsubstring = splitstring[2].Split( ':' );            IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitsubstring[0]), int .Parse(splitsubstring[1]));            switch (splitstring[0])            {              // 如果是登入資訊,向客戶端傳送應答訊息和廣播有新使用者登入訊息              case "login" :                User user = new User(splitstring[1], clientIPEndPoint);                // 往線上的使用者列表新增新成員                userList.Add(user);                AddItemToListBox( string .Format( "使用者{0}({1})加入" , user.GetName(), user.GetIPEndPoint()));                string sendString = "Accept," + tcpPort.ToString();                // 向客戶端傳送應答訊息                SendtoClient(user, sendString);                AddItemToListBox( string .Format( "向{0}({1})發出:[{2}]" , user.GetName(), user.GetIPEndPoint(), sendString));                for ( int i = 0; i < userList.Count; i++)                {                  if (userList[i].GetName() != user.GetName())                  {                    // 給線上的其他使用者傳送廣播訊息                    // 通知有新使用者加入                    SendtoClient(userList[i], message);                  }                }                  AddItemToListBox( string .Format( "廣播:[{0}]" , message));                break ;              case "logout" :                for ( int i = 0; i < userList.Count; i++)                {                  if (userList[i].GetName() == splitstring[1])                  {                    AddItemToListBox( string .Format( "使用者{0}({1})退出" ,userList[i].GetName(),userList[i].GetIPEndPoint()));                    userList.RemoveAt(i); // 移除使用者                  }                }                for ( int i = 0; i < userList.Count; i++)                {                  // 廣播登出訊息                  SendtoClient(userList[i], message);                }                AddItemToListBox( string .Format( "廣播:[{0}]" , message));                break ;            }          }          catch          {            // 傳送異常退出迴圈            break ;          }        }        AddItemToListBox( string .Format( "服務執行緒{0}終止" , serverIPEndPoint));      }        // 向客戶端傳送訊息      private void SendtoClient(User user, string message)      {        // 匿名方式傳送        sendUdpClient = new UdpClient(0);        byte [] sendBytes = Encoding.Unicode.GetBytes(message);        IPEndPoint remoteIPEndPoint =user.GetIPEndPoint();        sendUdpClient.Send(sendBytes,sendBytes.Length,remoteIPEndPoint);        sendUdpClient.Close();      }            // 接受客戶端的連線      private void ListenClientConnect()      {        TcpClient newClient = null ;        while ( true )        {          try          {            newClient = tcpListener.AcceptTcpClient();            AddItemToListBox( string .Format( "接受客戶端{0}的TCP請求" ,newClient.Client.RemoteEndPoint));          }          catch          {            AddItemToListBox( string .Format( "監聽執行緒({0}:{1})" , serverIp, tcpPort));            break ;          }            Thread sendThread = new Thread(SendData);          sendThread.Start(newClient);        }      }        // 向客戶端傳送線上使用者列表資訊      // 伺服器通過TCP連線把線上使用者列表資訊傳送給客戶端      private void SendData( object userClient)      {        TcpClient newUserClient = (TcpClient)userClient;        userListstring = null ;        for ( int i = 0; i < userList.Count; i++)        {          userListstring += userList[i].GetName() + ","            + userList[i].GetIPEndPoint().ToString() + ";" ;        }          userListstring += "end" ;        networkStream = newUserClient.GetStream();        binaryWriter = new BinaryWriter(networkStream);        binaryWriter.Write(userListstring);        binaryWriter.Flush();        AddItemToListBox( string .Format( "向{0}傳送[{1}]" , newUserClient.Client.RemoteEndPoint, userListstring));        binaryWriter.Close();        newUserClient.Close();      }

客戶端核心程式碼:

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64

相關推薦

C# 網路程式設計系列實現類似QQ即時通訊程式

https://www.jb51.net/article/101289.htm   引言: 前面專題中介紹了UDP、TCP和P2P程式設計,並且通過一些小的示例來讓大家更好的理解它們的工作原理以及怎樣.Net類庫去實現它們的。為了讓大家更好的理解我們平常中常見的軟體QQ的工作原理,所以在本專題

C# 網絡編程系列實現類似QQ即時通信程序

並且 會話 hat chat .sh odin unicode 情況 plist 引言: 前面專題中介紹了UDP、TCP和P2P編程,並且通過一些小的示例來讓大家更好的理解它們的工作原理以及怎樣.Net類庫去實現它們的。為了讓大家更好的理解我們平常中常見的軟件QQ的工作原理

[C# 網路程式設計系列]專題十實現簡單的郵件收發器

引言: 在我們的平常工作中,郵件的傳送和接收應該是我們經常要使用到的功能的。因此知道電子郵件的應用程式的原理也是非常有必要的,在這一個專題中將介紹電子郵件應用程式的原理、電子郵件應用程式中涉及的協議和實現一個簡答的電子郵件收發器程式。 一、郵件應用程式基本知識 1

C#網路程式設計系列文章(一)之Socket實現非同步TCP伺服器

原創性宣告 文章系列目錄 程式碼下載地址 開篇 本人因為對於網路程式設計的喜愛,經常性的使用c#編寫各類伺服器(e.g TCP伺服器,UDP伺服器),但是基本上都是搞著玩,網上也有很多講c#網路程式設計的文章,當然我也參考了很多作者寫的文章。看了這篇

linux下C/C++網路程式設計基本socket實現tcp和udp的例子

簡單的linux下socket程式設計,分別基於TCP和UDP協議實現的簡單程式 linux下socket程式設計可以概括為以下幾個函式的運用: socket() bind() listen

C#網路程式設計系列文章(八)之UdpClient實現同步UDP伺服器

原創性宣告 本文作者:小竹zz 本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 轉載請註明出處 文章系列目錄 本文介紹 UdpClient 類在同步阻塞模式中為傳送和接收無連線的 U

C#網路程式設計系列文章(二)之Socket實現同步TCP伺服器

原創性宣告 本文作者:小竹zz  本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 轉載請註明出處 文章系列目錄 本文介紹 在上一篇部落格中我說了,我將會介紹c#中使用Socke

C#網路程式設計系列文章(三)之TcpListener實現非同步TCP伺服器

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net.Sockets; using System.Net; namespace NetFrame.Net.TC

C#網路程式設計系列文章(五)之Socket實現非同步UDP伺服器

原創性宣告 本文作者:小竹zz 本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 轉載請註明出處 文章系列目錄 本文介紹 在.Net中,System.Net.Sockets 名稱

圖文網路程式設計

1.網路程式設計 什麼是網路程式設計 多臺計算機連線形成網路 網路的作用 資料互動 資料共享 2.網路三要素 網路協議:雙方實體完成通訊或服務所必須遵循的規則和約定  &

c++網路程式設計3UDP程式設計

一.概念:         UDP是傳輸層中面向無連線的協議,所以UDP丟包後是不會重傳的,而且他在程式設計上服務端和客戶端是沒有區別的,有的只是“虛擬上”的服務端和客戶端,他在程式設計的實現上也很簡單,不像TCP那麼複雜。 二.UDP終端的程式設計       由於UD

c++網路程式設計2TCP連線概念及程式設計

一.TCP建立連線的三次握手 在TCP/IP協議中,TCP協議提供可靠的連線服務,採用三次握手建立一個連線。 第一次握手:建立連線時,客戶端傳送SYN包(SYN=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認;【客戶端->服務端:SYN(j)】 第二次握手

Java網路程式設計——使用NIO實現非阻塞Socket通訊

       除了普通的Socket與ServerSocket實現的阻塞式通訊外,java提供了非阻塞式通訊的NIO API。先看一下NIO的實現原理。        從圖中可以看出,伺服器上所有Channel(包括ServerSocketChannel和Socket

Visual C++網路程式設計經典案例 第3章 多執行緒與非同步套接字程式設計 實現執行緒同步 互斥物件 使用API函式操作互斥物件

互斥物件和臨界區物件和事件物件作用一樣 用於實現執行緒同步 互斥物件可以線上程中使用 CreateMutex()建立並返回互斥物件 原型如下 HANDLE CreateMutex(   LPSECURITY_ATTIRIBUTES lpMutexAttributes,  

Visual C++網路程式設計經典案例 第3章 多執行緒與非同步套接字程式設計 實現執行緒同步 互斥物件 程式的唯一執行

互斥物件可在程序中使用 使用者在程序建立互斥物件實現程式例項唯一執行 建立控制檯工程 #include<windows.h>                                //包含標頭檔案 #include<stdio.h> in

Visual C++網路程式設計經典案例 第5章 網頁瀏覽器 製作個性化介面 如何實現收藏夾功能 新增訊息響應函式

使用者將網址新增到收藏夾以後 便可以直接單擊選單選單中的網址進行瀏覽 使用者單擊選單的訊息響應函式重要 首先在CMainFrame類的標頭檔案MainFrm.h 中定義一個彈出選單的訊息響應函式 程式碼如下 afx_msg void OnMenuClick(int nID); //定

Visual C++網路程式設計經典案例 第2章 Winsock網路程式開發流程 基於UDP的Sockets程式設計 UDP伺服器

vc建立控制檯程式視窗的應用程式 命名為 UDP伺服器UDPSever.cpp #include<winsock2.h> #include<stdio.h> #include<windows.h> #pragma comment(lib,

Visual C++網路程式設計經典案例 第2章 Winsock網路程式開發流程 基於UDP的Sockets程式設計 UDP客戶端

在VC中建立UDP客戶端程式 控制檯 #include<winsock2.h> #include<stdio.h> #include<windows.h> #pragma comment(lib,"WS2_32.lib") int mai

Visual C++網路程式設計經典案例 第2章 Winsock網路程式開發流程 TCP伺服器程式 介面初始化

與TCP客戶端一樣 伺服器程式啟動時需要介面初始化 不管伺服器在初始化時 還應該同時完成套接字的建立以及地址繫結等處理工作 class CTCPDlg : public CDialog { // Construction public:     CTCPDlg(CWnd*

Visual C++網路程式設計經典案例 第3章 多執行緒與非同步套接字程式設計 程序間通訊 命名管道 命名管道例項

vc新增控制檯工程 名字命名管道例項 新增原始檔 名字 伺服器 #include<windows.h>                                //包含標頭檔案 #include<stdio.h> int main() {