Java NIO基础

Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。

Posted by 南方 on September 11, 2017

IO 是主存和外部设备 ( 硬盘、终端和网络等 ) 拷贝数据的过程。 IO 是操作系统的底层功能实现,底层通过 I/O 指令进行完成。 所有语言运行时系统提供执行I/O较高级别的工具。在java编程中,标准低版本IO使用流的方式完成I/O操作,所有的I/O都被视为单个的字节流动,称为一个Stream的对象一次移动一个字节。

  • NIO有如下特性:
    1. 为所有的原始类型提供(Buffer)缓存支持;
    2. 字符集编码解决方案(Charset);
    3. Channel : 一个新的原始I/O抽象;
    4. 支持锁和内存映射文件的文件访问接口;
    5. 提供多路(non-bloking)非阻塞式的高伸缩性网路I/O。
  • NIO4个关键的抽象数据类型:
    1. Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
    2. Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。
    3. Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道。
    4. Selector:它将多元异步I/O操作集中到一个或多个线程中。

1. Buffer(缓冲区)

标准IO基于字节流和字符流进行操作,而NIO基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。

通道Channel是对原IO中流的模拟,所有数据都要通过通道进行传输;Buffer实质上是一个容器对象,发送给通道的所有对象都必须首先放到一个缓冲区中。

1.1 Buffer

缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

简单的说Buffer是:一块连续的内存块,是NIO数据读或写的中转地。

1.2 Buffer类图

1.3 Buffer在JDK中实现

抽象类,共有5个属性,分别是mark、position、limit、capacity、address

并且还有一行注释:// Invariants: mark <= position <= limit <= capacity

一个 buffer 主要由 position、limit、capacity 三个变量来控制读写的过程。

参数 读模式 写模式
position 当前写入的单位数据的数量 当前读入的单位数据的数量
limit 代表最多能写多少单位的数据量,默认和capacity一致 代表最多能读多少单位的数据量,和之前写入的数据量一致
capacity Buffer的容量 Buffer的容量

1.4 Buffer方法

属性操作

最基本的对应属性操作的方法,查看源码知道要得到当前Buffer的limit值使用public final int limit()方法,设定limit的值使用public final Buffer limit(int)方法,其它属性有对应的方法。

public final Buffer flip() : 用于将写模式转换成读模式
limit = position;    //将limit设置为刚才写入的位置
position = 0;         //将position设置为0从头开始读
mark = -1;
return this;
public final Buffer clear() : 用于清空缓冲区,准备再次被写入,limit设置为capacity,position设置为0
public final Buffer rewind() : 源码实现为position=0,mark=-1。目的是为了重复读。
public fremaininginal int () : return limit - position;
public final boolean hasRemaining() : return position < limit;

继承自Buffer的重要类ByteBuffer类中的方法:

首先可以看到在ByteBuffer类中多了三个属性,一个byte数组型的,一个int型的offset,还有一个boolean型的isReadOnly,两个构造函数均是Package-private型的。

产生ByteBuffer对象

  1. 方法一:ByteBuffer bbuf = ByteBuffer.allocate(1024); 查看源码知道allocate执行这样一句话: return new HeapByteBuffer(int capacity, int capacity); 而HeapByteBuffer又是ByteBuffer的子类,并且在HeapByteBuffer的构造方法中执行的是这样一个语句: super(-1, 0, lim, cap, new byte[cap], 0); 也就是说调用的还是ByteBuffer中的构造方法,包范围内使用。这个方法做了如下工作,首先调用Buffer的构造方法,依次初始化mark、position、limit、capacity,然后初始化ByteBuffer的属性byte数组,接着初始offset,这样使用allocate方法就可以构造出一个ByteBuffer对象了。

  2. 方法二:ByteBuffer bbuf = ByteBuffer.wrap(new Byte[1024] array, 0, 1024); 这个方法比较好用的一点是当这个Byte数组已经存在的话,直接传入这个Byte数组,然后传入起始值和结束值即可。默认wrap实现是初始值传入为0,结束值传入为Byte数组的长度array.length。

ByteBuffer其它重要方法

get(byte[] dst) 或者 get(byte[] dst, int offset, int len) (该方法是用来获取当前ByteBuffer中的指定位置的数据并赋值给dst,最终返回当前对象本身。方法实现时第一步检查参数是否合法,调用的是checkBounds静态包范围私有方法。然后检查len是否大于remaining,接着对dst数组循环赋值,最终返回该对象。)

put(byte[] src) 或者 put(byte[] src, int offerset, int len) (该方法和上面的一对get方法类似,功能是将已有的byte数组从0位置开始放入当前的ByteBuffer中,最终返回ByteBuffer本身。)

put(ByteBuffer src) (该方法将src的remaining逐个放入当前ByteBuffer中,最终返回当前ByteBuffer。)

除此之外还有类型化的get方法,例如getInt(), getFloat(), getShort()等。

1.5 Buffer更多

