TCP三次握手源码分析

Posted seanloveslife

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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,用于唯一标识该连接成功的客户端。

以上是关于TCP三次握手源码分析的主要内容,如果未能解决你的问题,请参考以下文章

TCP三次握手源码分析

TCP三次握手源码分析

深入理解TCP协议及其源代码——三次握手

深入理解TCP实现,TCP三次握手四次挥手全讲明白了

用tcpdump分析tcp三次握手,四次挥手

TCP协议三次握手过程分析