listen函数backlog参数的一点探讨

2023-10-11 23:30

本文主要是介绍listen函数backlog参数的一点探讨,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言:

        今年上半年的时候, 因为自己工作的失误, 导致程序的TCP三次握手非常缓慢, 存在大量syn_recv状态连接. 查了很多资料(尤其是listen的相关资料)都无法完美解决问题, 虽然调大了backlog参数, 但是连接数达到一定值(backlog)后, 三次握手同样非常缓慢. 后面才发现是自己代码的原因, 具体来讲, 就是我那部分代码阻塞了进程, 相当于sleep了下(虽然不是真的sleep). 后面我把这部分代码给删除了, 程序恢复了正常.

        最近一段时间, 又想到了这个问题, 所以就想写个测试程序来模拟当时的场景, 顺便对当时搜的资料做个总结. 

         本文基于centos7环境, linux3.10内核. 好吧, 那就开始吧.

开始:

        这里有几个名词/参数需要了解下, 本文会遇到.

        全连接队列: 完成三次握手处于ESTABLISHED状态.

        未全连接队列: 此处称之为半连接队列. 服务端收到了SYN后半连接队列+1然后再回复SYN+ACK. 回复SYN + ACK后此时处于SYN_RECV状态.

        变量backlog: 函数listen的第二个参数.

        变量somaxconn: /proc/sys/net/core/somaxconn.

        变量tcp_max_syn_backlog: /proc/sys/net/ipv4/tcp_max_syn_backlog.

        变量syncookies: /proc/sys/net/ipv4/tcp_syncookies.

        变量tcp_synack_retries: net.ipv4.tcp_synack_retries, 含义是未激活的TCP连接发送SYN/ACK段的重试次数. 可以写在/etc/sysctl.conf内, 也可以通过代码设置.

        可以通过man 2 listen命令可以查看如下解释:

         大概意思是: backlog指定的是等待被accept的全连接socket队列长度, 代替了未全连接请求的数量. 未全连接socket队列的最大长度可以通过tcp_max_syn_backlog设置, 当设置了syncookies, 则不是逻辑上的最大长度, 这个设置可以忽略.

        还有一个系统默认参数somaxconn, 两者取最小值为全连接队列长度.

        读完这个解释后, 心里冒出几个问题:

        1, 半连接队列长度是多大.

        2, 全连接队列长度是多大.

        3, 半连接队列长度和全连接队列长度何时加减.

        4, 如何测试.

        关于第1, 2个问题, 详细解答可以查看下面的链接, 这里只做简答答疑.

TCP 的backlog详解及半连接队列和全连接队列_Blue summer的博客-CSDN博客_tcp 半连接

第1部分, 半连接队列长度:        

         这个函数的功能是判断半连接队列是否满了. 第一个变量为当前半连接队列的实际大小, 第二个变量为2^n(实际分析下来, 2^n就是半连接队的设置大小)中的指数n. 现在只需要知道max_qlen_log这个数值如何得来的就可以了. 看下图.

        传入的参数nr_table_entries的实际值为min(backlog, somaxconn), 那么:

        第46行: x = min(backlog, somaxconn, tcp_max_syn_backlog)

        第47行: y = max(x, 8)

        第48行: nr_table_entries的最终值为 (y + 1)最接近的2^n, 此为半连接队列大小, max_qlen_log的值为n.

第2部分, 全连接队列长度:

        这个函数的功能是判断全连接队列是否满了. 第一个变量指的是当前全连接队列的实际大小, 第二个变量是全连接队列的设置大小, 如此我们只需知道第二个变量是如何得来的就可以了.

        在这里我们需要查看两个函数, 第一个函数为listen的系统调用SYSCALL_DEFINE2(listen, int, fd, int, backlog). 第二个函数为其调用的具体函数int inet_listen(struct socket *sock, int backlog). 两个函数里分别有如下实现:

         可以看到, 函数1的实现就是取两者最小值, 函数2的实现就跟上述判断全连接队列是否满了函数的第二个变量匹配起来, 所以全连接队列的最终大小是: min(backlog, somaxconn)

第3部分, 队列加减操作: 

3.1 半连接队列+1:

        在服务端收到第一次握手的SYN时半连接队列+1, 具体调用如下.

 3.2 半连接队列-1同时全连接队列+1:

        在服务端收到第三次握手客户端发送的ACK时半连接队列会-1, 同时全连接队列会+1. 具体调用如下.

 3.3 全连接队列-1:

        在服务端调用accept时全连接队列-1. 具体调用如下.

第4部分, 如何测试: 

        主要测试的内容是, 全连接队列已满时的各种信息. 所以, 测试代码实现的是不完整的非常规socket: 服务端只listen而没有accept调用, 客户端只connect而不读写. 测试代码见末尾.

4.1 条件1:

