Wireshark TS | 关闭连接和超时重传

前言

来自于技术群讨论的一个问题,原话如下:

「讨论个问题,我们都知道tcp的超时重传机制。在超时重传过程中,如第9次重传间隔要等待52s。问题:在52s间隔还没有到,此时上层应用主动放弃了,这个重传还会出现吗?」

按照自己的理解,我简单写这么一篇文章,讨论下该问题,名字也就随便起了。

问题分析

问题简化成:在数据段超时重传过程中,如果上层应用主动关闭连接,会发生什么现象。

首先是应用主动关闭连接的方式,一种是正常 TCP 四次挥手通过 FIN 终止连接的过程称为 orderly release,另一种是直接主动 Reset 一个 TCP 连接,并且不需要得到确认,这种情况下的连接中断过程称为 abortive release。

orderly release 可以说是一种优雅的四次挥手过程,通过 FIN 和 ACK 包的交换确保双方都完成数据传输并安全关闭连接;而 abortive release 则是通过发送 RST 包来强制立即终止连接的方式,它会丢弃所有未发送的数据并跳过正常的关闭流程,通常用于处理错误情况或需要紧急断开连接的场景。

如果是本端正常 FIN ,连接实际还存在,而直接 RST,连接就应该不存在了,以下就这两种情况分别做下测试。

问题测试

针对 orderly release,可以通过 packetdrill 脚本模拟,如下,在尝试写入 100 字节数据后,并没有模拟收到 ACK 确认,因此会进入超时重传过程,然后间隔 1 秒后主动关闭连接。

plain 复制代码
# cat test.pkt 
0  socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0

+0 < S 0:0(0) win 10000 <mss 1000,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.05 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4

+0.01 write(4,...,100) = 100

+1 close(4) = 0

+0 `sleep 10`
#

执行脚本,同时通过 tcpdump 抓取数据包,现象如下。

可以看到发送端在发出数据段 Seq 1:101 后,由于得不到 ACK 确认,所以在不断进行超时重传,在重传两次后,应用关闭连接,因此正常发出了 FIN 数据包 Seq 101 ,但同样得不到 ACK 确认,之后仍然是第一个数据段在不断进行超时重传,RTO 时间不断翻倍。

plain 复制代码
# packetdrill test.pkt
#


# tcpdump -i any -nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:09:23.954982 tun0  In  IP 192.0.2.1.38747 > 192.168.55.3.8080: Flags [S], seq 0, win 10000, options [mss 1000,nop,nop,sackOK], length 0
22:09:23.955012 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [S.], seq 767095716, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
22:09:24.005104 tun0  In  IP 192.0.2.1.38747 > 192.168.55.3.8080: Flags [.], ack 1, win 10000, length 0
22:09:24.015337 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:09:24.274691 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:09:24.530705 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:09:25.015412 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [F.], seq 101, ack 1, win 64240, length 0
22:09:25.042696 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:09:26.066702 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:09:28.114731 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:09:32.242783 tun0  Out IP 192.168.55.3.8080 > 192.0.2.1.38747: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:09:35.017335 ?     In  IP 192.0.2.1.38747 > 192.168.55.3.8080: Flags [R.], seq 1, ack 1, win 10000, length 0
#

另外上述实验还有一个现象,就是第一个数据段没有得到 ACK 确认从而不断重传的过程中,FIN 并不会进入超时重传,这个和连续发两个数据段,在发生超时重传后,因为拥塞窗口降为了 1 ,所以超时重传第一个数据段的情况一样,毕竟 FIN 实际上也是带有 1 个字节,需要得到 ACK 确认的。

继续修改脚本,在 close() 之后,增加对第一个数据段的 ACK 确认。

plain 复制代码
# cat test.pkt 
0  socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0

+0 < S 0:0(0) win 10000 <mss 1000,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.05 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4

+0.01 write(4,...,100) = 100

+1 close(4) = 0

+0.5 < . 1:1(0) ack 101 win 10000

+0 `sleep 10`
#

执行脚本,同时通过 tcpdump 抓取数据包,现象如下。

相比上面的实验,可以看到在第一个数据段得到 ACK 确认后,紧接着开始了 FIN 数据包的超时重传过程。

plain 复制代码
# packetdrill test.pkt 
#


# tcpdump -i any -nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:36:37.955006 tun0  In  IP 192.0.2.1.55011 > 192.168.50.67.8080: Flags [S], seq 0, win 10000, options [mss 1000,nop,nop,sackOK], length 0
22:36:37.955042 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [S.], seq 124772434, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
22:36:38.005152 tun0  In  IP 192.0.2.1.55011 > 192.168.50.67.8080: Flags [.], ack 1, win 10000, length 0
22:36:38.015262 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:36:38.290753 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:36:38.550713 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:36:39.015312 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [F.], seq 101, ack 1, win 64240, length 0
22:36:39.058694 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [P.], seq 1:101, ack 1, win 64240, length 100: HTTP
22:36:39.515373 tun0  In  IP 192.0.2.1.55011 > 192.168.50.67.8080: Flags [.], ack 101, win 10000, length 0
22:36:39.515402 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [F.], seq 101, ack 1, win 64240, length 0
22:36:40.530729 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [F.], seq 101, ack 1, win 64240, length 0
22:36:42.578719 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [F.], seq 101, ack 1, win 64240, length 0
22:36:46.802696 tun0  Out IP 192.168.50.67.8080 > 192.0.2.1.55011: Flags [F.], seq 101, ack 1, win 64240, length 0
22:36:49.518141 ?     In  IP 192.0.2.1.55011 > 192.168.50.67.8080: Flags [R.], seq 1, ack 101, win 10000, length 0
#

