Java NIO,全称Java non-blocking IO(也有说是Java New IO,个人认为前者各贴切),是Java在1.4版本时引入的一套新的IO和网络编程的API,可以作为Java标准IO的替代选择。
Java BIO(Java blocking IO,即Java standard IO)是同步阻塞IO,Java NIO的引入是为了实现同步非阻塞的IO,从而提供多路(non-blocking) 非阻塞式的高伸缩性网络I/O 。
阻塞IO和非阻塞IO的主要差异,这里用生活中的例子类比一下:
阻塞IO:
老板:小明,去取个包裹,取不到你就不要回来了。
于是小明一直在快递站等到包裹送来并取件才回到公司。
非阻塞IO:
老板:小明,去取个包裹,取不到的话你就先回来写生产报告。
小明去了快递站,包裹还没到,于是小明先回到公司写报告了。
通常非阻塞IO是需要循环调用的,大概就是下面这个场景:
老板:小明,去取个包裹,取不到的话你就先回来写生产报告,过一会儿再去取快递。
小明去了快递站,包裹还没到,于是小明先回到公司写报告了。
写了会儿报告小明又去快递站取快递了,还是没有到,于是又回公司写报告,如此往复直到取到快递。
从上面的描述我们直到,非阻塞最主要就是为了让当前线程,在进行IO操作时无论没有获取到资源就立即返回,从而避免阻塞当前线程。其实要达到避免阻塞当前线程的目的,使用BIO也可以实现,那就是配合多线程,每当需要进行一个IO操作时,就新建一个线程,或者使用线程池,就可以避免阻塞当前线程。
那为什么我们还要使用NIO呢,这是因为在频繁大量的IO操作时,每次都新建线程,对系统的开销非常大,即使使用线程池,依然需要维护大量的线程,系统开销依然很大,而如果使用NIO,则一个线程就可以实现非阻塞,系统开销小。
其实并不一定,NIO性能比BIO性能好主要体现在两者均为单线程时,因为NIO没有获取到资源时可以立即返回,而BIO则会阻塞,性能差异主要体现在这里,所以这种情况NIO性能确实比BIO好。
但如果是我们上面说的多线程配合BIO使用,多个线程同时进行IO操作,可以认为是同时进行的,而且当前线程也不会阻塞(一些地方将这种方式成为伪异步),而NIO虽然不会阻塞,但是当前线程中始终只有一个IO操作在进行,所以多线程BIO的性能理论上优于单线程NIO,但系统开销更大。
当然,NIO也可以和多线程配合使用,这个时候无论是整体性能,还是系统开销,都优于多线程BIO。
所以可以肯定的是,在单个线程中,NIO的处理性能远优于BIO。
在BIO中我们使用流(Stream)来进行资源的输入输出,而在NIO中,是使用信道(Channel,很多地方译为管道,但通常我们理解中的管道是单向的,所以觉得信道可能更贴切),stream和channel很相似,都是用来输入输出数据资源的,但它们有一些差异:
我们在使用传统的IO时,在读写数据时,通常都是直接实现的缓存,在NIO中,Java直接引入了缓存概念,信道在进行读写时,都是针对缓存进行操作。
NIO提供了以下类型的缓存:
可以看到,这些缓存类型,涵盖了Java所有的基础数据类型。
Buffer有两个很重要的概念,position和limit。
position:游标当前所在的位置。
limit:缓存的边界。
在read和write中,position和limit有一些差异。read时,position是当前写入的位置,limit是Buffer的末尾位置;write时,position是当前读取的位置,limit是有效数据的末尾位置。
下面是示意图:
上面了解了position和limit,flip()就是用来改变buffer的position和limit状态的。
调用flip函数后,position会重置为0,即最开始位置,而limit如果此时在buffer末尾,那么limit会变更为有效内容的末尾,如果limit此时在有效内容的末尾,那么会变更为buffer末尾。
通过flip改变状态后,buffer可以用来读取数据或重新写入数据。
NIO中有一个很有意思也很重要的概念,就是选择器Selector,选择器是为了在一个线程中同时管理多个channel而设计出来的,channel可以将自己注册到一个selector,并告诉selector想要进行的操作,selector可以定时从所有注册的channel中选出已经准备好进行之前声明的操作的channel对应的key,继而进行相应的的预期操作。
大家都说NIO采用了事件驱动模型,先了解一下什么是事件驱动模型。
参考链接:https://baike.baidu.com/item/%E4%BA%8B%E4%BB%B6%E9%A9%B1%E5%8A%A8%E6%A8%A1%E5%9E%8B/1419787?fr=aladdin
通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;
(2)每收到一个请求,创建一个新的线程,来处理该请求;
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
上面的几种方式,各有千秋,
第(1)种方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。
第(3)种方式,事件实现了异步处理,事件源线程不用阻塞,提高了响应速度,但是在写应用程序代码时,逻辑比前面两种都复杂。
综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式。
事件驱动模型图解:
其实NIO的事件驱动模型,正是基于Selector实现的,当我们需要进行一次IO操作,主要步骤是这样的:
可以看到其实和上图中所描述的一样的,需要注意的是,因为Selector是基于事件驱动模型设计的,所以要求注册到Selector的channel必须是non-blocking的,可以通过channel.configureBlocking(boolean block)进行设置。
FileChannel不支持设置为non-blocking,所以FileChannel不能基于Selector实现异步IO。
当使用FileChannel读取一个文件时,代码如下:
static void channelStudy() throws IOException {
//获取需要读写的文件,并包装为RandomAccessFile实例
RandomAccessFile aFile = new RandomAccessFile("C:\\Users\\admin\\Desktop\\tmp/application.yml", "rw");
//获取需要的channel
FileChannel inChannel = aFile.getChannel();
//刚分配的buffer为write模式
ByteBuffer buf = ByteBuffer.allocate(128);
//从channel中读取数据写入buffer中
int bytesRead = inChannel.read(buf);
//当读取到数据末尾时,channel.read()会返回-1
while (bytesRead != -1) {
//将buffer置为read模式,position置为0,limit置为实际读取到的数据长度
buf.flip();
//通过buf.hasRemaining()判断buffer中是否还有未读取的数据
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
//读取完毕后将buffer清空
buf.clear();
//继续从channel中向buffer中读取数据
bytesRead = inChannel.read(buf);
}
//使用完毕后将file关闭
aFile.close();
}
解析:
上面已经提到,FileChannel不能被设置为non-blocking的,所以不能结合Selector使用,这段代码依然是阻塞的。
使用NIO进行网络编程,主要关注Selector、ServerSocketChannel和SocketChannel,Selector前面已经详细介绍过,ServerSocketChannel可以理解为一个网络服务器,是用来接收网络请求的,而一个SocketChannel就代表着一次请求。
下面的代码实现了一个简单的网络时间服务和客户端,主要讲解已经写在代码注释中。
学习代码仓库地址:https://gitee.com/imdongrui/study-repo
仓库中的ioserver和ioclient
package com.dongrui.study.ioserver.nioserver;
import com.google.common.primitives.Bytes;
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.*;
/**
* 简单时间服务器
*
* @author dongrui
*/
public class SimpleTimeServer implements Runnable {
/**
* 多路控制器
*/
private Selector selector;
/**
* 用于接收请求的ServerSocket通道,对应于BIO中的ServerSocket
*/
private ServerSocketChannel serverSocketChannel;
/**
* 是否继续执行监听的标识
*/
private boolean isContinue = true;
/**
* 初始化
*
* @param port
*/
public SimpleTimeServer(int port) {
try {
selector = Selector.open();//打开多路控制器
serverSocketChannel = ServerSocketChannel.open();//打开ServerSocketChannel
serverSocketChannel.configureBlocking(false);//将阻塞模式设置为非阻塞,否则无法注册到多路控制器
serverSocketChannel.bind(new InetSocketAddress(port));//绑定端口
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//注册到多路控制器
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 处理SelectionKey的函数
*
* @param key 当前需要处理的SelectionKey
* @throws IOException
*/
private void handleKey(SelectionKey key) throws IOException {
if (key.isValid()) {
if (key.isAcceptable()) {
//ServerSocketChannel才会出现accept事件,调用accept函数获取接收到的SocketChannel,并将其注册到多路控制器
((ServerSocketChannel) key.channel()).accept().configureBlocking(false).register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {//处理read事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(2);//此处取buffer长度为2,是为了验证多次read
int byteCount = channel.read(buffer);
List<Byte> bytes = new ArrayList<>();//获取完整byte数据后再进行处理
while (byteCount > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
bytes.add(buffer.get());
}
buffer.clear();
byteCount = channel.read(buffer);
}
System.out.println("time server received client request: " + new String(Bytes.toArray(bytes), "utf-8"));
key.interestOps(SelectionKey.OP_WRITE);//将当前key的interest设置为write,也可以在read后直接write
} else if (key.isWritable()) {//处理write事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(256);
buffer.put((new Date().getTime() + "").getBytes()).flip();
channel.write(buffer);
//处理完毕后,将key的状态置为canceled,将channel关闭,本次请求处理完毕
key.cancel();
channel.close();
}
}
}
/**
* 停止监听
*/
public void stop() {
this.isContinue = false;
}
@Override
public void run() {
while (isContinue) {
try {
selector.select(5000);//多路控制器获取需要处理的key的数量,如果当前没有可处理的key,则会等待timeout时间
Set<SelectionKey> selectionKeys = selector.selectedKeys();//获取可以处理的key
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
handleKey(key);//处理当前key
iterator.remove();//处理后将其移除
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new SimpleTimeServer(8080).run();
}
}
需要注意在使用时如果配合多线程使用,必须保证主线程后于子线程结束,否则主线程结束,子线程会被强制结束,通讯失败。
package com.dongrui.study.ioclient.nioclient;
import com.google.common.primitives.Bytes;
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.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public class SimpleTimeClient implements Runnable {
private Selector selector;
private boolean isContinue = true;
private String host;
private int port;
public SimpleTimeClient(String host, int port) throws IOException {
this.host = host;
this.port = port;
selector = Selector.open();
}
/**
* 产生SocketChannel并进行连接
*
* @throws IOException
*/
private void doConnect() throws IOException {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(host, port));
if (channel.finishConnect()) {
//连接成功,注册到多路控制器,触发事件为write
channel.register(selector, SelectionKey.OP_WRITE);
} else {
//连接不成功,则注册到多路控制器,触发事件为connect,在connect中会再次尝试连接
channel.register(selector, SelectionKey.OP_CONNECT);
}
}
private void handleKey(SelectionKey key) throws IOException {
if (key.isConnectable()) {//尝试再次连接
SocketChannel channel = (SocketChannel) key.channel();
channel.connect(new InetSocketAddress(host, port));
if (channel.finishConnect()) {
key.interestOps(SelectionKey.OP_WRITE);
} else {
System.out.println("连接失败");
key.cancel();
channel.close();
}
} else if (key.isReadable()) {//读取服务端返回数据
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(2);
List<Byte> bytes = new ArrayList<>();
int byteCounts = channel.read(buffer);
while (byteCounts > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
bytes.add(buffer.get());
}
buffer.clear();
byteCounts = channel.read(buffer);
}
System.out.println("当前时间:" + new String(Bytes.toArray(bytes), "utf-8"));
key.cancel();
channel.close();
} else if (key.isWritable()) {//写入发送给服务端的数据
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(256);
buffer.put("识相的快点报时".getBytes()).flip();
channel.write(buffer);
key.interestOps(SelectionKey.OP_READ);
}
}
public void stop() {
this.isContinue = false;
}
@Override
public void run() {
try {
while (isContinue) {
Thread.sleep(1000);
doConnect();
selector.select(5000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
handleKey(key);
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
new SimpleTimeClient("localhost", 8080).run();
} catch (IOException e) {
e.printStackTrace();
}
}
}