深入理解TCP协议及其源代码

TCP三次握手过程

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

深入理解TCP协议及其源代码_第1张图片

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

(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack (number )=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之间可以开始传输数据了。

  以上是我们熟知的TCP的三次握手的过程,我们可以通过wireshark等工具来验证次过程,而我们主要想研究的是在底层socket的API层面上,TCP的三次握手到底调用了哪些API以及具体的过程到底是怎么样的。根据我们的分析,由于客户端主动发起连接,因此结合底层API我们分析出客户端和服务器端的连接过程如图所示:

深入理解TCP协议及其源代码_第2张图片

  TCP协议的双方分为主动打开和被动打开,从三次握手的角度讲,主动发起握手的一方属于主动打开;被动接受握手的一方属于被动打开。客户端属于主动打开,服务器端属于被动打开。API分为两类,一类客户端和服务器端都可以调用;另一类API独属于客户端或者服务器端。

客户端和服务器端都可以调用的API:socket(), bind(), send/write(),write()/recv(),close()

独属于客户端和服务器端的API:客户端:connect(),服务器端:listen(),accept()

  实际上当双方三次握手后正常通信时无需区分服务器端和客户端,服务器端可以向客户端发送数据,客户端也可以向服务器端发送数据,在这个阶段客户端也好服务器也罢没什么区别,唯一能区分服务器还是客户端其实是通过三次握手这个过程来实现的。当我们只关注connect、listen以及accept函数与三次握手的关系时,可以画出过程图如下:

深入理解TCP协议及其源代码_第3张图片

  

  从图中我们可以看到,三次握手其实是客户端通过connect函数发起的,客户端调用connect函数不会立即返回,只有当三次握手成功完成后connect函数才会返回。

  对于服务器server来说,调用listen仅仅是服务器告诉操作系统已经准备好了被动打开,也就是被动接受握手,当服务器端还没有执行listen函数时,客户端调用connect函数是不会成功返回的,原因很简单,connect函数的功能实际上是发起三次握手,但此时服务器端还没有准备好,因此三次握手不会成功,connect函数也不会成功返回。只有当服务器端调用listen函数后,服务器端才会做好准备来进行三次握手,这和调没调用accept函数没有任何关系。只要服务器端调用了listen函数,即使没有调用accept三次握手也可以成功。

  三次握手后服务器端和客户端成功建立起链接(准确讲是成功交换了彼此说话的起始序号),服务器和相应客户端的连接信息会被放到操作系统的等待队列中,等等,为什么要放入队列中呢?因为一个服务器可以和多个客户端建立连接,三次握手成功后需要维护这些客户端的连接信息,因此这些信息通常是操作系统用队列来维护的。那么队列中的这些连接数据什么时候会被取出来呢?这就是accept函数的作用了,服务器端调用accept函数后会从队列中取出一个已经成功三次握手的连接数据,此后双方就可以进行正常通信了。

  从这里我们也可以看出accept函数不会影响三次握手,但该函数能否很快返回是和三次握手有关的,当服务器端调用listen准备进行三次握手后假设还没有任何客户端同服务器端进行通信,这时服务器端调用accept函数是不会返回的,原因很简单,因为此时队列中还没有任何成功建立的连接,该情形就是上图所示,当第一个客户端同服务器端成功三次握手后队列中才会有连接信息,此时accept函数从队列取出该数据后才会返回。

  基于以上分析,connect、listen以及accept同三次握手有密切关系。connect函数用于发起三次握手因此只能被客户端使用。listen用于准备接受握手,accept函数用于取出成功进行三次握手的连接信息,因此这两个函数只能被服务器端使用。

menuos中验证三次握手过程

  与上次实验过程类似,我们重复和上次实验类似的过程,由于socket API在底层调用接口时通过socketcall这个函数来实现,我们同样对该函数打上断点,然后进行replyhi/hello的过程,然后观察调用API的过程并分析其细节。

  具体过程如下:

深入理解TCP协议及其源代码_第4张图片

  途中截取的为输入replyhi和hello指令之后的断点部分,但是还并不涉及recv和send两个过程,即只是三次握手的建立过程。

  我们根据返回的call值到源代码中查找这些sys_socketcall到底是在实现哪些功能,我们根据提示找到SYSCALL_DEFINE2所在的代码,在目录LinuxKernel/linux-5.0.1/net/socket.c中,下面是截取的SYSCALL_DEFINE2定义的部分。

复制代码
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    unsigned long a[AUDITSC_ARGS];
    unsigned long a0, a1;
    int err;
    unsigned int len;

    if (call < 1 || call > SYS_SENDMMSG)
        return -EINVAL;
    call = array_index_nospec(call, SYS_SENDMMSG + 1);

    len = nargs[call];
    if (len > sizeof(a))
        return -EINVAL;

    /* copy_from_user should be SMP safe. */
    if (copy_from_user(a, args, len))
        return -EFAULT;

    err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
    if (err)
        return err;

    a0 = a[0];
    a1 = a[1];

    switch (call) {
    case SYS_SOCKET:                                                      #call=1
        err = __sys_socket(a0, a1, a[2]);
        break;
    case SYS_BIND:                                                        #call=2
        err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_CONNECT:                                                     #call=3
        err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_LISTEN:                                                      #call=4
        err = __sys_listen(a0, a1);
        break;
    case SYS_ACCEPT:                                                      #call=5
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], 0);
        break;
    case SYS_GETSOCKNAME:                                                 #call=6
        err =
            __sys_getsockname(a0, (struct sockaddr __user *)a1,
                      (int __user *)a[2]);
        break;
    case SYS_GETPEERNAME:                                                 #call=7
        err =
            __sys_getpeername(a0, (struct sockaddr __user *)a1,
                      (int __user *)a[2]);
        break;
    case SYS_SOCKETPAIR:                                                  #call=8
        err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
        break;
    case SYS_SEND:                                                        #call=9
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   NULL, 0);
        break;
    case SYS_SENDTO:                                                      #call=10
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   (struct sockaddr __user *)a[4], a[5]);
        break;
    case SYS_RECV:                                                        #call=11
        err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     NULL, NULL);
        break;
    case SYS_RECVFROM:                                                    #call=12
        err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                     (struct sockaddr __user *)a[4],
                     (int __user *)a[5]);
        break;
    case SYS_SHUTDOWN:                                                    #call=13
        err = __sys_shutdown(a0, a1);
        break;
    case SYS_SETSOCKOPT:                                                  #call=14
        err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
                       a[4]);
        break;
    case SYS_GETSOCKOPT:                                                  #call=15
        err =
            __sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
                     (int __user *)a[4]);
        break;
    case SYS_SENDMSG:                                                     #call=16
        err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_SENDMMSG:                                                    #call=17
        err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
                     a[3], true);
        break;
    case SYS_RECVMSG:                                                     #call=18
        err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);
        break;
    case SYS_RECVMMSG:                                                    #call=19
        if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
            err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
                         a[2], a[3],
                         (struct __kernel_timespec __user *)a[4],
                         NULL);
        else
            err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
                         a[2], a[3], NULL,
                         (struct old_timespec32 __user *)a[4]);
        break;
    case SYS_ACCEPT4:                                                      #call=20
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], a[3]);
        break;
    default:
        err = -EINVAL;
        break;
    }
    return err;
}
复制代码

  我已在上图中标记出了call值对应的socket API,我们分别对应捕捉到的断点序列1,2,4,5,1,3分别对应着socket,bind,listen,accept,socket,connect。至此已经验证了我们之前分析的TCP三次握手在底层API上的实现过程,接下来我们具体分析每个过程完成的功能。

