首页 > 代码库 > Linux下关于TCP的keep alive的实现源码分析

Linux下关于TCP的keep alive的实现源码分析

 TCP下的Keep Alive

我们常说的TCP的keep alive,就是为了保证连接的有效性,在间隔一定的时间发探测包,根据回复来确认该连接是否有效。通常上层应用会自己提供心跳检测机制,而Linux内核本身也提供了从内核层面的确保连接有效性的方式。

在sock 函数中可以设置是否需要打开keep alive开关,默认建立socket 是关闭keep alive的。代码如下

  optval = 1;
   optlen = sizeof(optval);
   if(setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) {
      perror("setsockopt()");
      close(s);
      exit(EXIT_FAILURE);
   }

Keep Alive 的控制参数

tcp_keepalive_time 参数

控制keep alive的最长空闲时间

tcp_keepalive_probes 参数

当超过最长空间时间后,内核会尝试发出探测包确认客户端时候存活,该参数控制的是尝试的次数

tcp_keepalive_intvl 参数

当超过最长空闲时间后,内核会发出探测包,当没有收到确认回复的,该参数控制下个探测包的时间

Linux下如何实现Keep Alive

sock结构体中的timer_list

在sock结构体中,存在timer_list的结构体sk_timer,参考下面结构

struct sock{
...
struct timer_list	sk_timer;
...
}
struct timer_list {
	struct list_head entry;
	unsigned long expires;

	void (*function)(unsigned long);
	unsigned long data;

	struct tvec_base *base;
#ifdef CONFIG_TIMER_STATS
	void *start_site;
	char start_comm[16];
	int start_pid;
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};
timer_list结构体,是在sock里常用的timer执行链表,entry代表的是链表的头, expires代表的失效时间,而function就是执行的函数。

注册keepalive处理函数

void inet_csk_init_xmit_timers(struct sock *sk,
			       void (*retransmit_handler)(unsigned long),
			       void (*delack_handler)(unsigned long),
			       void (*keepalive_handler)(unsigned long))
{
	struct inet_connection_sock *icsk = inet_csk(sk);

	setup_timer(&icsk->icsk_retransmit_timer, retransmit_handler,
			(unsigned long)sk);
	setup_timer(&icsk->icsk_delack_timer, delack_handler,
			(unsigned long)sk);
	setup_timer(&sk->sk_timer, keepalive_handler, (unsigned long)sk);//注册了函数在sk_timer中
	icsk->icsk_pending = icsk->icsk_ack.pending = 0;
}

当连接完成的时候(也就是握手成功的时候),在新生成的sock里面的sk_timer结构体中,注册了函数keepalive_handler函数

void tcp_init_xmit_timers(struct sock *sk)
{
	inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,
				  &tcp_keepalive_timer);
}

而keepalive_handler函数就是tcp_keepalive_timer函数

static void tcp_keepalive_timer (unsigned long data)
{
	......

	if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
		goto out;

	elapsed = keepalive_time_when(tp);

	/* It is alive without keepalive 8) */
	if (tp->packets_out || tcp_send_head(sk))
		goto resched;

	elapsed = tcp_time_stamp - tp->rcv_tstamp;

	if (elapsed >= keepalive_time_when(tp)) {
		if (icsk->icsk_probes_out >= keepalive_probes(tp)) {
			tcp_send_active_reset(sk, GFP_ATOMIC);
			tcp_write_err(sk);
			goto out;
		}
		if (tcp_write_wakeup(sk) <= 0) {
			icsk->icsk_probes_out++;
			elapsed = keepalive_intvl_when(tp);
		} else {
			/* If keepalive was lost due to local congestion,
			 * try harder.
			 */
			elapsed = TCP_RESOURCE_PROBE_INTERVAL;
		}
	} else {
		/* It is tp->rcv_tstamp + keepalive_time_when(tp) */
		elapsed = keepalive_time_when(tp) - elapsed;
	}
         .....
}

上面只是截取了一部分代码,重点是前面提到的参数的实现,代码首先先检查了是否在sock里设置了参数SO_KEEPALIVE,也就是sock里面的flag:SOCK_KEEPOPEN。

