即時通信聊天工具的原理與設計


       該軟件采用P2P方式,各個客戶端之間直接發消息進行會話聊天,服務器在其中只扮演協調者的角色(混合型P2P)。

1.會話流程設計

       當一個新用戶通過自己的客戶端登陸系統后,從服務器獲取當前在線的用戶信息列表,列表信息包括了系統中每個用戶的地址。用戶就可以開始獨立工作,自主地向其他用戶發送消息,而不經過服務器。每當有新用戶加入或在線用戶退出時,服務器都會及時發消息通知系統中的所有其他用戶,以便它們實時地更新用戶信息列表。

      按照上述思路,設計系統會話流程如下:

      (1)用戶通過客戶端進入系統,向服務器發出消息,請求登陸。

      (2)服務器收到請求后,向客戶端返回應答消息,表示同意接受該用戶加入,並順帶將自己服務線程所在的監聽端口號告訴用戶。

      (3)客戶端按照服務器應答中給出的端口號與服務器建立穩定的連接。

      (4)服務器通過該連接將當前在線用戶的列表信息傳給新加入的客戶端。

      (5)客戶端獲得了在線用戶列表,就可以獨立自主地與在線的其他用戶通信了。

      (6)當用戶退出系統時要及時地通知服務器。

2.用戶管理

    系統中,無論是服務器還是客戶端都保存一份在線用戶列表,客戶端的用戶表在一開始登陸時從服務器索取獲得。在程序運行的過程中,服務器負責實時地將系統內用戶的變動情況及時地通知在線的每個成員用戶。

    新用戶登錄時,服務器將用戶表傳給他,同時向系統內每個成員廣播“login”消息,各成員收到后更新自己的用戶表。

    同樣,在有用戶退出系統時,服務器也會及時地將這一消息傳給各個用戶,當然這也就要求每個用戶在自己想要退出之前,必須要先告訴服務器。

3.協議設計

3.1 客戶端與服務器會話

    (1)登陸過程。

      客戶端用匿名UDP向服務器發送消息:

      login,username,localIPEndPoint

      消息內容包括3個字段,各字段之間用“,”分隔:“login”表示請求登陸;“username”為用戶名;“localIPEndPoint”是客戶端本地地址。

      服務器收到后以匿名UDP返回如下消息:

      Accept,port

      其中,“Accept”表示服務器接受了請求;“port”是服務所在端口,服務線程在這個端口上監聽可能的客戶連接,該連接使用同步的TCP。

      連上服務器,獲取用戶列表:

      客戶端從上一會話的“port”字段的值服務所在端口,於是向端口發起TCP連接,向服務器索取在線的用戶列表,服務器接受連接后將用戶列別傳輸給客戶端。

      用戶列表格式如下:

      username1,IPEndPoint1;username2,IPEndPoint2;.....;end

      username1,username2.....為用戶名,IPEndPoint1,IPEndPoint2....為它們對應的端點。每個用戶的信息都有個“用戶名+端點”組成,用戶信息之間以“;”隔開,整個用戶列表以“end”結尾。

3.1 服務器協調管理用戶

    (1)新用戶加入通知。

      由於系統中已存在的每個用戶都有一份當前用戶表,因此當有新成員加入時,服務器無需重復給系統中的每個成員再傳送用戶表,只要將新加入成員的信息告訴系統內的其他用戶,再由他們各自更新自己的用戶表就行了。

      服務器向系統內用戶廣播發送如下消息:

      端點字段寫為“remoteIPEndPoint”,表示是遠程某個用戶終端登陸了,本地客戶線程據此更新用戶列表。其實,在這個過程中,服務器只是將受到的“login”消息簡單地轉發而已。

     (2)用戶退出。

      與新成員加入時一樣,服務器將用戶退出的消息直接進行廣播轉發:

      logout,username,remoteIPEndPoint

      其中,“remoteIPEndPoint”為退出系統的遠程用戶終端的端點地址。

3.1 用戶終端之間聊天

      用戶聊天時,他們各自的客戶端之間是以P2P方式工作的,彼此地位對等,獨立,不與服務器發生直接聯系。

      聊天時發送的信息格式為:

      talk,longTime,selfUserName,message

      “talk”表明這是聊天內容;“longTime”是長時間格式的當前系統時間;“selfUserName”為自己的用戶名;“message”是聊天的內容。

4.系統實現

4.1 服務線程

    系統運行后,先有服務器啟動服務線程,只需單擊“啟動”按鈕即可。

    “啟動”按鈕的事件過程:

 1  //點擊開始事件處理函數
