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

1 // 登錄服務器 2 private void btnlogin_Click(object sender, EventArgs e) 3 { 4 // 創建接受套接字 5 IPAddress clientIP = IPAddress.Parse(txtLocalIP.Text); 6 clientIPEndPoint = new IPEndPoint(clientIP, int.Parse(txtlocalport.Text)); 7 receiveUdpClient = new UdpClient(clientIPEndPoint); 8 // 啟動接收線程 9 Thread receiveThread = new Thread(ReceiveMessage); 10 receiveThread.Start(); 11 12 // 匿名發送 13 sendUdpClient = new UdpClient(0); 14 // 啟動發送線程 15 Thread sendThread = new Thread(SendMessage); 16 sendThread.Start(string.Format("login,{0},{1}", txtusername.Text, clientIPEndPoint)); 17 18 btnlogin.Enabled = false; 19 btnLogout.Enabled = true; 20 this.Text = txtusername.Text; 21 } 22 23 // 客戶端接受服務器回應消息 24 private void ReceiveMessage() 25 { 26 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any,0); 27 while (true) 28 { 29 try 30 { 31 // 關閉receiveUdpClient時會產生異常 32 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint); 33 string message = Encoding.Unicode.GetString(receiveBytes,0,receiveBytes.Length); 34 35 // 處理消息 36 string[] splitstring = message.Split(','); 37 38 switch (splitstring[0]) 39 { 40 case "Accept": 41 try 42 { 43 tcpClient = new TcpClient(); 44 tcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitstring[1])); 45 if (tcpClient != null) 46 { 47 // 表示連接成功 48 networkStream = tcpClient.GetStream(); 49 binaryReader = new BinaryReader(networkStream); 50 } 51 } 52 catch 53 { 54 MessageBox.Show("連接失敗", "異常"); 55 } 56 57 Thread getUserListThread = new Thread(GetUserList); 58 getUserListThread.Start(); 59 break; 60 case "login": 61 string userItem = splitstring[1] + "," + splitstring[2]; 62 AddItemToListView(userItem); 63 break; 64 case "logout": 65 RemoveItemFromListView(splitstring[1]); 66 break; 67 case "talk": 68 for (int i = 0; i < chatFormList.Count; i++) 69 { 70 if (chatFormList[i].Text == splitstring[2]) 71 { 72 chatFormList[i].ShowTalkInfo(splitstring[2], splitstring[1], splitstring[3]); 73 } 74 } 75 76 break; 77 } 78 } 79 catch 80 { 81 break; 82 } 83 } 84 } 85 86 // 從服務器獲取在線用戶列表 87 private void GetUserList() 88 { 89 while (true) 90 { 91 userListstring = null; 92 try 93 { 94 userListstring = binaryReader.ReadString(); 95 if (userListstring.EndsWith("end")) 96 { 97 string[] splitstring = userListstring.Split(';'); 98 for (int i = 0; i < splitstring.Length - 1; i++) 99 { 100 AddItemToListView(splitstring[i]); 101 } 102 103 binaryReader.Close(); 104 tcpClient.Close(); 105 break; 106 } 107 } 108 catch 109 { 110 break; 111 } 112 } 113 } 114 // 發送登錄請求 115 private void SendMessage(object obj) 116 { 117 string message = (string)obj; 118 byte[] sendbytes = Encoding.Unicode.GetBytes(message); 119 IPAddress remoteIp = IPAddress.Parse(txtserverIP.Text); 120 IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(txtServerport.Text)); 121 sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint); 122 sendUdpClient.Close(); 123 }
程序的運行結果:
首先先運行服務器窗口,在服務器窗口點擊“啟動”按鈕來啟動服務器,然后客戶端首先指定服務器的端口號,修改用戶名(這里也可以不修改,使用默認的也可以),然后點擊“登錄”按鈕來登陸服務器(也就是告訴服務器本地的客戶端地址),然后從服務器端獲得在線用戶列表,界面演示如下:
然后用戶可以雙擊在線用戶進行聊天(此程序支持與多人進行聊天),下面是功能的演示圖片:
雙方進行聊天時,這里沒有實現像QQ一樣,有人發信息來在對應的客戶端就有消息提醒的功能的, 所以雙方進行聊天的過程中,每個客戶端都需要在在線用戶列表中點擊聊天的對象來激活聊天對話框(意思就是從圖片中可以看出“天涯”客戶端想和劍痴聊天的話,就在“在線用戶”列表雙擊劍痴來激活聊天窗口,同時“劍痴”客戶端也必須雙擊“天涯”來激活聊天窗口,這樣雙方就看到對方發來的信息了,(不激活窗口,也是發送了信息,只是沒有一個窗口來進行顯示)),而且從圖片中也可以看出——此程序支持與多人聊天,即天涯同時與“劍痴”和"大地"同時聊天。
四、總結
本專題介紹了如何去實現一個類似QQ的聊天程序,一方面讓大家可以鞏固前面專題的內容,另一方面讓大家更好的理解即時通信軟件(騰訊QQ)的工作原理和軟件協議的設計。
后面一專題將介紹如何去實現郵件系統中常用的功能——實現一個簡單的郵件應用。
本程序的源代碼鏈接:http://files.cnblogs.com/zhili/IM.zip 覺得有幫助的話還望推薦下,如果有任何意見可以留言。謝謝大家的支持