所谓NIO,即new I/O,在JDK1.4用于改善原来I/O中的不足,通过进一步减少I/O操作中阻塞的粒度来提高I/O效率,所以也被称为NonBlocking I/O,非阻塞I/O
一、IO与NIO的区别
要说两种I/O的区别,其实它是被划分进不同的I/O模型中的,所以我们就先来看一下五种I/O模型的区别
五种I/O模型:
1.1 阻塞I/O模型
在用户进程(线程)中调用执行的时候,进程会等待该IO操作,而使得这个进程的其他操作无法执行。
举个例子,假如现在有一个socket需要读取通过网络传输获得的一段信息,当这个socket调用read()操作后,因为种种原因这段信息可能没有被接收或者说没有接收完全,而此时由于这个线程阻塞而被挂起,直到整个IO操作全部完成才能返回线程。
可见,这个模型在内存IO或者是网络IO中的效率都是很低的;另外,在并发量较大的情况下,也就会因为线程容易挂起而创建更多的线程,进而导致对于线程创建销毁的开销过大而影响整体性能。但是,它也有仅存的一点好处:该模型挂起时不消耗CPU资源,并且能够及时响应每个操作。
应用:阻塞socket、Java BIO(blocking IO)。
1.2 非阻塞IO模型
在用户进程中调用执行的时候,无论成功与否,该IO操作会立即返回(若缓冲区没有数据,则抛出异常然后返回),之后进程可以进行其他操作。
举个例子,仍然是一个socket需要读取通过网络传输获得的一段信息,当这个socket调用read()操作后,若缓冲区有数据,则直接读取;若缓冲区没有数据,则抛出异常返回。然后,这个线程会持续read()方法,检查缓冲区是否有新的数据进入,直到完成整个IO操作。
这个模型相比于之前效率有一定提高,但是暂时没有完成IO操作的线程持续轮询调用,会消耗CPU资源,降低CPU吞吐量。
应用:socket的同步非阻塞IO。
1.3 IO复用模型
这个模型中的每个socket会注册到一个selector上,统一监听IO操作,然后分出一个线程调用这个select()方法,这样就实现了从一线程一IO到一个线程来管理多个连接的转变。另外,值得说明的是,该模型中的selector实际上是实现了观察者模式,用一个selector来统一接收IO的信息,并且返回给相应的socket。但需要注意的是,该模型是一个仍然异步阻塞IO操作,是因为select()方法是阻塞的,或者也可以理解一种阻塞粒度更细的IO操作。
应用:Java NIO。
1.4 IO信号驱动模型
当一个进程中的socket发起一个IO操作,会向内核注册一个信号处理函数,并且进程返回不阻塞;当这个信号处理函数返回时,内核会向进程发送一个信号量,以便通知进程调用IO读取数据,换句话说就是通过一个信号量来实现一个回调机制,提高CPU的吞吐量。
1.5 异步IO模型
该模型应用于Java 7中AIO,会在后续的文章中加以详细介绍。
二、NIO中关键接口
2.1 Class SocketChannel
即为面向流的socket通道,或者可以理解为Socket的一个完善类,除了可以提供Socket的功能外,还提供了更多的特性。
2.1.1 public abstract Socket socket()
可以返回一个与Channel关联的Socket,进而实现Socket类的功能。
因为该方法本身是抽象的,所以在SocketChannelImpl实现类中实现了这个方法,如上。
2.1.2 public static SocketChannel open() throws IOException
打开套接字通道,通过调用系统范围默认的SelectorProvider对象的openSocketChannel方法创建新Channel;若不能创建,则抛出IOException异常。
2.1.3 public final SelectableChannel configureBlocking(boolean) throws IOException
比较重要的一个方法,设置为false,则在该方法后的Socket则是非阻塞的,否则,仍然阻塞。
另外,这个方法是AbstractSelectableChannel类中的final方法,而SocketChannel则是这个抽象类的子类。
2.1.4 public final SelectionKey register(Selector , int ) throws ClosedChannelException
向Selector中注册,其中参数int表示监听事件的类型,如下:
这样的好处是,selector 不会去遍历所有关联的 socket,而是只会是那些符合事件类型,并且完成就绪操作的 socket,减少了大量无效的遍历操作。
2.2 Class Selector
SelectableChannel类的多路复用器,每个SelectableChannel类(上文中SocketChannel的基类)可向复用器中注册,然后由Selector统一监听IO操作。
2.2.1 public abstract int select() throws IOException
返回Selector中注册的SelectableChannel的数量。
2.3 SelectionKey
上文中register()返回值即为SelectionKey,用作已注册在Selector中的Channel的指针。
//遍历Selector中的Channel
while(selector.select()>0){
Set keys=selector.selectedKeys();
Integer it=keys.iterator();
while (it.hasNext()){
SelectionKey key = (SelectionKey)it.next();
it.remove();
if (key.isConnectable())
connect(key);
else if (key.isWritable())
write(key);
else if (key.isReadable())
receive(key);
}
}
三、NIO的阻塞实现
完整代码如下:
public static class HttpConstant{
private static String[] HOSTS;
private static Integer PORT;
}
用于存放需要访问的服务器域名以及端口号。
public class NioBlockingClient{
private SocketChannel socketChannel;
private String host;
public static void main(String[] args) throws IOException {
for(String host:HttpConstant.HOSTS) {
NioBlockingHttpClient client=new NioBlockingHttpClient(host,HttpConstant.PORT);
client.request();
}
}
}
private void request() throws IOException{
PrintWriter writer=new PrintWriter(socketChannel.socket().getOutputStream());//装饰者模式
BufferedReader reader=new BufferedReader(new InputStreamReader(
socketChannel.socket().getInputStream()));
writer.write(host);
writer.flush();
String msg;
while((msg=reader.readLine())!=null)
System.out.println(msg);
}
四、NIO的非阻塞实现
public class NioNonBlockingClient{
private static Selector selector;
private Charset charset=Charset.forName("utf-8");
static{
try{
selector=Selector.open();
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException{
NioBlockingHttpClient client=new NioBlockingHttpClient();
for(String host:HOSTS)
client.request(host,PORT);
client.select();
}
public void request(String host,int port) throws IOException{
SocketChannel socketChannel=SocketChannel.open();
socketChannel.socket().setSoTimeout(5000);
SocketAddress remote=new InetSocketAddress(host,port);
socketChannel.configureBlocking(false);
socketChannel.connect(remote);
socketChannel.register(selector,SelectionKey.OP_READ);
}
public void select(){
while(selector.select(500)>0){
Set keys=selector.selectedKeys();
Iterator it=keys.iterator();
while(selector.select()>0){
Set keys=selector.selectedKeys();
Integer it=keys.iterator();
while (it.hasNext()){
SelectionKey key = (SelectionKey)it.next();
it.remove();
if (key.isConnectable())
connect(key);
else if (key.isWritable())
write(key);
else if (key.isReadable())
receive(key);
}
}
}
}
}