Java NIO详解

概述

BIO是面向字节流和字符流的,数据从流中顺序获取

NIO是面向通道和缓冲区的,数据总是从通道中读到buffer缓冲区内,或者从buffer缓冲区内写入通道

Channel通道和Buffer缓冲区是NIO的核心,几乎在每一个IO操作中使用它们,Selector选择器则允许单个线程操作多个通道,对于高并发多连接很有帮助。

操作系统的IO一般分为两个阶段,等待和就绪操作,比如读可以分为等待系统可读和真正的读,写可以分为等待系统可写和真正的写,在传统的BIO中是这两个阶段都会阻塞,在NIO中第一个阶段不是阻塞的,第二个阶段是阻塞的,如下图,BIO是阻塞IO,NIO是非阻塞IO

Buffer(缓冲区)

常用Buffer类型

ByteBuffer, MappedByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer。对应了几大基本数据类型。

ByteBuffer可以选择实例化为DirectByteBuffer和HeapByteBuffer,如果数据量比较小的中小应用情况下,可以考虑使用heapBuffer;反之可以用directBuffer。一般来说DirectByteBuffer可以减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高,更不宜维护,通常会用内存池来提高性能。其他buffer类似。

利用Buffer读写数据步骤

  1. 将数据写入buffer
  2. 调用flip将写模式改为读模式
  3. 从buffer中读取数据,进行操作
  4. 调用clear()清除整个buffer数据或者调用compact()清空已读数据

buffer的属性

  • capacity容量,该buffer最多存储的字节数
  • position位置,写模式下,position从0到capacity-1,变更为读模式后,position归零,边读边移动
  • limit限制,写模式下,代表我们能写的最大量为capacity,读模式下,变更为原position位置,即有数据的位置

buffer_modes

buffer api

  • 通过allocate()方法为buffer分配内存大小,如开辟一个48字节的ByteBuffer buffer:ByteBuffer.allocate(48)
  • 写数据可以通过通道写,如:FileChannel.read(buffer);也可以通过put方法来写数据,如:buffer.put(127)
  • flip()翻转方法,将写模式切换到读模式,position归零,设置limit为之前的position位置
  • 读数据可以读到通道,如:FileChannel.write(buffer);也可以调用get()方法读取,byte aByte=buffer.get()
  • buffer.rewind()将position置0,limit不变,这样我们就可以重复读取数据啦
  • buffer.clear()将position置为0,limit设置为capacity,这里并没有删除buffer里面的数据,只是把标记位置改了;
  • buffer.compact()清除已读数据,这里也没有删除数据,将position设置为未读的个数,将后面几个未读的字节顺序的复制到前面的几个字节,limit设置为capacity,比如buffer容量3个字节,读取hello,在读取了2个字节后我就调用了compact()方法,那么此时position为1,limit为3,buffer内部存储的数据buff[0]=’l’,buff[1]=’e’,buff[2]=’l’,因为有一个’l’没有读完,将’l’提取到最前面供下次读取
  • mark()可以标记当前的position位置,通过reset来恢复mark位置,可以用来实现重复读取满足条件的数据块
  • equals()两个buffer相等需满足,类型相同,buffer剩余(未读)字节数相同,所有剩余字节数相同
  • compareTo()比较buffer中的剩余元素,只不过此方法适合排序

Channel(通道)

Channel的重要实现

  • FileChannel用于文件数据的读写,transferTo()方法可以将通道的数据传送至另外一个通道,完成数据的复制
  • DatagramChannel用于UDP数据的读写
  • SocketChannel用于TCP的数据读写,通常我们所说的客户端套接字通道
  • ServerSocketChannel允许我们监听TCP链接请求,通常我们所说的服务端套接字通道,每一个请求都会创建一个SocketChannel

Scatter和Gather

Java nio在Channel实现类也实现了Scatter和Gather相关类

  • Scatter.read()是从通道读取的操作能把数据写入多个buffer,即一个通道向多个buffer写数据的过程,但是read必须写满一个buffer后才会向后移动到下一个buffer,因此read不适合大小会动态改变的数据。代码如下:
1
2
3
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);
Gather.write()是从可以把多个buffer的数据写入通道,write是只会写position到limit之间的数据,因此写是可以适应大小动态改变的数据。代码如下:

1
2
3
4
5
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);

在有些场景会非常有用,比如处理多份需要分开传输的数据,举例来说,假设一个消息包含了header和body,我们可能会把header和body分别放在不同的buffer

Selector(选择器)

Selector用于检查一个或多个NIO Channel的状态是否可读可写,这样就可以实现单线程管理多个Channels,也就是可以管理多个网络连接,NIO非阻塞主要就是通过Selector注册事件监听,监听通道将数据就绪后,就进行实际的读写操作,因此前面说的IO两个阶段,一阶段NIO仅仅是异步监听,二阶段就是同步实际操作数据。

Selector监听事件类别

  • SelectionKey.OP_CONNECT是Channel和server连接成功后,连接就绪
  • SelectionKey.OP_ACCEPT是server Channel接收请求连接就绪
  • SelectionKey.OP_READ是Channel有数据可读时,处于读就绪
  • SelectionKey.OP_WRITE是Channel可以进行数据写入是,写就绪

使用Selector的步骤

  • 创建一个Selector,Selector selector = Selector.open();
  • 注册Channel到Selector上面,将Channel切换为非阻塞的:channel.configureBlocking(false),然后绑定Selector:SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
  • SelectionKey.OP_READ为监听的事件类别,如果要监听多个事件,可利用位的或运算结合多个常量如:int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

代码如下:

1
2
3
4
5
Selector selector = Selector.open();
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.connect(new InetSocketAddress(port));
clientChannel.register(selector, SelectionKey.OP_CONNECT);

需要使用Selector的Channel必须是非阻塞的,FileChannel不能切换为非租塞的,因此FileChannel不适用于Selector

API

  • Selector.select()方法是阻塞的,因此可以放心的将它写在while(true)中,不用担心cpu会空转
  • Selector.wakeup()唤醒select()造成的阻塞,可能是有新的事件注册,优先级更高的事件触发(如定时器事件),希望及时处理。其原理是向通道或者连接中写入一个字节,阻塞的select因为有IO事件就绪,立即返回