在上一次实验中,我们探究了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。
3、TCP通信
3.1 TCP简介
传输控制协议(TCP,Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的传输协议。
3.2 TCP特点
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的三次握手就已经完成了,然后就可以开始进行端与端之间的信息传递。