关闭 TcpListener 和 TcpClient 连接的正确顺序(哪一侧应该是主动关闭)

Posted

技术标签:

【中文标题】关闭 TcpListener 和 TcpClient 连接的正确顺序(哪一侧应该是主动关闭)【英文标题】:Proper order for closing TcpListener and TcpClient connections (which side should be the active close) 【发布时间】:2015-03-12 08:09:50 【问题描述】:

我读到this answer on a previous question 说:

因此,发起终止的对等方——即首先调用 close()——将最终处于 TIME_WAIT 状态。 [...]

但是,如果服务器上的大量套接字处于 TIME_WAIT 状态,则可能会出现问题,因为它最终可能会阻止新连接被接受。 [...]

相反,请设计您的应用程序协议,使连接终止始终从客户端启动。如果客户端总是知道它何时读取了所有剩余的数据,它可以启动终止序列。例如,浏览器从 Content-Length HTTP 标头中知道它何时读取了所有数据并可以启动关闭。 (我知道在 HTTP 1.1 中它会保持打开一段时间以便可能重用,然后关闭它。)

我想使用 TcpClient/TcpListener 来实现这个,但不清楚如何让它正常工作。

方法一:两边合拢

这是大多数 MSDN 示例说明的典型方式 - 双方都调用 Close(),而不仅仅是客户端:

private static void AcceptLoop()

    listener.BeginAcceptTcpClient(ar =>
    
        var tcpClient = listener.EndAcceptTcpClient(ar);

        ThreadPool.QueueUserWorkItem(delegate
        
            var stream = tcpClient.GetStream();
            ReadSomeData(stream);
            WriteSomeData(stream);
            tcpClient.Close();   <---- note
        );

        AcceptLoop();
    , null);


private static void ExecuteClient()

    using (var client = new TcpClient())
    
        client.Connect("localhost", 8012);

        using (var stream = client.GetStream())
        
            WriteSomeData(stream);
            ReadSomeData(stream);
        
    

在我用 20 个客户端运行此程序后,TCPView 显示很多来自客户端和服务器的套接字卡在 TIME_WAIT 中,需要很长时间才能消失。

方法2:只关闭客户端

根据上面的引用,我删除了监听器上的 Close() 调用,现在我只依赖客户端关闭:

var tcpClient = listener.EndAcceptTcpClient(ar);

ThreadPool.QueueUserWorkItem(delegate

    var stream = tcpClient.GetStream();
    ReadSomeData(stream);
    WriteSomeData(stream);
    // tcpClient.Close();   <-- Let the client close
);

AcceptLoop();

现在我不再有任何TIME_WAIT,但我确实在CLOSE_WAITFIN_WAIT 等的各个阶段留下了套接字,这些套接字也需要很长时间才能消失。

方法3:先给客户时间关闭

这次我在关闭服务器连接之前添加了一个延迟:

var tcpClient = listener.EndAcceptTcpClient(ar);

ThreadPool.QueueUserWorkItem(delegate

    var stream = tcpClient.GetStream();
    ReadSomeData(stream);
    WriteSomeData(stream);
    Thread.Sleep(100);      // <-- Give the client the opportunity to close first
    tcpClient.Close();      // <-- Now server closes
);

AcceptLoop();

这似乎更好——现在只有客户端套接字在TIME_WAIT;服务器套接字已全部正确关闭:

这似乎与之前链接的文章所说的一致:

因此,发起终止的对等方——即首先调用 close()——将最终处于 TIME_WAIT 状态。

问题:

    哪些方法是正确的方法,为什么? (假设我希望客户端成为“主动关闭”端) 是否有更好的方法来实施方法 3?我们希望由客户端发起关闭(以便客户端留下 TIME_WAIT),但是当客户端关闭时,我们也希望关闭服务器上的连接。 我的场景实际上与Web服务器相反;我有一个客户端,可以连接和断开许多不同的远程机器。我宁愿服务器将连接卡在TIME_WAIT 中,以释放客户端上的资源。在这种情况下,我应该让服务器执行主动关闭,并将睡眠/关闭放在我的客户端上吗?

自己尝试的完整代码在这里:

https://gist.github.com/PaulStovell/a58cd48a5c6b14885cf3

编辑:另一个有用的资源:

http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html

对于确实建立出站连接以及接受入站连接的服务器,那么黄金法则是始终确保如果 TIME_WAIT 需要发生,它会在另一个对等方而不是服务器上结束。最好的方法是永远不要从服务器启动主动关闭,不管是什么原因。如果您的对等方超时,请使用 RST 中止连接,而不是关闭它。如果您的对等方发送无效数据、中止连接等。想法是,如果您的服务器从不启动主动关闭,则它永远不会累积 TIME_WAIT 套接字,因此永远不会受到它们引起的可伸缩性问题的影响。虽然很容易看出如何在发生错误情况时中止连接,但正常的连接终止呢?理想情况下,您应该在协议中设计一种方式,让服务器告诉客户端它应该断开连接,而不是简单地让服务器发起主动关闭。因此,如果服务器需要终止连接,服务器会发送应用程序级别的“我们完成”消息,客户端将其作为关闭连接的理由。如果客户端未能在合理的时间内关闭连接,则服务器将中止连接。

在客户端,事情稍微复杂一些,毕竟,必须有人发起主动关闭才能干净地终止 TCP 连接,如果是客户端,那么 TIME_WAIT 将结束。但是,让 TIME_WAIT 最终出现在客户端有几个优点。首先,如果由于某种原因,客户端由于 TIME_WAIT 中套接字的积累而最终出现连接问题,则它只是一个客户端。其他客户端不会受到影响。其次,快速打开和关闭与同一台服务器的 TCP 连接效率低下,因此除了 TIME_WAIT 问题之外,尝试将连接保持更长的时间而不是更短的时间是有意义的。不要设计一个客户端每分钟连接到服务器并通过打开一个新连接来实现的协议。而是使用持久连接设计,并且仅在连接失败时重新连接,如果中间路由器拒绝在没有数据流的情况下保持连接打开,那么您可以实现应用程序级别 ping、使用 TCP 保持活动或只是接受路由器正在重置您的连接;好消息是您没有累积 TIME_WAIT 套接字。如果您在连接上所做的工作自然是短暂的,那么请考虑某种形式的“连接池”设计,从而使连接保持打开和重用。最后,如果您绝对必须快速打开和关闭从客户端到同一服务器的连接,那么也许您可以设计一个您可以使用的应用程序级别的关闭序列,然后在此之后进行中止关闭。您的客户端可以发送“我完成了”消息,然后您的服务器可以发送“再见”消息,然后客户端可以中止连接。

【问题讨论】:

【参考方案1】:

这就是 TCP 的工作原理,你无法避免。您可以为服务器上的 TIME_WAIT 或 FIN_WAIT 设置不同的超时时间,仅此而已。

这样做的原因是,在 TCP 上,数据包可以到达您很久以前关闭的套接字。如果您已经在同一个 IP 和端口上打开了另一个套接字,它将接收用于上一个会话的数据,这会使它变得混乱。特别是考虑到大多数人认为 TCP 是可靠的 :)

如果您的客户端和服务器都正确实现了 TCP(例如,正确处理干净关闭),则客户端或服务器是否关闭连接并不重要。既然听起来你管理双方,这应该不是问题。

您的问题似乎与服务器正确关闭有关。当套接字的一侧关闭时,另一侧将Read 长度为0 - 这是您的通信结束的消息。您很可能在您的服务器代码中忽略了这一点 - 这是一种特殊情况,表示“您现在可以安全地处理这个套接字,现在就去做”。

在您的情况下,服务器端关闭似乎是最合适的。