2 private void buttonStart_Click(object sender, EventArgs e)
3 {
4 //創建接收套接字
5 serverIp = IPAddress.Parse(textBoxServerIp.Text);
6 serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));
7 receiveUdpClient = new UdpClient(serverIPEndPoint);
8
9 //啟動接收線程
10 Thread threadReceive = new Thread(ReceiveMessage);
11 threadReceive.Start();
12 buttonStart.Enabled = false;
13 buttonStop.Enabled = true;
14
15 //隨機指定監聽端口 N( P+1 ≤ N < 65536 )
16 Random random = new Random();
17 tcport = random.Next(port + 1, 65536);
18
19 //創建監聽套接字
20 myTcpListener = new TcpListener(serverIp, tcport);
21 myTcpListener.Start();
22
23 //啟動監聽線程
24 Thread threadListen = new Thread(ListenClientConnect);
25 threadListen.Start();
26 AddItemToListBox(string.Format("服務線程({0})啟動,監聽端口{1}",serverIPEndPoint,tcport));
27 }

      可以看到,服務器先后啟動了兩個線程:一個是接收線程threadReceive,它在一個實名UDP端口上,時刻准備着接收客戶端發來的會話消息;另一個是監聽線程threadListen,它在某個隨機指定的端口上監聽。

      服務器接收線程關聯的ReceiveMessage()方法:

View Code
 1   //接收數據
2 private void ReceiveMessage()
3 {
4 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
5 while (true)
6 {
7 try
8 {
9 //關閉receiveUdpClient時此句會產生異常
10 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
11 string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
12
13 //顯示消息內容
14 AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));
15
16 //處理消息數據
17 string[] splitString = message.Split(',');
18
19 //解析用戶端地址
20 string[] splitSubString = splitString[2].Split(':'); //除去':'
21 IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitSubString[0]), int.Parse(splitSubString[1]));
22 switch (splitString[0])
23 {
24 //收到注冊關鍵字"login"
25 case "login":
26 User user = new User(splitString[1], clientIPEndPoint);
27 userList.Add(user);
28 AddItemToListBox(string.Format("用戶{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
29 string sendString = "Accept," + tcport.ToString();
30 SendtoClient(user, sendString); //向該用戶發送同意關鍵字
31 AddItemToListBox(string.Format("向{0}({1})發出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
32 for (int i = 0; i < userList.Count; i++)
33 {
34 if (userList[i].GetName() != user.GetName())
35 {
36 //向除剛加入的所有用戶發送更新消息
37 SendtoClient(userList[i], message);
38 }
39 }
40 AddItemToListBox(string.Format("廣播:[{0}]", message));
41 break;
42
43 //收到關鍵字"logout"
44 case "logout":
45 for (int i = 0; i < userList.Count; i++)
46 {
47 if (userList[i].GetName() == splitString[1])
48 {
49 AddItemToListBox(string.Format("用戶{0}({1})退出", userList[i].GetName(), userList[i].GetIPEndPoint()));
50 userList.RemoveAt(i);
51 }
52 }
53
54 //向所用用戶發送更新消息
55 for (int i = 0; i < userList.Count; i++)
56 {
57 SendtoClient(userList[i], message);
58 }
59 AddItemToListBox(string.Format("廣播:[{0}]", message));
60 break;
61 }
62 }
63 catch
64 {
65 break;
66 }
67 }
68 AddItemToListBox(string.Format("服務線程({0})終止", serverIPEndPoint));
69 }

     接收線程執行該方法,進入while()循環,對每個收到的消息進行解析,根據消息頭是“login”或“logout”轉入相應的處理。

     監聽線程對應ListenClientConnect()方法:

 1 //接受客戶端連接
2 private void ListenClientConnect()
3 {
4 TcpClient newClient = null;
5 while (true)
6 {
7 try
8 {
9 //獲得用於傳遞數據的TCP套接口
10 newClient = myTcpListener.AcceptTcpClient();
11 AddItemToListBox(string.Format("接受客戶端{0}的 TCP 請求", newClient.Client.RemoteEndPoint));
12 }
13 catch
14 {
15 AddItemToListBox(string.Format("監聽線程({0}:{1})終止", serverIp, tcport));
16 break;
17 }
18
19 //啟動發送用戶列表線程
20 Thread threadSend = new Thread(SendData);
21 threadSend.Start(newClient);
22 }
23 }

      當客戶端請求到達后,與之建立TCP連接,然后創建一個新的線程threadSend,他通過執行SendData()方法傳送用戶列表。

      在服務器運行過程中,可隨時通過點擊“停止”按鈕關閉服務線程。

      ”停止“按鈕的事件過程:

1    //當點擊關閉按鈕的事件處理程序
2 private void buttonStop_Click(object sender, EventArgs e)
3 {
4 myTcpListener.Stop();
5 receiveUdpClient.Close();
6 buttonStart.Enabled = true;
7 buttonStop.Enabled = false;
8 }

     這里myTcpListener是TCP監聽套接字,而receiveUdpClient是UDP套接字。當執行Stop()方法關閉監聽套接字時,myTcpListener.AcceptTcpClient()會產生異常。

     運行服務器,先后單擊”啟動“和”停止“按鈕,狀態監控屏上就顯示出服務線程的工作狀態,如下圖所示。

                                

                                                        圖1 服務線程的啟動/停止狀態

4.2 登陸/注銷

 (1) 用戶對象

     為了便於服務器對全體用戶的管理,在服務器工程中添加自定義User類。代碼如下:

 1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 //添加的命名空間引用
7 using System.Net;
8
9 namespace Server
10 {
11 //用戶信息類 蒲泓全(18/3/2012)
12 class User
13 {
14 private string userName; //用戶名
15 private IPEndPoint userIPEndPoint; //用戶地址
16 public User(string name, IPEndPoint ipEndPoint)
17 {
18 userName = name;
19 userIPEndPoint = ipEndPoint;
20 }
21 public string GetName()
22 {
23 return userName;
24 }
25 public IPEndPoint GetIPEndPoint()
26 {
27 return userIPEndPoint;
28 }
29 }
30 }

      User類具有用戶名和端點地址兩個屬性,這也正是用戶列表中需要填寫的信息項。
(2) 用戶登錄功能

      當服務器的兩個服務線程運行起來之后,各用戶就可以通過客戶端程序登錄到系統了。用戶在客戶端上單擊“登錄”按鈕后,客戶端就向服務器發出“login”請求。

     “登錄”按鈕的事件過程為:

View Code
  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; //使當前窗體名字變為當前用戶名
}

     可以看到客戶端在登錄時也啟動了兩個線程,其中一個threadReceive是用實名UDP創建的接收線程,又稱為客戶線程,這是因為,它代表客戶端程序處理與服務器的會話消息。另一個線程threadSend則是臨時創建的,並以匿名UDP向服務器發出“login”消息。

     登陸請求發出之后,客戶線程就循環執行ReceiveMessage()方法,以隨時接受和處理服務器的應答消息。

     客戶線程關聯的ReceiveMessage()方法:

