在套接字接口抽象下,网络 I/O 的出入口就是 Socket 的读和写,Socket 在操作系统接口中被抽象为数据流,网络 I/O 可以理解为对流的操作。
每一次网络访问,从远程主机返回的数据会先存放到操作系统内核的缓冲区中,然后内核的缓冲区复制到应用程序的地址空间,
所以当发生一次网络请求发生后,
将会按顺序经历
“等待数据从远程主机到达缓冲区” - 阻塞/非阻塞
和
“将数据从缓冲区拷贝到应用程序地址空间” - 同步/异步
两个阶段,
根据实现这两个阶段的不同方法,人们把网络 I/O 模型总结为两类、五种模型:
两类是指 同步 I/O 与 异步 I/O ,
五种是指在同步 IO 中又分有划分出 阻塞 I/O 、 非阻塞 I/O 、 多路复用 I/O 和 信号驱动 I/O 四种细分模型。
这里笔者先解释一下同步和异步、阻塞和非阻塞的概念。
同步是指调用端发出请求之后,得到结果之前必须一直等待,与之相对的就是异步,发出调用请求之后将立即返回,不会马上得到处理结果,结果将通过状态变化和回调来通知调用者。
阻塞和非阻塞是针对请求处理过程,指收到调用请求之后,返回结果之前,当前处理线程是否会被挂起。
这种概念上的叙述估计还是不太好理解的,笔者以“你如何领到盒饭”为情景,将之类比解释如下:
- 异步 I/O (Asynchronous I/O):好比你在美团外卖订了个盒饭,付款之后你自己该干嘛还干嘛去,饭做好了 骑手自然会到门口打电话通知你 。异步 I/O 中数据到达缓冲区后,不需要由调用进程主动进行从缓冲区复制数据的操作,而是复制完成后由操作系统向线程发送信号,所以它一定是非阻塞的。
- 同步 I/O (Synchronous I/O):好比 你自己去饭堂打饭 ,这时可能有如下情形发生:
- 阻塞 I/O (Blocking I/O):你去到饭堂, 发现饭还没做好 ,你也干不了别的,只能打个瞌睡(线程休眠),直到饭做好,这就是被阻塞了。阻塞 I/O 是最直观的 I/O 模型,逻辑清晰,也比较节省 CPU 资源,但缺点就是线程休眠所带来的上下文切换,这是一种需要切换到内核态的重负载操作,不应当频繁进行。
- 非阻塞 I/O (Non-Blocking I/O):你去到饭堂,发现饭还没做好, 你就回去了,然后每隔 3 分钟来一次饭堂看饭做好了没 ,直到饭做好。非阻塞 I/O 能够避免线程休眠,对于一些很快就能返回结果的请求,非阻塞 I/O 可以节省切换上下文切换的消耗,但是对于较长时间才能返回的请求,非阻塞 I/O 反而白白浪费了 CPU 资源,所以目前并不常用。
- 多路复用 I/O (Multiplexing I/O):多路复用 I/O 本质上是阻塞 I/O 的一种,但是它的好处是可以在 同一条阻塞线程上处理多个不同端口的监听 。类比的情景是你名字叫雷锋,代表整个宿舍去饭堂打饭,去到饭堂,发现饭还没做好,还是继续打瞌睡,但哪个舍友的饭好了,你就马上把那份饭送回去,然后继续打着瞌睡哼着歌等待其他的饭做好。多路复用 I/O 是目前的高并发网络应用的主流,它下面还可以细分 select、epoll、kqueue 等不同实现,这里就不作展开了。
- 信号驱动 I/O (Signal-Driven I/O):你去到饭堂,发现饭还没做好,但你跟厨师熟,跟他说饭做好了叫你,然后回去该干嘛干嘛,等收到厨师通知后,你把饭从“”饭堂拿回宿舍。这里厨师的通知就是那个“信号”,信号驱动 I/O 与异步 I/O 的区别是“从缓冲区获取数据”这个步骤的处理,前者收到的通知是可以开始进行复制操作了,即要你自己从饭堂拿回宿舍,在复制完成之前线程处于阻塞状态,所以它仍属于同步 I/O 操作,而后者收到的通知是复制操作已经完成,即外卖小哥已经把饭送到了。
显而易见,异步 I/O 模型是最方便的,毕竟能叫外卖谁愿意跑饭堂啊,但前提是你学校里有开展外卖业务。同样,异步 I/O 受限于操作系统,Windows NT 内核早在 3.5 以后,就通过 IOCP 实现了真正的异步 I/O 模型。而 Linux 系统下,是在 Linux Kernel 2.6 才首次引入,目前也还并不算很完善,因此在 Linux 下实现高并发网络编程时仍是以多路复用 I/O 模型模式为主。
Summary
多路复用对应到 netty 中就是 worker 线程。对应到 linux 上就比较像 Socket 连接之一个端口可以有几个连接 提到的 listen 和 connect 操作。
非阻塞 IO 和 多路复用 的区别看上去主要是轮询??