首页 文章

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

提问于
浏览
17

我读了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显示 both the client and server 中的很多套接字卡在 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状态 .

问题:

  • 这些方法中哪一种是正确的方法,为什么? (假设我希望客户端成为'active close'方)

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

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

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

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

Edit :另一个有用的资源:

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

对于确实 Build 出站连接以及接受入站连接的服务器,黄金法则始终确保如果需要发生TIME_WAIT,则最终在另一个对等体而不是服务器上 . 执行此操作的最佳方法是永远不要从服务器启动主动关闭,无论原因是什么 . 如果您的对等体超时,则使用RST中止连接而不是关闭它 . 如果您的对等方发送无效数据,则中止连接等 . 这个想法是,如果您的服务器永远不会启动活动关闭,它永远不会累积TIME_WAIT套接字,因此永远不会遇到它们导致的可伸缩性问题 . 虽然在发生错误情况时很容易看到如何中止连接,但正常的连接终止呢?理想情况下,您应该在协议中设计一种方法,让服务器告诉客户端它应该断开连接,而不是简单地让服务器发起主动关闭 . 因此,如果服务器需要终止连接,则服务器发送应用程序级别“我们已完成”消息,客户端将此消息作为关闭连接的理由 . 如果客户端无法在合理的时间内关闭连接,则服务器将中止连接 . 在客户端,事情稍微复杂一点,毕竟,有人必须发起一个主动关闭以干净地终止TCP连接,如果它是客户端,那么TIME_WAIT将结束 . 但是,将TIME_WAIT结束在客户端上有几个优点 . 首先,如果出于某种原因,由于TIME_WAIT中套接字的累积,客户端最终会遇到连接问题,那么它只是一个客户端 . 其他客户不会受到影响 . 其次,快速打开和关闭到同一服务器的TCP连接是低效的,因此在TIME_WAIT问题之外尝试维持连接的时间较长而不是较短的时间段是有意义的 . 不要设计客户端每分钟连接到服务器的协议,并通过打开新连接来实现 . 而是使用持久连接设计,只在重新连接时才使用连接失败,如果中间路由器拒绝在没有数据流的情况下保持连接打开,那么你可以实现应用程序级ping,使用TCP keep alive或者只是接受路由器正在重置你的连接;好处是你没有累积TIME_WAIT套接字 . 如果您在连接上所做的工作自然是短暂的,那么请考虑某种形式的“连接池”设计,从而保持连接的开放和重用 . 最后,如果您绝对必须从客户端快速打开和关闭连接到同一服务器,那么您可以设计一个可以使用的应用程序级别关闭序列,然后关闭它 . 您的客户端可以发送“我已完成”消息,然后您的服务器可以发送“再见”消息,然后客户端可以中止连接 .

3 回答

  • 3

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

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

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

    您的问题似乎与服务器上的正确关闭有关 . 当套接字的一边关闭时,另一边将 Read 的长度为 0 - 这很可能在服务器代码中忽略了这一点 - 这是一个特殊情况,即"you can now safely dispose of this socket, do it now" .

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

    但实际上,TCP相当复杂 . 例如,它并不太难以找到C的良好样本,并且忽略了协议的许多重要部分 . 我有一个可能适合你的东西的简单样本 - 例如,它仍然不比MSDN样本好得多.785283_ .

  • 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端口,所以你需要一个合适的连接流失来实现这一点 . 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资源 .

    我希望其中一些有用 .

  • 1

    这些方法中哪一种是正确的方法,为什么? (假设我希望客户成为'主动关闭'一方)

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

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

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

    是 . 与上面相同,让服务器发送数据包以请求客户端关闭其与服务器的连接 .

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

    是的,如果's what you'之后可以随意在服务器上调用 Dispose 来摆脱客户端 .

    另外,您可能希望使用原始Socket对象而不是 TcpClient ,因为它非常有限 . 如果直接操作套接字,则可以使用 SendAsync 以及所有其他异步方法进行套接字操作 . 使用手动 Thread.Sleep 调用是我应该在 SendAsync 的回调中而不是在 Sleep 之后写入某些内容 .

相关问题