参考:《UNIX网络编程》、hping3命令使用小林网络51CTO

内核为每个监听套接字维护两个队列:未完成连接队列和已完成连接队列 。前者又称半连接队列、SYN队列,后者又称全连接队列、accept 队列。

半连接队列:服务器收到客户端发来的 SYN 报文后,将该连接存入此队列中。这些连接处于 SYN_RCVD 状态。
全连接队列:服务端收到第三次握手的 ACK 后,该连接被内核从半连接队列转移到全连接队列,此时已经完成三次握手,处于 ESTABLISHED 状态。当调用 accept 函数时,该连接将从全连接队列中移除。


backlog

“backlog的含义从未有过正式的定义”,不同的 UNIX 操作系统对其有不同的实现 。这里只以笔者的环境 Ubuntu 16.04 进行说明。

其他版本的相关说明请参见《UNP》第 3 版 84 页。

先说结论:

在 Linux 内核 2.2 之后,backlog 参数影响全连接队列的长度, tcp_max_syn_backlog 作为系统变量则影响半连接队列的长度

为什么说“影响”而不是“决定”?因为两个队列长度的具体定义是采用的如下方式:

  • 全连接队列长度 = min(somaxconn, backlog)+1
  • 半连接队列的长度计算较为复杂,且不同版本的操作系统实现不一样,故没有必要掌握其计算方法。只需要知道,半连接队列长度可能同时取决于 backlog 、somaxconntcp_max_syn_backlog ,这三者越大,则半连接队列容量越大

somaxconn 是 Linux 内核的参数,默认值是 128,可以通过 /proc/sys/net/core/somaxconn 来设置其值。

下面就全连接队列进行 backlog 的实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
////======!!!代码中的Bind、Listen等函数是博主自己包装的,读者可以直接改成小写的形式!!!=======
//server
int main()
{
int sock_lsn = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in addr_lsn;
memset(&addr_lsn, 0, sizeof(addr_lsn));
addr_lsn.sin_addr.s_addr = htonl(INADDR_ANY);
addr_lsn.sin_port = htons(12345);
addr_lsn.sin_family = AF_INET;
Bind(sock_lsn, (struct sockaddr*)&addr_lsn, sizeof(addr_lsn));
Listen(sock_lsn, 5);
//注意,没有accept和close
while(1);
}
//=====================
//client
int main()
{
struct sockaddr_in addr_clnt;
memset(&addr_clnt, 0 , sizeof(addr_clnt));
addr_clnt.sin_port = htons(12345);
addr_clnt.sin_addr.s_addr = inet_addr("127.0.0.1");
addr_clnt.sin_family = AF_INET;
for(int i = 0; i < 10; i++) //创建10个客户进程并发送连接请求
{
if(0 == fork())
{
int sock_clnt = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
Connect(sock_clnt, (struct sockaddr*)&addr_clnt, sizeof(addr_clnt));
sleep(10);//暂停10s,以便我们观察连接状态
close(sock_clnt);
exit(0);
}
}
}

编译并运行以上代码,结果如下:

netstat 分别以客户端和用户端为角度输出了结果,所以有 20 个条目,实际上是 10 条连接,我们只需要看一个纵列的红色条目即可 。可以看到,10 条连接中,只有 6 条是 ESTABLISHED 状态,剩下 4 条是 SYN_SENT 。很奇怪,我们指定的 backlog 是 5,所以不应该只有 5 条是 ESTABLISHED 状态吗?嗯,这里挺坑的,这是因为大多数操作系统的实现都为 backlog 引入了 模糊因子 ,我们这里的模糊因子就是 +1,也就是说,实际全连接队列容量 = backlog + 1 。其他操作系统的模糊因子参考:

关于更多模糊因子的描述,参见《UNP》85~87 页。

接着我们查看 somaxconn 的值:

1
2
$ cat /prog/sys/net/core/somaxconn
128

将其修改为 3:

1
2
//必须在管理员权限下才能修改
$ echo 3 > /prog/sys/net/core/somaxconn

然后再运行服务器和客户端,得到以下结果:

可见,有 4 条连接处于已建立状态,比我们指定的 somaxconn 还要多 1 。

综上,我们得出结论:全连接队列长度 = min(somaxconn, backlog) + 1

当全连接队列满了怎么办
默认行为是直接丢弃,我们也可通过指定 tcp_abort_on_overflow 参数来调整其行为:

  • 0 :直接忽略客户端发过来的 ACK ;
  • 1 :回复 RST 给客户端,表示终止本次连接;
