1. 程式人生 > >C#實現的可複用Socket接收/傳送共享緩衝區類

C#實現的可複用Socket接收/傳送共享緩衝區類

在Socket的接收/傳送方法:Send()、BeginSend()、Receive()、BeginReceive()中,第一個引數是位元組數陣列,表示當前接收資料區或需要傳送的資料。普通Socket應用中,往往是接收/傳送時建立陣列,使用後陣列空間由託管堆回收(Socket關閉後其關聯的緩衝區情況類似)。顯然,頻繁建立接收/傳送緩衝區將在託管堆上留下很多的記憶體碎塊,影響系統性能。 使用Socket非同步調事件引數類SocketAsyncEventArgs時考慮了上述情況,基本構思為:自定義一個緩衝區管理類如BufferManager,開闢一個大的、可重用接收/傳送收緩衝區,用於SendAsync()、ReceiveAsync()等方法,之前使用SetBuffer()和屬性OffSet、Count設定緩衝區空間。 事實上,在.NET 2.0平臺上的Socket傳統APM(非同步程式設計模型)中仍然可用該這個技術。下面是修改的BufferManager類:
public sealed class BufferManager
{
// ... 全部欄位為private,型別和名稱見建構函式

public BufferManager(int maxSessionCount, int receivevBufferSize, int sendBufferSize)
{
m_maxSessionCount = maxSessionCount; // 最大可連線客戶端數, int
m_receiveBufferSize = receivevBufferSize; // 接收緩衝區大小, int
m_sendBufferSize = sendBufferSize; // int

m_bufferBlockIndex = 0; // 當前未用緩衝區塊索引號, int
m_bufferBlockIndexStack = new Stack(); // 可重用緩衝區塊索引號, Stack<int>泛型

m_receiveBuffer = new byte[m_receiveBufferSize * m_maxSessionCount]; // 接收緩衝區大小
m_sendBuffer = new byte[m_sendBufferSize * m_maxSessionCount];
}

public int ReceiveBufferSize
{
get { return m_receiveBufferSize; }
}

public int SendBufferSize
{
get { return m_sendBufferSize; }
}

public byte[] ReceiveBuffer
{
get { return m_receiveBuffer; }
}

public byte[] SendBuffer
{
get { return m_sendBuffer; }
}

public void FreeBufferBlockIndex(int bufferBlockIndex) // 回收塊索引號
{
if (bufferBlockIndex == -1)
{
return;
}

lock (this)
{
m_bufferBlockIndexStack.Push(bufferBlockIndex);
}
}

public int GetBufferBlockIndex() // 獲取可用緩衝區塊索引號
{
lock (this)
{
int blockIndex = -1;

if (m_bufferBlockIndexStack.Count > 0) // 有用過釋放的緩衝塊
{
blockIndex = m_bufferBlockIndexStack.Pop();
}
else
{
if (m_bufferBlockIndex < m_maxSessionCount) // 有未用緩衝區塊
{
blockIndex = m_bufferBlockIndex++;
}
}

return blockIndex;
}
}

public int GetReceivevBufferOffset(int bufferBlockIndex)
{
if (bufferBlockIndex == -1) // 沒有使用共享塊
{
return 0; // 表示新建緩衝區,偏移為0
}

return bufferBlockIndex * m_receiveBufferSize; // 接收塊的偏移(陣列起始下標)
}

public int GetSendBufferOffset(int bufferBlockIndex)
{
if (bufferBlockIndex == -1) // 沒有使用共享塊
{
return 0;
}

return bufferBlockIndex * m_sendBufferSize; // 傳送塊偏移(陣列起始下標)
}

public void Clear()
{
lock (this)
{
m_bufferBlockIndexStack.Clear();
m_receiveBuffer = null;
m_sendBuffer = null;
}
}
}
上述程式碼中,m_maxSessionCount是Socket伺服器最大的可連客戶端Socket數,BufferManager建構函式要求該數以及接收和傳送緩衝區的大小,從而建立兩個大的、可重複使用共享緩衝區。 具體使用步驟如下:
  1. 建立一個BufferManager物件 m_bufferManager
  2. 獲取緩衝區塊索引號:m_bufferBlockIndex = m_bufferManager.GetBufferBlockIndex()
  3. 非同步接收:先計算出緩衝區偏移地址,然後開始接收
  4. 非同步傳送:先考慮傳送串長度,然後決定是否使用緩衝區,見隨後的程式碼
  5. 不使用塊索引號時:m_bufferManager.FreeBufferBlockIndext(m_bufferBlockIndex)回收
