TCP保活机制
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
-
如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
-
如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。
TCP保活相关参数如下:
-
SO_KEEPALIVE:是否开启保活
-
TCP_KEEPIDLE:Start keeplives after this period
-
TCP_KEEPINTVL:Interval between keepalives
-
TCP_KEEPCNT:Number of keepalives before death
保活机制示例
开启一个简单的tcp服务器,代码如下
/*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){//绑定地址结构体和socket printf("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; }
上述保活参数(int keep_alive = 1;int keep_idle = 3;int keep_interval = 1;int keep_count = 57;)表示3秒内无交互后,每隔1秒检测一次,57次都没得到响应时会断开连接。
编译可执行文件:gcc server.c -o server
服务端开启服务:./server
客户端连接服务:telnet 192.168.137.101 4000
这里服务端地址为192.168.137.101
上图可以看到每隔1秒有保活报文,且客户端正常响应。
接下来我们将192.168.137.102的网卡down掉
可以看到客户端已经无响应了,经过1分种左右就可看见服务端的连接断掉了
TCP异常断开连接场景
几个思考点:
是否开启keepalive?
是否存在数据交互?
进程崩溃;
客户端主机宕机(客户端网络不可用 是同种情况);
keepalive开启,服务器会探测客户端,不会出现问题。
keepalive未开启,存在以下情况
1、客户端进程崩溃
使用 kill -9 来模拟进程崩溃的情况,发现在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手。
所以,即使没有开启 TCP keepalive,且双方也没有数据交互的情况下,如果其中一方的进程发生了崩溃,这个过程操作系统是可以感知的到的,于是就会发送 FIN 报文给对方,然后与对方进行 TCP 四次挥手。
2、存在数据交互,客户端主机宕机
-
第一种,客户端主机宕机,又迅速重启,会发生什么?
-
第二种,客户端主机宕机,一直没有重启,会发生什么?
客户端主机宕机,又迅速重启
在客户端主机宕机后,服务端向客户端发送的报文会得不到任何的响应,在一定时长后,服务端就会触发超时重传机制,重传未得到响应的报文。
服务端重传报文的过程中,刚好客户端主机重启完成,这时客户端的内核就会接收重传的报文,:
-
如果客户端主机上没有进程监听该 TCP 报文的目标端口号,由于找不到目标端口,客户端内核就会回复 RST 报文,重置该 TCP 连接;
-
如果客户端主机上有进程监听该 TCP 报文的目标端口号,由于客户端主机重启后,之前的 TCP 连接的数据结构已经丢失了,客户端内核里协议栈会发现找不到该 TCP 连接的 socket 结构体,于是就会回复 RST 报文,重置该 TCP 连接。
所以,只要有一方重启完成后,收到之前 TCP 连接的报文,都会回复 RST 报文,以断开连接。
客户端主机宕机,一直没有重启
这种情况,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了。
那具体重传几次呢?
在 Linux 系统中,提供了一个叫 tcp_retries2 配置项,默认值是 15,如下:
[root@node01 ~]# cat /proc/sys/net/ipv4/tcp_retries2 15
这个内核参数是控制,在 TCP 连接建立的情况下,超时重传的最大次数。
不过 tcp_retries2 设置了 15 次,并不代表 TCP 超时重传了 15 次才会通知应用程序终止该 TCP 连接,内核还会基于「最大超时时间」来判定。
每一轮的超时时间都是倍数增长的,比如第一次触发超时重传是在 2s 后,第二次则是在 4s 后,第三次则是 8s 后,以此类推。
内核会根据 tcp_retries2 设置的值,计算出一个最大超时时间。
在重传报文且一直没有收到对方响应的情况时,先达到「最大重传次数」或者「最大超时时间」这两个的其中一个条件后,就会停止重传。
3、不存在数据交互,客户端主机宕机
如果客户端主机崩溃了,服务端是无法感知到的,在加上服务端没有开启 TCP keepalive,又没有数据交互的情况下,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。
在没有使用 TCP 保活机制,且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态时,并不代表另一方的 TCP 连接还一定是正常的。