socket

  socket函数是一种可用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数,函数原型为int socket(int domain,int type, int protocol)。返回值:非负描述符 – 成功,-1 - 出错。

  其中:family指明了协议族/域,通常AF_INET、AF_INET6、AF_LOCAL等;type是套接口类型,主要SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW(原始socket);protocol一般取为0。成功时,返回一个小的非负整数值,与文件描述符类似。

  在我们截取的过程当中,包括两个socket函数的调用,其实也很好理解,分别是客户端和服务端的socket初始化。因为在TCP连接的过程中,实际上是套接字之间建立联系的过程,在连接建立之前,我们首先要先将套接字初始化完成,以方便后面进行连接。

bind

  bind函数原型为int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen)。其返回值:0 – 成功,-1 - 出错。

       当socket函数返回一个描述符时,只是存在于其协议族的空间中,并没有分配一个具体的协议地址(这里指IPv4/IPv6和端口号的组合),bind函数可以将一组固定的地址绑定到sockfd上。

  此时再看我们之前的流程图,bind函数是服务端完成的操作,该过程用于将IP端口绑定到套接字上,也就是我们后面显示的通信IP地址。

listen 

  此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:int PASCAL FAR listen(SOCKET s, int backlog);

  参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。

  此步骤也为服务端进行的操作,主要用于监听来自客户端的连接请求。

accept

  accept()的调用格式如下:SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);

  参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。

  accept()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。

connect

  connect()的调用格式如下:int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);

  参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。

  由于地址簇总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议簇相关。因此bind()和connect()无须协议作为参数。

 

  因此,TCP的三次握手总结如下:首先是服务器端的socket初始化,之后服务端进行bind进行端口绑定,并设置监听函数listen()监听来自客户端的连接请求,而客户端首先进行socket初始化,之后发出connect请求,connect阻塞;最后服务端同意连接之后执行accept()函数,此时accept阻塞,发回客户端回应信息之后,客户端的connect完成,发送回应信息给服务端,accept()执行完成,至此三次握手完成,开始进行双端之间的信息传递。

你可能感兴趣的:(深入理解TCP协议及其源代码)