大家好,我是 moon,上一次和大家聊了一下 socket(这次 moon 要把 socket 玩的明明白白),相信大家对 socket 有了一定的认识,对于 socket 还不熟悉的同学,可以先看看 socket 这篇文章,今天这篇文章是基于 socket,再和大家讲一讲「网络I/O」相关的知识,也刚好为后续 netty 的文章做下铺垫
I:其实就是 「Input」,输入 O:其实就是 「Output」,输出 所以 I/O 很好理解,就是输入和输出
生活中最简单的例子,你用微信和别人聊天,你「发送信息」给对方,就是「输入」,「对方回给你信息」,你接受到了,就是「输出」。
一般情况下,在软件中我们常说的 I/O 是指「网络 I/O 和磁盘 I/O」,今天我们就来聊下网络 I/O
其实网络 I/O 就是网络中的输入与输出,我们再说详细点,正常的网络通信中,一条消息发送的过程中有一个很重要的媒介,叫做「网卡」,它的作用有两个
网卡能「接收所有在网络上传输的信号」,但正常情况下只接受发送到该电脑的帧和广播帧,将其余的帧丢弃。
所以网络 I/O 其实网络与服务端(电脑内存)之间的输入与输出
一般情况下,一次网络数据的传输会从客户端发送给服务端,由服务端网卡接受,转交给内存,最后由 cpu 执行相应的业务操作
只要有一点电脑知识的读者大多数都知道,cpu、显卡、内存等电脑中你数得上名字的模块,运行效率最高的就是 cpu 了,所以「为了整个网络传输的提效,就诞生出了五种网络 I/O 模型」
当然还有一点原因
不管是客户端还是服务端,在发送和接受数据的时候,都是要与 socket 缓冲区去进行交互的,那么「什么时候交互?是否需要等待响应?」基于这些「不同的业务场景」,也就「有了五种网络 I/O 模型」
其实这个结论是通用的,「任何模型的调优改造健全一般都是围绕着性能、安全、业务场景这三个方向来前进的」
好了,我们说完了 I/O 模型的诞生,就来具体的研究下具体是哪五种 I/O 模型
阻塞 I/O,顾名思义,就是在各个状态完全阻塞住整个 I/O 过程是个串行化的,在数据传输开始时,都要等到后续的每一个操作完成后才可以继续,这也是 linux 系统默认的 I/O 模型,当然,这种模型的问题就是「效率非常的低」
第二个问题就是,如果是在生产环境多线程的情况下,会有频繁线程的「上下文切换」,而这个切换又是「非常消耗资源」的。
非阻塞 I/O 针对于阻塞 I/O 有了一些改进
在系统调用内核后,内核如果「没有准备好」数据,则直接会「返回一个错误码」,直到数据「准备好」后,「再走后续的流程」
中间会有一个「一直定时去询问」的动作,所以它的「缺点」也就是在这里,需要「一直去处理数据没有准备好的情况」,也「无法判断该数据多久会准备好」。但是在并发的情况下相比阻塞 I/O 来说它的性能会好很多
其实通过阻塞或者非阻塞 I/O,你能发现一个问题,其实「拷贝数据都是由内核完成」的,那又「何必让应用进程去触发拷贝这个操作呢」,由内核直接完成不就好了?
所以就出现了异步 I/O 模型
比如有一个读取数据的请求,应用「只需要向内核发送一个 read」,告诉内核它要读取数据后即刻返回。
之后内核收到请求并且建立一个信号连接,当数据准备就绪,内核会主动把数据从内核复制到用户空间,「等所有操作都完成之后,内核再发起一个通知告诉应用」,完成了所有操作,应用程序可以读取数据了,这就是异步 I/O 模型。
异步 IO 的好处是「将发送询问请求、发送接收数据请求两个请求合并为一次请求」就可以完成状态询问和数拷贝的所有操作,并且「无阻塞」,内核准备好数据后直接通知。
信号驱动就是进程发起一个 IO 操作,会「向内核注册一个信号处理程序」,然后立即返回
当「内核将数据报准备好」后会「发送一个信号」给进程,这时候进程便可以在信号处理程序中调用 IO 处理数据报。
I/O 多路复用解决的问题就是「单线程怎么管理多个 socket 连接」,内核负责轮询所有 socket,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
「其主要有 select、poll、epoll 三种多路复用的网络I/O模型」。
select 的设计思路是唤醒模式,「通过一个 socket 列表维护所有的 socket」,socket 对应文件列表中的 fd,select 会默认限制最大文件句柄数为 1024,间接控制 fd[] 最大为 1024。
那么有个问题来了,如果有多个 socket 任务同时唤醒怎么办,也就是说说有多个 socket 任务同时进来,那到底执行哪个 socket 任务?
所以,当进程被唤醒后,「至少有一个 socket 是有数据的」,这时候只需要「遍历一遍 socket 列表」就知道了此次需要执行哪些 socket 了。
缺点:
poll 其实内部实现基本跟 select 一样,区别在于它们底层组织 fd[] 的数据结构不太一样,从而实现了 poll 的最大文件句柄数量限制去除了
我们前面说到 select 是需要遍历来解决查询就绪 socket 的,效率很低,epoll 就对此做了改进
select 的添加等待队列和阻塞进程是合并在一起的,每次调用select()操作时都得执行一遍这两个操作,从而导致每次都要将fd[]传递到内核空间,并且遍历fd[]的每个fd的等待队列,将进程放入各个fd的等待队列中。
「直接返回有数据的 socket 是怎么实现的?」
其实就是 epoll 会先注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知。
epoll对文件描述符的操作有两种模式:「LT」(level trigger)和 「ET」(edge trigger)默认为 LT :
I/O 模型这块儿可是重中之重,作为网络通信的核心,只要你想去大厂一定会被问到,今天这篇文章应该帮你梳理清楚了各个网络 I/O 模型之间的关系以及优缺点,让你对网络 I/O 模型有了更进一步的了解,下次我们准备开始突击 netty ~