1.NIO和普通IO
在JDK1.4中,Java引进了新的IO框架——NIO。原有IO系统基于“流”来实现,NIO则是基于“块”来实现的。
流与块:
面向流的IO系统在处理数据时是一个字节一个字节地进行的,哪怕是字符流,其本质也是通过字节流来实现的,面向流处理数据是比较简单的,可以通过链接过滤器的方式丰富IO操作(过滤流)。但是比较不利的是,流式IO非常慢。
面向块的IO系统以块的形式处理数据,每一个操作都在一步中产生或消费一个数据块。面向块的IO虽然复杂度较流式IO更高,但是其速度非常快。
阻塞型和非阻塞型:
NIO系统是非阻塞型IO,而普通IO则是阻塞型的。阻塞型是指在对数据流的操作过程中,如read方法是通过调取操作系统底层方法来实现数据读取的,如果底层端口没有足够数据,那么该方法就会一直等待数据过来,这样就会形成阻塞。而NIO则不一样,它通过监听等方式实现了数据操作的非阻塞。
2.测试实现
//NIO文件复制测试 public static void main(String[] args) throws Exception { FileInputStream fileInputStream = new FileInputStream("D:\\Baofeng5.exe"); FileOutputStream fileOutputStream = new FileOutputStream("D:\\COPYBaofeng5.exe"); FileChannel channelIn = fileInputStream.getChannel(); FileChannel channelOut = fileOutputStream.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(128); while(true){ buffer.clear(); int i = channelIn.read(buffer); if (i == -1) { break; } buffer.flip(); channelOut.write(buffer); } }
上面的实例通过FileInputStream获取FileChannel,FileChannel是一个通道(channel)对象,Channel就像是普通IO中的“流”,但它与流不同:Channel是双向的,而流只能是一个方向。Channel可以用于读、写,或同时用于读写。
上例中还使用了ByteBuffer,每一个Buffer类都是Buffer抽象类的子类,除了ByteBuffer,每个Buffer类都有完全一样的操作,只是它们处理的数据类型不一样。由于大部分标准IO操作都使用ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有操作。
输入通道channelIn将数据读到缓冲区Buffer中,然后将缓冲区Buffer中的数据写入输出通道channelOut。其中读写之间调用的buffer.flip()方法,JavaDOC中对该方法有这样一个定义:
After a sequence of channel-read or put operations, invoke this method to prepare for a sequence of channel-write or relative get operations.
也就是说,在一系列读通道或写入操作后,调用这个方法,可以让缓冲区准备进行一系列的写通道或对应的读取操作。
上面实例中,对于数据的读取和写入,都没有明确标明具体的量,这是因为缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。
3.相关原理
3.1缓冲区内部细节
NIO中两个重要的缓冲区组件:状态变量和访问方法(accessor)。
- 状态变量:可以用三个值指定缓冲区在任意时刻的状态——position、limit、capacity
这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。
- position
缓冲区从本质上是包装过的数组,从通道读取的数据被放在底层数组中。position变量跟踪的是已经往数组中写了多少数据,更准确地讲,它指定了下一个字节将放在数组的哪一个位置。例如:通道已经往缓冲区读入了3个字节,那么其position将会被设置为3,也就是指向了数组的第4个元素。根据前面的说法,可以理解成数组中写了3个元素,下一个字节将放在下标为3,即第4个元素。
同理,将缓冲区的数据写入通道区的时候,position记录的是从缓冲区中获取的数据量,更准确地说,是下一个从缓冲区中获取的字节在数组的哪一个位置。例如:从缓冲区写入通道区,写了4个元素后,position会被设置为4,指向数组的第5个元素。
- limit
limit变量表明还有多少数据需要取出(从缓冲区写入通道),或者还有多少空间可以放入数据(从通道读入缓冲区)。
position总是小于或等于limit。
- capacity
capacity即容量,它表示可以存储在缓冲区的最大容量,实际上它表示了底层数据大小(或者至少是准许我们使用的数组容量)。
limit不能大于capacity。
- 关于变量的作用
首先一个例子:
public static void main1(String[] args) throws Exception { FileOutputStream stream = new FileOutputStream("D:\\a.txt"); FileChannel inChannel = stream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(8); byte[] asdfg = "asdfg".getBytes(); for (int i = 0; i < asdfg.length; i++) { byteBuffer.put(asdfg[i]); } byteBuffer.flip(); System.out.println(inChannel.write(byteBuffer)); } public static void main2(String[] args) throws Exception { FileOutputStream stream = new FileOutputStream("D:\\a.txt"); FileChannel inChannel = stream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(8); byte[] asdfg = "asdfg".getBytes(); for (int i = 0; i < asdfg.length; i++) { byteBuffer.put(asdfg[i]); } //byteBuffer.flip(); System.out.println(inChannel.write(byteBuffer)); }
上面两个方法,除了被注释的那行(byteBuffer.flip()),其余一模一样。但是第一个方法的执行结果是5,第二个方法则是3。
首先这里说一下结论:输出通道的write方法,将缓冲区中的数据写入通道,在前面说过,NIO的通道输入输出不需要明确指明输入输出的量,但是这个量在通道中是怎么控制的呢?其实,输出通道的write方法,它是指:将缓冲区的数据,从position位置开始(包含),到limit位置结束(不包含),之间的数据写入通道。
先看main2()方法,byteBuffer一共8个字节的容量,在初始时候,position为0,limit和capacity都是8,当byteBuffer中放入"asdfg"拆解的byte[]数组后(每次调用byteBuffer的put方法往缓冲区放一个字节,会将position加1),position变为5,limit不变。接下来直接调用write方法,根据上面的结论可知,通道会把byteBuffer中第5个位置开始(包含)到第8个位置结束(不包含)的数据写入通道,由于"asdfg"只占据缓冲区的前五位,后三位是byte[]数据的默认值,也就是byte型的0,所以实际上往通道写入的是3个byte型的0,这样在打开a.txt文件后,会发现里面是3个空格。
再看main1()方法,前面都一样,只是在写入前调用了byteBuffer.flip()方法,api中对缓冲区的flip方法是这样定义的:它首先将limit设为当前的position,再将position设为0。这样一来,byteBuffer的limit变为5,position变为0,这时候再调用write方法,写入的就是从位置0到位置5之间的数,也就是5个byte型字节:a、s、d、f、g。这样在打开a.txt后,会发现里面是“asdfg”。
这也就是为什么在write前需要调用flip方法的原因。
TIP:结合这里可以回顾之前文件复制代码中,之所以在read和write之间调用flip方法,就是为了通过改变limit和position,保证write方法写入的是之前read进的数据。
缓冲区有一个remaining()方法,它的返回值是int,表示的是positon和limit之间的数据量(Returns the number of elements between the current position and the limit.)。
此外,缓冲区还有一个比较比较重要的方法:clear()方法。该方法会做两件事:第一,将limit设置为与capacity相同;第二,它设置position为0。也就是将缓冲区的状态变量恢复到初始状态。
- 访问方法
get()方法:
-
- byte get();
- ByteBuffer get( byte dst[] );
- ByteBuffer get( byte dst[], int offset, int length );
- byte get( int index );
第一个方法获取单字节——实际上获取的是当前position位置的字节,并且将position加1。
第二个方法是获取dst.length个字节——也是从当前position位置取,取dst.length个字节后,将position加dst.length。如果dst.length>(limit-position)则报java.nio.BufferUnderflowException。
第三个方法与第二个方法类似,其中后面的offset和length是控制写入dst数组的起始位置和写入数量。
第四个方法与前三个不一样,它会直接忽略观察变量(绕过它们,并且获取数据后也不影响它们)。它直接获取缓冲区index位置上的数据。
put()方法:
-
- ByteBuffer put( byte b );
- ByteBuffer put( byte src[] );
- ByteBuffer put( byte src[], int offset, int length );
- ByteBuffer put( ByteBuffer src );
- ByteBuffer put( int index, byte b );
第一个方法往缓冲区写入单字节——获取当前postion位置的字节,并将positon位置加1.
第二三方法往缓冲区写入来自byte数组的一组字节,只是offset和length控制写入的起始位置和数量。
第四个方法将给定缓冲区src写入目标缓冲区,该写入的内容remaining()方法中指定的数据,即position和limit之间的数据。
第五个方法则是将字节写入缓冲区指定的位置。
所有上述方法的返回值均是ByteBuffer,返回的都是缓冲的this值。
- 缓冲区的使用:一个内部循环
while(true){ buffer.clear(); int i = channelIn.read(buffer); if (i == -1) { break; } buffer.flip(); channelOut.write(buffer); }
截取前面测试实现DEMO中的一段,该实例概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。
通过简化后的read和write方法实现数据的读取和写入,通过clear和flip方法用于将缓冲区进行读写转换。
3.2关于缓冲区的更多内容
- 缓冲区分配与包装
根据前面实例可知,缓冲区的创建是通过ByteBuffer的静态方法allocate()来实现。Buffer的allocate()方法分配一个具有指定大小的底层数组,并将其包装到一个缓冲区对象中。
其实也可以通过静态方法wrap(byte[] array)将一个现有数组array转换为缓冲区,但是需要注意的是:这类操作需要非常小心,因为完成包装后,通过数组或缓冲区都直接访问底层数据。
public static void main(String[] args) throws Exception{ byte[] s = {‘h‘,‘e‘,‘l‘,‘l‘,‘o‘}; ByteBuffer buffer = ByteBuffer.wrap(s); System.out.println(new String(buffer.array())); //对数组进行修改,同样会影响buffer的变化 s[0] = ‘N‘; System.out.println(new String(buffer.array())); //对buffer进行修改,同样会影响数组的变化 buffer.put((byte)‘K‘); System.out.println(new String(s)); }
根据上面的实例代码可以看出,在包装buffer后,对被包装的数组s的修改,同样会影响buffer;而对buffer的修改,也会影响数组的变化。
- 缓冲区分片与数据共享
slice()方法可以根据现有缓冲区创建一个子缓冲区(从position到limit)。需要注意的是:新创建的子缓冲区与原来缓冲区共享该子缓冲区的数据。
缓冲区片在用于只处理缓冲区部分数据很有帮助,slice()方法帮助我们取出指定片,然后通过处理普通缓冲区的方法处理该片,就可以达到处理整个缓冲区中的一部分数据的目的。
public static void main(String[] args) throws Exception{ byte[] s = {‘h‘,‘e‘,‘l‘,‘l‘,‘o‘}; ByteBuffer buffer = ByteBuffer.wrap(s); buffer.limit(2); ByteBuffer slice = buffer.slice(); //处理slice slice.put((byte)‘N‘); slice.put((byte)‘O‘); System.out.println(new String(buffer.array())); }
该段代码的输出为:NOllo。
- 只读缓冲区
只读缓冲区只能读取,不能写入。缓冲区的asReadOnlyBuffer()方法,可以将任何常规缓冲区转换为只读缓冲区。只读缓冲区不能被转换为可读写缓冲区。
- 直接和间接缓冲区
直接缓冲区是为了加快IO速度,以一种特殊的方式分配其内存的缓冲区。ByteBuffer提供静态方法allocateDirect(int capacity)构造一个直接缓冲区。
- 内存映射文件IO
内存映射文件IO是一种读写文件数据的方法,它可以比基于流或通道的IO快得多。
3.3联网和异步IO
之所以将NIO称为非阻塞IO,就是因为它可以异步实现。
NIO非阻塞的实现原理:
1.由一个专门的线程来处理所有的IO事件,并负责分发;
2.事件驱动机制:事件到的时候触发,而不是同步地去监听事件;
3.线程通讯:线程之间通过wait、notify等方式通讯。保证每次上下文切换是有意义的,减少无谓的线程切换。
专门用于处理IO事件的是Selector,它会轮询每个注册的channel,一旦发现channel有事件发生,便获取该事件并进行处理。
4.非阻塞IO实现
package nio; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; 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; /** * NIO测试服务端 * @author Leo * */ public class NIOServer { //通道管理器 private Selector selector; //初始化方法,获取一个ServerSocket通道,并将该通道初始化 public void init(int port) throws IOException{ //获取一个ServerSocket通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //设置为非阻塞 serverSocketChannel.configureBlocking(false); //获取通道对应的serverSocket,并绑定到指定端口 ServerSocket serverSocket = serverSocketChannel.socket(); serverSocket.bind(new InetSocketAddress(port)); this.selector = Selector.open();//获得一个通道管理器 //将该通道管理器与该通道绑定,并为其注册SelectionKey.OP_ACCEPT事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } /** * 采用轮询的方式监听selector(通道管理器)上是否有需要处理的事件,如果有,则进行处理 * @throws IOException */ public void listen() throws IOException{ System.out.println("服务端启动,开始监测是否有需要处理的事件"); //轮询访问selector while (true) { //当注册的事件到达后,方法返回;否则,该方法一直阻塞 selector.select(); //获得selector中选中项(即是注册的事件)的迭代器 Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); //客户端请求连接事件 if (key.isAcceptable()) { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); //获得与客户端连接的通道 SocketChannel channel = serverSocketChannel.accept(); //设置成非阻塞 channel.configureBlocking(false); //通过该通道往客户端输入数据 channel.write(ByteBuffer.wrap("服务端往客户端发送的信息".getBytes())); //为了让客户端可以与服务端进行传递数据,需要给通道设置读权限 channel.register(selector, SelectionKey.OP_READ); } else if(key.isReadable()){//可读事件 serverRead(key); } //操作完毕,删除事件的key,防止重复处理 iterator.remove(); } } } /** * 服务端处理客户端发来的信息 * @param key */ private void serverRead(SelectionKey key) throws IOException{ SocketChannel socketChannel = (SocketChannel) key.channel(); //创建字符缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); socketChannel.read(buffer); String msg = new String(buffer.array()).trim(); msg = "服务端接收到的信息为:" + msg; System.out.println(msg); socketChannel.write(ByteBuffer.wrap(msg.getBytes())); } public static void main(String[] args) throws IOException{ NIOServer nioServer = new NIOServer(); nioServer.init(8000); nioServer.listen(); } }
package 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; /** * NIO测试客户端 * @author Leo * */ public class NIOClient { /** * 通道管理器 */ private Selector selector; /** * 初始化方法,获取socket通道,并初始化该通道 * @param ip * @param port * @throws IOException */ public void initClient(String ip, int port) throws IOException{ SocketChannel channel = SocketChannel.open(); //设置通道为非阻塞 channel.configureBlocking(false); //获取通道管理器 this.selector = Selector.open(); //客户端准备连接服务端,该方法并不真正连接,后续有finishConnect方法才能完成连接 channel.connect(new InetSocketAddress(ip, port)); //绑定通道与通道管理器,并为该通道注册SelectionKey.OP_CONNECT事件 channel.register(selector, SelectionKey.OP_CONNECT); } /** * 轮询方式监听该selector上是否有需要处理的时间,如果有则进行处理 * @throws IOException */ private void listen() throws IOException{ while (true) { selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isConnectable()) { SocketChannel channel = (SocketChannel) key.channel(); //如果正在连接,则完成连接 if (channel.isConnectionPending()) { channel.finishConnect(); } //设置非阻塞 channel.configureBlocking(false); //往服务器发送消息 channel.write(ByteBuffer.wrap("客户端往服务端发送的信息".getBytes())); //在于服务端连接成功后,为了可以接收到服务端的消息,需要给通道设置读权限 channel.register(selector, SelectionKey.OP_READ); } else if(key.isReadable()){ clientRead(key); // break loop; } //删除已处理的key,防止重复 iterator.remove(); } } } private void clientRead(SelectionKey key) throws IOException{ SocketChannel socketChannel = (SocketChannel) key.channel(); //创建字符缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); socketChannel.read(buffer); String msg = new String(buffer.array()).trim(); msg = "服务端接收到的信息为:" + msg; System.out.println(msg); //停止发送消息,否则会死循环 // socketChannel.write(ByteBuffer.wrap("客户端接收到服务端的消息后又往服务端发送的消息".getBytes())); } public static void main(String[] args) throws IOException{ NIOClient client = new NIOClient(); client.initClient("localhost", 8000); client.listen(); } }
5.NIO相关技术框架(Netty、MINA)
后续新开文章