17

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

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

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

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

4

6 回答 6

10

如果您想检查套接字是否实际关闭而不是数据,您可以添加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 连接已关闭。EPIPE但是,如果 A 强制关闭了它的端点,那么直到您尝试在其上发送数据并接收到(假设您已抑制SIGPIPE)之后,您才会知道这一点。


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

于 2013-07-17T16:42:54.083 回答
2

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

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

于 2013-08-06T05:43:02.707 回答
2

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

在 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;
    }
}
于 2017-07-10T09:38:50.407 回答
1

我没有看到你看到的问题。假设 A 连接到服务器发送一些数据并关闭,它不需要返回任何消息。服务器在 B 连接之前不会读取其数据,一旦服务器读取套接字 A 并将数据发送到 B。第一次读取将返回 A 发送的数据,第二次返回 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
    }
    
于 2013-07-17T17:52:30.133 回答
0

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

所以如果客户端A在向B打开连接之前发送了一个数据并关闭了连接,不用担心,只要将数据正常转发给B即可(当打开到B的连接时)。

无需为此场景实施特殊处理。

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

  • read在打开与 B 的连接并读取来自 A 的所有待处理数据后返回零。或者
  • 您的程序尝试将数据(从 B)发送到 A。
于 2013-07-17T20:18:50.707 回答
-1

您可以通过尝试写入每个套接字的文件描述符来检查套接字是否仍然连接。那么如果write的返回值为-1或者errno = EPIPE,就知道socket已经关闭了。
例如

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;
}
于 2013-07-17T16:47:43.473 回答