TCP keepalive在很多场景下都不是必须的,但是在某些特定的场景下,这个特性却会是非常有用。从TCP keepalive(后面称为keepalive)这个名字就可以大概理解这个特性的作用:keep tcp alive。我们可以通过检查我们的socket(TCP sockets)来判断网络连接是不是正常的(running or broken)。

1. 什么是TCP Keepalive

keepalive的概念非常简单:当你建立一个TCP连接的时候,便有一组定时器与之绑定(associate)在一起。其中的一些定时器就用于处理keepalive过程。当keepalive定时器到0的时候,就会给对端发送一个不包含数据部分的keepalive探测包(probe packet),然后打开ACK标志。可以这样做(发送一个不包含数据部分的数据包)是因为TCP/IP规格里面有重复确认(duplicate ACK)机制,而且由于TCP是基于流的协议,所以对端也是没有任何参数的。另一方面,会收到对端(对端可以不支持keepalive选项,只要是TCP/IP就行)的确认消息,该消息也没有数据部分,只有ACK。

如果收到了keepalive探测包的回复消息,那么我们就可以断定连接依然是OK的,也不用担心用户层的实现。实际上,TCP允许我们处理没有数据包的流,而且数据部分长度为0的数据包对于用户程序来说也是没有什么危险的。这个过程是非常有用的,如果对端已经失去连接(比如机器重启了)我们就可以知道。如果没有收到对端keepalive探测包的回复消息,便可以断定连接已经不可用,进而采取一些措施。Keepalive除了会额外产生一些网络数据包外(这些包将加大网络流量,对路由器和防火墙造成一定的负担),没有什么坏处。

2. TCP Keepalive使用场景

下面介绍两种使用keepalive机制可以解决的网络异常场景。

2.1 检测对端的死连接

这里所谓的对端连接已经挂掉的场景有两种:(1)对端还没有来得及通知我们就已经死掉了。比如系统内核突然挂掉,或是进程被直接终止等。(2)对端进程虽然是正常的,但是网络链路却出了故障。这种场景下,如果网络链路不恢复正常的话,对我们来说,对端依旧是“挂掉”的。在这两种场景下,对端在挂掉之前都是无法通知我们的,而且一般的TCP操作是检测不出来连接状态的。

我们假设一种A和B的连接场景,如图1:A和B已经通过三步握手(A给B发送一个SYN,B给A回复了一个SYN/ACK,A给B回了一个ACK)建立了连接,此时认为连接已经稳定,我们可以在这条链路上面发送数据包了。但突然发生了一个意外:B端机器突然断电了,而B还没有来得及通知A连接出问题了。而再看此时的A端,A已经准备好接收B端发来的数据,却根本不知道B端已经crash了。这时我们再恢复B端的电源等待系统重启。此时的状态就是A和B都正常运行,并且A知道它和B之间有一条已经建立好的连接,但是B却不知道。这个时候,如果A试图通过这条连接向B发送数据,B将回复一个RST数据包(在一个已关闭的socket上收到数据时,将发送RST数据包,要求对端关闭异常连接且对端不需要回复ACK),这样将导致A最终关闭这个连接。至此,这个死连接才算清理掉。

1

Keepalive可以帮助我们判断出对端变得不可达(unreachable),并且不会误报。实际上,如果是因为两端的网络导致的问题,keepalive会过一些时间再重试一下,多次尝试之后才会将这个连接标记为不可用。

2.2 防止因为网络不活动而断连

Keepalive的另外一个目标就是防止因为网络不活动而断开网络连接。这是一个很常见的问题:当我们使用NAT代理或者防火墙的时候,经常会出现这种问题。这是由代理和防火墙内部的实现导致的:代理和防火墙一般会记录所有通过他们的连接。但由于机器的物理资源限制,它们只能在内存中保存有限数量的连接。最常见的策略就是保持最新的连接,丢掉老的和不活动的连接。

继续以刚才的场景为例,如图2,A端和B端已经通过三步握手建立了稳定的连接。但是过了很长一段时间,A和B才有事件发生,需要向对端发送数据。此时,A和B的连接是有效的(建立连接后,如果不断开,则该连接一直保持),但是代理或者防火墙却并不知道(该连接已经在他们的内存中被新的连接淘汰掉了)。当我们发出数据后,代理将不能正确处理我们的数据,最终导致连接断开。

2

3. Linux中的TCP Keepalive

3.1 与TCP Keepalive相关的三个参数

