概述

 

  如上图所示, 在TCP三次握手中,服务器维护一个半连接队列(sync queue) 和一个全连接队列(accept queue)。

当服务端接收到客户端第一次SYN握手请求时,将创建的request_sock结构,存储在半连接队列中(向客户端发送SYN+ACK,并期待客户端响应ACK),此时的连接在服务器端出于SYN_RECV状态。当服务端收到客户端最后的ACK确认时,将半连接中的相应条目删除,然后将相应的连接放入 全连接队列中, 此时服务端连接状态为ESTABLISHED。 进入全连接队列中的连接等待accept()调用取用。 

  既然是队列,肯定就有大小,那么当这两个队列满了没有空间了怎么办呢? 例如如果我们listen()后不去accept() ,那么全连接队列肯定会满的。 我们下面分别对于这两个队列结合试验进行描述。

试验环境:

CentOS Linux release 7.5.1804 (Core)

Linux version 3.10.0-229.4.2.el7.x86_64

syns queue 半连接队列

  首先说一下 SYN flooding攻击,为了应对SYN flooding(即客户端只发送SYN包发起握手而不回应ACK完成连接建立,快速填满server端的半连接队列,让它无法处理正常的握手请求),Linux实现了一种称为SYNcookie的机制,通过net.ipv4.tcp_syncookies控制,设置为1表示开启。简单说SYNcookie就是将连接信息编码在ISN(initialsequencenumber)中返回给客户端,这时server不需要将半连接保存在队列中,而是利用客户端随后发来的ACK带回的ISN还原连接信息,以完成连接的建立,避免了半连接队列被攻击SYN包填满。 也就是说,如果开启了syncookies的话(通过 TCP参数 net.ipv4.tcp_syncookies配置 ),半连接队列就相当于是无限大的了。在我的环境中就是默认开启的。

  如果我们将syncookies关闭的话,半连接队列的长度将为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog) ,,此时对半连接填满时的处理策略是 server将 丢弃请求连接的SYN,不回复SYN+ACK,这样就会造成client收不到握手响应,始终处在SYN_SENT状态,经过几次重传后,客户端 connect() 调用失败。

accept queue 全连接队列

  全连接队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128(/proc/sys/net/core/somaxconn),表示最多有 129 的 ESTAB 的连接等待 accept(),而 backlog 的值则是由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 可以有我们的应用程序去定义。 当全连接队列满了后的处理策略基于TCP参数net.ipv4.tcp_abort_on_overflow,在我的机器上默认为0。

  1.tcp_abort_on_overflow 关闭时

  当server收到最后一次ACK时,希望将连接从半连接队列中取出放入全连接队列,但是此时全连接队列已满,此时的策略是 将最后接收到的ACK丢弃,并且根据net.ipv4.tcp_synack_retries定义的次数重新向client发送SYN+ACK, client在接收到重传的SYN+ACK后会认为之前的ACK丢失了进而重传ACK,这样在下次重新接收到ACK后,如果全连接队列有空间了,连接就可以正确完成建立。 如果重传了规定次数后全连接队列中依旧没有空间,那么server会简单终止这次连接。这里简单终止的意思是server并没有像client发送RST表明连接无法建立,而是直接丢弃了,这样就会导致在client中的连接处在ESTABLISHED状态,并一直如此。如果client端在此之后发送数据到server端,才会引起server响应RST。

  2.tcp_abort_on_overflow 开启时

  在收到握手的最后一次ACK后,在全连接中如果没有空间,直接向client回复RST,表示连接无法建立。

实验

  首先我们看看我实验环境的默认配置:

  这些配置都是linux的默认配置,开启syncookies,当全连接队列满了后不会abort,而是采用重传syn + ack, 重传次数为5。下面我们结合代码进行实验。

  1.server端 我们故意将backlog的值设置为1,那么全连接的长度就为2。我们在server代码中永远不调用accept,也就是全连接中的连接永远不会被取走,因此真正会建立的连接(server端显示ESTABLISHED的连接)最多就2个。

 1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<errno.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<netinet/in.h>
