第三次挥手
服务端的第三次挥手是在调用了 close/shutdown 函数之后发出的,这里的逻辑过程是和第一次挥手类似的,借助 tcp_close_state 函数使 TCP 的状态由CLOSE_WAIT
转变为LAST_ACK
状态,并发出FIN
报文。
第三次挥手丢失,会发生什么?
在服务端发出第三次挥手之后(特别注意一点,调用了 close 会直接清空未发送的数据包),即进入了 LAST_ACK 状态,而如果数据包未能被对端接收到,那么会进行重发。数据包的最大重发次数是tcp_orphan_retries(默认值是0,代码中会处理为8)
,而在达到最大重发次数之后,会自动转变为CLOSE
状态。
对于客户端来说,因为此时正处于FIN_WAIT_2
状态,此状态能够维持的最长时间由 TCP_LINGER2(如果有设置的话)或者 tcp_fin_timeout(默认值是60s)以及实际在传输过程中的 rto 决定。当通过 tcp_fin_time 函数计算得到的剩余时间小于TCP_TIMEWAIT_LEN(默认是60,因此要修改TIME_WAIT时长只能通过重新编译内核)
时会立即进入TIME_WAIT
状态,否则则等待相应的差值后再进入。
rxsi@VM-20-9-debian:~$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
和第二次挥手一起发出
TCP 存在有一种机制,称为延迟确认
,这个机制的实现是每次发送 ACK 报文时都先进行延后,注意这里不是生成一个 ACK 报文缓存起来,只是启动了一个定时器,毕竟当前需要返回哪个 ack 值,可以直接计算得知。
在这种机制之下,如果服务端在定时器超时之前调用了 close/shutdown 下发第三次挥手,则该报文就会和第二次挥手合并,最终形成 三次挥手。
延迟确认的具体的规则如下:
- 在定时器时间内,如果没有接收到新数据包或者有数据包要发出,那么会因为定时器到期,而下发 ACK 报文,定时器时间为
40ms ~ 200ms
,发出的 ACK 报文是 pure ack; - 如果接收到了新的数据包,那么如果本身已经有延迟 ACK 未发出,则立即发送一个 ACK 报文,最终表现为两个数据包合并为一个 ACK,发出的 ACK 报文是 pure ack;
- 如果有新的数据包要发送,因为数据包本身带有 ACK 标记,因此此时就顺带发送出去了,此时的 ACK 报文带有数据,非 pure ack;
在代码中的具体实现如下:
// 当有开启延迟ACK功能时,即会进入此逻辑
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
struct tcp_sock *tp = tcp_sk(sk);
unsigned long rtt, delay;
/* More than one full frame received... */
if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss && // 当前接收到的数据包已经超过了一个MSS大小,证明至少已经接收了两个报文
/* ... and right edge of window advances far enough.
* (tcp_recvmsg() will send ACK otherwise).
* If application uses SO_RCVLOWAT, we want send ack now if
* we have not received enough bytes to satisfy the condition.
*/
(tp->rcv_nxt - tp->copied_seq < sk->sk_rcvlowat ||
__tcp_select_window(sk) >= tp->rcv_wnd)) || // 发送窗口还有空间
/* We ACK each frame or... */
tcp_in_quickack_mode(sk) || // 此时正处于快速ack阶段,当TCP处于synsent、发送dupack、接收到窗口之外的数据段、收到ECN标志段就会进入快速ack阶段,最多只能连续发送8个ack后就要退出这个模式
/* Protocol state mandates a one-time immediate ACK */
inet_csk(sk)->icsk_ack.pending & ICSK_ACK_NOW) { // 打上了立即发送的标志,这个是当out_of_order_queue满时打上的,乱序队列满了说明存在丢包问题,当前的网络可能拥塞,因此需要立即返回ACK,以通知对端降低发送速率
send_now:
tcp_send_ack(sk); // 立即发送ACK报文
return;
}
if (!ofo_possible || RB_EMPTY_ROOT(&tp->out_of_order_queue)) { // 不存在乱序问题,或者乱序队列还没有满,那么发送延迟ACK即可
tcp_send_delayed_ack(sk);
return;
}
// .....
}
// 发送延迟ACK
void tcp_send_delayed_ack(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
int ato = icsk->icsk_ack.ato;
unsigned long timeout;
if (ato > TCP_DELACK_MIN) { // TCP_DELACK_MIN是40ms
// ...
ato = min_t(u32, ato, inet_csk(sk)->icsk_delack_max); // 最终结构范围就是40ms~200ms,TCP_DELACK_MAX是200ms
/* Stay within the limit we were given */
timeout = jiffies + ato;
/* Use new timeout only if there wasn't a older one earlier. */
if (icsk->icsk_ack.pending & ICSK_ACK_TIMER) {
/* If delack timer is about to expire, send ACK now. */
if (time_before_eq(icsk->icsk_ack.timeout, jiffies + (ato >> 2))) { // 定时器快要过期了,就不必要设置了,直接发送ACK
tcp_send_ack(sk);
return;
}
if (!time_before(timeout, icsk->icsk_ack.timeout))
timeout = icsk->icsk_ack.timeout;
}
icsk->icsk_ack.pending |= ICSK_ACK_SCHED | ICSK_ACK_TIMER;
icsk->icsk_ack.timeout = timeout;
sk_reset_timer(sk, &icsk->icsk_delack_timer, timeout); // 设置定时时间
}
要关闭延迟确认机制,只需要通过setsockopt设置TCP_QUICKACK
属性即可。
延迟确认机制的设计目的在于提升单个数据包的有效数据量,进而达到减少数据包的传输,提升网络性能的目的。同样的措施还有一个nagle
算法,该算法是通过缓存要发送的小数据包,将多个小数据包整合成一个大数据包发送的方式,减少数据包传输量。但是当 nagle 算法和延迟确认同时作用时,将会是通讯延迟增大。
nagle 算法的规则如下:
- 当发送端有未确认的数据包(即没有得到 ack)且本次要发送的数据包较小时,则等待后续数据包,直到累计大于
MSS
才发送。
以下面的模拟情景为例:
- A 发送给 B 一个数据包,大小刚好被切分为N(偶数)个数据包且最后一个数据包的容量小于 MSS,A 开启了 nagle 算法,B 开启了延迟确认机制;
- 由于前 N-2 个数据包的大小都是 MSS,因此会立即发送,而延迟确认机制在收到两个数据包时会立即下发 ACK 报文,所以这部分的数据包能够快速的下发并收到回复;
- 当 A 下发第 N-1 个包时,由于大小是
MSS
,因此也会立即下发,但是后续的第 N 个数据包由于容量过小,因此不会立即下发; - B 接收到了第 N-1 个数据包,进入延迟回复;
- 此时造成的现象就成了:A 等待 B 回复第 N-1 个数据包的 ack,好满足 nagle 算法的条件,进而下发第 N 个数据包;而 B 则是等待 A 下发第 N 个数据包,好满足延迟确认机制的条件。所以最终会在等待
200ms
之后 B 才回复 ACK 报文,A 才可以把最后一个数据包发送完,造成了非常大的延迟;
综上,我们应该关闭两种机制的其一,建议使用TCP_NODELAY
关闭 nagle 算法