深入理解TCP协议及其源代码——connect及bind、listen、accept背后的三次握手

1 TCP概述

  传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。互联网络与单个网络有很大的不同,因为互联网络的不同部分可能有截然不同的拓扑结构、带宽、延迟、数据包大小和其他参数。TCP的设计目标是能够动态地适应互联网络的这些特性,而且具备面对各种故障时的健壮性。

主要功能:

  当应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,TCP则把数据流分割成适当长度的报文段,最大传输段大小(MSS)通常受该计算机连接的网络的数据链路层的最大传送单元(MTU)限制。之后TCP把数据包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
  • 在数据正确性与合法性上,TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和;同时可以使用md5认证对数据进行加密。
  • 在保证可靠性上,采用超时重传和捎带确认机制。
  • 在流量控制上,采用滑动窗口协议,协议中规定,对于窗口内未经确认的分组需要重传。
  • 在拥塞控制上,采用TCP拥塞控制算法:慢启动、拥塞避免、快速重传、快速恢复。

2 TCP三次握手

  所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:

(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

简单来说,就是:

  建立连接时,客户端发送SYN包(SYN=i)到服务器,并进入到SYN-SEND状态,等待服务器确认。

  服务器收到SYN包,必须确认客户的SYN(ack=i+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器进入SYN-RECV状态。

  客户端收到服务器的SYN+ACK包,向服务器发送确认报ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手,客户端与服务器开始传送数据。

2 connect及bind、listen、accept背后的三次握手

1)首先再lab3目录下执行以下命令,在qemu中启动gdb server

qemu -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append  nokaslr -s -S

qemu虚拟机启动后重新打开一个终端,输入以下命令

gdb
file ~/LinuxKernel/linux-5.0.1/vmlinux
target remote:1234
# 依次为各个函数设置断点
b __sys_socket
b __sys_bind
b __sys_connect
b __sys_listen
b __sys_accept4

  

 断点设置成功,然后输入replyhi,不断在终端输入c,可以通过终端输出的断点信息知道哦各个函数执行顺序,如图所示

 断点顺序为1,2,4,5,1,3,5,所以函数的执行顺序为,__sys_socket, __sys_bind, __sys_listen, __sys_accept4, __sys_socket, __sys_connect, __sys_accept4,接下来对相应的函数源码进行分析。

bind 函数主要是服务器端使用,把一个本地协议地址赋予套接字;socket 函数并没有为套接字绑定本地地址和端口号,对于服务器端则必须显性绑定地址和端口号。

isten() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度(这个长度有什么用,后面做详细的解释),TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。

accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。与三次握手也没有直接的联系,

然后是connect函数,这里是真正tcp三次握手的开始,由客户端调用connect主动发起连接,查询__sys_connect函数源码:

/*
*    Attempt to connect to a socket with the server address.  The address
*    is in user space so we verify it is OK and move it to kernel space.
*
*    For 1003.1g we need to add clean support for a bind to AF_UNSPEC to
*    break bindings
*
*    NOTE: 1003.1g draft 6.3 is broken with respect to AX.25/NetROM and
*    other SEQPACKET protocols that take time to connect() as it doesn't
*    include the -EINPROGRESS status for such sockets.
*/

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;

    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;

    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:

/* 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;

 

    lock_sock(sk);/* 获取套接口的锁 */

 

    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;

        /* Fall out of switch with err, set for this state */

        break;

    case SS_UNCONNECTED:/* 只有此状态才能调用connect */

        err = -EISCONN;

        if (sk->sk_state != TCP_CLOSE)/* 如果不是TCP_CLOSE状态,说明已经连接了 */

            goto out;

 

/* 调用传输层接口tcp_v4_connect建立与服务器连接,并发送SYN段 */

        err = sk->sk_prot->connect(sk, uaddr, addr_len);

        if (err < 0)

            goto out;

 

        /* 发送SYN段后,设置状态为SS_CONNECTING */

        sock->state = SS_CONNECTING;

 

 

        err = -EINPROGRESS;/* 如果是以非阻塞方式进行连接,则默认的返回值为

EINPROGRESS,表示正在连接 */

        break;

    }

 

    /* 获取连接超时时间,如果指定非阻塞方式,则不等待直接返回 */

    timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);

 

    if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {/* 发送完SYN

后,连接状态一般为这两种状态,但是如果连接建立非常快,则可能越过这两种状态 */

        if (!timeo || !inet_wait_for_connect(sk, timeo))/* 等待连接完成或超时 */

            goto out;

 

        err = sock_intr_errno(timeo);

        if (signal_pending(current))

            goto out;

    }

 

    if (sk->sk_state == TCP_CLOSE)/* 运行到这里说明连接建立失败 */

        goto sock_error;

 

    sock->state = SS_CONNECTED;/* 连接建立成功,设置为已经连接状态 */

    err = 0;

