Rxsi Blog GameServer Developer

握手&挥手源码解析:第三次挥手

2022-09-17
Rxsi
TCP

第三次挥手

服务端的第三次挥手是在调用了 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 下发第三次挥手,则该报文就会和第二次挥手合并,最终形成 三次挥手。

延迟确认的具体的规则如下:

  1. 在定时器时间内,如果没有接收到新数据包或者有数据包要发出,那么会因为定时器到期,而下发 ACK 报文,定时器时间为40ms ~ 200ms,发出的 ACK 报文是 pure ack;
  2. 如果接收到了新的数据包,那么如果本身已经有延迟 ACK 未发出,则立即发送一个 ACK 报文,最终表现为两个数据包合并为一个 ACK,发出的 ACK 报文是 pure ack;
  3. 如果有新的数据包要发送,因为数据包本身带有 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 算法


Similar Posts

Comments