缓冲区分片:slice()方法根据现有的缓冲区创建一种子缓冲区,新的缓冲区与原缓冲区共享部分数据。

只读缓冲区:可以通过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。只读缓冲区对于保护数据很有用。没有办法将只读缓冲区改变为可写的。 下面例子对缓冲区进行分片,并操作数据:

//产生一个ByteBuffer实例
ByteBuffer buffer = ByteBuffer.allocate( 10 );
//对该ByteBuffer实例进行初始化
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
//修改buffer的position(起点)和limit(终点)
buffer.position( 3 );
buffer.limit( 7 );
//对缓冲区进行分片
ByteBuffer slice = buffer.slice();
//对分片的数据进行操作
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 11;
slice.put( i, b );
}
//重新定位并输出结果
buffer.position( 0 );
buffer.limit( buffer.capacity() );
while (buffer.remaining()>0) {
System.out.println( buffer.get() );
}

运行结果:

0
1
2
33
44
55
66
7
8
9

直接或者间接缓冲区:直接缓冲区可以加快I/O的读写速度,使用allocateDirect(int capacity)产生一个直接缓冲区。

内存映射文件:下面代码将一个 FileChannel (它的全部或者部分)映射到内存 中。将文件的前1024个字节映射到内存中: MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );

2 Channel(通道)

2.1 Channel

Channel 是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。

所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

简单的说Channel是:数据的源头或者数据的目的地,用于向buffer提供数据或者读取buffer数据,并且对I/O提供异步支持。

2.2 Channel类图

java.nio.channels.Channel是一个公共的接口,所有子Channel均实现了该接口,在java.nio.channels包中还实现了Channels、FileLock、SelectionKey、Selector、Pipe等比较好用的类。包含socket,file和pipe三种管道,它实际上是双向交流的通道。

2.3 Channel在JDK中实现

在Channel接口中共定义了两个方法

public boolean isOpen();   //Tells whether or not this channel is open
public void close() throws IOException();     //Close this channel

FileChannel : 使用以下三个方法可以得到一个FileChannel的实例

FileInputStream.getChannel()
FileOutputStream.getChannel()
RandomAccessFile.getChannel()

上面提到Channel是数据的源头或者数据的目的地,用于向bufer提供数据或者从buffer读取数据。那么在实现了该接口的子类中应该有相应的read和write方法。

在FileChannel中有以下方法可以使用:

public long read(ByteBuffer[] dsts)
Reads a sequence of bytes from this channel into the given buffers.
public long write(ByteBuffer[] srcs)
Writes a sequence of bytes to this channel from the given buffers.

附加:文件锁定 FileChannel提供两种方法获得FileLock

FileLock lock();
FileLock lock(long position, long size, boolean size);

使用方法举例:

要获取文件的一部分上的锁,您要调用一个打开的 FileChannel 上的 lock() 方法。注意,如果要获取一个排它锁,您必须以写方式打开文件。

RandomAccessFile raf = new RandomAccessFile( "filelocks.txt", "rw" );
FileChannel fc = raf.getChannel();
FileLock lock = fc.lock( start, end, false );

在拥有锁之后,您可以执行需要的任何敏感操作,然后再释放锁:

lock.release();

SocketChannel : 使用以下两个方法得到一个SocketChannel的实例

SocketChannel.open();      //打开一个socket channel
SocketChannel.open(SocketAddress remote);  

//调用上面的方法,并connect(remote) 例子代码:

InetSocketAddress socketAddress = new InetSocketAddress(“www.baidu.com”,80);
SocketChannel sc = SocketChannel.open(socketAddress);
sc.read(buffer);
buffer.flip();
buffer.clear();
sc.write(bufer);

DatagramChannel : 与其它的Channel有相同或者相似的方法。


3 Charset(字符集)

3.1 Charset(字符集)

在java.nio.charset包中共提供了Charset、CharsetDecoder、CharsetEncoder、CodeResult、CodingErrorAction五个类,均继承自Object类,其中Charset实现了Comparable接口,其它类均为自身实现。

Java中的字符使用unicode编码,每个字符占用两个字节。字节码本身只是一些数字,放在正确的上下文中可以被正确的解析。向ByteBuffer中存放数据时需要考虑字符集的编码方式,从中读取时需要考虑字符集的解码。

要读和写文本需要分别使用CharsetDecoder(解码器)和CharsetEncoder(编码器)。

编码:百科中这样定义,编码(coding)是在一个主题或单元上为数据存储,管理和分析的目的而转换信息为编码值(典型地如数字)的过程。在密码学中,编码是指在编码或密码中写的行为。n位二进制数可以组合成2的n次方个不同的信息,给每个信息规定一个具体码组,这种过程也叫编码。数字系统中常用的编码有两类,一类是二进制编码,另一类是二—十进制编码。

(1)如何得到一个CharSet?

在JDK源码中提供两种方式得到一个CharSet实例:

       CharSet cs = CharSet.forName(“编码方式”);
       CharSet cs = CharSet.defaultCharSet();

