即时通信聊天工具的原理与设计
轉載:http://www.cnblogs.com/phquan/archive/2012/03/26/2417460.html
該軟件采用P2P方式,各個客戶端之間直接發消息進行會話聊天,服務器在其中只扮演協調者的角色(混合型P2P)。
1.會話流程設計
當一個新用戶通過自己的客戶端登陸系統后,從服務器獲取當前在線的用戶信息列表,列表信息包括了系統中每個用戶的地址。用戶就可以開始獨立工作,自主地向其他用戶發送消息,而不經過服務器。每當有新用戶加入或在線用戶退出時,服務器都會及時發消息通知系統中的所有其他用戶,以便它們實時地更新用戶信息列表。按照上述思路,設計系統會話流程如下:(1)用戶通過客戶端進入系統,向服務器發出消息,請求登陸。(2)服務器收到請求后,向客戶端返回應答消息,表示同意接受該用戶加入,并順帶將自己服務線程所在的監聽端口號告訴用戶。(3)客戶端按照服務器應答中給出的端口號與服務器建立穩定的連接。(4)服務器通過該連接將當前在線用戶的列表信息傳給新加入的客戶端。(5)客戶端獲得了在線用戶列表,就可以獨立自主地與在線的其他用戶通信了。(6)當用戶退出系統時要及時地通知服務器。2.用戶管理
系統中,無論是服務器還是客戶端都保存一份在線用戶列表,客戶端的用戶表在一開始登陸時從服務器索取獲得。在程序運行的過程中,服務器負責實時地將系統內用戶的變動情況及時地通知在線的每個成員用戶。新用戶登錄時,服務器將用戶表傳給他,同時向系統內每個成員廣播“login”消息,各成員收到后更新自己的用戶表。同樣,在有用戶退出系統時,服務器也會及時地將這一消息傳給各個用戶,當然這也就要求每個用戶在自己想要退出之前,必須要先告訴服務器。3.協議設計
3.1 客戶端與服務器會話
(1)登陸過程??蛻舳擞媚涿鸘DP向服務器發送消息:login,username,localIPEndPoint消息內容包括3個字段,各字段之間用“,”分隔:“login”表示請求登陸;“username”為用戶名;“localIPEndPoint”是客戶端本地地址。服務器收到后以匿名UDP返回如下消息:Accept,port其中,“Accept”表示服務器接受了請求;“port”是服務所在端口,服務線程在這個端口上監聽可能的客戶連接,該連接使用同步的TCP。連上服務器,獲取用戶列表:客戶端從上一會話的“port”字段的值服務所在端口,于是向端口發起TCP連接,向服務器索取在線的用戶列表,服務器接受連接后將用戶列別傳輸給客戶端。用戶列表格式如下:username1,IPEndPoint1;username2,IPEndPoint2;.....;endusername1,username2.....為用戶名,IPEndPoint1,IPEndPoint2....為它們對應的端點。每個用戶的信息都有個“用戶名+端點”組成,用戶信息之間以“;”隔開,整個用戶列表以“end”結尾。3.2 服務器協調管理用戶
(1)新用戶加入通知。由于系統中已存在的每個用戶都有一份當前用戶表,因此當有新成員加入時,服務器無需重復給系統中的每個成員再傳送用戶表,只要將新加入成員的信息告訴系統內的其他用戶,再由他們各自更新自己的用戶表就行了。服務器向系統內用戶廣播發送如下消息:端點字段寫為“remoteIPEndPoint”,表示是遠程某個用戶終端登陸了,本地客戶線程據此更新用戶列表。其實,在這個過程中,服務器只是將受到的“login”消息簡單地轉發而已。(2)用戶退出。與新成員加入時一樣,服務器將用戶退出的消息直接進行廣播轉發:logout,username,remoteIPEndPoint其中,“remoteIPEndPoint”為退出系統的遠程用戶終端的端點地址。3.3 用戶終端之間聊天
用戶聊天時,他們各自的客戶端之間是以P2P方式工作的,彼此地位對等,獨立,不與服務器發生直接聯系。聊天時發送的信息格式為:talk,longTime,selfUserName,message“talk”表明這是聊天內容;“longTime”是長時間格式的當前系統時間;“selfUserName”為自己的用戶名;“message”是聊天的內容。4.系統實現
4.1 服務線程
系統運行后,先有服務器啟動服務線程,只需單擊“啟動”按鈕即可。“啟動”按鈕的事件過程: //點擊開始事件處理函數private void buttonStart_Click(object sender, EventArgs e){//創建接收套接字serverIp = IPAddress.Parse(textBoxServerIp.Text);serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));receiveUdpClient = new UdpClient(serverIPEndPoint);//啟動接收線程Thread threadReceive = new Thread(ReceiveMessage);threadReceive.Start();buttonStart.Enabled = false;buttonStop.Enabled = true;//隨機指定監聽端口 N( P+1 ≤ N < 65536 )Random random = new Random();tcport = random.Next(port + 1, 65536);//創建監聽套接字myTcpListener = new TcpListener(serverIp, tcport);myTcpListener.Start();//啟動監聽線程Thread threadListen = new Thread(ListenClientConnect);threadListen.Start();AddItemToListBox(string.Format("服務線程({0})啟動,監聽端口{1}",serverIPEndPoint,tcport));}可以看到,服務器先后啟動了兩個線程:一個是接收線程threadReceive,它在一個實名UDP端口上,時刻準備著接收客戶端發來的會話消息;另一個是監聽線程threadListen,它在某個隨機指定的端口上監聽。
服務器接收線程關聯的ReceiveMessage()方法: //接收數據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]){//收到注冊關鍵字"login"case "login":User user = new User(splitString[1], clientIPEndPoint);userList.Add(user);AddItemToListBox(string.Format("用戶{0}({1})加入", user.GetName(), user.GetIPEndPoint()));string sendString = "Accept," + tcport.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;//收到關鍵字"logout"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));}接收線程執行該方法,進入while()循環,對每個收到的消息進行解析,根據消息頭是“login”或“logout”轉入相應的處理。
監聽線程對應ListenClientConnect()方法: //接受客戶端連接private void ListenClientConnect(){TcpClient newClient = null;while (true){try{//獲得用于傳遞數據的TCP套接口newClient = myTcpListener.AcceptTcpClient();AddItemToListBox(string.Format("接受客戶端{0}的 TCP 請求", newClient.Client.RemoteEndPoint));}catch{AddItemToListBox(string.Format("監聽線程({0}:{1})終止", serverIp, tcport));break;}//啟動發送用戶列表線程Thread threadSend = new Thread(SendData);threadSend.Start(newClient);}}當客戶端請求到達后,與之建立TCP連接,然后創建一個新的線程threadSend,他通過執行SendData()方法傳送用戶列表。
在服務器運行過程中,可隨時通過點擊“停止”按鈕關閉服務線程?!蓖V埂鞍粹o的事件過程: //當點擊關閉按鈕的事件處理程序private void buttonStop_Click(object sender, EventArgs e){myTcpListener.Stop();receiveUdpClient.Close();buttonStart.Enabled = true;buttonStop.Enabled = false;}這里myTcpListener是TCP監聽套接字,而receiveUdpClient是UDP套接字。當執行Stop()方法關閉監聽套接字時,myTcpListener.AcceptTcpClient()會產生異常。
4.2 登陸/注銷
(1) 用戶對象
為了便于服務器對全體用戶的管理,在服務器工程中添加自定義User類。代碼如下: using System; using System.Collections.Generic; using System.Linq; using System.Text;//添加的命名空間引用 using System.Net;namespace Server {//用戶信息類 蒲泓全(18/3/2012)class User{private string userName; //用戶名private IPEndPoint userIPEndPoint; //用戶地址 public User(string name, IPEndPoint ipEndPoint){userName = name;userIPEndPoint = ipEndPoint;}public string GetName(){return userName;}public IPEndPoint GetIPEndPoint(){return userIPEndPoint;}} }User類具有用戶名和端點地址兩個屬性,這也正是用戶列表中需要填寫的信息項。
(2) 用戶登錄功能
可以看到客戶端在登錄時也啟動了兩個線程,其中一個threadReceive是用實名UDP創建的接收線程,又稱為客戶線程,這是因為,它代表客戶端程序處理與服務器的會話消息。另一個線程threadSend則是臨時創建的,并以匿名UDP向服務器發出“login”消息。
登陸請求發出之后,客戶線程就循環執行ReceiveMessage()方法,以隨時接受和處理服務器的應答消息。
客戶線程關聯的ReceiveMessage()方法:
如下圖所示:為第一個用戶登陸系統時,從狀態監控屏幕上看到的客戶端與服務器程序的會話過程。
(3) 用戶注銷
圖3 注銷時雙方的會話
(3) 更新用戶列表
系統內的在線用戶收到服務器發來的消息后,實時地更新自己的用戶列表。當服務器發來“login”消息時,說明有新成員加入,客戶端執行下面的代碼: AddItemToListBox(string.Format("新用戶{0}({1})加入", splitString[1], splitString[2]));string userItemInfo = splitString[1] + "," + splitString[2];AddItemToListView(userItemInfo);break;若收到的是“logout”,則執行下面的代碼:
AddItemToListBox(string.Format("用戶{0}({1})退出", splitString[1], splitString[2])); RmvItemfromListView(splitString[1]);break;為了是程序簡單,客戶端并沒有使用特定的數據結構存儲用戶列表,而是直接將列表用ListView空間顯示在界面上,并用委托機制定義了兩個回調函數AddItemToListView()和RmvItemfromListView(),向空間中添加/刪除用戶信息。
4.3 及時聊天
帶有聊天談話內容的消息以“talk”為首部,采用點對點(P2P)方式發給對方?!皌alk”消息的發送,接收和顯示都由專門的聊天子窗口負責,當客戶端主程序收到“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;系統中的每個用戶都對應一個聊天子窗體對象,上段代碼的作用就是將一個“talk”消息定位到它的接受者的子窗體對象,再由該對象調用自身的ShwoTalkInfo()方法顯示聊天內容。
要打開對應某個用戶的子窗口,只需雙擊在線用戶列表中的該用戶項即可,代碼如下: //當點擊兩次發起回話的事件處理函數private void listViewOnline_DoubleClick(object sender, EventArgs e){ string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;if (peerName == textBoxUserName.Text){return;}string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;string[] splitString = ipendp.Split(':'); //除去':'IPAddress peerIp = IPAddress.Parse(splitString[0]);IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));ChatForm dlgChatForm = new ChatForm();dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);dlgChatForm.Text = peerName;chatFormList.Add(dlgChatForm);dlgChatForm.Show(); }其中,chatFormList是客戶端程序定義的數據結構,用于保存每一個在線用戶的子窗口列表,它與服務器端的userList結構是相對應的。每當用戶雙擊了列表中的某個用戶項時,程序就用該項的信息創建一個新的子窗體對象并添加到chatFormList表中。子窗體的初始化使用其自身的SetUserInfo()方法。
哈哈哈哈,現在整個通信聊天軟件就完成了,我們看看下面運行的效果。5.運行效果
同時運行一個服務器(Server)程序和三個客戶端(Client)程序,啟動服務線程,在三個客戶端分別以用戶名“泓全”,“愛田”,“愛盼”登陸服務器。如下圖所示:
圖4 登陸服務器
圖5 在線交談
6.源代碼
6.1 服務器端
6.2 客戶端
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms;//添加的命名空間引用 using System.Net; using System.Net.Sockets; using System.Threading; using System.IO;namespace Client {public partial class MainForm : Form{int port; //端口號 private UdpClient sendUdpClient; //匿名發送套接口private UdpClient receiveUdpClient; //實名接收套接口private IPEndPoint clientIPEndPoint; //客戶端地址private TcpClient myTcpClient; //TCP套接字private NetworkStream networkStream; //網絡流private BinaryReader br; //避免網絡邊界問題的讀數據流string userListString; //用戶名字串private List<ChatForm> chatFormList = new List<ChatForm>(); //用戶窗體列表public MainForm(){InitializeComponent();//本地IP和端口號的初始化IPAddress[] LocalIP = Dns.GetHostAddresses("");IPAddress address = IPAddress.Any;for (int i = 0; i < LocalIP.Length; i++){if (LocalIP[i].AddressFamily == AddressFamily.InterNetwork){address = LocalIP[i];break;}}textBoxServerIp.Text = address.ToString();textBoxLocalIp.Text = address.ToString();//獲得隨機端口號port = new Random().Next(1024, 65535);textBoxLocalPort.Text = port.ToString();//隨機生成用戶名Random r = new Random((int)DateTime.Now.Ticks); //類似于與C++中的種子textBoxUserName.Text = "user" + r.Next(100, 999);buttonLogout.Enabled = false;}private void buttonLogin_Click(object sender, EventArgs e){//創建接收套接字IPAddress clientIp = IPAddress.Parse(textBoxLocalIp.Text);clientIPEndPoint = new IPEndPoint(clientIp, int.Parse(textBoxLocalPort.Text));receiveUdpClient = new UdpClient(clientIPEndPoint);//啟動接收線程Thread threadReceive = new Thread(ReceiveMessage);threadReceive.Start();AddItemToListBox(string.Format("客戶線程({0})啟動", clientIPEndPoint));//匿名發送sendUdpClient = new UdpClient(0);//啟動發送線程Thread threadSend = new Thread(SendMessage);threadSend.Start(string.Format("login,{0},{1}",textBoxUserName.Text,clientIPEndPoint));AddItemToListBox(string.Format("發出:[login,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));buttonLogin.Enabled = false;buttonLogout.Enabled = true;this.Text = textBoxUserName.Text; //使當前窗體名字變為當前用戶名}//發送數據private void SendMessage(object obj){string message = (string)obj;byte[] sendbytes = Encoding.Unicode.GetBytes(message);//服務器端的IP和端口號IPAddress remoteIp = IPAddress.Parse(textBoxServerIp.Text);IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(textBoxServerPort.Text));sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint); //匿名發送sendUdpClient.Close();}//接收數據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(','); //除去','switch (splitString[0]){//若接收連接case "Accept":try{AddItemToListBox(string.Format("連接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));myTcpClient = new TcpClient();myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));if (myTcpClient != null){AddItemToListBox("連接成功!");networkStream = myTcpClient.GetStream();br = new BinaryReader(networkStream);}}catch{AddItemToListBox("連接失敗!");}Thread threadGetList = new Thread(GetUserList);threadGetList.Start(); //請求獲得用戶列表信息線程啟動break;//若收到注冊關鍵字"login",代表有新的用戶加入,并更新用戶列表case "login":AddItemToListBox(string.Format("新用戶{0}({1})加入", splitString[1], splitString[2]));string userItemInfo = splitString[1] + "," + splitString[2];AddItemToListView(userItemInfo);break;//若收到注冊關鍵字"logout",代表有用戶退出,并更新用戶列表case "logout":AddItemToListBox(string.Format("用戶{0}({1})退出", splitString[1], splitString[2]));RmvItemfromListView(splitString[1]);break;//若收到回話關鍵字"talk",則表明有用戶發起回話,并開始準備回話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;}}AddItemToListBox(string.Format("客戶線程({0})終止", clientIPEndPoint));}//獲得用戶列表private void GetUserList(){while (true){userListString = null;try{userListString = br.ReadString();if (userListString.EndsWith("end")){AddItemToListBox(string.Format("收到:[{0}]", userListString));string[] splitString = userListString.Split(';');for (int i = 0; i < splitString.Length - 1; i++){AddItemToListView(splitString[i]);}br.Close();myTcpClient.Close();break;}}catch{break;}}}//當點擊退出按鈕的事件處理函數private void buttonLogout_Click(object sender, EventArgs e){//匿名發送sendUdpClient = new UdpClient(0);//啟動發送線程Thread threadSend = new Thread(SendMessage);threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));AddItemToListBox(string.Format("發出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));receiveUdpClient.Close();listViewOnline.Items.Clear();buttonLogin.Enabled = true;buttonLogout.Enabled = false;this.Text = "Client"; //恢復到原來的名字}//當點擊兩次發起回話的事件處理函數private void listViewOnline_DoubleClick(object sender, EventArgs e){ string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;if (peerName == textBoxUserName.Text){return;}string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;string[] splitString = ipendp.Split(':'); //除去':'IPAddress peerIp = IPAddress.Parse(splitString[0]);IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));ChatForm dlgChatForm = new ChatForm();dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);dlgChatForm.Text = peerName;chatFormList.Add(dlgChatForm);dlgChatForm.Show(); }//利用委托機制顯示信息private delegate void AddItemToListBoxDelegate(string str);private void AddItemToListBox(string str){if (listBoxStatus.InvokeRequired){AddItemToListBoxDelegate d = AddItemToListBox;listBoxStatus.Invoke(d, str);}else{listBoxStatus.Items.Add(str);listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;listBoxStatus.ClearSelected();}}private delegate void AddItemToListViewDelegate(string str);private void AddItemToListView(string str){if (listViewOnline.InvokeRequired){AddItemToListViewDelegate d = AddItemToListView;listViewOnline.Invoke(d, str);}else{string[] splitString = str.Split(',');ListViewItem item = new ListViewItem();item.SubItems.Add(splitString[0]);item.SubItems.Add(splitString[1]);listViewOnline.Items.Add(item);}}private delegate void RmvItemfromListViewDelegate(string str);private void RmvItemfromListView(string str){if (listViewOnline.InvokeRequired){RmvItemfromListViewDelegate d = RmvItemfromListView;listViewOnline.Invoke(d, str);}else{for (int i = 0; i < listViewOnline.Items.Count; i++){if (listViewOnline.Items[i].SubItems[1].Text == str){listViewOnline.Items[i].Remove();}}}}} }6.3 聊天子窗口的代碼
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms;//添加的命名空間引用 using System.Net; using System.Net.Sockets; using System.Threading;namespace Client {public partial class ChatForm : Form{private string selfUserName; //自己的用戶名private string peerUserName; //對方的用戶名private IPEndPoint peerUserIPEndPoint; //對方的地址private UdpClient sendUdpClient; //匿名發送套接口public ChatForm(){InitializeComponent();}//類似于構造函數public void SetUserInfo(string selfName,string peerName,IPEndPoint peerIPEndPoint){ selfUserName = selfName;peerUserName = peerName;peerUserIPEndPoint = peerIPEndPoint;}//點擊發送按鈕的事件處理程序private void buttonSend_Click(object sender, EventArgs e){//匿名發送sendUdpClient = new UdpClient(0);//啟動發送線程Thread threadSend = new Thread(SendMessage);threadSend.Start(string.Format("talk,{0},{1},{2}", DateTime.Now.ToLongTimeString(), selfUserName, textBoxSend.Text)); richTextBoxTalkInfo.AppendText(selfUserName + "" + DateTime.Now.ToLongTimeString() + Environment.NewLine + textBoxSend.Text);richTextBoxTalkInfo.AppendText(Environment.NewLine);richTextBoxTalkInfo.ScrollToCaret(); textBoxSend.Text = "";textBoxSend.Focus();}//數據發送函數private void SendMessage(object obj){string message = (string)obj;byte[] sendbytes = Encoding.Unicode.GetBytes(message);sendUdpClient.Send(sendbytes, sendbytes.Length, peerUserIPEndPoint);sendUdpClient.Close();}//顯示通話內容public void ShowTalkInfo(string peerName, string time, string content){richTextBoxTalkInfo.AppendText(peerName + "" + time + Environment.NewLine + content);richTextBoxTalkInfo.AppendText(Environment.NewLine);richTextBoxTalkInfo.ScrollToCaret();}//當點擊關閉時的事件處理程序private void buttonClose_Click(object sender, EventArgs e){this.Close();}} }總結
以上是生活随笔為你收集整理的即时通信聊天工具的原理与设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 毕业设计——宠物店管理系统
- 下一篇: 表单美化+html+css