backlogsomaxconntcp_max_syn_backlogsyncookiestcp_synack_retries
1616161(开启)127

        根据第1部分和第2部分, 我们可以知道半连接队列设置大小为32, 全连接队列设置大小为16. 下面通过测试也简单验证下.

        4.1.1 测试1:

        客户端启动10个tcp连接. 通过ss -ln命令分析, 结果如下图:

        可以看到, 全连接队列设置大小为16, 结果跟预测匹配; 全连接队列当前大小为10, 跟10个tcp连接匹配. 

        4.1.2 测试2:

        客户端启动25个tcp连接. 通过ss和netstat命令分析, 结果如下图:

        可以看到, 全连接队列当前大小为17, ESTABLISHED状态的个数也为17,结果互相匹配; 半连接状态的个数为8, 总共25个tcp连接, 结果匹配. 

        4.1.3 测试3:

        客户端启动49个tcp连接. 通过netstat命令分析, 结果如下图:

        可以看到, 半连接状态的最大值为32, 那么半连接队列当前大小的最大值为32. 

        4.1.4 测试4:

        客户端启动50个tcp连接. 通过netstat命令分析, 并用demsg查看内核日志, 结果如下图:

        可以发现, 半连接状态的最大值为32, 那么半连接队列当前大小最大值为32, 再通过和测试3比对, 可以知道半连接队列设置大小为32, 跟预测结果一致. 如果半连接队列已满时还有客户端的SYN请求, 那么内核将会出现syn flooding日志. 内核实现如下:

4.2 条件2:

backlogsomaxconntcp_max_syn_backlogsyncookiestcp_synack_retries
1616160(关闭)127

       4.2.1 测试1:

        客户端启动30个tcp连接, 用demsg命令查看内核日志, 并无明显日志.

       4.2.2 测试2:

        客户端启动31个tcp连接, 用dmesg命令查看内核日志:

        说明, 当关闭syncookies时, 半连接队列未满就已经开始丢弃SYN请求了. 内核实现如下:

结尾:

        其实我还是有些疑问的.

        1, 为什么全连接队列设置大小为16, 而最终ESTABLISHED的个数为17, 多个数值尝试都为+1的关系.

        这个疑问通过跟别人交流, 已经有了答案. 如下:

        切换到全连接队列的处理逻辑是这样子的: 先判断全连接队列是否已满, 再执行++操作; 如果backlog为16, 第16个ACK(第三次握手)到来时, 全连接队列实际大小为15, 未满, 则++, 此时才为16; 第17个ACK到来时, 实际大小为16, 而判断逻辑是>, 未满, 又++, 此时为17, 后续的就满了, 符合+1的逻辑关系.

        2, 4.1.3 测试3, 49个tcp连接, 此时并没有引起syn flooding, 为何半连接队列当前大小不能维持在32个, 因为我设置的重试次数为127次, 并没有达到重试的次数就开始降低了.

        3, 4.2.2 测试2, 根据代码, 半连接数达到13个可以使第二个条件成立(16 – 13 < 4), 那么理论上总连接数30(13 + 17全连接)个就能触发drop条件, 而最终的结果是启动31个连接数才能触发.

        本人能力有限, 文中或许有些错误, 如果有同学发现, 敬请指教. 或者有同学知道我的疑问, 也希望不吝赐教. 但是通过这次测试, 我对backlog参数有一个比较深的理解. 也期望自己能在余下的时间完善自己, 找到这些问题的答案.

