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

Posted 19chenjian

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解TCP协议及其源代码相关的知识,希望对你有一定的参考价值。

一.TCP协议

1.定义
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。

2.TCP协议的特点

TCP是在不可靠的IP层之上实现的可靠的数据传输协议,它主要解决传输的可靠、有序、无丢失和不重复的问题。TCP是TCP/IP体系中非常复杂的一个协议,主要特点有:

(1)TCP是面向连接的传输层协议。

(2)每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(一对一)。

(3)TCP提供可靠的交付服务,保证传送的数据无差错、不丢失、不重复且有序。

(4)TCP提供全双工通信,TCP允许通信双方的应用进程在任何时候都能发送数据,为此TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。

(5)TCP是面向字节流的,虽然应用程序和TCP的交互是一次一个数据块,但TCP把应用程序交下来的数据看成仅仅是一连串的无结构的字节流。

3.TCP报文段

TCP传送的数据单元称为报文段。一个TCP报文段分为TCP首部和TCP数据两部分,整个TCP段作为IP数据段的数据部分封装在IP数据报中,如下图1所示。其首部的前20个字节是固定的。TCP报文段的首部最短为20字节,后面有4N字节是根据需要而增加的选项,通常长度为4字节的整数倍。TCP报文段既可以用来运载数据,也可以用来建立连接、释放连接和应答。

技术图片

                            图1.TCP报文段的首部

 

4.TCP连接管理

TCP是面向连接的协议,因此每一个TCP连接都有三个阶段:连接建立、数据传送和连接释放。TCP连接的管理就是使运输连接的建立和释放都能正常运行。

TCP把连接作为最基本的抽象,每一条TCP连接有两个端点,TCP连接的端点不是主机,不是主机的IP地址,不是应用进程,也不是传输层的协议端口。TCP连接的端口叫做套接字(socket)或插口。端口拼接到IP地址即构成了套接字。

每一条TCP连接唯一地被通信两端的两个端点(即两个套接字)所确定。

TCP连接的建立采用客户/服务器方式。主动发起连接建立的应用进程叫做客户机(Client),而被动等待连接建立的应用进程叫做服务器(Server)。

4.1TCP连接的建立

连接的建立经历以下3个步骤,通常称为"三次握手”,如图2所示。

第一步:客户机的TCP首先向服务器的TCP发送一个连接请求报文段。这个特殊的报文段中不含应用层数据,其首部的SYN标志位被置为1。另外,客户机会随机选择一个起始序号seq=x。

第二步:服务器的TCP收到连接请求报文段后,如同意建立连接,就向客户机发回确认,并为该TCP连接分配TCP缓存和变量。在确认报文段中,SYN和ACK位都被置为1,确认号字段的值为x+1,并且服务器随机产生起始序号seq=y,确认报文段同样不含应用层数据。

第三步:当客户机收到确认报文段后,还要向服务器给出确认,并且要给该连接分配缓存和变量。这个报文段的ACK标志位被置为1,序号字段为x+1,确认号字段ack=y+1。

在成功进行了以上三步之后,TCP连接就建立了,接下来就可以传送应用层数据了。TCP提供的是全双工通信,因此通信双方的应用进程在任何时候都能发送数据。

 技术图片  

                    图2.用三次握手建立TCP连接

4.2TCP连接的释放

参与TCP连接的两个进程中的任何一个都能终止该连接,TCP连接释放的过程通常被称为"四次挥手”,如图3所示。

第一步:客户机打算关闭连接,就向其TCP发送一个连接释放报文段,并停止再发送数据,主动关闭TCP连接,该报文段的FIN标志位被置为1,seq=u,它等于前面已传送过的数据的最后一个字节的序号加1.TCP是全双工的,当发送FIN报文时,发送FIN的一端就不能再发送数据,也就是关闭了其中一条数据通路,但对方还可以发送数据。

第二步:服务器收到连接释放报文段后即发出确认,确认号是ack=u+1,而这个报文段自己的序号是v,等于前面已传送过的数据的最后一个字节的序号加1.此时,从客户机到服务器这个方向的连接就释放了,TCP连接处于半关闭状态。但服务器若发送数据,客户机仍要接收,即从服务器到客户机这个方向的连接并未关闭。

第三步:若服务器已经没有要向客户机发送的数据,就通知TCP释放连接,此时其发出FIN=1的连接释放报文段。

第四步:客户机收到连接释放报文段后,必须发出确认。在确认报文段中,ACK字段被置为1,确认号ack=w+1,序号seq=u+1。此时TCP连接还没有释放掉,必须经过时间等待计数器设置的时间2MSL后,A进入到连接关闭状态。

技术图片

             图3.用"四次挥手“释放TCP连接

 

二.TCP"三次握手"过程分析

TCP"三次握手"建立连接的过程中服务端会依次调用socket()、bind()、listen()、accept(),客户端会依次调用socket()、connect()。接下来先来对三次握手过程中涉及到的这几个函数调用的源码来进行下分析。

1.socket()函数

在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:

int socket(int af, int type, int protocol);

(1)af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,如 1030::C9B4:FF12:48AA:1A2B。

(2)type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。

(3)protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

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);
}

2.bind()函数

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。 在linux下bind() 函数的原型为:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);

其中sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

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);
}

3.listen()函数

 对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。

通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:

int listen(int sock, int backlog);

其中,sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。

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);
}

4.connect()函数

connect() 函数用来建立连接,它的原型为:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);

其中sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

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);
}

5.accept()函数

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为: 

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

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);
}

 

 接下来对TCP"三次握手”的过程进行下跟踪验证

首先进入linuxnet目录下,打开MenuOs

cd LinuxKernel
cd linuxnet
qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S

技术图片

打开一个新的终端

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
#查看断点信息
info breakpoints

技术图片

 

 按c,此时,需要在qemu中输入replyhi才能继续往下执行

技术图片

 

 继续按c,此时需要在qmenu中输入hello才能继续往下执行

技术图片

 

继续按c,至此,完成了一次通信过程

技术图片

 

 可以看到,TCP“三次握手”过程中依次涉及到的系统调用有socket、bind、listen、connect、accept。服务端调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

以上是关于深入理解TCP协议及其源代码的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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