Java NIO(3): IO模型

本节课是小密圈《进击的Java新人》第十六周第三课。

这一节课,我们要讲解真正的异步编程了。本节课的大部分内容,图片都来自《Unix网络编程》这本书。如果有这本书的同学,可以直接去看这本书了,在我看来,目前还没遇到过比这本书讲得更清楚的。如果没有这本书,没关系,看我的文章也一样(反正我也是抄的,哈哈)

说点题外话。做Java的,大多数是做服务端的,而现在绝大多数的IT公司的服务端都会运行在Linux 系统上,所以,掌握一定的 Linux 的知识,是成为一个好的服务端所必需的。学习服务端编程,最好的两本书,《unix环境高级编程》和《unix网络编程》,算是比较经典的。你看,《欢乐颂》里,应勤就是经常看这本书,才能追到杨紫的。

IO模型

《unix网络编程》里总结了五类IO模型。为了更好地理解,我会举一个叫外卖的例子来说明。第一种是完全阻塞模型,它的示意图如下:

就是说,如果我客户端发起了connect请求,那么当前线程就会休眠,等待服务端响应完毕,返回消息,才会继续走下去。这种代码,我们已经写过了:

socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("192.168.0.13", 8000));
System.out.println("Hello");

你会看到,在connect没有完成之前,最后第三行的println根本不会执行。也就是说客户端会阻塞在这个地方。

就好比,我叫个外卖,然后我就去大门口傻等着,外卖不送到,我也什么都不做,就坐在门口打盹,直到外卖小哥过来,把我叫醒,我才拿着外卖回家去吃。这样做显然效率不高,显得脑子有水。

第二种,非阻塞式IO。

把非阻塞的文件描述符称为非阻塞I/O。可以通过设置SOCK_NONBLOCK标记创建非阻塞的socket fd,或者使用fcntl将fd设置为非阻塞。

对非阻塞fd调用系统接口时,不需要等待事件发生而立即返回,事件没有发生,接口返回-1,此时需要通过errno的值来区分是否出错,有过网络编程的经验的应该都了解这点。不同的接口,立即返回时的errno值不尽相同,如,recv、send、accept errno通常被设置为EAGIN 或者EWOULDBLOCK,connect 则为EINPRO- GRESS 。

就是说,客户端程序会不停地去尝试读取数据,但是不会阻塞在那个读方法里,如果读的时候,没有读到内容,也会立即返回。这就允许我们在客户端里,读到不数据的时候可以搞点其他的事情了。

仍然以外卖举例,就相当于,我一边扫地,一边等外卖。我不再像原来一样,在门口傻等了,而是扫两下,就跑到门口看看外卖到了没有。一直这样循环,直到我取到外卖,才从这个循环中跳出来,进入吃的流程。

第三种,IO多路复用

非常非常非常重要。可能是我的课程开始以来,最重要的一部分。请务必保证能理解透彻,这是NIO的核心与关键,也是高性能服务器的第一要诀。

最常用的I/O事件通知机制就是I/O复用(I/O multiplexing)。Linux 环境中使用select/poll/epoll_wait 实现I/O复用,I/O复用接口本身是阻塞的,在应用程序中通过I/O复用接口向内核注册fd所关注的事件,当关注事件触发时,通过I/O复用接口的返回值通知到应用程序。I/O复用接口可以同时监听多个I/O事件以提高事件处理效率。


这个代码我明天给出示例,今天先理解这张图的意思。还是外卖的例子,如果我们整栋楼的人,很多人叫了外卖,都有下楼来看外卖到没到的需求,于是物业就出了个招,让门卫小哥帮大家看着,整栋楼上的,不管是谁的外卖到了,先放到门卫小哥那里,然后门卫小哥再通知你下来拿自己的外卖。这样一来,我们就把本来多个人要跑去看自己的外卖到了这件事交给门卫小哥去做了。而我们解放出来,就可以继续看电视,打扫卫生,刷知乎了。由于我们可以继续 做自己的事情,外卖小哥和门卫小哥在同时也在工作,互不干扰,所以这种工作方式就被称为异步模型

第四各,SIGIO

除了I/O复用方式通知I/O事件,还可以通过SIGIO信号来通知I/O事件,如图所示。两者不同的是,在等待数据达到期间,I/O复用是会阻塞应用程序,而SIGIO方式是不会阻塞应用程序的。

上面这张图,就是我们现实生活中真正的外卖。数据到达以后,给客户端发一个消息,让客户端过来取数据。这就像外卖小哥到你家门口给你打电话,让你出来取一下。显然,这种是最方便的,也是最合理的。

但实际上,在真正的编程中,我们很少使用这种模型。

第五种,async io

POSIX规范定义了一组异步操作I/O的接口,不用关心fd 是阻塞还是非阻塞,异步I/O是由内核接管应用层对fd的I/O操作。异步I/O向应用层通知I/O操作完成的事件,这与前面介绍的I/O 复用模型、SIGIO模型通知事件就绪的方式明显不同。以aio_read 实现异步读取IO数据为例,如图所示,在等待I/O操作完成期间,不会阻塞应用程序。

这个图,如果对应到外卖有点不合适了,比较像网购空调,我所要做的,只是下单,而快递小哥会把空调送过来,他也不会让你自己去取,他会让安装师傅直接帮你安装。这个过程中,你什么都不需要做。你所要做的仅仅是发起一个请求。这种IO模型就是纯正的异步IO。

这种纯异步IO的最典型例子就是node.js中的callback。

这5种IO并不是相互对立的,通过一定的技巧,是可以相互转化的。更具体的转化方法和代码,我们下节课会给出。

本节课看不懂也不要紧,就把外卖这个例子能讲清楚就行了。我们以后有了代码了,对照着看,就融汇贯通了。

上一节课:nio(2):channel

下一节课:Java NIO(4): 阻塞与非阻塞 - 知乎专栏

目录:课程目录

文章被以下专栏收录