Linux内核源码剖析之TCP保活机制(KeepAlive)

写在前面:

版本信息:

Linux内核2.6.24(大部分centos、ubuntu应该都在3.1+。但是2.6的版本比较稳定,后续版本本质变化也不是很大)

ipv4 协议

https://blog.csdn.net/ComplexMaze/article/details/124201088

本文使用案例如上地址,感谢案例的分享,本篇文章核心部分还是在Linux内核源码分析~

为什么写下这篇文章,因为在实际项目中,是无法避免TCP通讯(对于这点,可能大部分Java程序员感受不到底层的网络通讯),正因为无法避免TCP通讯,恰好TCP通讯存在三次握手和四次挥手的过程,如果建立一次连接就三次握手和四次挥手,而我们清楚的知道三次握手和四次挥手是同步的过程,此过程也会带来不少的时间浪费和资源的浪费。所以Linux内核TCP网络协议栈就出现了KeepAlive机制,此机制减少三次握手和四次挥手次数,第一次建立连接后保持长连接,后续通讯就可以只考虑发送数据报文即可。往往出现一个机制解决某个问题,其他问题又出现,如果所有连接都建立长连接保活机制,而连接数又有限制,此时该如何解决呢?如下代码,Linux使用心跳机制去检测连接是否存活~

#define TCP_KEEPALIVE_TIME	(120*60*HZ)	    // 首次,2小时
#define TCP_KEEPALIVE_PROBES	9		    // 重试9次
#define TCP_KEEPALIVE_INTVL	(75*HZ)         // 后续,每75秒一次
  1. 在Linux内核中默认关闭KeepAlive
  2. 开启KeepAlive后,默认2小时后往对端发送心跳包,检查是否还活着
  3. 默认后续每75秒往对端发送心跳包,检查是否还活着
  4. 默认当对端9次都没有响应报文就发送RST报文,断开TCP连接,释放资源!
  5. 当然这一切参数都可以配置,通过sys_setsockopt系统调用,当然setsockopt函数库就行啦

回到上述描述的话题,往往出现一个机制解决某个问题,其他问题又出现。解决了频繁握手和挥手的时间,但是连接数量不够的问题又出现了,可能很多连接建立在那里,完全不通讯了,或者对端已经断网,或者宕机等等原因占用连接不释放,而Linux默认一个连接存活检测需要2个小时+ 才去检测对端是否活着,如果说服务器的负荷比较大,2小时才检测一次,会导致正常请求无法进行,所以此参数需要通过setsockopt函数库重新设置参数(当然,如果是Java等等虚拟机语言,本身也有自身的封装函数去操作setsockopt函数库,或者直接调用sys_setsockopt系统调用,这个需要看语言手册~!)话又说回来,如果设置的阈值大小、时间太短的问题也会很明显,一直都在发心跳包检测,甚至性能损耗大于了握手和挥手的时间,所以需要根据业务环境、服务器的硬件从性能损耗和空闲连接数量做折中考虑~

案例:

下面是C语言的服务端的案例源码,此案例是借用的,但是我们重点关心机制~