但实际上,TCP 相当复杂。互联网上的大多数示例都存在严重缺陷(尤其是 C# 的示例 - 例如,找到 C++ 的好示例并不难)并且忽略了协议的许多重要部分,这无济于事。我有一个可能对你有用的简单示例 - https://github.com/Luaancz/Networking/tree/master/Networking%20Part%201 它仍然不是完美的 TCP,但它比 MSDN 示例要好得多。

【讨论】:

“当套接字的一侧关闭时,另一侧将以 0 的长度读取 - 这就是你的通信结束的消息” - 这是我需要弄清楚连接的提示被另一个同伴关闭,而不仅仅是睡眠。谢谢!【参考方案2】:

保罗,你自己做了一些很棒的研究。我也一直在这个领域工作。我发现一篇关于 TIME_WAIT 主题非常有用的文章是这样的:

http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html

它有一些特定于 Linux 的问题,但所有 TCP 级别的内容都是通用的。

最终双方都应该关闭(即完成 FIN/ACK 握手),因为您不希望 FIN_WAIT 或 CLOSE_WAIT 状态持续存在,这只是“坏” TCP。我会避免使用 RST 来强制关闭连接,因为这可能会导致其他地方出现问题,并且感觉就像是一个可怜的网民。

确实,TIME_WAIT 状态将发生在最先终止连接的一端(即发送第一个 FIN 数据包),您应该优化以在连接流失最少的一端首先关闭连接。

在 Windows 上,默认情况下,每个 IP 将有超过 15,000 个 TCP 端口可用,因此您需要适当的连接搅动才能实现这一目标。 TCB 跟踪 TIME_WAIT 状态的内存应该是完全可以接受的。

https://support.microsoft.com/kb/929851

还需要注意的是,TCP 连接可以是半关闭的。也就是说,一端可以选择关闭连接以进行发送,但将其保持打开状态以进行接收。在 .NET 中是这样完成的:

tcpClient.Client.Shutdown(SocketShutdown.Send);

http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.shutdown.aspx

在将部分 netcat 工具从 Linux 移植到 PowerShell 时,我发现这是必要的:

http://www.powershellmagazine.com/2014/10/03/building-netcat-with-powershell/

我必须重申一个建议,即如果您可以保持连接打开和空闲直到您再次需要它,这通常会对减少 TIME_WAIT 产生巨大影响。

除此之外,尝试测量 TIME_WAIT 何时成为问题...确实需要大量连接搅动才能耗尽 TCP 资源。

我希望这对你有所帮助。

【讨论】:

合并客户端肯定会对吞吐量产生很大影响。【参考方案3】:

哪些方法是正确的方法,为什么? (假设我希望客户端成为“主动关闭”端)

在理想情况下,您会让您的服务器向客户端发送带有特定操作码的RequestDisconnect 数据包,然后客户端通过在接收到该数据包时关闭连接来处理该数据包。这样一来,您就不会在服务器端得到陈旧的套接字(因为套接字是资源,而资源是有限的,所以陈旧是一件坏事)。

如果客户端随后通过释放套接字执行其断开序列(或者如果您使用的是TcpClient,则调用Close(),它会将套接字置于服务器上的CLOSE_WAIT 状态,这意味着连接正在关闭中。

有没有更好的方法来实现方法 3?我们希望由客户端发起关闭(以便客户端留下 TIME_WAIT),但是当客户端关闭时,我们也希望关闭服务器上的连接。

是的。同上,让服务器发送一个数据包请求客户端关闭它与服务器的连接。

我的场景实际上与 Web 服务器相反;我有一个客户端,可以连接和断开许多不同的远程机器。我宁愿服务器将连接卡在 TIME_WAIT 中,以释放客户端上的资源。在这种情况下,我应该让服务器执行主动关闭,并将睡眠/关闭放在我的客户端上吗?

是的,如果这是您想要的,请随时在服务器上调用 Dispose 以摆脱客户端。

附带说明,您可能需要考虑使用原始 Socket 对象而不是 TcpClient,因为它非常有限。如果您直接对套接字进行操作,您可以使用SendAsync 和所有其他用于套接字操作的异步方法。我会避免使用手动 Thread.Sleep 调用 - 这些操作本质上是异步的 - 在将内容写入流后断开连接应该在 SendAsync 的回调中完成,而不是在 Sleep 之后完成。

【讨论】:

以上是关于关闭 TcpListener 和 TcpClient 连接的正确顺序(哪一侧应该是主动关闭)的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法检查连接是否已关闭然后完成线程?

C# TcpListener如何知道客户端已经断开连接

TcpListener 类

C#使用TcpListener和TcpClient实现简单通信

C# 在客户端和服务器上使用 TcpListener

TCPListener和TCPClient之间的通信代码