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的代码:
- <span style="font-size:18px">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("服务端启动成功!");
- // 轮询访问selector
- while (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(1024);
- 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();
- }
- }
- </span>
- <span style="font-size:18px">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 {
- // 轮询访问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("向服务端发送了一条信息")
- .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(1024);
- 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();
- }
- }
- </span>
原文链接:http://blog.csdn.net/burningsheep/article/details/12918189