我想使用 SocketAsyncEventArgs 事件创建一个异步套接字服务器。
服务器应该同时管理大约 1000 个连接。处理每个数据包逻辑的最佳方法是什么?
Server 设计基于 this MSDN example ,因此每个套接字都有自己的 SocketAsyncEventArgs 用于接收数据。
不会产生任何开销,但由于在逻辑完成之前不会执行下一次 ReceiveAsync() 调用,因此无法从套接字读取新数据。我的两个主要问题是:如果客户端发送大量数据并且逻辑处理很重,系统将如何处理(由于缓冲区已满而丢失数据包)?另外,如果所有客户端同时发送数据,会不会有1000个线程,还是有内部限制,新线程在另一个线程完成执行之前无法启动?
接收函数将非常短且执行得很快,但由于队列的存在,您将有可观的开销。问题是,如果您的工作线程在繁重的服务器负载下速度不够快,您的队列可能会满,因此您可能必须强制丢弃数据包。您还会遇到生产者/消费者问题,这可能会因许多锁而减慢整个队列的速度。
那么什么是更好的设计、接收函数中的逻辑、工作线程中的逻辑或迄今为止我错过的任何完全不同的东西。
关于数据发送的另一个问题。
将 SocketAsyncEventArgs 绑定(bind)到套接字(类似于接收事件)并使用缓冲系统对几个小数据包进行一次发送调用(假设数据包有时会一个接一个地直接发送)或使用每个数据包都有不同的 SocketAsyncEventArgs 并将它们存储在池中以重用它们?
最佳答案
为了有效地实现异步套接字,每个套接字将需要 1 个以上的 SocketAsyncEventArgs。每个 SocketAsyncEventArgs 中的 byte[] 缓冲区也存在问题。简而言之,每当发生托管 - native 转换(发送/接收)时,字节缓冲区将被固定。如果根据需要分配 SocketAsyncEventArgs 和字节缓冲区,由于碎片和 GC 无法压缩固定内存,许多客户端可能会遇到 OutOfMemoryExceptions。
处理此问题的最佳方法是创建一个 SocketBufferPool 类,该类将在应用程序首次启动时分配大量字节和 SocketAsyncEventArgs,这样固定内存将是连续的。然后根据需要简单地重新使用池中的缓冲区。
在实践中,我发现最好围绕 SocketAsyncEventArgs 创建一个包装类和一个 SocketBufferPool 类来管理资源的分配。
例如,以下是 BeginReceive 方法的代码:
private void BeginReceive(Socket socket)
{
Contract.Requires(socket != null, "socket");
SocketEventArgs e = SocketBufferPool.Instance.Alloc();
e.Socket = socket;
e.Completed += new EventHandler<SocketEventArgs>(this.HandleIOCompleted);
if (!socket.ReceiveAsync(e.AsyncEventArgs)) {
this.HandleIOCompleted(null, e);
}
}
这是 HandleIOCompleted 方法:
private void HandleIOCompleted(object sender, SocketEventArgs e)
{
e.Completed -= this.HandleIOCompleted;
bool closed = false;
lock (this.sequenceLock) {
e.SequenceNumber = this.sequenceNumber++;
}
switch (e.LastOperation) {
case SocketAsyncOperation.Send:
case SocketAsyncOperation.SendPackets:
case SocketAsyncOperation.SendTo:
if (e.SocketError == SocketError.Success) {
this.OnDataSent(e);
}
break;
case SocketAsyncOperation.Receive:
case SocketAsyncOperation.ReceiveFrom:
case SocketAsyncOperation.ReceiveMessageFrom:
if ((e.BytesTransferred > 0) && (e.SocketError == SocketError.Success)) {
this.BeginReceive(e.Socket);
if (this.ReceiveTimeout > 0) {
this.SetReceiveTimeout(e.Socket);
}
} else {
closed = true;
}
if (e.SocketError == SocketError.Success) {
this.OnDataReceived(e);
}
break;
case SocketAsyncOperation.Disconnect:
closed = true;
break;
case SocketAsyncOperation.Accept:
case SocketAsyncOperation.Connect:
case SocketAsyncOperation.None:
break;
}
if (closed) {
this.HandleSocketClosed(e.Socket);
}
SocketBufferPool.Instance.Free(e);
}
上面的代码包含在一个 TcpSocket 类中,该类将引发 DataReceived 和 DataSent 事件。需要注意的一件事是 SocketAsyncOperation.ReceiveMessageFrom: block;如果套接字没有错误,它会立即启动另一个 BeginReceive(),它将从池中分配另一个 SocketEventArgs。
另一个重要的注意事项是在 HandleIOComplete 方法中设置的 SocketEventArgs SequenceNumber 属性。尽管异步请求将按照排队的顺序完成,但您仍然会受到其他线程竞争条件的影响。由于代码在引发 DataReceived 事件之前调用 BeginReceive,因此服务原始 IOCP 的线程可能会在调用 BeginReceive 之后但在引发事件之前阻塞,而第二个异步接收在首先引发 DataReceived 事件的新线程上完成。尽管这是一种相当罕见的边缘情况,但它可能会发生,并且 SequenceNumber 属性使消费应用程序能够确保以正确的顺序处理数据。
另一个需要注意的领域是异步发送。通常,异步发送请求将同步完成(如果调用同步完成,SendAsync 将返回 false)并且会严重降低性能。在 IOCP 上返回异步调用的额外开销实际上会导致比简单使用同步调用更差的性能。异步调用需要两个内核调用和一个堆分配,而同步调用发生在堆栈上。
希望这可以帮助,
账单
关于c# - 使用 SocketAsyncEventArgs 的服务器设计,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/1538348/