如果设置了socket的SO_KEEPALIVE,才继续检查时间戳,取的上次收到包的时间戳和当前时间戳的差值,进行和参数keepalive_time的比较,如果已经超时了,那么检查发已经出探测包失败的次数,如果次数已经比keepalive_probes的大,那么发出reset包,同时写错误报告,关闭sock。

 如果比设置的探测包次数小的话,那发出探测包,同时设置下次的校验的时间戳为keepalive_intvl, 而不在是keepalive_time。

注意:在这里keepalive_intvl只是控制触发下次校验的时间

计算结束无效连接的时间N会有两种情况

a.  keepalive_intvl 的时间比 keepalive_time 大

N=keepalive_time +keepalive_intvl*keepalive_probes

b. keepalive_intvl 的时间比 keepalive_time小

N=keepalive_time +keepalive_time*keepalive_probes

这也就是为什么在默认设置里,认为无效的连接的时间实际上是7200*6 要12小时才会断掉连接

代码中设置keepalive_time,keepalive_probes,keepalive_intvl

                     setsockopt(s, SOL_TCP, TCP_KEEPIDLE, &val, sizeof(int))
                     setsockopt(s, SOL_TCP, TCP_KEEPINTVL, &val, sizeof(int))
                     setsockopt(s, SOL_TCP, TCP_KEEPCNT, &val, sizeof(int))
        

所对应的3个参数 TCP_KEEPIDLE  --> keepalive_time, TCP_KEEPINTVL--> keepalive_intvl, TCP_KEEPCNT--> keepalive_probes
3个参数本身也有最大值的保护,TCP_KEEPIDLE 最大是32767 TCP_KEEPINTVL 最大值是32767 TCP_KEEPCNT 最大值是127

JAVA中并没有提供对这些参数的修改

处理中的定时器

而TCP连接过程中,会有很多的定时器timer,做一些定时的检查,比如前面的博客里提到的清除accept queue的定时器,ack的定时器,有兴趣的可以参考tcp_timer.c
定时器的主要作用就是在固定时的状态下进行程序调用,在keep alive中就是定时发送探测包以确定包的有效性。

JAVA设置Keep alive


Java 里只允许打开keep alive,但却不允许设置keep alive的几个相关参数,Java 对于客户端中打开keep alive直接调用Socket.setKeepAlive函数,而在服务器端的ServerSocket 却不允许设置keep alive的开关,你只能在accept 一个新的连接的socket 的时候设置。

如何能设置刚才的需要的参数在java中?这里只是提一个思路

在java里的最后JNI的调用中,设置了常量数组的保护,只有数组中的参数才被允许设置到socket中
const opts[] = {
        { java_net_SocketOptions_TCP_NODELAY,           IPPROTO_TCP,    TCP_NODELAY },
        { java_net_SocketOptions_SO_OOBINLINE,          SOL_SOCKET,     SO_OOBINLINE },
        { java_net_SocketOptions_SO_LINGER,             SOL_SOCKET,     SO_LINGER },
        { java_net_SocketOptions_SO_SNDBUF,             SOL_SOCKET,     SO_SNDBUF },
        { java_net_SocketOptions_SO_RCVBUF,             SOL_SOCKET,     SO_RCVBUF },
        { java_net_SocketOptions_SO_KEEPALIVE,          SOL_SOCKET,     SO_KEEPALIVE },
        { java_net_SocketOptions_SO_REUSEADDR,          SOL_SOCKET,     SO_REUSEADDR },
        { java_net_SocketOptions_SO_BROADCAST,          SOL_SOCKET,     SO_BROADCAST },
        { java_net_SocketOptions_IP_TOS,                IPPROTO_IP,     IP_TOS },
        { java_net_SocketOptions_IP_MULTICAST_IF,       IPPROTO_IP,     IP_MULTICAST_IF },
        { java_net_SocketOptions_IP_MULTICAST_IF2,      IPPROTO_IP,     IP_MULTICAST_IF },
        { java_net_SocketOptions_IP_MULTICAST_LOOP,     IPPROTO_IP,     IP_MULTICAST_LOOP },
    };

直接设置是没有办法了,但可以用自己写JNI的方法来添加,因为在socket里有FileDescriptor fd,而里面的int fd 就是对应到内核中socket的 fd, 只要拿到这个fd 就可以调用自己的native方法来设置需要的参数,当然你也可以写自己的 socket, 来封装一个写setsocketoption的native 函数。


Linux下关于TCP的keep alive的实现源码分析