.net 关闭TcpListener和TcpClient连接的正确顺序(哪一侧应为主动关闭)

.net 关闭TcpListener和TcpClient连接的正确顺序(哪一侧应为主动关闭),.net,sockets,tcpclient,tcplistener,.net,Sockets,Tcpclient,Tcplistener,我看到上面写着: 因此,发起终止的对等方——即首先调用close()——将以TIME_WAIT状态结束。[……] 但是,服务器上有很多处于TIME_WAIT状态的套接字可能会出现问题,因为这可能最终会阻止新连接被接受。[……] 相反,请设计应用程序协议,使连接终止始终从客户端启动。如果客户机始终知道何时已读取所有剩余数据,则可以启动终止序列。例如,浏览器从内容长度HTTP头知道何时已读取所有数据并可以启动关闭。(我知道,在HTTP 1.1中,它会将其打开一段时间,以便可能的重用,然后将其关闭。)

我看到上面写着:

因此,发起终止的对等方——即首先调用close()——将以TIME_WAIT状态结束。[……]

但是,服务器上有很多处于TIME_WAIT状态的套接字可能会出现问题,因为这可能最终会阻止新连接被接受。[……]

相反,请设计应用程序协议,使连接终止始终从客户端启动。如果客户机始终知道何时已读取所有剩余数据,则可以启动终止序列。例如,浏览器从内容长度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);
        }
    }
}
现在我不再有任何
等待时间
,但在
CLOSE\u WAIT
FIN\u 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();
var tcpClient=listener.EndAcceptTcpClient(ar);
ThreadPool.QueueUserWorkItem(委托
{
var stream=tcpClient.GetStream();
ReadSomeData(流);
写媒体(流);

Thread.Sleep(100);//这正是TCP的工作方式,您无法避免。您可以为服务器上的等待时间或等待时间设置不同的超时,但仅此而已

之所以这样,是因为在TCP上,一个数据包可以到达一个你很久以前就已经关闭的套接字。如果你已经在同一个IP和端口上打开了另一个套接字,它会接收到前一个会话的数据,这会把它弄糊涂的。特别是考虑到大多数人认为TCP是可靠的: 如果您的客户机和服务器都正确地实现了TCP(例如,正确地处理干净的关机),那么客户机或服务器是否关闭了连接实际上并不重要。因为这听起来像是您管理了双方,所以这不应该是个问题


您的问题似乎与服务器的正确关闭有关。当套接字的一侧关闭时,另一侧将读取长度为
0
-这是您的消息,表明通信已结束。您很可能在服务器代码中忽略了这一点-这是一种特殊情况,表示“您现在可以安全地处置此套接字,请立即执行”

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

但是,TCP相当复杂,互联网上的大多数样本都是严重缺陷(尤其是C的样本),例如,不难找到C++的好样本。忽略协议的许多重要部分。我有一个简单的示例,它可能对您有用-它仍然不是完美的TCP,但它比MSDN示例要好得多,例如

以下哪种方法是正确的,为什么?(假设我希望客户是“主动关闭”方)

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

如果客户端随后通过处理套接字(或调用
Close()
执行其断开连接顺序,如果您使用的是
TcpClient
,则客户端将在服务器上将套接字置于
Close\u WAIT
状态,这意味着连接正在关闭过程中

有没有更好的方法来实现方法3?我们希望关闭由客户端启动(这样客户端就有了等待的时间),但是当客户端关闭时,我们还希望关闭服务器上的连接

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

我的场景实际上与web服务器相反;我有一个单独的客户端,可以连接和断开与许多不同远程机器的连接。我宁愿服务器的连接停留在时间上,而不是等待,以释放客户端上的资源。在这种情况下,我应该让服务器执行活动关闭,并在客户端上执行睡眠/关闭吗

是的,如果这是您想要的,请随时在服务器上调用
Dispose
,以摆脱客户端


另一方面,您可能希望研究使用原始对象,而不是
TcpClient
,因为它非常有限。如果您直接对套接字进行操作,您可以使用
sendsync
和所有其他用于套接字操作的异步方法。使用手动
Thread.Sleep
调用是我应该避免的事情-这些操作操作本质上是异步的-在您向流写入某些内容之后断开连接应该在
SendAsync
的回调中进行,而不是在
睡眠之后进行。Paul,您自己也做了一些伟大的研究。我也在这方面做了一些工作。我发现关于时间等待主题的一篇非常有用的文章是这:

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

最终双方都应该关闭(即完成FIN/ACK握手),因为您不希望FIN\u WAIT或close\u WAIT状态延迟,这只是“坏”TCP。我会避免使用RST强制关闭连接,因为这可能会在其他地方造成问题
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();
tcpClient.Client.Shutdown(SocketShutdown.Send);