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

时间:2013-07-17 16:24:38

标签: c select 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),如果存在这样的函数)。

6 个答案:

答案 0 :(得分:9)

如果您想检查套接字是否实际已关闭而不是数据,可以在MSG_PEEK上添加recv()标记,以查看数据是否已到达,或者您是否获得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)。


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

答案 1 :(得分:2)

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

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

答案 2 :(得分:1)

我看不到你遇到的问题。假设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
    }
    

答案 3 :(得分:1)

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

在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;
    }
}

答案 4 :(得分:0)

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

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

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

您的代理将在以下情况下检测已关闭的连接:

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

答案 5 :(得分:-1)

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

int isSockStillConnected(int *fileDescriptors, int numFDs){
     int i,n;
     for (i=0;i<numFDs;i++){
          n = write(fileDescriptors+i,"heartbeat",9);
          if (n < 0) return -1;
          if (errno == EPIPE) return -1;
     }
     //made it here, must be okay
     return 0;
}