【TCP/IP详解】TCP保活机制

2024-03-09 13:32
文章标签 ip 详解 tcp 机制 保活

本文主要是介绍【TCP/IP详解】TCP保活机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在需要长连接的网络通信程序中,经常需要心跳检测机制,来实现检测对方是否在线或者维持网络连接的需要。这一机制是在应用层实现的,对应的,在TCP协议中,也有类似的机制,就是TCP保活机制。


一、为什么需要保活机制?

设想这种情况,TCP连接建立后,在一段时间范围内双发没有互相发送任何数据。思考以下两个问题:

  1. 怎么判断对方是否还在线。这是因为,TCP对于非正常断开的连接系统并不能侦测到(比如网线断掉)。
  2. 长时间没有任何数据发送,连接可能会被中断。这是因为,网络连接中间可能会经过路由器、防火墙等设备,而这些有可能会对长时间没有活动的连接断掉。

基于上面两点考虑,需要保活机制。

二、TCP保活机制的实现(Linux)

保活机制是由一个保活计时器实现的。当计时器被激发,连接一段将发送一个保活探测报文,另一端接收报文的同时会发送一个ACK作为响应。

1、相关配置

具体实现上有以下几个相关的配置:

  • 保活时间:默认7200秒(2小时)
  • 保活时间间隔:默认75秒
  • 保活探测数:默认9次

查看Linux系统中TCP保活机制对应的系统配置如下(不同系统实现可能不同):

sl@Li:/proc/sys/net/ipv4$ cat tcp_keepalive_time 
7200
sl@Li:/proc/sys/net/ipv4$ cat tcp_keepalive_intvl 
75
sl@Li:/proc/sys/net/ipv4$ cat tcp_keepalive_probes 
9
2、过程描述

连接中启动保活功能的一端,在保活时间内连接处于非活动状态,则向对方发送一个保活探测报文,如果收到响应,则重置保活计时器,如果没有收到响应报文,则经过一个保活时间间隔后再次向对方发送一个保活探测报文,如果还没有收到响应报文,则继续,直到发送次数到达保活探测数,此时,对方主机将被确认为不可到达,连接被中断。

TCP保活功能工作过程中,开启该功能的一端会发现对方处于以下四种状态之一:

  1. 对方主机仍在工作,并且可以到达。此时请求端将保活计时器重置。如果在计时器超时之前应用程序通过该连接传输数据,计时器再次被设定为保活时间值。
  2. 对方主机已经崩溃,包括已经关闭或者正在重新启动。这时对方的TCP将不会响应。请求端不会接收到响应报文,并在经过保活时间间隔指定的时间后超时。超时前,请求端会持续发送探测报文,一共发送保活探测数指定次数的探测报文,如果请求端没有收到任何探测报文的响应,那么它将认为对方主机已经关闭,连接也将被断开。
  3. 客户主机崩溃并且已重启。在这种情况下,请求端会收到一个对其保活探测报文的响应,但这个响应是一个重置报文段RST,请求端将会断开连接。
  4. 对方主机仍在工作,但是由于某些原因不能到达请求端(例如网络无法传输,而且可能使用ICMP通知也可能不通知对方这一事实)。这种情况与状态2相同,因为TCP不能区分状态2与状态4,结果是都没有收到探测报文的响应。
三、保活机制的弊端

理解了上面的实现,就可以发现其存在以下两点主要弊端:

  1. 在出现短暂的网络错误的时候,保活机制会使一个好的连接断开;
  2. 保活机制会占用不必要的带宽;

所以,保活机制是存在争议的,主要争议之处在于是否应在TCP协议层实现,有两种主要观点:其一,保活机制不必在TCP协议中提供,而应该有应用层实现;其二,认为大多数应用都需要保活机制,应该在TCP协议层实现。

保活功能在默认情况下是关闭的。没有经过应用层的请求,Linux系统不会提供保活功能。

