C++

TCP连接保活

"TCP连接保活"

Posted by Simon on September 14, 2020

“Better code, better life. ”

TCP连接保活

对于一个TCP长连接的保活又两种方式,一种在应用层发心跳包,一种是在传输层,通过SO_Keeplive实现。

保活的必要性

现在的网络编程大多使用长连接,对于一个TCP长连接,服务端为维持每一个客户端的连接都需要一定的资源,这就需要服务端每隔一段时间将不活跃的连接剔除调,这样就面临下面两问题:

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

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

传输层的保活

传输层的保活需要通过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. 额外的资源占用。保活探测报文需要占用额外的连接资源,这在极端情况下是不能接受的。
  2. 不能完全满足需求。协议层保活有很多限制,比如最小探测报文间隔为1秒;保活对于所有连接均等看待,但业务中常常需要分优先级。
  3. 越少异常处理机制。协议层保活发现对方断链后回直接断开连接,不能增加自定义异常处理,比如重连或者其他failover处理。