探索Java NIO
什么是NIO?
java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO),NIO提供了与标准IO不同的IO工作方式。
核心部分:
-
- Channels(通道)
- Buffers(缓冲区)
- Selectors
- 除此之外还有组件,像Pipe、FileLock,但这些都是建立在以上三个核心基础之上的。
与传统IO的区别:
IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
传统IO的流是阻塞的,这就意味着当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。NIO的非阻塞模式,不是保持线程阻塞,在数据可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
Channel
在标准的IO当中,都是基于字节流/字符流进行操作的,而在NIO中则是是基于Channel和Buffer进行操作,其中的Channel的虽然模拟了流的概念,类似与流,但并不相同。
区别 | Stream | Channel |
---|---|---|
支持异步 | 不支持 | 支持 |
是否可双向传输数据 | 不能,只能单向 | 可以,既可以从通道读取数据,也可以向通道写入数据 |
是否结合Buffer使用 | 不 | 必须结合Buffer使用 |
性能 | 较低 | 较高 |
Channel必须结合着Buffer使用,不能直接往通道里面读写数据
Channel的实现类
-
- FileChannel(从文件中读写数据)
- DatagramChannel(通过UDP读写网络传输的数据)
- SocketChannel(通过TCP读写网络传输的数据)
- ServerSocketChannel(可以监听CP连接,对每一个新的连接都会创建一个SocketChannel)
简单的一个读写文件的实例:
RandomAccessFile aFile = new RandomAccessFile("filePath/inFileName", "rw"); RandomAccessFile outFile = new RandomAccessFile("filePath/outFileName", "rw"); FileChannel inChannel = aFile.getChannel(); FileChannel outChannel = outFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(1024); int bytesRead = inChannel.read(buf); while (bytesRead != -1) { buf.flip(); while(buf.hasRemaining()){ outChannel.write(buf); } buf.clear(); bytesRead = inChannel.read(buf); } aFile.close(); }
在读取数据中遇到中文乱码问题
Charset charset = Charset.forName("UTF-8");// 创建UTF-8字符集,或者 Charset.forName("GBK"); System.out.print(charset.decode(buf));
数据传输
假如有两个Channel,我们可以直接把Channel1的数据传输给Channel2
主要的方法:
transferFrom(ReadableByteChannel src, long position, long count) transferTo(long position, long count, WritableByteChannel target)
// 从position为0的位置把大小为Channel1.size()的数据写到Channel2中。 Channel2.transferFrom(Channel1, 0, Channel1.size()); Channel1.transferTo(0, Channel2.size(), Channel2);
Buffer
缓冲区(Buffer)就是在内存中预留指定字节数的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区;在Java NIO中,缓冲区的作用也是用来临时存储数据,可以理解为是I/O操作中数据的中转站。缓冲区直接为通道(Channel)服务,写入数据到通道或从通道读取数据,这样的操利用缓冲区数据来传递就可以达到对数据高效处理的目的。在NIO中主要有八种缓冲区类(其中MappedByteBuffer是专门用于内存映射的一种ByteBuffer)
Buffer的基本用法
实际上在Channel的例子中已经体现了Buffer的使用方法:
- 创建一个特定长度的Buffer
- 读取数据到Buffer
- 调用flip()方法,使向Buffer写数据转换为向Buffer读数据
- 从Buffer中读取数据
- 调用clear()或者compat()方法对缓冲区进行清空
clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
Buffer重要的三个属性
- capacity
- position
- limit
position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。
他们三者在读写时候的关系如图所示:
capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
Buffer的类型
这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。
MappedByteBuffer
在FileChannel上调用map方法 返回一个MappedByteBuffer对象
MapMode 有三种Mode:READ_ONLY 、READ_WRITE 、PRIVATE(通过put方法对MappedByteBuffer的修改 不会修改到磁盘文件 只是虚拟内存的修改)
MappedByteBuffer继承与Buffer,在父类的基础上增加了三个方法:
- force缓冲区在READ_WRITE模式下,此方法对缓冲区所做的内容更改强制写入文件
- load:将缓冲区的内容载入物理内存,并返回该缓冲区的引用
- isLoaded:判断缓冲区的内容是否在物理内存,如果在则返回true,否则返回false
MappedByteBuffer的用法和ByteBuffer的用法类似,只是创建有所差别
// 创建一个MappedByteBuffer MappedByteBuffer mappedByteBuffer = inChannel.map(MapMode.READ_ONLY, 0, fileChannel.size());
如果要把数据写入磁盘中需调用force()方法,如果不调用只会更新内存中的值。
Scatter/Gather
分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。
scatter样例
// 定义一个ByteBuffer的数组 ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; // Buffer从Channel中读取数据 channel.read(bufferArray);
数据在读入到buffer中的时候,优先填满第一个buffer,如果在消息传输中,header的字节小于第一个buffer的(capacity)容量大小,那么就会把body的一部分数据写到header中,破坏数据。因此scatter不适合做动态消息。
gather样例
// 定义一个Byte Buffer数组 ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); // 把Buffer中的数据写到Channel中 ByteBuffer[] bufferArray = { header, body }; channel.write(bufferArray);
而这种方式会依次把数组中的ByteBuffer写道Channel中,因此不会打破原始数据。因此gather可用作动态消息的场景中。
Selector(此处内容部分来自于与小菜fly的基于NIO的Socket通信)
选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。
选择器执行过程
- 创建一个或者多个可选择的通道
- 将这些创建的通道注册到选择器对象中
- 选择键会记住开发者关心的通道,它们也会追踪对应的通道是否已经就绪
- 开发者调用一个选择器对象的select()方法时,相关的键会被更新,用来检查所有被注册到该选择器的通道
- 获取一个键的集合,从而找到当时已经就绪的通道,通过遍历这些键,开发者可以选择出每个从上次调用select()开始直到现在已经就绪的通道
一个选择器可以被注册多个Channel(通道),选择器可以轮番从迭代器SelectedKeys获取注册的事件。
服务端和客户端各自维护一个通道调度器(Selector)对象,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。
服务端和客户端的实例
package cn.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; /** * * @author*/ public class NIOServer { //通道调度器 private Selector selector; /** * 获得一个ServerSocket通道,并对该通道做一些初始化的工作 * @param port 绑定的端口号 * @throws IOException */ public void initServer(int port) throws IOException { // 获得一个ServerSocket通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 设置通道为非阻塞 serverChannel.configureBlocking(false); // 将该通道对应的ServerSocket绑定到port端口 serverChannel.socket().bind(new InetSocketAddress(port)); // 获得一个通道调度器 this.selector = Selector.open(); //将通道调度器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后, //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。 serverChannel.register(selector, SelectionKey.OP_ACCEPT); } /** * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 * @throws IOException */ @SuppressWarnings("unchecked") public void listen() throws IOException { System.out.println("server start success"); // 轮询访问selector while (true) { //当注册的事件到达时,方法返回;否则,该方法会一直阻塞 selector.select(); // 获得selector中选中的项的迭代器,选中的项为注册的事件 Iterator ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = (SelectionKey) ite.next(); // 删除已选的key,以防重复处理 ite.remove(); // 客户端请求连接事件 if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key .channel(); // 获得和客户端连接的通道 SocketChannel channel = server.accept(); // 设置成非阻塞 channel.configureBlocking(false); //在这里可以给客户端发送信息哦 channel.write(ByteBuffer.wrap(new String("send a message to client").getBytes())); //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); // 获得了可读的事件 } else if (key.isReadable()) { read(key); } } } } /** * 处理读取客户端发来的信息 的事件 * @param key * @throws IOException */ public void read(SelectionKey key) throws IOException{ // 服务器可读取消息:得到事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 创建读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(10); channel.read(buffer); byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("received message from client:"+msg); ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes()); channel.write(outBuffer);// 将消息回送给客户端 } /** * 启动服务端测试 * @throws IOException */ public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.initServer(8000); server.listen(); } }
package com.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; /** * * @author 14 */ public class NIOClient { //通道调度器 private Selector selector; /** * 获得一个Socket通道,并对该通道做一些初始化的工作 * @param ip 连接的服务器的ip * @param port 连接的服务器的端口号 * @throws IOException */ public void initClient(String ip,int port) throws IOException { // 获得一个Socket通道 SocketChannel channel = SocketChannel.open(); // 设置通道为非阻塞 channel.configureBlocking(false); // 获得一个通道调度器 this.selector = Selector.open(); // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调 //用channel.finishConnect();才能完成连接 channel.connect(new InetSocketAddress(ip,port)); //将通道调度器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。 channel.register(selector, SelectionKey.OP_CONNECT); } /** * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 * @throws IOException */ @SuppressWarnings("unchecked") public void listen() throws IOException { // 轮询访问selector while (true) { selector.select(); // 获得selector中选中的项的迭代器 Iterator ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = (SelectionKey) ite.next(); // 删除已选的key,以防重复处理 ite.remove(); // 连接事件发生 if (key.isConnectable()) { SocketChannel channel = (SocketChannel) key .channel(); // 如果正在连接,则完成连接 if(channel.isConnectionPending()){ channel.finishConnect(); } // 设置成非阻塞 channel.configureBlocking(false); //在这里可以给服务端发送信息哦 channel.write(ByteBuffer.wrap(new String("send a message to server").getBytes())); //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); // 获得了可读的事件 } else if (key.isReadable()) { read(key); } } } } /** * 处理读取服务端发来的信息 的事件 * @param key * @throws IOException */ public void read(SelectionKey key) throws IOException{ //和服务端的read方法一样 } /** * 启动客户端测试 * @throws IOException */ public static void main(String[] args) throws IOException { NIOClient client = new NIOClient(); client.initClient("localhost",8000); client.listen(); } }
非阻塞式服务器
参考外国友人搭建的非阻塞式服务器,我把GitHub上的项目fork到我自己的仓库里可供大家学习