[C#] 撰寫非同步方法 TCP socket #2

URL Link //n.sfs.tw/11626

2017-08-14 21:23:09 By 張○○

此文分為二個部分

[C#] 撰寫非同步方法 TCP socket #1

[C#] 撰寫非同步方法 TCP socket #2

三、伺服端程式(續)

上一篇寫到伺服器的 Listen 函式 Server(),當Socket一開始接收到Client的連線要求時,由於BeginAccept定義了非同步的方法:OnClientConnect

接下來我們要來研究實作 OnClientConnect() 函式

// 定義最大Clients 常數
const int MAX_CLIENTS=20;

// 定義每個連線的工作類別域變數 Socket陣列
private Socket[] workerSocket = new Socket[MAX_CLIENTS];

public void OnClientConnect(IAsyncResult asyn)
{
  try{
    //宣告並定義空頻道的ID=-1,表示無空頻道資料
    int Empty_channel_ID = -1;
    //若主Socket為空則跳出
    if (mainSocket == null) return;

    Socket temp_Socket = mainSocket.EndAccept(asyn);
    //取得遠端節點的EndPoint
    EndPoint RemoteEP = temp_Socket.RemoteEndPoint;
    Empty_channel_ID = FindEmptyChannel();
    if (Empty_channel_ID == -1) throw new NoSocketAvailableException();
    //將方才暫存的Socket交給空的 Socket接收
    workerSocket[Empty_channel_ID] = temp_Socket;
    //將暫存的Socket設為空
    temp_Socket = null;
    WaitForData(workerSocket[Empty_channel_ID]);
  }
  catch (ObjectDisposedException) { …處理已釋放記憶體的資源例外處理略... }
  catch (SocketException) { …因TCP Socket造成的例外處理略... }
  //自定了一個錯誤的Exception類型,當找不到空頻道表示所有頻道都被佔用
  catch (NoSocketAvailableException err) { …無可用的頻道例外處理略... }
  finally{
    //將方才關閉的主要Socket重新接收新的連線
    mainSocket.BeginAccept(new AsyncCallback(OnClientConnect), null);
  }
}

第15行 將主要正在 listening 的主要Socket轉交給另一個臨時的Socket變數,並且結束接受此一客戶端的連線

第18行 傳回空頻道,函式FindEmptyChannel() 列於後,主要是找尋未佔用的Socket(Channel)

第19行 自定了一個錯誤的Exception類型,當找不到空頻道表示所有頻道都被佔用,則會丟出一個 NoSocketAvailableException() 的錯誤

第24行,重新叫用 WaitForData 開始接收資料

//自定錯誤類型,當所有頻道都被佔用時叫用,其中只覆寫了Exception.Message 屬性
public class NoSocketAvailableException : System.Exception
{
    new public string Message = "所有的頻道都已佔滿,請先釋放其他的頻道";
}

// 尋找第一個空頻道的函式,傳回Channel ID或是-1 全被佔滿
private int FindEmptyChannel()
{
   for (int i = 0; i < MAX_CLIENTS; i++)
   {
        if (m_workerSocket[i] == null || !m_workerSocket[i].Connected)
        {
          return i;
        }
    }
    return -1;
}

上面的OnClientConnect()函式中,當Server 接受 Client 的連線後,有指定當該Socket有資料傳入時,要叫用接受資料WaitForData()函式:

//宣告AsyncCallback類別的變數 pfnWorkerCallBack
public AsyncCallback pfnWorkerCallBack;
public void WaitForData(System.Net.Sockets.Socket soc)
{
  try{
    //當pfnWorkerCallBack物件尚未實體化時,進行實體化
    if (pfnWorkerCallBack == null)
    {
      pfnWorkerCallBack = new AsyncCallback(OnDataReceived);
    }
    //自行定義的型別 SocketPacket,附於此小節尾,內容只有一個Socket類和一個int。
    SocketPacket theSocPkt = new SocketPacket();
    //指定此一建立連線之Socket soc 給定義的 theSocPkt
    theSocPkt.m_currentSocket = soc;

    soc.BeginReceive(theSocPkt.dataBuffer, 0, theSocPkt.dataBuffer.Length, SocketFlags.None, pfnWorkerCallBack, theSocPkt);
  }
  catch (SocketException) { …例外處理略... }
 }

//自型定義的物件封包的類別
public class SocketPacket
{
    //目前Activating之Socket
    public System.Net.Sockets.Socket m_currentSocket;
    public byte[] dataBuffer = new byte[1500]; //接受資料陣列
}

第9行,假設已連線的RCC客戶端要傳送資料給RCS時,所指定的回呼函式-OnDataReceive,這也是C#中的delagate型別

第17行,定義當Socket 接收到資料時要交給BeginReceive() 這個函數。參數第一個為存放的位置,為byte[],傳入的資料會置於此陣列中;第二參數為啟始位置、 第三為一次置入的長度;第四為封包的旗標,例如標記為廣播封包 之類,第五參數為開始 接受資料時叫用的回呼函式,由於pfnWorkerCallBack已實體化指定OnDataReceived為回呼函式, 所以當有 客戶端將資料傳進來時,OnDataReceived函式會被叫用;第六為狀態的參數, 這個是用戶自訂,當回呼函式OnDataReceived被叫用 時,此參數的值會被傳遞到OnDataReceived, 因此雖然SocketPacket theSocPkt宣告的是區域變數,但是回呼函式仍會曉得是哪一個Socket在傳送資料。

當建立連線的 Socket 有資料傳入時,要叫用的回呼函式為OnDataReceived() 。

此回呼函式有一參數類型為IasyncResult,因為該類型是.NET中很特別的類型,稱之為介面(Interface),簡單的說,介面算是一個只有定義沒有實作項的類別。由於BeginReceive 方法被叫用時的傳入的最未個參數是自定的theSocPkt物件,會轉型成IAsyncResult.AsyncState 傳入,因此宣告的IasyncResult類變數asyn,則asyn.AsyncState 可取得符合或包含非同步作業資訊的使用者定義的物件。

下方將實作出OnDataReceived() 函式,由於此函式必需將收到的資料分析及編碼處理,所以原函式稍長,故僅節錄出和TCP非同步連線有關之部分於下:

public void OnDataReceived(IAsyncResult asyn)
{
  try{
    SocketPacket socketData = (SocketPacket)asyn.AsyncState;//取得接受的資料
    string msg = "";//宣告及定義訊息字串
    int iRx = 0;//宣告及定義訊息長度

    iRx = socketData.m_currentSocket.EndReceive(asyn);
    byte[] databuff = socketData.dataBuffer;
    msg = Encoding.UTF8.GetString(databuff);
    // ...分析處理部分略,主要msg訊息的利用和重組...

    WaitForData(socketData.m_currentSocket);
  }
  catch (SocketException) { …例外處理略... }
}

第8行,叫用 EndReceive 完成指定的非同步接收作業, 參數asyn 識別要完成的非同步接收作業,並要從其中擷取最終結果。

第9行,將取出的部分轉成 UTF8的編碼方式,有關編碼方式轉換可依需求自行修改。

第13行,重新叫用 WaitForData 重新開始接收資料

以上幾個 函式就完成了 非同步方法 TCP socket ,這裡將其整理一下:

Server() 啟動監聽

OnClientConnect(IAsyncResult) 當客戶端建立連線時叫用

WaitForData(Socket) 建立連線等待資料傳入

OnDataReceived(IAsyncResult)  已連線連線並有資料傳入時叫用

FindEmptyChannel() 尋找未用的頻道

祝各位使用愉快

參考資料

[1] Asynchronous Socket Programming in C#

[2] http://www.codeguru.com/csharp/csharp/cs_network/sockets/article.php/c7695/#more


原文 2010-04-02 00:52:54