接受TCP连接,但写入数据会导致它使用陈旧连接

时间:2012-07-02 09:12:19

标签: c++ sockets tcp berkeley-sockets

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

它在10个连接/断开循环中运行良好。第十个(随机发生)连接让服务器接受连接,然后当它实际写入它时(通常是30 + s之后),根据Wireshark(见截图),它看起来像是在写一个旧的陈旧连接,使用客户端已经找到的端口号(前一段时间),但服务器尚未找到。因此客户端和服务器连接似乎不同步 - 客户端建立新连接,服务器尝试写入前一个连接。一旦进入这种破碎状态,每次后续连接尝试都会失败。超过最大无线范围可以启动破碎状态半分钟(如前10个情况中的9个有效,但有时会导致破坏状态)。

Wireshark screenshot behind link

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

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

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

// 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()调用之间,客户端的端口将更改为以前使用的客户端端口。

所以问题是:服务器如何接受连接(从而打开文件描述符),然后通过先前(现在陈旧和死亡)的连接/文件描述符发送数据?在缺少的系统调用中是否需要某种选项?

2 个答案:

答案 0 :(得分:1)

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

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

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

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

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

服务器进程仍在为挂起的客户端准备数据。它没有注意到其他一切。同时,另一个客户连接。内核完成握手。这个新客户端暂时不会受到服务器进程的任何关注,但在内核级别,第二个连接现在在两端都是ESTABLISHED。

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

客户端获取此数据包并且其内核使用RST回复,因为它知道服务器不知道的内容:客户端套接字已经关闭以进行读取和写入,可能已关闭,并且可能已经忘记了。

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

现在服务器终于准备接受()下一个客户端了。它开始了另一个缓慢的准备步骤,并且它进一步落后于计划,因为第二个客户已经等待了一段时间。在客户端连接速度降低到服务器可以处理的速度之前,问题将不断恶化。

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

答案 1 :(得分:0)

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

原因的另一半是与问题中遗漏的信息有关(我认为这与上面的错误假设无关)。有一个主连接端口(称之为A),以及这个问题的二级麻烦连接端口(称之为B)。正确的连接顺序是A建立一个连接(A1),然后B尝试在200ms的时间范围内建立一个连接(它将成为B1)...我已经将超时从100ms加倍,这是在很久以前写的,所以我以为我很慷慨!)。如果它在200ms内没有得到B连接,则它会丢弃A1。然后,B1与服务器的内核建立连接,等待被接受。它仅在A2建立连接时在下一个连接周期被接受,并且客户端也发送B2连接。服务器接受A2连接,然后获取B队列上的第一个连接,即B1(尚未被接受 - 队列看起来像B1,B2)。这就是当客户端断开B1时服务器没有为B1发送FIN的原因。所以服务器的两个连接是A2和B1,显然不同步。它尝试写入B1,这是一个死连接,所以它丢弃A2和B1。然后下一对是A3和B2,它们也是无效对。在服务器进程被终止并且TCP连接都被重置之前,它们永远不会从不同步中恢复。

所以解决办法就是将B套接字等待的超时时间从200ms更改为5s。这么简单的解决方法让我头疼了好几天(并在将它放在stackoverflow上的24小时内修复)!我还通过将socket B添加到主select()调用,然后接受()它并立即关闭()它来使它从杂散B连接中恢复(这只有在B连接花费超过5秒时才会建立) )。感谢@AlanCurry建议将其添加到select()并添加关于listen()backlog参数的拼图作为提示。