第一种方法返回一个指定字符格式的CharSet,第二种方法返回当前虚拟机默认的字符编码格式的CharSet。

(2)如何使用CharSet?

得到一个CharSet实例后,我们需要创建一个编码器和一个解码器,使用下面方法进行创建:

       CharSetDecoder decoder = cs.newDecoder();
       CharSetEncoder encoder = cs.newEncoder();

接着我们把ByteBuffer传递给decoder进行编码,返回一个CharBuffer:

       CharBuffer cb = decoder.decode(inputData);

然后我们可以使用encoder进行解码返回一个ByteBuffer:

       ByteBuffer outputData = encoder.encode(cb);

接下来可以进行写等其它操作。

4 Selector

4.1 Selector

在传统的IO模式中,需要启动多个线程以阻塞的方式监听Socket,等待数据到达并进行处理。在NIO中,Selector相当于一个事件管理器,可以向它注册不同连接的多个事件,并以一定间隔询问Selector是否有注册的事件发生,再进行后续的处理。这样只需要一个阻塞的线程,就可以管理所有的连接了。

Selector管理的事件包含两个要素:

  1. 事件所对应的连接,以SocketChannel作为描述。
  2. 事件的类型,SelectionKey.OP_* 常量之一。 两个要素唯一确定一个发生的事件。

当调用 int n = selector.select() 方法时,返回值n就是新产生的事件个数。调用 selector.selectedKeys() 获取具体事件集合,每一个事件对应一个 SelectionKey 对象。通过 SelectionKey 获取对应的 SocketChannel 以及事件的类型,接下来就可以进行建立连接、读取数据等操作。

Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

异步IO是一种没有阻塞的读写数据的方法,通常,在代码进行 read() 调用时,代码会阻塞直至有可供读取的数据。同样,write() 调用将会阻塞直至数据能够写入。 异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。

异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

Selector本身为抽象类,AbstractSelector是Selector类的抽象实现类,具体的实现类更加底层(SelectorImpl,位于sun.nio.ch);Selector即为”选择器”,支撑了NIO的多路复用.

Selector不能直接创建,需要通过Selector.open()获得,该方法将使用系统默认的选择器提供者创建新的选择器(SelectorProvider),可以通过选择器的close方法关闭它. 通过SelectionKey对象来表示可选择通道(SelectableChannel)到选择器的注册.SelectionKey由Selector维护.

通常一个Channel只会被注册到一个Selector,一个Selector可以“监测”多个Channel;事实上Channel可以注册在任意一个Selector上,ServerSocketChannel和SocketChannel可以共用一个Selector,也可以各自使用不同的。

  • 第一步:创建一个Selector
         Selector selector = Selector.open();
    
  • 第二步:打开一个远程连接
         InetSocketAddress socketAddress =
    new InetSocketAddress("www.baidu.com", 80);
         SocketChannel sc = SocketChannel.open(socketAddress);
         sc.configureBlocking(false);
    
  • 第三步:选择键,注册
         SelectionKey key = sc.register(selector, SelectionKey.OP_CONNECT);
    

    注册时第一个参数总是当前的这个selector。 注册读事件:SelectionKey key = sc.register(selector, SelectionKey.OP_READ); 注册写事件:SelectionKey key = sc.register(selector, SelectionKey.OP_WRITE);

  • 第四步:内部循环处理
    int num = selector.select();
    Set selectedKeys = selector.selectedKeys();
    Iterator it = selectedKeys.iterator();
    while (it.hasNext()) {
    SelectionKey key = (SelectionKey)it.next();
    // ... deal with I/O event ...
    }
    

    首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。该方法必须首先执行。

接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。

我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。

  • 第五步:监听事件并做出处理 SelectionKey中共定义了四种事件,OP_ACCEPT(socket accept)、OP_CONNECT(socket connect)、OP_READ(read)、OP_WRITE(write)。

  • 第六步:删除处理过的SelectionKey 在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。

如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。

我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey:it.remove();

参考资料

官网 https://docs.oracle.com/javase/7/docs/api/java/nio/package-summary.html

java nio学习笔记 http://blog.csdn.net/tsyj810883979/article/details/6876594

epoll 浅析以及 nio 中的 Selector http://www.importnew.com/24794.html

NIO-Selector类详解 http://shift-alt-ctrl.iteye.com/blog/1840411

Java NIO 的前生今世 之四 NIO Selector 详解 https://segmentfault.com/a/1190000006824196

Java NIO系列教程 http://ifeve.com/selectors/

深入浅出NIO Socket实现机制 http://www.jianshu.com/p/0d497fe5484a

Java NIO(6): Selector https://zhuanlan.zhihu.com/p/27434028

Java NIO浅析 https://tech.meituan.com/nio.html

Java NIO系列学习笔记(五) - Selector http://www.hifreud.com/2017/04/18/java-nio-05-selector/