zoukankan      html  css  js  c++  java
  • TCP三次握手源码分析

    一. 基本知识

    TCP报文结构:

    相信大多数人都了解三次握手是什么了,这里就直接上图了:

    已知内核会为任何一个给定的监听套接口维护一个队列,该队列由两部分构成,分别是完成连接接队列、未完成连接队列:

    1、未完成连接队列(incomplete connection queue),当服务器每收到客户端的一个SYN,就会将该客户端放入未完成连接队列,而服务器套接口处于 SYN_RCVD 状态。

    2、已完成连接队列(completed connection queue),当客户端和服务器彻底完成三次握手过程,客户端将从未完成连接队列升级成已完成连接队列,并从未完成连接队列中清空该客户端,这些套接口处于 ESTABLISHED 状态。

     当来自客户端的SYN到达时,TCP在为完成连接队列中创建一个新项,然后是服务器的SYN响应,其中捎带对客户SYN的ACK,这一想一直保留到未完成连接队列,直到第三次握手时客户端向服务器发送的SYN和ACK到达或者该项超时为止,如果三次握手正常完成,该项就从未完成队列移到已完成连接队列的队尾。

    linux中TCP编程的步骤:

     本次实验主要关注三次握手的建立过程中源码究竟干了什么。

    二. linux中的TCP握手过程

    1. 第一次握手

    client发送两个包,一个SYN包,一个对服务器的响应 ACK包。

    首先客户端调用connect主动发起连接:

    int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
    {
        struct socket *sock;
        struct sockaddr_storage address;
        int err, fput_needed;
    
            //得到socket对象
        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (!sock)
            goto out;
        //将地址对象从用户空间拷贝到内核空间
            err = move_addr_to_kernel(uservaddr, addrlen, &address);
        if (err < 0)
            goto out_put;
            //内核相关,不需要管他
        err =
            security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
        if (err)
            goto out_put;
    
            //对于流式套接字,sock->ops为 inet_stream_ops --> inet_stream_connect
            //对于数据报套接字,sock->ops为 inet_dgram_ops --> inet_dgram_connect
        err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
                      sock->file->f_flags);
    out_put:
         fput_light(sock->file, fput_needed);
    out:
         return err;
    }
    SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
            int, addrlen)
    {
        return __sys_connect(fd, uservaddr, addrlen);
    }                                    

    该函数一共做了三件事:

    第一,根据文件描述符找到指定的socket对象;

    第二,将地址信息从用户空间拷贝到内核空间;

    第三,调用指定类型套接字的connect函数。

    对应流式套接字的connect函数是inet_stream_connect,内部实际调用的是__inet_stream_connect,接着我们分析该函数:

    int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
                  int addr_len, int flags)
    {
        struct sock *sk = sock->sk;
        int err;
        long timeo;
     
        if (addr_len < sizeof(uaddr->sa_family))
            return -EINVAL;
     
        //检查协议族
        if (uaddr->sa_family == AF_UNSPEC) {
            err = sk->sk_prot->disconnect(sk, flags);
            sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
            goto out;
        }
     
        switch (sock->state) {
        default:
            err = -EINVAL;
            goto out;
        case SS_CONNECTED:
            err = -EISCONN;
            goto out;
        case SS_CONNECTING:
            err = -EALREADY;
            break;
        case SS_UNCONNECTED:
            err = -EISCONN;
            if (sk->sk_state != TCP_CLOSE)
                goto out;
            //对于流式套接字,sock->ops为 inet_stream_ops -->         inet_stream_connect  --> tcp_prot  --> tcp_v4_connect
            err = sk->sk_prot->connect(sk, uaddr, addr_len);
            if (err < 0)
                goto out;
            //协议方面的工作已经处理完成了,但是自己的一切工作还没有完成,所以切换至正在连接中
            sock->state = SS_CONNECTING;
            err = -EINPROGRESS;
            break;
        }
     
        //获取阻塞时间timeo。如果socket是非阻塞的,则timeo是0
        //connect()的超时时间为sk->sk_sndtimeo,在sock_init_data()中初始化为MAX_SCHEDULE_TIMEOUT,表示无限等待,可以通过SO_SNDTIMEO选项来修改
        timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
     
        if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
            int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
                    tcp_sk(sk)->fastopen_req &&
                    tcp_sk(sk)->fastopen_req->data ? 1 : 0;
     
        //如果socket是非阻塞的,那么就直接返回错误码-EINPROGRESS。
        //如果socket为阻塞的,就调用inet_wait_for_connect(),通过睡眠来等待。在以下三种情况下会被唤醒:
        //(1) 使用SO_SNDTIMEO选项时,睡眠时间超过设定值,返回0。connect()返回错误码-EINPROGRESS。
        //(2) 收到信号,返回剩余的等待时间。connect()返回错误码-ERESTARTSYS或-EINTR。
        //(3) 三次握手成功,sock的状态从TCP_SYN_SENT或TCP_SYN_RECV变为TCP_ESTABLISHED,
        if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
                goto out;
     
            err = sock_intr_errno(timeo);
            //进程收到信号,如果err为-ERESTARTSYS,接下来库函数会重新调用connect()
            if (signal_pending(current))
                goto out;
        }
    
        if (sk->sk_state == TCP_CLOSE)
            goto sock_error;
     
        //更新socket状态为连接已建立
        sock->state = SS_CONNECTED;
        err = 0;
    out:
        return err;
     
    sock_error:
        err = sock_error(sk) ? : -ECONNABORTED;
        sock->state = SS_UNCONNECTED;
     
        //如果使用的是TCP,则sk_prot为tcp_prot,disconnect为tcp_disconnect()
        if (sk->sk_prot->disconnect(sk, flags))
            //如果失败
            sock->state = SS_DISCONNECTING;
        goto out;
    }

    该函数主要做了三件事:

    1. 检查socket地址长度和使用的协议族;

    2. 检查socket的状态,必须是SS_UNCONNECTED或SS_CONNECTING;

    3. 调用实现协议的connect函数,对于流式套接字,实现协议是tcp,调用的是tcp_v4_connect();

    4.对于阻塞调用,等待后续握手的完成;对于非阻塞调用,则直接返回 -EINPROGRESS。

    TCP的三次握手一般由客户端通过connect发起,因此我们先来分析tcp_v4_connect的源代码:

    int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
    {
        ...
    //将下一跳地址和目的地址的临时变量都暂时设为用户提交的地址。 if (inet_opt && inet_opt->opt.srr) { if (!daddr) return -EINVAL; nexthop = inet_opt->opt.faddr; } //源端口和目的端口 orig_sport = inet->inet_sport; orig_dport = usin->sin_port; fl4 = &inet->cork.fl.u.ip4; //如果使用了来源地址路由,选择一个合适的下一跳地址。 rt = ip_route_connect(fl4, nexthop, inet->inet_saddr, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, IPPROTO_TCP, orig_sport, orig_dport, sk);
      ...
    //进行路由查找,并校验返回的路由的类型,TCP是不被允许使用多播和广播的 if (!inet_opt || !inet_opt->opt.srr) daddr = fl4->daddr; //更新目的地址临时变量——使用路由查找后返回的值 if (!inet->inet_saddr) inet->inet_saddr = fl4->saddr; sk_rcv_saddr_set(sk, inet->inet_saddr); //如果还没有设置源地址,和本地发送地址,则使用路由中返回的值 if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) { tp->rx_opt.ts_recent = 0; tp->rx_opt.ts_recent_stamp = 0; if (likely(!tp->repair)) tp->write_seq = 0; }
      ...
    //保存目的地址及端口 inet->inet_dport = usin->sin_port; sk_daddr_set(sk, daddr); inet_csk(sk)->icsk_ext_hdr_len = 0; if (inet_opt) inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen; //设置最小允许的mss值 536 tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT; //套接字状态被置为 TCP_SYN_SENT, tcp_set_state(sk, TCP_SYN_SENT); err = inet_hash_connect(&tcp_death_row, sk); if (err) goto failure; sk_set_txhash(sk); //动态选择一个本地端口,并加入 hash 表,与bind(2)选择端口类似 rt = ip_route_newports(fl4, rt, orig_sport, orig_dport, inet->inet_sport, inet->inet_dport, sk); ... //设置下一跳地址,以及网卡分片相关 sk->sk_gso_type = SKB_GSO_TCPV4; sk_setup_caps(sk, &rt->dst); //还未计算初始序号 if (!tp->write_seq && likely(!tp->repair)) //根据双方地址、端口计算初始序号 tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr, inet->inet_daddr, inet->inet_sport, usin->sin_port); //为 TCP报文计算一个 seq值(实际使用的值是 tp->write_seq+1) --> 根据初始序号和当前时间,随机算一个初始id inet->inet_id = tp->write_seq ^ jiffies; //函数用来根据 sk 中的信息,构建一个完成的 syn 报文,并将它发送出去。 err = tcp_connect(sk); ...   

    在该函数主要完成:

    1. 路由查找,得到下一跳地址,并更新socket对象的下一跳地址;

    2. 将socket对象的状态设置为TCP_SYN_SENT;

    3. 如果没设置序号初值,则选定一个随机初值;

    4. 调用函数tcp_connect完成报文构建和发送。

    继续看tcp_connect:

    //由tcp_v4_connect()->tcp_connect()->tcp_transmit_skb()发送,并置为TCP_SYN_SENT.
    int tcp_connect(struct sock *sk)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        struct sk_buff *buff;
        int err;
     
        //初始化传输控制块中与连接相关的成员
        tcp_connect_init(sk);
     
        if (unlikely(tp->repair)) {
            tcp_finish_connect(sk, NULL);
            return 0;
        }
        //分配skbuff   --> 为SYN段分配报文并进行初始化
        buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
        if (unlikely(!buff))
            return -ENOBUFS;
     
        //构建syn报文
        
        //在函数tcp_v4_connect中write_seq已经被初始化随机值
        tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
        
        tp->retrans_stamp = tcp_time_stamp;
     
        //将报文添加到发送队列上
        tcp_connect_queue_skb(sk, buff);
     
        //显式拥塞通告 ---> 
        //路由器在出现拥塞时通知TCP。当TCP段传递时,路由器使用IP首部中的2位来记录拥塞,当TCP段到达后,
        //接收方知道报文段是否在某个位置经历过拥塞。然而,需要了解拥塞发生情况的是发送方,而非接收方。因
        //此,接收方使用下一个ACK通知发送方有拥塞发生,然后,发送方做出响应,缩小自己的拥塞窗口。
        tcp_ecn_send_syn(sk, buff);
     
        err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
     
              //构造tcp头和ip头并发送
              tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
        if (err == -ECONNREFUSED)
            return err;
     
        tp->snd_nxt = tp->write_seq;
        tp->pushed_seq = tp->write_seq;
        TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);
     
        //启动重传定时器
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                      inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
        return 0;
    }

    该函数完成:

    1 初始化套接字跟连接相关的字段。   2 申请sk_buff空间。  3 将sk_buff初始化为syn报文,实质是操作tcp_skb_cb,在初始化TCP头的时候会用到。   4 调用tcp_connect_queue_skb()函数将报文sk_buff添加到发送队列sk->sk_write_queue。  5 调用tcp_transmit_skb()函数构造tcp头,然后交给网络层。 6 初始化重传定时器

    tcp_connect_queue_skb()函数的原理主要是移动sk_buff的data指针,然后填充TCP头。再然后将报文交给网络层,将报文发出。

    这样,三次握手中的第一次握手在客户端的层面完成,报文到达服务端,由服务端处理完毕后,第一次握手完成,客户端socket状态变为TCP_SYN_SENT。

    2. 第二次握手

    当服务端收到客户端发送的报文之后,处理第二次握手,调用tcp_v4_rev,

    int tcp_v4_rcv(struct sk_buff *skb)
    {
        ...//如果不是发往本地的数据包,则直接丢弃
        if (skb->pkt_type != PACKET_HOST)
            goto discard_it;
     
        /* Count it even if it's bad */
        __TCP_INC_STATS(net, TCP_MIB_INSEGS);
     
     
        ////包长是否大于TCP头的长度
        if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
            goto discard_it;
     
        ...//根据源端口号,目的端口号和接收的interface查找sock对象------>先在建立连接的哈希表中查找------>如果没找到就从监听哈希表中找 
     
        //对于建立过程来讲肯是监听哈希表中才能找到
        sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
                       th->dest, &refcounted);
        
        //如果找不到处理的socket对象,就把数据报丢掉
        if (!sk)
            goto no_tcp_socket;

      ...
    //如果socket处于监听状态 --> 我们重点关注这里 if (sk->sk_state == TCP_LISTEN) { ret = tcp_v4_do_rcv(sk, skb); goto put_and_return; } sk_incoming_cpu_update(sk); bh_lock_sock_nested(sk); tcp_segs_in(tcp_sk(sk), skb); ret = 0; //查看是否有用户态进程对该sock进行了锁定 //如果sock_owned_by_user为真,则sock的状态不能进行更改 if (!sock_owned_by_user(sk)) { if (!tcp_prequeue(sk, skb)) ret = tcp_v4_do_rcv(sk, skb); } else if (tcp_add_backlog(sk, skb)) { goto discard_and_relse; } bh_unlock_sock(sk); put_and_return: if (refcounted) sock_put(sk); return ret;   ...
    }

    该函数主要工作就是根据tcp头部信息查到报文的socket对象,然后检查socket状态从而做出不同处理,这里是状态是TCP_LISTEN,直接调用函数tcp_v4_do_rcv,不过该函数主要作用是防止洪泛和拥塞控制,和三次握手无关,接着是调用tcp_rcv_state_process,

    //除了ESTABLISHED和TIME_WAIT状态外,其他状态下的TCP段处理都由本函数实现
    int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
    {
        ...switch (sk->sk_state) {
            
        //SYN_RECV状态的处理 
        case TCP_CLOSE:
            goto discard;
     
        //服务端第一次握手处理
        case TCP_LISTEN:
            if (th->ack)
                return 1;
            if (th->rst)
                goto discard;
            if (th->syn) {
                if (th->fin)
                    goto discard;
                if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
                    return 1;
                consume_skb(skb);
                return 0;
            }
            goto discard;
     
        //客户端第二次握手处理 
        case TCP_SYN_SENT:
            tp->rx_opt.saw_tstamp = 0;
     
            //处理SYN_SENT状态下接收到的TCP段
            queued = tcp_rcv_synsent_state_process(sk, skb, th);
            if (queued >= 0)
                return queued;
     
            //处理完第二次握手后,还需要处理带外数据
            tcp_urg(sk, skb, th);
            __kfree_skb(skb);
     
            //检测是否有数据需要发送
            tcp_data_snd_check(sk);
            return 0;
        }
     
       ...switch (sk->sk_state) {
        case TCP_SYN_RECV:
            if (!acceptable)
                return 1;
     
            if (!tp->srtt_us)
                tcp_synack_rtt_meas(sk, req);
     
            if (req) {
                inet_csk(sk)->icsk_retransmits = 0;
                reqsk_fastopen_remove(sk, req, false);
            } else {
                //建立路由,初始化拥塞控制模块
                icsk->icsk_af_ops->rebuild_header(sk);
                tcp_init_congestion_control(sk);
     
                tcp_mtup_init(sk);
                tp->copied_seq = tp->rcv_nxt;
                tcp_init_buffer_space(sk);
            }
            smp_mb();
            //正常的第三次握手,设置连接状态为TCP_ESTABLISHED 
            tcp_set_state(sk, TCP_ESTABLISHED);
            sk->sk_state_change(sk);
     
            //状态已经正常,唤醒那些等待的线程
            if (sk->sk_socket)
                sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
     
            ...
     
            //更新最近一次发送数据包的时间
            tp->lsndtime = tcp_time_stamp;
     
            tcp_initialize_rcv_mss(sk);
     
            //计算有关TCP首部预测的标志
            tcp_fast_path_on(tp);
            break;
     
        ...
    }

    这是TCP建立连接的核心所在,几乎所有状态的套接字,在收到报文时都会在这里完成处理。对于服务端来说,收到第一次握手报文时的状态为TCP_LISTEN,接下来将由tcp_v4_conn_request函数处理,该函数实际调用的tcp_conn_request:

    int tcp_conn_request(struct request_sock_ops *rsk_ops,
                 const struct tcp_request_sock_ops *af_ops,
                 struct sock *sk, struct sk_buff *skb)
    {
        ...
     
        //处理TCP SYN FLOOD攻击相关的东西
    //Client发送SYN包给Server后挂了,Server回给Client的SYN-ACK一直没收到Client的ACK确认,这个时候这个连接既没建立起来, //也不能算失败。这就需要一个超时时间让Server将这个连接断开,否则这个连接就会一直占用Server的SYN连接队列中的一个位置, //大量这样的连接就会将Server的SYN连接队列耗尽,让正常的连接无法得到处理。 //目前,Linux下默认会进行5次重发SYN-ACK包,重试的间隔时间从1s开始,下次的重试间隔时间是前一次的双倍,5次的重试时间间隔 //为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s, //TCP才会把断开这个连接。由于,SYN超时需要63秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的SYN包给Server(俗称 SYN flood 攻击), //用于耗尽Server的SYN队列。对于应对SYN 过多的问题,linux提供了几个TCP参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。 if ((net->ipv4.sysctl_tcp_syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) { want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name); if (!want_cookie) goto drop; } if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) { NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS); goto drop; } //分配一个request_sock对象来代表这个半连接 //在三次握手协议中,服务器维护一个半连接队列,该队列为每个客户端的SYN包开设一个条目(服务端在接收到SYN包的时候, //就已经创建了request_sock结构,存储在半连接队列中),该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客 //户的确认包(会进行第二次握手发送SYN+ACK 的包加以确认)。这些条目所标识的连接在服务器处于Syn_RECV状态,当服 //务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。该队列为SYN 队列,长度为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog) , //在机器的tcp_max_syn_backlog值在/proc/sys/net/ipv4/tcp_max_syn_backlog下配置。 req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie); if (!req) goto drop; //特定协议的request_sock的特殊操作函数集 tcp_rsk(req)->af_specific = af_ops; tcp_clear_options(&tmp_opt); tmp_opt.mss_clamp = af_ops->mss_clamp; tmp_opt.user_mss = tp->rx_opt.user_mss; tcp_parse_options(skb, &tmp_opt, 0, want_cookie ? NULL : &foc); if (want_cookie && !tmp_opt.saw_tstamp) tcp_clear_options(&tmp_opt); tmp_opt.tstamp_ok = tmp_opt.saw_tstamp; //初始化连接请求块,包括request_sock、inet_request_sock、tcp_request_sock tcp_openreq_init(req, &tmp_opt, skb, sk); inet_rsk(req)->no_srccheck = inet_sk(sk)->transparent; inet_rsk(req)->ir_iif = inet_request_bound_dev_if(sk, skb); af_ops->init_req(req, sk, skb); ...//接收窗口初始化 tcp_openreq_init_rwin(req, sk, dst); if (!want_cookie) { tcp_reqsk_record_syn(sk, req, skb); fastopen_sk = tcp_try_fastopen(sk, skb, req, &foc, dst); }
    ...

    drop_and_release: dst_release(dst); drop_and_free: reqsk_free(req); drop: tcp_listendrop(sk); return 0; }

    在该函数中做了不少的事情,但是我们这里重点了解两点:

    1 分配一个request_sock对象来代表这次连接请求(状态为TCP_NEW_SYN_RECV),如果没有设置防范syn  flood相关的选项,则将该request_sock添加到established状态的tcp_sock散列表(如果设置了防范选项,则request_sock对象都没有,只有建立完成时才会分配)

    2 调用tcp_v4_send_synack回复客户端ack,开启第二次握手

    static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst,
                      struct flowi *fl,
                      struct request_sock *req,
                      struct tcp_fastopen_cookie *foc,
                      enum tcp_synack_type synack_type)
    {
        const struct inet_request_sock *ireq = inet_rsk(req);
        struct flowi4 fl4;
        int err = -1;
        struct sk_buff *skb;
     
        /* First, grab a route. */
     
        //查找到客户端的路由
        if (!dst && (dst = inet_csk_route_req(sk, &fl4, req)) == NULL)
            return -1;
     
        //根据路由、传输控制块、连接请求块中的构建SYN+ACK段
        skb = tcp_make_synack(sk, dst, req, foc, synack_type);
     
        //生成SYN+ACK段成功
        if (skb) {
            //生成校验码
            __tcp_v4_send_check(skb, ireq->ir_loc_addr, ireq->ir_rmt_addr);
     
            //生成IP数据报并发送出去
            err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
                            ireq->ir_rmt_addr,
                            ireq->opt);
            err = net_xmit_eval(err);
        }
        return err;
    }

    查找客户端路由,构造syn包,然后调用ip_build_and_send_pkt,依靠网络层将数据报发出去。至此,第二次握手完成。客户端socket状态变为TCP_ESTABLISHED,此时服务端socket的状态为TCP_NEW_SYN_RECV。

    3. 第三次握手

    接下来调用如下函数进行第三次握手

    int tcp_v4_rcv(struct sk_buff *skb)
    { 
      ...

    //收到握手最后一个ack后,会找到TCP_NEW_SYN_RECV状态的req,然后创建一个新的sock进入TCP_SYN_RECV状态,最终进入TCP_ESTABLISHED状态. 并放入accept队列通知select/epoll if (sk->sk_state == TCP_NEW_SYN_RECV) { struct request_sock *req = inet_reqsk(sk); struct sock *nsk; sk = req->rsk_listener; if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) { sk_drops_add(sk, skb); reqsk_put(req); goto discard_it; } if (unlikely(sk->sk_state != TCP_LISTEN)) { inet_csk_reqsk_queue_drop_and_put(sk, req); goto lookup; } sock_hold(sk); refcounted = true; //创建新的sock进入TCP_SYN_RECV state nsk = tcp_check_req(sk, skb, req, false); if (!nsk) { reqsk_put(req); goto discard_and_relse; } if (nsk == sk) { reqsk_put(req); //调用 tcp_rcv_state_process } else if (tcp_child_process(sk, nsk, skb)) { tcp_v4_send_reset(nsk, skb); goto discard_and_relse; } else {//成功后直接返回 sock_put(sk); return 0; } } }

    进入tcp_check_req查看在第三次握手中如何创建新的socket,

    struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
                   struct request_sock *req,
                   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;
        bool own_req;
     
        tmp_opt.saw_tstamp = 0;
        if (th->doff > (sizeof(struct tcphdr)>>2)) {
            tcp_parse_options(skb, &tmp_opt, 0, NULL);
     
            if (tmp_opt.saw_tstamp) {
                tmp_opt.ts_recent = req->ts_recent;
                tmp_opt.ts_recent_stamp = get_seconds() - ((TCP_TIMEOUT_INIT/HZ)<<req->num_timeout);
                paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
            }
        }
        if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn &&
            flg == TCP_FLAG_SYN &&
            !paws_reject) {
            if (!tcp_oow_rate_limited(sock_net(sk), skb,
                          LINUX_MIB_TCPACKSKIPPEDSYNRECV,
                          &tcp_rsk(req)->last_oow_ack_time) &&
     
                !inet_rtx_syn_ack(sk, req)) {
                unsigned long expires = jiffies;
     
                expires += min(TCP_TIMEOUT_INIT << req->num_timeout,
                           TCP_RTO_MAX);
                if (!fastopen)
                    mod_timer_pending(&req->rsk_timer, expires);
                else
                    req->rsk_timer.expires = expires;
            }
            return NULL;
        }
     
        if ((flg & TCP_FLAG_ACK) && !fastopen &&
            (TCP_SKB_CB(skb)->ack_seq !=
             tcp_rsk(req)->snt_isn + 1))
            return sk;
     
        if (paws_reject || !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
                          tcp_rsk(req)->rcv_nxt, tcp_rsk(req)->rcv_nxt + req->rsk_rcv_wnd)) {
            /* Out of window: send ACK and drop. */
            if (!(flg & TCP_FLAG_RST) &&
                !tcp_oow_rate_limited(sock_net(sk), skb,
                          LINUX_MIB_TCPACKSKIPPEDSYNRECV,
                          &tcp_rsk(req)->last_oow_ack_time))
                req->rsk_ops->send_ack(sk, skb, req);
            if (paws_reject)
                __NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSESTABREJECTED);
            return NULL;
        }
     
        if (tmp_opt.saw_tstamp && !after(TCP_SKB_CB(skb)->seq, tcp_rsk(req)->rcv_nxt))
            req->ts_recent = tmp_opt.rcv_tsval;
     
        if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn) {
            flg &= ~TCP_FLAG_SYN;
        }
     
        if (flg & (TCP_FLAG_RST|TCP_FLAG_SYN)) {
            __TCP_INC_STATS(sock_net(sk), TCP_MIB_ATTEMPTFAILS);
            goto embryonic_reset;
        }
    if (!(flg & TCP_FLAG_ACK)) return NULL; if (fastopen) return sk; if (req->num_timeout < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept && TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) { inet_rsk(req)->acked = 1; __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP); return NULL; }
    // 生成child sk, 从ehash中删除req sock ipv4_specific --> tcp_v4_syn_recv_sock child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req); if (!child) goto listen_overflow; //sk->sk_rxhash = skb->hash; sock_rps_save_rxhash(child, skb); //更新rtt_min,srtt,rto tcp_synack_rtt_meas(child, req); //插入accept队列 return inet_csk_complete_hashdance(sk, child, req, own_req); listen_overflow: if (!sysctl_tcp_abort_on_overflow) { inet_rsk(req)->acked = 1; return NULL; } embryonic_reset: if (!(flg & TCP_FLAG_RST)) { req->rsk_ops->send_reset(sk, skb); } else if (fastopen) { reqsk_fastopen_remove(sk, req, true); tcp_reset(sk); } if (!fastopen) { inet_csk_reqsk_queue_drop(sk, req); __NET_INC_STATS(sock_net(sk), LINUX_MIB_EMBRYONICRSTS); } return NULL; }

    重点关注两点:

    1 通过调用链tcp_v4_syn_recv_sock --> tcp_create_openreq_child --> inet_csk_clone_lock 生成新sock,状态设置为TCP_SYN_RECV;且tcp_v4_syn_recv_sock通过调用inet_ehash_nolisten将新sock加入ESTABLISHED状态的哈希表中;

    2 通过调用inet_csk_complete_hashdance,将新sock插入accept队列.

    至此我们得到一个代表本次连接的新sock,状态为TCP_SYN_RECV,接着调用tcp_child_process,进而调用tcp_rcv_state_process:

    int tcp_child_process(struct sock *parent, struct sock *child,
                  struct sk_buff *skb)
    {
        int ret = 0;
        int state = child->sk_state;
     
        tcp_segs_in(tcp_sk(child), skb);
        if (!sock_owned_by_user(child)) {
            ret = tcp_rcv_state_process(child, skb);
            /* Wakeup parent, send SIGIO */
            if (state == TCP_SYN_RECV && child->sk_state != state)
                parent->sk_data_ready(parent);
        } else {
            /* Alas, it is possible again, because we do lookup
             * in main socket hash table and lock on listening
             * socket does not protect us more.
             */
            __sk_add_backlog(child, skb);
        }
     
        bh_unlock_sock(child);
        sock_put(child);
        return ret;
    }

    又回到了函数tcp_rcv_state_process,TCP_SYN_RECV状态的套接字将由以下代码处理:

     //服务端第三次握手处理
        case TCP_SYN_RECV:
            if (!acceptable)
                return 1;
     
            if (!tp->srtt_us)
                tcp_synack_rtt_meas(sk, req);
     
            if (req) {
                inet_csk(sk)->icsk_retransmits = 0;
                reqsk_fastopen_remove(sk, req, false);
            } else {
     
                //建立路由,初始化拥塞控制模块
                icsk->icsk_af_ops->rebuild_header(sk);
                tcp_init_congestion_control(sk);
     
                tcp_mtup_init(sk);
                tp->copied_seq = tp->rcv_nxt;
                tcp_init_buffer_space(sk);
            }
            smp_mb();
            //正常的第三次握手,设置连接状态为TCP_ESTABLISHED 
            tcp_set_state(sk, TCP_ESTABLISHED);
            sk->sk_state_change(sk);
            //状态已经正常,唤醒那些等待的线程
            if (sk->sk_socket)
                sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
     
            tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
            tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
            tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
     
            if (tp->rx_opt.tstamp_ok)
                tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
     
            if (req) {
                tcp_rearm_rto(sk);
            } else
                tcp_init_metrics(sk);
     
            if (!inet_csk(sk)->icsk_ca_ops->cong_control)
                tcp_update_pacing_rate(sk);
            //更新最近一次发送数据包的时间
            tp->lsndtime = tcp_time_stamp;
     
            tcp_initialize_rcv_mss(sk);
     
            //计算有关TCP首部预测的标志
            tcp_fast_path_on(tp);
            break;

    可以看到代码对socket的窗口,mss等进行设置,以及最后将sock的状态设置为TCP_ESTABLISHED,致辞三次握手完成。等待用户调用accept,取出套接字使用。

    另外,服务器端的listen()负责监听客户端的连接请求,并维护一个listen队列,当有客户端连接成功时,就把它放在listen队列里。

    accept函数负责查看listen队列里面有没有成功连接,如果有则从队列中取出,没有则阻塞(直到获得一个成功连接返回)。它每取出一个成功连接,就会生成一个对应的accept fd,用于唯一标识该连接成功的客户端。

  • 相关阅读:
    mongoDB
    昆仑会员此打印方式只针对用会员卡结账的消费,放开限制解决方案
    用jdk1.6的pack200和unpack200,对jar文件进行压缩和解压 .pack.gz
    ffmpeg
    关于golang-mod的使用方法
    组件&Props
    元素渲染
    JSX 简介
    React-HelloWorld
    Vue与REACT两个框架的区别和优势对比
  • 原文地址:https://www.cnblogs.com/seanloveslife/p/12103830.html
Copyright © 2011-2022 走看看