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

18

我阅读了之前问题的这个答案,其中说到:

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

但是,在服务器上有大量处于TIME_WAIT状态的套接字可能会妨碍新连接的接受。[...]

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

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

方法1:双方都关闭

这是大多数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);
        }
    }
}

运行此操作后,使用TCPView检查后,发现来自客户端和服务器的许多套接字都被困在TIME_WAIT状态中,并需要花费相当一段时间才能消失。

enter image description here

方法二:仅关闭客户端

根据上述引用,我删除了对监听器的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等等,它们也需要很长时间才能消失。

TCPView, with everything closing

方法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状态,所有服务器套接字都已正确关闭:

enter image description here

这似乎符合先前链接的文章所说的:

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

问题:

  1. 哪种方法是正确的方式,为什么?(假设我希望客户端是“主动关闭”方)
  2. 是否有更好的方法来实现第3种方法?我们希望关闭由客户端发起(这样客户端就留下了TIME_WAITs),但当客户端关闭时,我们也希望在服务器上关闭连接。
  3. 我的场景与Web服务器相反。我有一个单独的客户端连接并断开连接许多不同的远程计算机。我宁愿服务器在TIME_WAIT状态下保持连接,以释放客户端的资源。在这种情况下,我应该使服务器执行“主动关闭”,并在我的客户端上进行sleep / close操作吗?

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

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 keep alive或者接受路由器正在重置您的连接;好处是您不会积累TIME_WAIT套接字。如果您在连接上进行的工作自然是短暂的,则考虑某种形式的“连接池”设计,以便保持连接并重复使用。最后,如果您绝对必须从客户端快速打开和关闭与同一服务器的连接,那么也许您可以设计一个应用程序级别的关闭序列,然后跟随它一个中止关闭。您的客户端可以发送一个“我已经做完了”的消息,您的服务器然后可以发送一个“再见”的消息,然后客户端可以中止连接。

3个回答

4
这就是TCP的工作原理,你无法避免它。你可以为你的服务器设置不同的TIME_WAIT或FIN_WAIT超时时间,但仅限于此。
这是因为在TCP上,一个数据包可能会到达你很久以前关闭的套接字。如果你已经在相同的IP和端口上打开了另一个套接字,它将接收到为之前的会话而设计的数据,这将使它混乱不堪。特别是考虑到大多数人认为TCP是可靠的 :)
如果你的客户端和服务器都正确实现了TCP(例如,正确处理了干净的关闭),那么无论是客户端还是服务器关闭连接都无关紧要。既然听起来你管理着两边,这应该不是问题。
你的问题似乎在于服务器的正确关闭。当套接字的一侧关闭时,另一侧将使用长度为0的“Read” - 这就是你通信结束的消息。你很可能忽略了服务器代码中的这个特殊情况 - 它是一种说“你现在可以安全地处理这个套接字了,马上处理。”的方式。
在你的情况下,服务器端关闭似乎是最合适的选择。
但事实上,TCP相当复杂。这并没有帮助,因为互联网上的大多数示例都有严重的缺陷(尤其是C#的示例 - 例如,找到一个好的C++示例并不太难),而且忽略了协议的许多重要部分。我有一个简单的示例,在某些情况下可能对你有用 - https://github.com/Luaancz/Networking/tree/master/Networking%20Part%201 它仍然不是完美的TCP,但比MSDN示例好得多。

1
当套接字的一侧关闭时,另一侧将以长度为0的方式读取 - 这就是您通信结束的消息。这是我需要的提示,以便确定连接是由对等方关闭的,而不仅仅是休眠。谢谢! - Paul Stovell

3
保罗,您自己也做了一些出色的研究。我也在这个领域工作了一段时间。我发现一篇关于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端口,因此您需要相当大的连接变化才能达到该限制。跟踪TIME_WAIT状态的TCB的内存应该是可以接受的。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_WAITs 产生巨大影响。

除此之外,尝试测量何时 TIME_WAITs 成为问题... 这确实需要大量的连接翻滚来耗尽 TCP 资源。

希望这些信息对你有所帮助。


将客户端进行汇集,对吞吐量确实产生了很大的影响。 - Paul Stovell

1
假设我希望客户端成为“主动关闭”方,哪种方法才是正确的?为什么?
理想情况下,您需要让服务器向客户端发送一个具有特定操作码的RequestDisconnect数据包,然后客户端在接收到该数据包时通过关闭连接来处理它。这样,您就不会在服务器端得到过期的套接字(因为套接字是资源,而资源是有限的,所以拥有过期的套接字是一件坏事)。
如果客户端随后通过处理套接字(或调用Close()(如果您正在使用TcpClient),则会将套接字置于服务器上的CLOSE_WAIT状态,这意味着连接正在关闭过程中。
实施第3种方法是否有更好的方法?我们希望由客户端发起关闭(以便客户端留下TIME_WAITs),但当客户端关闭时,我们也希望关闭服务器上的连接。
是的。与上述相同,让服务器发送一个数据包请求客户端关闭其与服务器的连接即可。
我的情况恰好与Web服务器相反;我有一个单一的客户端连接和断开多个不同的远程机器。我宁愿服务器保持在TIME_WAIT状态,以释放客户端的资源。在这种情况下,我应该让服务器执行主动关闭,并在客户端上进行休眠/关闭吗?
是的,如果你想要这样做,可以随意在服务器上调用Dispose来摆脱客户端。
另外,你可能想考虑使用原始Socket对象而不是TcpClient,因为它非常有限。如果你直接操作套接字,你可以使用SendAsync和所有其他异步套接字操作方法。使用手动的Thread.Sleep调用是我要避免的 - 这些操作是异步的 - 在写入流后断开连接应该在SendAsync的回调中完成,而不是在Sleep之后完成。

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