针对 abortive release,也就是主动通过 RST 关闭连接,实际上是可以通过设置 SO_LINGER 选项来达到目的。但研究了下 packetdrill 脚本,尝试了很多次,发现无法模拟出效果 😅 ,所以改用了 python 来实现。

plain 复制代码
客户端
#!/usr/bin/env python3

import socket
import struct
import time

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
client.bind(('', 50000))
client.connect(('localhost', 9999))

client.sendall(b'1234567890')
time.sleep(1)

# 设置 SO_LINGER 选项,关闭时触发 RST
l_onoff = 1    # 启用 SO_LINGER
l_linger = 0   # 设置延迟时间为 0 秒
client.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', l_onoff, l_linger))

# 关闭 socket,会触发 RST 而不是 FIN
client.close()

time.sleep(10)
plain 复制代码
服务器端
#!/usr/bin/env python3

import socket
import time

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('localhost', 9999))
server.listen(5)
print("Server is running...")

while True:
    client, addr = server.accept()

然后在服务器端通过 iptables 过滤了 PSH 和 RST 数据包。

plain 复制代码
iptables -A INPUT -p tcp --dport 9999 --tcp-flags PSH PSH -j DROP
iptables -A INPUT -p tcp --dport 9999 --tcp-flags RST RST -j DROP

通过 tcpdump 抓取数据包,现象如下,应用通过 RST 关闭连接后,第一个数据段的超时重传过程就终止了,同时本地连接也就释放了。

plain 复制代码
# tcpdump -i any -nn port 9999                                                   
tcpdump: data link type LINUX_SLL2                                                               
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode                        
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes           
21:07:59.283332 lo    In  IP 127.0.0.1.50000 > 127.0.0.1.9999: Flags [S], seq 3881859695, win 654
95, options [mss 65495,sackOK,TS val 3838123872 ecr 0,nop,wscale 7], length 0                    
21:07:59.283346 lo    In  IP 127.0.0.1.9999 > 127.0.0.1.50000: Flags [S.], seq 946078152, ack 388
1859696, win 65483, options [mss 65495,sackOK,TS val 3838123872 ecr 3838123872,nop,wscale 7], len
gth 0                                                                                            
21:07:59.283355 lo    In  IP 127.0.0.1.50000 > 127.0.0.1.9999: Flags [.], ack 1, win 512, options
 [nop,nop,TS val 3838123872 ecr 3838123872], length 0                                            
21:07:59.283385 lo    In  IP 127.0.0.1.50000 > 127.0.0.1.9999: Flags [P.], seq 1:11, ack 1, win 5
12, options [nop,nop,TS val 3838123872 ecr 3838123872], length 10                                
21:07:59.490698 lo    In  IP 127.0.0.1.50000 > 127.0.0.1.9999: Flags [P.], seq 1:11, ack 1, win 5
12, options [nop,nop,TS val 3838124080 ecr 3838123872], length 10                                
21:07:59.698728 lo    In  IP 127.0.0.1.50000 > 127.0.0.1.9999: Flags [P.], seq 1:11, ack 1, win 5
12, options [nop,nop,TS val 3838124288 ecr 3838123872], length 10                                
21:08:00.114705 lo    In  IP 127.0.0.1.50000 > 127.0.0.1.9999: Flags [P.], seq 1:11, ack 1, win 5
12, options [nop,nop,TS val 3838124704 ecr 3838123872], length 10                                
21:08:00.284543 lo    In  IP 127.0.0.1.50000 > 127.0.0.1.9999: Flags [R.], seq 11, ack 1, win 512
, options [nop,nop,TS val 3838124873 ecr 3838123872], length 0

问题总结

当然,就个人来说,我到是没有见到实际环境有这样的现象,但原理总是那些,通过实验也验证了问题的答案。

相关推荐
Hannah4 小时前
网络包分析工具wireshark使用教程
wireshark
qq19226388 小时前
双机并联自适应虚拟阻抗下垂控制(droop)MATLAB仿真模型 即原价 下垂控制 电压电流双...
tcp/ip
2739920298 小时前
qt 获取IP地址(公网 本机)
qt·tcp/ip·php
天天扭码9 小时前
京东前端开发实习生 一面
前端·网络协议·面试
了一梨9 小时前
网络编程:TCP Socket
linux·c语言·tcp/ip
FPGA技术实战10 小时前
基于XADC IP核的FPGA芯片温度读取设计
网络协议·tcp/ip·fpga开发
老蒋新思维10 小时前
范式重构:从场景锚点到价值闭环——AI智能体落地知识产业的非技术视角|创客匠人
网络·人工智能·网络协议·tcp/ip·数据挖掘·创始人ip·创客匠人
ICT技术最前线11 小时前
电信宽带怎么申请公网ip?企业组网避坑指南
网络·网络协议·tcp/ip·电信宽带
葱卤山猪13 小时前
【Qt】 TCP套接字通信学习文档
qt·学习·tcp/ip