1. 介绍
在1.4版本之前,Java的IO库的阻塞IO,简称OIO(Old IO);其后开始,就引入了新的异步IO,称为Java New IO类库,简称Java NIO;主要解决的问题就是同步阻塞的问题;IO模型介绍可参考/mp_blog/creation/editor/125082530
Buffer(缓冲区)Channel(通道)Selector(选择器)Java NIO有三个核心组件:
结合IO模型的介绍,Java NIO属于IO多路复用模型
1.1. Java NIO和OIO的对比
a. OIO是面向流的,NIO是面向缓冲区的;所以OIO是顺序读取,不能随意改变读取位置,而NIO是从缓冲区读数据,可以随意改变读取的位置
b. 一个是阻塞的,一个是非阻塞的;IO模型中有讲就不展开了
c. OIO没有选择器的概念,这个是最主要的差异原始点;NIO的实现是基于底层选择器的系统调用,需要操作系统提供支持,而OIO不需要选择器
1.2. 缓冲区
应用程序与通道的交互主要是读取和写入,NIO为了非阻塞读写,准备了此重要的组件Buffer;通道的读取是将数据从通道读取到缓冲区,通道的写入是将数据冲缓冲区写入到通道中;此特性是OIO所没有的,也是NIO非阻塞的重要前提之一;
1.3. 通道
在OIO中,一个网络连接会关联两个流:一个输入流,一个输出流(InputStream OutputStream)
在NIO中,使用通道的概念进行表示,即可以从通道中读取数据,也可以向通道写入数据
1.4. 选择器
选择器可以理解为是IO事件的监听和查询器,通过一个进程或者线程查询多个通道的IO事件的就绪状态;与OIO相比,NIO最大的优势是由于统一的监听和查询器,大大减小了系统开销,这种高效取决于Java Selector和操作系统IO多路复用的支持
理论扯差不多了,接下去扯一扯Java NIO中的Buffer
2. Buffer类
Buffer是一个抽象类,在java.nio包中,它就是一个内存块(由于数据类型不一致,内存块都定义在子类中)
具体实现有8种:
ByteBufferCharBufferDoubleBufferFloatBufferIntBufferLongBufferShortBufferMappedByteBuffer
前7种覆盖了Java的基本数据类型;第8种是专门用于内存映射的
ByteBuffer是最最最常用的
2.1. Buffer类的重要属性
2.1.1. capacity属性
表示缓冲区的大小,即能写入数据的容量;此属性初始化后,就不能再进行修改,数组内存分配好后就不能再进行修改了
说通俗点,就是大家学java时,实例化一个数组的代码byte[] hb = new byte[1024];这个1024就是capacity,实例化后hb的大小就不能改了,除非重新初始化个新的数组,把hb的指针指向新的内存块;没很高深,大家都会
2.1.2. position属性
表示当前操作的位置,为什么叫操作的位置?因为和读写模式有关
写模式
> 刚进入写模式时(初始化allocate或者flip时),position值为0,从头开始写入
> 每当一个数据写到缓冲区之后,position会向后移动(nextPutIndex方法中,获取当前写入位置后,position+1)
> 当position到达limit时,缓冲区满,无法再写入,否则会报BufferOverflowException异常
读模式
> 刚进入读模式时(lip时),position值为0
> 从缓冲区读取数据后,position会向后移动(nextGetIndex方法中,获取当前读取位置后,position+1)
>当position到达limit时,缓冲区无数据可读,否则会报BufferOverflowException异常
2.1.3. limit属性
表示可读取或者写入的数据最大上限
> 写模式下,limit会在初始化时被设置成缓冲区的capacity值,表示可以将缓冲区写满
> 读模式下,limit表示能从缓冲区中读多少数据
先写才能读,所以默认是写模式的,写完后flip翻转一下就是模式切换,这时limit值就会发生变化,flip时主要进行调整的也是position和limit两个属性,这种调整比较微妙(通俗点说就是大家有机会去写这个JavaNIO代码时,这是第一个大家容易写出bug的地方)
用一个例子来讲吧:
a. 先申请一个10长度的ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
此时,capacity=10,由于是新创建模式为写模式,position=0
b. 写入三个数据
byteBuffer.put((byte)1);byteBuffer.put((byte)2);byteBuffer.put((byte)3);
此时position=3
c. 数据读取
byteBuffer.flip();
此时,模式变为读模式,position=0,limit=3,表示从0开始读取数据,最多读三个
同时还有一个mark属性,用来临时记录position的位置,需要的时候再恢复至position上
2.2. Buffer类的重要方法
2.2.1. allocate()
用于创建一个固定大小的缓冲区对象,抽象方法定义在Buffer类中,在子类中具体实现
例如
ByteBuffer.allocate(10):创建一个ByteBuffer对象,分配内存空间10 * 1字节(new byte[10])
IntBuffer.allocate(10):创建一个IntBuffer对象,分配内存空间10 * 4字节(new int[10])
这个方法比较简单,核心的内容就是实例化一个对应的数组(学程序入门时创建数组的代码)
2.2.2. put()
在初始化完成后,即可通过put方法进行数据的写入
byteBuffer.put((byte)1);
2.2.3. flip()
数据写入后,是不能直接读取的,由于position,limit这些数据读写公用,所以需要进行模式切换去调整这些值,达到可读取的状态;所以flip就是这个作用进行模式翻转,转成读模式,以下是flip的源码
/*** Flips this buffer. The limit is set to the current position and then* the position is set to zero. If the mark is defined then it is* discarded.** <p> After a sequence of channel-read or <i>put</i> operations, invoke* this method to prepare for a sequence of channel-write or relative* <i>get</i> operations. For example:** <blockquote><pre>* buf.put(magic); // Prepend header* in.read(buf);// Read data into rest of buffer* buf.flip(); // Flip buffer* out.write(buf); // Write header + data to channel</pre></blockquote>** <p> This method is often used in conjunction with the {@link* java.nio.ByteBuffer#compact compact} method when transferring data from* one place to another. </p>** @return This buffer*/public final Buffer flip() {limit = position;position = 0;mark = -1;return this;}
然后问题来了,如果读完后是不是再flip一下变成写模式?
悲伤的告诉你,不是的
可以通过clear()或者compact()转换成写模式,上面的源码可以看出,flip时,第一个limit=position,flip时limit只小不大
顺便也提一下几个属性的大小顺序
mark <= position <= limit <= capacity
2.2.4. get()
这个方法也比较简单,当flip变成读模式时,就可以通过get()方法一个个读取数据了,每get一次,position指向下一个可读的下标
2.2.5. rewind()
这个方法也比较简单,你已经读完数据了,但是还想再读一遍?rewind一下
/*** Rewinds this buffer. The position is set to zero and the mark is* discarded.** <p> Invoke this method before a sequence of channel-write or <i>get</i>* operations, assuming that the limit has already been set* appropriately. For example:** <blockquote><pre>* out.write(buf); // Write remaining data* buf.rewind();// Rewind buffer* buf.get(array); // Copy data into array</pre></blockquote>** @return This buffer*/public final Buffer rewind() {position = 0;mark = -1;return this;}
rewind和flip其实很像,只是rewind不动limit属性,而flip会修改limit,所以连续flip两次就凉凉了,因为limit=0了
2.2.6. mark()和reset()
就像游戏里的一些标记技能,标记英雄的位置,再手动触发使英雄回到标记的位置
当你读取到某个位置后,希望后续再从这个位置开始,就可以mark一下,当你希望的时候reset一下,再从这个位置开始读数据
2.2.7. clear()
在读模式下,调用clear()将缓冲区切换成写模式
> 将position=0
> 将limit=capacity
2.2.8. 综上所诉,常见的操作步骤如下
> allocate创建缓冲区
> put写入数据
> flip切换成读模式
> get读取数据
> clear切换成写模式,继续put写入
3. Channel类
一个通道可以表示一个底层的文件描述符(什么是文件描述符(file descriptor)这里就不展开了),例如文件、网络连接,Java NIO中通道比较细化,本文主要讲其中4个
FileChannel:文件通道,用于文件读写SocketChannel:套接字通道,用于TCP连接数据读写ServerSocketChannel:服务器套接字通道,用于监听TCP请求连接DatagramChannel:数据报通道,用于UDP数据读写
3.1. FileChannel
FileChannel是专门用于操作文件的通道,可读写文件,其为阻塞模式,不能设置为非阻塞模式
3.1.1. 获取FileChannel
a. 通过文件输入流
FileInputStream fis = new FileInputStream(file);FileChannel channel = fis.getChannel();
b. 通过文件输出流
FileOutputStream fos = new FileOutputStream(file);FileChannel channel = fos.getChannel();
c. RandomAccessFile
RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");FileChannel channel = raf.getChannel();
3.1.2. 读取FileChannel
RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");FileChannel channel = raf.getChannel();ByteBuffer buf = ByteBuffer.allocate(1024);int length = -1;while ((length = channel.read(buf)) != -1) {}
3.1.3. 写入FileChannel
buf.flip();int length = -1;while ((length = channel.write(buf)) != 0) {}
3.1.4. 关闭通道
通道使用完后需要将其关闭
channel.close()
3.1.5. 数据刷盘
操作系统出于性能考虑,不会实时刷盘,需要时,可自行调用fore方法进行即时刷盘
channel.force(true);
3.2. SocketChannel
针对网络连接有两个类,一个SocketChannel,一个ServerSocketChannel
ServerSocketChannel负责连接的监听,SocketChannel负责数据传输
ServerSocketChannel仅用于服务器,SocketChannel双端使用
两者都支持阻塞和非阻塞模式,通过configureBlocking进行设置
3.2.1. 获取SocketChannel
SocketChannel socketChannel = SocketChannel.open();socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress("127.0.0.1", 80))
注意非阻塞模式下,connect方法不阻塞,但是有可能未连上服务器,需要自旋socketChannel.finishConnect()检测是否连接上服务器
3.2.2. 读取SocketChannel
和文件类似
socketChannel.read(buf)
3.2.3. 写入SocketChannel
和文件类似
socketChannel.write(buf)
3.2.4. 关闭SocketChannel
socketChannel.shutdownOutput();
IOUtil.closeQuietly(socketChannel);
3.3. DatagramChannel
用来处理UDP的数据传输
3.3.1. 获取DatagramChannel
DatagramChannel datagramChannel = DatagramChannel.open();datagramChannel.configureBlocking(false);datagramChannel.connect(new InetSocketAddress("127.0.0.1", 80))
3.2.2. 读取DatagramChannel
datagramChannel.receive(buf)
3.2.3. 写入DatagramChannel
datagramChannel.send(buf, new InetSocketAddress("127.0.0.1", 80))
3.2.4. 关闭DatagramChannel
datagramChannel.close();
4. Selector类
选择器是什么?
简而言之,选择器的使命是完成IO多路复用,进行通道的注册、监听、事件查询;通道和选择器之间的管理通过register的方式完成,Channel.register(selector, int ops)
可供选择器监控的通道IO事件类型包括以下4种:
可读:SelectionKey.OP_READ可写:SelectionKey.OP_WRITE连接:SelectionKey.OP_CONNECT接收:SelectionKey.OP_ACCEPT
传递多种事件可通过按位或进行实现
SelectionKey.OP_READ |SelectionKey.OP_WRITE
4.1. SelectableChannel
并不是所有的通道都能被选择器监控和选择的,例如上面提到的FileChannel,只有继承了SelectableChannel的通道才可以
4.2.SelectionKey
通道和选择器之间的关系注册成功后,具体的选择工作可通过Selector的select方法来完成,选择器不停的轮训通道中的状态,并返回注册过的IO事件,SelectionKey就是那些被选择器选中的事件
// 实例化选择器Selector selector = Selector.open();// 获取通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8080));// 注意:此部分仅样例代码,由于非阻塞模式,需要注意通道是否准备好等,此处不体现// 将通道注册到选择器上serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// 然后轮询感兴趣的IO事件while (selector.select() > 0) {Set selectedKeys = selector.selectedKeys();// 轮询处理对应的事件}
其中select()方法有多个重载实现版本
select():阻塞调用,至少有一个事件select(timeout):和select一样,只是增加了超时时间selectNow():非阻塞,不管有没有都会立刻返回
整个理论部分就先讲到这里,后面会补充一些代码样例,未完待续...