實驗功能:c#
設計程序,分別構建通訊的兩端:服務器端和客戶端應用程序,套接字類型爲面向鏈接的Socket,本身構建雙方的應答模式,實現雙方的數據的發送和接收(S發給C,C發給S)。服務器
服務端程序能響應單個或任意多個客戶端鏈接請求;服務端能向單個客戶發送消息,支持羣發消息給全部客戶端;數據結構
通訊的雙方具有異常響應功能,包括對方異常退出的處理。若是客戶端退出,服務器有響應;反之亦然。spa
客戶端之間直接通訊,C與C之間直接通訊(不是經過S傳遞)。線程
設計思路:設計
服務器設計思路:服務器的設計是此次實驗最複雜的部分,由於服務器的功能比較多。做爲服務器,它要能夠同時與多個客戶端鏈接,爲每個鏈接的客戶端建立一個通訊Socket,本身還要有一個Socket用於監聽客戶端的鏈接請求;服務器要建立一個數據結構用於保存鏈接進來的客戶端的信息(Socket和客戶端的名字);服務器要將鏈接進來的客戶端顯示出來,用戶能夠根據顯示出來的用戶列表來向指定的客戶端發信息;服務器要能及時地刷新客戶端列表,當有新的客戶端鏈接進來或是退出的時候要及時通知全部的客戶端並刷新本身的客戶端列表;服務器要能接收全部的客戶端的信息,並將信息無錯地轉發給指定的客戶端。code
客戶端設計思路:客戶端的設計相對於服務器來講的話對會比較簡單一點。客戶端要有接收服務器信息的功能,但客戶端只向服務器發信息,客戶端經過服務器的轉發功能向其它的客戶端發送信息。客戶端要能夠處理服務器發過來的信息,還要有數據結構用來保存全部客戶端的名字,並將全部客戶端名字列表顯示出來。能夠指定客戶端列表裏面的多個項來向不一樣的客戶端發信息。orm
通訊數據處理:不管是服務器發給客戶端,仍是客戶端發給服務器的數據,雙方都要進行處理。對於不用的類型的數據要設計不用的標誌信息,當雙方收到信息後跟據標誌信息進行不一樣的處理。數據能夠分爲三種 :事件
a)登錄信息。這類信息提示有新的客戶端鏈接進來。該信息由客戶端首先發給服務器,服務器收到後會更新本身的在線客戶端列表,增長與該客戶端通訊的Socket和名字,並將該信息轉發給全部在線的客戶端,提醒客戶端即時更新客戶端列表。這類信息以「login,客戶端名」的形式發送。ip
b)退出信息。這類信息提示發信息的客戶端即將退出服務器。該信息由客戶端首先發給服務器,服務器收到後會更新本身的在線客戶端列表,刪除與該客戶端通訊的Socket和名字,並將該信息轉發給全部在線的客戶端,提醒客戶端即時更新客戶端列表。這類信息以「logout,客戶端名」的形式發送。
c)通訊信息。這類信息提示發送信息的客戶端向在線的某個客戶端或是服務器發起了通訊,也能夠是服務器與某個客戶端發起了通訊。若是該信息是服務器發給客戶端或是客戶端發給服務器,則直接發送,不用通過轉發;若是是客戶端向另外一個客戶端發送信息,則是先發給服務器,服務再轉發給指定的客戶端。這類信息以「talk,目的客戶端名,發送的信息」的形式發送。
線程的設計思路:在服務器方面,須要一個程專門用於監聽客戶端的鏈接請求,對於鏈接進來的每個客戶端,還要建立一個線程用於接收信息,程序的主線程用於向不一樣的客戶端發送信息,因此服務器至少須要要n+2(n>=0)個線程;在客戶端方面,須要一個線程用於接收服務的信息,還要一個線程用於向服務器發送信息,因此只須要2個線程。
信息無邊界問題:因爲這裏用的C#裏面原始Socket套接字,因此在數據收發的過程當中會出現無邊界的問題。有時服務器向客戶端發送多條不一樣類型的信息,客戶端會把它們合併在一塊兒,當成一條信息處理。爲了提取不一樣類型的信息,發送信息以前要爲每一條信息加特定的結束符。
客戶端之間直接通訊問題:爲了實現客戶端之間的直接通訊,客戶端之間必須知道其它客戶端的IP和端口,這能夠經過服務器的轉發獲得客戶端之間的IP和端口。客戶端也必須有一個本身可用的端口號用來和其它客戶端之間的通訊,因此除了第一次的客戶端與服務器的鏈接之外,客戶端便是服務器也是客戶端。
服務器處理不一樣類型信息代碼:
string[] splitString = receiveString.Split(','); //分割字符 switch (splitString[0].ToLower()) { case "login": // 登錄信息 user.username = splitString[1]; userList.Add (user); // 增長用戶列表 AddItemToListBox (user.username); // 刷新用戶列表 sendToAllClient (user,receiveString); // 通知全部在線用戶 FirstLogin (user); break; case "logout": // 退出信息 DeletItemInListBox (user.username); sendToAllClient (user,receiveString);// 通知全部在線用戶 RemoveUser (user); // 刪除用戶信息 UserCount (--usercount); // 刷新用戶列表 break; case "talk": // 對話信息 multMessage (user,receiveString); // 轉發對話 break; default: sendMessageTorichBox ("不知道什麼意思!"); break; }
服務器監聽客戶端代碼:
private void button1_Click(object sender, EventArgs e) { isNormalExit = false; buttu_richBoxDelegate d = buttu_richBox; // 委託事件 try { myListener.Listen (10); // 開始監聽 richTextBox1.Invoke(d,"成功監聽."); // 成功監聽 } catch{ richTextBox1.Invoke(d,"監聽失敗。"); } Thread mhThread = new Thread(ListenClientConnect); // 建立新的線程 mhThread.IsBackground = true; // 設置爲後臺線程 mhThread.Start (); button1.Enabled=false; // 開始監聽按鈕不可用 button2.Enabled= true; }
服務器接受客戶端代碼:
private void ListenClientConnect () { Socket newClient =null; While (isNormalExit==false) { try { newClient = myListener.Accept(); // 接受客戶端 if(isNormalExit == true) // 若是服務器中止監聽 { newClient.Close(); // 關閉Socket usercount = 0; UserCount(usercount); Break; } }Catch{ break; } User user = new User(newClient); // 保存客戶端列表 Thread threadReceive = new Thread(ReceiveData); // 建立新的線程 threadReceive.IsBackground=true; //設置爲後臺線程 threadReceive.Start(user); // 開始線程 UserCount(++usercount); // 客戶端人數加1 } }
客戶端鏈接服務器代碼:
Private void button1_Click(object sender, EventArgs e) { button1.Enabled = false; client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //新建套接字 AddrichTextBox1Massage d = sendrichTextBox1Massage; Try { String name = Dns.GetHostName(); // 得到計算機的名字 IPHostEntry me = Dns.GetHostEntry (name); //得到計算機IP foreach(IPAddress ips in me.AddressList) { Try { IPEndPoint ep = new IPEndPoint(ips, 8889); client.Connect(new IPEndPoint(ips, 8889)); // 鏈接服務器 break; } catch {//若獲取的IP是vs6的話 } } client.Send(Encoding.UTF8.GetBytes("login," + textBox1.Text));//向服務器發信息 Thread threadReceive = new Thread(new ThreadStart(ReceiveData));//建立新線程 threadReceive.IsBackground = true; // 設置爲後臺線程 threadReceive.Start(); //開始線程 }
客戶端接受服務器信息代碼:
private void ReceiveData() { AddrichTextBox1Massage d = sendrichTextBox1Massage; int receiveLength; while(isExit==false) { try{ receiveLength = client.Receive(result); //開始接收信息 recieveMessage=Encoding.UTF8.GetString(result,0,receiveLength); }catch{ if (isExit == false){ richTextBox1.Invoke(d, "與服務器失去聯繫。"); client.Shutdown(SocketShutdown.Both); // 關閉套接字 client.Close(); } break; } string[] splitString = recieveMessage.Split(','); //處理信息 string command = splitString[0].ToLower(); switch(command) { case "login":AddOnline(recieveMessage); // 登錄信息 break; case "logout": RemoveUserName(splitString[1]); // 退出信息 break; case "talk": richTextBox1.Invoke(d, "["+splitString[1] + "]對我說: " + splitString[2]); // 對話信息 break; default: richTextBox1.Invoke(d,"不知什麼意思。"); break; } } LostConnect(); //關閉鏈接 }
客戶端監聽其它客戶端代碼:
private void ServerReceive(Object client) { AddrichTextBox1Massage d = sendrichTextBox1Massage; Socket myClientSocket = (Socket)client; byte[] str =new byte[1024]; while (true) { try { int n = myClientSocket.Receive(str); richTextBox1.Invoke(d, Encoding.UTF8.GetString(str, 0, n)); break; } catch { myClientSocket.Close(); //richTextBox1.Invoke(d, "接收消息失敗!"); break; } } myClientSocket.Close(); }
程序運行效果:
服務器運行界面:
有客戶端鏈接進服務器:
在線客戶列表顯示了鏈接進的客戶端的名字,在線客戶人數顯示爲3人
上圖表示有3個客戶端鏈接進了服務器。
服務器向客戶端發送信息:
服務器向在線客戶列表裏的2個客戶同時發了信息,2個客戶端收到了正確的信息。
客戶端的啓動界面:
客戶端自動生成用戶的名字。
客戶端登錄的界面:
客戶端顯示鏈接成功,並刷新在線用戶列表。
多個客戶端鏈接服務器時的界面:
當有多個客戶端與服務器鏈接時,客戶端會自動更新在線用戶列表。
客戶端向其它客戶端發TCP信息:
客戶端能夠同時向服務器和多個客戶端發送信息。
客戶端接收來自其它客戶端的TCP信息:
接收的信息是其它客戶端直接發過來的,不通過服務器的轉發。
客戶端退出時:
客戶端退出時,服務器會知道退出的用戶,並把該客戶端移出列表,同時發信息通知其它的客戶端,使它們能夠及時地更新用戶列表。
服務器退出時:
當服務器退出時,全部的客戶端會提示與服務器失去聯繫,並將在線用戶列表清空。