View Code
 1  //接收數據
2 private void ReceiveMessage()
3 {
4 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
5 while (true)
6 {
7 try
8 {
9 //關閉receiveUdpClient時此句會產生異常
10 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
11 string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
12
13 //顯示消息內容
14 AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));
15
16 //處理消息數據
17 string[] splitString = message.Split(','); //除去','
18 switch (splitString[0])
19 {
20 //若接收連接
21 case "Accept":
22 try
23 {
24 AddItemToListBox(string.Format("連接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));
25 myTcpClient = new TcpClient();
26 myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));
27 if (myTcpClient != null)
28 {
29 AddItemToListBox("連接成功!");
30 networkStream = myTcpClient.GetStream();
31 br = new BinaryReader(networkStream);
32 }
33 }
34 catch
35 {
36 AddItemToListBox("連接失敗!");
37 }
38 Thread threadGetList = new Thread(GetUserList);
39 threadGetList.Start(); //請求獲得用戶列表信息線程啟動
40 break;
41
42 //若收到注冊關鍵字"login",代表有新的用戶加入,並更新用戶列表
43 case "login":
44 AddItemToListBox(string.Format("新用戶{0}({1})加入", splitString[1], splitString[2]));
45 string userItemInfo = splitString[1] + "," + splitString[2];
46 AddItemToListView(userItemInfo);
47 break;
48
49 //若收到注冊關鍵字"logout",代表有用戶退出,並更新用戶列表
50 case "logout":
51 AddItemToListBox(string.Format("用戶{0}({1})退出", splitString[1], splitString[2]));
52 RmvItemfromListView(splitString[1]);
53 break;
54
55 //若收到回話關鍵字"talk",則表明有用戶發起回話,並開始准備回話
56 case "talk":
57 for (int i = 0; i < chatFormList.Count; i++)
58 {
59 if (chatFormList[i].Text == splitString[2])
60 {
61 chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]);
62 }
63 }
64 break;
65 }
66 }
67 catch
68 {
69 break;
70 }
71 }
72 AddItemToListBox(string.Format("客戶線程({0})終止", clientIPEndPoint));
73 }

    如下圖所示:為第一個用戶登陸系統時,從狀態監控屏幕上看到的客戶端與服務器程序的會話過程。

                     

                                                                               圖2 登陸過程中雙方的會話

(3) 用戶注銷

    當用用戶需要下線退出時,單擊客戶端界面上的“注銷”按鈕。

    “注銷”按鈕的過程代碼:

 1    //當點擊退出按鈕的事件處理函數
