服务器(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()调用之间,客户端的端口将更改为以前使用的客户端端口。
所以问题是:服务器如何接受连接(从而打开文件描述符),然后通过先前(现在陈旧和死亡)的连接/文件描述符发送数据?在缺少的系统调用中是否需要某种选项?
答案 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)
原因的另一半是与问题中遗漏的信息有关(我认为这与上面的错误假设无关)。有一个主连接端口(称之为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参数的拼图作为提示。