【发布时间】: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_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_WAIT),但是当客户端关闭时,我们也希望关闭服务器上的连接。
- 我的场景实际上与Web服务器相反;我有一个客户端,可以连接和断开许多不同的远程机器。我宁愿服务器将连接卡在
TIME_WAIT中,以释放客户端上的资源。在这种情况下,我应该让服务器执行主动关闭,并将睡眠/关闭放在我的客户端上吗?
自己尝试的完整代码在这里:
https://gist.github.com/PaulStovell/a58cd48a5c6b14885cf3
编辑:另一个有用的资源:
对于确实建立出站连接以及接受入站连接的服务器,那么黄金法则是始终确保如果 TIME_WAIT 需要发生,它会在另一个对等方而不是服务器上结束。最好的方法是永远不要从服务器启动主动关闭,不管是什么原因。如果您的对等方超时,请使用 RST 中止连接,而不是关闭它。如果您的对等方发送无效数据、中止连接等。想法是,如果您的服务器从不启动主动关闭,则它永远不会累积 TIME_WAIT 套接字,因此永远不会受到它们引起的可伸缩性问题的影响。虽然很容易看出如何在发生错误情况时中止连接,但正常的连接终止呢?理想情况下,您应该在协议中设计一种方式,让服务器告诉客户端它应该断开连接,而不是简单地让服务器发起主动关闭。因此,如果服务器需要终止连接,服务器会发送应用程序级别的“我们完成”消息,客户端将其作为关闭连接的理由。如果客户端未能在合理的时间内关闭连接,则服务器将中止连接。
在客户端,事情稍微复杂一些,毕竟,必须有人发起主动关闭才能干净地终止 TCP 连接,如果是客户端,那么 TIME_WAIT 将结束。但是,让 TIME_WAIT 最终出现在客户端有几个优点。首先,如果由于某种原因,客户端由于 TIME_WAIT 中套接字的积累而最终出现连接问题,则它只是一个客户端。其他客户端不会受到影响。其次,快速打开和关闭与同一台服务器的 TCP 连接效率低下,因此除了 TIME_WAIT 问题之外,尝试将连接保持更长的时间而不是更短的时间是有意义的。不要设计一个客户端每分钟连接到服务器并通过打开一个新连接来实现的协议。而是使用持久连接设计,并且仅在连接失败时重新连接,如果中间路由器拒绝在没有数据流的情况下保持连接打开,那么您可以实现应用程序级别 ping、使用 TCP 保持活动或只是接受路由器正在重置您的连接;好消息是您没有累积 TIME_WAIT 套接字。如果您在连接上所做的工作自然是短暂的,那么请考虑某种形式的“连接池”设计,从而使连接保持打开和重用。最后,如果您绝对必须快速打开和关闭从客户端到同一服务器的连接,那么也许您可以设计一个您可以使用的应用程序级别的关闭序列,然后在此之后进行中止关闭。您的客户端可以发送“我完成了”消息,然后您的服务器可以发送“再见”消息,然后客户端可以中止连接。
【问题讨论】:
标签: .net sockets tcpclient tcplistener