30分钟掌握NIO
NIO对文件的操作
先来看一段BIO也就是传统IO复制文档的代码片段。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public static void bioCopy() throws Exception { //读取这个文件 FileInputStream in = new FileInputStream("C:\\Users\\szwb295\\Desktop\\1.txt"); //往这个文件写入内容 FileOutputStream out = new FileOutputStream("C:\\Users\\szwb295\\Desktop\\2.txt"); //输入流缓冲区 BufferedInputStream bufferedInputStream = new BufferedInputStream(in); //输出流缓冲区 BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out); //临时保存字节,字节数组的 Capacity(容量) 为1024 最多一次放1024个字节 byte[] b = new byte[3]; //一次读取到的字节数量 int len; /* * 例如 b的Capacity为3,总共需要读取的数据10,则read()方法会执行5次len分别为3,3,3,1,-1 * */ while ((len = bufferedInputStream.read(b)) != -1) { //将每次放入字节数组中的数据写入到流中,position(位置),0开始写,最大限度(limit)为len bufferedOutputStream.write(b, 0, len); } //关闭流 bufferedInputStream.close(); bufferedOutputStream.close(); }
下面看NIO复制文件代码片段。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public static void nioCopy() throws Exception { // 1. 输入流 FileInputStream fin = new FileInputStream("C:\\Users\\szwb295\\Desktop\\a.txt"); //输出流 FileOutputStream fout = new FileOutputStream("C:\\Users\\szwb295\\Desktop\\b.txt"); // 2. 获取数据源的输入输出通道 FileChannel fcin = fin.getChannel(); FileChannel fcout = fout.getChannel(); // 3. 创建 缓冲区 对象 参数为Buffer的 Capacity(容量)代表这个缓冲区最大存储字节数 ByteBuffer buff = ByteBuffer.allocate(3); int len; // 4. 从通道读取数据 & 写入到缓冲区 // 注:若 以读取到该通道数据的末尾,则返回-1 while ((len = fcin.read(buff)) != -1) { // 5. 将 limit设置为当前位置然后将 position设置为0 buff.flip(); // 6. 从 Buffer 中读取数据 & 传出数据到通道 fcout.write(buff); //将 limit设置为当前位置然后将 position设置为0 buff.flip(); } //直接关闭FileInputStream会自动关闭channel fin.close(); //同上 fout.close(); }
以上NIO代码片段包含两部分内容 Buffer(缓冲区)和Channel(通道)。
我们从InputStream和OutputStream中获取两个通道,通道是连接输入/输出的一端,另一端则是Buffer。
例如 fcin.read(buff)这一行代码是从磁盘中将文件内容读取到Buffer。fcout.write(buff);则是从缓冲区将数据写入到磁盘中。如下图
Channel有多种类型,以上我们通过InputStream/OutputStream获取的是对文件操作的管道。在API文档中还有针对Socket的通道 ServerSocketChannel SocketChannel
ByteBuffer buff = ByteBuffer.allocate(3);创建一个字节类型的缓冲区,其Capacity为3,也就是最大容量为3个字节
Buffer有三个重要的属性:Capacity(容量)代表一个缓冲器最多存储的内容。Position(标记位)一个指针,指向下一个操作的位置,
例如读一个Capacity为10的Buffer,起始位的Position是0每读一个位置增加1。若Position为10则不能继读,0-9。Limit(限制位)代表可以对当前缓冲区操作的最大位置-1
,例如当前缓冲区的Capacity为10,若Limit为5则表示无论是读或写只能到4的位置。
接下来的while循环读,就和BIO的逻辑一样了,若读的返回值不是-1则表示有值,读到的数据放到Buffer中,若Buffer满了,则进入循环体。此时Position,Capacity,Limit一样。
接下来我们要执行写操作,将Buffer中的数据写入到输出流中,我们执行了。flip()方法。该方法会将Limit更改为Position,然后将Position更改为0,表示从0开始,直到Position和Limit
一致则表示已经把缓冲区的数据全部复制到输入流了。此时我们又要往缓冲区写数据,所以在调用flip()将Position更改为0,表示从0开始写入缓冲区,直到Position和Limit。然后诸如
此循环。
其实上边代码循环体最后一个flip()方法使用的不得当。思考一个问题,设缓冲区Capacity为10,若最后一次往Buffer中放数据
的个数为5在循环体内取数据也是5.那么最后一个flip()会将Limit置为5。然后循环结束。但如果我们在循环外想在此使用该Buffer
那无论是读或写最多只能操作5个空间的位置,因为Limit()为5。
Buffer还有两个方法。clear()这个方法将Limit置为Capacity,将Position置为0。rewind()这个方法Limit位置不变,只将Position置为0。
NIO网络编程
关于BIO网络编程,也就是Socket的代码已经弊端,在我的上一篇博客中有写到,感兴趣的可以去看。这里直接贴出来NIO的网络编程。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.datang.pet.control.test; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class NIOClient { public static void main(String[] args) throws Exception { for (int i = 0; i < 20; i++) { new Thread() { @Override public void run() { send(); } }.start(); } } public static void send() { try { //获取一个客户端的通道 SocketChannel clientChannel = SocketChannel.open(); //设置非阻塞客户端 clientChannel.configureBlocking(false); //服务端的地址 InetSocketAddress serverAddr = new InetSocketAddress("localhost", 9999); /* * 当connect()正确的连接上服务端后,才会返回true * 因为上边设置了非阻塞的,意味着无论连接成功或者 * 失败都会执行后续代码,若连接失败则后边发送数据 * 也会失败。 * finishConnect()则是刷新连接,尝试重新连接。 * 如果连接成功返回 true。 * */ if (!clientChannel.connect(serverAddr)) { while (!clientChannel.finishConnect()) { System.out.println("正在尝试重新连接服务端"); continue; } } ByteBuffer byteBuffer = ByteBuffer.wrap("我是客户端".getBytes("UTF-8")); //发送消息 clientChannel.write(byteBuffer); System.out.println("已发送消息"); //将缓存区的limit设置为capacity,将position设为0 byteBuffer.clear(); //获取服务端的响应信息 clientChannel.read(byteBuffer); System.out.println(new String(byteBuffer.array(),"UTF-8")); System.in.read(); } catch (Exception e) { System.out.println(e); } } }
上边代码片段为客户端代码。为了模拟多客户的情况,使用了循环+线程模拟。
clientChannel.configureBlocking(false);这行代码设置了客户端的是否是阻塞的。如果true,则表示是阻塞客户端
意味着在调用 clientChannel.connect(serverAddr)时,如果连接不上服务端,则会一直阻塞,直到超时。我们选择
的是非阻塞客户端。这意味着调用 clientChannel.connect(serverAddr) 无论是否连接上都会继续执行剩余代码。
那肯定是会报错的,没有连接上服务端,发送接收消息都会失败。所以我们使用while循环,如果连接失败则尝试重新连接。
剩下的代码逻辑上和Socket并无差别。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.datang.pet.control.test; 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; import java.util.Set; public class NIOServer { public static void main(String[] args) throws Exception { //服务端channel ServerSocketChannel serverChannel = ServerSocketChannel.open(); //非阻塞的服务端 serverChannel.configureBlocking(false); //绑定服务端端口号 serverChannel.bind(new InetSocketAddress(9999)); //多路复用器 selector Selector selector = Selector.open(); //将服务端channel注册到多路复用器,并告诉selector,这个管道对“接受客户端请求的事件感兴趣” serverChannel.register(selector, SelectionKey.OP_ACCEPT); //循环不停的执行select() while (true) { /* 轮询监听 有 Channel就绪,所谓的就绪就是 channel感兴趣的事件来了。 * 如果select()返回值是0,则表示没有就绪的channel * */ if (selector.select(1000) == 0) { System.out.println("没有channel准备就绪"); continue; } //能走到这说明有就绪的channel,一个channel有一个唯一的SelectionKey。 //这个集合包含所有就绪的channel。 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //注意这里一定要用迭代器循环,不要用增强for。很多博客上直接用的增强for,其实会有bug。 Iterator<SelectionKey> iterator = selectionKeys.iterator(); //遍历所有就绪的key while (iterator.hasNext()) { SelectionKey key = iterator.next(); //如果当期key的类型为 OP_ACCEPT,说明可以接受客户端连接了。 if (key.isAcceptable()) { System.out.println("-------------接受到Client的连接了-----------------"); //获取一个客户端的 channel SocketChannel clientChannel = serverChannel.accept(); //设置客户端为非阻塞 clientChannel.configureBlocking(false); //获取到客户端channel后,需要将其注册到多路复用器,并告诉selector,这个管道对“我对读事件感兴趣” clientChannel.register(selector, SelectionKey.OP_READ); } //如果当期key的类型为 OP_READ,说明可以读客户端的数据了。 if (key.isReadable()) { try { System.out.println("----------------channel可读了----------------"); ByteBuffer buffer = ByteBuffer.allocate(1024); //注意,当前类里的操作都是异步的,以为这,当前这个key,上边的key肯定不是同一个。 //这个key是上一个里获取到的客户端channel。 SocketChannel clientChannel = (SocketChannel) key.channel(); //将数据读到Buffer clientChannel.read(buffer); //将buffer转换为字节数组 byte[] array = buffer.array(); System.out.println(new String(array, "UTF-8")); //向客户端发送消息 clientChannel.write(ByteBuffer.wrap("我是服务端".getBytes())); } catch (Exception e) { //如果客户端关闭了,会走到这里,则调用cancel关闭此通道 key.cancel(); } } //防止重复执行 已准备好的key iterator.remove(); } } } }
上边代码片段是服务端代码。首先我们需要关注的是创建服务端,设置非阻塞服务端,绑定端口。接下来Selector多路复用器就要
出场了。Selector.open();获取一个多路复用器。serverChannel.register(selector, SelectionKey.OP_ACCEPT);服务端管道将
自身注册到Selector中,并且添加感兴趣的事件,事件类型为接受客户端的连接。
接下来while(true)在循环中调用 select(1000)方法,此方法是阻塞方法,参数则为阻塞的毫秒数。select(1000)方法,轮询的去查看
当前注册到selector中的channel有没有准备就绪的,也就是有没有触发相应的事件。如果1秒内有准备就绪的channel。则拿到准备
就绪的集合。这里需要说明一点,所有注册到selector的channel都变成了一个SelectionKey,该对象有channel的所有特征。
接下来拿到set的迭代器。遍历迭代器,判断SelectionKey的事件类型。对于一个客户端连接来说,最开始触发的肯定是 isAcceptable()
也就是OP_ACCEPT,然后调用服务端channel的 accept()获取一个客户端连接channel,这里就类似bio的socket了。同样的,我们需要
将这个客户端channel注册到selector中,此时我们关注的事件类型是OP_READ,可读。也就是说当一个客户端连接到达后,我们需要
读取客户端发来的消息,但是这个读取本身是阻塞的,在bio中我们做个测试。所以我们将这个channel注册到selector中,并且告诉
selector如果这个客户端channel可以读了,你告诉我。剩下的就是等待可读后,再次遍历SelectionKey。selector流程类似下图。
最后我们要注意两点。在while迭代的最后,我们将当前的key从迭代器中移除了,为什么要移除?思考一下,如果当前的key为OP_ACCEPT,代表客户端连接了,我们获取到客户端连接后,如果不将
此key从准备就绪的 selectedKeys中移除掉,那再次执行select()方法这个key依然是准备就绪的状态,会再次的到 if(key.isAcceptable()) 此时就出现问题了,同样的一个客户端被我们
获取了两次channel,报错!。
最后一点,在可读事件执行的代码块中,用try{}catch(key.cancle()),因为当客户端关闭后,同样会发送一条消息,该消息会被识别到OP_READ,但是此时客户端已经关闭,读取会报错。所以
执行cancle()表示将该key,对应的channel移除掉,表示会话结束。
以上是NIO的基础使用,学习NIO是为了学习NIO框架Netty。如有错误,望指正。