TCP三次握手源码分析(客户端接收SYN+ACK以及发送ACK)

发布时间 2024-01-11 21:25:25作者: 划水的猫

内核版本:Linux 3.10
内核源码地址:https://elixir.bootlin.com/linux/v3.10/source (包含各个版本内核源码,且网页可全局搜索函数)
TCP三次握手源码分析(客户端发送SYN)
TCP三次握手源码分析(服务端接收SYN以及发送SYN+ACK)

一、客户端接收SYN+ACK

随着SYN+ACK报文的发送,连接建立随着第二次握手报文来到客户端。
客户端接收到这个SYN+ACK报文,经过网卡、软中断,依然进入到tcp_v4_rcv。

1.tcp_v4_rcv()函数

细节详情见《TCP三次握手源码分析(服务端接收SYN以及发送SYN+ACK)》。

// file: net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
    ......

    //根据报文的源地址和目的地址在established哈希表以及listen哈希表中查找连接
    //由于之前调用connect时已经sock接链入到established哈希表中(查询端口的时候链入)
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); //最终在ehash中查找到状态为SYN_SENT的sock
    if (!sk) //没有找到接收的sock结构就跳转到no_tcp_socket
        goto no_tcp_socket;

    ......
    ret = 0;
    if (!sock_owned_by_user(sk)) { //检测sock结构是否还可用(没有被用户锁定、没在使用)
        ......
        {
            if (!tcp_prequeue(sk, skb)) //链入预处理队列
                ret = tcp_v4_do_rcv(sk, skb); //处理数据包
        }
    }
    ......
}

最终还是来到了tcp_v4_do_rcv()处理函数。

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    ......
    if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb)) //检查数据块长度,检查校验和
        goto csum_err;

    if (sk->sk_state == TCP_LISTEN) {
        ......
    } else
        sock_rps_save_rxhash(sk, skb);

    if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
        rsk = sk;
        goto reset;
    }
    return 0;
}

由于sock是SYN_SENT状态,所以直接进入到tcp_rcv_state_process(),不过相较于服务端,会进入另外一个分支处理。

// file: net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, unsigned int len)
{
    ......
    switch (sk->sk_state) {
    case TCP_CLOSE: //处理CLOSE状态的sock
        ......

    case TCP_LISTEN: //处理LISTEN状态的sock
        ......

    case TCP_SYN_SENT: //处理SYN_SENT状态的sock
        queued = tcp_rcv_synsent_state_process(sk, skb, th, len); //处理SYN+ACK报文
        if (queued >= 0)
            return queued;

        /* Do step6 onward by hand. */
        tcp_urg(sk, skb, th);
        __kfree_skb(skb);
        tcp_data_snd_check(sk);
        return 0;
    }
    ......
}

2.tcp_rcv_synsent_state_process()函数

// file: net/ipv4/tcp_input.c
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, unsigned int len)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    struct tcp_fastopen_cookie foc = { .len = -1 };
    int saved_clamp = tp->rx_opt.mss_clamp;

    tcp_parse_options(skb, &tp->rx_opt, 0, &foc); //分析该请求的tcp各个选项,比如时间戳、窗口大小、快速开启等选项
    if (tp->rx_opt.saw_tstamp)
        tp->rx_opt.rcv_tsecr -= tp->tsoffset;

    if (th->ack) { //处理带ACK标识的报文
        //如果接收到的确认号小于或等于已发送未确认的序列号,
        //或者大于下次要发送数据的序列号,非法报文,发送RST报文
        // ack <= una || ack > nxt
        if (!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_una) ||
            after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))
            goto reset_and_undo;

        //如果开启了时间戳选项,且回显时间戳不为空
        //且回显时间戳不在当前时间和SYN报文发送的时间窗内,就认为该报文非法
        //对端时间戳不合法
        if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
            !between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp, tcp_time_stamp)) {
            NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSACTIVEREJECTED);
            goto reset_and_undo;
        }

        if (th->rst) { //ACK报文不允许出现RST标志
            tcp_reset(sk);
            goto discard;
        }

        if (!th->syn) //未设置SYN标识,丢弃
            goto discard_and_undo;

        TCP_ECN_rcv_synack(tp, th); //ecn标识

        tcp_init_wl(tp, TCP_SKB_CB(skb)->seq); //记录窗口更新时数据包序号
        tcp_ack(sk, skb, FLAG_SLOWPATH); //确认ACK的确认号正常(可能会更新发送窗口)


        tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1; //更新下一个要接收的序号
        tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1; //更新窗口中最小的序号
        tp->snd_wnd = ntohs(th->window); //获取发送窗口

        if (!tp->rx_opt.wscale_ok) { //没有窗口扩大因子
            tp->rx_opt.snd_wscale = tp->rx_opt.rcv_wscale = 0; //设置为0
            tp->window_clamp = min(tp->window_clamp, 65535U); //设置最大值
        }

        if (tp->rx_opt.saw_tstamp) { //有时间戳选项
            tp->rx_opt.tstamp_ok = 1;
            tp->tcp_header_len =
                sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED; //tcp首部需要增加时间戳长度
            tp->advmss -= TCPOLEN_TSTAMP_ALIGNED; //mss需要减去时间戳长度
            tcp_store_ts_recent(tp); //设置回显时间戳
        } else {
            tp->tcp_header_len = sizeof(struct tcphdr); //记录tcp首部长度
        }

        if (tcp_is_sack(tp) && sysctl_tcp_fack) //有sack选项,开启了fack算法,则打标记
            tcp_enable_fack(tp);

        tcp_mtup_init(sk); //MTU探测相关初始化
        tcp_sync_mss(sk, icsk->icsk_pmtu_cookie); //计算mss
        tcp_initialize_rcv_mss(sk); //初始化rcv_mss

        tp->copied_seq = tp->rcv_nxt; //记录用户空间待读取的序号

        smp_mb();
        
        //连接建立完成,将连接状态推向established
        //然后唤醒等在该socket的所有睡眠进程
        tcp_finish_connect(sk, skb);

        if ((tp->syn_fastopen || tp->syn_data) && tcp_rcv_fastopen_synack(sk, skb, &foc)) //fastopen处理
            return -1;

        /* 如果有以下情况,不会马上发送ACK报文
         * 1.有数据等待发送
         * 2.用户设置了TCP_DEFER_ACCEPT选项
         * 3.禁用快速确认模式,可通过TCP_QUICKACK设置
        */
        if (sk->sk_write_pending || icsk->icsk_accept_queue.rskq_defer_accept || icsk->icsk_ack.pingpong) { //延迟确认
            inet_csk_schedule_ack(sk); //设置ICSK_ACK_SCHED标识,有ACK等待发送,当前不发送
            icsk->icsk_ack.lrcvtime = tcp_time_stamp;
            tcp_enter_quickack_mode(sk); //进入快速ack模式
            //激活延迟ACK定时器,超时时间为200ms
            //最多延迟200ms就会发送ACK报文
            inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,TCP_DELACK_MAX, TCP_RTO_MAX);
