zoukankan      html  css  js  c++  java
  • 深入理解TCP协议及其源代码

    一.tcp的三次握手分析:

                                                                       

     第一次握手:建立接时客户端发送 SYN 包(tcp协议中syn位置1,序号为J)到服务器,并进入 SYN_SEND 状态,等待服务器确认; 

     第二次握手:服务器收到 SYN 包,必须确认客户的 SYN,同时自己也发送一个 SYN 包,即 SYN+ACK包(tcp协议中syn位置1,ack位置1,序号K,确定序号为J+1),此时服务器进入 SYN_RCVD 状态; 

     第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(tcp协议中ack位置1,确认序号K+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

    具体状态转换图如下:

                                                        

    上图中A/B(A代表收到的消息,B代表发出的消息),沿着黑虚线是服务器端经过的状态,沿着黑实线是客户端的状态,红线是在三次握手的过程中出现其他状况是状态的相应的转换。

    二.下面对在三次握手中所使用的具体函数进行分析,由上次实验我们知道了在三次握手中使用了socket、connect、listen、bind、accept等函数,本次实验我们看看这些函数具体如何调用其他函数完成相应的连接。

    对于服务器端的流程——类似于接电话过程:

    socket()[找到一个可以通话的手机]----->bind()[插入一个固定号码]------>listen()[随时准备接听]-------> accept------->recv()------->send()------>close();

    对于客户端的主要流程----类似于打电话过程:

    socket()----->connect()------>recv/read/send------>close()

    流程如下图所示:

                                                              

    下面对具体函数分析:

    在服务端:

    1,socket:

    socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

    当服务器程序调用socket系统调用之后,内核会创建一个struct socket和一个struct sock结构,两者可以通过指针成员变量相互访问对方。内核直接操作的是struct sock结构。struct socket的存在是为了适应linux的虚拟文件系统,把socket也当作一个文件系统,通过指定superblock中不同的操作函数实现完成相应的功能。在linux内核中存在不同的sock类型,与TCP相关的有struct sock、 struct inet_connection_sock,、struct tcp_sock等。这些结构的实现非常灵活,可以相互进行类型转换。这个机制的实现是结构体的一层层包含关系:struct tcp_sock的第一个成员变量是struct inet_connection_sock,struct inet_connection_sock的第一个成员变量是struct sock。

    查找socket源码如下:

    int __sys_socket(int family, int type, int protocol)
    {
        int retval;
        struct socket *sock;
        int flags;
    
        /* Check the SOCK_* constants for consistency.  */
        BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
        BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
        BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
        BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
    
        flags = type & ~SOCK_TYPE_MASK;
        if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
            return -EINVAL;
        type &= SOCK_TYPE_MASK;
    
        if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
            flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
    
        retval = sock_create(family, type, protocol, &sock);
        if (retval < 0)
            return retval;
    
        return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    }
    
    SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
    {
        return __sys_socket(family, type, protocol);
    }

    当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

    struct socket源码如下:

    struct socket {
        socket_state        state;       // 连接状态:SS_CONNECTING, SS_CONNECTED 等
        short           type;            // 类型:SOCK_STREAM, SOCK_DGRAM 等
        unsigned long       flags;       // 标志位:SOCK_ASYNC_NOSPACE(发送队列是否已满)等
        struct socket_wq __rcu  *wq;     // 等待队列
        struct file     *file;           // 该socket结构体对应VFS中的file指针
        struct sock     *sk;             // socket网络层表示,真正处理网络协议的地方
        const struct proto_ops  *ops;    // socket操作函数集:bind, connect, accept 等
    };

    bind()、connect()使用socket创建的文件描述符来找到socket结构体,再进一步找到sock结构体,从而进行端口的绑定。

    2,bind()函数

    /*
     *    Bind a name to a socket. Nothing much to do here since it's
     *    the protocol's responsibility to handle the local address.
     *
     *    We move the socket address to kernel space before we call
     *    the protocol layer (having also checked the address is ok).
     */
    
    int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
    {
        struct socket *sock;
        struct sockaddr_storage address;
        int err, fput_needed;
    
        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (sock) {
            err = move_addr_to_kernel(umyaddr, addrlen, &address);
            if (!err) {
                err = security_socket_bind(sock,
                               (struct sockaddr *)&address,
                               addrlen);
                if (!err)
                    err = sock->ops->bind(sock,
                                  (struct sockaddr *)
                                  &address, addrlen);
            }
            fput_light(sock->file, fput_needed);
        }
        return err;
    }

    创建完socket之后就是地址的绑定了,通过bind系统调用实现。该调用通过传递进来的文件描述符找到对应的socket结构,然后通过socket访问sock结构。操作sock进行地址的绑定。如果指定了端口检查端口的可用性并绑定,否则随机分配一个端口进行绑定。但是怎样获知当前系统的端口绑定状态呢?通过一个全局变量inet_hashinfo进行,每次成功绑定一个端口会都将该sock加入到inet_hashinfo的绑定散列表中。加入之后bind的系统调用已基本完成了。

    函数的三个参数分别为:

    fd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。

    umyaddr:一个 struct sockaddr _user*指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,

    addrlen:对应的是地址的长度

    3.listen()

    listen()源码如下

       int err, fput_needed;
        int somaxconn;
    
        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (sock) {
            somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
            if ((unsigned int)backlog > somaxconn)
                backlog = somaxconn;
    
            err = security_socket_listen(sock, backlog);
            if (!err)
                err = sock->ops->listen(sock, backlog);
    
            fput_light(sock->file, fput_needed);
        }
        return err;
    }
    
    SYSCALL_DEFINE2(listen, int, fd, int, backlog)
    {
        return __sys_listen(fd, backlog);
    }
                                    

    同样的内核通过文件描述符找到对应的sock,然后将其转换为inet_connection_sock结构。在inet_connection_sock结构体中含有一个类型为request_sock_queue的icsk_accept_queue变量,存储一些希望建立连接的sock相关的信息。

    结构如下;

    struct request_sock_queue {
        struct request_sock *rskq_accept_head;
        struct request_sock *rskq_accept_tail;
        rwlock_t             syn_wait_lock;
        u8                    rskq_defer_accept;
        struct listen_sock     *listen_opt;
    };

    listen_opt用了存储当前正在请求建立连接的sock,称作半连接状态,当内核收到一个带有skb之后需找到对应的sock。该过程通过__inet_lookup_skb进行实现。该函数主要调用__inet_lookup,其中:首先看看这个包是不是一个已经建立好连接的sock中的包,通过__inet_lookup_established函数进行操作,失败的话可能是一个新的SYN数据包,此时还没有建立连接所以没有对应的sock,和该sock相关的只可能是监听sock了。

    所以通过__inet_lookup_listener函数找到在本地的监听对应端口的sock。

    无论哪种情况,找到sock之后便会将sock和skb一同传入tcp_v4_do_rcv函数作统一处理:

    if (sk->sk_state == TCP_ESTABLISHED) {
        sock_rps_save_rxhash(sk, skb->rxhash);
        TCP_CHECK_TIMER(sk);
        if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
            rsk = sk;
            goto reset;
        }
        TCP_CHECK_TIMER(sk);
        return 0;
    }
    if (sk->sk_state == TCP_LISTEN) {
        struct sock *nsk = tcp_v4_hnd_req(sk, skb);
        if (!nsk)
            goto discard;
     
        if (nsk != sk) {
            if (tcp_child_process(sk, nsk, skb)) {
                rsk = nsk;
                goto reset;
            }
            return 0;
        }
    }
    if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
            rsk = sk;
            goto reset;

    如果是一个已建立连接的sock,调用tcp_rcv_established函数进行相应的处理。

    如果是一个正在监听的sock,需要新建一个sock来保存这个半连接请求,该操作通过tcp_v4_hnd_req实现。
    这里我们只关注tcp的建立过程,所以只分析tcp_v4_hnd_req和tcp_child_process函数:

    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;
     
        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);
     
        nsk = inet_lookup_established(sock_net(sk), &tcp_hashinfo, iph->saddr,
                th->source, iph->daddr, th->dest, inet_iif(skb));
     
        if (nsk) {
            if (nsk->sk_state != TCP_TIME_WAIT) {
                bh_lock_sock(nsk);
                return nsk;
            }
            inet_twsk_put(inet_twsk(nsk));
            return NULL;
        }
     
        return sk;
    }

    通过tcp_v4_hnd_req函数就能够找到或创建和这个skb相关的sock。

     A.如果返回的sock和处于Listen状态的sock不同,表示返回的是一个新的sock,第三次握手已经完成了。调用tcp_child_process处理。该函数的逻辑是让这个新的tcp_sock开始处理TCP段,同时唤醒应用层调用accept阻塞的程序,告知它有新的请求建立完成,可以从全连接队列中取出了。

    B.如果返回的sock没有变化,表示是一个新的请求,调用tcp_rcv_state_process函数处理第一次连接的情况。该函数的逻辑较为复杂,简单的可以概括为新建一个request_sock并插入半连接队列,设置该request_sock的sock为SYN_RCV状态。然后构建SYN+ACK发送给客户端,完成TCP三次握手连接的第二步。

    4.accept()

    源码如下:

    /*
     *    For accept, we attempt to create a new socket, set up the link
     *    with the client, wake up the client, then return the new
     *    connected fd. We collect the address of the connector in kernel
     *    space and move it to user at the very end. This is unclean because
     *    we open the socket then return an error.
     *
     *    1003.1g adds the ability to recvmsg() to query connection pending
     *    status to recvmsg. We need to add that support in a way thats
     *    clean when we restructure accept also.
     */
    
    int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,
              int __user *upeer_addrlen, int flags)
    {
        struct socket *sock, *newsock;
        struct file *newfile;
        int err, len, newfd, fput_needed;
        struct sockaddr_storage address;
    
        if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
            return -EINVAL;
    
        if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
            flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
    
        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (!sock)
            goto out;
    
        err = -ENFILE;
        newsock = sock_alloc();
        if (!newsock)
            goto out_put;
    
        newsock->type = sock->type;
        newsock->ops = sock->ops;

    该调用创建新的struct socket表示新的连接,搜寻全连接队列,如果队列为空,将程序自身挂起等待连接请求的完成。否则从队列中取出头部request_sock,并设置新的struct socket和request_sock中的struct sock的对应关系。这样一个连接至此就建立完成了。客户端可以通过新返回的struct socket进行通信,同时旧的struct socket继续在监听。

    客户端

    1,socket()

    socket的实现和服务器端一样

    2,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);
    

    connect系统调用根据文件描述符找到socket和sock,如果当前socket的状态是SS_UNCONNECTED的情况下才正常处理连接请求。首先调用tcp协议簇的connect函数(即tcp_v4_connect)发送SYN,然后将socket状态置为SS_CONNECTING,将进程阻塞等待连接的完成。剩下的两次握手由协议栈自动完成。

    tcp_v4_connect函数:

    该函数首先进行一些合法性验证,随后调用ip_route_connect函数查找路由信息,将当前sock置为SYN_SENT状态,然后调用inet_hash_connect函数绑定本地的端口,和服务器端绑定端口的过程类似,但是会额外的将sock添加inet_hashinfo中的ehash散列表中(添加到这的原因是因为希望以后收到的SYN+ACK时能够找到对应的sock,虽然当前并没有真正意义上的建立连接)。到最后调用tcp_connect构建SYN数据包发送。

    tcp_connect函数:

    该函数逻辑比较简单,构造SYN数据段并设置相应的字段,将该段添加到发送队列上调用tcp_transmit_skb发送skb,最后重设重传定时器以便重传SYN数据段。

     当客户端收到SYN+ACK之后,首先会通过tcp_v4_rcv从已建立连接散列表中找到对应的sock,然后调用tcp_v4_do_rcv函数进行处理,在该函数中主要的执行过程是调用tcp_rcv_state_process。又回到了tcp_rcv_state_process函数,它处理sock不处于ESTABLISHED和LISTEN的所有情况。当发现是一个SYN+AC段并且当前sock处于SYN_SENT状态时,表示连接建立马上要完成了。首先初始化TCP连接中一些需要的信息,如窗口大小,MSS,保活定时器等信息。然后给该sock的上层应用发送信号告知它连接建立已经完成,最后通过tcp_send_ack发送ACK完成最后一次握手。

     

     

     

     

  • 相关阅读:
    Flink 状态生命周期
    jpa使用@CollectionTable创建表
    Java的四种引用---强软弱虚
    ThreadLocal与内存泄露
    Flink 1.11 Table API 实现kafka到mysql
    FLIink 1.11 SQL 构建一个端到端的流式应用
    Flink1.11编译
    Flink运行yarn-session报错 java.lang.NoClassDefFoundError: org/apache/hadoop/yarn/exceptions/YarnException
    欢迎订阅AI科技导读微信公众号,获取人工智能的最新技术解读教程!
    深度学习深刻理解和应用--人工智能从业人员必看知识
  • 原文地址:https://www.cnblogs.com/wwwxuexi/p/12101956.html
Copyright © 2011-2022 走看看