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

时间:2015-01-14 07:38:17

标签: .net sockets tcpclient tcplistener

我读了this answer on a previous question,其中说:

  

因此,启动终止的对等体 - 即首先调用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);
        }
    }
}

运行20个客户端之后,TCPView显示了很多来自客户端和服务器的套接字卡在TIME_WAIT中,这需要相当长的时间才能消失。

enter image description here

方法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等不同阶段留下了插座,这也需要很长时间才能消失。

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_WAIT),但是当客户端关闭时,我们还希望关闭服务器上的连接。
  3. 我的方案实际上与Web服务器相反;我有一个客户端连接和断开许多不同的远程计算机。我宁愿服务器在TIME_WAIT中卡住连接,以释放客户端上的资源。在这种情况下,我应该让服务器执行主动关闭,并将睡眠/关闭放在我的客户端上吗?
  4. 自己试用的完整代码就在这里:

    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套接字,因此永远不会遇到它们导致的可伸缩性问题。虽然很容易看到在发生错误情况时如何中止连接正常连接终止?理想情况下,您应该在协议中设计一种方法,让服务器告诉客户端它应该断开连接,而不是简单地让服务器发起主动关闭。因此,如果服务器需要终止连接,则服务器发送应用程序级别&#34;我们已完成&#34;客户端将关闭连接的消息作为理由。如果客户端无法在合理的时间内关闭连接,则服务器将中止连接。

         

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

3 个答案:

答案 0 :(得分:3)

这就是TCP的工作方式,你无法避免。您可以为服务器上的TIME_WAIT或FIN_WAIT设置不同的超时,但这是关于它的。

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

如果客户端和服务器都正确实现TCP(例如,正确处理干净关闭),客户端或服务器是否关闭连接并不重要。因为听起来你管理双方,所以不应该成为一个问题。

您的问题似乎与服务器上的正确关闭有关。当插座的一侧关闭时,另一侧将Read的长度为0 - 这表明通信已结束。您很可能在服务器代码中忽略了这一点 - 它是一个特殊情况,表示&#34;您现在可以安全地处理此套接字,立即执行此操作&#34;。

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

但实际上,TCP相当复杂。互联网上的大多数样本都存在严重缺陷(特别是C#的样本 - 例如,为C ++找到一个好的样本并不难,并且忽略了许多重要的内容)并没有帮助协议的一部分。我已经得到了一些可能对您有用的简单示例 - https://github.com/Luaancz/Networking/tree/master/Networking%20Part%201它仍然不是完美的TCP,但它比MSDN样本要好得多,例如。

答案 1 :(得分:3)

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

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

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

最终双方都应该关闭(即完成FIN / ACK握手),因为你不希望FIN_WAIT或CLOSE_WAIT状态延迟,这只是&#34;坏&#34; 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资源。

我希望其中一些有用。

答案 2 :(得分:1)

  

这些方法中哪一种是正确的方法,为什么? (假设我希望客户成为&#39;活跃的关闭方)

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

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

  

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

是。如上所述,让服务器发送数据包以请求客户端关闭其与服务器的连接。

  

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

是的,如果您有什么意思,请随时致电服务器上的Dispose以取消客户。

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