有没有办法检测远程对等端已关闭TCP套接字,而不读取它?

首先,一些背景来解释动机:我正在研究一个非常简单的基于select()的TCP“镜像代理”,它允许两个防火墙的客户端间接地相互通信。 两个客户端都连接到此服务器,并且只要两个客户端都连接,客户端A发送到服务器的任何TCP字节都将转发到客户端B,反之亦然。

这或多或少有效,只有一个小问题:如果客户端A连接到服务器并在客户端B连接之前开始发送数据,则服务器没有任何地方可以放置数据。 我不想在RAM中缓冲它,因为这可能最终使用大量的RAM; 我也不想丢弃数据,因为客户端B可能需要它。 所以我选择了第三个选项,即在客户端B连接之前,不要在客户端A的套接字上选择() – for-read-ready。 这样客户端A就会阻塞,直到一切都准备就绪。

这或多或少也有效,但是在客户端A的套接字上没有选择准备就绪的副作用是,如果客户端A决定关闭他与服务器的TCP连接,则服务器不会收到有关该事实的通知 – – 至少,直到客户端B出现并且服务器最终在客户端A的套接字上选择for-ready-ready,读取任何未决数据,然后获得套接字关闭通知(即recv()返回0)。

如果服务器有一些方法知道(及时)客户端A关闭他的TCP连接,我更喜欢它。 有没有办法知道这个? 在这种情况下轮询是可以接受的(例如,我可以让select()每分钟唤醒一次并在所有套接字上调用IsSocketStillConnected(sock),如果存在这样的函数)。

如果要检查套接字是否实际已关闭而不是数据,可以在recv()上添加MSG_PEEK标志以查看数据是否到达或者是否为0或出现错误。

 /* handle readable on A */ if (B_is_not_connected) { char c; ssize_t x = recv(A_sock, &c, 1, MSG_PEEK); if (x > 0) { /* ...have data, leave it in socket buffer until B connects */ } else if (x == 0) { /* ...handle FIN from A */ } else { /* ...handle errors */ } } 

即使A在发送一些数据后关闭,您的代理可能希望在将FIN转发给B之前先将该数据转发给B,因此没有必要知道A在连接上发送了FIN,而不是在读取所有数据之后它发送了。

在双方发送FIN之前,TCP连接不被视为关闭。 但是,如果A强制关闭其端点,则在您尝试在其上发送数据并接收EPIPE之前不会知道(假设您已经抑制了SIGPIPE )。


在阅读了您的镜像代理应用程序之后,由于这是一个防火墙遍历应用程序,您似乎确实需要一个小的控制协议来允许您validation这些对等实际上是否允许彼此通信。 如果您有一个控制协议,那么您可以使用许多解决方案,但我提倡的那个解决方案是将其中一个连接描述为服务器,另一个连接将自身描述为客户端。 然后,如果没有服务器,则可以重置客户端的连接以进行连接。 您可以让服务器等待客户端连接达到某个超时。 服务器不应启动任何数据,如果没有连接的客户端,则可以重置服务器连接。 这消除了为死连接缓冲数据的问题。

看来我的问题的答案是“不,除非你愿意并且能够修改你的TCP堆栈以获得对必要的私有套接字状态信息的访问”。

由于我无法做到这一点,我的解决方案是重新设计代理服务器以始终从所有客户端读取数据,并丢弃从其合作伙伴尚未连接的客户端到达的任何数据。 这不是最佳的,因为这意味着通过代理的TCP流不再具有TCP使用程序所期望的可靠有序传递的类似流的属性,但它足以满足我的目的。

我看不到你遇到的问题。 假设A连接到服务器发送一些数据并关闭,它不需要任何消息。 一旦服务器读取套接字A并将数据发送到B,服务器将不会读取其数据。第一次读取将返回已发送的数据,第二次返回0或-1,无论是套接字是关闭,服务器关闭B.让我们假设A发送大量数据,A的send()方法将阻塞,直到服务器开始读取并消耗缓冲区。

我会使用一个带有select的函数,它返回0,1,2,11,22或-1,其中;

  • 0 =任一套接字中没有数据(超时)
  • 1 = A有要读取的数据
  • 2 = B有要读取的数据
  • 11 =套接字有错误(已断开连接)
  • 22 = B套接字有错误(断开连接)
  • -1:一个/两个套接字无效

     int WhichSocket(int sd1, int sd2, int seconds, int microsecs) { fd_set sfds, efds; struct timeval timeout={0, 0}; int bigger; int ret; FD_ZERO(&sfds); FD_SET(sd1, &sfds); FD_SET(sd2, &sfds); FD_SET(sd1, &efds); FD_SET(sd2, &efds); timeout.tv_sec=seconds; timeout.tv_usec=microsecs; if (sd1 > sd2) bigger=sd1; else bigger=sd2; // bigger is necessary to be Berkeley compatible, Microsoft ignore this param. ret = select(bigger+1, &sfds, NULL, &efds, &timeout); if (ret > 0) { if (FD_ISSET(sd1, &sfds)) return(1); // sd1 has data if (FD_ISSET(sd2, &sfds)) return(2); // sd2 has data if (FD_ISSET(sd1, &efds)) return(11); // sd1 has an error if (FD_ISSET(sd2, &efds)) return(22); // sd2 has an error } else if (ret < 0) return -1; // one of the socket is not valid return(0); // timeout } 

对我来说,解决方案是轮询套接字状态。

在Windows 10上,以下代码似乎有效(但其他系统似乎存在等效实现):

 WSAPOLLFD polledSocket; polledSocket.fd = socketItf; polledSocket.events = POLLRDNORM | POLLWRNORM; if (WSAPoll(&polledSocket, 1, 0) > 0) { if (polledSocket.revents &= (POLLERR | POLLHUP)) { // socket closed return FALSE; } } 

如果您的代理必须是任何协议的通用代理,那么您还应该处理那些发送数据并在发送后立即调用close客户端(仅限单向数据传输)。

因此,如果客户端A在连接打开到B之前发送数据并关闭连接,请不要担心,只需将数据正常转发到B(当打开B连接时)。

无需为此方案实施特殊处理。

您的代理将在以下情况下检测到已关闭

  • read连接到B并读取A中的所有待处理数据后, read返回零。 要么
  • 您的程序尝试将数据(从B)发送到A.

您可以通过尝试写入每个套接字的文件描述符来检查套接字是否仍然连接。 然后,如果写入的返回值为-1或者errno = EPIPE,则表示已关闭套接字。
例如

 int isSockStillConnected(int *fileDescriptors, int numFDs){ int i,n; for (i=0;i