代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>typedef struct config
{int mode;char *addr;unsigned short int port;int backlog;int conn_num;struct sockaddr_in addr_in;socklen_t  addr_in_len;
}config_t;static int tcp_socket(const config_t *conf)
{int sfd = socket(AF_INET, SOCK_STREAM, 0);if (sfd == -1){printf("socket failed, err msg: %s\n", strerror(errno));return -1;}int val1 = 1;if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR | (conf->mode == 0 ? SO_REUSEPORT : 0), (void *)&val1, sizeof(val1)) == -1){printf("setsockopt failed, err msg: %s\n", strerror(errno));goto FAILED;               }if (conf->mode == 0){if (bind(sfd, (struct sockaddr*)&conf->addr_in, conf->addr_in_len) == -1){printf("bind failed, err msg: %s\n", strerror(errno));goto FAILED;               }if (listen(sfd, conf->backlog)){printf("bind failed, err msg: %s\n", strerror(errno));goto FAILED;                              }                       }else{if (connect(sfd, (struct sockaddr*)&conf->addr_in, conf->addr_in_len)){printf("connect failed, err msg: %s\n", strerror(errno));goto FAILED;                        }                }return sfd;FAILED:close(sfd);return -1;
}static void sig_call(int sig)
{printf("capture sig: %d\n", sig);
}static void server(config_t *conf)
{int sock_fd = tcp_socket(conf);if (sock_fd == -1){return;}// 不acceptsleep(3600);
}static void client(config_t *conf)
{const int SIZE = conf->conn_num;int fds[SIZE], i = 0;for(; i < SIZE; ++i){if ((fds[i] = tcp_socket(conf)) == -1){return;}if (i > conf->backlog){sleep(1);}}sleep(3600);
}int main(int argc, char *argv[])
{signal(SIGPIPE, sig_call);config_t conf = {.mode = atoi(argv[1]),          // 0-服务端, 非0-客户端.addr = argv[2],                // 地址.port = atoi(argv[3]),          // 端口.backlog = atoi(argv[4]),       // 服务端listen的第二个参数.conn_num = atoi(argv[5]),      // 客户端启动TCP连接数.addr_in.sin_family = AF_INET,.addr_in.sin_addr.s_addr = inet_addr(argv[2]),.addr_in.sin_port = htons(atoi(argv[3])),.addr_in_len = sizeof(struct sockaddr_in),};if (conf.mode == 0){server(&conf);}else{client(&conf);}return 0;
}

这篇关于listen函数backlog参数的一点探讨的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/191608

相关文章

Django中的函数视图和类视图以及路由的定义方式

《Django中的函数视图和类视图以及路由的定义方式》Django视图分函数视图和类视图,前者用函数处理请求,后者继承View类定义方法,路由使用path()、re_path()或url(),通过in... 目录函数视图类视图路由总路由函数视图的路由类视图定义路由总结Django允许接收的请求方法http

MySQL常用字符串函数示例和场景介绍

《MySQL常用字符串函数示例和场景介绍》MySQL提供了丰富的字符串函数帮助我们高效地对字符串进行处理、转换和分析,本文我将全面且深入地介绍MySQL常用的字符串函数,并结合具体示例和场景,帮你熟练... 目录一、字符串函数概述1.1 字符串函数的作用1.2 字符串函数分类二、字符串长度与统计函数2.1

python使用try函数详解

《python使用try函数详解》Pythontry语句用于异常处理,支持捕获特定/多种异常、else/final子句确保资源释放,结合with语句自动清理,可自定义异常及嵌套结构,灵活应对错误场景... 目录try 函数的基本语法捕获特定异常捕获多个异常使用 else 子句使用 finally 子句捕获所

postgresql使用UUID函数的方法

《postgresql使用UUID函数的方法》本文给大家介绍postgresql使用UUID函数的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录PostgreSQL有两种生成uuid的方法。可以先通过sql查看是否已安装扩展函数,和可以安装的扩展函数

MySQL字符串常用函数详解

《MySQL字符串常用函数详解》本文给大家介绍MySQL字符串常用函数,本文结合实例代码给大家介绍的非常详细,对大家学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录mysql字符串常用函数一、获取二、大小写转换三、拼接四、截取五、比较、反转、替换六、去空白、填充MySQL字符串常用函数一、

C++中assign函数的使用

《C++中assign函数的使用》在C++标准模板库中,std::list等容器都提供了assign成员函数,它比操作符更灵活,支持多种初始化方式,下面就来介绍一下assign的用法,具有一定的参考价... 目录​1.assign的基本功能​​语法​2. 具体用法示例​​​(1) 填充n个相同值​​(2)

MySql基本查询之表的增删查改+聚合函数案例详解

《MySql基本查询之表的增删查改+聚合函数案例详解》本文详解SQL的CURD操作INSERT用于数据插入(单行/多行及冲突处理),SELECT实现数据检索(列选择、条件过滤、排序分页),UPDATE... 目录一、Create1.1 单行数据 + 全列插入1.2 多行数据 + 指定列插入1.3 插入否则更

PostgreSQL中rank()窗口函数实用指南与示例

《PostgreSQL中rank()窗口函数实用指南与示例》在数据分析和数据库管理中,经常需要对数据进行排名操作,PostgreSQL提供了强大的窗口函数rank(),可以方便地对结果集中的行进行排名... 目录一、rank()函数简介二、基础示例:部门内员工薪资排名示例数据排名查询三、高级应用示例1. 每

全面掌握 SQL 中的 DATEDIFF函数及用法最佳实践

《全面掌握SQL中的DATEDIFF函数及用法最佳实践》本文解析DATEDIFF在不同数据库中的差异,强调其边界计算原理,探讨应用场景及陷阱,推荐根据需求选择TIMESTAMPDIFF或inte... 目录1. 核心概念:DATEDIFF 究竟在计算什么?2. 主流数据库中的 DATEDIFF 实现2.1

MySQL中的LENGTH()函数用法详解与实例分析

《MySQL中的LENGTH()函数用法详解与实例分析》MySQLLENGTH()函数用于计算字符串的字节长度,区别于CHAR_LENGTH()的字符长度,适用于多字节字符集(如UTF-8)的数据验证... 目录1. LENGTH()函数的基本语法2. LENGTH()函数的返回值2.1 示例1:计算字符串