C#網(wǎng)絡(luò)編程系列九:類(lèi)似QQ的即時(shí)通信程序
引言:前面專(zhuān)題中介紹了UDP、TCP和P2P編程,并且通過(guò)一些小的示例來(lái)讓大家更好的理解它們的工作原理以及怎樣.Net類(lèi)庫(kù)去實(shí)現(xiàn)它們的。為了讓大家更好的理解我們平常中常見(jiàn)的軟件QQ的工作原理,所以在本專(zhuān)題中將利用前面專(zhuān)題介紹的知識(shí)來(lái)實(shí)現(xiàn)一個(gè)類(lèi)似QQ的聊天程序。
一、即時(shí)通信系統(tǒng)
在我們的生活中經(jīng)常使用即時(shí)通信的軟件,我們經(jīng)常接觸到的有:QQ、阿里旺旺、MSN等等。這些都是屬于即時(shí)通信(Instant Messenger,IM)軟件,IM是指所有能夠即時(shí)發(fā)送和接收互聯(lián)網(wǎng)消息的軟件。
在前面專(zhuān)題P2P編程中介紹過(guò)P2P系統(tǒng)分兩種類(lèi)型——單純型P2P和混合型P2P(QQ就是屬于混合型的應(yīng)用),混合型P2P系統(tǒng)中的服務(wù)器(也叫索引服務(wù)器)起到協(xié)調(diào)的作用。在文件共享類(lèi)應(yīng)用中,如果采用混合型P2P技術(shù)的話,索引服務(wù)器就保存著文件信息,這樣就可能會(huì)造成版權(quán)的問(wèn)題,然而在即時(shí)通信類(lèi)的軟件中, 因?yàn)榭蛻舳藗鬟f的都是簡(jiǎn)單的聊天文本而不是網(wǎng)絡(luò)媒體資源,這樣就不存在版權(quán)問(wèn)題了,在這種情況下,就可以采用混合型P2P技術(shù)來(lái)實(shí)現(xiàn)我們的即時(shí)通信軟件。前面已經(jīng)講了,騰訊的QQ就是屬于混合型P2P的軟件。
因此本專(zhuān)題要實(shí)現(xiàn)一個(gè)類(lèi)似QQ的聊天程序,其中用到的P2P技術(shù)是屬于混合型P2P,而不是前一專(zhuān)題中的采用的單純型P2P技術(shù),同時(shí)本程序的實(shí)現(xiàn)也會(huì)用到TCP、UDP編程技術(shù)。具體的相關(guān)內(nèi)容大家可以查看本系列的相關(guān)專(zhuān)題的。
二、程序?qū)崿F(xiàn)的詳細(xì)設(shè)計(jì)
本程序采用P2P方式,各個(gè)客戶端之間直接發(fā)消息進(jìn)行聊天,服務(wù)器在其中只是起到協(xié)調(diào)的作用,下面先理清下程序的流程:
2.1 程序流程設(shè)計(jì)
當(dāng)一個(gè)新用戶通過(guò)客戶端登陸系統(tǒng)后,從服務(wù)器獲取當(dāng)在線的用戶信息列表,列表信息包括系統(tǒng)中每個(gè)用戶的地址,然后用戶就可以單獨(dú)向其他發(fā)消息。如果有用戶加入或者在線用戶退出時(shí),服務(wù)器就會(huì)及時(shí)發(fā)消息通知系統(tǒng)中的所有其他客戶端,達(dá)到它們即時(shí)地更新用戶信息列表。
根據(jù)上面大致的描述,我們可以把系統(tǒng)的流程分為下面幾步來(lái)更好的理解(大家可以參考QQ程序?qū)?huì)更好的理解本程序的流程):
1.用戶通過(guò)客戶端進(jìn)入系統(tǒng),向服務(wù)器發(fā)出消息,請(qǐng)求登陸
2.服務(wù)器收到請(qǐng)求后,向客戶端返回回應(yīng)消息,表示同意接受該用戶加入,并把自己(指的是服務(wù)器)所在監(jiān)聽(tīng)的端口發(fā)送給客戶端
3.客戶端根據(jù)服務(wù)器發(fā)送過(guò)來(lái)的端口號(hào)和服務(wù)器建立連接
4.服務(wù)器通過(guò)該連接 把在線用戶的列表信息發(fā)送給新加入的客戶端。
5.客戶端獲得了在線用戶列表后就可以自己選擇在線用戶聊天。(程序中另外設(shè)計(jì)一個(gè)類(lèi)似QQ的聊天窗口來(lái)進(jìn)行聊天)
6.當(dāng)用戶退出系統(tǒng)時(shí)也要及時(shí)通知服務(wù)器,服務(wù)器再把這個(gè)消息轉(zhuǎn)發(fā)給每個(gè)在線的用戶,使客戶端及時(shí)更新本地的用戶信息列表。
2.2 通信協(xié)議設(shè)計(jì)
所謂協(xié)議就是約定,即服務(wù)器和客戶端之間會(huì)話信息的內(nèi)容格式進(jìn)行約定,使雙方都可以識(shí)別,達(dá)到更好的通信。
下面就具體介紹下協(xié)議的設(shè)計(jì):
1. 客戶端和服務(wù)器之間的對(duì)話
(1)登陸過(guò)程
① 客戶端用匿名UDP的方式向服務(wù)器發(fā)出下面的信息:
login, username, localIPEndPoint |
消息內(nèi)容包括三個(gè)字段,每個(gè)字段用 “,”分割,login表示的是請(qǐng)求登陸;username表示用戶名;localIPEndPint表示客戶端本地地址。
② 服務(wù)器收到后以匿名UDP返回下面的回應(yīng):
Accept, port |
其中Accept表示服務(wù)器接受請(qǐng)求,port表示服務(wù)器所在的端口號(hào),服務(wù)器監(jiān)聽(tīng)著這個(gè)端口的客戶端連接
③ 連接服務(wù)器,獲取用戶列表
客戶端從上一步獲得了端口號(hào),然后向該端口發(fā)起TCP連接,向服務(wù)器索取在線用戶列表,服務(wù)器接受連接后將用戶列表傳輸?shù)娇蛻舳恕S脩袅斜硇畔⒏袷饺缦拢?/p>
username1,IPEndPoint1;username2,IPEndPoint2;...;end |
username1、username2表示用戶名,IPEndPoint1,IPEndPoint2表示對(duì)應(yīng)的端點(diǎn),每個(gè)用戶信息都是由"用戶名+端點(diǎn)"組成,用戶信息以“;”隔開(kāi),整個(gè)用戶列表以“end”結(jié)尾。
(2)注銷(xiāo)過(guò)程
用戶退出時(shí),向服務(wù)器發(fā)送如下消息:
logout,username,localIPEndPoint |
這條消息看字面意思大家都知道就是告訴服務(wù)器 username+localIPEndPoint這個(gè)用戶要退出了。
2. 服務(wù)器管理用戶
(1)新用戶加入通知
因?yàn)橄到y(tǒng)中在線的每個(gè)用戶都有一份當(dāng)前在線用戶表,因此當(dāng)有新用戶登錄時(shí),服務(wù)器不需要重復(fù)地給系統(tǒng)中的每個(gè)用戶再發(fā)送所有用戶信息,只需要將新加入用戶的信息通知其他用戶,其他用戶再更新自己的用戶列表。
服務(wù)器向系統(tǒng)中每個(gè)用戶廣播如下信息:
login,username,remoteIPEndPoint |
在這個(gè)過(guò)程中服務(wù)器只是負(fù)責(zé)將收到的"login"信息轉(zhuǎn)發(fā)出去。
(2)用戶退出
與新用戶加入一樣,服務(wù)器將用戶退出的消息進(jìn)行廣播轉(zhuǎn)發(fā):
logout,username,remoteIPEndPoint |
3. 客戶端之間聊天
用戶進(jìn)行聊天時(shí),各自的客戶端之間是以P2P方式進(jìn)行工作的,不與服務(wù)器有直接聯(lián)系,這也是P2P技術(shù)的特點(diǎn)。
聊天發(fā)送的消息格式如下:
talk, longtime, selfUserName, message |
其中,talk表明這是聊天內(nèi)容的消息;longtime是長(zhǎng)時(shí)間格式的當(dāng)前系統(tǒng)時(shí)間;selfUserName為發(fā)送發(fā)的用戶名;message表示消息的內(nèi)容。
協(xié)議設(shè)計(jì)介紹完后,下面就進(jìn)入本程序的具體實(shí)現(xiàn)的介紹的。
注:協(xié)議是本程序的核心,也是所有軟件的核心,每個(gè)軟件產(chǎn)品的協(xié)議都是不一樣的,QQ有自己的一套協(xié)議,MSN又有另一套協(xié)議,所以使用的QQ的用戶無(wú)法和用MSN的朋友進(jìn)行聊天。
#p#
三、程序的實(shí)現(xiàn)
服務(wù)器端核心代碼:
- View Code
- // 啟動(dòng)服務(wù)器
- // 根據(jù)博客中協(xié)議的設(shè)計(jì)部分
- // 客戶端先向服務(wù)器發(fā)送登錄請(qǐng)求,然后通過(guò)服務(wù)器返回的端口號(hào)
- // 再與服務(wù)器建立連接
- // 所以啟動(dòng)服務(wù)按鈕事件中有兩個(gè)套接字:一個(gè)是接收客戶端信息套接字和
- // 監(jiān)聽(tīng)客戶端連接套接字
- 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);
- // 啟動(dòng)接收線程
- Thread receiveThread = new Thread(ReceiveMessage);
- receiveThread.Start();
- btnStart.Enabled = false;
- btnStop.Enabled = true;
- // 隨機(jī)指定監(jiān)聽(tīng)端口
- Random random = new Random();
- tcpPort = random.Next(port + 1, 65536);
- // 創(chuàng)建監(jiān)聽(tīng)套接字
- tcpListener = new TcpListener(serverIp, tcpPort);
- tcpListener.Start();
- // 啟動(dòng)監(jiān)聽(tīng)線程
- Thread listenThread = new Thread(ListenClientConnect);
- listenThread.Start();
- AddItemToListBox(string.Format("服務(wù)器線程{0}啟動(dòng),監(jiān)聽(tīng)端口{1}",serverIPEndPoint,tcpPort));
- }
- // 接收客戶端發(fā)來(lái)的信息
- private void ReceiveMessage()
- {
- IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
- while (true)
- {
- try
- {
- // 關(guān)閉receiveUdpClient時(shí)下面一行代碼會(huì)產(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è)計(jì)部分,從客戶端發(fā)送來(lái)的消息是具有一定格式的
- // 服務(wù)器接收消息后要對(duì)消息做處理
- 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++)
- {
- // 廣播注銷(xiāo)消息
- 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請(qǐng)求",newClient.Client.RemoteEndPoint));
- }
- catch
- {
- AddItemToListBox(string.Format("監(jiān)聽(tīng)線程({0}:{1})", serverIp, tcpPort));
- break;
- }
- Thread sendThread = new Thread(SendData);
- sendThread.Start(newClient);
- }
- }
- // 向客戶端發(fā)送在線用戶列表信息
- // 服務(wù)器通過(guò)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();
- }
客戶端核心代碼:
- View Code
- // 登錄服務(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);
- // 啟動(dòng)接收線程
- Thread receiveThread = new Thread(ReceiveMessage);
- receiveThread.Start();
- // 匿名發(fā)送
- sendUdpClient = new UdpClient(0);
- // 啟動(dòng)發(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時(shí)會(huì)產(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ā)送登錄請(qǐng)求
- 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)擊“啟動(dòng)”按鈕來(lái)啟動(dòng)服務(wù)器,然后客戶端首先指定服務(wù)器的端口號(hào),修改用戶名(這里也可以不修改,使用默認(rèn)的也可以),然后點(diǎn)擊“登錄”按鈕來(lái)登陸服務(wù)器(也就是告訴服務(wù)器本地的客戶端地址),然后從服務(wù)器端獲得在線用戶列表,界面演示如下:
然后用戶可以雙擊在線用戶進(jìn)行聊天(此程序支持與多人進(jìn)行聊天),下面是功能的演示圖片:
雙方進(jìn)行聊天時(shí),這里沒(méi)有實(shí)現(xiàn)像QQ一樣,有人發(fā)信息來(lái)在對(duì)應(yīng)的客戶端就有消息提醒的功能的, 所以雙方進(jìn)行聊天的過(guò)程中,每個(gè)客戶端都需要在在線用戶列表中點(diǎn)擊聊天的對(duì)象來(lái)激活聊天對(duì)話框(意思就是從圖片中可以看出“天涯”客戶端想和劍癡聊天的話,就在“在線用戶”列表雙擊劍癡來(lái)激活聊天窗口,同時(shí)“劍癡”客戶端也必須雙擊“天涯”來(lái)激活聊天窗口,這樣雙方就看到對(duì)方發(fā)來(lái)的信息了,(不激活窗口,也是發(fā)送了信息,只是沒(méi)有一個(gè)窗口來(lái)進(jìn)行顯示)),而且從圖片中也可以看出——此程序支持與多人聊天,即天涯同時(shí)與“劍癡”和"大地"同時(shí)聊天。
四、總結(jié)
本專(zhuān)題介紹了如何去實(shí)現(xiàn)一個(gè)類(lèi)似QQ的聊天程序,一方面讓大家可以鞏固前面專(zhuān)題的內(nèi)容,另一方面讓大家更好的理解即時(shí)通信軟件(騰訊QQ)的工作原理和軟件協(xié)議的設(shè)計(jì)。
后面一專(zhuān)題將介紹如何去實(shí)現(xiàn)郵件系統(tǒng)中常用的功能——實(shí)現(xiàn)一個(gè)簡(jiǎn)單的郵件應(yīng)用。
本程序的源代碼鏈接:http://files.cnblogs.com/zhili/IM.zip
原文鏈接:http://www.cnblogs.com/zhili/archive/2012/09/23/QQ_P2P.html
【編輯推薦】
- C#網(wǎng)絡(luò)編程系列一:網(wǎng)絡(luò)協(xié)議簡(jiǎn)介
- C#網(wǎng)絡(luò)編程系列二:HTTP協(xié)議詳解
- C#網(wǎng)絡(luò)編程系列三:自定義Web服務(wù)器
- C#網(wǎng)絡(luò)編程系列四:自定義Web瀏覽器
- C#網(wǎng)絡(luò)編程系列五:TCP編程
- C#網(wǎng)絡(luò)編程系列六:UDP編程
- C#網(wǎng)絡(luò)編程系列七:UDP編程補(bǔ)充
- C#網(wǎng)絡(luò)編程系列八:P2P編程
- C#網(wǎng)絡(luò)編程系列十:實(shí)現(xiàn)簡(jiǎn)單的郵件收發(fā)器