.net 要关闭套接字,不要Close()套接字,嗯?

afdcj2ne  于 2023-01-14  发布在  .NET
关注(0)|答案(6)|浏览(190)

我知道TIME_WAIT是TCP/IP不可或缺的一部分,但是在SO(和其他地方)上存在许多问题,在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状态吗?)

plicqrtu

plicqrtu1#

这对我来说没有多大意义,因为这是一个IDisposable对象,GC不应该也调用该对象的Dispose()方法吗?
Dispose pattern也称为IDisposable,它提供了两种清理非托管对象的方法。Dispose方法提供了一种直接而快速的清理资源的方法。finalize方法由垃圾收集器调用,是一种确保在使用该代码的其他开发人员忘记调用Dispose方法的情况下清理非托管资源的防故障方法。这有点类似于C++开发人员忘记在堆分配内存上调用Delete-这会导致内存泄漏。
根据引用的链接:
“尽管终结器在某些清理方案中很有效,但它们有两个明显的缺点:
1.当GC检测到一个对象符合收集条件时,终结器被调用。这发生在不再需要该资源后的某个不确定的时间段。在获取许多稀缺资源的程序中,开发人员可以或想要释放该资源的时间与终结器实际释放该资源的时间之间的延迟可能是不可接受的(容易耗尽的资源)或者在保持资源使用的成本很高的情况下(例如,大的非托管存储器缓冲区)。
1.当CLR需要调用终结器时,它必须将对象内存的收集推迟到下一轮垃圾回收(终结器在两次回收之间运行)。这意味着对象(及其引用的所有对象)的内存在较长时间内不会被释放。”
使用这个方法,套接字永远不会进入TIME_WAIT状态。如果我只是在手动调用Close()或Dispose()之前关闭应用程序,情况也是如此。
有人能解释一下这是否是一个好的做法吗(我想人们会说这不是)。
它关闭需要一段时间的原因是因为代码lingers by default给予了应用一些时间来处理任何排队的消息。根据MSDN上的TcpClient.Close方法文档:
“Close方法将范例标记为已释放并请求相关的Socket关闭TCP连接。根据LingerState属性,TCP连接在调用Close方法之后,当数据仍要发送时,可能会保持打开一段时间。当基本连接完成关闭时,不会提供通知。
调用此方法将最终导致关闭关联的Socket,并且还将关闭用于发送和接收数据的关联NetworkStream(如果已创建)。”
此超时值可以通过following code

// 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状态是否是一个好的实践,以及最终这是否是某个bug(即,所有套接字都应该经历TIME_WAIT状态吗?)
至于将对TcpClient对象的引用设置为null,推荐的方法是调用Close方法。当引用被设置为null时,GC最终调用finalize方法。finalize方法最终调用Dispose方法,以便合并清理非托管资源的代码。因此,关闭套接字将起作用-这只是不推荐。
在我看来,这取决于应用程序是否应该允许一些延迟时间,让应用程序有时间处理排队的消息。如果我确定我的客户端应用程序已经处理了所有必要的消息,那么我可能会给它0秒或1秒的延迟时间,如果我认为这可能会在未来改变。
对于一个非常忙碌的客户端和/或薄弱的硬件-那么我可能会给予它更多的时间。对于一个服务器,我将不得不在负载下基准不同的值。
其他有用的参考资料:
What is the proper way of closing and cleaning up a Socket connection?
Are there any cases when TcpClient.Close or Socket.Close(0) could block my code?

ktecyv1j

ktecyv1j2#

@Bob Bryan在我准备我的连接时给出了一个很好的答案。它说明了为什么要避免终结器,以及如何异常关闭连接以避免服务器上的TIME_WAITs问题。
我想参考https://stackoverflow.com/a/13088864/2138959关于SO_LINGER的一个很好的答案来回答问题TCP option SO_LINGER (zero) - when it's required,它可能会向您澄清更多的事情,以便您可以在每个特定的情况下决定使用哪种方法来关闭套接字。
总而言之,您应该设计客户端-服务器通信协议,使客户端关闭连接以避免服务器上的TIME_WAIT。

cngwdvgl

cngwdvgl3#

Socket类有一个相当长的方法protected virtual void Dispose(bool disposing),调用该方法时,true作为.Dispose()的参数,false作为垃圾回收器调用的析构函数的参数。
很有可能,您可以在这个方法中找到处理套接字的处置的任何差异的答案。事实上,它没有从析构函数对false做任何事情,所以您有了自己的解释。

62o28rlo

62o28rlo4#

最后我查了一大堆这样的链接,终于把我的问题理清了。这真的很有帮助。
在服务器端,我基本上什么也不做。接收、发送响应,然后退出处理程序。我确实添加了一个LingerState 1,但我认为它什么也不做。
在客户端,我使用相同的LingerState,但在接收之后(我知道数据都在那里,因为我是基于数据包开头的UInt 32长度接收的),我关闭()客户端套接字,然后将Socket对象设置为NULL。
在同一台机器上同时运行客户端和服务器,它会立即清理所有的套接字;我之前在TIME_WAIT中留下了数千个。

rggaifut

rggaifut5#

用途

tcpClient.Close(0);

指定0秒超时就足够了。

j8yoct9x

j8yoct9x6#

我在NET 6中遇到了同样的TimeWait问题(好吧,理论上timewait不是问题),而我想立即中止-关闭套接字(tcp RST)。问题是TcpClient.Close()不是TcpClient.Client.Close()。当TcpClient的示例被释放时,释放的顺序将是:
tcpclient.Dispose()调用networkstream.Dispose(),networkstream.Dispose()又调用socket.Dispose()。
networkstream.Dispose(),在释放套接字之前,它调用Shutdown(both),这(如果我理解得很好的话)会在关闭套接字之前触发优雅的终止,而不管我们设置的linger 0 time选项是什么,并且TimeWait状态会在这里进入。由于(对于实际的NET 6/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();

相关问题