关闭套接字时,不要使用Close()函数关闭套接字。嗯?

12

我知道TIME_WAIT是TCP/IP的一个重要部分,但在SO(和其他地方)上有很多关于每秒创建多个套接字并且服务器最终用尽短暂端口的问题。

我发现当使用TCPClient(或者Socket)时,如果调用Close()Dispose()方法,套接字的TCP状态会变为TIME_WAIT,并且会在完全关闭之前遵守超时期间。

然而,如果我只是将变量设置为null,则套接字将在下一次GC运行时完全关闭,而不需要经历TIME_WAIT状态。

对我来说,这没有太多意义,因为这是一个IDisposable对象,难道GC不应该也调用Dispose()方法吗?

这里有一些PowerShell代码可以演示(此机器未安装VS)。 我使用Sysinternals的TCPView实时检查套接字状态:

$sockets = @()
0..100 | % {
    $sockets += New-Object System.Net.Sockets.TcpClient
    $sockets[$_].Connect('localhost', 80)
}

Start-Sleep -Seconds 10

$sockets = $null

[GC]::Collect()
使用这种方法,套接字永远不会进入TIME_WAIT状态。如果我在手动调用Close()Dispose()之前只是关闭应用程序也是如此。
有人能否解释一下,这是否是一个好的做法(我想象人们会说不是)。
编辑
GC已经回答了该问题的相关部分,但我仍然对为什么这会对套接字状态产生任何影响以及这是否应该由操作系统而不是.NET控制感兴趣。
还有,我想知道使用这种方法来防止TIME_WAIT状态是否是一个好的做法,最终是否存在某个bug(即,所有套接字是否都应该经过TIME_WAIT状态?)

7
“GC是否也应该调用对象的Dispose()方法?” GC永远不会调用Dispose()。它只会调用类的终结器(如果有的话)。通常,Dispose()从终结器中调用,但需要重申的是:Dispose仅适用于使用(using)场景。GC完全不关心它。 - Jeroen Mostert
这是一个关于GC在所有这些方面参与的很好的解释,谢谢! - cogumel0
1
请注意,您的代码没有给垃圾回收器足够的时间来实际收集套接字,因为它们具有终结器。您需要执行 GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); 来强制执行。您的 GC.Collect(); 调用只是将套接字放入终结器队列(如果有的话)- 套接字终结器可能需要很长时间(实际上经常会导致进程崩溃)。套接字最可能不处于 TIME_WAIT 状态,是因为它们甚至还没有被关闭。当您杀死进程时也是如此 - 即使对于操作系统来说,关闭 TCP 套接字也是一项非常棘手的任务。列出所有端口,而不仅仅是 TIME_WAIT。 - Luaan
1
我没有过滤TIME_WAIT套接字,我正在查看所有状态的套接字。在我告诉GC运行之前,我可以看到它们全部都是已建立状态。在我告诉GC运行大约2秒钟后,它们全部完全消失,而不是先进入TIME_WAIT状态。如果我先使用Close()Dispose()关闭它们,那么它们就会从已建立状态变为TIME_WAIT状态。所以,虽然你可能是对的,我的代码可能缺少某些东西,但它们肯定不会因为尚未关闭而不进入TIME_WAIT状态。 - cogumel0
时间等待是因为在客户端和服务器同时关闭套接字时存在定时问题。 解决方法是只在客户端关闭套接字。 TCP 可靠,并且每个数据报都有一个确认。 当客户端和服务器同时关闭时,其中一个ACK不会发生,使套接字处于等待 ACK 的 Time_Wait 状态,但从未发生 ACK。 问题是如何退出服务器上的应用程序。 大多数系统会等到从客户端收到关闭事件。 但是,Net 库没有该事件。 - jdweng
6个回答

10
这对我来说并不太有意义,因为这是一个IDisposable对象,GC不应该调用对象的Dispose()方法吗? Dispose模式,也称为IDisposable,提供了两种清理非托管对象的方法。Dispose方法提供了一种直接而快速的清理资源的方式。finalize方法由垃圾回收器调用,是一种故障安全的方法,以确保在其他使用代码的开发人员忘记调用Dispose方法的情况下,非托管资源得到清理。这有点类似于C++开发人员忘记在堆分配的内存上调用Delete,导致内存泄漏。
根据引用链接所述:
“虽然终结器在某些清理方案中是有效的,但它们具有两个重大缺点:
  1. 当GC检测到一个对象符合回收条件时,会调用终结器。这个时间是在资源不再被需要之后的某个不确定的时刻。在获取许多稀缺资源(容易耗尽的资源)的程序或者在资源使用成本高昂的情况下(例如大型非托管内存缓冲区),开发人员能够或想要释放资源的时间与终结器实际释放资源的时间之间的延迟可能是无法接受的。

  2. 当CLR需要调用终结器时,它必须推迟对象内存的回收,直到下一轮垃圾回收(终结器在回收之间运行)。这意味着对象内存(以及它引用的所有对象)将在更长的时间内不被释放。"

使用这种方法,套接字永远不会进入TIME_WAIT状态。如果我在手动调用Close()或Dispose()之前关闭应用程序也是如此。

有人能够解释一下这是否是一个好习惯吗(我想人们会说不是)。