2 private void buttonLogout_Click(object sender, EventArgs e)
3 {
4 //匿名發送
5 sendUdpClient = new UdpClient(0);
6
7 //啟動發送線程
8 Thread threadSend = new Thread(SendMessage);
9 threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));
10 AddItemToListBox(string.Format("發出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
11 receiveUdpClient.Close();
12 listViewOnline.Items.Clear();
13 buttonLogin.Enabled = true;
14 buttonLogout.Enabled = false;
15 this.Text = "Client"; //恢復到原來的名字
16 }

     注銷操作很簡單,凡要注銷的用戶只需向服務器發出“logou”消息,告知服務器就可以了,最好還要關閉客戶端自身的UDP套接字。

     服務器在收到“logout”消息后,執行下面代碼:

View Code
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));

     服務程序在自己維護的User對象列表中刪除這個用戶,並且將這個消息廣播給所有的用戶。

     在這個過程中,退出的客戶端與服務器的會話記錄如下圖所示:

                               

                                                                          圖3 注銷時雙方的會話

(3) 更新用戶列表

     系統內的在線用戶收到服務器發來的消息后,實時地更新自己的用戶列表。當服務器發來“login”消息時,說明有新成員加入,客戶端執行下面的代碼:

1  AddItemToListBox(string.Format("新用戶{0}({1})加入", splitString[1], splitString[2]));
2 string userItemInfo = splitString[1] + "," + splitString[2];
3 AddItemToListView(userItemInfo);
4 break;

     若收到的是“logout”,則執行下面的代碼:

1 AddItemToListBox(string.Format("用戶{0}({1})退出", splitString[1], splitString[2]));
2 RmvItemfromListView(splitString[1]);
3 break;

    為了是程序簡單,客戶端並沒有使用特定的數據結構存儲用戶列表,而是直接將列表用ListView空間顯示在界面上,並用委托機制定義了兩個回調函數AddItemToListView()和RmvItemfromListView(),向空間中添加/刪除用戶信息。

 4.3 及時聊天

    帶有聊天談話內容的消息以“talk”為首部,采用點對點(P2P)方式發給對方。“talk”消息的發送,接收和顯示都由專門的聊天子窗口負責,當客戶端主程序收到“talk”的消息時,執行下面的代碼:

1 for (int i = 0; i < chatFormList.Count; i++)
2 {
3 if (chatFormList[i].Text == splitString[2])
4 {
5 chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]);
6 }
7 }
8 break;

    系統中的每個用戶都對應一個聊天子窗體對象,上段代碼的作用就是將一個“talk”消息定位到它的接受者的子窗體對象,再由該對象調用自身的ShwoTalkInfo()方法顯示聊天內容。

    要打開對應某個用戶的子窗口,只需雙擊在線用戶列表中的該用戶項即可,代碼如下:

View Code
 1  //當點擊兩次發起回話的事件處理函數
2 private void listViewOnline_DoubleClick(object sender, EventArgs e)
3 {
4 string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;
5 if (peerName == textBoxUserName.Text)
6 {
7 return;
8 }
9 string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;
10 string[] splitString = ipendp.Split(':'); //除去':'
11 IPAddress peerIp = IPAddress.Parse(splitString[0]);
12 IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));
13 ChatForm dlgChatForm = new ChatForm();
14 dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);
15 dlgChatForm.Text = peerName;
16 chatFormList.Add(dlgChatForm);
17 dlgChatForm.Show();
18 }

    其中,chatFormList是客戶端程序定義的數據結構,用於保存每一個在線用戶的子窗口列表,它與服務器端的userList結構是相對應的。每當用戶雙擊了列表中的某個用戶項時,程序就用該項的信息創建一個新的子窗體對象並添加到chatFormList表中。子窗體的初始化使用其自身的SetUserInfo()方法。

    哈哈哈哈,現在整個通信聊天軟件就完成了,我們看看下面運行的效果。

5. 運行效果

   同時運行一個服務器(Server)程序和三個客戶端(Client)程序,啟動服務線程,在三個客戶端分別以用戶名“泓全”,“愛田”,“愛盼”登陸服務器。如下圖所示:

                 

                                                                                    圖4 登陸服務器

      此時,每個在線用戶兩兩之間就都可以即時聊天了。不過,在聊天之前,聊天雙方要先打開對方的子窗口,操作方法很簡單,只要在客戶端界面上雙擊“在線”列表中相應的用戶項即可。如下圖所示:

                                    

                                                                                          圖5 在線交談

6. 源代碼

6.1 服務器端

View Code
  1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Text;
