C# Socket多執行緒程式設計例項-聊天程式
C#是微軟隨著VS.net新推出的一門語言。它作為一門新興的語言,有著C++的強健,又有著VB等的RAD特性。而且,微軟推出C#主要的目的是為了 對抗Sun公司的Java。大家都知道Java語言的強大功能,尤其在網路程式設計方面。於是,C#在網路程式設計方面也自然不甘落後於人。本文就向大家介紹一下 C#下實現套接字(Sockets)程式設計的一些基本知識,以期能使大家對此有個大致瞭解。首先,我向大家介紹一下套接字的概念。 套接字基本概念: 套接字是通訊的基石,是支援TCP/IP協議的網路通訊的基本操作單元。可以將套接字看作不同主機間的程序進行雙向通訊的端點,它構成了單個主機內及整個 網路間的程式設計介面。套接字存在於通訊域中,通訊域是為了處理一般的執行緒通過套接字通訊而引進的一種抽象概念。套接字通常和同一個域中的套接字交換資料(數 據交換也可能穿越域的界限,但這時一定要執行某種解釋程式)。各種程序使用這個相同的域互相之間用 套接字可以根據通訊性質分類,這種性質對於使用者是可見的。應用程式一般僅在同一類的套接字間進行通訊。不過只要底層的通訊協議允許,不同型別的套接字間也照樣可以通訊。套接字有兩種不同的型別:流套接字和資料報套接字。 套接字工作原理: 要通過網際網路進行通訊,你至少需要一對套接字,其中一個運行於客戶機端,我們稱之為ClientSocket,另一個運行於伺服器端,我們稱之為ServerSocket。 根據連線啟動的方式以及本地套接字要連線的目標,套接字之間的連線過程可以分為三個步驟:伺服器監聽,客戶端請求,連線確認。 所謂伺服器監聽,是伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態。 所謂客戶端請求,是指由客戶端的套接字提出連線請求,要連線的目標是伺服器端的套接字。為此,客戶端的套接字必須首先描述它要連線的伺服器的套接字,指出伺服器端套接字的地址和埠號,然後就向伺服器端套接字提出連線請求。 所謂連線確認,是指當伺服器端套接字監聽到或者說接收到客戶端套接字的連線請求,它就響應客戶端套接字的請求,建立一個新的執行緒,把伺服器端套接字的描述 發給客戶端,一旦客戶端確認了此描述,連線就建立好了。而伺服器端套接字繼續處於監聽狀態,繼續接收其他客戶端套接字的連線請求。 通過向大家簡單的介紹套接字的基本概念和實現套接字程式設計的基本原理,我想大家對套接字程式設計已有了初步的瞭解。不過,上面介紹的僅僅是基本概念和原理,要真 正運用還是需要一定的工作的。對基本概念和原理的真正理解的最好方法莫過於自己動手做一個例項,下面我就向大家介紹一個很好的用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、QU99v等命令,處理的方法和伺服器端的類 似。具體函式實現如下:
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] == "QU99v") { 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#進行套接字程式設計乃至網路程式設計有許多優越之處。例項程式實現的思路清晰明瞭而且通俗易懂,是一個相當不錯的例子,希望各位能好好 研讀。同時還希望大家能進一步完善該程式,使之功能更強大、介面更友好。