前言
来自于技术群讨论的一个问题,原话如下:
「讨论个问题,我们都知道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
问题总结
当然,就个人来说,我到是没有见到实际环境有这样的现象,但原理总是那些,通过实验也验证了问题的答案。