从三年前第一次接触TCP backlog开始,TCP建连时到底时两个队列还是一个队列就一直作为一个问题存在脑子了,没想到三年过去了才终于搞明白。

上图摘自«TCP/IP详解卷1»关于backlog的描写,我到现在也没有明白这段话想精确表达的含义。

无论如何,希望大家尽量去读英文书,搜英文资料,避免吃屎。

原理

首先看一下经典老图,

第一个问题到底是一个队列还是两个队列

  1. 一个队列: 此场景下要将处于SYNC_RECV和ESTABLISHED的状态全部放入其中

  2. 两个队列: SYNC_RECV一个队列,ESTABLISHED一个队列

准确来说两种实现都是正确的,因为这不属于TCP协议的范畴。

  1. BSD选择了第一种,当队列满时,仅仅是丢弃收到的SYN,让客户端重试。

  2. Linux2.2之后选择了第二种,存在两个队列。因此对于绝大多数程序员来说,记住下图就可以应用100%的场景了。

TCP中的两个队列

  1. When SYN Queue is Full

    1. if net.ipv4.tcp_syncookies=0, 丢弃SYN
    2. if net.ipv4.tcp_syncookies=1,
      1. if ACP Queue is Full and req_young_len > 1,丢弃SYN。req_young_len是指SYN队列中没有被重传的SYN/ACK pakcet的数量
      2. 否则对这个SYNC packet生成syncookies。
  2. When Accept Queue is Full

    1. if tcp_abort_on_overflow=1, 返回 RST packet,并且从SYN Queue中移除。
    2. if tcp_abort_on_overflow=0, 仅仅标记此连接为acked,并不从SYN Queue中移除。一段时间后,服务端会因为[假装]没有收到ACK而重发SYN+ACK packet.

重现

了解上述原理后,我们只需要将SYN Queue和ACK Queue设置的尽量小,就可以重现Connection Timeout,甚至重现Server discard syn的场景,本文会重现Server discard syn,这也是Connection Timeout的一种。

  1. 设置SYN Queue大小为1
  2. 关闭syncookies
  3. 设置系统级backlog最大值

打开/etc/sysctl.conf文件,加入以下内容

net.ipv4.tcp_max_syn_backlog = 1
net.ipv4.tcp_syncookies = 0
net.core.somaxconn = 1

Reload sysctl.conf

sysctl -p
  1. 编写服务端代码
import socket

s = socket.socket()
s.bind(("127.0.0.1", 1234))

s.listen(1)

while True:
	pass	# don't accept

启动代码

python server.py &
  1. 编写客户端代码
import socket

sockets = []
for i in range(0, 5):
	try:
		s = socket.socket()
		s.settimeout(10)
		s.connect(("127.0.0.1", 1234))
		print("The socket %s connect success." % i)
		sockets.append(s)
	except Exception as e:
		print(e)
python client.py

抓包

运行tcpdump抓包

 sudo tcpdump -i lo port 1234 -nn -tt

看下结果 photo_2021-06-19_11-52-41

再看下netstat的状态 photo_2021-06-19_11-52-35

这里有几个问题:

1.为什么Server ESTABLISHED有2个,Client ESTABLISHED有4个。

首先上图中1234是server的port,client port从49996递增。

Server端看49996和49998处于ESTABLISHED的原因是,虽然ACCEPT队列设置为1,但实际大小为len(AcceptQueue)+1。出处 当这两个连接建立成功后,50000和50002尝试建连。

此时Accept Queue已满,但SYN Queue为0,所以Server会回复[SYN, ACK]。但由于AcceptQueue满了,Server不会接收Client的ACK,进而导致了Server重传了[SYN, ACK]

看上图中488253时刻的packet,就可以发现。至于为什么SYN Queue也是2,我猜测和Accpet Queue原理是一样的

2.再看50004尝试发送的Packet

由于此时SYN和ACP Queue均满了,所以Server Discard SYN,客户端会一直重试。