7 #include<sys/socket.h>
8 #include<sys/wait.h>
9
10 #define PORT 8888
11 //故意将backlog设置为1,全连接的长度为2
12 #define BACKLOG 1
13
14 int main(){
15 int sockfd,new_fd;
16 struct sockaddr_in my_addr;
17 struct sockaddr_in their_addr;
18 int sin_size;
19
20 sockfd=socket(AF_INET,SOCK_STREAM,0);
21
22 if(sockfd==-1){
23 printf("socket failed:%d\n",errno);
24 return -1;
25 }
26 my_addr.sin_family=AF_INET;
27 my_addr.sin_port=htons(PORT);
28 my_addr.sin_addr.s_addr=htonl(INADDR_ANY);
29 bzero(&(my_addr.sin_zero),8);
30 if(bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr))<0){
31 printf("bind error\n");
32 printf("%s\n", strerror(errno));
33 return -1;
34 }
35
36 listen(sockfd,BACKLOG);
37 //简单的休眠,永远不去调用accept从全连接中取走连接
38 sleep(10000000);
39 }

  2.客户端代码中我们在连接建立完成后,首先执行一个写操作,然后执行一个读操作,因为server端始终没有accept,自然客户端的读操作将阻塞。

 1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <errno.h>
5 #include <string.h>
6 #include <sys/types.h>
7 #include <netinet/in.h>
8 #include <sys/socket.h>
9 #include <sys/wait.h>
10
11
12 #define DEST_PORT 8888
13 #define DEST_IP "192.168.4.14"
14 #define MAX_DATA 100
15
16 int main(){
17 int sockfd;
18 struct sockaddr_in dest_addr;
19 char buf[MAX_DATA];
20
21 sockfd=socket(AF_INET,SOCK_STREAM,0);
22 if(sockfd==-1){
23 printf("socket failed:%d\n",errno);
24 }
25
26
27 dest_addr.sin_family=AF_INET;
28 dest_addr.sin_port=htons(DEST_PORT);
29 dest_addr.sin_addr.s_addr=inet_addr(DEST_IP);
30 bzero(&(dest_addr.sin_zero),8);
31
32 if(connect(sockfd,(struct sockaddr*)&dest_addr,sizeof(struct sockaddr))==-1){
33 printf("connect failed:%d\n",errno);
34 } else{
35 printf("connect success\n");
36 //客户端连接建立后,首先进行一次写操作,对于没有真正建立起的连接,这个写的数据包将会被server端直接忽略
37 size_t t = send(sockfd,"Hello World!",12, MSG_DONTWAIT);
38 if (t == -1) {
39 printf("send failed: %s\n", strerror(errno));
40 exit(-1);
41 } else {
42 printf("send successfully !");
43 }
44
45 printf("the client will try to read from server and then block ! \n");
46 //写操作后进行一次阻塞的读操作
47 size_t rt = recv(sockfd,buf,MAX_DATA,0);
48 if(rt == -1) {
49 printf("recv return error: %s\n", strerror(errno));
50 exit(-1);
51 }
52 }
53 }

  我们会分别启动三个客户端来建立连接,根据上面所讲,这三个连接中将有两个真正的和server端完全建立连接,在双方我们都将看到处于ESTABLISHED状态的连接。但是对于剩下的那一个,在server端由于永远都无法加入的全连接中来,所以将处于SYN_RECV的状态,而在客户端,则将处于ESTABLISHED状态,然后server端就会重传5次syn + ack,最后直接结束这次连接。在我们的代码中,客户端建立连接后会有一个写操作,对于这个没有真正建立的连接,写操作所发送的数据包到达服务端后同样会被直接忽略掉,那么客户端也就收不到ack确认,然后会开启数据包的重传。 当服务端彻底关闭了之前那个SYN_RECV状态的连接后,等到再收到重传的数据包后,就会发送RST,客户端收到这个RST后,关闭连接,然后阻塞的读操作就会出错返回。

  在分别运行server端和三个client端后双方的连接情况如下:

  server端:

  server端最开始有两个ESTABLISHED的连接,并且在 接收队列(Recv-Q)中都有12个字节的数据,这说明它们和客户端已经正常连接了。剩下的那个处于SYN_RECV状态的连接接收队列为0,证明了上面说的 此时客户端发来的数据包将被丢弃,也不会发送任何ack,从而客户端会重传这12个字节的数据。过一段时间后,这个处于SYN_RECV的连接将消失。

  client端:

  client端的三个连接全部处于ESTABLISHED状态,通过对比端口号,我们知道37784那个连接此时在服务端处于SYN_RECV状态,而且与服务端对应的,此时这个连接的发送队列(Send-Q)中有12个字节的数据,说明此时发送并没有成功,从而会开启数据包的重传。

  在server端的那个处于SYN_RECV状态的连接消失很短一段时间后,上面37784那个连接对应的客户端会读操作出错返回,如下图:

  从出错信息中,我们很容易的看出来,是由于客户端收到了服务端发来的RST导致的。而这个RST之所以会从服务端发出,自然就是由于那12个字节的数据包在服务端彻底关闭了对应的连接后再次重传导致的。

  下面我们看看这整个过程中,在server端抓取的数据包。

  下面是端口号为37784的那个没有真正建立起来的连接的数据包

  首先的11-13代表着最初建立连接的三次握手,然后14代表着客户端向服务端发送了一个数据包。在此之后,我们看到从server(4.14)发向client(108.180)的除了最后面那个RST外,一共有5次SYN+ACK的重传,在每次收到server发来的SYN+ACK重传后,客户端都会响应一次ACK,除此之外