/*server.c*/
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include <netinet/tcp.h>
​
#define PORT 4000//端口号 
#define BACKLOG 5/*最大监听数*/ 
#define MAX_DATA 100//接收到的数据最大程度 
​
int main(){int sockfd,new_fd;/*socket句柄和建立连接后的句柄*/struct sockaddr_in my_addr;/*本方地址信息结构体,下面有具体的属性赋值*/struct sockaddr_in their_addr;/*对方地址信息*/int sin_size;char buf[MAX_DATA];//储存接收数据 
​sockfd=socket(AF_INET,SOCK_STREAM,0);//建立socket if(sockfd==-1){printf("socket failed:%d",errno);return -1;}my_addr.sin_family=AF_INET;/*该属性表示接收本机或其他机器传输*/my_addr.sin_port=htons(PORT);/*端口号*/my_addr.sin_addr.s_addr=htonl(INADDR_ANY);/*IP,括号内容表示本机IP*/bzero(&(my_addr.sin_zero),8);/*将其他属性置0*/if(bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr))<0){//绑定地址结构体和socketprintf("bind error");return -1;}listen(sockfd,BACKLOG);//开启监听 ,第二个参数是最大监听数 while(1){sin_size=sizeof(struct sockaddr_in);new_fd=accept(sockfd,(struct sockaddr*)&their_addr,&sin_size);//在这里阻塞知道接收到消息,参数分别是socket句柄,接收到的地址信息以及大小 // 开启保活,1分钟内探测不到,断开连接int keep_alive = 1;int keep_idle = 3;int keep_interval = 1;int keep_count = 57;if (setsockopt(new_fd, SOL_SOCKET, SO_KEEPALIVE, &keep_alive, sizeof(keep_alive))) {perror("Error setsockopt(SO_KEEPALIVE) failed");exit(1);}if (setsockopt(new_fd, IPPROTO_TCP, TCP_KEEPIDLE, &keep_idle, sizeof(keep_idle))) {perror("Error setsockopt(TCP_KEEPIDLE) failed");exit(1);}if (setsockopt(new_fd, SOL_TCP, TCP_KEEPINTVL, (void *)&keep_interval, sizeof(keep_interval))) {perror("Error setsockopt(TCP_KEEPINTVL) failed");exit(1);}if (setsockopt(new_fd, SOL_TCP, TCP_KEEPCNT, (void *)&keep_count, sizeof(keep_count))) {perror("Error setsockopt(TCP_KEEPCNT) failed");exit(1);}while(new_fd != -1) {recv(new_fd,buf,MAX_DATA,0);//将接收数据打入buf,参数分别是句柄,储存处,最大长度,其他信息(设为0即可)。 printf("%s",buf);}}return 0;
} 

此服务端案例非常的简单,当客户端与服务端建立连接后,修改KeepAlive的机制参数,使用setsockopt库函数修改。

SO_KEEPALIVE:开启KeepAlive机制

TCP_KEEPIDLE:首次检测的时长

TCP_KEEPINTVL:下次检测的间隔时长

TCP_KEEPCNT:重试阈值次数

源码分析:

首先看到TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT这三个参数的设置,源码在net/ipv4/tcp.c 文件do_tcp_setsockopt方法,此方法由sys_setsockopt系统调用方法调用。

static int do_tcp_setsockopt(struct sock *sk, int level,int optname, char __user *optval, int optlen)
{struct tcp_sock *tp = tcp_sk(sk);struct inet_connection_sock *icsk = inet_csk(sk);int val;int err = 0;switch (optname) {…………case TCP_KEEPIDLE:		// 设置第一次触发的时间if (val < 1 || val > MAX_TCP_KEEPIDLE)err = -EINVAL;else {// 算出设置的时间tp->keepalive_time = val * HZ;// 如果KeepAlive机制已开启,并且当前不是关闭状态和监听状态。if (sock_flag(sk, SOCK_KEEPOPEN) &&!((1 << sk->sk_state) &(TCPF_CLOSE | TCPF_LISTEN))) {// 当前时间 - 上次ACK的时候 = 相对时间__u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;if (tp->keepalive_time > elapsed)// 如果上次ACK同步的时间小于设置的时间,那就把剩余的时间算出来elapsed = tp->keepalive_time - elapsed;else// 如果上次ACK同步的时间大于设置的时间,那就立马检测elapsed = 0;// 设置内核的定时器inet_csk_reset_keepalive_timer(sk, elapsed);}}break;case TCP_KEEPINTVL:			// 设置每次的间隔时间if (val < 1 || val > MAX_TCP_KEEPINTVL)err = -EINVAL;elsetp->keepalive_intvl = val * HZ;break;case TCP_KEEPCNT:			// 设置阈值次数if (val < 1 || val > MAX_TCP_KEEPCNT)err = -EINVAL;elsetp->keepalive_probes = val;break;release_sock(sk);return err;
}

这里非常的简单,通过switch case的形式把参数添加到结构体中,并且设置了首次触发的时间

接下来,我们看到定时器何时设置的。在net/ipv4/tcp_ipv4.c 文件中tcp_v4_init_sock方法。

static int tcp_v4_init_sock(struct sock *sk)
{…………tcp_init_xmit_timers(sk);…………return 0;
}void tcp_init_xmit_timers(struct sock *sk)
{inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,&tcp_keepalive_timer);
}void inet_csk_init_xmit_timers(struct sock *sk,void (*retransmit_handler)(unsigned long),void (*delack_handler)(unsigned long),void (*keepalive_handler)(unsigned long))
{struct inet_connection_sock *icsk = inet_csk(sk);…………// 初始化sk->sk_timer,也即初始化timer_list// timer_list在内核是一个定时器的结构体init_timer(&sk->sk_timer);// 设置定时器的回调函数sk->sk_timer.function		     = keepalive_handler;…………
}

