我阅读了之前问题的这个答案,其中说到:
因此,首先终止对等体(即先调用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
状态中,并需要花费相当一段时间才能消失。
方法二:仅关闭客户端
根据上述引用,我删除了对监听器的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_WAIT
,FIN_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_WAITs),但当客户端关闭时,我们也希望在服务器上关闭连接。
- 我的场景与Web服务器相反。我有一个单独的客户端连接并断开连接许多不同的远程计算机。我宁愿服务器在
TIME_WAIT
状态下保持连接,以释放客户端的资源。在这种情况下,我应该使服务器执行“主动关闭”,并在我的客户端上进行sleep / close操作吗?
尝试自己的完整代码在这里:
https://gist.github.com/PaulStovell/a58cd48a5c6b14885cf3
编辑:另一个有用的资源:
对于一个既建立出站连接又接受入站连接的服务器,黄金法则是要始终确保如果需要进行TIME_WAIT,则应该在对等方而不是服务器上完成。最好的方法是从服务器永远不要启动主动关闭(无论原因如何),如果对等方超时,请使用RST中止连接而不是关闭它,如果您的对等方发送无效数据,请中止连接等。这样做的想法是,如果您的服务器从不启动主动关闭,则永远不会积累TIME_WAIT套接字,因此永远不会遭受它们所引起的可扩展性问题。虽然很容易看出在出现错误情况时如何中止连接,但常规连接终止怎么办?理想情况下,您应该在协议中设计一种让服务器告诉客户端应该断开连接的方法,而不仅仅是让服务器发起主动关闭。因此,如果服务器需要终止连接,则服务器发送一个应用程序级别的“我们已经完成”消息,客户端将其视为关闭连接的原因。如果客户端未能在合理的时间内关闭连接,则服务器中止连接。
对于客户端来说,事情略微复杂,毕竟,有人必须启动主动关闭以清除TCP连接,如果是客户端,那么TIME_WAIT就会出现在客户端。但是,让TIME_WAIT出现在客户端上具有几个优点。首先,如果由于积累TIME_WAIT套接字而导致客户端出现连接问题,那么只是一个客户端受到影响,其他客户端不会受到影响。其次,快速打开和关闭与同一服务器的TCP连接是低效的,因此除了TIME_WAIT问题之外,尝试保持更长时间而不是更短时间的连接是有意义的。不要设计一种协议,其中客户端每分钟连接一次服务器并通过打开新连接来实现。相反,使用持久连接设计,仅在连接失败时重新连接,如果中间路由器拒绝在没有数据流的情况下保持连接,则可以实现应用程序级别的ping,使用TCP keep alive或者接受路由器正在重置您的连接;好处是您不会积累TIME_WAIT套接字。如果您在连接上进行的工作自然是短暂的,则考虑某种形式的“连接池”设计,以便保持连接并重复使用。最后,如果您绝对必须从客户端快速打开和关闭与同一服务器的连接,那么也许您可以设计一个应用程序级别的关闭序列,然后跟随它一个中止关闭。您的客户端可以发送一个“我已经做完了”的消息,您的服务器然后可以发送一个“再见”的消息,然后客户端可以中止连接。