首页 > 编程语言 >TCP三次握手源码分析(服务端接收ACK&TCP连接建立完成)

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

时间:2024-01-20 10:44:25浏览次数:31  
标签:struct ACK req sock tcp sk 源码 TCP skb

内核版本: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,唤醒服务器程序来接收客户端的连接请求。

 

标签:struct,ACK,req,sock,tcp,sk,源码,TCP,skb
From: https://www.cnblogs.com/573583868wuy/p/17976000

相关文章

  • 《Java并发实现原理:JDK源码剖析》PDF
    《Java并发实现原理:JDK源码剖析》全面而系统地剖析了JavaConcurrent包中的每一个部分,对并发的实现原理进行了深刻的探讨。全书分为8章,第1章从最基础的多线程知识讲起,理清多线程中容易误解的知识点,探究背后的原理,包括内存重排序、happen-before、内存屏障等;第2~8章,从简单到复杂,逐......
  • 《Java并发实现原理:JDK源码剖析》PDF
    《Java并发实现原理:JDK源码剖析》全面而系统地剖析了JavaConcurrent包中的每一个部分,对并发的实现原理进行了深刻的探讨。全书分为8章,第1章从最基础的多线程知识讲起,理清多线程中容易误解的知识点,探究背后的原理,包括内存重排序、happen-before、内存屏障等;第2~8章,从简单到复杂,逐......
  • A021 《斗图大赛》编程 源码
    一、课程介绍本节课将学习新的while循环,并结合布尔值True实现无限循环,最终实现一个动态表情包的效果。二、重难点解析布尔值在编程中,True是真,False是假。“真,假”,也是“对,错”的意思,它们是由英国著名数学家和逻辑学家乔治布尔提出的。所以,True和False也叫做布尔值,用于表示......
  • Linked list reversal using stack【1月19日学习笔记】
    点击查看代码//Linkedlistreversalusingstack#include<iostream>#include<stack>//stackfromstandardtemplatelibrary(STL)usingnamespacestd;structnode{ chardata; node*next;};node*A;//全局头指针voidreverse(){ if(A==NULL)return;//空......
  • string reversal using stack【1月19日学习笔记】
    点击查看代码//stringreversalusingstack#include<iostream>#include<stack>//stackfromstandardtemplatelibrary(STL)usingnamespacestd;voidreverse(char*c,intn){ stack<char>S; for(inti=0;i<n;i++){ S.push(c[i]);//st......
  • 布局控件:Grid和StackPanel
    布局控件:Grid和StackPanel本文同时为b站WPF课程的笔记,相关示例代码一个窗口顶上的部分叫做非客户区,下面的部分叫做客户区域。非客户区域主要就是一个Title和三个窗口样式按钮。我们主要学习修改客户区域。Grid直接在<Window>标签下当然也是可以直接写元素的,但是只能写一个。......
  • 布局控件:Grid和StackPanel
    布局控件:Grid和StackPanel本文同时为b站WPF课程的笔记,相关示例代码一个窗口顶上的部分叫做非客户区,下面的部分叫做客户区域。非客户区域主要就是一个Title和三个窗口样式按钮。我们主要学习修改客户区域。Grid直接在<Window>标签下当然也是可以直接写元素的,但是只能写一个。......
  • BackgroundWorker 简单示例()
    //异步使用前一定要把下面的代码进行初始化才能成功调用//if(!bgwtemp.IsBusy)//{//bgwtemp.RunWorkerAsync();//}publicBackgroundWorkerbgwtemp=null;publicForm2(){InitializeComponent();bgwtemp=newBackgroundWorker();bgwtemp.DoWork+=bgwtemp_work;......
  • 纯网页语音视频聊天和桌面分享(附源码,PC版+手机版)
    在网页里实现文字聊天是比较容易的,但若要实现视频聊天,就比较麻烦了。本文将实现一个纯网页版的视频聊天和桌面分享的Demo,可直接在浏览器中运行,不需要安装任何插件。一.主要功能及支持平台1.本Demo的主要功能有(1)一对一语音视频聊天。(2)远程桌面观看。(3)当客户端掉线时,会......
  • 若依框架入门一源码分析一登录验证码
    若依框架入门一源码分析一关于登录页面的验证码问题前端页面的验证码开关设置的是true,但是打开画面验证码没有被显示,原因是后端代码判断了redis中是否有值,有则覆盖前端<el-form-itemprop="code"v-if="captchaEnabled"><el-inputv-model="loginForm......