8 using System.Windows.Forms;
9
10 //添加的命名空間引用
11 using System.Net;
12 using System.Net.Sockets;
13 using System.Threading;
14 using System.IO;
15
16 namespace Server
17 {
18 public partial class MainForm : Form
19 {
20 private List<User> userList = new List<User>(); //保存登錄的所有用戶
21 int port; //服務端口
22 int tcport; //監聽端口
23 private UdpClient sendUdpClient; //匿名發送套接口
24 private UdpClient receiveUdpClient; //實名接收套接口
25 private IPEndPoint serverIPEndPoint; //服務器地址
26 private TcpListener myTcpListener; //服務器監聽套接口
27 private IPAddress serverIp; //服務器IP
28 private NetworkStream networkStream; //網絡流
29 private BinaryWriter bw; //避免出現網絡邊界問題的寫入流
30 string userListString; //用戶列表串
31 public MainForm()
32 {
33 InitializeComponent();
34
35 //服務器IP
36 IPAddress[] ServerIP = Dns.GetHostAddresses("");
37 IPAddress address = IPAddress.Any;
38 for (int i = 0; i < ServerIP.Length; i++ )
39 {
40 if (ServerIP[i].AddressFamily == AddressFamily.InterNetwork)
41 {
42 address = ServerIP[i];
43 break;
44 }
45 }
46 textBoxServerIp.Text = address.ToString();
47
48 //隨機選擇服務端口 Port( Port > 1024 )
49 port = new Random().Next(1024, 65535);
50 textBoxServerPort.Text = port.ToString();
51 buttonStop.Enabled = false;
52 }
53
54 //點擊開始事件處理函數
55 private void buttonStart_Click(object sender, EventArgs e)
56 {
57 //創建接收套接字
58 serverIp = IPAddress.Parse(textBoxServerIp.Text);
59 serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));
60 receiveUdpClient = new UdpClient(serverIPEndPoint);
61
62 //啟動接收線程
63 Thread threadReceive = new Thread(ReceiveMessage);
64 threadReceive.Start();
65 buttonStart.Enabled = false;
66 buttonStop.Enabled = true;
67
68 //隨機指定監聽端口 N( P+1 ≤ N < 65536 )
69 Random random = new Random();
70 tcport = random.Next(port + 1, 65536);
71
72 //創建監聽套接字
73 myTcpListener = new TcpListener(serverIp, tcport);
74 myTcpListener.Start();
75
76 //啟動監聽線程
77 Thread threadListen = new Thread(ListenClientConnect);
78 threadListen.Start();
79 AddItemToListBox(string.Format("服務線程({0})啟動,監聽端口{1}",serverIPEndPoint,tcport));
80 }
81
82
83 //接受客戶端連接
84 private void ListenClientConnect()
85 {
86 TcpClient newClient = null;
87 while (true)
88 {
89 try
90 {
91 //獲得用於傳遞數據的TCP套接口
92 newClient = myTcpListener.AcceptTcpClient();
93 AddItemToListBox(string.Format("接受客戶端{0}的 TCP 請求", newClient.Client.RemoteEndPoint));
94 }
95 catch
96 {
97 AddItemToListBox(string.Format("監聽線程({0}:{1})終止", serverIp, tcport));
98 break;
99 }
100
101 //啟動發送用戶列表線程
102 Thread threadSend = new Thread(SendData);
103 threadSend.Start(newClient);
104 }
105 }
106
107 //向客戶端發送在線用戶列表信息
108 private void SendData(object userClient)
109 {
110 TcpClient newUserClient = (TcpClient)userClient;
111 userListString = null;
112 for (int i = 0; i < userList.Count; i++)
113 {
114 userListString += userList[i].GetName() + "," + userList[i].GetIPEndPoint().ToString() + ";";
115 }
116 userListString += "end";
117 networkStream = newUserClient.GetStream();
118 bw = new BinaryWriter(networkStream);
119 bw.Write(userListString);
120 bw.Flush(); //不保留現在寫入的數據
121 AddItemToListBox(string.Format("向{0}傳送:[{1}]", newUserClient.Client.RemoteEndPoint, userListString));
122 bw.Close();
123 newUserClient.Close();
124 }
125
126 //接收數據
127 private void ReceiveMessage()
128 {
129 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
130 while (true)
131 {
132 try
133 {
134 //關閉receiveUdpClient時此句會產生異常
135 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
136 string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
137
138 //顯示消息內容
139 AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));
140
141 //處理消息數據
142 string[] splitString = message.Split(',');
143
144 //解析用戶端地址
145 string[] splitSubString = splitString[2].Split(':'); //除去':'
146 IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitSubString[0]), int.Parse(splitSubString[1]));
147 switch (splitString[0])
148 {
149 //收到注冊關鍵字"login"
150 case "login":
151 User user = new User(splitString[1], clientIPEndPoint);
152 userList.Add(user);
153 AddItemToListBox(string.Format("用戶{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
154 string sendString = "Accept," + tcport.ToString();
155 SendtoClient(user, sendString); //向該用戶發送同意關鍵字
156 AddItemToListBox(string.Format("向{0}({1})發出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
157 for (int i = 0; i < userList.Count; i++)
158 {
159 if (userList[i].GetName() != user.GetName())
160 {
161 //向除剛加入的所有用戶發送更新消息
162 SendtoClient(userList[i], message);
163 }
164 }
165 AddItemToListBox(string.Format("廣播:[{0}]", message));
166 break;
167
168 //收到關鍵字"logout"
169 case "logout":
170 for (int i = 0; i < userList.Count; i++)
171 {
172 if (userList[i].GetName() == splitString[1])
173 {
174 AddItemToListBox(string.Format("用戶{0}({1})退出", userList[i].GetName(), userList[i].GetIPEndPoint()));
175 userList.RemoveAt(i);
176 }
177 }
178
179 //向所用用戶發送更新消息
180 for (int i = 0; i < userList.Count; i++)
181 {
182 SendtoClient(userList[i], message);
183 }
184 AddItemToListBox(string.Format("廣播:[{0}]", message));
185 break;
186 }
187 }
188 catch
189 {
190 break;
191 }
192 }
193 AddItemToListBox(string.Format("服務線程({0})終止", serverIPEndPoint));
194 }
195
196 private void SendtoClient(User user, string message)
197 {
198 //匿名發送
199 sendUdpClient = new UdpClient(0);
200 byte[] sendbytes = Encoding.Unicode.GetBytes(message);
201 IPEndPoint remoteIPEndPoint = user.GetIPEndPoint();
202 sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);
203 sendUdpClient.Close();
204 }
205
206 //當點擊關閉按鈕的事件處理程序
207 private void buttonStop_Click(object sender, EventArgs e)
208 {
209 myTcpListener.Stop();
210 receiveUdpClient.Close();
211 buttonStart.Enabled = true;
212 buttonStop.Enabled = false;
213 }
214
215 //用委托機制解決顯示問題
216 private delegate void AddItemToListBoxDelegate(string str);
217 private void AddItemToListBox(string str)
218 {
219 if (listBoxStatus.InvokeRequired)
220 {
221 AddItemToListBoxDelegate d = AddItemToListBox;
222 listBoxStatus.Invoke(d, str);
223 }
224 else
225 {
226 listBoxStatus.Items.Add(str);
227 listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;
228 listBoxStatus.ClearSelected();
229 }
230 }
231 }
232 }