out:

    release_sock(sk);

    return err;

 

sock_error:

    err = sock_error(sk) ? : -ECONNABORTED;

    sock->state = SS_UNCONNECTED;

    if (sk->sk_prot->disconnect(sk, flags))

        sock->state = SS_DISCONNECTING;

    goto out;

}

该函数首先检查socket地址长度和使用的协议族,检查socket的状态,必须是SS_UNCONNECTED或SS_CONNECTING,调用实现协议的connect函数,对于流式套接字,实现协议是tcp,调用的是tcp_v4_connect(),对于阻塞调用,等待后续握手的完成;对于非阻塞调用,则直接返回 -EINPROGRESS。

然后是tcp_v4_connect的源代码:

/* This will initiate an outgoing connection. */
 
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
    struct inet_sock *inet = inet_sk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    __be16 orig_sport, orig_dport;
    __be32 daddr, nexthop;
    struct flowi4 *fl4;
    struct rtable *rt;
    int err;
    struct ip_options_rcu *inet_opt;
 
    if (addr_len < sizeof(struct sockaddr_in))
        return -EINVAL;
 
    if (usin->sin_family != AF_INET)
        return -EAFNOSUPPORT;
 
    nexthop = daddr = usin->sin_addr.s_addr;
    inet_opt = rcu_dereference_protected(inet->inet_opt,
                         lockdep_sock_is_held(sk));
 
    //将下一跳地址和目的地址的临时变量都暂时设为用户提交的地址。 
    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);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        if (err == -ENETUNREACH)
            IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
        return err;
    }
 
    if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
        ip_rt_put(rt);
        return -ENETUNREACH;
    }
 
    //进行路由查找,并校验返回的路由的类型,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) {
        /* Reset inherited state */
        tp->rx_opt.ts_recent       = 0;
        tp->rx_opt.ts_recent_stamp = 0;
        if (likely(!tp->repair))
            tp->write_seq       = 0;
    }
 
    if (tcp_death_row.sysctl_tw_recycle &&
        !tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr)
        tcp_fetch_timewait_stamp(sk, &rt->dst);
 
    //保存目的地址及端口
    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;
 
    /* Socket identity is still unknown (sport may be zero).
     * However we set state to SYN-SENT and not releasing socket
     * lock select source port, enter ourselves into the hash tables and
     * complete initialization after this.
     */
 
    //套接字状态被置为 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);
 
                   
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        rt = NULL;
        goto failure;
    }
    /* OK, now commit destination to socket.  */
 
    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);

    inet->inet_id = tp->write_seq ^ jiffies;
    
    //函数用来根据 sk 中的信息,构建一个完成的 syn 报文,并将它发送出去。
    err = tcp_connect(sk);
 
    rt = NULL;
    if (err)
        goto failure;
 
    return 0;
 
failure:
    /*
     * This unhashes the socket and releases the local port,
     * if necessary.
     */
    tcp_set_state(sk, TCP_CLOSE);
    ip_rt_put(rt);
    sk->sk_route_caps = 0;
    inet->inet_dport = 0;
    return err;
}

  

该函数完成了路由查找,得到下一跳地址,并更新socket对象的下一跳地址,将socket对象的状态设置为TCP_SYN_SENT,如果没设置序号初值,则选定一个随机初值, 调用函数tcp_connect完成报文构建和发送。

