提问者:小点点

如何正确使用带有TCPClient的TPL?


我使用TcpListener编写了一个服务器,它应该处理成千上万的并发连接。

因为我知道大多数时候,大多数连接都是空闲的(偶尔会进行乒乓运动,以确保对方还在那里),所以异步编程似乎是解决方案。

然而,在最初的几百个客户之后,业绩迅速恶化。如此之快,事实上,我只能勉强达到1000个并发连接。

CPU没有达到最大值(平均约为4%),RAM使用率<100MB,并且没有太多的网络流量在进行。

当我在Visual Studio中暂停服务器并查看“tasks”窗口时,有无数(数百)个状态为“已计划”的任务,只有很少(少于30)个“正在运行/活动”的任务。

我尝试使用Visual Studio和dotTrace Peformacne进行分析,但没有发现任何错误。没有锁争用,没有使用大量CPU的“热路径”。似乎整个应用程序都变慢了。

我有一个简单的while(true),里面有以下内容:

var client = await tcpListener.AcceptTcpClientAsync().ConfigureAwait(false);
Task.Run(() => OnClient(client));

为了处理连接,我制作了一些方法来封装连接的不同阶段。例如,在上面的onclient中有await HandleLogin(。。。),然后它进入一个while(Client.isConnected)循环,该循环只是await stream.readBuffer(1)stream只是从TcpClient.GetStream获得的正常网络流,而ReadBuffer是一个自定义方法,其实现方式如下:

public static async Task<byte[]> ReadBuffer(this Stream stream, int length)
{
    byte[] buffer = new byte[length];
    int read = 0;

    while (read < length)
    {
        int remaining = length - read;

        int readNow = await stream.ReadAsync(buffer, read, remaining).ConfigureAwait(false);
        read += readNow;

        if (readNow <= 0)
            throw new SocketException((int)SocketError.ConnectionReset);
    }

    return buffer;
}

我在我等待任何东西的每个地方都使用。configureAwait(false),因为我需要任何类型的同步上下文,而且我不想支付到处检索/创建同步上下文的性能开销。

我注意到的一件事是,当我从我的测试工具中生成50个连接,然后随机地关闭它(因此它创建的所有连接都应该在服务器上接收到ConnectionReset SocketException)时,服务器需要很长时间才能做出反应,经常是完全挂起,直到新的连接到达。

有没有可能因为某种原因,某些延续想要同步并在某个特定的线程上运行?有可能(当在适当的时刻断开连接时)在只有20个连接的情况下使服务器应用程序几乎无法使用。

我做错了什么?如果它是某个bug(我假设它是),我该如何去寻找它呢?我将这个问题缩小到了许多任务,这些任务只停留在NetworkStream.ReadAsync(。。。)上,即使它们应该立即接收到SocketException(ConnectionReset)。

我尝试在远程机器和本地机器上启动我的测试工具(它只是使用TcpClient),我得到了相同的结果。

我的OnClient定义为异步任务OnClient(TcpClient client)。在它内部,它等待连接的不同阶段:身份验证,一些设置协商,然后进入循环,在循环中它等待消息。

我使用Task.run是因为我不想等到一个客户机完成,而是希望尽可能快地接受所有客户机,为每个客户机生成一个新任务。然而,我不确定我是否不能/不应该在没有任务的情况下编写OnClient(client),在它周围运行,也不等待OnClient(这会导致一个不会消失的提示,但这是我想要的,我不想等到客户端完成)。

在身份验证和设置后,连接进入的最后一个阶段是一个循环,在这个循环中,服务器等待来自客户端的消息。但是,在此之前,服务器还执行另一个task.run()(while(is connected)and await task.delay.。。)来发送ping数据包和其他一些“管理”的事情。通过使用Nito AsyncEx库中的锁定机制来同步对NetworkStream的所有写入,以确保没有数据包以某种方式交错。如果任何地方发生异常(当读或写时),我总是在TcpClient上调用。close,以确保所有其他挂起的不完整的读和写都抛出异常。


共1个答案

匿名用户

我把这个问题缩小到NetworkStream.ReadAsync(。。。)即使它们应该立即收到SocketException(ConnectionReset)。

这是一个不正确的假设。您必须向套接字写入以检测丢弃的连接。

这是TCP/IP编程的许多陷阱之一,这就是为什么我建议人们尽可能使用SignalR。

从代码/描述中跳出来的其他陷阱:

  • 您正在尝试使用异步API,但您的代码也有task.run。所以它仍然在做线程跳转。这可能是可取的,也可能不可取。(假设onclient是一个async方法;如果它使用sync-over-async,那么它肯定不是一个好模式)。
  • while(Client.IsConnected)是常见的错误模式。应该同时运行读循环和写队列处理器。特别是,isconnected是绝对没有意义的--它从字面上看只是表示套接字在过去的某个点连接过。并不意味着它仍然是连接的。如果代码具有isconnected,则存在bug.