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

            在上一次实验中,我们探究了socket底层API的具体功能以及具体调用过程,简单分析了replyhi/hello这个通讯过程,并且我们已经分析得出,这个过程是一个基于TCP协议的通信过程,本次实验我们将具体分析TCP协议以及相关源码,并深入理解TCP协议connect及bind、listen、accept背后的三次握手。本次实验是在Ubuntu18.0.4下基于 5.0.1内核和64位的MenuOS进行的。

    1、网络中进程之间如何通信?

           本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:

           消息传递(管道、FIFO、消息队列)
           同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
           共享内存(匿名的和具名的)
           远程过程调用(Solaris门和Sun RPC)

           那么在网络中进程之间如何通信?在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

           使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX  BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,因此可以说“一切皆socket”。

    2、什么是Socket?

            socket起源于Unix,而Unix/Linux基本理念之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。因此我们可以认为socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

           网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

      建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;socket是发动机,提供了网络通信的能力。
           socket通信过程如下:
                                       
     

    3、TCP通信

    3.1 TCP简介

        传输控制协议(TCP,Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的传输协议。

          互联网络与单个网络有很大的不同,因为互联网络的不同部分可能有截然不同的拓扑结构、带宽、延迟、数据包大小和其他参数。TCP的设计目标是能够动态地适应互联网络的这些特性,而且具备面对各种故障时的健壮性。 不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。 
           应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
            每台支持TCP的机器都有一个TCP传输实体。TCP实体可以是一个库过程、一个用户进程,或者内核的一部分。在所有这些情形下,它管理TCP流,以及与IP层之间的接口。TCP传输实体接受本地进程的用户数据流,将它们分割成不超过64KB(实际上去掉IP和TCP头,通常不超过1460数据字节)的分段,每个分段以单独的IP数据报形式发送。当包含TCP数据的数据报到达一台机器时,它们被递交给TCP传输实体,TCP传输实体重构出原始的字节流。为简化起见,我们有时候仅仅用“TCP”来代表TCP传输实体(一段软件)或者TCP协议(一组规则)。根据上下文语义你应该能很消楚地推断出其实际含义。例如,在“用户将数据交给TCP”这句话中,很显然这里指的是TCP传输实体。 
           IP层并不保证数据报一定被正确地递交到接收方,也不指示数据报的发送速度有多快。正是TCP负责既要足够快地发送数据报,以便使用网络容量,但又不能引起网络拥塞:而且,TCP超时后,要重传没有递交的数据报。即使被正确递交的数据报,也可能存在错序的问题,这也是TCP的责任,它必须把接收到的数据报重新装配成正确的顺序。简而言之,TCP必须提供可靠性的良好性能,这正是大多数用户所期望的而IP又没有提供的功能。 

    3.2 TCP特点

    TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式: [1] 
    (1)基于流的方式;
    (2)面向连接;
    (3)可靠通信方式;
    (4)在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销;
    (5)通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。

    3.3 TCP通信过程

         过程的文字描述如下:

    1、服务器初始化——LISTEN
    (1)调用socket函数创建文件描述符。
    (2)调用bind函数将当前的文件描述符和ip/port绑定在一起。如果这个端口已经被其他进程占用了,就会bind失败。
    (3)调用listen函数声明当前这个文件描述符作为一个服务器的文件描述符,为accept做好准备。
    (4)调用accept函数阻塞等待客户端连接起来。

    2、建立连接的过程——三次握手
         第一次:调用connect函数发出SYN段向服务器发起连接请求,并阻塞等待服务器应答。
         第二次:服务器收到客户端的SYN段后,会应答一个SYN-ACK段表示“同一建立连接”。
         第三次:服务器端收到SYN-ACK后会从connect函数中返回,同时应答一个ACK段。

    3、数据传输的过程
          建立连接后,TCP协议提供全双工的通信服务。所谓全双工,意思是:在同一条链路中的同一时刻,通信双方可以同时写数据。

    相对的概念叫做半双工,即:在同一条链路中的同一时刻,只能由一方来写数据。

    (1)服务器从accept函数返回后立刻调用read函数读socket里的数据。读socket就像读管道一样,如果没有数据到达就阻塞等待。
    (2)客户端调用write函数发送请求给服务器,服务器收到后就向客户端回复ACK,并从read函数中返回,对客户端的请求进行处理。

      在此期间客户端调用read函数阻塞等待服务器的应答。
    (3)服务器调用write函数将处理结果发回客户端,客户端收到后就回复ACK。服务器再次调用read函数阻塞等待下一条请求,。
    (4)客户端从read函数中返回,并发送下一条请求,如此循环下去。

    4、断开连接的过程——四次挥手
          第一次:如果客户端没有更多的请求就调用close函数关闭连接,客户端会向服务器端发送FIN。
          第二次:服务器收到FIN后会回应一个ACK,同时read返回0。
          第三次:直到所有报文发送完,服务端向服务器端发送FIN.

          第四次:客户端收到FIN后,再返回一个ACK给服务器。

          至此TCP通信过程就完成了,而本次实验我们将重点研究TCP socket通信的连接过程

    3.4 TCP socket通信的连接

          TCP通过三次握手建立连接,大致流程如下:

              客户端向服务器发送一个SYN J

             服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1

             客户端再想服务器发一个确认ACK K+1

             至此,就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

               

           从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态,因此三次握手其实是客户端通过connect函数发起的,客户端调用connect函数不会立即返回,只有当三次握手成功完成后connect函数才会返回;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

    3.5 MenuOS中验证三次握手过程

            运行qemu:进入menu文件夹下,打开MenuOS

    #输入命令
    make rootfs

    #重新打开一个终端
    cd LinuxKernel
    cd linux-5.0.1
    gdb
    file ./vmlinux
    target remote:1234
    #设置断点
    b __sys_socket
    b __sys_bind
    b __sys_connect
    b __sys_listen
    b __sys_accept4

            然后一直按c,直到完成一次replyhi/hello的过程

         从上图中可以看到,捕捉到的断点序列为1,2,4,5,1,3分别对应着socket,bind,listen,accept,socket,connect。至此已经验证了我们之前分析的TCP三次握手在底层API上的实现过程,接下来我们具体分析每个过程完成的功能。我们发现socket,bind,listen,accept,connect都在socket.c文件中,我们来逐一分析一下源码

       (1)sokect函数

    //socket参数意义分别为 family:即协议族,type:指定socket类型,protocol:故名思义,就是指定协议
    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; //sock_map_fd就是一个用于通信的套接字文件描述符,这个套接字描述符可以作为稍后bind()函数的绑定对象
    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描述符,它唯一

    标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

    当我们调用socket函数创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个

    具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

    (2)bind函数

    //bind的参数分别为 fd:即socket描述字,umyaaddr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址,
    //addrlen:对应的是地址的长度
    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; } SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen) { return __sys_bind(fd, umyaddr, addrlen); }

         bind()函数把一个地址族中的特定地址赋给socket。

         通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),调用bind函数时将IP端口绑定到套接字上,也就是我们后面显示的通信IP地址。

     (3)listen函数

    //listen函数的参数分别为 fd:socket描述字,backlog:socket可以排队的最大连接个数
    int
    __sys_listen(int fd, int backlog) { struct socket *sock; 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); }

           服务器在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。listen函数将主动的socket变为被动类型的,等待客户的连接请求。

    (4)accept函数

    //accept函数的参数分别为 fd:socket描述字,upeer_sockaddr:指向struct sockaddr *的指针,用于返回客户端的协议地址,
    //upeer_addrlen:协议地址的长度,flag为0时accept和accept4效果相同
    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;
    
        /*
         * We don't need try_module_get here, as the listening socket (sock)
         * has the protocol module (sock->ops->owner) held.
         */
        __module_get(newsock->ops->owner);
    
        newfd = get_unused_fd_flags(flags);
        if (unlikely(newfd < 0)) {
            err = newfd;
            sock_release(newsock);
            goto out_put;
        }
        newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
        if (IS_ERR(newfile)) {
            err = PTR_ERR(newfile);
            put_unused_fd(newfd);
            goto out_put;
        }
    
        err = security_socket_accept(sock, newsock);
        if (err)
            goto out_fd;
    
        err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
        if (err < 0)
            goto out_fd;
    
        if (upeer_sockaddr) {
            len = newsock->ops->getname(newsock,
                        (struct sockaddr *)&address, 2);
            if (len < 0) {
                err = -ECONNABORTED;
                goto out_fd;
            }
            err = move_addr_to_user(&address,
                        len, upeer_sockaddr, upeer_addrlen);
            if (err < 0)
                goto out_fd;
        }
    
        /* File flags are not inherited via accept() unlike another OSes. */
    
        fd_install(newfd, newfile);
        err = newfd;
    
    out_put:
        fput_light(sock->file, fput_needed);
    out:
        return err;
    out_fd:
        fput(newfile);
        put_unused_fd(newfd);
        goto out_put;
    }
    
    SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
            int __user *, upeer_addrlen, int, flags)
    {
        return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, flags);
    }
    
    SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
            int __user *, upeer_addrlen)
    {
        return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
    }

         如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

    (5)connect函数

    
    
    //connect函数参数分别为 fd:客户端的socket描述字;uservaddr:服务器的socket地址addrlen:socket地址的长度。
    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函数来建立与TCP服务器的连接。

           

          以上五个系统调用函数阐述了TCP协议建立连接的过程。因此,TCP的三次握手可以总结如下:

          1.服务端的socket初始化。

          2.服务端进行bind进行端口绑定,并设置监听函数listen()监听来自客户端的连接请求。

          3.客户端进行socket初始化。

          4.客户端发出connect请求,connect阻塞。

          5.服务端同意连接之后执行accept()函数,此时accept阻塞,服务端发送回应信息给客户端。

          6.客户端收到服务端的回应信息之后,完成connect,发送回应信息给服务端。

          7.服务端accept()执行完成。

           至此,TCP的三次握手就已经完成了,然后就可以开始进行端与端之间的信息传递。

  • 相关阅读:
    Django 之memcached的应用
    Django 之验证和授权
    Django 之安全篇
    Django 之上下文处理器和中间件
    博客都在标签里。
    kubernetes下rook-ceph部署
    Istio部署
    推荐一个学习k8s网站
    今天发生了一件事。。
    推荐书单,电影等
  • 原文地址:https://www.cnblogs.com/ustc-kunkun/p/12101636.html
Copyright © 2011-2022 走看看