discard:
            __kfree_skb(skb);
            return 0;
        } else {
            tcp_send_ack(sk);  //回复ACK
        }
        return -1;
    }

    ......
}

里面比较重要的三个函数:
tcp_ack()删除第一次握手的发送队列以及在connect时设置的重传定时器;
tcp_finish_connect()连接建立并唤醒在该socket睡眠的进程;
tcp_send_ack()发送ACK,发起第三次握手请求。

3.tcp_finish_connect()函数

tcp_finish_connect()函数的主要工作:

  • 将sock状态推向ESTABLISHED,也就意味着在客户端来看,连接已经建立;
  • 然后是初始化路由和拥塞控制等参数;
  • 同时如果用户开启了保活定时器,此时开始生效,计算连接空闲时间;
  • 最后就是唤醒该socket上所有睡眠的进程,如果有进程使用异步通知,则发送SIGIO信号通知进程可写;
// file: net/ipv4/tcp_input.c
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);

    tcp_set_state(sk, TCP_ESTABLISHED); //对于客户端来说,此时连接已经建立,设置连接状态为ESTABLISHED

    if (skb != NULL) {
        icsk->icsk_af_ops->sk_rx_dst_set(sk, skb); //设置接收路由缓存
        security_inet_conn_established(sk, skb);
    }

    icsk->icsk_af_ops->rebuild_header(sk); //检查或重建路由

    tcp_init_metrics(sk);
    tcp_init_congestion_control(sk); //初始化拥塞控制
    tp->lsndtime = tcp_time_stamp; //记录最后一次发送数据包的时间
    tcp_init_buffer_space(sk);

    if (sock_flag(sk, SOCK_KEEPOPEN)) //开启了保活,则打开保活定时器
        inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));

    if (!tp->rx_opt.snd_wscale) //设置预测标志,判断快慢路径的条件之一
        __tcp_fast_path_on(tp, tp->snd_wnd);
    else
        tp->pred_flags = 0;

    if (!sock_flag(sk, SOCK_DEAD)) {
        sk->sk_state_change(sk); //指向sock_def_wakeup,唤醒该socket上所有睡眠的进程
        sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT); //如果进程使用了异步通知,发送SIGIO信号通知进程可写
    }
}

4.tcp_send_ack()函数,发送ACK

// net/ipv4/tcp_output.c
void tcp_send_ack(struct sock *sk)
{
    struct sk_buff *buff;
    if (sk->sk_state == TCP_CLOSE)
        return;
    
    //分配skb,失败需要启用延迟ack定时器
    buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));
    if (buff == NULL) {
        inet_csk_schedule_ack(sk);
        inet_csk(sk)->icsk_ack.ato = TCP_ATO_MIN;
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK, TCP_DELACK_MAX, TCP_RTO_MAX);
        return;
    }

    //预留头部空间,准备控制信息
    skb_reserve(buff, MAX_TCP_HEADER);
    tcp_init_nondata_skb(buff, tcp_acceptable_seq(sk), TCPHDR_ACK);

    TCP_SKB_CB(buff)->when = tcp_time_stamp;
    tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC)); //...传递到IP层,最终经网卡发送到服务端
}