天天看點

C#網絡程式設計系列九:類似QQ的即時通信程式

引言:前面專題中介紹了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的朋友進行聊天。

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

三、程式的實作

伺服器端核心代碼

View Code   
  // 啟動伺服器  
         // 根據部落格中協定的設計部分  
         // 用戶端先向伺服器發送登入請求,然後通過伺服器傳回的端口号  
         // 再與伺服器建立連接配接  
         // 是以啟動服務按鈕事件中有兩個套接字:一個是接收用戶端資訊套接字和  
         // 監聽用戶端連接配接套接字  
         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();  
         } 
           
View Code   
  // 登入伺服器  
         private void btnlogin_Click(object sender, EventArgs e)  
         {  
             // 建立接受套接字  
             IPAddress clientIP = IPAddress.Parse(txtLocalIP.Text);  
             clientIPEndPoint = new IPEndPoint(clientIP, int.Parse(txtlocalport.Text));  
             receiveUdpClient = new UdpClient(clientIPEndPoint);  
             // 啟動接收線程  
             Thread receiveThread = new Thread(ReceiveMessage);  
             receiveThread.Start();  
   
             // 匿名發送  
             sendUdpClient = new UdpClient(0);  
             // 啟動發送線程  
             Thread sendThread = new Thread(SendMessage);  
             sendThread.Start(string.Format("login,{0},{1}", txtusername.Text, clientIPEndPoint));  
   
             btnlogin.Enabled = false;  
             btnLogout.Enabled = true;  
             this.Text = txtusername.Text;  
         }  
   
         // 用戶端接受伺服器回應消息   
         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);  
   
                     // 處理消息  
                     string[] splitstring = message.Split(',');  
   
                     switch (splitstring[0])  
                     {  
                         case "Accept":  
                             try 
                             {  
                                 tcpClient = new TcpClient();  
                                 tcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitstring[1]));  
                                 if (tcpClient != null)  
                                 {  
                                     // 表示連接配接成功  
                                     networkStream = tcpClient.GetStream();  
                                     binaryReader = new BinaryReader(networkStream);  
                                 }  
                             }  
                             catch 
                             {  
                                 MessageBox.Show("連接配接失敗", "異常");  
                             }  
   
                             Thread getUserListThread = new Thread(GetUserList);  
                             getUserListThread.Start();  
                             break;  
                         case "login":  
                             string userItem = splitstring[1] + "," + splitstring[2];  
                             AddItemToListView(userItem);  
                             break;  
                         case "logout":  
                             RemoveItemFromListView(splitstring[1]);  
                             break;  
                         case "talk":  
                             for (int i = 0; i < chatFormList.Count; i++)  
                             {  
                                 if (chatFormList[i].Text == splitstring[2])  
                                 {  
                                     chatFormList[i].ShowTalkInfo(splitstring[2], splitstring[1], splitstring[3]);  
                                 }  
                             }  
   
                             break;  
                     }  
                 }  
                 catch 
                 {  
                     break;  
                 }  
             }  
         }  
   
         // 從伺服器擷取線上使用者清單  
         private void GetUserList()  
         {  
             while (true)  
             {  
                 userListstring = null;  
                 try 
                 {  
                     userListstring = binaryReader.ReadString();  
                     if (userListstring.EndsWith("end"))  
                     {  
                         string[] splitstring = userListstring.Split(';');  
                         for (int i = 0; i < splitstring.Length - 1; i++)  
                         {  
                             AddItemToListView(splitstring[i]);  
                         }  
   
                         binaryReader.Close();  
                         tcpClient.Close();  
                         break;  
                     }  
                 }  
                 catch 
                 {  
                     break;  
                 }  
             }  
         }  
    // 發送登入請求  
         private void SendMessage(object obj)  
         {  
             string message = (string)obj;  
             byte[] sendbytes = Encoding.Unicode.GetBytes(message);  
             IPAddress remoteIp = IPAddress.Parse(txtserverIP.Text);  
             IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(txtServerport.Text));  
             sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);  
             sendUdpClient.Close();  
         } 
           

首先先運作伺服器視窗,在伺服器視窗點選“啟動”按鈕來啟動伺服器,然後用戶端首先指定伺服器的端口号,修改使用者名(這裡也可以不修改,使用預設的也可以),然後點選“登入”按鈕來登陸伺服器(也就是告訴伺服器本地的用戶端位址),然後從伺服器端獲得線上使用者清單,界面示範如下:

C#網絡程式設計系列九:類似QQ的即時通信程式

然後使用者可以輕按兩下線上使用者進行聊天(此程式支援與多人進行聊天),下面是功能的示範圖檔:

C#網絡程式設計系列九:類似QQ的即時通信程式

雙方進行聊天時,這裡沒有實作像QQ一樣,有人發資訊來在對應的用戶端就有消息提醒的功能的, 是以雙方進行聊天的過程中,每個用戶端都需要在線上使用者清單中點選聊天的對象來激活聊天對話框(意思就是從圖檔中可以看出“天涯”用戶端想和劍癡聊天的話,就在“線上使用者”清單輕按兩下劍癡來激活聊天視窗,同時“劍癡”用戶端也必須輕按兩下“天涯”來激活聊天視窗,這樣雙方就看到對方發來的資訊了,(不激活視窗,也是發送了資訊,隻是沒有一個視窗來進行顯示)),而且從圖檔中也可以看出——此程式支援與多人聊天,即天涯同時與“劍癡”和"大地"同時聊天。

四、總結

本專題介紹了如何去實作一個類似QQ的聊天程式,一方面讓大家可以鞏固前面專題的内容,另一方面讓大家更好的了解即時通信軟體(騰訊QQ)的工作原理和軟體協定的設計。

後面一專題将介紹如何去實作郵件系統中常用的功能——實作一個簡單的郵件應用。

本程式的源代碼連結:http://files.cnblogs.com/zhili/IM.zip 

原文連結:http://www.cnblogs.com/zhili/archive/2012/09/23/QQ_P2P.html