“Better code, better life. ”
TCP连接保活
对于一个TCP长连接的保活又两种方式,一种在应用层发心跳包,一种是在传输层,通过SO_Keeplive实现。
保活的必要性
现在的网络编程大多使用长连接,对于一个TCP长连接,服务端为维持每一个客户端的连接都需要一定的资源,这就需要服务端每隔一段时间将不活跃的连接剔除调,这样就面临下面两问题:
- 如何判断对方是否还在线。TCP对于非正常断开的连接系统并不能侦测到(比如网线断掉)。
- 长时间没有任何数据发送,连接可能会被中断。这是因为,网络连接中间可能会经过路由器、防火墙等设备,而这些有可能会对长时间没有活动的连接断掉。
基于上面两点考虑,需要保活机制。
传输层的保活
传输层的保活需要通过setsockopt
函数来完成,设置KEEP_ALIVE
选项。
和KEEP_ALIVE
相关的参数有三个:
tcp_keepalive_intvl (integer; default: 75; since Linux 2.4)
The number of seconds between TCP keep-alive probes.
tcp_keepalive_probes (integer; default: 9; since Linux 2.2)
The maximum number of TCP keep-alive probes to send before giving up and killing the connection if no
response is obtained from the other end.
tcp_keepalive_time (integer; default: 7200; since Linux 2.2)
The number of seconds a connection needs to be idle before TCP begins sending out keep-alive probes. Keep-
alives are sent only when the SO_KEEPALIVE socket option is enabled. The default value is 7200 seconds (2
hours). An idle connection is terminated after approximately an additional 11 minutes (9 probes an interval
of 75 seconds apart) when keep-alive is enabled.
再C语言中,需要通过setsockopt
系统调用来设置socket
连接选项,其函数原型为:
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
下面来看一段摘自redis
的代码
//节选自redis源码
int val = 1;
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val)) == -1) {
NetSetError(err_msg, "setsockopt SO_KEEPALIVE: %s", strerror(errno));
return NET_ERR;
}
val = interval;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val)) < 0) {
NetSetError(err_msg, "setsockopt TCP_KEEPIDLE: %s\n", strerror(errno));
return NET_ERR;
}
val = interval / 3;
if (val == 0)val = 1;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &val, sizeof(val)) < 0) {
NetSetError(err_msg, "setsockopt TCP_KEEPIDLE: %s\n", strerror(errno));
return NET_ERR;
}
val = 3;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &val, sizeof(val)) < 0) {
NetSetError(err_msg, "setsockopt TCP_KEEPCNT: %s\n", strerror(errno));
return NET_ERR;
}
我们从上到下依次解释。
int val = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val))
这两行等于打开了SO_KEEPALIVE
保活选项,此时保活数据包为系统间隔为系统默认
TCP_KEEPIDLE
选项可以设置当连接空闲N秒(没有数据传输)后发送保活数据包;
TCP_KEEPINTVL
选项可以设置如果上一个保活连接对方没有相应,间隔多少秒后再发,最小间隔为1秒
TCP_KEEPCNT
选项可以设置经过多少个未响应探测数据包后断开连接。
业务层的保活
所谓业务层保活就是在协议层之上,服务器和客户端自定义的一种保活协议。业务层保活可以有更灵活的设置,加入各种奇怪的业务逻辑。
为什么有了协议层保活,还需要再自己设计保活协议呢?
我认为主要有以下几点原因:
- 额外的资源占用。保活探测报文需要占用额外的连接资源,这在极端情况下是不能接受的。
- 不能完全满足需求。协议层保活有很多限制,比如最小探测报文间隔为1秒;保活对于所有连接均等看待,但业务中常常需要分优先级。
- 越少异常处理机制。协议层保活发现对方断链后回直接断开连接,不能增加自定义异常处理,比如重连或者其他failover处理。