Linux中已经内建支持Keepalive,我们需要使能TCP/IP网络来使用它。并且需要使用procfs和sysctl来在内核运行过程中配置相关的几个参数。同时,也可以使用三个用户可配置的参数在程序中使用Keepalive。

  • tcp_keepalive_time :这个参数是最后一次包含数据(data)的数据包(data packet)发送(这里我们不认为简单的ACKs是数据)到第一个keepalive探测包发送的时间间隔。当一个连接被标记为需要keepalive的时候,这个值就不再使用了。
  • tcp_keepalive_intvl :这个参数是连续keepalive探测包发送的时间间隔,不管在这个期间这个连接进行过什么数据交换。
  • tcp_keepalive_probes :这个参数是keepalive在认为一个连接是死连接并且通知应用层之前尝试发送(未答复的)keepalive探测包的次数。

虽然Linux内核里面提供了对keepalive的支持,但是一般默认是未开启的。所以写程序的时候我们需要使用setsockopt接口来打开keepalive。如果在某次keepalive探测包发了之后收到了对端的回复,则说明对端是正常的。定时器重置。过一段时间后再进行检测。

3.2 查看与设置系统中三个参数的值

查看系统中的默认值:

allan@ubuntu:workspace$ cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
allan@ubuntu:workspace$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl 
75
allan@ubuntu:workspace$ cat /proc/sys/net/ipv4/tcp_keepalive_probes 
9

改变系统的默认值:

root@ubuntu:/home/allan/workspace# echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time
root@ubuntu:/home/allan/workspace# echo 60 > /proc/sys/net/ipv4/tcp_keepalive_intvl
root@ubuntu:/home/allan/workspace# echo 20 > /proc/sys/net/ipv4/tcp_keepalive_probes

我们可以在任何时候更改这几个参数的值,因为keepalive会在每次用到这几个参数的时候重新读取他们的值。所以,如果在连接还正常使用的时候修改这几个参数的值,内核便会在下一次使用这个新值。但一般我们会选择在三个地方去设置这几个参数的值:(1)第一次配置网络的时候(2)rc.local脚本里面。一般的发行版里面都包含该脚本,一般认为该脚本执行后用户的设置就算完成了。(3)/etc/sysctl.conf配置文件里面。sysctl工具就是读取和设置该配置文件。

并不是所有的网络应用都需要keepalive的支持,只有TCP支持keepalive,所以我们也只能在TCP套接字中使用keepalive。

4. 在C网络编程中使用Keepalive

4.1 setsockopt函数

函数原型:

#include <sys/socket.h>

int setsockopt(int socket, int level, int option_name, 
      const void *option_value, socklen_t option_len);

我们在需要使能Keepalive的socket上面调用setsockopt函数便可以打开该socket上面的keepalive。第一个参数是要设置的套接字;第二个参数是SOL_SOCKET;第三个参数必须是SO_KEEPALIVE;第四个参数必须是一个布尔整型值,0表示关闭,1表示打开;最后一个参数是第四个参数值的大小。

还有其他三个socket选项可供我们在程序中设置keepalive,它们都是在SOL_TCP层使用,而不是SOL_SOCKET,而且它们只在当前socket中覆盖系统的变量值。如果在设置之前就读的话,将返回系统预设值的值。这三个选项如下:

  • TCP_KEEPCNT : 在当前socket中覆盖系统的tcp_keepalive_probes值
  • TCP_KEEPIDLE : 在当前socket中覆盖系统的tcp_keepalive_time值
  • TCP_KEEPINTVL : 在当前socket中覆盖系统的tcp_keepalive_intvl值

4.2 程序实例

下面程序实例先检测系统的Keepalive默认设置,然后再打开Keepalive:

/* --- begin of keepalive test program --- */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(void);

int main()
{
   int s;
   int optval;
   socklen_t optlen = sizeof(optval);

   /* Create the socket */
   if((s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
      perror("socket()");
      exit(EXIT_FAILURE);
   }

   /* Check the status for the keepalive option */
   if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
      perror("getsockopt()");
      close(s);
      exit(EXIT_FAILURE);
   }
   printf("SO_KEEPALIVE is %sn", (optval ? "ON" : "OFF"));

   /* Set the option active */
   optval = 1;
   optlen = sizeof(optval);
   if(setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) {
      perror("setsockopt()");
      close(s);
      exit(EXIT_FAILURE);
   }
   printf("SO_KEEPALIVE set on socketn");

   /* Check the status again */
   if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
      perror("getsockopt()");
      close(s);
      exit(EXIT_FAILURE);
   }
   printf("SO_KEEPALIVE is %sn", (optval ? "ON" : "OFF"));

   close(s);

   exit(EXIT_SUCCESS);
}
/* ---  end of keepalive test program  --- */

编译运行结果:

allan@ubuntu:workspace$ ./a.out 
SO_KEEPALIVE is OFF
SO_KEEPALIVE set on socket
SO_KEEPALIVE is ON

原文参见:《TCP Keepalive HOWTO