1
$ echo 1 > /prog/sys/net/ipv4/tcp_abort_on_overflow

一般情况下默认为 0 即可,这是更合理的方式,因为全连接队列已满的状态只是暂时的,客户端会因迟迟未收到 ACK 重发报文,期待不久就能等到全连接队列腾出可用空间。


半连接队列的长度受多方面影响,且不同操作系统版本的实现方式各不相同 ,所以无需掌握具体计算方式。这里笔者简单演示如何间接查看半连接队列的长度。

半连接队列中的状态为 SYN_RCV ,所以我们可以发起 SYN 泛洪,向端口发送大量虚假的 SYN 报文,然后查看该端口上最多时有多少个连接处于 SYN_RCV 状态,此时就是半连接队列的最大长度。

  1. 下载 hping3 ,用来发起 SYN-flood 攻击

    1
    sudo apt-get install hping3
  2. 修改系统配置:

    1
    2
    tcp_max_syn_backlog = 128
    somaxconn = 150

    笔者将 backlog 设置为 9(那么全连接队列的容量就为 10),读者随意。

  3. 运行服务器端

  4. 发起攻击:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    hping3 -c 1000 -d 120 -S -w 64 -p 12345 --flood --rand-source 127.0.0.1
    //-c 1000 = 发送的数据包的数量
    //-d 120 = 发送到目标机器的每个数据包的大小,单位是字节
    //-S = 只发送 SYN 数据包
    //-w 64 = TCP 窗口大小
    //-p 12345 = 目的地端口为12345
    //–flood = flood攻击模式
    //--rand-source 源IP随机
    //目标IP为主机127.0.0.1
  5. 查看 SYN_RECV 的个数:

    1
    2
    netstat -nat | grep :12345 | grep SYN_RECV  | wc -l
    9

    可见,半连接队列长度等于 backlog

  6. 再将 backlog 设置为 200,重复以上操作:

    1
    2
    netstat -nat | grep :12345 | grep SYN_RECV  | wc -l 
    150

    可见,在笔者环境下(Ubuntu 16.04) ,如果 backlog 小于 somaxconn ,则半连接队列容量为 backlog,反之则为 somaxconn ,与 tcp_max_syn_backlog 无关。

综上, 半连接队列容量可能同时受 tcp_max_syn_backlogsomaxconnbacklog 的影响,想要增大半连接队列的长度,就需要同时增大这三个参数,仅增大 tcp_max_syn_backlog 是无效的

对于半连接队列,更多的是需要掌握它与 SYN 泛洪的关系。关于 SYN 泛洪,详细可参见博主另一篇文章-SYN泛洪攻击


另外,还可以从 ss 命令直接观察全连接队列的大小:

1
2
3
$ ss -nlt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 11 10 *:12345 *:*
1
2
3
4
5
$ ss -nt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 100 0 127.0.0.1:12345 127.0.0.1:59492
ESTAB 100 0 127.0.0.1:12345 127.0.0.1:59496
.......

-n 不解析服务名称
-t 只显示 tcp sockets
-l 显示正在监听(LISTEN)的 sockets

对于 LISTEN 状态的 socket

  • Recv-Q:当前全连接队列的中的连接个数,即已完成三次握手等待应用程序 accept 的 TCP 连接
  • Send-Q:全连接队列的容量

对于非 LISTEN 状态的 socket

  • Recv-Q:已收到 但未被应用程序读取 的字节数
  • Send-Q:已发送 但未收到确认 的字节数

注意上面的输出,LISTEN 状态下的 Recv-Q ,即全连接队列中的连接个数为 11;但 Send-Q,即全连接的最大长度才为 10,这是怎么回事?这可能是因为 Send-Q 没有包含模糊因子,直接等同于 backlog 。

另外注意,ss 命令和 netstat 命令对 LISTEN 状态输出的 Recv-Q 和 Send-Q 的含义不相同

1
2
3
4
5
6
$ netstat -at
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 *:12345 *:* LISTEN
$ ss -nlt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 11 10 *:12345 *:*

这两条命令是在同一次网络请求下进行的,但两者的 Recv-Q 和 Send-Q 却不相同。个人猜测 netstat 下的 Recv-Q 和 Send-Q 就是单纯的收发字节数,不再表示连接个数或队列长度。

本文结束。