我使用 TcpListener 编写了一个服务器,它应该可以处理数千个并发连接。
因为我知道大多数时间大多数连接都是空闲的(偶尔会进行乒乓以确保另一端仍然存在)异步编程似乎是解决方案。
然而,在最初的几百个客户端之后,性能迅速恶化。事实上,速度如此之快,以至于我几乎无法达到 1000 个并发连接。
CPU 未达到最大值(平均约为 4%),RAM 使用率 <100MB,并且没有大量网络流量。
当我在 Visual Studio 中暂停服务器并查看“任务”窗口时,有无数(数百个)状态为“已调度”的任务,而只有少数(少于 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 获得的普通 NetworkStream,而 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)
await
任何事情,因为我需要任何类型的同步上下文,而且我不想在任何地方支付检索/创建同步上下文的性能开销。我注意到的一件事是,当我从我的测试工具生成 50 个连接然后随机关闭它时(因此它建立的所有连接都应该在服务器上收到 ConnectionReset SocketException),服务器通常需要很长时间才能使用react完全挂起,直到新连接到达。
难道某些延续以某种方式想要同步并以某种方式在某些特定线程上运行?
有可能(在正确的时刻断开连接时)使服务器应用程序几乎无法使用少至 20 个连接。
我究竟做错了什么?
如果它是一些错误(我认为是),我将如何找到它?
我将问题缩小到许多任务只是坐在
NetworkStream.ReadAsync(...)
即使他们应该立即收到 SocketException (ConnectionReset)。我尝试在远程机器和本地启动我的测试工具(它只是使用 TcpClient),我得到了相同的结果。
编辑 1
我的 OnClient 定义为
async Task OnClient(TcpClient client)
.在其中,它等待连接的不同阶段:身份验证、一些设置协商,然后进入等待消息的循环。我用
Task.Run
因为我不想等到一个客户端完成,但我想尽快接受所有客户端,为每个客户端生成一个新任务。然而,我不确定我是否不能/不应该只写 OnClient(client)
没有 Task.Run 绕过它,也没有等待 OnClient (会导致一个不会消失的提示,但我认为这是我想要的,我不想等到客户端完成)。最后阶段
连接在身份验证和设置协商后进入的最后一个阶段是服务器等待来自客户端的消息的循环。
然而在此之前,服务器还做了另一个
Task.Run()
(使用 while(已连接)并等待 Task.Delay...)发送 ping 数据包和其他一些“管理”的东西。通过使用 Nito AsyncEx 库中的锁定机制来同步对 NetworkStream 的所有写入,以确保没有数据包以某种方式交错。
如果任何地方(读取或写入时)发生任何异常,我总是在 TcpClient 上调用 .Close 以确保所有其他未完成的未完成读取和写入抛出异常。
最佳答案
I narrowed the problem down to many Tasks just sitting at NetworkStream.ReadAsync(...) even though they should instantly receive a SocketException (ConnectionReset).
这是一个错误的假设。 You have to write to the socket to detect dropped connections.
这是 TCP/IP 编程的众多陷阱之一,这就是为什么我建议人们尽可能使用 SignalR。
从您的代码/描述中跳出的其他陷阱:
Task.Run
.所以它仍然立即进行线程跳转。这可能是可取的,也可能不是。 (假设 OnClient
是一个 async
方法;如果它使用的是异步同步,那么它绝对不是一个好的模式)。 while(client.IsConnected)
是一种常见的错误模式。您应该同时运行读取循环和写入队列处理器。特别是IsConnected
绝对没有意义 - 它实际上只是意味着套接字在过去的某个时间点连接过。确实如此 不是 表示它仍然连接。如果代码有 IsConnected
,然后有一个错误。 关于c# - 如何在 TcpClient 中正确使用 TPL?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43301378/