詳解C# 網(wǎng)絡(luò)編程系列:實現(xiàn)類似QQ的即時通信程序
引言:
前面專題中介紹了UDP、TCP和P2P編程,并且通過一些小的示例來讓大家更好的理解它們的工作原理以及怎樣.Net類庫去實現(xiàn)它們的。為了讓大家更好的理解我們平常中常見的軟件QQ的工作原理,所以在本專題中將利用前面專題介紹的知識來實現(xiàn)一個類似QQ的聊天程序。
一、即時通信系統(tǒng)
在我們的生活中經(jīng)常使用即時通信的軟件,我們經(jīng)常接觸到的有:QQ、阿里旺旺、MSN等等。這些都是屬于即時通信(Instant Messenger,IM)軟件,IM是指所有能夠即時發(fā)送和接收互聯(lián)網(wǎng)消息的軟件。
在前面專題P2P編程中介紹過P2P系統(tǒng)分兩種類型——單純型P2P和混合型P2P(QQ就是屬于混合型的應(yīng)用),混合型P2P系統(tǒng)中的服務(wù)器(也叫索引服務(wù)器)起到協(xié)調(diào)的作用。在文件共享類應(yīng)用中,如果采用混合型P2P技術(shù)的話,索引服務(wù)器就保存著文件信息,這樣就可能會造成版權(quán)的問題,然而在即時通信類的軟件中, 因為客戶端傳遞的都是簡單的聊天文本而不是網(wǎng)絡(luò)媒體資源,這樣就不存在版權(quán)問題了,在這種情況下,就可以采用混合型P2P技術(shù)來實現(xiàn)我們的即時通信軟件。前面已經(jīng)講了,騰訊的QQ就是屬于混合型P2P的軟件。
因此本專題要實現(xiàn)一個類似QQ的聊天程序,其中用到的P2P技術(shù)是屬于混合型P2P,而不是前一專題中的采用的單純型P2P技術(shù),同時本程序的實現(xiàn)也會用到TCP、UDP編程技術(shù)。
二、程序?qū)崿F(xiàn)的詳細(xì)設(shè)計
本程序采用P2P方式,各個客戶端之間直接發(fā)消息進(jìn)行聊天,服務(wù)器在其中只是起到協(xié)調(diào)的作用,下面先理清下程序的流程:
2.1 程序流程設(shè)計
當(dāng)一個新用戶通過客戶端登陸系統(tǒng)后,從服務(wù)器獲取當(dāng)在線的用戶信息列表,列表信息包括系統(tǒng)中每個用戶的地址,然后用戶就可以單獨(dú)向其他發(fā)消息。如果有用戶加入或者在線用戶退出時,服務(wù)器就會及時發(fā)消息通知系統(tǒng)中的所有其他客戶端,達(dá)到它們即時地更新用戶信息列表。
根據(jù)上面大致的描述,我們可以把系統(tǒng)的流程分為下面幾步來更好的理解(大家可以參考QQ程序?qū)玫睦斫獗境绦虻牧鞒蹋?br />
1.用戶通過客戶端進(jìn)入系統(tǒng),向服務(wù)器發(fā)出消息,請求登陸
2.服務(wù)器收到請求后,向客戶端返回回應(yīng)消息,表示同意接受該用戶加入,并把自己(指的是服務(wù)器)所在監(jiān)聽的端口發(fā)送給客戶端
3.客戶端根據(jù)服務(wù)器發(fā)送過來的端口號和服務(wù)器建立連接
4.服務(wù)器通過該連接 把在線用戶的列表信息發(fā)送給新加入的客戶端。
5.客戶端獲得了在線用戶列表后就可以自己選擇在線用戶聊天。(程序中另外設(shè)計一個類似QQ的聊天窗口來進(jìn)行聊天)
6.當(dāng)用戶退出系統(tǒng)時也要及時通知服務(wù)器,服務(wù)器再把這個消息轉(zhuǎn)發(fā)給每個在線的用戶,使客戶端及時更新本地的用戶信息列表。
2.2 通信協(xié)議設(shè)計
所謂協(xié)議就是約定,即服務(wù)器和客戶端之間會話信息的內(nèi)容格式進(jìn)行約定,使雙方都可以識別,達(dá)到更好的通信。
下面就具體介紹下協(xié)議的設(shè)計:
1. 客戶端和服務(wù)器之間的對話
(1)登陸過程
① 客戶端用匿名UDP的方式向服務(wù)器發(fā)出下面的信息:
login, username, localIPEndPoint
消息內(nèi)容包括三個字段,每個字段用 “,”分割,login表示的是請求登陸;username表示用戶名;localIPEndPint表示客戶端本地地址。
② 服務(wù)器收到后以匿名UDP返回下面的回應(yīng):
Accept, port
其中Accept表示服務(wù)器接受請求,port表示服務(wù)器所在的端口號,服務(wù)器監(jiān)聽著這個端口的客戶端連接
③ 連接服務(wù)器,獲取用戶列表
客戶端從上一步獲得了端口號,然后向該端口發(fā)起TCP連接,向服務(wù)器索取在線用戶列表,服務(wù)器接受連接后將用戶列表傳輸?shù)娇蛻舳恕S脩袅斜硇畔⒏袷饺缦拢?/p>
username1,IPEndPoint1;username2,IPEndPoint2;...;end
username1、username2表示用戶名,IPEndPoint1,IPEndPoint2表示對應(yīng)的端點(diǎn),每個用戶信息都是由"用戶名+端點(diǎn)"組成,用戶信息以“;”隔開,整個用戶列表以“end”結(jié)尾。
(2)注銷過程
用戶退出時,向服務(wù)器發(fā)送如下消息:
logout,username,localIPEndPoint
這條消息看字面意思大家都知道就是告訴服務(wù)器 username+localIPEndPoint這個用戶要退出了。
2. 服務(wù)器管理用戶
(1)新用戶加入通知
因為系統(tǒng)中在線的每個用戶都有一份當(dāng)前在線用戶表,因此當(dāng)有新用戶登錄時,服務(wù)器不需要重復(fù)地給系統(tǒng)中的每個用戶再發(fā)送所有用戶信息,只需要將新加入用戶的信息通知其他用戶,其他用戶再更新自己的用戶列表。
服務(wù)器向系統(tǒng)中每個用戶廣播如下信息:login,username,remoteIPEndPoint
在這個過程中服務(wù)器只是負(fù)責(zé)將收到的"login"信息轉(zhuǎn)發(fā)出去。
(2)用戶退出
與新用戶加入一樣,服務(wù)器將用戶退出的消息進(jìn)行廣播轉(zhuǎn)發(fā):logout,username,remoteIPEndPoint
3. 客戶端之間聊天
用戶進(jìn)行聊天時,各自的客戶端之間是以P2P方式進(jìn)行工作的,不與服務(wù)器有直接聯(lián)系,這也是P2P技術(shù)的特點(diǎn)。
聊天發(fā)送的消息格式如下:talk, longtime, selfUserName, message
其中,talk表明這是聊天內(nèi)容的消息;longtime是長時間格式的當(dāng)前系統(tǒng)時間;selfUserName為發(fā)送發(fā)的用戶名;message表示消息的內(nèi)容。
協(xié)議設(shè)計介紹完后,下面就進(jìn)入本程序的具體實現(xiàn)的介紹的。
注:協(xié)議是本程序的核心,也是所有軟件的核心,每個軟件產(chǎn)品的協(xié)議都是不一樣的,QQ有自己的一套協(xié)議,MSN又有另一套協(xié)議,所以使用的QQ的用戶無法和用MSN的朋友進(jìn)行聊天。
三、程序的實現(xiàn)
服務(wù)器端核心代碼:
// 啟動服務(wù)器 // 根據(jù)博客中協(xié)議的設(shè)計部分 // 客戶端先向服務(wù)器發(fā)送登錄請求,然后通過服務(wù)器返回的端口號 // 再與服務(wù)器建立連接 // 所以啟動服務(wù)按鈕事件中有兩個套接字:一個是接收客戶端信息套接字和 // 監(jiān)聽客戶端連接套接字 private void btnStart_Click(object sender, EventArgs e) { // 創(chuàng)建接收套接字 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; // 隨機(jī)指定監(jiān)聽端口 Random random = new Random(); tcpPort = random.Next(port + 1, 65536); // 創(chuàng)建監(jiān)聽套接字 tcpListener = new TcpListener(serverIp, tcpPort); tcpListener.Start(); // 啟動監(jiān)聽線程 Thread listenThread = new Thread(ListenClientConnect); listenThread.Start(); AddItemToListBox(string.Format("服務(wù)器線程{0}啟動,監(jiān)聽端口{1}",serverIPEndPoint,tcpPort)); } // 接收客戶端發(fā)來的信息 private void ReceiveMessage() { IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0); while (true) { try { // 關(guān)閉receiveUdpClient時下面一行代碼會產(chǎn)生異常 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint); string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length); // 顯示消息內(nèi)容 AddItemToListBox(string.Format("{0}:{1}",remoteIPEndPoint,message)); // 處理消息數(shù)據(jù) // 根據(jù)協(xié)議的設(shè)計部分,從客戶端發(fā)送來的消息是具有一定格式的 // 服務(wù)器接收消息后要對消息做處理 string[] splitstring = message.Split(','); // 解析用戶端地址 string[] splitsubstring = splitstring[2].Split(':'); IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitsubstring[0]), int.Parse(splitsubstring[1])); switch (splitstring[0]) { // 如果是登錄信息,向客戶端發(fā)送應(yīng)答消息和廣播有新用戶登錄消息 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(); // 向客戶端發(fā)送應(yīng)答消息 SendtoClient(user, sendString); AddItemToListBox(string.Format("向{0}({1})發(fā)出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString)); for (int i = 0; i < userList.Count; i++) { if (userList[i].GetName() != user.GetName()) { // 給在線的其他用戶發(fā)送廣播消息 // 通知有新用戶加入 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 { // 發(fā)送異常退出循環(huán) break; } } AddItemToListBox(string.Format("服務(wù)線程{0}終止", serverIPEndPoint)); } // 向客戶端發(fā)送消息 private void SendtoClient(User user, string message) { // 匿名方式發(fā)送 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("接受客戶端{(lán)0}的TCP請求",newClient.Client.RemoteEndPoint)); } catch { AddItemToListBox(string.Format("監(jiān)聽線程({0}:{1})", serverIp, tcpPort)); break; } Thread sendThread = new Thread(SendData); sendThread.Start(newClient); } } // 向客戶端發(fā)送在線用戶列表信息 // 服務(wù)器通過TCP連接把在線用戶列表信息發(fā)送給客戶端 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}發(fā)送[{1}]", newUserClient.Client.RemoteEndPoint, userListstring)); binaryWriter.Close(); newUserClient.Close(); }
客戶端核心代碼:
// 登錄服務(wù)器 private void btnlogin_Click(object sender, EventArgs e) { // 創(chuàng)建接受套接字 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(); // 匿名發(fā)送 sendUdpClient = new UdpClient(0); // 啟動發(fā)送線程 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; } // 客戶端接受服務(wù)器回應(yīng)消息 private void ReceiveMessage() { IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any,0); while (true) { try { // 關(guān)閉receiveUdpClient時會產(chǎn)生異常 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; } } } // 從服務(wù)器獲取在線用戶列表 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; } } } // 發(fā)送登錄請求 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(); }
程序的運(yùn)行結(jié)果:
首先先運(yùn)行服務(wù)器窗口,在服務(wù)器窗口點(diǎn)擊“啟動”按鈕來啟動服務(wù)器,然后客戶端首先指定服務(wù)器的端口號,修改用戶名(這里也可以不修改,使用默認(rèn)的也可以),然后點(diǎn)擊“登錄”按鈕來登陸服務(wù)器(也就是告訴服務(wù)器本地的客戶端地址),然后從服務(wù)器端獲得在線用戶列表,界面演示如下:
然后用戶可以雙擊在線用戶進(jìn)行聊天(此程序支持與多人進(jìn)行聊天),下面是功能的演示圖片:
雙方進(jìn)行聊天時,這里沒有實現(xiàn)像QQ一樣,有人發(fā)信息來在對應(yīng)的客戶端就有消息提醒的功能的, 所以雙方進(jìn)行聊天的過程中,每個客戶端都需要在在線用戶列表中點(diǎn)擊聊天的對象來激活聊天對話框(意思就是從圖片中可以看出“天涯”客戶端想和劍癡聊天的話,就在“在線用戶”列表雙擊劍癡來激活聊天窗口,同時“劍癡”客戶端也必須雙擊“天涯”來激活聊天窗口,這樣雙方就看到對方發(fā)來的信息了,(不激活窗口,也是發(fā)送了信息,只是沒有一個窗口來進(jìn)行顯示)),而且從圖片中也可以看出——此程序支持與多人聊天,即天涯同時與“劍癡”和"大地"同時聊天。
本程序的源代碼鏈接:demo
四、總結(jié)
本專題介紹了如何去實現(xiàn)一個類似QQ的聊天程序,一方面讓大家可以鞏固前面專題的內(nèi)容,另一方面讓大家更好的理解即時通信軟件(騰訊QQ)的工作原理和軟件協(xié)議的設(shè)計。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- C# TcpClient網(wǎng)絡(luò)編程傳輸文件的示例
- C# 網(wǎng)絡(luò)編程之UDP
- c# 網(wǎng)絡(luò)編程之tcp
- c# 網(wǎng)絡(luò)編程之http
- 深入學(xué)習(xí)C#網(wǎng)絡(luò)編程之HTTP應(yīng)用編程(下)
- 深入學(xué)習(xí)C#網(wǎng)絡(luò)編程之HTTP應(yīng)用編程(上)
- 淺談C#網(wǎng)絡(luò)編程詳解篇
- 總結(jié)C#網(wǎng)絡(luò)編程中對于Cookie的設(shè)定要點(diǎn)
- C# Socket網(wǎng)絡(luò)編程實例
- C#網(wǎng)絡(luò)編程基礎(chǔ)之進(jìn)程和線程詳解
- c# socket網(wǎng)絡(luò)編程接收發(fā)送數(shù)據(jù)示例代碼
- C#開發(fā)之Socket網(wǎng)絡(luò)編程TCP/IP層次模型、端口及報文等探討
- C#網(wǎng)絡(luò)編程中常用特性介紹
相關(guān)文章
英雄聯(lián)盟輔助lol掛機(jī)不被踢的方法(lol掛機(jī)腳本)
lol掛機(jī)不會被踢,調(diào)用API設(shè)置鼠標(biāo)位置并模擬鼠標(biāo)右鍵讓人物走動2013-12-12