幾天前在博問中看到一個C# Socket問題,就想到筆者2004年做的一個省級交通流量接收伺服器項目,當時的基本求如下: 接收自動觀測裝置通過無線網卡、Internet和Socket上報的交通量資料包 全年365*24啟動並執行自動觀測裝置5分鐘上報一次觀測資料,每筆記錄約2K大小 規劃全省將有100個左右的自動觀測裝置(截止2008年10月還只有30個)
當時,VS2003才發布年多,筆者也是接觸C#不久。於是Google了國內國外網,希望找點應用C#解決Socket通訊問題的思路和代碼。最後,找 到了兩篇協助最大的文章:一篇是國人寫的Socket接收器架構,應用了獨立的用戶端Socket會話(Session)概念,給筆者提供了一個接收服務 器的總體架構思路;另一篇是美國人寫的,提出了多線程、分段接收資料包的技術方案,描述了多線程、非同步Socket的許多實現細節,該文堅定了筆者採用多 線程和非同步方式處理Socket接收器的技術路線。
具體實現和測試時筆者還發現,在Internet環境下的Socket應用中,需要系統有極強的容錯能力:沒有辦法控制異常,就必須允許它們存在(附加源 代碼中可以看到,try{}catch{}語句較多)。對此,筆者設計了一個專門的檢查和清理線程,完成無效或逾時會話的清除和資源釋放工作。
依稀記得,國內架構作者的名稱空間有ibm,認為是IBM公司職員,通過郵件後才知道其人在深圳。筆者向他請教了幾個問題,相互探討了幾個技術關鍵點。可 惜,現在再去找,已經查不到原文和郵件了。只好藉此機會,將本文獻給這兩個素未謀面的技術高人和同行,也盼望拙文或源碼能給讀者一點有用的啟發和協助。
1、主要技術思路
整個系統由三個核心線程組成,並由.NET線程池統一管理: 偵聽用戶端串連請求線程:ListenClientRequest(),迴圈偵聽用戶端 串連請求。如果有,檢測該用戶端IP,看是否是同一觀測裝置,然後建立一個用戶端TSession對象,並通過Socket非同步呼叫方法 BeginReceive()接收資料包、EndReceive()處理資料包 資料包處理線程:HandleDatagrams(),迴圈檢測資料包隊列_datagramQueue,完成資料包解析、判斷類型、儲存等工作 用戶端狀態檢測線程:CheckClientState(),迴圈檢查用戶端工作階段表_sessionTable,判斷會話對象是否有效,設定逾時會話關閉標誌,清楚無效會話對象及釋放其資源
2、主要類簡介
系統主要由3個類組成: TDatagramReceiver(資料包接收伺服器):系統的核心進程類,建立Socket串連、處理與儲存資料包、清理系統資源,該類提供全部的public屬性和方法 TSession(用戶端工作階段):由每個用戶端的Socket對象組成,有自己的資料緩衝區,清理線程根據該對象的最近會話時間判斷是否逾時 TDatagram(資料包類):判斷資料包類別、解析資料包
3、關鍵函數和代碼
下面簡介核心類TDatagramReceiver的關鍵實現代碼。
3.1 系統啟動
系統啟動方法StartReceiver()首先清理資源、建立資料庫連接、初始化若干計數值,然後建立伺服器端偵聽Socket對象,最後調用靜態方法ThreadPool.QueueUserWorkItem()線上程池中建立3個核心處理線程。 Code
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->/// <summary>
/// 啟動接收器
/// </summary>
public bool StartReceiver()
{
try
{
_stopReceiver = true;
this.Close();
if (!this.ConnectDatabase()) return false;
_clientCount = 0;
_datagramQueueCount = 0;
_datagramCount = 0;
_errorDatagramCount = 0;
_exceptionCount = 0;
_sessionTable = new Hashtable(_maxAllowClientCount);
_datagramQueue = new Queue<TDatagram>(_maxAllowDatagramQueueCount);
_stopReceiver = false; // 迴圈中均要該標誌
if (!this.CreateReceiverSocket()) //建立伺服器端 Socket 對象
{
return false;
}
// 偵聽用戶端串連請求線程, 使用委託推斷, 不建 CallBack 對象
if (!ThreadPool.QueueUserWorkItem(ListenClientRequest))
{
return false;
}
// 處理資料包隊列線程
if (!ThreadPool.QueueUserWorkItem(HandleDatagrams))
{
return false;
}
// 檢查客戶工作階段狀態, 長時間未通訊則清除該對象
if (!ThreadPool.QueueUserWorkItem(CheckClientState))
{
return false;
}
_stopConnectRequest = false; // 啟動接收器,則自動允許串連
}
catch
{
this.OnReceiverException();
_stopReceiver = true;
}
return !_stopReceiver;
}
下面是建立偵聽Socket對象的方法代碼。 Code
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->/// <summary>
/// 建立接收伺服器的 Socket, 並偵聽用戶端串連請求
/// </summary>
private bool CreateReceiverSocket()
{
try
{
_receiverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_receiverSocket.Bind(new IPEndPoint(IPAddress.Any, _tcpSocketPort)); // 綁定連接埠
_receiverSocket.Listen(_maxAllowListenQueueLength); // 開始監聽
return true;
}
catch
{
this.OnReceiverException();
return false;
}
}
3.2 偵聽用戶端串連請求
伺服器端迴圈等待用戶端串連請求。一旦有請求,先判斷用戶端串連數是否超限,接著檢測該用戶端IP地址,一切正常後建立TSession對象,並調用非同步方法呼叫接收用戶端Socket資料包。
代碼中,Socket讀到資料時的回調AsyncCallback委託方法EndReceiveData()完成資料接收工作,正常情況下啟動另一個非同步BeginReceive()調用。
.NET中,每個非同步方法呼叫都有自己的獨立線程,非同步處理其實也基於多線程機制的。下面代碼中的非同步套非同步呼叫,既佔用較大的系統資源,也給處理帶來意想不到的結果,更是出現異常時難以控制和處理的關鍵所在。 Code
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->/// <summary>
/// 迴圈偵聽用戶端請求,由於要用線程池,故帶一個參數
/// </summary>
private void ListenClientRequest(object state)
{
Socket client = null;
while (!_stopReceiver)
{
if (_stopConnectRequest) // 停止用戶端串連請求
{
if (_receiverSocket != null)
{
try
{
_receiverSocket.Close(); // 強制關閉接收器
}
catch
{
this.OnReceiverException();
}
finally
{
// 必須為 null,否則 disposed 對象仍然存在,將引發下面的錯誤
_receiverSocket = null;
}
}
continue;
}
else
{
if (_receiverSocket == null)
{
if (!this.CreateReceiverSocket())
{
continue;
}
}
}
try
{
if (_receiverSocket.Poll(_loopWaitTime, SelectMode.SelectRead))
{
// 頻繁關閉、啟動時,這裡容易產生錯誤(提示通訊端只能有一個)
client = _receiverSocket.Accept();
if (client != null && client.Connected)
{
if (this._clientCount >= this._maxAllowClientCount)
{
this.OnReceiverException();
try
{
client.Shutdown(SocketShutdown.Both);
client.Close();
}
catch { }
}
else if (CheckSameClientIP(client)) // 已存在該 IP 地址
{
try
{
client.Shutdown(SocketShutdown.Both);
client.Close();
}
catch { }
}
else
{
TSession session = new TSession(client);
session.LoginTime = DateTime.Now;
lock (_sessionTable)
{
int preSessionID = session.ID;
while (true)
{
if (_sessionTable.ContainsKey(session.ID)) // 有可能重複該編號
{
session.ID = 100000 + preSessionID;
}
&nbs