在Socket的接收/发送方法:Send()、BeginSend()、Receive()、BeginReceive()中,第一个参数是字节数数组,表示当前接收数据区或需要发送的数据。普通Socket应用中,往往是接收/发送时创建数组,使用后该数组空间由托管堆回收。显然,频繁接收/发送将在托管堆上创建很多的内存碎块,影响系统性能。
使用Socket异步调事件参数类SocketAsyncEventArgs时考虑了上述情况,基本构思为:自定义一个缓冲区管理类如BufferManager,开辟一个大的、可重用接收/发送收缓冲区,用于SendAsync()、ReceiveAsync()等方法,之前使用SetBuffer()和属性OffSet、Count设定缓冲区空间。
事实上,在.NET 2.0平台上的Socket传统APM(异步编程模型)中,仍然可以使用这个技术。下面是修改的BufferManager类:
public sealed class BufferManager
{
// ...成员字段见其构造函数
public BufferManager(int maxSessionCount, int recvBufferSize, int sendBufferSize)
{
m_maxSessionCount = maxSessionCount; // 服务器允许的最大连接(会话)数
m_recvBufferSize = recvBufferSize; // 接收缓冲区大小
m_sendBufferSize = sendBufferSize;
m_availableRecvOffset = 0; // 当前可以的接收缓冲区偏移地址
m_availbleSendOffset = 0;
m_availbleOffsetStack = new Stack(); // 可重复使用的缓冲区偏移地址
m_totalRecvLength = m_recvBufferSize * m_maxSessionCount;
m_totalSendLength = m_sendBufferSize * m_maxSessionCount;
m_recvBuffer = new byte[m_totalRecvLength]; // 接收缓冲区
m_sendBuffer = new byte[m_totalSendLength];
}
public int RecvBufferSize
{
get { return m_recvBufferSize; }
}
public int SendBufferSize
{
get { return m_sendBufferSize; }
}
public byte[] RecvBuffer
{
get { return m_recvBuffer; }
}
public byte[] SendBuffer
{
get { return m_sendBuffer; }
}
public void FreeOffset(int recvOffset, int sendOffset)
{
lock (this)
{
m_availbleOffsetStack.Push(recvOffset); // 回收缓冲区偏移地址
m_availbleOffsetStack.Push(sendOffset);
}
}
public void GetOffset(ref int recvOffset, ref int sendOffset)
{
lock (this)
{
if (m_availbleOffsetStack.Count > 0) // 有释放的可重用缓冲区
{
sendOffset = m_availbleOffsetStack.Pop(); // 再次使用
recvOffset = m_availbleOffsetStack.Pop();
}
else
{
if (m_totalRecvLength >= m_availableRecvOffset + m_recvBufferSize &&
m_totalSendLength >= m_availbleSendOffset + m_recvBufferSize) // 有空间
{
recvOffset = m_availableRecvOffset;
sendOffset = m_availbleSendOffset;
m_availableRecvOffset += m_recvBufferSize; // 调整可用块指针
m_availbleSendOffset += m_sendBufferSize;
}
else
{
throw new IndexOutOfRangeException("buffer index out of range.");
}
}
}
}
}
上述代码中,m_maxSessionCount是Socket服务器最大的可连客户端Socket数,BufferManager构造函数要求该数以及接收和发送缓冲区的大小,从而创建两个大的、可重复使用共享缓冲区。
具体使用步骤如下:
创建一个BufferManager对象 m_bufferManager;
获取一个Socket对象m_socket;
获取接收/发送缓冲区偏移 m_bufferManager.Getffset(ref m_recvBufferOffset, ref m_sendBufferOffset);
异步接收:m_socket(m_bufferManager.RecvBuffer, m_recvBufferOffset, m_bufferManager.RecvBufferSize,...);
异步发送:先考虑发送串长度,然后决定是否使用缓冲区,见随后的代码;
关闭Socket时需要调用:m_bufferManager.FreeOffset(m_recvBufferOffset, m_sendBufferOffset)回收偏移。
下面是发送字符串datagramText的示例代码:
int byteLength = Encoding.ASCII.GetByteCount(datagramText);
if (byteLength <= m_bufferManager.SendBufferSize) // 可以使用发送缓冲区
{
byte[] sendBuffer = m_bufferManager.SendBuffer;
Encoding.ASCII.GetBytes(datagramText, 0, byteLength, sendBuffer, m_sendBufferOffset); // 拷贝到缓冲区
m_socket.BeginSend(sendBuffer, m_sendBufferOffset, byteLength, ...); // 发送
}
else // 字符串太长,另建字节数组发送
{
byte[] data = Encoding.ASCII.GetBytes(datagramText); // 获得数据字节数组
m_socket.BeginSend(data, 0, data.Length, ...); // 发送
}
在数据发送时,如果发送缓冲的大小比实际发送包大的话,那么上述异步发送就可以使用BufferManager公共缓冲区。当然,用缓冲区分多次发送数据,也是一个可以考虑和有效的方案,但实现比较复杂(留待以后解决)。数据接收则直接使用BufferManager,因为长数据包由Socket自动分多次接收,不需要考虑分包及包接收顺序等问题。
基于事件驱动的SocketAsyncEventArgs性能的改善,不仅与使用共享缓冲区的技术相关,更与其在完成端口(IOCP)共享SocketAsyncEventArgs对象有关,该对象可重复使用。而在传统的异步Socket处理时,总会创建一个IAsyncResult对象,该对象不可重复使用,且必须调用AsyncWaitHandle.Close()释放资源。
使用Socket异步调事件参数类SocketAsyncEventArgs时考虑了上述情况,基本构思为:自定义一个缓冲区管理类如BufferManager,开辟一个大的、可重用接收/发送收缓冲区,用于SendAsync()、ReceiveAsync()等方法,之前使用SetBuffer()和属性OffSet、Count设定缓冲区空间。
事实上,在.NET 2.0平台上的Socket传统APM(异步编程模型)中,仍然可以使用这个技术。下面是修改的BufferManager类:
public sealed class BufferManager
{
// ...成员字段见其构造函数
public BufferManager(int maxSessionCount, int recvBufferSize, int sendBufferSize)
{
m_maxSessionCount = maxSessionCount; // 服务器允许的最大连接(会话)数
m_recvBufferSize = recvBufferSize; // 接收缓冲区大小
m_sendBufferSize = sendBufferSize;
m_availableRecvOffset = 0; // 当前可以的接收缓冲区偏移地址
m_availbleSendOffset = 0;
m_availbleOffsetStack = new Stack(); // 可重复使用的缓冲区偏移地址
m_totalRecvLength = m_recvBufferSize * m_maxSessionCount;
m_totalSendLength = m_sendBufferSize * m_maxSessionCount;
m_recvBuffer = new byte[m_totalRecvLength]; // 接收缓冲区
m_sendBuffer = new byte[m_totalSendLength];
}
public int RecvBufferSize
{
get { return m_recvBufferSize; }
}
public int SendBufferSize
{
get { return m_sendBufferSize; }
}
public byte[] RecvBuffer
{
get { return m_recvBuffer; }
}
public byte[] SendBuffer
{
get { return m_sendBuffer; }
}
public void FreeOffset(int recvOffset, int sendOffset)
{
lock (this)
{
m_availbleOffsetStack.Push(recvOffset); // 回收缓冲区偏移地址
m_availbleOffsetStack.Push(sendOffset);
}
}
public void GetOffset(ref int recvOffset, ref int sendOffset)
{
lock (this)
{
if (m_availbleOffsetStack.Count > 0) // 有释放的可重用缓冲区
{
sendOffset = m_availbleOffsetStack.Pop(); // 再次使用
recvOffset = m_availbleOffsetStack.Pop();
}
else
{
if (m_totalRecvLength >= m_availableRecvOffset + m_recvBufferSize &&
m_totalSendLength >= m_availbleSendOffset + m_recvBufferSize) // 有空间
{
recvOffset = m_availableRecvOffset;
sendOffset = m_availbleSendOffset;
m_availableRecvOffset += m_recvBufferSize; // 调整可用块指针
m_availbleSendOffset += m_sendBufferSize;
}
else
{
throw new IndexOutOfRangeException("buffer index out of range.");
}
}
}
}
}
上述代码中,m_maxSessionCount是Socket服务器最大的可连客户端Socket数,BufferManager构造函数要求该数以及接收和发送缓冲区的大小,从而创建两个大的、可重复使用共享缓冲区。
具体使用步骤如下:
创建一个BufferManager对象 m_bufferManager;
获取一个Socket对象m_socket;
获取接收/发送缓冲区偏移 m_bufferManager.Getffset(ref m_recvBufferOffset, ref m_sendBufferOffset);
异步接收:m_socket(m_bufferManager.RecvBuffer, m_recvBufferOffset, m_bufferManager.RecvBufferSize,...);
异步发送:先考虑发送串长度,然后决定是否使用缓冲区,见随后的代码;
关闭Socket时需要调用:m_bufferManager.FreeOffset(m_recvBufferOffset, m_sendBufferOffset)回收偏移。
下面是发送字符串datagramText的示例代码:
int byteLength = Encoding.ASCII.GetByteCount(datagramText);
if (byteLength <= m_bufferManager.SendBufferSize) // 可以使用发送缓冲区
{
byte[] sendBuffer = m_bufferManager.SendBuffer;
Encoding.ASCII.GetBytes(datagramText, 0, byteLength, sendBuffer, m_sendBufferOffset); // 拷贝到缓冲区
m_socket.BeginSend(sendBuffer, m_sendBufferOffset, byteLength, ...); // 发送
}
else // 字符串太长,另建字节数组发送
{
byte[] data = Encoding.ASCII.GetBytes(datagramText); // 获得数据字节数组
m_socket.BeginSend(data, 0, data.Length, ...); // 发送
}
在数据发送时,如果发送缓冲的大小比实际发送包大的话,那么上述异步发送就可以使用BufferManager公共缓冲区。当然,用缓冲区分多次发送数据,也是一个可以考虑和有效的方案,但实现比较复杂(留待以后解决)。数据接收则直接使用BufferManager,因为长数据包由Socket自动分多次接收,不需要考虑分包及包接收顺序等问题。
基于事件驱动的SocketAsyncEventArgs性能的改善,不仅与使用共享缓冲区的技术相关,更与其在完成端口(IOCP)共享SocketAsyncEventArgs对象有关,该对象可重复使用。而在传统的异步Socket处理时,总会创建一个IAsyncResult对象,该对象不可重复使用,且必须调用AsyncWaitHandle.Close()释放资源。