使用SocketAsyncEventArgs的服务器设计

时间:2009-10-08 14:55:27

标签: c# .net-3.5 sockets

我想使用SocketAsyncEventArgs事件创建一个异步套接字服务器。

服务器应同时管理大约1000个连接。处理每个数据包逻辑的最佳方法是什么?

服务器设计基于this MSDN example,因此每个套接字都有自己的SocketAsyncEventArgs用于接收数据。

  1. 执行接收函数中的逻辑内容。 不会产生任何开销,但由于在逻辑完成之前不会完成下一个ReceiveAsync()调用,因此无法从套接字读取新数据。对我来说两个主要问题是:如果客户端发送大量数据并且逻辑处理很重,系统将如何处理它(由于缓冲区已满而丢失数据包)?此外,如果所有客户端同时发送数据,是否会有1000个线程,或者是否存在内部限制,并且在另一个线程完成执行之前新线程无法启动?

  2. 使用队列。 接收函数将非常短并且执行速度很快,但由于队列的原因,您将获得不错的开销。问题是,如果你的工作线程在服务器负载很重的情况下速度不够快,你的队列就会变满,所以也许你必须强制丢包。您还会遇到生产者/消费者问题,这可能会导致整个队列的速度变慢。

  3. 那么更好的设计,接收函数中的逻辑,工作线程中的逻辑或者我到目前为止完全不同的任何东西。

    关于数据发送的另一个任务。

    将SocketAsyncEventArgs绑定到套接字(类似于接收事件)并使用缓冲系统对一些小数据包进行一次发送调用(比方说有时会直接发送一个接一个的数据包)更好吗? )或为每个数据包使用不同的SocketAsyncEventArgs并将它们存储在池中以重用它们?

2 个答案:

答案 0 :(得分:10)

要有效地实现异步套接字,每个套接字将需要多于1个SocketAsyncEventArgs。每个SocketAsyncEventArgs中的byte []缓冲区也存在问题。简而言之,只要发生托管 - 本机转换(发送/接收),就会固定字节缓冲区。如果根据需要分配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&amp; DataSent事件。需要注意的一件事是SocketAsyncOperation.ReceiveMessageFrom:block;如果套接字没有出错,它立即启动另一个BeginReceive(),它将从池中分配另一个SocketEventArgs。

另一个重要的注意事项是HandleIOComplete方法中设置的SocketEventArgs SequenceNumber属性。虽然异步请求将在排队的顺序中完成,但您仍然受其他线程竞争条件的限制。由于代码在引发DataReceived事件之前调用BeginReceive,因此服务原始IOCP的线程有可能在调用BeginReceive之后但在第二次异步接收在第一次引发DataReceived事件的新线程上完成第二次异步接收之前阻塞事件。虽然这是一个相当罕见的边缘情况,但它可以发生,并且SequenceNumber属性使消费应用程序能够确保以正确的顺序处理数据。

要注意的另一个领域是异步发送。通常,异步发送请求将同步完成(如果调用同步完成,SendAsync将返回false)并且可能严重降低性能。在IOCP上返回的异步调用的额外开销实际上可能导致比仅使用同步调用更差的性能。当同步调用发生在堆栈上时,异步调用需要两个内核调用和一个堆分配。

希望这有帮助,   比尔

答案 1 :(得分:0)

在您的代码中,您执行此操作:

if (!socket.ReceiveAsync(e.AsyncEventArgs)) {
  this.HandleIOCompleted(null, e);
}

但这样做是错误的。有一个原因是为什么在同步完成时不会调用回调,这样的操作可以填满堆栈。

想象一下,每个ReceiveAsync始终是同步返回的。如果你的HandleIOCompleted有一段时间,你可以处理在同一堆栈级别同步返回的结果。如果没有同步返回,你就会打破。 但是,通过执行您的操作,您最终会在堆栈中创建一个新项目......所以,如果运气不好,则会导致堆栈溢出异常。