首先了解下网络套接字(socket)的概念,可以把它简单理解为 TCP 网络层中应用层和传输层之间的一个抽象层:
客户端和服务端建立抽象的网络连接时,TCP/IP 层需要做很多操作,如各种报文,消息头以及消息结构的封装。
而 socket 把这些复杂的操作,抽象成了几个简单的接口,供应用层来调用以实现进程在网络中通信。
TCP/IP 只是一个抽象的协议栈,网络连接时要具体实现,同时还得对外提供具体的接口,这就是 socket 接口。当一个请求连接到服务端时,可以把这个连接看作是 socket 节点连接。
举个例子,模拟一个服务器处理 3 个 socket 连接,在没有 IO 多路复用之前,我们的客户端与服务端是这么进行连接的:
- 一次只能连接 1 个 socket
- 永久阻塞直到 socket 连接有数据可读
不难发现,如果一个服务器有数以万计的请求时,处理效率将非常低下。
这时,IO 多路复用登场了,首先给出一个故事来帮助我们理解什么是 IO 多路复用:假设你是一个老师,让 30 个学生解答一道题目,然后检查学生做的是否正确,你有下面三个选择:
第一种选择:学生开始做题之前,由自己判断可能会先做完题目,然后举手。你负责检查先举手的学生,等着这个学生把题目做完,中间哪也不去,直到这个学生完成题目后,再检查下一位举手的学生。这时,如果有一位学生解答不出来,全班都会被耽误。这就是没有 IO 多路复用的情况。
第二种选择:你创建 30 个分身,每个分身检查一个学生的答案是否正确。这种类似于为每一个用户创建一个进程或者线程处理连接。这种方式看起来效率也很高,但连接数很多时,频繁创建进程或线程,资源十分有限。
第三种选择,你站在讲台上等,谁解答完谁举手。这时 C、D 举手,表示他们解答问题完毕,你下去依次检查 C、D 的答案,然后继续回到讲台上等。此时 E、A 又举手,然后去处理 E 和 A…
这就是 IO 复用模型,Linux下的select、poll 和 epoll 就是这样做的。比如,epoll 实现时,首先将用户 socket 对应的文件描述符(file descriptor,简称 fd)注册进 epoll,然后 epoll 帮你监听哪些 socket 上有消息到达,这样就避免了大量的无用操作。
此时的 socket 应该采用非阻塞模式,即收发客户消息不会阻塞(可以理解为大部分时间下,老师不再监听某个特定的学生做作业)。这样,整个过程只在调用 select、poll、epoll 这些调用的时候才会阻塞,整个进程或者线程就被充分利用起来。