TCP 保活机制

为什么需要 TCP 保活机制

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

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

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

其实 有一部分人认为,keep-alive 的检测应该放在 应用层 而不是 传输层。

TCP保活机制的实现 (Linux)

系统级别:

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

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

可以通过 /proc/sys/net/ipv4/ 接口查看

1
2
3
$ cat /proc/sys/net/ipv4/tcp_keepalive_time
$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl

或 通过 sysctl 查看

1
$ sysctl -A | grep keepalive

TCP 保活机制试验:

首先将 tcp_keepalive_time 设置为 20,即 20s 内 连接上没有数据收发就启动 间隔定时器;

1
$ echo 20 | sudo tee /proc/sys/net/ipv4/tcp_keepalive_time

tcp_keepalive_intvl 设置为 5,即如果连接不活跃(开启定时器后,发送一个探测报文,但是没收到响应),则每 5s 发送一个探测报文;

1
$ echo 5 | sudo tee /proc/sys/net/ipv4/tcp_keepalive_intvl

tcp_keepalive_probes 设置为 2。即如果 发出探测报文后 对端没有回应则重复发送探测报文的次数。

1
$ echo 2 | sudo tee /proc/sys/net/ipv4/tcp_keepalive_probes

注意:修改 /proc 接口中的内容,不能用 vi/vim 编辑器,因为 vi/vim 的做法是先根据源文件创建一个 .swap 临时文件,而 /proc 中的内容都是 内存中的映像,根本不存在于 磁盘中,如果用 vi/vim 去修改必定得到 E667: Fsync failed 错误。

在两台云服务器上进行实验,监听的一端设置 tcp keep-alive

1
2
# ip: 124.70.82.205
$ nohup sudo nc -l -p443 -k & # -k 表示开启 tcp keep-alive 机制

在另一台服务器上对 124.70.82.205 443 发起连接

1
2
# ip: 112.124.36.253
$ nc 124.70.82.205 443

在 ip: 124.70.82.205 上对 端口 443 进行抓包:

1
2
3
4
5
6
7
8
9
$ sudo tcpdump -i eth0 port 443
10:45:09.891760 IP 124.70.82.205.https > 112.124.36.253.44338: Flags [.], ack 2867648125, win 510, options [nop,nop,TS val 956683255 ecr 4136667676], length 0
10:45:09.920708 IP 112.124.36.253.44338 > 124.70.82.205.https: Flags [.], ack 1, win 229, options [nop,nop,TS val 4136687900 ecr 956519734], length 0

10:45:30.115763 IP 124.70.82.205.https > 112.124.36.253.44338: Flags [.], ack 1, win 510, options [nop,nop,TS val 956703479 ecr 4136687900], length 0
10:45:30.144706 IP 112.124.36.253.44338 > 124.70.82.205.https: Flags [.], ack 1, win 229, options [nop,nop,TS val 4136708124 ecr 956519734], length 0

10:45:50.339764 IP 124.70.82.205.https > 112.124.36.253.44338: Flags [.], ack 1, win 510, options [nop,nop,TS val 956723703 ecr 4136708124], length 0
10:45:50.368749 IP 112.124.36.253.44338 > 124.70.82.205.https: Flags [.], ack 1, win 229, options [nop,nop,TS val 4136728348 ecr 956519734], length 0

可以看到 由于连接不活跃,每隔 一个 tcp_keepalive_time 都会向对端 发送一个 keep-alive 报文,来探测对端是否还“活着”。

上述实验的过程描述:

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

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

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

tcp 保活机制的弊端:保活机制会占用不必要的带宽

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

这里修改的 /proc 中的变量会导致 全局(整个系统)的 tcp keep-alive 机制发送变化,那么能不能只针对一个 socket 进行 keep-alive 机制的制定呢?

针对单个 socket 的保活机制

下面介绍针对单个 socket 连接 细粒度设置 的三个选项参数:

保活时间:TCP_KEEPIDLE、保活探测时间间隔:TCP_KEEPINTVL、探测循环次数:TCP_KEEPCNT(可通过 man 7 tcp 中 Socket options 这一节查看细节)

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/* server */
#include<sys/epoll.h>
···

#define MAX_EVENTS 1024
#define LISTEN_PORT 33333
#define MAX_BUF 65536

struct 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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* client */
#include<unistd.h>
···

#define SERVER_PORT 33333
#define MAXLEN 65535

void 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;
}

参考

  1. 【TCP/IP详解】TCP保活机制
  2. HTTP keep-alive和TCP keepalive的区别,你了解吗?