client(108.180)向server(4.14)发送的所有数据包都是最开始那次PSH的重传,在server彻底关闭了连接后,最后的那个重传导致server响应了RST。

  总结:我们考虑在一个高负载的服务器上,当server端没有能力立即accept到来的tcp请求时,就会将这些请求缓存到 连接队列中来,当连接队列也满了的时候,不会立即RST,而是采用重传的方式。实际上就是如何相对平缓的应对此时高负载的请求,而不是硬性的拒绝。

在《TCP/IP详解 卷一》  13.7.4 节讲到连接队列时,有类似的一段话:

后记

在前面的例子中,除了能够进入全连接的客户端请求外只有一个请求无法进入全连接,当我把并发请求的数量调整到100个时,发生的情况却很让我意外,我们先看看客户端的并发请求的代码:

 1 #include <unistd.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <stdlib.h>
5 #include <errno.h>
6 #include <string.h>
7 #include <sys/types.h>
8 #include <netinet/in.h>
9 #include <sys/socket.h>
10 #include <sys/wait.h>
11 #include <pthread.h>
12
13
14 #define DEST_PORT 8888
15 #define DEST_IP "192.168.104.60"
16 #define MAX_DATA 100
17
18
19 void *threadFunc(void *arg)
20 {
21 int sockfd;
22 struct sockaddr_in dest_addr;
23 char buf[MAX_DATA];
24
25 sockfd=socket(AF_INET,SOCK_STREAM,0);
26 if(sockfd==-1){
27 printf("socket failed:%d\n",errno);
28 }
29
30
31 dest_addr.sin_family=AF_INET;
32 dest_addr.sin_port=htons(DEST_PORT);
33 dest_addr.sin_addr.s_addr=inet_addr(DEST_IP);
34 bzero(&(dest_addr.sin_zero),8);
35
36 if(connect(sockfd,(struct sockaddr*)&dest_addr,sizeof(struct sockaddr))==-1){
37 printf("connect failed:%d\n",errno);
38 return NULL;
39 } else{
40 printf("connect success\n");
41 //客户端连接建立后,首先进行一次写操作,对于没有真正建立起的连接,这个写的数据包将会被server端直接忽略
42 size_t t = send(sockfd,"Hello World!",12, 0);
43 if (t == -1) {
44 printf("send failed: %s\n", strerror(errno));
45 return NULL;
46 } else {
47 printf("send successfully !");
48 }
49
50 printf("the client will try to read from server and then block ! \n");
51 //写操作后进行一次阻塞的读操作
52 size_t rt = recv(sockfd,buf,MAX_DATA,0);
53 if(rt == -1) {
54 printf("recv return error: %s\n", strerror(errno));
55 return NULL;
56 }
57 }
58
59 return NULL;
60 }
61
62 int main(void)
63 {
64 pthread_t pth[100];
65 int i = 0;
66
67 for(i = 0; i < 100; i++) {
68 pthread_create(&pth[i],NULL,threadFunc,"foo");
69 }
70
71 printf("main waiting for thread to terminate...\n");
72 sleep(10000000);
73
74 return 0;
75 }