把大部分无关的代码省略掉以后,源码看起来非常的简单,这里初始化了定时器,并且把定时器的回调函数设置成tcp_keepalive_timer,所以接下来,我们直接分析tcp_keepalive_timer方法即可。在net/ipv4/tcp_timer.c 文件中 tcp_keepalive_timer方法。

// 当达到keepalive设置的值以后回掉此方法。
static void tcp_keepalive_timer (unsigned long data)
{struct sock *sk = (struct sock *) data;struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);__u32 elapsed;/* Only process if socket is not in use. */bh_lock_sock(sk);if (sock_owned_by_user(sk)) {// 这里很简单,因为锁的原因,所以需要重试。inet_csk_reset_keepalive_timer (sk, HZ/20);goto out;}// 4次挥手阶段,而此时达到了保活的检测,此时发送RST报文给对端,表示我要断开了,然后释放资源即可。if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {if (tp->linger2 >= 0) {const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;if (tmo > 0) {tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);goto out;}}tcp_send_active_reset(sk, GFP_ATOMIC);goto death;}// 如果KeepAlive没有开启,或者当前已经是关闭状态if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)goto out;// 算出下次检测的时间elapsed = keepalive_time_when(tp);// 此时正在发送报文,所以无须检测,直接重置下次检测的时间if (tp->packets_out || tcp_send_head(sk))goto resched;// 算出距离上一次ACK的相对时间elapsed = tcp_time_stamp - tp->rcv_tstamp;// 如果上一次ACK的相对时间 大于等于 设置的时间,那么就代表达到一次阈值if (elapsed >= keepalive_time_when(tp)) {// 查看是否达到次数阈值,达到阈值后直接发送RST报文给对方,然后关闭连接。if ((!tp->keepalive_probes && icsk->icsk_probes_out >= sysctl_tcp_keepalive_probes) ||(tp->keepalive_probes && icsk->icsk_probes_out >= tp->keepalive_probes)) {tcp_send_active_reset(sk, GFP_ATOMIC);tcp_write_err(sk);goto out;}// 没达到阈值的情况// 尝试发送报文给对方,看是否还活着if (tcp_write_wakeup(sk) <= 0) {// 如果回复了,那就把下次检测的时间设置好icsk->icsk_probes_out++;elapsed = keepalive_intvl_when(tp);} else {		// 对端没有回复,不知道是因为丢失还是怎么了,所以加快速度,尝试下一次。elapsed = TCP_RESOURCE_PROBE_INTERVAL;}} else {// 没有达到上次ACK的相对时间,所以算出差值,设置到定时器中。elapsed = keepalive_time_when(tp) - elapsed;}TCP_CHECK_TIMER(sk);sk_stream_mem_reclaim(sk);resched:// 把最新值设置到定时器中。inet_csk_reset_keepalive_timer (sk, elapsed);goto out;death:// 关闭连接,释放资源。tcp_done(sk);out:bh_unlock_sock(sk);sock_put(sk);
}

此方法是当定时器结束后回调执行,检测是否达到了我们设置或者默认的阈值,如果没有达到,再设置下一次定时器的时间,如果达到了就发送RST报文,关闭连接,释放资源~!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/41916.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高级AI赋能Fortinet FortiXDR解决方案