由于代码默认延迟关闭以便应用程序有时间处理任何排队的消息,所以关闭需要一些时间。根据MSDN上TcpClient.Close方法文档的描述:
“Close方法将实例标记为已处理,并请求关联的Socket关闭TCP连接。基于LingerState属性,当仍有数据要发送时,在调用Close方法后,TCP连接可能会保持打开状态一段时间。在底层连接完成关闭时不提供通知。
如果创建了一个用于发送和接收数据的相关NetworkStream,则调用此方法最终将导致关联Socket的关闭并关闭相关的NetworkStream。”
通过以下代码可以减少或完全消除此超时值:
// Allow 1 second to process queued msgs before closing the socket.
LingerOption lingerOption = new LingerOption (true, 1);
tcpClient.LingerState = lingerOption;
tcpClient.Close();

// Close the socket right away without lingering.
LingerOption lingerOption = new LingerOption (true, 0);
tcpClient.LingerState = lingerOption;
tcpClient.Close();

我也想知道使用这种方法来防止TIME_WAIT状态是否是一个好的做法,最终是否存在某个错误(即,所有套接字是否都应该经过TIME_WAIT状态?)

至于将TcpClient对象的引用设置为null,推荐的方法是调用Close方法。当引用被设置为null时,GC会调用finalize方法。finalize方法最终调用Dispose方法以整合清理非托管资源的代码。因此,关闭套接字是可行的,只是不推荐。

在我看来,这取决于应用程序是否允许一些等待时间来处理排队的消息。如果我确定客户端应用程序已处理了所有必要的消息,那么我可能会将其等待时间设置为0秒,或者如果我认为这可能会在未来发生变化,则可能设置为1秒。

对于非常繁忙的客户端和/或弱硬件-那么我可能会给它更多时间。对于服务器,我将在负载下基准测试不同的值。

其他有用的参考:

关闭和清理套接字连接的正确方法是什么?

是否存在TcpClient.Close或Socket.Close(0)会阻塞代码的情况?


抱歉,我写这个已经有很长时间了,忘记了我的实际发现是什么,感谢您指出来。我的意思是Close()与终结器的区别。我现在会尝试使用LingerOption。 - cogumel0
1
尝试使用LingerOption,但没有任何改变。在Windows 10上进行了测试,它总是首先进入TIME_WAIT状态,无论我使用什么LingerOption,它都会在TIME_WAIT状态下停留30秒。即使将LingerOption构造函数的第一个参数设置为false - cogumel0
你尝试过使用 true、0 作为延迟选项吗?如果该选项无效,那么你应该将对象设置为 null。 - Bob Bryan
你在使用哪个版本的C#和 .NET Framework版本是多少? - Bob Bryan
让我们在聊天中继续这个讨论 - cogumel0
显示剩余3条评论

2

在我准备我的回答时,@Bob Bryan 发表了一篇非常好的答案。它展示了为什么要避免使用 finalizers 以及如何中止连接以避免服务器上的 TIME_WAIT 问题。

我想提到一个关于 SO_LINGER 的很棒的答案https://dev59.com/wW865IYBdhLWcg3wkPSD#13088864,来回答这个问题:TCP option SO_LINGER (zero) - when it's required,这可能会更加清晰地解释问题,因此您可以在每个特定情况下决定使用哪种方法关闭套接字。

总之,你应该设计你的客户端-服务器通信协议,使得客户端关闭连接以避免服务器上的 TIME_WAIT。


1

Socket类有一个相当冗长的方法protected virtual void Dispose(bool disposing),它在.Dispose()中以true参数被调用,在由垃圾回收器调用的析构函数中以false参数被调用。

很有可能,您对处理套接字的处置差异的任何答案都可以在这个方法中找到。事实上,它在来自析构函数的false时什么也不做,所以这就是您的解释。


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - cogumel0

1

我最终查阅了许多这些链接,并最终解决了我的问题。这真的很有帮助。

在服务器端,我基本上什么都不做。接收,发送响应并退出处理程序。我确实添加了一个LingerState为1,但我认为它没有什么作用。

在客户端,我使用相同的LingerState,但是在接收后(我知道数据全部在那里,因为我基于UInt32长度接收数据包的开头),我关闭客户端套接字,然后将Socket对象设置为NULL。

在同一台机器上非常积极地运行客户端和服务器,它会立即清理所有套接字;以前我留下了成千上万个TIME_WAIT。


0

使用

tcpClient.Close(0);

只需指定0秒的超时时间即可。


0
我在NET6中遇到了同样的TimeWait问题(理论上timewait不是问题),当我想要立即中止关闭套接字(tcp RST)时。问题在于TcpClient.Close()不是TcpClient.Client.Close()。
当TcpClient的实例被处理时,处理的顺序将是:
tcpclient.Dispose()调用networkstream.Dispose(),networkstream.Dispose()调用socket.Dispose()。
在处理套接字之前,networkstream.Dispose()会调用Shutdown(both),这会(如果我理解正确)触发优雅地终止套接字,而不管我们设置的linger 0time选项如何,此时进入TimeWait状态。
由于(截至实际NET6/7),只有socket.Dispose()能够发送RST,所以我最终以相反的顺序处理它们,像这样:
// socket.Dispose(bool) is the place where a RST can be sent, if the timeout is set to 0
tcpcl.Client.Close(0);

// if ownssocket(true in this case) will call socket.internalshutdown(both)....then socket.close(timeout), but the shutdown already placed the tcp into TimeWait
// but if we already closed the underlying socket, no problem
netstream.Close(0);

// if networkstream not null (if we called GetStream()), it calls networkstream.dispose and it hopes it will close the socket for us
// same problem of before, but if we previously closed the networkstream, it will then try to close socket itself, but still with the socket.internalshutdown(both)
// so again, if we already closed the socket, no problem
tcpcl.Close(); 

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