tcp_connect的源码:

/* 构造并发送SYN段 */

int tcp_connect(struct sock *sk)

{

    struct tcp_sock *tp = tcp_sk(sk);

    struct sk_buff *buff;

 

    tcp_connect_init(sk);/* 初始化传输控制块中与连接相关的成员 */

 

    /* 为SYN段分配报文并进行初始化 */

    buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation);

    if (unlikely(buff == NULL))

        return -ENOBUFS;

 

    /* Reserve space for headers. */

    skb_reserve(buff, MAX_TCP_HEADER);

 

    TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN;

    TCP_ECN_send_syn(sk, tp, buff);

    TCP_SKB_CB(buff)->sacked = 0;

    skb_shinfo(buff)->tso_segs = 1;

    skb_shinfo(buff)->tso_size = 0;

    buff->csum = 0;

    TCP_SKB_CB(buff)->seq = tp->write_seq++;

    TCP_SKB_CB(buff)->end_seq = tp->write_seq;

    tp->snd_nxt = tp->write_seq;

    tp->pushed_seq = tp->write_seq;

    tcp_ca_init(tp);

 

    /* Send it off. */

    TCP_SKB_CB(buff)->when = tcp_time_stamp;

    tp->retrans_stamp = TCP_SKB_CB(buff)->when;

 

    /* 将报文添加到发送队列上 */

    __skb_queue_tail(&sk->sk_write_queue, buff);

    sk_charge_skb(sk, buff);

    tp->packets_out += tcp_skb_pcount(buff);

    /* 发送SYN段 */

    tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL));

    TCP_INC_STATS(TCP_MIB_ACTIVEOPENS);

 

    /* Timer for repeating the SYN until an answer. */

    /* 启动重传定时器 */

    tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto);

    return 0;

}

  tcp_connect()中又调用了tcp_transmit_skb函数:

/* 发送一个TCP报文 */

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb)

