赞
踩
正所谓工欲善其事,必先利其器,在学习NIO之前,我们先了解一些基本概念。
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式。
同步:应用程序要直接参与IO读写的操作。在处理IO事件的时候,必须阻塞在某个方法上靣等待IO事件完成。
异步:所有的IO读写交给操作系统去处理,应用程序只需要等待通知。这个时候,我们可以去做其他的事情,当搡作系统完成IO后.会给应用程序一个通知。
在Java1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统一次一个字节地处理数据,但是面向流的I/O速度非常慢。而在Java 1.4中推出了NIO,这是一个面向块的I/O系统,系统以块的方式处理数据,比按字节处理数据快的多。
IO模型 | IO | NIO |
---|---|---|
方式 | 从硬盘到内存 | 从内存到硬盘 |
通信 | 面向流(乡村公路) | 面向缓存(高速公路,多路复用技术) |
处理 | 阻塞IO(多线程) | 非阻塞IO(反应堆Reactor) |
触发 | 无 | 选择器(轮询机制) |
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器),下面我们会对其进行分别介绍。
缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。
在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应。
下面我们举个例子
import java.nio.IntBuffer; public class Demo { public static void main(String[] args) { // 分配新的int缓冲区,参数为缓冲区容量 IntBuffer buffer = IntBuffer.allocate(10); for (int i=0;i<buffer.capacity();i++) { // 将给定整数写入此缓冲区的当前位置,当前位置递增 buffer.put(i-1); } // 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0 buffer.flip(); // 查看在当前位置和限制位置之间是否有元素 while (buffer.hasRemaining()) { // 读取此缓冲区当前位置的整数,然后当前位置递增 int j = buffer.get(); System.out.print(j + " "); } } }
运行结果
-1 0 1 2 3 4 5 6 7 8
1 当我们刚刚初始化buffer数组时
2 向buffer写入数据
3 准备把buffer中的数据写到channel管道中,调用buffer.flip();
这时知道刚刚写入的数据在position到limit之间
4 这时底层操作系统就可以从缓冲区中正确读取这 5 个字节数据并发送出去。在下一次写数据之前我们在调一下 clear() 方法。缓冲区的索引状态又回到初始位置。
通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象(通道),通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
在NIO中,提供了多种通道对象,而所有的通道对象都实现了Channel接口。
任何时候读取数据,都不是直接从通道读取,而是从通道读取到缓冲区。所以NIO读取数据其实可以分为三个步骤
import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class Demo { public static void main(String[] args) throws IOException { FileInputStream fin = new FileInputStream("d:\\my.txt"); // 获取通道 FileChannel fc = fin.getChannel(); // 创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取数据到缓冲区 fc.read(buffer); // 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0 buffer.flip(); while (buffer.remaining()>0) { byte b = buffer.get(); System.out.print(((char)b)); } fin.close(); } }
与读取数据的过程类似,三个步骤与读取数据一模一样
import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class Demo { static private final byte message[] = {1,2,3,4,5,6,7,8,9,10}; static public void main( String args[] ) throws Exception { FileOutputStream fout = new FileOutputStream( "d:\\my.txt" ); FileChannel fc = fout.getChannel(); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); for (byte me : message) { buffer.put( me ); } buffer.flip(); fc.write( buffer ); fout.close(); } }
关于选择器的学习参考这篇文章:Java NIO 的前生今世 之四 NIO Selector 详解
选择器允许一个单一的线程来操作多个 Channel. 如果我们的应用程序中使用了多个 Channel, 那么使用 Selector 很方便的实现这样的目的, 但是因为在一个线程中使用了多个 Channel, 因此也会造成了每个 Channel 传输效率的降低。
为了使用 Selector, 我们首先需要将 Channel 注册到 Selector 中, 随后调用 Selector 的 select()方法, 这个方法会阻塞, 直到注册在 Selector 中的 Channel 发送可读写事件. 当这个方法返回后, 当前的这个线程就可以处理 Channel 的事件了。
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
注意:如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的。 因此,FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的。
在使用 Channel.register()方法时, 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣, 这些事件有:
一个 Channel发出一个事件也可以称为 对于某个事件, Channel 准备好了. 因此一个 Channel 成功连接到了另一个服务器也可以被称为 connect ready
当我们使用 register 注册一个 Channel 时, 会返回一个 SelectionKey 对象, 这个对象包含了如下内容:
我们可以通过 Selector.select()方法获取对某件事件准备好了的 Channel, 即如果我们在注册 Channel 时, 对其的可写事件感兴趣, 那么当 select()返回时, 我们就可以获取 Channel 了。
如果 select()方法返回值表示有多个 Channel 准备好了, 那么我们可以通过 Selected key set 访问这个 Channel:
Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); }
注意, 在每次迭代时, 我们都调用 “keyIterator.remove()” 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中。
1 通过 Selector.open() 打开一个 Selector
2 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)
3 不断重复:
3.1 调用 select() 方法
3.2 调用 selector.selectedKeys() 获取 selected keys
3.3 迭代每个 selected key:
当调用了 Selector.close()方法时, 我们其实是关闭了 Selector 本身并且将所有的 SelectionKey 失效, 但是并不会关闭 Channel
public class NioEchoServer { private static final int BUF_SIZE = 256; private static final int TIMEOUT = 3000; public static void main(String args[]) throws Exception { // 打开服务端 Socket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 打开 Selector Selector selector = Selector.open(); // 服务端 Socket 监听8080端口, 并配置为非阻塞模式 serverSocketChannel.socket().bind(new InetSocketAddress(8080)); serverSocketChannel.configureBlocking(false); // 将 channel 注册到 selector 中. // 通常我们都是先注册一个 OP_ACCEPT 事件, 然后在 OP_ACCEPT 到来时, 再将这个 Channel 的 OP_READ // 注册到 Selector 中. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 通过调用 select 方法, 阻塞地等待 channel I/O 可操作 if (selector.select(TIMEOUT) == 0) { System.out.print("."); continue; } // 获取 I/O 操作就绪的 SelectionKey, 通过 SelectionKey 可以知道哪些 Channel 的哪类 I/O 操作已经就绪. Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); // 当获取一个 SelectionKey 后, 就要将它删除, 表示我们已经对这个 IO 事件进行了处理. keyIterator.remove(); if (key.isAcceptable()) { // 当 OP_ACCEPT 事件到来时, 我们就有从 ServerSocketChannel 中获取一个 SocketChannel, // 代表客户端的连接 // 注意, 在 OP_ACCEPT 事件中, 从 key.channel() 返回的 Channel 是 ServerSocketChannel. // 而在 OP_WRITE 和 OP_READ 中, 从 key.channel() 返回的是 SocketChannel. SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); clientChannel.configureBlocking(false); //在 OP_ACCEPT 到来时, 再将这个 Channel 的 OP_READ 注册到 Selector 中. // 注意, 这里我们如果没有设置 OP_READ 的话, 即 interest set 仍然是 OP_CONNECT 的话, 那么 select 方法会一直直接返回. clientChannel.register(key.selector(), OP_READ, ByteBuffer.allocate(BUF_SIZE)); } if (key.isReadable()) { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer buf = (ByteBuffer) key.attachment(); long bytesRead = clientChannel.read(buf); if (bytesRead == -1) { clientChannel.close(); } else if (bytesRead > 0) { key.interestOps(OP_READ | SelectionKey.OP_WRITE); System.out.println("Get data length: " + bytesRead); } } if (key.isValid() && key.isWritable()) { ByteBuffer buf = (ByteBuffer) key.attachment(); buf.flip(); SocketChannel clientChannel = (SocketChannel) key.channel(); clientChannel.write(buf); if (!buf.hasRemaining()) { key.interestOps(OP_READ); } buf.compact(); } } } } }
最后我们通过对NIO与IO的比较结束本章节的学习
参考:NIO原理详解
Java NIO使用及原理分析 (一)
什么是NIO?NIO的原理是什么机制?
Java NIO 的前生今世 之四 NIO Selector 详解
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。