在介绍nio编程之前,我们首先需要澄清一个概念:nio到底是什么的简称?有人称之为new i/o,因为它相对于之前的i/o类库是新增的,所以被称为new i/o,这是它的官方叫法。但是,由于之前老的i/o类库是阻塞i/o,new i/o类库的目标就是要让java支持非阻塞i/o,所以,更多的人喜欢称之为非阻塞i/o(non-block i/o),由于非阻塞i/o更能够体现nio的特点,所以我们这里使用nio表示非阻塞i/o。
与socket类和serversocket类相对应,nio也提供了socketchannel和serversocketchannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载,低并发的应用程序可以选择同步阻塞i/o以降低编程复杂度,但是对于高负载,高并发的网络应用,需要使用nio的非阻塞模式进行开发。
nio类库简介
1、缓冲区buffer
我们首先介绍缓冲区(buffer)的概念,buffer是一个对象,它包含一些要写入或者要读出的数据。在nio类库中加入buffer对象,体现了新库与原i/o的一个重要区别。在面向流的i/o中,可以将数据直接写入或者将数据直接读到stream对象中。
在nio库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中。任何时候访问nio中的数据,都是通过缓冲区进行操作。
缓冲区实质上是一个数组。通常它是一个字节数组(bytebuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。
最常用的缓冲区是bytebuffer,一个bytebuffer提供了一组功能用于操作byte数组。除了bytebuffer,还有其他的一些缓冲区,事实上,每一种java基本类型(除了boolean类型)都对应有一种缓冲区,具体如下:
- bytebuffer:字节缓冲区
- charbuffer:字符缓冲区
- shortbuffer:短整型缓冲区
- intbuffer:整型缓冲区
- longbuffer:长整型缓冲区
- floatbuffer:浮点型缓冲区
- doublebuffer:双精度浮点型缓冲区
缓冲区的继承关系如下图所示:
每一个buffer类都是buffer接口的一个子实例。除了bytebuffer,每一个buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准i/o操作都使用bytebuffer,所以它除了具有一般缓冲区的操作之外还提供一些特有的操作,方便网络读写。
2、通道channel
channel是一个通道,可以通过它读取和写入数据,它就像输水管道一样,网络数据通过channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是inputstream 或者 outputstream的子类),而且通道可以用于读,写或者同时用于读写。
因为channel是全双工的,所以它可以比流更好地映射底层操作系统的api。特别是在unix网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。
channel的集成关系如下图所示:
自顶向下看, 前三层主要是channel接口,用于定义它的功能,后面是一些具体的功能类(抽象类)。从类图可以看出,实际上channel可以分为两大类:分别是用于网络读写的selectablechannel和用于文件操作的filechannel。
3、多路复用器selector
多路复用器selector,它是java nio编程的基础,熟练地掌握selector对于掌握nio编程至关重要。多路复用器提供选择已经就绪的任务的能力。简单来讲, selector会不断地轮询注册在其上的channel,如果某个channel上面有新的tcp连接接入,读和写事件,这个channel就处于就绪状态,会被selector轮询出来,然后通过selectionkey可以获取就绪channel的集合,进行后续的i/o操作 。
一个多路复用器selector可以同时轮询多个channel,由于jdk使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。
4、nio服务端序列图
尽管nio编程难度确实比同步阻塞bio大很多,但是我们要考虑到它的优点:
- 客户端发起的连接操作是异步的,可以通过在多路复用器注册op_connect等后续结果,不需要像之前的客户端那样被同步阻塞。
- socketchannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样io通信线程就可以处理其它的链路,不需要同步等待这个链路可用。
- 线程模型的优化:由于jdk的selector在linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降,因此,它非常适合做高性能、高负载的网络服务器。
jdk1.7升级了nio类库,升级后的nio类库被称为nio2.0。也就是我们要介绍的aio。nio2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供两种方式获取操作结果。
- 通过java.util.concurrent.future类来表示异步操作的结果;
- 在执行异步操作的时候传入一个java.nio.channels.
completionhandler接口的实现类作为操作完成的回调。
nio2.0的异步套接字通道是真正的异步非阻塞io,它对应unix网络编程中的事件驱动io(aio),它不需要通过多路复用器(selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了nio的编程模型。
我们可以得出结论:异步socket channel是被动执行对象,我们不需要像nio编程那样创建一个独立的io线程来处理读写操作。对于asynchronousserversocketchannel和asynchronoussocketchannel,它们都由jdk底层的线程池负责回调并驱动读写操作。正因为如此,基于nio2.0新的异步非阻塞channel进行编程比nio编程更为简单。