Rxsi Blog GameServer Developer

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

2022-09-14
Rxsi
TCP

第二次挥手

tcp_fin

在服务端进入稳定状态之后,处理数据包的顶层函数为 tcp_rcv_established ,其中该函数根据一些条件划分为了快处理和慢处理两种方式,再通过 tcp_data_queue 放入队列,当该数据包是FIN包,那么会进入 tcp_fin 函数:

void tcp_fin(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    inet_csk_schedule_ack(sk);

    sk->sk_shutdown |= RCV_SHUTDOWN; // 收到了FIN包,说明对端已经关闭了SEND端,因此从接收到这个包开始(注意前面的数据包已经处理完了),那么本端就关闭RCV端
    sock_set_flag(sk, SOCK_DONE);

    switch (sk->sk_state) {
    case TCP_SYN_RECV:
    case TCP_ESTABLISHED: // 当前是ESTABLISHED状态
        /* Move to CLOSE_WAIT */
        tcp_set_state(sk, TCP_CLOSE_WAIT); // 进入CLOSE_WAIT状态
        inet_csk_enter_pingpong_mode(sk); // pingpong??
        break;
    // .........
    }

    /* It _is_ possible, that we have something out-of-order _after_ FIN.
     * Probably, we should reset in this case. For now drop them.
     */
    skb_rbtree_purge(&tp->out_of_order_queue); // 清空乱序序列队列上的数据包
    if (tcp_is_sack(tp)) // 如果开启了SACK
        tcp_sack_reset(&tp->rx_opt); // 关闭SACK

    if (!sock_flag(sk, SOCK_DEAD)) { // 如果未处于DEAD状态
        sk->sk_state_change(sk); // 唤醒正在等待该套接口的进程

        /* Do not send POLL_HUP for half duplex close. */
        if (sk->sk_shutdown == SHUTDOWN_MASK ||
            sk->sk_state == TCP_CLOSE) // 如果在接收发送方向都关闭了,或者已经处于CLOSE状态,那么唤醒等待套接字的进程,通知链接已经停止
            sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_HUP);
        else // 否则,通知链接可以进行写操作
            sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
    }
}

在上面函数返回之后,在 tcp_rcv_established 函数内会根据条件判断是否需要发送ACK报文,并进入CLOSE_WAIT状态。

tcp_rcv_state_process

在服务端发出了第二次挥手之后,此时客户端处于FIN_WAIT_1状态,因此此时对于数据包处理的函数为 tcp_rcv_state_process,相关部分的代码实现如下:

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    // .....

    if (!th->ack && !th->rst && !th->syn) { // 这里就起到了过滤当第二次发出的ACK丢失,而服务端又直接下发第三次挥手包的情况,这里会被直接过滤掉,不作理会
        SKB_DR_SET(reason, TCP_FLAGS);
        goto discard;
    }
    // .....
    switch (sk->sk_state) {
    case TCP_SYN_RECV:
    // ...
    case TCP_FIN_WAIT1: {
        int tmo;

        if (req)
            tcp_rcv_synrecv_state_fastopen(sk); // 处于FIN_WAIT_1状态的TCP还有request_sock结构??这里的操作时删除掉request_sock结构

        if (tp->snd_una != tp->write_seq) // 序列号不对
            break;

        tcp_set_state(sk, TCP_FIN_WAIT2); // 进入FIN_WAIT_2状态
        sk->sk_shutdown |= SEND_SHUTDOWN; // 此时说明客户端的发送要关闭这件事已经让对方知晓,这里再|=SEND_SHUTDOWN是为了关闭发送端,有一点需要注意,在tcp_close中是进入函数时就设置sk_shutdown = SHUTDOWN_MASK 即同时关闭读写端,而tcp_shutdown则是等到进入FIN_WAIT_2阶段后再关闭读端

        sk_dst_confirm(sk);

        if (!sock_flag(sk, SOCK_DEAD)) { // sock还没DEAD
            /* Wake up lingering close() */
            sk->sk_state_change(sk); // 唤醒正在等待的进程
            break;
        }

        if (tp->linger2 < 0) { // 如果linger2<0,说明上层通过setsockopt设置了FIN_WAIT状态不等待,因此这里直接关闭socket
            tcp_done(sk);
            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
            return 1;
        }
        if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
            after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) { // 收到了一个超出序列号范围的FIN包???这里的处理是直接关闭
            /* Receive out of order FIN after close() */
            if (tp->syn_fastopen && th->fin)
                tcp_fastopen_active_disable(sk);
            tcp_done(sk);
            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
            return 1;
        }

        tmo = tcp_fin_time(sk);
        if (tmo > TCP_TIMEWAIT_LEN) { // 如果FIN_WAIT_2阶段的等待时间还大于1分钟,那么这里通过保活定时器去等待差值时间,而在差值时间到了之后,会直接进入TIME_WAIT状态,即调用tcp_time_wait函数
            inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
        } else if (th->fin || sock_owned_by_user(sk)) {
            /* Bad case. We could lose such FIN otherwise.
             * It is not a big problem, but it looks confusing
             * and not so rare event. We still can lose it now,
             * if it spins in bh_lock_sock(), but it is really
             * marginal case.
             */
            inet_csk_reset_keepalive_timer(sk, tmo);
        } else {
            tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); // 进入TIME_WAIT状态,这里就会释放掉重量的sock结构,转而使用inet_timewait_sock结构,因此TIME_WAIT状态占用的内存会少很多
            goto consume;
        }
        break;
    }
// ......
}

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

第二次挥手实际上是 ACK 报文,而 ACK 报文是不会进行定时重发的,因此如果该数据报丢失了,那么只能等待客户端因为迟迟未能接收到 ACK 回复,那么则会定时重发FIN报文,重发的次数上限为tcp_orphan_retries(默认0,代码处理为8)

如果在客户端还没有达到重发上限之前,服务端下发了FIN报文,由于客户端还处于FIN_WAIT_1状态,那么会直接忽略掉这个包

如果客户端因为达到重发上限而转为了CLOSE状态,此时客户端不再存在五元组对应的 socket,然后服务端下发了FIN报文,那么服务端会收到RST报文,进而关闭服务端的 TCP。服务端的第三次挥手也是有超时时间的,也是 tcp_orphan_retries,因此如果第二次挥手包丢失,往往都是客户端先重发 FIN 报文到超时后转为 close 状态。


Similar Posts

Comments