下面是申請一個緩衝區索引號的程式碼示例:
m_bufferBlockIndex = bufferManager.GetBufferBlockIndex();
if (m_bufferBlockIndex == -1) // 沒有空塊, 新建接收/傳送緩衝區
{
m_receiveBuffer = new byte[m_bufferManager.ReceiveBufferSize];
m_sendBuffer = new byte[m_bufferManager.SendBufferSize];
}
else // 有空的緩衝區塊,直接引用該塊
{
m_receiveBuffer = m_bufferManager.ReceiveBuffer;
m_sendBuffer = m_bufferManager.SendBuffer;
}
下面是Socket非同步接收資料的程式碼示例:
int bufferOffset = m_bufferManager.GetReceivevBufferOffset(m_bufferBlockIndex);  // 計算開始地址
m_socket.BeginReceive(m_receiveBuffer, bufferOffset, m_bufferManager.ReceiveBufferSize,
SocketFlags.None, this.EndReceiveDatagram, this);
下面是Socket非同步傳送字串datagramText的程式碼示例:
int byteLength = Encoding.ASCII.GetByteCount(datagramText);
if (byteLength <= m_bufferManager.SendBufferSize) // 可以用共享緩衝區
{
int bufferOffset = m_bufferManager.GetSendBufferOffset(m_bufferBlockIndex); // 計算開始地址
Encoding.ASCII.GetBytes(datagramText, 0, byteLength, m_sendBuffer, bufferOffset);
m_socket.BeginSend(m_sendBuffer, bufferOffset, byteLength, SocketFlags.None,
this.EndSendDatagram, this);
}
else // 不能使用共享緩衝區
{
byte[] data = Encoding.ASCII.GetBytes(datagramText); // 獲得資料位元組陣列
m_socket.BeginSend(data, 0, data.Length, SocketFlags.None, this.EndSendDatagram, this);
}
在資料傳送時,如果傳送緩衝區大小比實際傳送的包長度大,上述非同步傳送可以使用BufferManager公共緩衝區。否則,需要新建一個傳送緩衝區(位元組陣列)。此外,用共享緩衝區分多次傳送長資料包也是一個可考慮的方案,但實現比較複雜(留待以後解決)。資料接收則直接使用BufferManager,因為長資料包由Socket自動分多次接收,不需要考慮分包及包接收順序等問題。另一個需要注意的是,獲取的緩衝區索引塊號要記住回收它們。 基於事件驅動的SocketAsyncEventArgs效能的改善,不僅與使用共享緩衝區的技術相關,更與其在完成埠(IOCP)共享SocketAsyncEventArgs物件有關,該物件可重複使用。而在傳統的非同步Socket處理時,總會建立一個IAsyncResult物件,該物件不可重複使用,且必須呼叫AsyncWaitHandle.Close()釋放資源。顯然,共享緩衝區技術只稍稍改善了應用系統的效能,沒有從根本上消除Socket的APM的缺陷。 上述緩衝區類提供了一個Socket可重複使用的的接收/傳送緩衝區技術方案,具體實現可以參看拙文可擴充套件多執行緒非同步Socket伺服器框架EMTASS 2.0和版本歷史2.1中的簡介及原始碼資源下載地址。