Rxsi Blog GameServer Developer

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

2022-09-19
Rxsi
TCP

第四次挥手

此时的客户端正处于FIN_WAIT_2状态(有维持时间上限),在接收到了服务端的第三次挥手后,就会进入TIME_WAIT状态

int tcp_v4_rcv(struct sk_buff *skb)
    {
    // ....
    switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
    case TCP_TW_SYN: 
    // ....
    case TCP_TW_ACK: // 收到了第三次挥手
        tcp_v4_timewait_ack(sk, skb); // 回复第四次挥手
        break;
    case TCP_TW_RST: // 让对端重置
        tcp_v4_send_reset(sk, skb);
        inet_twsk_deschedule_put(inet_twsk(sk));
        goto discard_it;
    case TCP_TW_SUCCESS:; // 无事发生
    }
    goto discard_it;
    }

enum tcp_tw_sttus
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
               const struct tcphdr *th)
{
    // .....
    if (tw->tw_substate == TCP_FIN_WAIT2) { // 当前是FIN_WAIT_2
        /* Just repeat all the checks of tcp_rcv_state_process() */

        /* Out of window, send ACK */
        if (paws_reject ||
            !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
                   tcptw->tw_rcv_nxt,
                   tcptw->tw_rcv_nxt + tcptw->tw_rcv_wnd)) // 超出了窗口,返回ACK
            return tcp_timewait_check_oow_rate_limit(
                tw, skb, LINUX_MIB_TCPACKSKIPPEDFINWAIT2);

        if (th->rst) // 收到了RST包
            goto kill;

        if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt)) // 收到了SYN报文
            return TCP_TW_RST;

        /* Dup ACK? */
        if (!th->ack ||
            !after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) ||
            TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) { // 收到了一个Dup Ack,直接无事发生
            inet_twsk_put(tw); 
            return TCP_TW_SUCCESS;
        }

        /* New data or FIN. If new data arrive after half-duplex close,
         * reset.
         */
        if (!th->fin ||
            TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1)
            return TCP_TW_RST;
        
        // 证明收到了FIN报文
        /* FIN arrived, enter true time-wait state. */
        tw->tw_substate	  = TCP_TIME_WAIT;
        tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
        if (tmp_opt.saw_tstamp) { // 更新时间戳
            tcptw->tw_ts_recent_stamp = ktime_get_seconds();
            tcptw->tw_ts_recent	  = tmp_opt.rcv_tsval;
        }

        inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN); // 设置TIME_WAIT的最长时间为60s
        return TCP_TW_ACK;
    }
}

tcp_v4_timewait_ack

这个函数是用来发送第四次的挥手包

static void tcp_v4_timewait_ack(struct sock *sk, struct sk_buff *skb)
{
    struct inet_timewait_sock *tw = inet_twsk(sk);
    struct tcp_timewait_sock *tcptw = tcp_twsk(sk);

    tcp_v4_send_ack(sk, skb,
            tcptw->tw_snd_nxt, tcptw->tw_rcv_nxt,
            tcptw->tw_rcv_wnd >> tw->tw_rcv_wscale,
            tcp_time_stamp_raw() + tcptw->tw_ts_offset,
            tcptw->tw_ts_recent,
            tw->tw_bound_dev_if,
            tcp_twsk_md5_key(tcptw),
            tw->tw_transparent ? IP_REPLY_ARG_NOSRCCHECK : 0,
            tw->tw_tos
            ); // 发送ACK报文,第四次挥手包

    inet_twsk_put(tw);
}

tcp_rcv_state_process

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    // .....
    switch (sk->sk_state) {
    // ...
    case TCP_LAST_ACK: // 正处于LAST_ACK状态
        if (tp->snd_una == tp->write_seq) { // 缓冲中的数据序列号等于想要接受的那一个,说明这个包是等待的第四次挥手
            tcp_update_metrics(sk); // 更新指标
            tcp_done(sk); // 关闭socket
            goto consume;
        }
        break;
    }
    // .....
consume:
    __kfree_skb(skb);
    return 0;
}

第四次挥手丢失,会发生什么?

第四次挥手是一个 ACK 报文,在前面已经提到过 ACK 报文是不会重发的,因此如果服务端没能成功接收到,则会进行第三次挥手 FIN 报文的重发,重发次数由tcp_orphan_retries(默认值0,代码处理为8)控制,当达到上限之后,服务端就自动转为 CLOSE 状态了。

而此时的客户端已经是处于 TIME_WAIT 状态,该状态是有维持时间上限的,默认是60s,所以客户端会在 TIME_WAIT 状态结束之后,自动转为 CLOSE 状态。该状态主要有两个作用:

  • 确保四次挥手能够“优雅”的关闭:

因为如果第四次挥手丢失了,那么服务端会重发第三次挥手的 FIN 报文,此时就需要有足够的时间去等待接收这个数据报然后再重发 ACK 报文,否则如果客户端直接进入了 CLOSE 状态,那么在收到 FIN 报文之后就会直接回复 RST 报文,这就使得服务端是因为异常而关闭;

  • 确保旧链接的数据包过期:

TIME_WAIT 状态的持续时长是 60s,这等价于 2MSL,而一个报文在网络中最长存活时间是 MSL,所以 TIME_WAIT 状态就能够起到防止复用之前地址和端口构建起来的新链接会接收到旧的数据包的问题。

处于 TIME_WAIT 状态的 TCP 也可能会因为收到了 RST 报文而直接转为了 CLOSE 状态,比方说客户端收到了一个被重发的数据包并回复了 ACK 报文,而此时服务端已经由于收到了第四次挥手包而处于了 CLOSE 状态,这时就会回复 RST 报文,然后就致使客户端的 TIME_WAIT 状态结束,这个问题称为TMA现象。为了避免这个问题,处于TIME_WAIT 状态的 TCP 会忽略 RST 报文。


Similar Posts

下一篇 bind源码解析

Comments