扩展检测和响应 (XDR&#xff1a;Extended Detection and Response) 解决方案旨在帮助组织整合分布式安全技术&#xff0c;更有效地识别和响应活动的威胁。虽然 XDR 是一种新的技术概念&#xff0c;但其构建基础是端点检测和响应 (EDR&#xff1a;Endpoint Detection and Respo…

代码随想录算法训练营第50天|动态规划part11

8.16周三 123.买卖股票的最佳时机III 188.买卖股票的最佳时机IV 详细布置 123.买卖股票的最佳时机III 题目&#xff1a;最多买卖两次 题解&#xff1a; 1、 dp[i][0]没有操作 &#xff08;其实我们也可以不设置这个状态&#xff09; dp[i][1]第一次持有股票 dp[i][2]第一…

CSDN✖索尼 toio™应用创意开发征集征集活动 创意公示! 入选的用户看过来~

索尼toio™应用创意开发征集活动自开启以来&#xff0c;收到了很多精彩的创意&#xff01;接下来&#xff0c;我们将公示入选的20个优秀创意和10个入围创意&#xff0c;以下提到ID的小伙伴注意啦&#xff0c;你们将有机会顺利进入活动的第二阶段&#xff0c;注意查收你们的信箱…

javaScript:快乐学习计时器

目录 一.前言 二.计时器 1.计时器的分类 2. 创建计时器的方式 创建间隔计时器 创建方式三种 1.匿名函数 2.使用函数直接作为计时器的执行函数 2.使用函数直接作为计时器的执行函数,用字符串的形式写入 3.计时器的返回值 4.清除计时器 5.延迟计时器 相关代码 一.前言 在…

Linux--实用指令与方法(部分)

下文主要是一些工作中零碎的常用指令与方法 实用指令与方法&#xff08;部分&#xff09; linux长时间保持ssh连接 这个问题的原因是&#xff1a;设置检测时间太短&#xff0c;或者没有保持tcp长连接。 解决步骤&#xff1a; 步骤1&#xff1a;打开sshd配置文件&#xff0…

nbcio-boot从3.0升级到3.1的出现用户管理与数据字典bug

升级后出现 系统管理里的用户管理出现下面问题 2023-08-17 09:44:38.902 [http-nio-8080-exec-4] [1;31mERROR[0;39m [36mo.jeecg.common.exception.JeecgBootExceptionHandler:69[0;39m - java.lang.String cannot be cast to java.lang.Long java.lang.ClassCastException:…

【JS 线性代数算法之向量与矩阵】

线性代数算法 一、向量的加减乘除1. 向量加法2. 向量减法3. 向量数乘4. 向量点积5. 向量叉积 二、矩阵的加减乘除1. 矩阵加法2. 矩阵减法3. 矩阵数乘4. 矩阵乘法 常用数学库 线性代数是数学的一个分支&#xff0c;用于研究线性方程组及其解的性质、向量空间及其变换的性质等。在…

windows bat脚本,使用命令行增加/删除防火墙:入站-出站,规则

常常手动设置防火墙的入站或出站规则&#xff0c;比较麻烦&#xff0c;其实可以用命令行搞定。 下面是禁用BCompare.exe连接网络的例子&#xff1a; ECHO OFF&(PUSHD "%~DP0")&(REG QUERY "HKU\S-1-5-19">NUL 2>&1)||(powershell -Comm…

web即时通讯系统与APP即时通讯系统有什么区别?

随着互联网的不断发展&#xff0c;即时通讯技术也在不断地完善和发展&#xff0c;其中Web即时通讯系统和APP即时通讯系统成为了人们广泛使用的两种通讯方式。那么&#xff0c;这两者之间究竟有什么区别呢&#xff1f;在本文中&#xff0c;我们将为您详细介绍这两种通讯方式的区…

如何将labelImg打包成exe

最近整理一下数据标注这块的内容&#xff0c;在目标检测和目标分割里面用的最多的标注工具labelimg&#xff0c;labelme labelimg主要用于目标检测领域制作自己的数据集&#xff0c;如&#xff1a;YOLO系列目标检测模型 labelme主要用于图像分割领域制作自己的数据集&#xf…

如何仿写简易tomcat 实现思路+代码详细讲解

仿写之前&#xff0c;我们要搞清楚都要用到哪些技术 自定义注解&#xff0c;比如Tomcat使用的是Servlet&#xff0c;我们可以定义一个自己的MyServlet构造请求体和返回体&#xff0c;比如tomcat使用HttpRequest&#xff0c;我们可以自己定义myHttpRequestjava去遍历一个指定目…

Structs新增接口 报错404,找不到资源

起因&#xff1a;最近在一个古老框架structs上开发新功能&#xff0c;由于之前没接触过&#xff0c;故此记录 新增接口&#xff0c; 接口类&#xff1a; Path("/A") Produces({ MediaType.APPLICATION_JSON }) public interface Money {POSTPath("/B")Resu…

数据结构——链表详解

链表 文章目录 链表前言认识链表单链表结构图带头单循环链表结构图双向循环链表结构图带头双向循环链表结构图 链表特点 链表实现(带头双向循环链表实现)链表结构体(1) 新建头节点(2) 建立新节点(3)尾部插入节点(4)删除节点(5)头部插入节点(6) 头删节点(7) 寻找节点(8) pos位置…

网络编程socket.close/output.close/socket.shutdownOutput区别与流程分析

文章目录 三种方法效果的区别套接字Socket关闭与释放的区别服务器执行三种关闭操作后&#xff0c;继续发送/接收数据会发生什么socket.shutdownOutput 关闭连接 找了半个小时没一个说明白的帖子&#xff0c;真的折磨 三种方法效果的区别 socket.close()Socket主动禁止输入和输…

APP外包开发原生和H5的区别

原生开发和H5开发是两种不同的方法&#xff0c;用于创建移动应用程序。它们具有各自的特点、优势和劣势&#xff0c;适用于不同的应用场景。以下是原生开发和H5开发之间的一些主要区别&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发…

DELETE 与TRUNCATE区别

DELETE 与TRUNCATE区别 要清空 PostgreSQL 中的表数据&#xff0c;可以使用 DELETE 或 TRUNCATE 语句。下面是两种方法的示例&#xff1a; 使用 DELETE 语句清空表数据&#xff1a; DELETE FROM 表名;例如&#xff0c;要清空名为 users 的表数据&#xff1a; DELETE FROM u…

未来公文的智能化进程

随着技术的飞速发展&#xff0c;公文——这个有着悠久历史的官方沟通方式&#xff0c;也正逐步走向智能化的未来。自动化、人工智能、区块链...这些现代科技正重塑我们的公文制度&#xff0c;让其变得更加高效、安全和智慧。 1.语义理解与自动生成 通过深度学习和NLP&#xff…

14-案例:购物车

综合案例-购物车 需求说明: 1. 渲染功能 v-if/v-else v-for :class 2. 删除功能 点击传参 filter过滤覆盖原数组 3. 修改个数 点击传参 find找对象 4. 全选反选 计算属性computed 完整写法 get/set 5. 统计 选中的 总价 和 数量 计算属性conputed reduce条件求和 6. 持久化到本…

电子商务公开密钥加密法

(一)定义与应用原理 公开密钥加密法是针对私有密钥加密法的缺陷而提出来的。是电子商务应 用的核心密码技术。所谓公开密钥加密&#xff0c;就是指在计算机网络上甲、乙两用户之间 进行通信时&#xff0c;发送方甲为了保护要传输的明文信息不被第三方窃取&#xff0c;采用密…

从零基础到精通IT:探索高效学习路径与成功案例

文章目录 导语&#xff1a;第一步&#xff1a;明确学习目标与方向选择适合的IT方向设定具体的学习目标咨询和调研 第二步&#xff1a;系统学习基础知识选择适合的编程语言学习数据结构和算法掌握操作系统和计算机网络基础 第三步&#xff1a;实践项目锻炼技能选择合适的项目编写…