Socket

Linux一切皆文件的概念,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

想要客户端和服务端在网络间通信则必须使用Socket,支持跨主机通信

在建立两端通信时,需要将客户端和服务端都创建一个Socket,像一条线通过Socket连接起两端主机,Socket创建时可以指定使用的网络协议,通常情况下是使用TCPUDP,而相对来说TCP的场景偏多一些

如何创建两端通信呢?首先,服务端调用 socket()函数,创建网络协议IPV4,传输协议为TCPSocket,接着调用 bind()函数给这个Socket绑定IP和端口,接着会调用 listen() 函数监听连接接入,通过 accept()函数等待连接连入,如无客户端连接则阻塞,客户端则在创建好 Socket 后调用 connect() 函数发起连接,指定服务端的IP及端口,然后通过TCP的三次握手后连接建立完成

这个连接过程中,监听和连接的Socket 实际上是两个Socket

T9V5Uf.png
T9V5Uf.png

通过上图可以发现,Socket本质上与文件传输很接近,而Linux一切皆文件的概念,Socket本质上也是文件,而上面这种Socket通信,是最简单的一对一通信,当服务端未处理完一个客户端的网络I/O时则会阻塞,而这个时候其他客户端是无法连接的,只能等待

多进程模型

基于上述的Socket阻塞传输I/O,衍生出了多进程模型,即为每个连接进来的客户端都分配一个进程去处理,阻塞也只阻塞当前进程,服务器主进程通过accept()监听客户端连接,当接收到连接后,调用系统fork()函数创建子进程,将父进程的一切内容复制到子进程中,这种复制更像是一种指针,通过子进程来继续与客户端通信,待子进程处理完后再返回给父进程,通过不同的返回值,子进程为 0

T9eyXd.png
T9eyXd.png

这种做法很容易出现垃圾无法回收的问题,子进程无法销毁,因此父进程可以调用wait()waitpid()来回收子进程

这种多进程模型的缺点在于,不断地fork子进程会造成大量的进程间上下文切换,而且进程并不足够轻量,每次创建进程则也需要创建对应的 堆栈.寄存器等一系列资源

多线程模型

通过上面的多进程模型理念可以看出,不够轻量会导致更多的负载,那么后续又衍生出了多线程模型的概念,既然进程不满足那么就从多线程去下手

通过线程池的概念,连接连接进来时,将已连接的Socket塞到Socket等待队列中,另一边从线程池不断地取出已连接的Socket,然后创建线程交给线程去处理

T9Ju7D.png
T9Ju7D.png

结合之前的文章,实际应用中,每个创建的线程创建后都需要加锁,避免资源竞争问题

而这种多线程的做法很大程度上提高了一些效率,也减少了上下文切换的资源消耗问题,但是当数量很大的请求过来时,维护一个超大数量的线程池,或者说给线程池加上限,这并不是一个很好的做法,而维护也更麻烦,调度可能就会宕机,调度宕机也就面临着不可预测的问题。

I/O多路复用

多路复用的概念则是从源头出发,这种一对一的方式显然不是最好的,那么就衍生出了 一个进程/线程 对应多个Socket的技术

一个进程同时只能处理一个Socket,但是如果将这个任务时间控制到毫秒微妙级时,将多个Socket都指向同一个进程则是多路复用,他与CPU的并发模型很接近,也被称为时分多路复用

I/O 复用其实复用的不是 I/O 连接,而是复用线程,让一个 thread of control 能够处理多个连接(I/O 事件)

T9YOs0.png
T9YOs0.png

select

将已连接的Socket放到一个文件描述集合(类指针)中,调用select函数将集合拷贝到内核中,内核遍历其中的Socket是否有事件产生,然后将其标记为可读,可写标识,将改过的集合送回用户态,然后用户态则继续遍历找到可读,可写Socket对其处理

select使用bitsmap标识文件描述集合,所支持的描述符个数有限制,在Linux中的FD_SETSIZE限制,默认最大值是1024,因此只能监听0~1023文件描述符

poll

poll与select很接近,只是不采用bitsmap存储,改用了链表,解除了限制

但由于结构未变,因此poll与select都有遍历两次循环的问题,且都需要复制两次集合的操作

epoll

epoll的诞生就是为了解决select/poll留下的问题,且目前epoll的应用场景也很多

关于存储文件描述符,epoll使用红黑树来跟踪进程所有待检测的文件描述符,将需要监控的 Socket 使用 epoll_ctl() 函数将其加入至内核中的红黑树中,这样检查 Socket 是否有事件发生则只需要传入一个待检测的 Socket 即可,不需要整个集合复制,减少了多次数据拷贝的问题

epoll 使用事件驱动,内核中维护一个链表来记录就绪事件,当 Socket 发生了事件时,内核会将其加入这个链表中,用户调用 epoll_wait() 时,只返回链表(事件发生的个数)即可,不需要整个集合返回

T9aLhF.png
T9aLhF.png

水平触发

实则上面的 select/poll/epoll 都支持水平触发,当Socket有可读事件发生时,服务端不断地苏醒,直到read函数执行完才停止

边缘触发

epoll独有的边缘触发,当 Socket 有可读事件发生时,服务端从 epoll_wait 中苏醒,并执行read读取数据,由于他只苏醒一次的概念,因此我们在读取时要一次性将数据读完

边缘触发是循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK

结语

多路复用 API 返回的事件并不一定可读写的,因此使用多路复用时最好配合 非阻塞 I/O一起使用,应对特殊情况