检测TCP客户端断开连接

99

假设我正在运行一个简单的服务器,并从客户端接受了连接。

如何最好地判断客户端已经断开连接?通常,客户端应该发送关闭命令,但如果它手动断开连接或完全失去网络连接怎么办?服务器如何检测或处理这种情况?


请查看此处(针对最坏情况):http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html (检查死亡节点)。 - Blauohr
4
由于有很多错误和误导性答案,这里是正确的答案:遵循你正在实现的基于TCP的协议规范。它应该规定使用超时、写入失败或其他机制来完成此任务。如果你正在设计一个协议,请确保设计一些方法来检测客户端的断开连接,如果需要的话。 - David Schwartz
10个回答

153
在TCP中,只有一种方法可以检测到有序断开连接,那就是从read()/recv()/recvXXX()函数读取时返回0。
同样地,只有一种可靠的方法可以检测到断开的连接:写入它。在向已断开的连接写入足够的次数后,TCP将进行足够的重试和超时以知道它已经断开,并最终导致write()/send()/sendXXX()函数返回-1,同时返回一个值为ECONNRESET或者某些情况下为'connection timed out' 的errno/WSAGetLastError()。需要注意的是,后者与“connect timeout”不同,后者可能发生在连接阶段。
您还应该设置合理的读取超时时间,并丢弃未能通过超时测试的连接。
关于ioctl()FIONREAD的回答是完全无意义的。它只告诉您目前在套接字接收缓冲区中有多少字节可供读取而不会阻塞。如果客户端五分钟内没有向您发送任何内容,这并不构成断开连接,但这确实会导致FIONREAD变为零。这两种情况根本不同。

3
@Jay 这个问题是关于如何检测TCP断开连接,而不是造成连接重置的原因。有许多导致“连接重置”的原因,我不同意任何一种情况都构成了“正常操作”。从定义上来说,这是一种异常情况。 - user207421
2
@user1055568 通常情况下,单个写操作只会被缓存并异步发送到网络上,除非它非常大。您需要发出足够的写操作,以便在原始写操作上所有内部计时器和重试都已耗尽时才能检测到错误。 - user207421
2
如果应用程序不持续发出写入请求,那么就不能保证在连接中断后它会发出任何写入请求。虽然在连接失败后发出的一个写入请求足以解决问题,但连接随时可能会失败,如果您无限期停止写入,则无法知道在连接失败后是否发出了任何写入请求。 - David Schwartz
3
我已经多次说过,如果这个应用程序正在等待select/epoll/kevent读取准备好的数据,那么它会被告知进行读取以捕捉错误。你一再争辩认为它必须进行更多的写入操作,但你并没有谈到读取方面。实际上,使用epoll时,不需要进行读或写操作,因为epoll可以直接发出超时信号。可能在kevent中也是如此。 - user1055568
2
@EJP,您说的话毫无意义。在单次写入超时后进行一次单次读取就足以捕获错误。如果您正在使用select/epoll/kqueue等I/O事件等待,则会在发生此情况时收到警报。 - user1055568
显示剩余22条评论

16

进一步解释一下:

如果您在运行服务器,则需要使用TCP_KEEPALIVE监视客户端连接,或自行执行类似的操作,或者对于正在运行连接的数据/协议有知识。

基本上,如果连接被杀死(即未正确关闭),则服务器将不会注意到,直到它试图向客户端写入某些内容,这就是keepalive为您实现的内容。或者,如果您更好地了解协议,也可以在活动超时后断开连接。


服务器还应该设置合理的读取超时时间,并且断开未能通过它的连接。 - user207421
断开失败的连接?如果超时时间按照默认推荐的200毫秒呢?难道不应该退回到某个合理的超时时间吗?也许这会给你带来太多的上下文切换?但是,当这样的“超时”如此之低时,仍然断开连接并不是一个明智的建议... - Jay
在Winsock2中,如果keepalive每5秒轮询一次,而我有一些阻塞的send或recv调用,那么keepalive会正常工作吗?另外,keepalive超时和间隔的最小限制是什么? - Anurag Daware
1
@EJP,那是什么操作系统?根据我上次检查的情况,大多数操作系统的默认读取超时时间为0.5-5秒...特别是针对TCP的rfc指出,TCP的默认值为0.2秒... - Jay
@Jay,我不知道你在说什么。SO_RCVTIMEO的默认值在所有操作系统上都是无限的。否则每个人都会一直遇到读取超时。你提出的200毫秒等建议是荒谬的。 - user207421
@Jay 而且你混淆了内部TCP定时器和读取超时。它们并不是同一回事。没有RFC规定了TCP/IP套接字API。 - user207421

2

2

如果您正在使用带有完成例程或完成端口的重叠(即异步)I/O,则在客户端关闭连接时(假设您有一个未完成的读取),您将立即收到通知。


不完全正确。只有当你读到流的末尾时,才会立即收到通知。如果在关闭之前从客户端传输了大量数据,则可能需要一定的时间。 - user207421

