大家好,在上篇《利用TCP和UDP協議,實現基於Socket的小聊天程序(初級版)》博客中,所寫程序只是實現簡單的連接通信,基於控制台實現,運用了TCP和UDP兩種傳輸協議。今天我和大家分享一個基於窗體的聊天程序,使用了多線程,實現的功能類似於QQ的聊天,不同的是只有一個服務器端,但可以有多個客戶端與其通信,只能實現簡單的文字信息交流。。。
同樣,這個聊天程序也需要一個服務器端,和N個客戶端來模擬實現,首先我們來搭建服務器端
首先貼上服務器端的界面圖:
界面很簡單,左邊一個客戶端在線的列表,一個顯示消息的文本框和一個發送消息的文本框,為了演示簡單,我把IP和Port都固定為127.0.0.1 和8888
首先我們來看看【啟動服務器】按鈕的代碼:

2 Thread threadWatch = null;
3
4 // 負責監聽的套接字
5 Socket socketServer = null;
6
7 private void btn_StartServer_Click( object sender, EventArgs e)
8 {
9 // 創建 服務器 負責監聽的套接字 參數(使用IP4尋址協議,使用流式連接,使用TCP傳輸協議)
10 socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
11
12 // 獲取IP地址
13 IPAddress ip = IPAddress.Parse(tb_IP.Text.Trim());
14
15 // 創建 包含IP和Port的網絡節點對象
16 IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(tb_Port.Text.Trim()));
17
18 // 將負責監聽 的套接字 綁定到 唯一的IP和端口上
19 socketServer.Bind(endPoint);
20
21 // 設置監聽隊列 一次可以處理的最大數量
22 socketServer.Listen( 10);
23
24 // 創建線程 負責監聽
25 threadWatch = new Thread(WatchConnection);
26 // 設置為后台線程
27 threadWatch.IsBackground = true;
28 // 開啟線程
29 threadWatch.Start();
30
31 ShowMsg( " =====================服 務 器 啟 動 成 功====================== ");
32
33 }
這里的代碼其實和初級版聊天程序中的服務器代碼並沒有多大差別,只是在這里我們使用了線程來專門負責監聽客戶端的請求,但是我們這里為什么要使用多線程呢??這就需要對多線程的概念及作用去做一個了解了,這里我簡單的說下,我們的程序運行是由一個主線程在執行着,而Socket的Accept()方法執行的時候會阻斷線程,如果我們沒有使用多線程的話也就是說我們的主線程被阻斷了,這是就會出現一個現象就是程序卡死來那里了,只有等到某個客戶端連接到該服務器,Accept()方法接受到了客戶端的請求了主線程才會被釋放出來,為了避免這種情況,所以我們這里用過了另外一個線程專門來負責監聽客戶端的請求,使得主線程仍然可以自由執行(個人理解)。線程實例化時要求傳入參數,參數的本質就是一個委托,所以在實例化的時候要求傳入的是一個方法作為參數,所以我們傳入WatchConnection負責監聽的方法,貼上該方法的代碼,如下:

Dictionary< string, Socket> socketDic = new Dictionary< string, Socket>();
// 用來接收數據的線程
Thread threadRec = null;
// 監聽方法
void WatchConnection()
{
// 持續不斷的監聽
while ( true)
{
// 開始監聽 客戶端 連接請求 【注意】Accept方法會阻斷當前的線程--未接受到請求 程序卡在那里
Socket sokConnection = socketServer.Accept(); // 返回一個 負責和該客戶端通信的 套接字
// 將返回的新的套接字 存儲到 字典序列中
socketDic.Add(sokConnection.RemoteEndPoint.ToString(), sokConnection);
// 向在線列表中 添加一個 客戶端的ip端口字符串 作為客戶端的唯一標識
lb_OnlineList.Items.Add(sokConnection.RemoteEndPoint.ToString());
// 打印輸出
ShowMsg( " 客戶端連接成功: "+sokConnection.RemoteEndPoint.ToString());
// 為該通信Socket 創建一個線程 用來監聽接收數據
threadRec = new Thread(RecMsg);
threadRec.IsBackground = true;
threadRec.Start(sokConnection);
}
}
其實這個方法里的內容很多,首先就我們要實現的效果,我們來思考一個問題,我們要實現的效果是一個服務器可以和多個客戶端進行交流,那服務器怎么知道是哪個客戶端發過來的呢?Socket的Accept()方法都會返回一個新的Socket對象,那當我們在不同的客戶端向服務器端發送數據時我們該用哪個返回的Socket對象呢??說說我的想法吧。。我的想法是把每個返回的Socket對象都存放起來,用的時候再對應的去拿就可以了,這時我就想到了字典(Dictionary)集合,好,一個問題解決了,但還有一個問題,就是我們根據什么去拿對應的Socket對象呢??字典就是一個鍵值對,我們已經解決了這個值,我們該用干什么鍵去拿呢??所謂鍵,就是要唯一能標識的東西,所以這是我們就想到了IP和Port了,這個能唯一標識是那個客戶端,所以我們可以通過返回的新的Socket對象的RemoteEndPoint屬性來拿到客戶端的IP和Port信息,這樣,我們兩個問題都解決了,這樣我們就存儲了返回的Socket對象及對應客戶端的Ip和Port,然后我們在服務器的“在線列表”上添加上該客戶端的IP和Port,用於服務器向客戶端發送信息時選擇對應的客戶端。還有就是,很明顯的能看到一個while的死循環,為什么要用死循環呢?因為如果沒有用循環,當客戶端只有一個的情況下,來連接到服務器,沒問題,服務器可以接受到請求,但如果多個呢,,這時就出問題了,之后連接的客戶端的請求都接受不到了,所以我們要用一個死循環,來持續的監聽請求。最后就是會發現,在這個方法中我又用到了一個線程,這個線程是干嘛的呢?其實,大家可以想想,當客戶端給服務器發送信息時,服務器端需要來監聽信息,也就是消息過來時要執行Socket的Receive方法來接受消息,當然我們要求務器端要持續的監聽接收客戶端發來的消息,所以我們就想到線程了,在服務器端用一個線程專門來接收客戶端發來的信息,所以我就將返回的Socket對象作為線程的參數傳遞給對應的方法,線程的參數傳遞是在Start()方法中實現。
下面貼上此方法中使用了線程的RecMsg方法:

2 void RecMsg( object socket)
3 {
4 // 持續監聽接收數據
5 while ( true)
6 {
7 // 實例化一個字符數組
8 byte[] data = new byte[ 1024 * 1024];
9 // 接受消息數據
10 int receiveBytes = ((Socket)socket).Receive(data);
11 // 轉換成字符串
12 string recMsg = Encoding.UTF8.GetString(data, 0, receiveBytes);
13 // 打印接收到的數據
14 ShowMsg(((Socket)socket).RemoteEndPoint.ToString() + " : " + recMsg);
15 }
16 }
可以看到該方法的參數來下是object,這是由於線程傳參就是要求參數的類型要是object型的,我們仍然用了一個while死循環來實現持續監聽接受數據,至於接受的其他代碼就和初級版中的一樣了,不多做介紹了,但這里需要注意一點,因為我們在線程的方法中對主線程的控件進行了操作,所以直接運行會報錯,解決方法,我們在窗體的構造函數中添加代碼,使其不對TextBox進行跨線程檢測,代碼如下:

2 {
3 InitializeComponent();
4
5 // 關閉對文本框的 跨線程操作
6 TextBox.CheckForIllegalCrossThreadCalls = false;
7 }
前面兩個方法中都用到了ShowMsg()這個方法了,至於這個方法,很簡單了,作用就是在“顯示消息”文本框中追加消息而已,為了代碼重用就寫了個方法,貼上代碼:

2 void ShowMsg( string msg)
3 {
4 tb_MsgShow.AppendText(msg + " \r\n ");
5
6 }
然后我們來看下【發送按鈕】的代碼:

private void btn_SendMsg_Click( object sender, EventArgs e)
{
// 調用發送方法
Send();
}
Send()方法代碼:

2 /// 發送消息到客戶端
3 /// </summary>
4 void Send()
5 {
6 if (lb_OnlineList.Text == "")
7 {
8 MessageBox.Show( " 請先選擇一個客戶端 ");
9 return;
10 }
11 // 獲取發送信息
12 string Message = tb_InputMsg.Text.Trim();
13 // 將字符串轉換成字節數組
14 byte[] data = System.Text.Encoding.UTF8.GetBytes(Message);
15 // 找到對應的客戶端 並發送數據
16 socketDic[lb_OnlineList.Text].Send(data, SocketFlags.None);
17 // 打印輸出
18 ShowMsg( " 發送數據: " + Message);
19 // 清空輸入消息的內容
20 tb_InputMsg.Text = "";
21
22 }
這樣我們服務器端就搭建完成了,接下來看看怎么搭建客戶端了,,
首先貼上客戶端界面的效果:
界面也非常的簡單,就是一個【連接服務器】按鈕、一個【發送】按鈕,一個“顯示消息” 框和一個“輸入消息” 框。。。
同樣我們來看看【連接服務器】按鈕的代碼:

2 Thread threadReceive = null;
3
4 // 客戶端套接字
5 Socket socketClient = null;
6 // 連接服務器
7 private void btn_ConnServer_Click( object sender, EventArgs e)
8 {
9 // 獲取IP
10 IPAddress ip = IPAddress.Parse(tb_IP.Text.Trim());
11 // 新建一個網絡節點
12 IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(tb_Port.Text.Trim()));
13 // 新建一個Socket 負責 監聽服務器的通信
14 socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
15 // 連接遠程主機
16 socketClient.Connect(endPoint);
17
18 // 打印輸出
19 ShowMsg( " =====================服 務 器 連 接 成 功====================== ");
20
21 // 創建線程 監聽服務器 發來的消息
22 threadReceive = new Thread(RecMsg);
23 // 設置為后台線程
24 threadReceive.IsBackground = true;
25 // 開啟線程
26 threadReceive.Start();
27 }
經過了服務器端代碼的講解之后,看客戶端的代碼應該輕松多了把。。這里同樣我們先獲取到IP和Port,這里的IP和Port必須和服務器端的一致,當然也有可能服務器的IP是任意IP,這時Port必須和服務器的一致,然后根據IP和Port建立一個網絡節點,然后新建一個Socket對象再連接到遠程主機,代碼和初級版中一樣,同樣,我們在客戶端也利用了線程來持續監聽服務器端發來的消息,線程代碼一樣,不再做贅述,同樣是監聽RecMsg()方法,那就貼上RecMsg()方法的代碼:

2 void RecMsg()
3 {
4 while ( true)
5 {
6 // 初始化一個 1M的 緩存區(字節數組)
7 byte[] data = new byte[ 1024 * 1024];
8 // 將接受到的數據 存放到data數組中 返回接受到的數據的實際長度
9 int receiveBytes = socketClient.Receive(data);
10 // 將字符串轉換成字節數組
11 string strMsg = Encoding.UTF8.GetString(data, 0, receiveBytes);
12 // 打印輸出
13 ShowMsg( " 接受數據: " + strMsg);
14 }
15 }
代碼不多加解釋了,和服務器端的一樣,差別就是服務器端需要Accetp()方法來監聽,然后返回個Socket對象,然后由這個對象來進行通信,客戶端就直接用一開始的Socket對象進行通信就可以了,用這個對象首發數據,這里為接收數據,while循環實現,這里也是非主線程操作主線程的控件,所以同樣要在窗體構造函數中添加關閉跨線程檢測代碼:

2 {
3 InitializeComponent();
4
5 // 關閉對TextBox的跨線程檢測
6 TextBox.CheckForIllegalCrossThreadCalls = false;
7 }
ShowMsg()方法,追加文本:

2 void ShowMsg( string msg)
3 {
4 tb_ShowMsg.AppendText(msg + " \r\n ");
5 }
下面我們要通過點擊【發送】按鈕實現發送數據,貼上代碼:

2 private void btn_SendMsg_Click( object sender, EventArgs e)
3 {
4 // 調用Send()方法
5 Send();
6 }
Send()方法代碼:

2 void Send()
3 {
4 string Message = tb_InputMsg.Text.Trim();
5
6 // 將字符串轉換成字節數組
7 byte[] data = System.Text.Encoding.UTF8.GetBytes(Message);
8 // 發送數據
9 socketClient.Send(data, SocketFlags.None);
10
11 ShowMsg( " 發送數據: " + Message);
12
13 // 清空輸入消息的內容
14 tb_InputMsg.Text = "";
15 }
發送信息也一樣,直接通過SocketClient的Send()方法發送消息就可以了,之前寫過幾次了,應該都熟悉了吧,,這里就不加敘述了,,,
哈哈,,這樣客戶端的搭建也完成了,,激動人心的時刻要來了。。。。。。。
下面我們來看看一步一步把整個程序運行起來的效果吧
第一步:開啟服務器
點擊【啟動服務器】按鈕:
可以看到提示:服務器啟動成功,這就說明服務器沒問題了,,,,
第二步:開啟客戶端,因為我們要實現的是服務器可以和多個客戶端進行通信,所以我們開啟三個客戶端
點擊【連接服務器】,由於這里三個效果一樣,就先貼出一個,看效果:
同樣提示:服務器連接成功。。。。。
這時服務器端也接收到了來自三個客戶端的連接了,看效果圖:
第三步:通信
現在客戶端向服務器發送數據:
同樣的還有兩個客戶端也一樣,二號:我是二號客戶端;三號:我是三號客戶端 ,圖就不貼了,和一號一樣,這樣發數據主要是在服務器端看起來有助於我們區別
這時,服務器端就接受到了我們客戶端發送過去的數據了,效果如圖:
這樣,,我們就可以確定從客戶端向服務器端發送數據沒有問題,,,接下來,從服務器向客戶端發送數據,這里我們主要是要看看服務器端的數據是不是可以發送到對應的客戶端。。。。
服務器發送數據時,我們首先要在“在線列表”中選擇一個客戶端進行發送,如圖:
這里我們選擇了IP為127.0.0.1 端口為2966的客戶端(也就是剛才的二號客戶端),向其發送:“你好,我是服務器” 數據,我們看效果:
一號:
二號:
三號:
可以看出,我們的程序沒什么問題,,數據發送到了對應的客戶端了,,,,這樣就達到寫這個程序的目的了,本次博客的任務也就完成了,哈哈,,,當然這個程序還是有很大的擴展的,比如現在只是實現文字消息的傳輸,也可以擴展成文件傳輸,圖片傳輸等,,,,歡迎各位擴展交流啊。。。。。。。