我们通过多线程从客户端发起了100个请求,除了2个进入全连接的请求正常建立起来以外,其他的请求并不是所有都同上面所讲的那个无法进入全连接的请求一样,主要要以下几种情况:

1. 始终无法建立连接,客户端始终处于SYN_SENT状态,最终syn重传超时,客户端显示 connect failed。 从服务端抓取的包来看就是收到了多个syn,却始终没有响应syn + ack。

2. 客户端经过几次重传syn后,服务端响应了syn+ack,但是并没有重传五次 syn+ack,有的重传了少于5次有的干脆一次都没有重传。

一次都没有重传:

重传了2次:

至于为什么当并发请求数量较多时会出现上面的情况,暂时还搞不清楚,如果你知道,也希望你能告诉我^_^。我从直观感受上猜测的原因可能是,当请求压力骤然增大时,对于这些并发的请求中的某些请求减少 重传syn+ack的次数或者干脆忽略syn,能相对的减小服务端的压力。

但是能确定的一点是通过合理的设置backlog的大小,我们可以缓存的established的连接数量是确定的。当服务端程序由于种种原因无法及时执行accept处理新请求时,更多的新请求将首先在全连接中缓存,如果全连接也用光了,那么新请求最有可能的是通过重传syn+ack延迟进入全连接的时机,再就是如果压力特别大的时候,也可能像我们最后分析的那样,直接忽略syn,或者缩短重传的次数。

