一、RFC1122
关于 TCP 中 keepalive 机制的描述在 RFC1122 中的 4.2.3.6 小结 ,内容不多,总结一下有下面几点:
- TCP 实现中 可以 包含 keepalive 这个功能,也可以不包含。
- 如果实现了 keepalive 这个功能,那么 必须 能为每个 TCP 连接开启或者关闭 keepalive 功能。同时,此功能默认 必须 是关闭的。
- 必须 在规定的时间间隔内,既没有收到包含数据包,也没有收到包含 ACK 标记的包时,才能发送 keepalive 的包。这个时间间隔 必须 是可配置的,并且默认 必须 不能小于两个小时。
- TCP 不能保证没有数据仅有 ACK 标记的包一定会被对端接收到。所以,如果实现了 keepalive 机制,必须 不能把任何特定探测失败的连接标记为死链接。
- keepalive 的实现 应该 是发送一个不包含数据的 keepalive 包。但是为了与不规范的 TCP 实现兼容,可以 配置在 keepalive 包中包含一个垃圾的字节,来避免第 4 点的情况。
- 一些 TCP 实现 keepalive 的方式是:发送一个 Seq 编号为 Next-1 (如果是正常的数据发送 Seq 的值应为 Next ),并且可能包含或者不包含一个垃圾字节的包给对端。因为 keepalive 包中的 Seq 值在对端 TCP 的滑动窗口之外(因为已经确认收到了),所以对端会回应一个包含有 ACK 标记的包,并据此来判断对端是否存活。如果对端网络故障或者崩溃,则会回应一个带有 RST 标记的包来重置连接。
- 一些不够完善的 TCP 实现可能不能正确的响应 Seq 编号为 Next-1 且不带数据的 keepalive 包,但是却可以响应带有一个字节垃圾信息的包。所以在实现 keepalive 机制时,需要判断这种情况,并据此决定在发送 keepalive 包时,是否要带上一字节的垃圾数据。
- TCP 的 keepalive 机制应该仅用在服务器的程序中,因为如果客户端崩溃,服务器就能及时的释放资源。
二、相关设置
1. SO_KEEPALIVE
默认情况下,新创建出来的套接字的 keepalive 功能都是关闭的。
int keepalive = 1;
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (const char*)&keepalive, sizeof(keepalive));
2. TCP_KEEPIDLE
这个选项用于控制经过多久后,既没有收到包含数据包,也没有收到包含 ACK 标记的包时,就发送 keepalive 的包。
int keepidle = 7200;
setsockopt(sock, SOL_TCP, TCP_KEEPIDLE, (const char*)&keepidle, sizeof(keepidle));
Windows下,代码中的
SOL_TCP
要替换成IPPROTO_TCP
,下同。
3. TCP_KEEPINTVL
这个选项用于控制一旦开始发送了 keepalive 后,经过多久时间后再发送下一个包。
int keepintvl = 75;
setsockopt(sock, SOL_TCP, TCP_KEEPINTVL, (const char*)&keepintvl, sizeof(keepintvl));
4. TCP_KEEPCNT
这个选项用于控制在经过发送了多少个 keepalive 包但是都没有收到回复后,可以认为连接已经断开了。
int keepcnt = 9;
setsockopt(sock, SOL_TCP, TCP_KEEPCNT, (const char*)&keepcnt, sizeof(keepcnt));
5. 默认值
Linux下,系统的默认值可以通过命令来查看:
cat /proc/sys/net/ipv4/tcp_keepalive_time #7200
cat /proc/sys/net/ipv4/tcp_keepalive_intvl #75
cat /proc/sys/net/ipv4/tcp_keepalive_probes #9
上面参数的意义是:在未收到包含数据或 ACK 标志的包 7200 秒后,开始发送 keepalive 包,并每隔 75 秒重发一次,如果连发 9 次都没有响应,就认为网络已断开。但是如果 9 次内的某次收到了 keepalive 包对应的 ACK 包,则又会在未收到包含数据或ACK 标志的包 7200 秒后重新开始发 keepalive 包。
Windows下,不同系统版本默认值不同,一般是7200秒,1秒,10次。具体的可以参考:https://docs.microsoft.com/en-us/windows/win32/winsock/sio-keepalive-vals#remarks。
三、平台实现
1. 测试代码
写一段代码简单的测试一下:
// file: keepalive.c
// gcc -o keepalive keepalive.c
#ifdef _WIN32
#include <WinSock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#pragma comment(lib, "ws2_32.lib")
#define socklen_t int
#define SOL_TCP IPPROTO_TCP
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define SOCKET int
#endif
#include <stdio.h>
#include <string.h>
#define IP "192.168.104.106"
#define PORT 9876
void print_sock_opt(const char* name, SOCKET sockfd, int level, int optname)
{
int optval = -1;
socklen_t optlen = sizeof(optval);
if (getsockopt(sockfd, level, optname, (char*)&optval, &optlen) != 0) {
perror("getsockopt failed!");
}
printf("%s: %d\n", name, optval);
}
void print_sock_keepalive(SOCKET sockfd)
{
print_sock_opt("keepalive", sockfd, SOL_SOCKET, SO_KEEPALIVE);
print_sock_opt("keepidle", sockfd, SOL_TCP, TCP_KEEPIDLE);
print_sock_opt("keepintvl", sockfd, SOL_TCP, TCP_KEEPINTVL);
print_sock_opt("keepcnt", sockfd, SOL_TCP, TCP_KEEPCNT);
puts("");
}
void set_sock_opt(SOCKET sockfd, int level, int optname, int optval)
{
if (setsockopt(sockfd, level, optname, (const char*)&optval, sizeof(optval)) != 0) {
perror("setsockopt failed!");
}
}
void set_sock_keepalive(SOCKET sockfd, int keepalive, int idle, int intvl, int cnt)
{
set_sock_opt(sockfd, SOL_SOCKET, SO_KEEPALIVE, keepalive);
set_sock_opt(sockfd, SOL_TCP, TCP_KEEPIDLE, idle);
set_sock_opt(sockfd, SOL_TCP, TCP_KEEPINTVL, intvl);
set_sock_opt(sockfd, SOL_TCP, TCP_KEEPCNT, cnt);
}
int main(int argc, char** argv)
{
#ifdef _WIN32
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif
SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = inet_addr(IP);
if (connect(sockfd, (const struct sockaddr*)&addr, sizeof(addr)) != 0) {
perror("connect failed!");
return -1;
}
print_sock_keepalive(sockfd);
set_sock_keepalive(sockfd, 1, 10, 15, 8);
print_sock_keepalive(sockfd);
while (1) {
char buf[1024] = {};
int n = recv(sockfd, buf, sizeof(buf), 0);
if (n <= 0) {
break;
}
buf[n] = '\0';
printf("receive: %s\n", buf);
send(sockfd, buf, n, 0);
}
return 0;
}
2. 服务器
在 Linux 上安装软件 socat
,并运行下面的命令来监听端口9876:
socat tcp-listen:9876,reuseaddr -
服务器的 IP 地址为:192.168.104.106。
3. Linux 下 keepalive 的实现
Linux 的 IP 地址为:192.168.102.63
Linux 的内核为:4.15.0-147-generic
首先安装 tcpdump 并运行下面的命令来抓包:
sudo tcpdump -pni enp0s31f6 -vvv -S "tcp port 9876"
其中
enp0s31f6
是网卡的名字,9876
为端口号。
然后运行测试代码编译出来的程序,tcpdump 输出(删除了一些不必要的信息):
14:21:17.008786 192.168.102.63.34882 > 192.168.104.106.9876: Flags [S], seq 3365757301, length 0
14:21:17.009112 192.168.104.106.9876 > 192.168.102.63.34882: Flags [S.], seq 3674364464, ack 3365757302, length 0
14:21:17.009152 192.168.102.63.34882 > 192.168.104.106.9876: Flags [.], seq 3365757302, ack 3674364465, length 0
14:21:27.142639 192.168.102.63.34882 > 192.168.104.106.9876: Flags [.], seq 3365757301, ack 3674364465, length 0
14:21:27.142904 192.168.104.106.9876 > 192.168.102.63.34882: Flags [.], seq 3674364465, ack 3365757302, length 0
14:21:37.382671 192.168.102.63.34882 > 192.168.104.106.9876: Flags [.], seq 3365757301, ack 3674364465, length 0
14:21:37.383214 192.168.104.106.9876 > 192.168.102.63.34882: Flags [.], seq 3674364465, ack 3365757302, length 0
14:21:47.622639 192.168.102.63.34882 > 192.168.104.106.9876: Flags [.], seq 3365757301, ack 3674364465, length 0
14:21:47.623224 192.168.104.106.9876 > 192.168.102.63.34882: Flags [.], seq 3674364465, ack 3365757302, length 0
14:21:50.787206 192.168.102.63.34882 > 192.168.104.106.9876: Flags [F.], seq 3365757302, ack 3674364465, length 0
14:21:50.791349 192.168.104.106.9876 > 192.168.102.63.34882: Flags [.], seq 3674364465, ack 3365757303, length 0
14:21:51.288252 192.168.104.106.9876 > 192.168.102.63.34882: Flags [F.], seq 3674364465, ack 3365757303, length 0
14:21:51.288293 192.168.102.63.34882 > 192.168.104.106.9876: Flags [.], seq 3365757303, ack 3674364466, length 0
从输出中可以看出:
- 在 TCP 三次握手成功后的 10 秒后,开始发送 keepalive 包。
- Linux 发出的 keepalive 包中,Seq 的值确实是 Next-1 ,且 keepalive 包中并未包含有一个字节的垃圾数据。
4. Windows
Windows 的 IP 地址为:192.168.104.134
Windows 的版本为:Version 21H2(OS Build 22000.51)
首先安装 wireshark 并使用下面的条件来嗅探网络包:
tcp.port == 9876
然后使用 VS2017 编译并运行测试代码, wireshark 捕获到:
10.902771 192.168.104.134 192.168.104.106 2389 → 9876 [SYN] Seq=0 Len=0
10.903375 192.168.104.106 192.168.104.134 9876 → 2389 [SYN, ACK] Seq=0 Ack=1 Len=0
10.903458 192.168.104.134 192.168.104.106 2389 → 9876 [ACK] Seq=1 Ack=1 Len=0
20.906729 192.168.104.134 192.168.104.106 2389 → 9876 [ACK] Seq=0 Ack=1 Len=1
20.907444 192.168.104.106 192.168.104.134 9876 → 2389 [ACK] Seq=1 Ack=1 Len=0
30.907197 192.168.104.134 192.168.104.106 2389 → 9876 [ACK] Seq=0 Ack=1 Len=1
30.907740 192.168.104.106 192.168.104.134 9876 → 2389 [ACK] Seq=1 Ack=1 Len=0
40.914386 192.168.104.134 192.168.104.106 2389 → 9876 [ACK] Seq=0 Ack=1 Len=1
40.915088 192.168.104.106 192.168.104.134 9876 → 2389 [ACK] Seq=1 Ack=1 Len=0
42.105023 192.168.104.134 192.168.104.106 2389 → 9876 [RST, ACK] Seq=1 Ack=1 Len=0
从输出中可以看出:
- 在 TCP 三次握手成功后的 10 秒后,开始发送 keepalive 包。
- Windows 发出的 keepalive 包中,Seq 的值确实是 Next-1 ,但 keepalive 包中包含有一个字节的垃圾数据。