四、保活机制应用代码示例
1、 服务端代码
/* server */
#include<sys/epoll.h>
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<netinet/tcp.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>
#include <sys/time.h>#define MAX_EVENTS 1024
#define LISTEN_PORT 33333
#define MAX_BUF 65536struct echo_data;
int setnonblocking(int sockfd);
int events_handle(int epfd, struct epoll_event ev);
void run();// 应用TCP保活机制的相关代码
int set_keepalive(int sockfd, int keepalive_time, int keepalive_intvl, int keepalive_probes) {int optval;socklen_t optlen = sizeof(optval);optval = 1;if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen)) {perror("setsockopt failure.");return -1;}optval = keepalive_probes;if (-1 == setsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, &optval, optlen)) {perror("setsockopt failure.");return -1;}optval = keepalive_intvl;if (-1 == setsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, &optval, optlen)) {perror("setsockopt failure.");return -1;}optval = keepalive_time;if (-1 == setsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, &optval, optlen)) {perror("setsockopt failure.");return -1;}
}int main(int _argc, char* _argv[]) {run();return 0;
}void run() {int epfd = epoll_create1(0);if (-1 == epfd) {perror("epoll_create1 failure.");exit(EXIT_FAILURE);}char str[INET_ADDRSTRLEN];struct sockaddr_in seraddr, cliaddr;socklen_t cliaddr_len = sizeof(cliaddr);int listen_sock = socket(AF_INET, SOCK_STREAM, 0);bzero(&seraddr, sizeof(seraddr));seraddr.sin_family = AF_INET;seraddr.sin_addr.s_addr = htonl(INADDR_ANY);seraddr.sin_port = htons(LISTEN_PORT);int opt = 1;setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));if (-1 == bind(listen_sock, (struct sockaddr*)&seraddr, sizeof(seraddr))) {perror("bind server addr failure.");exit(EXIT_FAILURE);}listen(listen_sock, 5);struct epoll_event ev, events[MAX_EVENTS];ev.events = EPOLLIN;ev.data.fd = listen_sock;if (-1 == epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev)) {perror("epoll_ctl add listen_sock failure.");exit(EXIT_FAILURE);}int nfds = 0;while (1) {nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);if (-1 == nfds) {perror("epoll_wait failure.");exit(EXIT_FAILURE);}for ( int n = 0; n < nfds; ++n) {if (events[n].data.fd == listen_sock) {int conn_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &cliaddr_len);if (-1 == conn_sock) {perror("accept failure.");exit(EXIT_FAILURE);}printf("accept from %s:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));set_keepalive(conn_sock, 120, 20, 3);setnonblocking(conn_sock);ev.events = EPOLLIN | EPOLLET;ev.data.fd = conn_sock;if (-1 == epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &ev)) {perror("epoll_ctl add conn_sock failure.");exit(EXIT_FAILURE);}} else {events_handle(epfd, events[n]);}}}close(listen_sock);close(epfd);
}int setnonblocking(int sockfd){if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1) {return -1;}return 0;
}struct echo_data {char* data;int fd;
};int events_handle(int epfd, struct epoll_event ev) {printf("events_handle, ev.events = %d\n", ev.events);int fd = ev.data.fd;if (ev.events & EPOLLIN) {char* buf = (char*)malloc(MAX_BUF);bzero(buf, MAX_BUF);int count = 0;int n = 0;while (1) {n = read(fd, (buf + count), 1024);printf("step in edge_trigger, read bytes:%d\n", n);if (n > 0) {count += n;} else if (0 == n) {break;} else if (n < 0 && EAGAIN == errno) {perror("errno == EAGAIN, break.");break;} else {perror("read failure.");if (ETIMEDOUT == errno) {if (-1 == epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev)) {perror("epoll_ctl del fd failure.");exit(EXIT_FAILURE);}close(fd);return 0;}exit(EXIT_FAILURE);}}if (0 == count) {if (-1 == epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev)) {perror("epoll_ctl del fd failure.");exit(EXIT_FAILURE);}close(fd);return 0;}printf("recv from client: %s\n", buf);struct echo_data* ed = (struct echo_data*)malloc(sizeof(struct echo_data));ed->data = buf;ed->fd = fd;ev.data.ptr = ed;ev.events = EPOLLOUT | EPOLLET;if (-1 == epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev)) {perror("epoll_ctl modify fd failure.");exit(EXIT_FAILURE);}return 0;} else if (ev.events & EPOLLOUT) {struct echo_data* data = (struct echo_data*)ev.data.ptr;printf("write data to client: %s", data->data);int ret = 0;int send_pos = 0;const int total = strlen(data->data);char* send_buf = data->data;while(1) {ret = write(data->fd, (send_buf + send_pos), total - send_pos);if (ret < 0) {if (EAGAIN == errno) {sched_yield();continue;}perror("write failure.");exit(EXIT_FAILURE);}send_pos += ret;if (total == send_pos) {break;}}ev.data.fd = data->fd;ev.events = EPOLLIN | EPOLLET;if (-1 == epoll_ctl(epfd, EPOLL_CTL_MOD, data->fd, &ev)) {perror("epoll_ctl modify fd failure.");exit(EXIT_FAILURE);}free(data->data);free(data);}return 0;
}
2、客户端代码
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include <time.h>
#include <sys/time.h>
#include<stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>#define SERVER_PORT 33333
#define MAXLEN 65535void client_handle(int sock);int main(int argc, char* argv[]) {for (int i = 1; i < argc; ++i) {printf("input args %d: %s\n", i, argv[i]);}struct sockaddr_in seraddr;int server_port = SERVER_PORT;if (2 == argc) {server_port = atoi(argv[1]);}int sock = socket(AF_INET, SOCK_STREAM, 0);bzero(&seraddr, sizeof(seraddr));seraddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr);seraddr.sin_port = htons(server_port);connect(sock, (struct sockaddr *)&seraddr, sizeof(seraddr));client_handle(sock);return 0;
}void client_handle(int sock) {char sendbuf[MAXLEN], recvbuf[MAXLEN];bzero(sendbuf, MAXLEN);bzero(recvbuf, MAXLEN);int n = 0;while (1) {if (NULL == fgets(sendbuf, MAXLEN, stdin)) {break;}// 按`#`号退出if ('#' == sendbuf[0]) {break;}struct timeval start, end;gettimeofday(&start, NULL);write(sock, sendbuf, strlen(sendbuf));n = read(sock, recvbuf, MAXLEN);if (0 == n) {break;}write(STDOUT_FILENO, recvbuf, n);gettimeofday(&end, NULL);printf("time diff=%ld microseconds\n", ((end.tv_sec * 1000000 + end.tv_usec)- (start.tv_sec * 1000000 + start.tv_usec)));}close(sock);
}

只要连接后,不输入数据,连接就没有数据发送,通过抓包可以发现每120秒就会有保活探测报文发出:
保活机制
如果一直没有收到回复,会中断连接(RST)。
在这里插入图片描述

参考文档:Requirements for Internet Hosts – Communication Layers

这篇关于【TCP/IP详解】TCP保活机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

SpringBoot3.4配置校验新特性的用法详解

《SpringBoot3.4配置校验新特性的用法详解》SpringBoot3.4对配置校验支持进行了全面升级,这篇文章为大家详细介绍了一下它们的具体使用,文中的示例代码讲解详细,感兴趣的小伙伴可以参考... 目录基本用法示例定义配置类配置 application.yml注入使用嵌套对象与集合元素深度校验开发

Python中的Walrus运算符分析示例详解

《Python中的Walrus运算符分析示例详解》Python中的Walrus运算符(:=)是Python3.8引入的一个新特性,允许在表达式中同时赋值和返回值,它的核心作用是减少重复计算,提升代码简... 目录1. 在循环中避免重复计算2. 在条件判断中同时赋值变量3. 在列表推导式或字典推导式中简化逻辑

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多

SpringBoot整合mybatisPlus实现批量插入并获取ID详解

《SpringBoot整合mybatisPlus实现批量插入并获取ID详解》这篇文章主要为大家详细介绍了SpringBoot如何整合mybatisPlus实现批量插入并获取ID,文中的示例代码讲解详细... 目录【1】saveBATch(一万条数据总耗时:2478ms)【2】集合方式foreach(一万条数

Python装饰器之类装饰器详解

《Python装饰器之类装饰器详解》本文将详细介绍Python中类装饰器的概念、使用方法以及应用场景,并通过一个综合详细的例子展示如何使用类装饰器,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录1. 引言2. 装饰器的基本概念2.1. 函数装饰器复习2.2 类装饰器的定义和使用3. 类装饰

MySQL 中的 JSON 查询案例详解

《MySQL中的JSON查询案例详解》:本文主要介绍MySQL的JSON查询的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录mysql 的 jsON 路径格式基本结构路径组件详解特殊语法元素实际示例简单路径复杂路径简写操作符注意MySQL 的 J

Go语言开发实现查询IP信息的MCP服务器

《Go语言开发实现查询IP信息的MCP服务器》随着MCP的快速普及和广泛应用,MCP服务器也层出不穷,本文将详细介绍如何在Go语言中使用go-mcp库来开发一个查询IP信息的MCP... 目录前言mcp-ip-geo 服务器目录结构说明查询 IP 信息功能实现工具实现工具管理查询单个 IP 信息工具的实现服

Python ZIP文件操作技巧详解

《PythonZIP文件操作技巧详解》在数据处理和系统开发中,ZIP文件操作是开发者必须掌握的核心技能,Python标准库提供的zipfile模块以简洁的API和跨平台特性,成为处理ZIP文件的首选... 目录一、ZIP文件操作基础三板斧1.1 创建压缩包1.2 解压操作1.3 文件遍历与信息获取二、进阶技

一文详解Java异常处理你都了解哪些知识

《一文详解Java异常处理你都了解哪些知识》:本文主要介绍Java异常处理的相关资料,包括异常的分类、捕获和处理异常的语法、常见的异常类型以及自定义异常的实现,文中通过代码介绍的非常详细,需要的朋... 目录前言一、什么是异常二、异常的分类2.1 受检异常2.2 非受检异常三、异常处理的语法3.1 try-

Java中的@SneakyThrows注解用法详解

《Java中的@SneakyThrows注解用法详解》:本文主要介绍Java中的@SneakyThrows注解用法的相关资料,Lombok的@SneakyThrows注解简化了Java方法中的异常... 目录前言一、@SneakyThrows 简介1.1 什么是 Lombok?二、@SneakyThrows