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

相关文章

Kotlin 作用域函数apply、let、run、with、also使用指南

《Kotlin作用域函数apply、let、run、with、also使用指南》在Kotlin开发中,作用域函数(ScopeFunctions)是一组能让代码更简洁、更函数式的高阶函数,本文将... 目录一、引言:为什么需要作用域函数?二、作用域函China编程数详解1. apply:对象配置的 “流式构建器”最

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

一文带你了解SpringBoot中启动参数的各种用法

《一文带你了解SpringBoot中启动参数的各种用法》在使用SpringBoot开发应用时,我们通常需要根据不同的环境或特定需求调整启动参数,那么,SpringBoot提供了哪些方式来配置这些启动参... 目录一、启动参数的常见传递方式二、通过命令行参数传递启动参数三、使用 application.pro

C++中::SHCreateDirectoryEx函数使用方法

《C++中::SHCreateDirectoryEx函数使用方法》::SHCreateDirectoryEx用于创建多级目录,类似于mkdir-p命令,本文主要介绍了C++中::SHCreateDir... 目录1. 函数原型与依赖项2. 基本使用示例示例 1:创建单层目录示例 2:创建多级目录3. 关键注

C++中函数模板与类模板的简单使用及区别介绍

《C++中函数模板与类模板的简单使用及区别介绍》这篇文章介绍了C++中的模板机制,包括函数模板和类模板的概念、语法和实际应用,函数模板通过类型参数实现泛型操作,而类模板允许创建可处理多种数据类型的类,... 目录一、函数模板定义语法真实示例二、类模板三、关键区别四、注意事项 ‌在C++中,模板是实现泛型编程

基于@RequestParam注解之Spring MVC参数绑定的利器

《基于@RequestParam注解之SpringMVC参数绑定的利器》:本文主要介绍基于@RequestParam注解之SpringMVC参数绑定的利器,具有很好的参考价值,希望对大家有所帮助... 目录@RequestParam注解:Spring MVC参数绑定的利器什么是@RequestParam?@

kotlin的函数forEach示例详解

《kotlin的函数forEach示例详解》在Kotlin中,forEach是一个高阶函数,用于遍历集合中的每个元素并对其执行指定的操作,它的核心特点是简洁、函数式,适用于需要遍历集合且无需返回值的场... 目录一、基本用法1️⃣ 遍历集合2️⃣ 遍历数组3️⃣ 遍历 Map二、与 for 循环的区别三、高

C语言字符函数和字符串函数示例详解

《C语言字符函数和字符串函数示例详解》本文详细介绍了C语言中字符分类函数、字符转换函数及字符串操作函数的使用方法,并通过示例代码展示了如何实现这些功能,通过这些内容,读者可以深入理解并掌握C语言中的字... 目录一、字符分类函数二、字符转换函数三、strlen的使用和模拟实现3.1strlen函数3.2st

MySQL中COALESCE函数示例详解

《MySQL中COALESCE函数示例详解》COALESCE是一个功能强大且常用的SQL函数,主要用来处理NULL值和实现灵活的值选择策略,能够使查询逻辑更清晰、简洁,:本文主要介绍MySQL中C... 目录语法示例1. 替换 NULL 值2. 用于字段默认值3. 多列优先级4. 结合聚合函数注意事项总结C

SpringBoot接收JSON类型的参数方式

《SpringBoot接收JSON类型的参数方式》:本文主要介绍SpringBoot接收JSON类型的参数方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、jsON二、代码准备三、Apifox操作总结一、JSON在学习前端技术时,我们有讲到过JSON,而在