TCP半连接队列和全连接的更多相关文章

  1. 【转】关于TCP 半连接队列和全连接队列

    摘要: # 关于TCP 半连接队列和全连接队列 > 最近碰到一个client端连接异常问题,然后定位分析并查阅各种资料文章,对TCP连接队列有个深入的理解 > > 查资料过程中发现没 ...

  2. 关于TCP 半连接队列和全连接队列

    关于TCP 半连接队列和全连接队列 http://jm.taobao.org/2017/05/25/525-1/ 发表于 2017-05-25   |   作者   蛰剑     |   分类于 网络 ...

  3. TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

    前言 网上许多博客针对增大 TCP 半连接队列和全连接队列的方式如下: 增大 TCP 半连接队列的方式是增大 /proc/sys/net/ipv4/tcp_max_syn_backlog: 增大 TC ...

  4. TCP实战二(半连接队列、全连接队列)

    TCP实验一我们利用了tcpdump以及Wireshark对TCP三次握手.四次挥手.流量控制做了深入的分析,今天就让我们一同深入理解TCP三次握手中两个重要的结构:半连接队列.全连接队列. 参考文献 ...

  5. 三次握手 四次握手 原因分析 TCP 半连接队列 全连接队列

    小结 1. 三次握手的原因:保证双方收和发消息功能正常: [生活模型] "请问能听见吗""我能听见你的声音,你能听见我的声音吗" [原理]A先对B:你在么?我在 ...

  6. 五分钟带你读懂 TCP全连接队列(图文并茂)

    爱生活,爱编码,微信搜一搜[架构技术专栏]关注这个喜欢分享的地方. 本文 架构技术专栏 已收录,有各种视频.资料以及技术文章. 一.问题 今天有个小伙伴跑过来告诉我有个奇怪的问题需要协助下,问题确实也 ...

  7. 性能分析之TCP全连接队列占满问题分析及优化过程(转载)

    前言 在对一个挡板系统进行测试时,遇到一个由于TCP全连接队列被占满而影响系统性能的问题,这里记录下如何进行分析及解决的. 理解下TCP建立连接过程与队列 从图中明显可以看出建立 TCP 连接的时候, ...

  8. TCP全连接队列和半连接队列已满之后的连接建立过程抓包分析[转]

    最近项目需要做单机100万长连接与高并发的服务器,我们开发完服务器以后,通过自己搭的高速压测框架压测服务端的时候,发生了奇怪的现象,就是服务端莫名其妙的少接收了连接,造成了数据包的丢失,通过网上查资料 ...

  9. 【转载】socket 的 connect、listen、accept 和全连接队列、半连接队列的原理

    转自:http://blog.csdn.net/tennysonsky/article/details/45621341 写在前面: 1. accept 只是从全连接队列拿出一个已经建立好的socke ...

随机推荐

  1. Semaphore信号量深度解析

    1. 使用指南 package com.multthread; import java.util.concurrent.ExecutorService; import java.util.concur ...

  2. SpringBoot+Vue 前后端合并部署

    前后端分离开发项目 前端vue项目 服务端springboot项目 如何将vue的静态资源整合到springboot项目里,通过启动jar包的方式部署服务. 前端项目执行npm run build 命 ...

  3. 使用Arduino点亮ESP-01S,ESP8266-01S上的板载LED

    因为在开发ESP-01s远程控制中觉得接线麻烦,又因为ESP-01s板子上带有LED灯,那就先点亮板载LED,  如图所示: 打开Arduino 把代码copy进去,再编译烧录,就可以看见LED灯每隔 ...

  4. C# 9 新特性——init only setter

    C# 9 新特性--init only setter Intro C# 9 中新支持了 init 关键字,这是一个特殊的 setter,用来指定只能在对象初始化的时候进行赋值,另外支持构造器简化的写法 ...

  5. 跟我一起学python(1):占位符

    模板 格式化字符串时,Python使用一个字符串作为模板.模板中有格式符,这些格式符为真实值预留位置,并说明真实数值应该呈现的格式.Python用一个tuple将多个值传递给模板,每个值对应一个格式符 ...

  6. orcl数据库自定义函数--金额小写转大写

    很多时候在打印票据的时候需要用到大写,ireport无法转换,只能先在查询语句里面进行转换,首先定义好函数,之后再调用函数 CREATE OR REPLACE Function MoneyToChin ...

  7. c通过ctfshow学习php反序列化

    web254 web255 web256 web257 web258 web259 web260 web262 web263 web264 web265 web266 web254 error_rep ...

  8. kafka如何保证消息得顺序性

    1. 问题 比如说我们建了一个 topic,有三个 partition.生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到 ...

  9. 在mapper.xml映射文件中添加中文注释报错

    问题描述: 在写mapper.xml文件时,想给操作数据库语句添加一些中文注释,添加后运行报如下错误: 思考 可能是写了中文注释,编译器在解析xml文件时,未能成功转码,从而导致乱码.但是文件开头也采 ...

  10. Linux下的screen和作业任务管理

    一.screen 首先介绍下screen,screen是Linux下的一个任务容器,开启了之后就可以让任务在后台执行而不会被网络中断或者是终端退出而影响到. 在Linux中有一些耗时比较久的操作(例如 ...