原文发表在:
setsockopt, SO_KEEPALIVE and Heartbeats心跳包主要有两个作用,对于后台应用,心跳包可以用于监控客户端状态,当客户端断连后能及时释放链接和对应的系统,业务资源;对于客户端,心跳则是用于防止链接资源被中间节点(比如NAT)释放,从而达成链接保活的目的。
本文将讲解如何用setsockopt()
配置socket 选项,SO_KEEPALIVE
, TCP_KEEPIDLE
, TCP_KEEPINTVL
and TCP_KEEPCNT
来发送心跳包;并且讨论使用心跳包来进行链接保活的通用原则。
实验环境:
OS: Unbutu 16.04
gcc: 5.4.0
链接保活
在很多情况下断连是无法察觉的,比如NAT记录超时。一个NAT记录包含了4元组信息(源ip,源端口,目标ip,目标端口)。由于内存限制,路由设备会在超时后随时删除一些被用于地址转换的记录。于是即使任何一侧都没有发送FIN或者RST包,失去NAT记录的的链接就实际上被切断了。
重连是有代价的。首先是握手的(3XRTT)时候,用户必须傻等;然后重连后无缝恢复体验也需要增加额外的开发量。
为了避免额外的握手和其引入的RTT,HTTP增加了KEEP-ALIVE机制,所以多个HTTP短会话能够复用同一个TCP长链接。但这是另外一个故事了。
接下来我们来用两组程序来说明心跳保活的详细机制,先来看一个服务端的代码,
<script src="gist.github.com/holmeshe/4e…"></script>
简单起见,这里没有使用多路服用,所以服务端一次只能接受一个客户端的链接。
客户端,
<script src="gist.github.com/holmeshe/a8…"></script>
当设置了上述的socket选项,客户端用connetc()
完成三次握手,然后直接调用sleep()
放弃CPU。
如果对网络编程不太熟,可以先看这篇文章。
然后,我们来看看网络交互,
sudo tcpdump -i wlp3s0 dst net 192.168.1.71 or src net 192.168.1.71 and not dst port 22 and not src port 22
// ========================> start handshakes
12:21:42.437163 IP 192.168.1.66.43066 > 192.168.1.71.6666: Flags [S], seq 3002564942, win 29200, options [mss 1460,sackOK,TS val 7961984 ecr 0,nop,wscale 7], length 0
12:21:42.439960 IP 192.168.1.71.6666 > 192.168.1.66.43066: Flags [S.], seq 3450454053, ack 3002564943, win 28960, options [mss 1460,sackOK,TS val 2221927 ecr 7961984,nop,wscale 7], length 0
12:21:42.440088 IP 192.168.1.66.43066 > 192.168.1.71.6666: Flags [.], ack 1, win 229, options [nop,nop,TS val 7961985 ecr 2221927], length 0
// ========================> end handshakes
12:21:52.452057 IP 192.168.1.66.43066 > 192.168.1.71.6666: Flags [.], ack 1, win 229, options [nop,nop,TS val 7964488 ecr 2221927], length 0
12:21:52.454443 IP 192.168.1.71.6666 > 192.168.1.66.43066: Flags [.], ack 1, win 227, options [nop,nop,TS val 2224431 ecr 7961985], length 0
12:22:02.468056 IP 192.168.1.66.43066 > 192.168.1.71.6666: Flags [.], ack 1, win 229, options [nop,nop,TS val 7966992 ecr 2224431], length 0
12:22:02.470458 IP 192.168.1.71.6666 > 192.168.1.66.43066: Flags [.], ack 1, win 227, options [nop,nop,TS val 2226935 ecr 7961985], length 0
12:22:12.484119 IP 192.168.1.66.43066 > 192.168.1.71.6666: Flags [.], ack 1, win 229, options [nop,nop,TS val 7969496 ecr 2226935], length 0
12:22:12.489786 IP 192.168.1.71.6666 > 192.168.1.66.43066: Flags [.], ack 1, win 227, options [nop,nop,TS val 2229440 ecr 7961985], length 0
这里我去掉了无关的ARP输出,如果你对tcpdump不熟,可以先看看这篇文章。
有了基本的上手经验后,现在开始解释心跳保活机制。
1)SO_KEEPALIVE
用于开启(或者关闭)心跳;
int flags =1;
if (setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags))) { perror("ERROR: setsocketopt(), SO_KEEPALIVE"); exit(0); };
2)开启心跳的一端(这里是客户端)发送ACK包用于心跳(👁 Flags [.]
);
3)收到心跳包后,另一(服务)端回复ACK(👁 Flags [.]);
4)TCP_KEEPIDLE
定义了心跳包的间隔(👁 时间戳)。
flags = 10;
if (setsockopt(sfd, SOL_TCP, TCP_KEEPIDLE, (void *)&flags, sizeof(flags))) { perror("ERROR: setsocketopt(), SO_KEEPIDLE"); exit(0); };
这里要注意,整个过程 服务端的 accept()
一直阻塞, 说明心跳包对接受者是透明的。
未完待续。。。