我使用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,以确保所有其他挂起的不完整的读和写都抛出异常。
我把这个问题缩小到NetworkStream.ReadAsync(。。。)即使它们应该立即收到SocketException(ConnectionReset)。
这是一个不正确的假设。您必须向套接字写入以检测丢弃的连接。
这是TCP/IP编程的许多陷阱之一,这就是为什么我建议人们尽可能使用SignalR。
从代码/描述中跳出来的其他陷阱:
task.run
。所以它仍然在做线程跳转。这可能是可取的,也可能不可取。(假设onclient
是一个async
方法;如果它使用sync-over-async,那么它肯定不是一个好模式)。while(Client.IsConnected)
是常见的错误模式。应该同时运行读循环和写队列处理器。特别是,isconnected
是绝对没有意义的--它从字面上看只是表示套接字在过去的某个点连接过。并不意味着它仍然是连接的。如果代码具有isconnected
,则存在bug.