6.2 客戶端

View Code
  1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Text;
8 using System.Windows.Forms;
9
10 //添加的命名空間引用
11 using System.Net;
12 using System.Net.Sockets;
13 using System.Threading;
14 using System.IO;
15
16 namespace Client
17 {
18 public partial class MainForm : Form
19 {
20 int port; //端口號
21 private UdpClient sendUdpClient; //匿名發送套接口
22 private UdpClient receiveUdpClient; //實名接收套接口
23 private IPEndPoint clientIPEndPoint; //客戶端地址
24 private TcpClient myTcpClient; //TCP套接字
25 private NetworkStream networkStream; //網絡流
26 private BinaryReader br; //避免網絡邊界問題的讀數據流
27 string userListString; //用戶名字串
28 private List<ChatForm> chatFormList = new List<ChatForm>(); //用戶窗體列表
29 public MainForm()
30 {
31 InitializeComponent();
32
33 //本地IP和端口號的初始化
34 IPAddress[] LocalIP = Dns.GetHostAddresses("");
35 IPAddress address = IPAddress.Any;
36 for (int i = 0; i < LocalIP.Length; i++)
37 {
38 if (LocalIP[i].AddressFamily == AddressFamily.InterNetwork)
39 {
40 address = LocalIP[i];
41 break;
42 }
43 }
44 textBoxServerIp.Text = address.ToString();
45 textBoxLocalIp.Text = address.ToString();
46
47 //獲得隨機端口號
48 port = new Random().Next(1024, 65535);
49 textBoxLocalPort.Text = port.ToString();
50
51 //隨機生成用戶名
52 Random r = new Random((int)DateTime.Now.Ticks); //類似於與C++中的種子
53 textBoxUserName.Text = "user" + r.Next(100, 999);
54 buttonLogout.Enabled = false;
55 }
56
57 private void buttonLogin_Click(object sender, EventArgs e)
58 {
59 //創建接收套接字
60 IPAddress clientIp = IPAddress.Parse(textBoxLocalIp.Text);
61 clientIPEndPoint = new IPEndPoint(clientIp, int.Parse(textBoxLocalPort.Text));
62 receiveUdpClient = new UdpClient(clientIPEndPoint);
63
64 //啟動接收線程
65 Thread threadReceive = new Thread(ReceiveMessage);
66 threadReceive.Start();
67 AddItemToListBox(string.Format("客戶線程({0})啟動", clientIPEndPoint));
68
69 //匿名發送
70 sendUdpClient = new UdpClient(0);
71
72 //啟動發送線程
73 Thread threadSend = new Thread(SendMessage);
74 threadSend.Start(string.Format("login,{0},{1}",textBoxUserName.Text,clientIPEndPoint));
75 AddItemToListBox(string.Format("發出:[login,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
76 buttonLogin.Enabled = false;
77 buttonLogout.Enabled = true;
78 this.Text = textBoxUserName.Text; //使當前窗體名字變為當前用戶名
79 }
80
81 //發送數據
82 private void SendMessage(object obj)
83 {
84 string message = (string)obj;
85 byte[] sendbytes = Encoding.Unicode.GetBytes(message);
86
87 //服務器端的IP和端口號
88 IPAddress remoteIp = IPAddress.Parse(textBoxServerIp.Text);
89 IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(textBoxServerPort.Text));
90 sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint); //匿名發送
91 sendUdpClient.Close();
92 }
93
94 //接收數據
95 private void ReceiveMessage()
96 {
97 IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
98 while (true)
99 {
100 try
101 {
102 //關閉receiveUdpClient時此句會產生異常
103 byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
104 string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);
105
106 //顯示消息內容
107 AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));
108
109 //處理消息數據
110 string[] splitString = message.Split(','); //除去','
111 switch (splitString[0])
112 {
113 //若接收連接
114 case "Accept":
115 try
116 {
117 AddItemToListBox(string.Format("連接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));
118 myTcpClient = new TcpClient();
119 myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));
120 if (myTcpClient != null)
121 {
122 AddItemToListBox("連接成功!");
123 networkStream = myTcpClient.GetStream();
124 br = new BinaryReader(networkStream);
125 }
126 }
127 catch
128 {
129 AddItemToListBox("連接失敗!");
130 }
131 Thread threadGetList = new Thread(GetUserList);
132 threadGetList.Start(); //請求獲得用戶列表信息線程啟動
133 break;
134
135 //若收到注冊關鍵字"login",代表有新的用戶加入,並更新用戶列表
136 case "login":
137 AddItemToListBox(string.Format("新用戶{0}({1})加入", splitString[1], splitString[2]));
138 string userItemInfo = splitString[1] + "," + splitString[2];
139 AddItemToListView(userItemInfo);
140 break;
141
142 //若收到注冊關鍵字"logout",代表有用戶退出,並更新用戶列表
143 case "logout":
144 AddItemToListBox(string.Format("用戶{0}({1})退出", splitString[1], splitString[2]));
145 RmvItemfromListView(splitString[1]);
146 break;
147
148 //若收到回話關鍵字"talk",則表明有用戶發起回話,並開始准備回話
149 case "talk":
150 for (int i = 0; i < chatFormList.Count; i++)
151 {
152 if (chatFormList[i].Text == splitString[2])
153 {
154 chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]);
155 }
156 }
157 break;
158 }
159 }
160 catch
161 {
162 break;
163 }
164 }
165 AddItemToListBox(string.Format("客戶線程({0})終止", clientIPEndPoint));
166 }
167
168 //獲得用戶列表
169 private void GetUserList()
170 {
171 while (true)
172 {
173 userListString = null;
174 try
175 {
176 userListString = br.ReadString();
177 if (userListString.EndsWith("end"))
178 {
179 AddItemToListBox(string.Format("收到:[{0}]", userListString));
180 string[] splitString = userListString.Split(';');
181 for (int i = 0; i < splitString.Length - 1; i++)
182 {
183 AddItemToListView(splitString[i]);
184 }
185 br.Close();
186 myTcpClient.Close();
187 break;
188 }
189 }
190 catch
191 {
192 break;
193 }
194 }
195 }
196
197 //當點擊退出按鈕的事件處理函數
198 private void buttonLogout_Click(object sender, EventArgs e)
199 {
200 //匿名發送
201 sendUdpClient = new UdpClient(0);
202
203 //啟動發送線程
204 Thread threadSend = new Thread(SendMessage);
205 threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));
206 AddItemToListBox(string.Format("發出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
207 receiveUdpClient.Close();
208 listViewOnline.Items.Clear();
209 buttonLogin.Enabled = true;
210 buttonLogout.Enabled = false;
211 this.Text = "Client"; //恢復到原來的名字
212 }
213
214 //當點擊兩次發起回話的事件處理函數
215 private void listViewOnline_DoubleClick(object sender, EventArgs e)
216 {
217 string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;
218 if (peerName == textBoxUserName.Text)
219 {
220 return;
221 }
222 string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;
223 string[] splitString = ipendp.Split(':'); //除去':'
224 IPAddress peerIp = IPAddress.Parse(splitString[0]);
225 IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));
226 ChatForm dlgChatForm = new ChatForm();
227 dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);
228 dlgChatForm.Text = peerName;
229 chatFormList.Add(dlgChatForm);
230 dlgChatForm.Show();
231 }
232
233 //利用委托機制顯示信息
234 private delegate void AddItemToListBoxDelegate(string str);
235 private void AddItemToListBox(string str)
236 {
237 if (listBoxStatus.InvokeRequired)
238 {
239 AddItemToListBoxDelegate d = AddItemToListBox;
240 listBoxStatus.Invoke(d, str);
241 }
242 else
243 {
244 listBoxStatus.Items.Add(str);
245 listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;
246 listBoxStatus.ClearSelected();
247 }
248 }
249
250 private delegate void AddItemToListViewDelegate(string str);
251 private void AddItemToListView(string str)
252 {
253 if (listViewOnline.InvokeRequired)
254 {
255 AddItemToListViewDelegate d = AddItemToListView;
256 listViewOnline.Invoke(d, str);
257 }
258 else
259 {
260 string[] splitString = str.Split(',');
261 ListViewItem item = new ListViewItem();
262 item.SubItems.Add(splitString[0]);
263 item.SubItems.Add(splitString[1]);
264 listViewOnline.Items.Add(item);
265 }
266 }
267
268 private delegate void RmvItemfromListViewDelegate(string str);
269 private void RmvItemfromListView(string str)
270 {
271 if (listViewOnline.InvokeRequired)
272 {
273 RmvItemfromListViewDelegate d = RmvItemfromListView;
274 listViewOnline.Invoke(d, str);
275 }
276 else
277 {
278 for (int i = 0; i < listViewOnline.Items.Count; i++)
279 {
280 if (listViewOnline.Items[i].SubItems[1].Text == str)
281 {
282 listViewOnline.Items[i].Remove();
283 }
284 }
285 }
286 }
287 }
288 }

