Java NIO

来源:互联网 发布:淘宝电器售后 编辑:程序博客网 时间:2024/06/01 10:26

Java NIO

    NIO是New IO的缩写,顾名思义,是用于输入输出的新的API,那么,这个NIO相较于旧的IO有什么差别呢?

   1、“阻塞”的通信机制

    在原有的IO下,我们要与A进行通信时,会怎么做呢?先创建一个线程,然后建立连接,然后不断轮询等待接收消息。当需要与另一个B进行通信时,仍然先创建一个线程,然后建立连接(accept),不断轮询等待接收消息(read)… 在这种情况下,若通信的对象变多时,需要的线程就相应增长,并且每个线程都需要不断轮询等待,若没有消息接受时,则阻塞线程。由于消息传送的频率往往不高,所以,线程大部分时间都处于阻塞状态。而阻塞态的线程仍占用着系统资源,多个线程的管理及线程状态的切换(阻塞-就绪-运行)是比较耗时的。

    由于这种方式在一个通信任务没有完成之前,是无法返回的,所以以上的这种方式称作“阻塞”的通信机制。

   2、观察者模式

    “阻塞”的通信机制,从本质上来看,就是每个通信的线程都需要询问“是否有我的数据?”,如果没有则将该线程阻塞,当有数据到来时,再唤醒线程。每个线程都在观察某个事物,等待自己的事件发生。这个描述,与观察者模式类似。我们可以使用观察者模式来进一步优化“阻塞”的通信机制。我们需要监听多个端口时,传统方式是建立多个线程进行监听。而现在,我们将每个端口封装为一个通道,所有传入该端口的数据都会出现在通道中,假设有3个需要监听的端口,则有3个通道,我们对这三个通道进行监听,使用Selector来注册我们对各个端口监听的行为。比如,对第一个端口(通道),需要监听他是否有可读数据,对于第二个端口(通道)需要监听他是否有accept请求… Selector记录了我们对各个通道的兴趣点,然后统一的对各个端口进行监听。当有相应的事件发生时,则调用相关的处理函数。这样,就完成了只用一个监听线程完成多个端口监听的任务。这种方式,就是“非阻塞”的通信机制。


   3、nio 的关键

    我们来想象一下读取文件的方式。首先,若直接从一个文件中读取数据,每次读取一个字符,若文件中有10个字符,那么就需要进行10次IO,而每一次IO都是很耗时的。为了减少IO的次数,我们使用了buffer,假定每次读取的数据都先存入buffer中,每次读取5个字符。那么,从buffer中间接读取字符,尽管同样是10次,但只需要2次的IO就够了,减少了IO次数,提高了效率。

    为了能够完成端到端的传输,需要有一定的媒介进行。就像计算机系统中,数据传输是通过总线一样,nio中,数据的传输是通过“通道”进行的。例如,需要将文件A的内容传到(写入)文件B,那么,通过buffer与通道,可以如下操作:



    在这里,通道作为中间的传输媒介,Buffer则附加在通道的两端,作为数据的“来源”与目标(真正的应该是File A与File B)。使用通道,则一方(File A)则只需要通过Buffer往通道里写,而另一方(File B)则只需要通过从通道里读即可,而无需关注二者的差异。

    在NIO中,通道的一端绑定了相应的目的或者源头,例如文件、socket等,而另一端则是让使用buffer来获取或者写入数据。如下图:


缓冲区的工作与通道紧密联系。通道是I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。

    NIO中,有以下几个关键类型

    1)缓存buffer

    NIO中,提供了各个基本类型的buffer,提供给我们以不同的方式进行读写。例如可以用ByteBuffer进行字节(8bit为单位)的数据读写,使用CharByte进行字符(16bit为单位)的数据读写。

    2)通道Channel

    NIO中有以下几种通道:SocketChannel、ServerSocketChannel、DiagramChannel和FileChannel。

    FileChannel只能通过FileOutputStream或者FileInputStream的getChannel调用得到,是单向的,即一端与相应的文件绑定,另一端进行write或者read。

    ServerSocketChannel用于服务器端的通道创建,绑定了一个端口号后,所有接受的数据都会在该通道中,配合后面的Selector,可以直接获取封装好(SelectionKey)的某个客户发送的数据。

    虽说通道既可读又可写,但实际上,一个绑定了某个源或者目的的通道,是只有读或写的功能的。

    3)选择器Selector

    他是使得多元I/O成为可能的关键,使用Selector来注册对通道的某些行为的关注,使之可以同时管理监控多个通道,当监控的时间发生时,唤醒并调用统一的事件处理函数进行相应。

    选择器Selector:选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。

    可选择通道SelectableChannel:这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。

选择键SelectionKey:选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()  返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

 

附上网上的一个NIO,Server和Client的代码:

 

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.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Iterator;/** * NIO服务端 * @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("服务端启动成功!");// 轮询访问selectorwhile (true) {//当注册的事件到达时,方法返回;否则,该方法会一直阻塞System.out.println("开始阻塞");selector.select();System.out.println("解除阻塞");// 获得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("向客户端发送了一条信息").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("服务端收到信息:" + 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(8001);server.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 小路 */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 {// 轮询访问selectorwhile (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("向服务端发送了一条信息").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方法一样// 服务器可读取消息:得到事件发生的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("服务端收到信息:" + msg);ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());}/** * 启动客户端测试 * @throws IOException  */public static void main(String[] args) throws IOException {NIOClient client = new NIOClient();client.initClient("localhost", 8001);client.listen();}}


参考:

JavaNIO系列教程(一) Java NIO 概述

非阻塞模式 

《Java NIO》