Java NIO详解

NIO含义

New I/O,原因在于它相对于之前的I/O类库是新增的。
由于之前老的I/O类库是阻塞I/O,New I/O类库的目标就是要让Java支持非阻塞I/O,所以,更多的人喜欢称之为非阻塞I/O(Non-block I/O)。

SocketChannel和ServerSocketChannel

与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。
低负载、低并发的应用程序可以选择同步阻塞I/O以降低编程复杂度;对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。

NIO类库简介

NIO弥补了原来同步阻塞I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。

通过定义包含数据的类,以及通过以块的形式处理这些数据,NIO不用使用本机代码就可以利用低级优化,这是原来的I/O包所无法做到的。

NIO类库简介 缓冲区Buffer

Buffer是一个对象,它包含一些要写入或者要读出的数据。
在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。
在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实质上是一个数组。
缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。
一个ByteBuffer提供了一组功能用于操作byte数组。
每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区:
ByteBuffer:字节缓冲区
CharBuffer:字符缓冲区
ShortBuffer:短整型缓冲区
IntBuffer:整形缓冲区
LongBuffer:长整形缓冲区
FloatBuffer:浮点型缓冲区
DoubleBuffer:双精度浮点型缓冲区

每一个Buffer类都是Buffer接口的一个子实例。除了ByteBuffer,每一个Buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。
因为大多数标准I/O操作都使用ByteBuffer,所以它在具有一般缓冲区的操作之外还提供了一些特有的操作,以方便网络读写。

NIO类库简介 通道Channel

Channel是一个通道,网络数据通过Channel读取和写入。
通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行。
因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

自顶向下看,前三层主要是Channel接口,用于定义它的功能,后面是一些具体的功能类(抽象类)。从类图可以看出,实际上Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。
ServerSocketChannel和SocketChannel都是SelectableChannel的子类

多路复用器Selector

多路复用器Selector是Java NIO编程的基础
多路复用器提供选择已经就绪的任务的能力。
Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channnel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。

下面用代码简单描述这个过程:


使用NIO技术编写TimeServer

使用NIO技术编写TimeClient原理讲解

时序图:



用代码讲解一下过程:

使用NIO技术编写TimeClient

注意

下面这种关闭socket的方法是错误的


如果服务端没有对这种情况进行处理会出现一下的错误:


服务端最好是在读取数据的时候做一下处理:

优势

NIO编程的难度比同步阻塞BIO大很多。
请注意以上的代码中并没有考虑“半包读”和“半包写”,如果加上这些,代码将会更加复杂。。
(1)客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。
(2)SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。
(3)线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此,它非常适合做高性能、高负载的网络服务器。

推荐阅读更多精彩内容