【发布时间】:2017-09-04 05:03:56
【问题描述】:
我使用 TcpListener 编写了一个服务器,它应该可以处理数千个并发连接。
因为我知道大多数时候大多数连接都是空闲的(偶尔会打乒乓球以确保另一端仍然存在)异步编程似乎是解决方案。
但是,在前几百个客户端之后,性能迅速恶化。实际上如此之快,以至于我几乎无法达到 1000 个并发连接。
CPU 未达到最大值(平均约为 4%),RAM 使用率
当我在 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;
}
我在await 任何地方都使用 .ConfigureAwait(false),因为我需要任何类型的同步上下文,并且我不想支付到处检索/创建同步上下文的性能开销.
我注意到的一件事是,当我从我的测试工具生成 50 个连接然后随机关闭它(因此它建立的所有连接都应该在服务器上收到 ConnectionReset SocketException)时,服务器需要很长时间才能做出反应经常完全挂起,直到新的连接到达。
会不会是某些延续想要以某种方式同步并在某个特定线程上运行? 有可能(在适当的时候断开连接)只有 20 个连接就使服务器应用程序几乎无法使用。
我做错了什么?
如果它是一些错误(我假设它是),我将如何找到它?
我将问题缩小到许多只是坐在NetworkStream.ReadAsync(...) 的任务,即使它们应该立即收到 SocketException (ConnectionReset)。
我尝试在远程机器上以及在本地启动我的测试工具(它只是使用 TcpClient),我得到了相同的结果。
编辑 1
我的 OnClient 定义为 async Task OnClient(TcpClient client)。在其中,它等待连接的不同阶段:身份验证、一些设置协商,然后进入等待消息的循环。
我使用Task.Run 是因为我不想等到一个客户端完成后,但我想尽快接受所有客户端,为每个客户端生成一个新任务。然而,我不确定我是否不能/不应该只写OnClient(client) 没有Task.Run 并且没有等待OnClient (会导致一个不会消失的提示,但这是我想要的我想,我不想等到客户端完成)。
最后阶段
身份验证和设置协商后连接进入的最后一个阶段是服务器等待来自客户端的消息的循环。
然而,在此之前,服务器还会执行另一个 Task.Run()(使用 while(is connected) 并等待 Task.Delay ...)来发送 ping 数据包和其他一些“管理”内容。
所有对 NetworkStream 的写入都通过使用 Nito AsyncEx 库中的锁定机制进行同步,以确保没有数据包以某种方式交错。
如果任何地方发生任何异常(读取或写入时),我总是在 TcpClient 上调用 .Close 以确保所有其他未完成的未完成读取和写入都抛出异常。
【问题讨论】:
标签: c# .net multithreading asynchronous tcpclient