{

    if (skb != NULL) {

        struct inet_sock *inet = inet_sk(sk);

        struct tcp_sock *tp = tcp_sk(sk);

        struct tcp_skb_cb *tcb = TCP_SKB_CB(skb);

        int tcp_header_size = tp->tcp_header_len;

        struct tcphdr *th;

        int sysctl_flags;

        int err;

 

        BUG_ON(!tcp_skb_pcount(skb));

 

#define SYSCTL_FLAG_TSTAMPS 0x1

#define SYSCTL_FLAG_WSCALE  0x2

#define SYSCTL_FLAG_SACK    0x4

 

        sysctl_flags = 0;/* 标识TCP选项 */

        /* 根据TCP选项调整TCP首部长度 */

        if (tcb->flags & TCPCB_FLAG_SYN) {/* 如果当前段是SYN段,需要特殊处理一下 */

            /* SYN段必须通告MSS,因此报头加上MSS通告选项的长度 */

            tcp_header_size = sizeof(struct tcphdr) + TCPOLEN_MSS;

            if(sysctl_tcp_timestamps) {/* 启用了时间戳 */

                /* 报头加上时间戳标志 */

                tcp_header_size += TCPOLEN_TSTAMP_ALIGNED;

                sysctl_flags |= SYSCTL_FLAG_TSTAMPS;

            }

            if(sysctl_tcp_window_scaling) {/* 处理窗口扩大因子选项 */

                tcp_header_size += TCPOLEN_WSCALE_ALIGNED;

                sysctl_flags |= SYSCTL_FLAG_WSCALE;

            }

            if(sysctl_tcp_sack) {/* 处理SACK选项 */

                sysctl_flags |= SYSCTL_FLAG_SACK;

                if(!(sysctl_flags & SYSCTL_FLAG_TSTAMPS))

                    tcp_header_size += TCPOLEN_SACKPERM_ALIGNED;

            }

        } else if (tp->rx_opt.eff_sacks) {/* 非SYN段,但是有SACK块 */

            /* 根据SACK块数调整TCP首部长度 */

            tcp_header_size += (TCPOLEN_SACK_BASE_ALIGNED +

                        (tp->rx_opt.eff_sacks * TCPOLEN_SACK_PERBLOCK));

        }

 

        if (tcp_is_vegas(tp) && tcp_packets_in_flight(tp) == 0)

            tcp_vegas_enable(tp);

 

        /* 在报文首部中加入TCP首部 */

        th = (struct tcphdr *) skb_push(skb, tcp_header_size);

        /* 更新TCP首部指针 */

        skb->h.th = th;

        /* 设置报文的传输控制块 */

        skb_set_owner_w(skb, sk);

 

        /* Build TCP header and checksum it. */

        /* 填充TCP首部中的数据 */

        th->source      = inet->sport;

        th->dest        = inet->dport;

        th->seq         = htonl(tcb->seq);

        th->ack_seq     = htonl(tp->rcv_nxt);

        *(((__u16 *)th) + 6)    = htons(((tcp_header_size >> 2) << 12) | tcb->flags);

        /* 设置TCP首部的接收窗口 */

        if (tcb->flags & TCPCB_FLAG_SYN) {

            th->window  = htons(tp->rcv_wnd);/* 对SYN段来说,接收窗口初始值为rcv_wnd */

        } else {

            /* 对其他段来说,调用tcp_select_window计算当前接收窗口的大小 */

            th->window  = htons(tcp_select_window(sk));

        }

        /* 初始化校验码和带外数据指针 */

        th->check       = 0;

        th->urg_ptr     = 0;

 

        if (tp->urg_mode &&/* 发送时设置了紧急方式 */

            between(tp->snd_up, tcb->seq+1, tcb->seq+0xFFFF)) {/*

紧急指针在报文序号开始的65535范围内 */

            /* 设置紧急指针和带外数据标志位 */

            th->urg_ptr     = htons(tp->snd_up-tcb->seq);

            th->urg         = 1;

        }

 

        /* 开始构建TCP首部选项 */

        if (tcb->flags & TCPCB_FLAG_SYN) {

            /* 调用tcp_syn_build_options构建SYN段的首部 */

            tcp_syn_build_options((__u32 *)(th + 1),

                          tcp_advertise_mss(sk),

                          (sysctl_flags & SYSCTL_FLAG_TSTAMPS),

                          (sysctl_flags & SYSCTL_FLAG_SACK),

                          (sysctl_flags & SYSCTL_FLAG_WSCALE),

                          tp->rx_opt.rcv_wscale,

                          tcb->when,

                              tp->rx_opt.ts_recent);

        } else {

            /* 构建普通段的首部 */

            tcp_build_and_update_options((__u32 *)(th + 1),

                             tp, tcb->when);

 

            TCP_ECN_send(sk, tp, skb, tcp_header_size);

        }

        /* 计算传输层的校验和 */

        tp->af_specific->send_check(sk, th, skb->len, skb);

 

        /* 如果发送的段有ACK标志,则通知延时确认模块,递减快速发送ACK

段的数量,同时停止延时确认定时器 */

        if (tcb->flags & TCPCB_FLAG_ACK)

            tcp_event_ack_sent(sk);

 

        if (skb->len != tcp_header_size)/*

发送的段有负载,则检测拥塞窗口闲置是否超时 */

            tcp_event_data_sent(tp, skb, sk);

 

        TCP_INC_STATS(TCP_MIB_OUTSEGS);

 

        /* 调用IP层的发送函数发送报文 */

        err = tp->af_specific->queue_xmit(skb, 0);

        if (err <= 0)

            return err;

 

        /* 如果发送失败,则类似于接收到显式拥塞通知的处理 */

        tcp_enter_cwr(tp);

 

        return err == NET_XMIT_CN ? 0 : err;

    }

    return -ENOBUFS;

#undef SYSCTL_FLAG_TSTAMPS

#undef SYSCTL_FLAG_WSCALE

#undef SYSCTL_FLAG_SACK

}

  

原文地址:https://www.cnblogs.com/sovegetabable/p/12104494.html