为什么套接字连接被阻止并且TCP内核不断重传[ACK]数据包

4
我们面临一个问题,从一段时间开始,特定的套接字连接被阻塞,客户端的TCP内核不断重传[ACK]数据包。
拓扑流如下:
   Client A ←→ Switch A ← Router A:NAT ← .. Internet .. 
               → Router B:NAT → Switch B ←→ Server B

这里是通过WireShark捕获的数据包:
A)服务器
1. 8013 > 6757 [PSH, ACK] Seq=56 Ack=132 Win=5840 Len=55     
2. 6757 > 8013 [ACK] Seq=132 Ack=111 Win=65425 Len=0     

B) 客户端

//lines 3 and 4 are exactly the same as line 1 and 2      
3. 8013 > 13000 [PSH, ACK] Seq=56 Ack=132 Win=5840 Len=55      
4. 13000 > 8013 [ACK] Seq=132 Ack=111 Win=65425 Len=0     
5. 13000 > 8013 [PSH, ACK] Seq=132 Ack=111 Win=65425 Len=17     

[TCP Retransmission]          
6. 13000 > 8013 [PSH, ACK] Seq=132 Ack=111 Win=65425 Len=17         

8013是服务器端口,6757是客户端NAT端口。

TCP内核为什么会不停地发送[ACK]数据包来告诉客户端已经收到第1个数据包(见数据包4、5和6),即使服务器已经接收到一个[ACK]数据包(见数据包2)?该连接的任一方在出现问题时都不关闭套接字。

在数据包6之后,连接丢失,我们不能再通过该套接字向服务器发送任何数据。

         psuedocode:  
         //client
         serverAddr.port =htons(8013) ;
         serverAddr.ip = inet_addr(publicIPB);
         connect(fdA, serverAddr,...);         

         //server
         listenfd = socket(,SO_STREAM,);
         localAddr.port = htons(8013);
         localAddr.ip = inet_addr(INADDR_ANY);
         bind(localAddr...)
         listen(listenfd, 100);

         ...
         //using select model
         select(fdSet, NULL, NULL, NULL);
         for(...)
         {
         if (FD_ISSET(listenfd))
            {
            ...
              }
         ...
         }

更新
UP1. 这里是重现问题的具体步骤。

  1. 有三台电脑,分别是PC1、PC2和PC3。所有三台电脑都在RouterA后面,而服务器在RouterB后面。

  2. 有两个用户,分别是U1和U2。U1从PC1登录,U2从PC3登录。U1和U2都会建立与服务器之间的TCP连接。现在U1能够通过它的TCP连接向服务器发送数据,然后服务器将所有数据中继给U2。一切正常到这一刻。

    表示与U1和服务器之间的TCP连接相对应的套接字号码: U1-OldSocketFd

  3. 不要注销U1,并拔掉PC1的电缆。然后U1从PC2登录,现在它与服务器建立了一个新的TCP连接。

    表示与U1和服务器之间的TCP连接相对应的套接字号码: U1-NewSocketFd

    从服务器端来看,当它更新其与U1的会话时,它调用close(U1-OldSocketFd)

4.1. 在第3步大约30秒后,我们发现U1无法通过它的新TCP连接向服务器发送任何数据。

4.2. 在第3步中,如果服务器不立即调用close(U1-OldSocketFd)(在U1和服务器之间的新连接建立的同一秒钟),而是在70-80秒后调用close(U1-OldSocketFd),那么一切都正常。

UP2. Router B在端口8013上使用端口转发。
UP3. 服务器运行的Linux操作系统的一些参数。

    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.tcp_tw_recycle = 1

1
有人投票认为这个话题不相关,但我觉得它是相关的。 - Celada
Steve,我想看到一次试验中的所有这些数字。这对诊断很重要。请包括本地IP地址。 - Tomas
@Tomas 我稍后会更新这篇文章,为每个TCP连接添加端口和IP地址。我现在没有它们。 - Wallace
好的。还有一个问题 - 如果您不从PC1拔出电缆会发生什么? - Tomas
Steve,你说过稍后会更新帖子并提供更多信息。我认为没有这些信息,任何人都无法回答。请看我上面的帖子。 - Tomas
显示剩余4条评论
2个回答

1
在第1个(与第3个相同)和第2个(与第4个相同)数据包经过后,您的客户端似乎向服务器传输了17个字节的数据(数据包5)。我不知道数据包5在第一个数据包交换之后多久出现,所以我不知道这是在多长时间后发生的。您的伪代码没有澄清这一点,因为它只显示套接字初始化,而没有显示哪一方在什么时间尝试传输什么数据。在这种情况下,ladder diagram可能有用,以代表您的协议交换。
无论如何,服务器显然没有确认这17个字节的数据,因此它们被重新传输(数据包6)。
除非您的网络或防火墙或NAT路由器或其他东西丢失了数据包,否则服务器应该能够接收TCP交换的早期部分,但显然无法接收数据包5或6。再次询问,先前的数据交换和数据包5之间是否有大量时间(例如,足够的时间让NAT路由器、防火墙或负载均衡器过期连接)?

1
根据您提供的问题重现步骤和 UPD3,可能是由于 net.ipv4.tcp_tw_recycle = 1 导致的。原因是内核在到期之前尝试回收 TIME_WAIT 连接(感谢 tw_recycle)。此答案解释了 tw_reuse 和 tw_recycle 的行为(NAT 部分在这里很有趣)。根据步骤和观察结果 4-1 和 4-2,当您立即调用 fclose() 时,连接进入 TIME_WAIT 状态,从那里 tw_recycle 可以启动并假定由于此端已关闭连接,套接字可以回收。由于连接从服务器的角度来看来自同一主机,tw_recycle 就会启动。当您等待一段时间再调用 fclose() 时,由于没有从服务器的角度触发断开连接,它将假定连接仍然存在,这会防止 tw_recycle 启动,可能/可能强制创建全新的连接。
根据1,为了从协议的角度保持安全,有两种情况:
  • 禁用tw_reuse和tw_recycle
  • 启用tw_reuse,启用TCP时间戳,禁用tw_recycle
考虑到你的网络拓扑结构,tw_recycle可能会始终触发无连接状态。

你认为从PC2建立的连接会和从PC1建立的连接具有相同的端口(因断开电缆而中断)吗? - Tomas
据我所了解(也通过阅读参考答案),服务器将尝试回收TIME_WAIT套接字连接,看到来自相同源IP的另一个连接(因为服务器在NAT后面)到达相同的目标IP:端口。内核期望时间戳增加,但NAT随机选择时间戳,因此超时。老实说,我无法确定/理解源端口是否重要。 - Fabio Scaccabarozzi
转念一想:源端口不应该有影响,否则回收将永远不会发生,因为您期望任何连接到您的目标端口始终来自相同的源端口(这等效于按源端口过滤)。 - Fabio Scaccabarozzi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接