0
TCP协议中有“打开”和“关闭”程序。一旦“打开”,连接将一直保持到“关闭”。但是,许多异常情况可能会阻止数据流动。因此,确定是否可以使用链接的技术高度依赖于协议和应用程序之间的软件层。上面提到的一些方法专注于程序员以非侵入方式(读取或写入0字节)尝试使用套接字,这可能是最常见的方法。某些库中的层将为程序员提供“轮询”。例如,Win32异步(延迟)调用可以启动一个读取操作,该操作将返回无错误和0字节,以表示不能再读取套接字(可能是TCP FIN过程)。其他环境可能会使用其包装层中定义的“事件”。对于这个问题,没有单一的答案。检测套接字何时不能使用并应该关闭的机制取决于库中提供的包装器。值得注意的是,套接字本身可以被应用程序库下面的层重复使用,因此明智的做法是弄清楚您的环境如何处理Berkley Sockets接口。

0
我遇到了一个类似的问题,即在建立连接后,我的服务器会盲目地发送数据,但难以检测对方是否仍在监听。我使用了TCP_USER_TIMEOUT选项: https://man7.org/linux/man-pages/man7/tcp.7.html 要设置此选项,请不要忘记使用SOL_TCP而不是SOL_SOCKET作为级别。
unsigned int timeout = 5000; //timeout in ms

if (setsockopt(yourSocket, SOL_TCP, TCP_USER_TIMEOUT, &timeout, sizeof(timeout))<0)
    fprintf(stderr,"setsockopt(SO_SNDTIMEO) failed");

如果一条消息在发送缓冲区停留的时间超过“timeout”毫秒,将会抛出一个错误,在我的情况下,它似乎是由于阻塞的recv()函数引起的。

-3

这很容易做到:可靠而且不会混乱:

        Try
            Clients.Client.Send(BufferByte)
        Catch verror As Exception
            BufferString = verror.ToString
        End Try
        If BufferString <> "" Then
            EventLog.Text &= "User disconnected: " + vbNewLine
            Clients.Close()
        End If

不够可靠。它没有区分有序和无序的关闭,并且至少要发生两个发送才能工作,因为存在套接字发送缓冲区的问题。 - user207421

-3

我尝试了几种解决方案,但这个似乎是在 Windows 中检测主机和/或客户端断开连接的最佳方法。它适用于非阻塞套接字,并源自IBM的示例

char buf;
int length=recv(socket, &buf, 0, 0);
int nError=WSAGetLastError();
if(nError!=WSAEWOULDBLOCK&&nError!=0){
    return 0;
}   
if (nError==0){
    if (length==0) return 0;
}

一个recv()在网络上不会有任何动作,因此它无法触发任何电缆拔出等检测。只有send()才能做到这一点。 - user207421

-4

如果连接丢失,receive 的返回值将为 -1,否则它将是缓冲区的大小。

void ReceiveStream(void *threadid)
{
    while(true)
    {
        while(ch==0)
        {
            char buffer[1024];
            int newData;
            newData = recv(thisSocket, buffer, sizeof(buffer), 0);
            if(newData>=0)
            {
                std::cout << buffer << std::endl;
            }
            else
            {
                std::cout << "Client disconnected" << std::endl;
                if (thisSocket)
                {
                    #ifdef WIN32
                        closesocket(thisSocket);
                        WSACleanup();
                    #endif
                    #ifdef LINUX
                        close(thisSocket);
                    #endif
                }
                break;
            }
        }
        ch = 1;
        StartSocket();
    }
}

2
仅在发生错误时返回-1,而不是在断开连接时返回。我已经在Windows和Linux上验证过,当对等方不正常断开连接时,recv将简单地返回一个由零填充的缓冲区。 - TekuConcept
@TekuConcept 不正确。它将返回 -1,且 errno == ECONNRESET,并且不会对缓冲区做任何事情。 - user207421
根据 man 手册,你是对的!我想我忽略了这一行:“底层协议模块可能会生成并返回其他错误”。 - TekuConcept

-8

使用设置了读掩码的select将返回已发出信号的句柄,但是当您使用ioctl*检查待读取的字节数时,它将为零。这表明套接字已断开连接。

这是一个关于各种检查客户端是否已断开连接的讨论:Stephen Cleary, Detection of Half-Open (Dropped) Connections

* 对于Windows,请使用ioctlsocket。


86
这绝对不是“插座已断开连接”的标志,而是表示插座接收缓冲区中没有数据的标志。这两者远远不同。你引用的文章甚至没有提到这种技术。 - user207421
3
很难相信。数据在通过校验和验证之前甚至不应进入套接字接收缓冲区。你有这个说法的来源或可重复实验吗? - user207421
2
@MarkKCowan 这只在你引用的错误中有记录,而不是在 IOCTL 的规范中有记录。任何时候都可能没有字节可读,最常见的原因是对等方没有发送任何内容。这不是一种正确的技术手段。 - user207421
2
@EJP不是表示0字节读取意味着EOF(即对等方已关闭连接)吗?如果套接字上没有任何内容,并且您尝试读取它,将会给出EWOULDBLOCK / EAGAIN错误,而不是0字节读取。 - ustulation
1
@Matthieu:你能指给我一个吗?我认为在应用程序层面上,你永远不可能得到一个0字节的TCP读取(是的,你可能会得到ACK等,但这不会传播到套接字的用户),这并不意味着EOF。 - ustulation
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接