C#是微軟隨著VS.net新推出的一門語言。它作為一門新興的語言,有著C++的強健,又有著VB等的RAD特性。而且,微軟推出C#主要的目的是為了對抗Sun公司的Java。大家都知道Java語言的強大功能,尤其在網路編程方面。於是,C#在網路編程方面也自然不甘落後於人。本文就向大家介紹一下C#下實現通訊端(Sockets)編程的一些基本知識,以期能使大家對此有個大致瞭解。首先,我向大家介紹一下通訊端的概念。
通訊端基本概念:
通訊端是通訊的基石,是支援TCP/IP協議的網路通訊的基本操作單元。可以將通訊端看作不同主機間的進程進行雙向通訊的端點,它構成了單個主機內及整個網路間的編程介面。通訊端存在於通訊域中,通訊域是為了處理一般的線程通過通訊端通訊而引進的一種抽象概念。通訊端通常和同一個域中的通訊端交換資料(資料交換也可能穿越域的界限,但這時一定要執行某種解釋程式)。各種進程使用這個相同的域互相之間用Internet協議簇來進行通訊。
通訊端可以根據通訊性質分類,這種性質對於使用者是可見的。應用程式一般僅在同一類的通訊端間進行通訊。不過只要底層的通訊協定允許,不同類型的通訊端間也照樣可以通訊。通訊端有兩種不同的類型:流通訊端和資料通訊端。
通訊端工作原理:
要通過互連網進行通訊,你至少需要一對通訊端,其中一個運行於客戶機端,我們稱之為ClientSocket,另一個運行於伺服器端,我們稱之為ServerSocket。
根據串連啟動的方式以及本地通訊端要串連的目標,通訊端之間的串連過程可以分為三個步驟:伺服器監聽,用戶端請求,串連確認。
所謂伺服器監聽,是伺服器端通訊端並不定位具體的用戶端通訊端,而是處於等待串連的狀態,即時監控網路狀態。
所謂用戶端請求,是指由用戶端的通訊端提出串連請求,要串連的目標是伺服器端的通訊端。為此,用戶端的通訊端必須首先描述它要已連線的服務器的通訊端,指出伺服器端通訊端的地址和連接埠號碼,然後就向伺服器端通訊端提出串連請求。
所謂串連確認,是指當伺服器端通訊端監聽到或者說接收到用戶端通訊端的串連請求,它就響應用戶端通訊端的請求,建立一個新的線程,把伺服器端通訊端的描述發給用戶端,一旦用戶端確認了此描述,串連就建立好了。而伺服器端通訊端繼續處於監聽狀態,繼續接收其他用戶端通訊端的串連請求。
C#中的通訊端編程執行個體:
通過向大家簡單的介紹通訊端的基本概念和實現通訊端編程的基本原理,我想大家對通訊端編程已有了初步的瞭解。不過,上面介紹的僅僅是基本概念和原理,要真正運用還是需要一定的工作的。對基本概念和原理的真正理解的最好方法莫過於自己動手做一個執行個體,下面我就向大家介紹一個很好的用C#實現通訊端編程的執行個體――聊天室程式。
本程式是基於C/S(伺服器/用戶端)構架的,程式包含一個伺服器端的應用程式和一個用戶端的應用程式。首先,在伺服器上運行伺服器端的應用程式,該程式一運行就開始伺服器監聽。然後,在客戶機上就可以開啟用戶端的應用程式。程式開啟後可以與伺服器端應用程式進行串連,即進行用戶端請求。在串連確認後,用戶端使用者可以和其他的用戶端使用者進行聊天。用戶端人數沒有限制,同時還支援“悄悄話”聊天模式,支援聊天記錄。所以這是一個學習通訊端編程的相當不錯的例子。而且,程式中為了處理每個用戶端的資訊還用到了多線程機制。在每個用戶端與伺服器端串連成功後,它們之間就建立一個線程。這樣運用了多線程之後,用戶端之間就不會相互影響,即使其中一個出了錯誤也不會影響到另一個。
下面,我就向大傢具體介紹該執行個體:
伺服器端程式:
1. 開啟VS.net,建立一個C#的模板為“Windows 應用程式”的項目,不妨命名為“ChatServer”。
2. 布置介面。只需在介面上添加一個ListBox控制項即可,該控制項主要用於顯示用戶端的使用者的一些資訊的。圖象如下:
3. 伺服器端程式的代碼編寫。
對於伺服器端,主要的作用是監聽用戶端的串連請求並確認其請求。程式一開始便開啟一個StartListening()線程。
private void StartListening() { listener = new TcpListener(listenport); listener.Start(); while (true) { try { Socket s = listener.AcceptSocket(); clientsocket = s; clientservice = new Thread(new ThreadStart(ServiceClient)); clientservice.Start(); } catch(Exception e) { Console.WriteLine(e.ToString() ); } } } |
該線程是一直處於運行狀態的。當伺服器端接收到一個來自用戶端的串連請求後,它就開啟一個ServiceClient()線程來服務用戶端。當一個串連被建立後,每個用戶端就被賦予一個屬於它自己的通訊端。同時,一個Client類的對象被建立。該對象包含了用戶端的一些相關資訊,該資訊被儲存在一個數組列表中。
Client類如下:
using System; using System.Threading; namespace ChatServer { using System.Net.Sockets; using System.Net; /// /// Client 的摘要說明。 /// public class Client { private Thread clthread; private EndPoint endpoint; private string name; private Socket sock; public Client(string _name, EndPoint _endpoint, Thread _thread, Socket _sock) { // TODO: 在此處添加建構函式邏輯 clthread = _thread; endpoint = _endpoint; name = _name; sock = _sock; } public override string ToString() { return endpoint.ToString()+ " : " + name; } public Thread CLThread { get{return clthread;} set{clthread = value;} } public EndPoint Host { get{return endpoint;} set{endpoint = value;} } public string Name { get{return name;} set{name = value;} } public Socket Sock { get{return sock;} set{sock = value;} } } } |
程式的主體部分應是ServiceClient()函數。該函數是一個獨立的線程,其主要部分是一個while迴圈。在迴圈體內,程式處理各種用戶端命令。伺服器端接收來自用戶端的以ASCII碼給出的字串,其中包含了一個“|”形式的分隔字元。字串中“|”以前的部分就是具體的命令,包括CONN、CHAT、PRIV、GONE四種類型。CONN命令建立一個新的用戶端串連,將現有的使用者列表發送給新使用者並告知其他使用者有一個新使用者加入。CHAT命令將新的資訊發送給所有使用者。PRIV命令將悄悄話發送給某個使用者。GONE命令從使用者列表中除去一個已離開的使用者並告知其他的使用者某某已經離開了。同時,GONE命令可以設定布爾型的變數keepalive為false從而結束與用戶端串連的線程。ServiceClient()函數如下:
private void ServiceClient() { Socket client = clientsocket; bool keepalive = true; while (keepalive) { Byte[] buffer = new Byte[1024]; client.Receive(buffer); string clientcommand = System.Text.Encoding.ASCII.GetString(buffer); string[] tokens = clientcommand.Split(new Char[]{'|'}); Console.WriteLine(clientcommand); if (tokens[0] == "CONN") { for(int n=0; n { Client cl = (Client)clients[n]; SendToClient(cl, "JOIN|" + tokens[1]); } EndPoint ep = client.RemoteEndPoint; Client c = new Client(tokens[1], ep, clientservice, client); clients.Add(c); string message = "LIST|" + GetChatterList() +"\r\n"; SendToClient(c, message); lbClients.Items.Add(c); } if (tokens[0] == "CHAT") { for(int n=0; n { Client cl = (Client)clients[n]; SendToClient(cl, clientcommand); } } if (tokens[0] == "PRIV") { string destclient = tokens[3]; for(int n=0; n { Client cl = (Client)clients[n]; if(cl.Name.CompareTo(tokens[3]) == 0) SendToClient(cl, clientcommand); if(cl.Name.CompareTo(tokens[1]) == 0) SendToClient(cl, clientcommand); } } if (tokens[0] == "GONE") { int remove = 0; bool found = false; int c = clients.Count; for(int n=0; n { Client cl = (Client)clients[n]; SendToClient(cl, clientcommand); if(cl.Name.CompareTo(tokens[1]) == 0) { remove = n; found = true; lbClients.Items.Remove(cl); } } if(found) clients.RemoveAt(remove); client.Close(); keepalive = false; } } } |
這樣,伺服器端程式就基本完成了。(其他略次要的代碼可以參見原始碼中的Form1.cs檔案)程式運行圖示如下: |
|
|
用戶端程式:
1. 開啟VS.net,建立一個C#的模板為“Windows 應用程式”的項目,不妨命名為“ChatClient”。
2. 布置介面。往介面上添加一個ListBox控制項(用於顯示使用者列表),一個RichTextBox控制項(用於顯示聊天訊息以及系統訊息),一個TextBox控制項(用於發送訊息),一個CheckBox控制項(確定是否為悄悄話),一個StatusBar控制項以及四個Button控制項(分別為“串連”、“中斷連線”、“開始記錄”、“發送”)。各個控制項的屬性設定可以參見原始碼中的具體設定,這裡從略。介面設計好後的圖象如下:
3. 用戶端程式的代碼編寫。
當用戶端試圖和伺服器端進行串連時,一個串連必須建立而且得向伺服器端進行註冊。 EstablishConnection()函數運用一個TcpClient來和伺服器端取得串連,同時建立一個NetworkStream來發送訊息。還有,連接埠號碼和伺服器端的是保持一致的,均為5555。EstablishConnection()函數如下:
private void EstablishConnection() { statusBar1.Text = "正在串連到伺服器"; try { clientsocket = new TcpClient(serveraddress,serverport); ns = clientsocket.GetStream(); sr = new StreamReader(ns); connected = true; } catch (Exception) { MessageBox.Show("不能串連到伺服器!","錯誤", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); statusBar1.Text = "已中斷連線"; } } |
在和伺服器端串連成功後,程式就用RegisterWithServer()函數向伺服器端發送一個CONN命令。該命令先是發送該使用者的名稱,然後從伺服器端獲得其他所有使用者的列表,所有使用者列表是在ListBox控制項中顯示的。該函數如下:
private void RegisterWithServer() { try { string command = "CONN|" + ChatOut.Text; Byte[] outbytes = System.Text.Encoding.ASCII.GetBytes(command.ToCharArray()); ns.Write(outbytes,0,outbytes.Length); string serverresponse = sr.ReadLine(); serverresponse.Trim(); string[] tokens = serverresponse.Split(new Char[]{'|'}); if(tokens[0] == "LIST") { statusBar1.Text = "已串連"; btnDisconnect.Enabled = true; } for(int n=1; n lbChatters.Items.Add(tokens[n].Trim(new char[]{'\r','\n'})); this.Text = clientname + ":已串連到伺服器"; } catch (Exception) { MessageBox.Show("註冊時發生錯誤!","錯誤", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } } |
在此之後,當然就是使用者之間的聊天了,由ReceiveChat()函數來完成。該函數是一個獨立的線程,它處理所有使用者獲得的訊息和使用者發送的訊息。它主要處理了CHAT、PRIV、JOIN、GONE、QUIT等命令,處理的方法和伺服器端的類似。具體函數實現如下:
private void ReceiveChat() { bool keepalive = true; while (keepalive) { try { Byte[] buffer = new Byte[2048]; ns.Read(buffer,0,buffer.Length); string chatter = System.Text.Encoding.ASCII.GetString(buffer); string[] tokens = chatter.Split(new Char[]{'|'}); if (tokens[0] == "CHAT") { rtbChatIn.AppendText(tokens[1]); if(logging) logwriter.WriteLine(tokens[1]); } if (tokens[0] == "PRIV") { rtbChatIn.AppendText("Private from "); rtbChatIn.AppendText(tokens[1].Trim() ); rtbChatIn.AppendText(tokens[2] + "\r\n"); if(logging) { logwriter.Write("Private from "); logwriter.Write(tokens[1].Trim() ); logwriter.WriteLine(tokens[2] + "\r\n"); } } if (tokens[0] == "JOIN") { rtbChatIn.AppendText(tokens[1].Trim() ); rtbChatIn.AppendText(" has joined the Chat\r\n"); if(logging) { logwriter.WriteLine(tokens[1]+" has joined the Chat"); } string newguy = tokens[1].Trim(new char[]{'\r','\n'}); lbChatters.Items.Add(newguy); } if (tokens[0] == "GONE") { rtbChatIn.AppendText(tokens[1].Trim() ); rtbChatIn.AppendText(" has left the Chat\r\n"); if(logging) { logwriter.WriteLine(tokens[1]+" has left the Chat"); } lbChatters.Items.Remove(tokens[1].Trim(new char[]{'\r','\n'})); } if (tokens[0] == "QUIT") { ns.Close(); clientsocket.Close(); keepalive = false; statusBar1.Text = "伺服器端已停止"; connected= false; btnSend.Enabled = false; btnDisconnect.Enabled = false; } } catch(Exception){} } } |
通過以上的一些函數,用戶端程式之間就可以進行自由地聊天了,各個使用者之間還可以互相發送悄悄話。所以程式已經實現了聊天室的準系統了,不過最後各個使用者還要正常地退出,那就要用到QuitChat()函數了。該函數的具體實現如下:
private void QuitChat() { if(connected) { try { string command = "GONE|" + clientname; Byte[] outbytes = System.Text.Encoding.ASCII.GetBytes(command.ToCharArray()); ns.Write(outbytes,0,outbytes.Length); clientsocket.Close(); } catch(Exception) { } } if(logging) logwriter.Close(); if(receive != null && receive.IsAlive) receive.Abort(); this.Text = "用戶端"; } |
到此為止,用戶端程式的主要部分都已經介紹完畢。還有一些按鈕控制項的訊息處理函數可以參見原始碼。同時,程式中還有一個聊天記錄功能,該功能和現在流行的聊天軟體的記錄功能類似。不過限於篇幅,在這裡就不一一介紹了,有興趣的讀者可以研究一下本文後面的原始碼。
這樣,用戶端程式就完成了。程式運行圖示如下:
總結:
本文向大家初步介紹了通訊端的基本概念和實現通訊端編程的基本原理,還通過一個很好的執行個體向大家展示了在C#下進行通訊端編程的實現方法和一些編程技巧。從中,我們不難發現運用C#進行通訊端編程乃至網路編程有許多優越之處。執行個體程式實現的思路清晰明了而且通俗易懂,是一個相當不錯的例子,希望各位能好好研讀。同時還希望大家能進一步完善該程式,使之功能更強大、介面更友好。