6.3 聊天子窗口的代碼

View Code
 1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Text;
8 using System.Windows.Forms;
9
10 //添加的命名空間引用
11 using System.Net;
12 using System.Net.Sockets;
13 using System.Threading;
14
15 namespace Client
16 {
17 public partial class ChatForm : Form
18 {
19 private string selfUserName; //自己的用戶名
20 private string peerUserName; //對方的用戶名
21 private IPEndPoint peerUserIPEndPoint; //對方的地址
22 private UdpClient sendUdpClient; //匿名發送套接口
23 public ChatForm()
24 {
25 InitializeComponent();
26 }
27
28 //類似於構造函數
29 public void SetUserInfo(string selfName,string peerName,IPEndPoint peerIPEndPoint)
30 {
31 selfUserName = selfName;
32 peerUserName = peerName;
33 peerUserIPEndPoint = peerIPEndPoint;
34 }
35
36 //點擊發送按鈕的事件處理程序
37 private void buttonSend_Click(object sender, EventArgs e)
38 {
39 //匿名發送
40 sendUdpClient = new UdpClient(0);
41
42 //啟動發送線程
43 Thread threadSend = new Thread(SendMessage);
44 threadSend.Start(string.Format("talk,{0},{1},{2}", DateTime.Now.ToLongTimeString(), selfUserName, textBoxSend.Text));
45 richTextBoxTalkInfo.AppendText(selfUserName + "" + DateTime.Now.ToLongTimeString() + Environment.NewLine + textBoxSend.Text);
46 richTextBoxTalkInfo.AppendText(Environment.NewLine);
47 richTextBoxTalkInfo.ScrollToCaret();
48 textBoxSend.Text = "";
49 textBoxSend.Focus();
50 }
51
52 //數據發送函數
53 private void SendMessage(object obj)
54 {
55 string message = (string)obj;
56 byte[] sendbytes = Encoding.Unicode.GetBytes(message);
57 sendUdpClient.Send(sendbytes, sendbytes.Length, peerUserIPEndPoint);
58 sendUdpClient.Close();
59 }
60
61 //顯示通話內容
62 public void ShowTalkInfo(string peerName, string time, string content)
63 {
64 richTextBoxTalkInfo.AppendText(peerName + "" + time + Environment.NewLine + content);
65 richTextBoxTalkInfo.AppendText(Environment.NewLine);
66 richTextBoxTalkInfo.ScrollToCaret();
67 }
68
69 //當點擊關閉時的事件處理程序
70 private void buttonClose_Click(object sender, EventArgs e)
71 {
72 this.Close();
73 }
74 }
75 }




 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM