1

服务器 (192.168.1.5:3001) 运行 Linux 3.2,设计为一次只接受一个连接。客户端 (192.168.1.18) 正在运行 Windows 7。连接是无线连接。这两个程序都是用 C++ 编写的。

它在 10 个连接/断开周期中有 9 个效果很好。根据 Wireshark(见截图)的说法,第十个(随机发生)连接让服务器接受连接,然后当它稍后实际写入它时(通常是 30 多秒后),看起来它正在写入一个旧的陈旧连接,带有客户端已 FIN(不久前)但服务器尚未 FIN 的端口号。所以客户端和服务器连接似乎不同步——客户端建立新连接,服务器尝试写入前一个连接。一旦进入这种断开状态,每个后续的连接尝试都会失败。可以通过超出最大无线范围半分钟来启动断开状态(如之前 10 次中有 9 次这样有效,但有时会导致断开状态)。

链接后面的 Wireshark 截图

屏幕截图中的红色箭头表示服务器何时开始发送数据(Len != 0),即客户端拒绝它并向服务器发送 RST 的时间点。右边缘下方的彩色点表示使用的每个客户端端口号的单一颜色。请注意在该颜色的其余点之后出现的一两个点如何很好地出现(并注意时间列)。

问题看起来像是在服务器端,因为如果您终止服务器进程并重新启动,它会自行解决(直到下次发生)。

希望代码不会太不寻常。我将listen()中的队列大小参数设置为0,我认为这意味着它只允许一个当前连接而没有挂起的连接(我尝试了1,但问题仍然存在)。在代码中显示“//错误”的跟踪打印中,没有任何错误出现。

// Server code

mySocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (mySocket == -1)
{
  // error
}

// Set non-blocking
const int saveFlags = ::fcntl(mySocket, F_GETFL, 0);
::fcntl(mySocket, F_SETFL, saveFlags | O_NONBLOCK);

// Bind to port

// Union to work around pointer aliasing issues.
union SocketAddress
{
  sockaddr myBase;
  sockaddr_in myIn4;
};

SocketAddress address;
::memset(reinterpret_cast<Tbyte*>(&address), 0, sizeof(address));
address.myIn4.sin_family = AF_INET;
address.myIn4.sin_port = htons(Port);
address.myIn4.sin_addr.s_addr = INADDR_ANY;
if (::bind(mySocket, &address.myBase, sizeof(address)) != 0)
{
  // error
}
if (::listen(mySocket, 0) != 0)
{
  // error
}


// main loop
{
  ...
  // Wait for a connection.
  fd_set readSet;
  FD_ZERO(&readSet);
  FD_SET(mySocket, &readSet);
  const int aResult = ::select(getdtablesize(), &readSet, NULL, NULL, NULL);
  if (aResult != 1)
  {
    continue;
  }
  // A connection is definitely waiting.
  const int fileDescriptor = ::accept(mySocket, NULL, NULL);
  if (fileDescriptor == -1)
  {
    // error
  }

  // Set non-blocking
  const int saveFlags = ::fcntl(fileDescriptor, F_GETFL, 0);
  ::fcntl(fileDescriptor, F_SETFL, saveFlags | O_NONBLOCK);

  ...
  // Do other things for 30+ seconds.
  ...
  const int bytesWritten = ::write(fileDescriptor, buffer, bufferSize);
  if (bytesWritten < 0)
  {
    // THIS FAILS!! (but succeeds the first ~9 times)
  }

  // Finished with the connection.
  ::shutdown(fileDescriptor, SHUT_RDWR);
  while (::close(fileDescriptor) == -1)
  {
    switch(errno)
    {
    case EINTR:
      // Break from the switch statement. Continue in the loop.
      break;
    case EIO:
    case EBADF:
    default:
      // error
      return;
    }
  }
}

因此,在 accept() 调用(假设这正是发送 SYN 数据包的时间点)和 write() 调用之间的某个地方,客户端的端口被更改为先前使用的客户端端口。

所以问题是:服务器怎么会接受一个连接(并因此打开一个文件描述符),然后通过以前的(现在是陈旧的和死的)连接/文件描述符发送数据?它是否需要在缺少的系统调用中提供某种选项?

4

2 回答 2

1

我正在提交一个答案来总结我们在评论中发现的内容,即使它还不是一个完整的答案。我认为它确实涵盖了要点。

您有一台服务器一次处理一个客户端。它接受一个连接,为客户端准备一些数据,写入数据,然后关闭连接。问题是准备数据的步骤有时比客户愿意等待的时间长。当服务器忙于准备数据时,客户端放弃了。

在客户端,当套接字关闭时,会发送一个 FIN 通知服务器客户端没有更多数据要发送。客户端的套接字现在进入 FIN_WAIT1 状态。

服务器收到 FIN 并回复 ACK。(ACK 由内核完成,无需用户空间进程的任何帮助。)服务器套接字进入 CLOSE_WAIT 状态。套接字现在是可读的,但服务器进程没有注意到,因为它正忙于它的数据准备阶段。

客户端收到 FIN 的 ACK 并进入 FIN_WAIT2 状态。我不知道客户端用户空间发生了什么,因为您没有显示客户端代码,但我认为这并不重要。

服务器进程仍在为挂断的客户端准备数据。它对其他一切都视而不见。同时,另一个客户端连接。内核完成握手。这个新客户端暂时不会得到服务器进程的任何关注,但在内核级别,第二个连接现在在两端都已建立。

最终,服务器的数据准备(为第一个客户端)完成。它尝试 write()。服务器的内核不知道第一个客户端不再愿意接收数据,因为 TCP 不传达该信息!所以写入成功并且数据被发送出去(你的wireshark列表中的数据包10711)。

客户端得到这个数据包,它的内核用 RST 回复,因为它知道服务器不知道的东西:客户端套接字已经为读写而关闭,可能已经关闭,也可能已经被遗忘。

在 Wireshark 跟踪中,服务器似乎只想向客户端发送 15 个字节的数据,因此它可能成功完成了 write()。但是 RST 很快就到达了,在服务器有机会执行它的 shutdown() 和 close() 之前,它会发送一个 FIN。收到 RST 后,服务器将不再在该套接字上发送任何数据包。shutdown() 和 close() 现在已执行,但没有任何在线效果。

现在服务器终于准备好接受()下一个客户端了。它开始了另一个缓慢的准备步骤,并且由于第二个客户已经等待了一段时间,所以它进一步落后于计划。这个问题会越来越严重,直到客户端连接速度减慢到服务器可以处理的程度。

修复必须是让您在准备步骤期间客户端挂断时通知服务器进程,并立即关闭套接字并转移到下一个客户端。你将如何做取决于数据准备代码的实际外观。如果它只是一个 CPU 绑定的大循环,您必须找到一些地方来插入对套接字的定期检查。或者创建一个子进程来进行数据准备和写入,而父进程只是监视套接字-如果客户端在子进程退出之前挂断,则杀死子进程。其他解决方案是可能的(例如 F_SETOWN 在套接字上发生某些事情时向进程发送信号)。

于 2012-07-03T02:12:56.527 回答
0

啊哈,成功!事实证明,在调用 accept() 之前,服务器正在接收客户端的 SYN,并且服务器的内核正在自动完成与另一个 SYN 的连接。所以肯定有一个监听队列,有两个连接在队列上等待是原因的一半。

另一半原因与问题中省略的信息有关(由于上述错误假设,我认为这无关紧要)。有一个主要的连接端口(称为 A),以及这个问题所涉及的次要的、麻烦的连接端口(称为 B)。正确的连接顺序是 A 建立一个连接(A1),然后 B 尝试建立一个连接(这将成为 B1)......在 200 毫秒的时间范围内(我已经将超时时间从很久以前写的 100 毫秒翻了一番,所以我以为我很慷慨!)。如果它在 200 毫秒内没有获得 B 连接,那么它会丢弃 A1。然后 B1 与服务器的内核建立连接,等待被接受。只有在 A2 建立连接时,它才会在下一个连接周期被接受,并且客户端也会发送 B2 连接。服务器接受 A2 连接,然后获得 B 队列上的第一个连接,即 B1(尚未被接受 - 队列看起来像 B1、B2)。这就是为什么当客户端断开 B1 时服务器没有为 B1 发送 FIN。所以服务器的两个连接是A2和B1,显然是不同步的。它尝试写入 B1,这是一个死连接,因此它丢弃 A2 和 B1。然后下一对是 A3 和 B2,它们也是无效的对。在服务器进程被杀死并且 TCP 连接全部重置之前,它们永远不会从不同步中恢复。所以服务器的两个连接是A2和B1,显然是不同步的。它尝试写入 B1,这是一个死连接,因此它丢弃 A2 和 B1。然后下一对是 A3 和 B2,它们也是无效的对。在服务器进程被杀死并且 TCP 连接全部重置之前,它们永远不会从不同步中恢复。所以服务器的两个连接是A2和B1,显然是不同步的。它尝试写入 B1,这是一个死连接,因此它丢弃 A2 和 B1。然后下一对是 A3 和 B2,它们也是无效的对。在服务器进程被杀死并且 TCP 连接全部重置之前,它们永远不会从不同步中恢复。

因此解决方案是将等待 B 套接字的超时从 200 毫秒更改为 5 秒。如此简单的修复让我摸不着头脑(并在将其放入 stackoverflow 后的 24 小时内修复)!我还通过将套接字 B 添加到主 select() 调用中,然后将其接受()并立即关闭(),使它从杂散的 B 连接中恢复(只有当 B 连接花费超过 5 秒的时间才能建立时才会发生这种情况)。感谢@AlanCurry 建议将其添加到 select() 并添加关于 listen() backlog 参数作为提示的拼图。

于 2012-07-03T07:24:47.850 回答