一、为什么出现Java NIO
由于Java的OutputStream和InputStream没有提供异步I/O的能力,OutputStream上的写操作write()方法会阻塞直至数据被成功写入,InputStream上的读操作read()方法也会阻塞,直到有数据可读,还有ServerSocket的accept()方法也会阻塞直至有客户端进行连接。它们都是阻塞方法,那当服务器需要处理上千个客户请求时,往往就需要一个客户对应一个线程,大量的闲置客户端会限制系统可以同时服务的客户端总数,所以JDK1.4引入了Java NIO(New Input/Output)。
二、Java NIO 基础
非阻塞:NIO引入了选择器(Selector)和通道(Channel)来实现非阻塞,通过表示到设备、文件、网络套接字这种可以执行不同I/O操作的程序组件的开放连接,有的通道允许选择器对它们进行轮询。通道可以注册一个选择器实例,通过该实例的select()方法来询问在一个或一组通道中,哪一个当前需要服务(读、写、接受)。非阻塞指的就是“在一个准备好的通道上进行相应的I/O操作,就不需要等待(阻塞)了”。
1.缓冲区
NIO一个主要的特性就是java.nio.Buffer。缓冲区代表了一个有限容量的容器(本质是一个数组),通道Channel使用Buffer实例来传递数据。流和通道的主要区别就是通道在读写数据时,所有的内容都是先放入缓冲区之中,再通过缓冲区取得的,这也说明了为什么通道是双向操作的,而流是单向的。
缓冲区有几个比较重要的变量:position、limit、capacity等;
还有几个操作缓冲区常用的方法:allocate()、wrap()、get()、put()、flip()等;
这里先不对这些进行说明,先大概从方法名和变量名能有一定的理解。
2.通道
Channel实例代表一个和设备的连接,通过它可以进行输入输出操作。
Java NIO中,ServerSocketChannel和SocketChannel对应于传统的基本套接字ServerSocket和Socket。
(1)ServerSocketChannel和SocketChannel都通过工厂方法创建,就是常见的open()方法。SocketChannel创建后,可以通过connect()方法连接到远程机器,通过close()关闭连接,和原来的Socket的操作没什么差别。
(2)SocketChannel读写数据时和通过Socket获取输入输出流进行读写不同,它使用前面说的Buffer缓冲区作为参数。一般使用read()/write()及其扩展方法。
ServerSocketChannel提供了和ServerSocket类似的accept()、close()方法;但不提供bind()方法,要实现绑定Socket到某端口,需要使用ServerSocket.socket().bind(...)方法。
(3)通过configureBlocking(false)方法可以可以把Channel设置为非阻塞式。
非阻塞式SocketChannel和阻塞式Socket有一些不同,比如:非阻塞式SocketChannel的connect()方法会立即返回,用户必须通过isConnected()判断连接是否已经建立,或者通过finishConnect()方法在非阻塞套接字上阻塞等待连接成功;非阻塞的read()在Socket上没有数据时会立即返回(0),不会等待;非阻塞的accept()如果没有等待的连接,将立即返回null。
(4)ServerSocketChannel和SocketChannel能够跟选择器(Selector)配合工作,避免非阻塞式I/O操作中很浪费资源的忙等方法。例如在连接很多但需要处理的请求很少时,就需要一种方法阻塞等待,直到至少一个Channel可以进行I/O操作,并指出是哪个通道,选择器就是为了这个功能而设计:一个Selector实例可以同时检查(等待)一组通道的I/O方法。
3.选择器
选择器(Selector)创建实例后,通过Channel的注册方法注册到想要监控的Channel实例上,然后调用选择器的select()方法,该方法会阻塞等待,直到有一个或多个通道准备好I/O操作。select()方法会返回可进行I/O操作的通道数量。它使得一个线程就能检查多个通道是否可以进行I/O操作,而不是一个线程对应一个通道了。
这里很重要的一点是选择器和通道进行关联的SelectionKey(选择器注册标记),它维护了一个通道上感兴趣的事件类型信息,包括4种:OP_READ(通道上有数据可读)、OP_WRITE(通道上已经可写)、OP_CONNECT(通道连接已建立)、OP_ACCEPT(通道上有连接请求)。
调用通道的register()方法,可以将一个选择器注册到通道,并指定该通道的初始兴趣时间集,注册完毕后便可以开始等待通道I/O事件了。
当select()方法的返回值大于0,表明有需要处理的I/O事件发生,选择器会将对应的通道放入已选键集中,通过Selector.selectedKeys()可以获得该集合,并在上面迭代处理关联通道上的I/O操作。比如通过selectedKeys()方法获取可进行I/O操作的通道,通过便利通道,利用SelectionKey判断通道上等待的操作(如key.isAcceptable()表明有一个入站请求),并做相应处理。
三、Java NIO 实例
下面一个demo用于更好地理解,实现的就是客户端向服务端的连接和互相发送当前时间。
NIOyyjServer.java
package com.yyj.nio;
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.Date;
import java.util.Iterator;
/**
* Created by GraceYang on 2017/4/6.
*/
public class NIOyyjServer {
public static void main(String args[])throws Exception{
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();// 获得一个ServerSocket通道
serverSocketChannel.configureBlocking(false);// 设置通道为非阻塞
ServerSocket serverSocket=serverSocketChannel.socket();//获得该通道对应的ServerSocket
InetSocketAddress address=new InetSocketAddress(8000);
serverSocket.bind(address);// 将该通道对应的ServerSocket绑定到8080端口
Selector selector=Selector.open();// 获得一个通道管理器
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
//注册OP_ACCEPT事件后:
//当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
selector.select();
Iterator iterator=selector.selectedKeys().iterator();// 获得selector中选中的项的迭代器,选中的项为注册的事件
while(iterator.hasNext()){
SelectionKey key=(SelectionKey) iterator.next();
iterator.remove();// 删除已选的key,以防重复处理
// 客户端请求连接事件
if(key.isAcceptable()){
ServerSocketChannel serverSocketChannel1=(ServerSocketChannel)key.channel();
SocketChannel socketChannel=serverSocketChannel1.accept();// 获得和客户端连接的通道
socketChannel.configureBlocking(false);// 设置成非阻塞
Thread.sleep(3000);
//给客户端发送信息当前时间
socketChannel.write(ByteBuffer.wrap(new String("向客户端发送当前时间"+new Date()).getBytes()));
//在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
socketChannel.register(selector, SelectionKey.OP_READ);
}
else if(key.isReadable()){// 获得了可读的事件 ;处理读取客户端发来的信息的事件
SocketChannel socketChannel=(SocketChannel)key.channel();// 服务器可读取消息:得到事件发生的Socket通道
ByteBuffer buf=ByteBuffer.allocate(100);// 创建读取的缓冲区
socketChannel.read(buf);//读取到了客户端发来的信息
byte[] data = buf.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:"+msg);
socketChannel.close();
}
}
}
}
}
NIOyyjClient.java
package com.yyj.nio;
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.Date;
import java.util.Iterator;
/**
* Created by GraceYang on 2017/4/7.
*/
public class NIOyyjClient {
public static void main(String args[]) throws Exception{
SocketChannel socketChannel=SocketChannel.open();// 获得一个Socket通道
socketChannel.configureBlocking(false);// 设置通道为非阻塞
Selector selector=Selector.open();// 获得一个通道管理器
InetSocketAddress address=new InetSocketAddress("localhost",8000);
// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调用channel.finishConnect();才能完成连接
socketChannel.connect(address);
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while (true){
selector.select();
Iterator iterator=selector.selectedKeys().iterator(); // 获得selector中选中的项的迭代器
while (iterator.hasNext()){
SelectionKey key=(SelectionKey) iterator.next();
iterator.remove();// 删除已选的key,以防重复处理
if(key.isConnectable()){ // 连接事件发生
SocketChannel socketChannel1=(SocketChannel)key.channel();
if(socketChannel1.isConnectionPending()){// 如果正在连接,则完成连接
socketChannel1.finishConnect();
}
socketChannel1.configureBlocking(false);// 设置成非阻塞
//给服务端发送信息
socketChannel1.write(ByteBuffer.wrap(new String("向服务端发送当前时间"+new Date()).getBytes()));
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
socketChannel1.register(selector,SelectionKey.OP_READ);
}
else if(key.isReadable()){// 获得了可读的事件,处理读取服务端发来的信息的事件
SocketChannel socketChannel1=(SocketChannel)key.channel();
ByteBuffer buf=ByteBuffer.allocate(100);
socketChannel1.read(buf);
byte[] data = buf.array();
String msg = new String(data).trim();
System.out.println("客户端收到信息:"+msg);
}
}
}
}
}
NIOyyjServer:
服务端收到信息:向服务端发送当前时间Mon Apr 10 15:11:46 CST 2017
NIOyyjClient:
客户端收到信息:向客户端发送当前时间Mon Apr 10 15:11:49 CST 2017
参考书籍:《Hadoop技术内幕:深入解析Hadoop Common和HDFS架构设计与实现原理》