HTTP有Keepalive功能,TCP也有Keepalive功能,虽然都叫Keepalive,但是它们的目的却是不一样的。
为了说明这一点,先来看下长连接和短连接的定义。
连接的“长短”是什么?
短连接:建立一条连接,传输一个请求,马上关闭连接。
长连接:建立一条连接,传输一个请求,过会儿,又传输若干个请求,最后再关闭连接。
长连接的好处是显而易见的,多个请求可以复用一条连接,省去连接建立和释放的时间开销和系统调用,
但也意味着服务器的一部分资源会被长时间占用着。
HTTP的Keepalive,顾名思义,目的在于延长连接的时间,以便在同一条连接中传输多个HTTP请求。
HTTP服务器一般会提供Keepalive Timeout参数,用来决定连接保持多久,什么时候关闭连接。
当连接使用了Keepalive功能时,对于客户端发送过来的一个请求,服务器端会发送一个响应,然后开始计时,
如果经过Timeout时间后,客户端没有再发送请求过来,服务器端就把连接关了,不再保持连接了。
TCP的Keepalive,是挂羊头卖狗肉的,目的在于看看对方有没有发生异常,如果有异常就及时关闭连接。
当传输双方不主动关闭连接时,就算双方没有交换任何数据,连接也是一直有效的。
如果这个时候对端、中间网络出现异常而导致连接不可用,本端如何得知这一信息呢?
答案就是保活定时器。它每隔一段时间会超时,超时后会检查连接是否空闲太久了,如果空闲的时间超过
了设置时间,就会发送探测报文。然后通过对端是否响应、响应是否符合预期,来判断对端是否正常,
如果不正常,就主动关闭连接,而不用等待HTTP层的关闭了。
当服务器发送探测报文时,客户端可能处于4种不同的情况:仍然正常运行、已经崩溃、已经崩溃并重启了、
由于中间链路问题不可达。在不同的情况下,服务器会得到不一样的反馈。
(1) 客户主机依然正常运行,并且从服务器端可达
客户端的TCP响应正常,从而服务器端知道对方是正常的。保活定时器会在两小时以后继续触发。
(2) 客户主机已经崩溃,并且关闭或者正在重新启动
客户端的TCP没有响应,服务器没有收到对探测包的响应,此后每隔75s发送探测报文,一共发送9次。
socket函数会返回-1,errno设置为ETIMEDOUT,表示连接超时。
(3) 客户主机已经崩溃,并且重新启动了
客户端的TCP发送RST,服务器端收到后关闭此连接。
socket函数会返回-1,errno设置为ECONNRESET,表示连接被对端复位了。
(4) 客户主机依然正常运行,但是从服务器不可达
双方的反应和第二种是一样的,因为服务器不能区分对端异常与中间链路异常。
socket函数会返回-1,errno设置为EHOSTUNREACH,表示对端不可达。
选项
内核默认并不使用TCP Keepalive功能,除非用户设置了SO_KEEPALIVE选项。
有两种方式可以自行调整保活定时器的参数:一种是修改TCP参数,一种是使用TCP层选项。
(1) TCP参数
tcp_keepalive_time
最后一次数据交换到TCP发送第一个保活探测报文的时间,即允许连接空闲的时间,默认为7200s。
tcp_keepalive_intvl
保活探测报文的重传时间,默认为75s。
tcp_keepalive_probes
保活探测报文的发送次数,默认为9次。
Q:一次完整的保活探测需要花费多长时间?
A:tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes,默认值为7875s。
如果觉得两个多小时太长了,可以自行调整上述参数。
(2) TCP层选项
TCP_KEEPIDLE:含义同tcp_keepalive_time。
TCP_KEEPINTVL:含义同tcp_keepalive_intvl。
TCP_KEEPCNT:含义同tcp_keepalive_probes。
Q:既然有了TCP参数可供调整,为什么还增加了上述的TCP层选项?
A:TCP参数是面向本机的所有TCP连接,一旦调整了,对所有的连接都有效。
而TCP层选项是面向一条连接的,一旦调整了,只对本条连接有效。
激活
在连接建立后,可以通过设置SO_KEEPALIVE选项,来激活保活定时器。
int keepalive = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
- int sock_setsockopt(struct socket *sock, int level, int optname, char __user *optval,
- unsigned int optlen)
- {
- ...
- case SO_KEEPALIVE:
- #ifdef CONFIG_INET
- if (sk->sk_protocol == IPPROTO_TCP && sk->sk_type == SOCK_STREAM)
- tcp_set_keepalive(sk, valbool);
- #endif
- sock_valbool_flag(sk, SOCK_KEEPOPEN, valbool);
- break;
- ...
- }
-
- static inline void sock_valbool_flag (struct sock *sk, int bit, int valbool)
- {
- if (valbool)
- sock_set_flag(sk, bit);
- else
- sock_reset_flag(sk, bit);
- }
- void tcp_set_keepalive(struct sock *sk, int val)
- {
-
-
-
-
- if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))
- return;
-
-
-
-
- if (val && !sock_flag(sk, SOCK_KEEPOPEN))
- inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk)));
- else if (!val)
-
- inet_csk_delete_keepalive_timer(sk);
- }
-
-
- static inline int keepalive_time_when(const struct tcp_sock *tp)
- {
- return tp->keepalive_time ? : sysctl_tcp_keepalive_time;
- }
-
- void inet_csk_reset_keepalive_timer (struc sock *sk, unsigned long len)
- {
- sk_reset_timer(sk, &sk->sk_timer, jiffies + len);
- }
可以使用TCP层选项来动态调整保活定时器的参数。
int keepidle = 600;
int keepintvl = 10;
int keepcnt = 6;
setsockopt(fd, SOL_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
setsockopt(fd, SOL_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
setsockopt(fd, SOL_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
- struct tcp_sock {
- ...
-
- u32 rcv_tstamp;
- ...
-
- unsigned int keepalive_time;
-
- unsigned int keepalive_intvl;
-
- u8 keepalive_probes;
- ...
- struct {
- ...
-
- __u32 lrcvtime;
- ...
- } icsk_ack;
- ...
- };
-
- #define TCP_KEEPIDLE 4
- #define TCP_KEEPINTVL 5
- #define TCP_KEEPCNT 6
-
- #define MAX_TCP_KEEPIDLE 32767
- #define MAX_TCP_KEEPINTVL 32767
- #define MAX_TCP_KEEPCNT 127
- static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,
- unsigned int optlen)
- {
- ...
- case TCP_KEEPIDLE:
- if (val < 1 || val > MAX_TCP_KEEPIDLE)
- err = -EINVAL;
- else {
- tp->keepalive_time = val * HZ;
-
-
-
-
- if (sock_flag(sk, SOCK_KEEPOPEN) &&
- !((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))) {
- u32 elapsed = keepalive_time_elapsed(tp);
-
- if (tp->keepalive_time > elapsed)
- elapsed = tp->keepalive_time - elapsed;
- else
- elapsed = 0;
- inet_csk_reset_keepalive_timer(sk, elapsed);
- }
- }
- break;
-
- case TCP_KEEPINTVL:
- if (val < 1 || val > MAX_TCP_KEEPINTVL)
- err = -EINVAL;
- else
- tp->keepalive_intvl = val * HZ;
- break;
-
- case TCP_KEEPCNT:
- if (val < 1 || val > MAX_TCP_KEEPCNT)
- err = -EINVAL;
- else
- tp->keepalive_probes = val;
- break;
- ...
- }
到目前为止,连接已经经历的空闲时间,即最后一次接收到报文至今的时间。
- static inline u32 keepalive_time_elapsed (const struct tcp_sock *tp)
- {
- const struct inet_connection_sock *icsk = &tp->inet_conn;
-
-
-
-
-
- return min_t(u32, tcp_time_stamp - icsk->icsk_ack.lrcvtime,
- tcp_time_stamp - tp->rcv_tstamp);
- }
超时处理函数
我们知道保活定时器、SYNACK重传定时器、FIN_WAIT2定时器是共用一个定时器实例sk->sk_timer,
所以它们的超时处理函数也是一样的,都为tcp_keepalive_timer()。
而在函数内部,可以根据此时连接所处的状态,来判断是哪个定时器触发了超时。
Q:什么时候判断对端为异常并关闭连接?
A:分两种情况。
1. 用户使用了TCP_USER_TIMEOUT选项。当连接的空闲时间超过了用户设置的时间,且有发送过探测报文。
2. 用户没有使用TCP_USER_TIMEOUT选项。当发送保活探测包的次数达到了保活探测的最大次数时。
- static void tcp_keepalive_timer (unsigned long data)
- {
- struct sock *sk = (struct sock *) data;
- struct inet_connection_sock *icsk = inet_csk(sk);
- struct tcp_sock *tp = tcp_sk(sk);
- u32 elapsed;
-
-
- bh_lock_sock(sk);
-
-
-
-
- if (sock_owned_by_user(sk)) {
-
- inet_csk_reset_keepalive_timer(sk, HZ/20);
- goto out;
- }
-
-
- if (sk->sk_state == TCP_LISTEN) {
- tcp_synack_timer(sk);
- goto out;
- }
-
-
- if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
- ...
- }
-
-
- if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
- goto out;
-
- elapsed = keepalive_time_when(tp);
-
-
-
-
-
-
-
-
-
-
- if (tp->packets_out || tcp_send_head(sk))
- goto resched;
-
-
- elapsed = keepalive_time_elapsed(tp);
-
-
- if (elapsed >= keepalive_time_when(tp)) {
-
-
-
-
-
- if (icsk->icsk_user_timeout != 0 && elapsed >= icsk->icsk_user_timeout &&
- icsk->icsk_probes_out > 0) || (icsk->icsk_user_timeout == 0 &&
- 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 {
-
- elapsd = TCP_RESOURCE_PROBE_INTERVAL;
- }
-
- } else {
-
- elapsed = keepalive_time_when(tp) - elapsed;
- }
-
- sk_mem_reclaim(sk);
-
- resched:
- inet_csk_reset_keepalive_timer(sk, elapsed);
- goto out;
-
- out:
- bh_unlock_sock(sk);
- sock_put(sk);
- }
Q:TCP是如何发送Keepalive探测报文的?
A:分两种情况。
1. 有新的数据段可供发送,且对端接收窗口还没被塞满。发送新的数据段,来作为探测包。
2. 没有新的数据段可供发送,或者对端的接收窗口满了。发送序号为snd_una - 1、长度为0的ACK包作为探测包。
-
-
- int tcp_write_wakeup (struct sock *sk)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- struct sk_buff *skb;
-
- if (sk->sk_state == TCP_CLOSE)
- return -1;
-
-
- if ((skb = tcp_send_head(sk)) != NULL && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) {
- int err;
- unsigned int mss = tcp_current_mss(sk);
-
- unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
-
-
- if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq))
- tp->pushed_seq = TCP_SKB_CB(skb)->end_seq;
-
-
- if (seg_size < TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq || skb->len > mss) {
- seg_size = min(seg_size, mss);
- TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
- if (tcp_fragment(sk, skb, seg_size, mss))
- return -1;
- } else if (! tcp_skb_pcount(skb))
- tcp_set_skb_tso_segs(sk, skb, mss);
-
- TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
- TCP_SKB_CB(skb)->when = tcp_time_stamp;
- err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);
- if (!err)
- tcp_event_new_data_sent(sk, skb);
-
- } else {
-
-
- if (between(tp->snd_up, tp->snd_una + 1, tp->snd_una + 0xFFFF))
- tcp_xmit_probe_skb(sk, 1);
-
-
-
-
-
- return tcp_xmit_probe_skb(sk, 0);
- }
- }
Q:当没有新的数据可以用作探测包、或者对端的接收窗口为0时,怎么办呢?
A:发送一个序号为snd_una - 1、长度为0的ACK包,对端收到此包后会发送一个ACK响应。
如此一来本端就能够知道对端是否还活着、接收窗口是否打开了。
-
-
-
-
-
-
-
-
-
-
-
-
- static int tcp_xmit_probe_skb (struct sock *sk, int urgent)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- struct sk_buff *skb;
-
-
- skb = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));
- if (skb == NULL)
- return -1;
-
-
- skb_reserve(skb, MAX_TCP_HEADER);
-
-
-
-
-
- tcp_init_nondata_skb(skb, tp->snd_una - !urgent, TCPHDR_ACK);
- TCP_SKB_CB(skb)->when = tcp_time_stamp;
- return tcp_transmit_skb(sk, skb, 0, GFP_ATOMIC);
- }
发送RST包。
-
-
-
-
-
- void tcp_send_active_reset (struct sock *sk, gfp_t priority)
- {
- struct sk_buff *skb;
-
- skb = alloc_skb(MAX_TCP_HEADER, priority);
- if (!skb) {
- NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTFAILED);
- return;
- }
-
-
- skb_reserve(skb, MAX_TCP_HEADER);
-
- tcp_init_nondata_skb(skb, tcp_acceptable_seq(sk), TCPHDR_ACK | TCPHDR_RST);
-
-
- TCP_SKB_CB(skb)->when = tcp_time_stamp;
- if (tcp_transmit_skb(sk, skb, 0, priority))
- NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTFAILED);
- TCP_INC_STATS(sock_net(sk), TCP_MIB_OUTRSTS);
- }
-
- static inline __u32 tcp_acceptable_seq (const struct sock *sk)
- {
- const struct tcp_sock *tp = tcp_sk(sk);
-
-
- if (! before(tcp_wnd_end(tp), tp->snd_nxt))
- return tp->snd_nxt;
- else
- return tcp_wnd_end(tp);
- }
TCP_USER_TIMEOUT选项
从上文可知同时符合以下条件时,保活定时器才会发送探测报文:
1. 网络中没有发送且未确认的数据包。
2. 发送队列为空。
3. 连接的空闲时间超过了设定的时间。
Q:如果网络中有发送且未确认的数据包、或者发送队列不为空时,保活定时器不起作用了,
岂不是不能够检测到对端的异常了?
A:可以使用TCP_USER_TIMEOUT,显式的指定当发送数据多久后还没有得到响应,就判定连接超时,
从而主动关闭连接。
TCP_USER_TIMEOUT选项会影响到超时重传定时器和保活定时器。
(1) 超时重传定时器
判断连接是否超时,分3种情况:
1. SYN包:当SYN包的重传次数达到上限时,判定连接超时。(默认允许重传5次,初始超时时间为1s,总共历时31s)
2. 非SYN包,用户使用TCP_USER_TIMEOUT:当数据包发出去后的等待时间超过用户设置的时间时,判定连接超时。
3. 非SYN包,用户没有使用TCP_USER_TIMEOUT:当数据包发出去后的等待时间超过以TCP_RTO_MIN为初始超时
时间,重传boundary次所花费的时间后,判定连接超时。(boundary的最大值为tcp_retries2,默认值为15)
(2) 保活定时器
判断连接是否异常,分2种情况:
1. 用户使用了TCP_USER_TIMEOUT选项。当连接的空闲时间超过了用户设置的时间,且有发送过探测报文。
2. 用户没有使用TCP_USER_TIMEOUT选项。当发送保活探测包的次数达到了保活探测的最大次数时。