.NET WebSockets强制关闭,尽管保持活动状态并在连接上进行操作

31

我们使用System.Net.WebSockets编写了一个简单的WebSocket客户端。ClientWebSocket上的KeepAliveInterval设置为30秒。

连接成功打开,并且在双向流量中正常工作,或者如果连接处于空闲状态,则客户端每30秒向服务器发送Pong请求(在Wireshark中可见)。

但是,100秒后,由于客户端关闭了TCP套接字(在Wireshark中观察到客户端发送了FIN),连接突然终止。服务器在关闭套接字之前响应了1001 Going Away。

经过大量的挖掘,我们找到了原因,并找到了一个相当笨重的解决方法。尽管进行了大量的Google和Stack Overflow搜索,我们只看到了少数几个人发布有关该问题的帖子,没有人给出答案,因此我发布此帖以节省其他人的痛苦,并希望有人能够建议更好的解决方法。

100秒超时的原因是WebSocket使用了System.Net.ServicePoint,它具有MaxIdleTime属性,允许关闭空闲套接字。在打开WebSocket时,如果存在Uri的现有ServicePoint,则使用该服务点,并使用创建时设置的MaxIdleTime属性。如果没有,则将创建新的ServicePoint实例,并将MaxIdleTime设置为当前System.Net.ServicePointManager MaxServicePointIdleTime属性的值(默认为100,000毫秒)。

问题在于,WebSocket流量和WebSocket keep-alives(Ping / Pong)似乎都不会被ServicePoint空闲计时器视为流量。因此,在打开WebSocket后的100秒钟后,它就被关闭了,尽管存在流量或保持活动状态的消息。

我们的猜测是,这可能是因为WebSocket最初是作为HTTP请求启动的,然后升级为websocket。似乎空闲计时器仅在寻找HTTP流量。如果确实发生了这种情况,则System.Net.WebSockets实现中存在重大错误。

我们目前采用的解决办法是将ServicePoint的MaxIdleTime设置为int.MaxValue,这样WebSocket就可以无限期地保持开启状态。但缺点是该值适用于该ServicePoint上的任何其他连接。在我们的情境中(即使用Visual Studio Web和Load测试进行负载测试),我们还为同一个ServicePoint打开了其他(HTTP)连接,并且实际上在我们打开WebSocket之前已经有一个活动的ServicePoint实例。这意味着在更新MaxIdleTime后,所有负载测试的HTTP连接都将没有空闲超时时间。虽然实际上Web服务器应该关闭空闲连接,但这并不令人感到舒适。
我们还简要探讨了是否可以创建一个仅用于WebSocket连接的新ServicePoint实例,但没有找到一种干净的方法。
还有一个小细节使得这更加难以跟踪,即尽管System.Net.ServicePointManager的MaxServicePointIdleTime属性默认为100秒,但Visual Studio会覆盖此值并将其设置为120秒,这使得搜索变得更加困难。

恰巧,就在前几天我偶然发现了这种行为。看起来像是一个 bug。考虑向 CLR 团队报告它。通过使用反射设置一些内部字段,应该可以保护 websocket 在 ServicePoint 超时时不被关闭,但我对任何一种解决方案都不完全满意。 - Anton Tykhyy
谢谢!我花了最近两天的时间试图找出为什么我的内部ClientWebsocket在大约100秒后神秘地断开连接,直到我偶然发现了这篇文章。看起来已经解决了我的问题。 - Dennis
2
System.Net.ServicePointManager.MaxServicePointIdleTime = int.MaxValue; 系统。网路。ServicePointManager.MaxServicePointIdleTime = int.MaxValue; - liuhongbo
3个回答

16

我本周遇到了这个问题。你的解决方法让我朝着正确方向前进,但我认为我已经缩小了根本原因。

如果WebSocket服务器的“101 Switching Protocols”响应中包含“Content-Length:0”头,则WebSocketClient会被困惑,并安排在100秒内清理连接。

以下是来自.Net Reference Source的有问题的代码:

//if the returned contentlength is zero, preemptively invoke calldone on the stream.
//this will wake up any pending reads.
if (m_ContentLength == 0 && m_ConnectStream is ConnectStream) {
    ((ConnectStream)m_ConnectStream).CallDone();
}
根据RFC 7230第3.3.2节规定,Content-Length在1xx(信息性)消息中是被禁止的,但我发现它被错误地包含在一些服务器实现中。
有关更多详细信息,包括用于诊断ServicePoint问题的一些示例代码,请参见此线程:https://github.com/ably/ably-dotnet/issues/107

1
这种行为仍然存在于客户端最新的.NET 4.7.1和IIS Express 10.0.14358,且它在其101响应中发送了 Content-Length: 0 - Anton Tykhyy

5
我将套接字的KeepAliveInterval设置为0,如下所示:

theSocket.Options.KeepAliveInterval = TimeSpan.Zero;

那就解决了websocket在超时时关闭的问题。但是,它也可能会完全关闭ping消息的发送。

1

我这几天研究了这个问题,在分别比较了Python的webclient-client和.Net的WebSocketClient的抓包后,发现了问题所在。在WebSocketClient中,“Options.KeepAliveInterval”只有在这段时间内从服务器未收到消息时才发送一个数据包给服务器。但是某些服务器只判断是否有来自客户端的活跃消息。因此,即使服务器端持续发送数据包,我们也必须定期手动向服务器发送任意数据包(不一定是ping数据包,WebSocketMessageType没有ping类型)。这就是解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接