【问题标题】:To close the socket, don't Close() the socket. Uhmm?要关闭套接字,请不要 Close() 套接字。嗯?
【发布时间】:2017-04-08 17:02:38
【问题描述】:

我知道 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 状态是否是一种好习惯,并最终确定这是否是某个地方的错误(即,是否所有套接字都应该经历 TIME_WAIT 状态?)

【问题讨论】:

  • "GC不应该也调用对象的Dispose()方法吗?" GC 从不 调用Dispose。它只调用类的终结器(如果有的话)。通常,Dispose() 是从终结器调用的,但这需要重复:Dispose 仅用于using。 GC 根本不关心它。
  • 这是对 GC 参与所有这一切的一个很好的解释,谢谢!
  • 请注意,您的代码没有给 GC 时间来实际收集套接字,因为它们具有终结器。您需要执行 GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); 来强制执行此操作。您的GC.Collect(); 调用只是将套接字放入终结器队列(如果是的话) - 套接字终结器可能需要很长时间(实际上通常会杀死您的进程)。套接字最有可能不在 TIME_WAIT 中,因为它们甚至还没有关闭。当您终止进程时也是如此 - 关闭 TCP 套接字实际上并非易事,即使对于操作系统也是如此。列出所有端口,而不仅仅是 TIME_WAIT。
  • 我没有过滤 TIME_WAIT 套接字,我正在查看 all 套接字,无论其状态如何。在我告诉 GC 运行之前,我可以看到它们都已建立。在我告诉 GC 运行大约 2 秒后,它们都完全消失而没有先进入 TIME_WAIT。如果我首先Close()Dispose(),那么它们会从ESTABLISHED 转到TIME_WAIT。所以,虽然我的代码缺少某些东西你可能是对的,但绝对不是因为它们还没有关闭而没有进入 TIME_WAIT。

标签: c# .net sockets powershell


【解决方案1】:

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

Dispose pattern,也称为 IDisposable,为清理非托管对象提供了两种方法。 Dispose 方法提供了一种直接且快速的方式来清理资源。由垃圾收集器调用的 finalize 方法是一种故障安全方法,可确保清理非托管资源,以防其他开发人员使用该代码忘记调用 Dispose 方法。这有点类似于 C++ 开发人员忘记在堆分配的内存上调用 Delete - 这会导致内存泄漏。

根据参考链接:

“虽然终结器在某些清理场景中很有效,但它们有两个明显的缺点:

  1. 当 GC 检测到对象符合收集条件时,将调用终结器。这发生在不再需要资源后的某个未确定的时间段内。开发人员可以或想要释放资源的时间与终结器实际释放资源的时间之间的延迟在获取许多稀缺资源(容易耗尽的资源)的程序中或在资源不足的情况下可能是不可接受的。保持使用成本很高(例如,大型非托管内存缓冲区)。

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

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

有人可以解释一下这是否是一个好的做法(我想人们会说它不是)。

它需要一段时间才能关闭的原因是代码lingers by default 给应用程序一些时间来处理任何排队的消息。根据MSDN上的TcpClient.Close方法文档:

"Close 方法将实例标记为已释放,并请求关联的 Socket 关闭 TCP 连接。根据 LingerState 属性,当数据仍有待发送时,调用 Close 方法后,TCP 连接可能会保持打开状态. 当底层连接完成关闭时没有提供通知。

调用此方法最终会导致关联的 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 状态是否是一种好习惯,并最终确定这是否是某个地方的错误(即,是否所有套接字都应该经历 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?

【讨论】:

  • 很好的答案,但我仍然不明白为什么Close() 方法上的代码应该与Dispose() 上的代码不同。我的意思是,为什么如果套接字在没有关闭的情况下被处理掉,它不会进入TIME_WAIT
  • @cogumel0 你在你的问题中说:“我发现当使用 TCPClient(或 Socket)时,如果我调用 Close() 或 Dispose() 方法,套接字的TCP 状态更改为 TIME_WAIT 并将遵守完全关闭之前的超时时间。”因此,当对象设置为 null 时,它不会进入 TIME_WAIT 状态。我怀疑这是因为终结器意识到不再有有效的对象来处理任何排队的消息。所以,再拖下去也没有意义。它只是在此时关闭套接字。
  • @cogumel0 您是否尝试过使用 LingerOption 来强制 Close 方法停止(或进入 TIME_WAIT 状态)?如果由于某种原因这不起作用,那么您可能别无选择,只能将对象设置为 null。
  • 对不起,很久没写了,忘了我的实际发现是什么,谢谢指点。我的意思是 Close() 与终结器。我现在就试试 LingerOption
  • 试过LingerOption,没有区别。在 Windows 10 上测试,它总是首先进入TIME_WAIT,无论我使用什么LingerOption,它都会一直停留在TIME_WAIT 30 秒。即使将LingerOption构造函数的第一个参数设置为false
【解决方案2】:

@Bob Bryan 在我准备我的时候发布了很好的答案。它说明了为什么要避免使用终结器以及如何中止关闭连接以避免服务器上出现 TIME_WAITs 问题。

我想参考一个很好的答案https://stackoverflow.com/a/13088864/2138959 关于SO_LINGER 到问题TCP option SO_LINGER (zero) - when it's required,这可能会更清楚地向您说明问题,以便您可以在每种特定情况下决定关闭套接字的方法使用。

总而言之,您应该按照客户端关闭连接的方式设计客户端-服务器通信协议,以避免服务器上出现 TIME_WAIT。

【讨论】:

    【解决方案3】:

    Socket class 有一个相当长的方法protected virtual void Dispose(bool disposing),它使用true 作为来自.Dispose() 的参数和false 作为来自垃圾收集器调用的析构函数的参数调用。

    很有可能,您对处理套接字处置的任何差异的答案都将在此方法中找到。事实上,它对析构函数的false 没有做任何事情,所以你有你的解释。

    【讨论】:

    • 这确实解释了 GC 方面的问题,但请记住,套接字是否进入 TIME_WAIT 是由操作系统本身(而不是由 .NET)控制的,为什么要调用析构函数 vs @ 987654327@ 对操作系统对套接字的作用有影响吗?我对TIME_WAIT的理解是所有的socket都应该通过这个状态来检测重传等等。仍然不明白为什么有办法绕过它(以及使用它是否是“好习惯”)。
    【解决方案4】:

    我最终查找了一堆这些链接,最后解决了我的问题。这真的很有帮助。

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

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

    在同一台机器上非常积极地运行客户端和服务器,它会立即清理所有套接字;我之前要在 TIME_WAIT 离开数千人。

    【讨论】:

      【解决方案5】:

      使用

      tcpClient.Close(0);
      

      指定 0 秒超时就足够了。

      【讨论】:

        猜你喜欢
        • 2012-09-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-09
        • 2017-03-16
        • 2018-06-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多