TCP三次握手源码分析(服务端接收ACK&TCP连接建立完成)

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

一、服务端接收ACK

客户端发送第三次握手ACK报文,此时客户端sock的状态已经是ESTABLISHED。
随着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)
{
    ......

    //服务端其实有两个socket:服务端调用socket()函数初始化的监听socket和用于跟客户端建立连接用的通信socket
    //根据报文的源地址和目的地址在established哈希表以及listen哈希表中查找连接
    //之前服务端接收到客户端的SYN报文时,并未修改监听socket的状态,所以socket的状态依然是listen
    //通信socket目前还处于半连接队列中
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); //最终在listen哈希表中查找到状态为listen的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()处理函数。

2.tcp_v4_do_rcv()函数

// file: net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    struct sock *rsk;
    ......
    if (sk->sk_state == TCP_ESTABLISHED) { //如果sock已经处于ESTABLISHED状态
        ...... //此时,服务器收到第三次握手的ACK,sock是同listen哈希表找到的监听sock,处于LISTEN状态
    }

    if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb)) //检查数据块长度,检查校验和
        goto csum_err;

    if (sk->sk_state == TCP_LISTEN) { //如果sock处理LISTEN状态
        struct sock *nsk = tcp_v4_hnd_req(sk, skb); //在半连接队列中查到到第一次握手创建的request sock
        if (!nsk) //没有找到,丢弃数据包
            goto discard;

        if (nsk != sk) { //找到的nsk是通信sock,与sk(监听sock)不是同一个sock
            sock_rps_save_rxhash(nsk, skb);
            if (tcp_child_process(sk, nsk, skb)) { //处理新创建的sock
                rsk = nsk;
                goto reset;
            }
            return 0;
        }
    } 
    ......
}

tcp_v4_hnd_req()函数在半连接队列中找到了第一次握手时创建的request sock结构;创建通信sock,并链入全连接队列中。

// file: net/ipv4/tcp_ipv4.c
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
    struct tcphdr *th = tcp_hdr(skb);
    const struct iphdr *iph = ip_hdr(skb);
    struct sock *nsk;
    struct request_sock **prev;
    
    // 查找连接请求结构(半连接队列查找),找到了第一次握手时创建的request sock结构
    struct request_sock *req = inet_csk_search_req(sk, &prev, th->source, iph->saddr, iph->daddr);
    if (req)
        return tcp_check_req(sk, skb, req, prev, false); //创建通信sock结构,将连接请求链入这个sock结构的接收队列(全连接队列)中

    ......
}

tcp_check_req()创建通信sock,加入ehash中,这样在下一次接收数据时,tcp_v4_rcv函数调用__inet_lookup_skb()查找sock结构则优先从ehash中获取通信sock;
同时删除半连接队列的request sock,还把通信sock链入到全连接队列。监听sock继续监听新的请求。

// file: net/ipv4/tcp_minisocks.c
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, struct request_sock **prev, bool fastopen)
{
    struct tcp_options_received tmp_opt;
    struct sock *child;
    const struct tcphdr *th = tcp_hdr(skb);
    __be32 flg = tcp_flag_word(th) & (TCP_FLAG_RST|TCP_FLAG_SYN|TCP_FLAG_ACK);
    bool paws_reject = false;

    ......
    // 创建通信sock,并初始化
    // inet_csk(sk)->icsk_af_ops在tcp_v4_init_sock()中挂入ipv4_specific结构,因此最终执行的是tcp_v4_syn_recv_sock()
    // tcp_v4_syn_recv_sock()克隆监听sock结构,并结合request sock的内容初始化一个新的通信sock,加入ehash、bhash中并修改通信sock的状态为SYN_RECV
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
    if (child == NULL)
        goto listen_overflow;

    inet_csk_reqsk_queue_unlink(sk, req, prev); //将request sock从半连接队列脱队
    inet_csk_reqsk_queue_removed(sk, req); //递减半连接队列的计数器、删除SYNACK定时器

    inet_csk_reqsk_queue_add(sk, req, child); //将通信sock链入到全连接队列
    return child;

    ......
}

inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL)最终执行的是tcp_v4_syn_recv_sock()函数,里面做了很多事。
克隆监听sock结构,并结合request sock的内容初始化一个新的通信sock,加入ehash、bhash中并修改通信sock的状态为SYN_RECV等等。
此时监听sock为LISTEN状态,通信sock为SYN_RECV状态。这个地方与平时花的三次握手图稍微有些许差异,第三次握手时,服务端通信sock才被初始化为SYN_RECV状态。

3.tcp_child_process()函数

// file: net/ipv4/tcp_minisocks.c
int tcp_child_process(struct sock *parent, struct sock *child, struct sk_buff *skb)
{
    int ret = 0;
    int state = child->sk_state;

    if (!sock_owned_by_user(child)) { //检查客户端sock是否可用
        ret = tcp_rcv_state_process(child, skb, tcp_hdr(skb), skb->len); //根据监听sock的状态处理数据包
        if (state == TCP_SYN_RECV && child->sk_state != state) //经过tcp_rcv_state_process()函数,监听sock的状态改变了
            parent->sk_data_ready(parent, 0); //唤醒经常处理连接请求
    } else {
        __sk_add_backlog(child, skb); //否则放入后备队列中
    }

    bh_unlock_sock(child); //解锁
    sock_put(child); //递减使用计数器
    return ret;
}

兜兜转转,最终还是进入到了根据sock状态处理数据包的tcp_rcv_state_process()函数。不过此时使用的是通信sock,状态为SYN_RECV。

二、建立连接

// 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)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct request_sock *req;
    int queued = 0;

    ......
    req = tp->fastopen_rsk; //fastopen选项相关
    ......

    /* step 5: check the ACK field */
    if (true) {
        int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) > 0; //检查ACK确认号的合法值

        switch (sk->sk_state) {
        case TCP_SYN_RECV:
            if (acceptable) {
                if (req) { //fastopen走这个流程
                    tcp_synack_rtt_meas(sk, req);
                    tp->total_retrans = req->num_retrans;

                    reqsk_fastopen_remove(sk, req, false);
                } else {
                    icsk->icsk_af_ops->rebuild_header(sk);
                    tcp_init_congestion_control(sk); ////初始化拥塞控制

                    tcp_mtup_init(sk); //mtu探测初始化
                    tcp_init_buffer_space(sk); ////初始化接收和发送缓存空间
                    tp->copied_seq = tp->rcv_nxt;
                }
                smp_mb();
                tcp_set_state(sk, TCP_ESTABLISHED); //修改通信sock状态为ESTABLISHED
                // 调用sock_def_wakeup唤醒该sock上等待队列的所有进程
                // 即唤醒服务器程序来接收客户端的连接请求,对接了accept()内容中的inet_csk_wait_for_connect()函数
                sk->sk_state_change(sk); 

            ......
        
        }
    }
    ......
    return 0;
}

三、总结

第三次握手,服务端所做的工作,就是把当前半连接对象删除,创建了通信sock后加入到全连接队列中,最后将新连接状态设置为 ESTABLISHED,唤醒服务器程序来接收客户端的连接请求。

 

posted @ 2024-01-20 10:41  划水的猫  阅读(107)  评论(0编辑  收藏  举报