1.IO 多路转接 (复用)
IO 多路转接也称为 IO 多路复用,它是一种网络通信的手段(机制),通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪( 可以读数据或者可以写数据)程序的阻塞就会被解除,之后就可以基于这些(一个或多个)就绪的文件描述符进行通信了。
通过这种方式在单线程 / 进程的场景下也可以在服务器端实现并发。常见的 IO 多路转接方式有:select、poll、epoll。
下面先对多线程 / 多进程并发和 IO 多路转接的并发处理流程进行对比(服务器端):
- 多线程 / 多进程并发
- 主线程 / 父进程:调用 accept() 监测客户端连接请求
- 如果没有新的客户端的连接请求,当前线程 / 进程会阻塞、
- 如果有新的客户端连接请求解除阻塞,建立连接
- 子线程/子进程:建立连接的客户端通信
- 调用 read() / recv() 接收客户端发送的通信数据,如果没有通信数据,当前线程 / 进程会阻塞,数据到达之后阻塞自动解除
- 调用 write() / send() 给客户端发送数据,如果写缓冲区已满,当前线程 / 进程会阻塞,否则将待发送数据写入写缓冲区中
- 主线程 / 父进程:调用 accept() 监测客户端连接请求
- IO 多路转接并发
- 使用 IO 多路转接函数委托内核检测服务器端所有的文件描述符(通信和监听两类),这个检测过程会导致进程 / 线程的阻塞,如果检测到已就绪的文件描述符阻塞解除,并将这些已就绪的文件描述符传出
- 根据类型对传出的所有已就绪文件描述符进行判断,并做出不同的处理
- 监听的文件描述符:和客户端建立连接
- 此时调用 accept() 是不会导致程序阻塞的,因为监听的文件描述符是已就绪的(有新请求)
- 通信的文件描述符:调用通信函数和已建立连接的客户端通信
- 调用 read() / recv() 不会阻塞程序,因为通信的文件描述符是就绪的,读缓冲区内已有数据
- 调用 write() / send() 不会阻塞程序,因为通信的文件描述符是就绪的,写缓冲区不满,可以往里面写数据
- 监听的文件描述符:和客户端建立连接
- 对这些文件描述符继续进行下一轮的检测(循环往复。。。)
- 文件描述符对应的内存结构:
- 一个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区
- 读数据: 通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区
- 写数据: 通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区
- 监听的文件描述符:
- 客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中
- 读缓冲区中有数据,说明有新的客户端连接
- 调用 accept () 函数,这个函数会检测监听文件描述符的读缓冲区
- 检测不到数据,该函数阻塞
- 如果检测到数据,解除阻塞,新的连接建立
- 通信的文件描述符:
- 客户端和服务器端都有通信的文件描述符
- 发送数据:调用函数 write () /send (),数据进入到内核中
- 数据并没有被发送出去,而是将数据写入到了通信的文件描述符对应的写缓冲区中
- 内核检测到通信的文件描述符写缓冲区中有数据,内核会将数据发送到网络中
- 接收数据:调用的函数 read () /recv (), 从内核读数据
- 数据如何进入到内核程序猿不需要处理,数据进入到通信的文件描述符的读缓冲区中
- 数据进入到内核,必须使用通信的文件描述符,将数据从读缓冲区中读出即可