本文知识点:
NIO 工作原理
NIO 的三大概念 Channel Selector Buffer
NIO 相比于BIO的优点
Java NIO 使用方法
上一篇讲了Java BIO的使用和原理以及BIO模型带来的性能问题,BIO模型中每新增加一个连接就需要一个线程处理;可以使用线程池进行优化,但在10k、100k面前还是缚鸡之力;为了应对10k、100k的场景又演变出了另外一种网络I/O模型NIO。
Java 4 提供了NIO的API,NIO主要有几个基础的概念:Channel、Selector、Buffer。
有4种常用的Channal,分别为FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel。
FileChannel用于文件的读写操作,DatagramChannel用于UDP网络环境,SocketChannel与ServerSocketChannel用于TCP网络环境,文件与UDP的放一边,今天主要讲TCP网络环境Channel。Channel有点像是BIO中的连接,在NIO中每新创建一个连接就会得到一个新的Channel,Channel对于数据的读写是双向的,这和BIO中的Socket相似,只不过Socket是面向流的,读写需要分别通过InputStream与OutputStream。当一个新的连接进来时将产生一个Channel,Channel上会有事件状态,当前Channel是新连接的还是数据已经准备好了还是可以写数据,而需要得知那些Channel是新连接的,那些是可以读那些是可以写,这就需要Selector参与了。
BIO读数据会阻塞线程,阻塞的原因在于内核准备数据与将数据从内核态复制到用户态这两个阶段,在这两个阶段的操作里用户处于等待状态,这是浪费时间的;而NIO则不会等待,这个不会等待的原因在于NIO的三大核心之一Selector。Selector的select()
方法是一个阻塞方法,当注册在这个Selector之内的所有Channel都没有准备好读写和没有新的连接(这些事件需要先注册在当前Selector中才能让其监听到)时会阻塞当前线程,当做这个阻塞有可以接受的,因为当前没有任何可以操作的内容嘛,当有事件发生时就可以使用Selector提供的selectedKeys()
方法获取当前能操作的channel,然后循环处理这些事件从而实现无阻塞的I/O模型nonblock I/O。事件的注册可以通过register()
方法进行,注册的事件类型有4种分别为OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT;通过命名可以看出别对应BIO中的监听、连接、读、写4种操作。
Selector与Channel与Thread的关系图:
BIO读数据使用流的形式,来了多少读多少;这在网络出现异常或者网络环境较差的情况下会拖慢读写的性能,需要等待更长的时间才能完成数据的读写;对些NIO也做了优化,将面向流的I/O操作优化为面向缓冲区的I/O操作,读写数据都经过Buffer完成,一块一块的读或者写数据,从而屏蔽掉因为网络问题导致的I/O等待时间的消耗,这极大的优化了读写的性能,能更好的应对不同的网络环境。
在Java JDK包中提供了众多Buffer的类型,定义在包java.nio,这里列出常用的几种Buffer以供参考:
Buffer工作的流程图:
上面说了这么多还不如用个示例来说说NIO到底怎么使用,使用BIO在实现上有什么区别。
服务端:
public static void main(String args[]) throws IOException{
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(Inet4Address.getLocalHost(),30889));
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey sKey = iterator.next();
iterator.remove();
if(sKey.isAcceptable()){
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
socketChannel.write(ByteBuffer.wrap("Connection Success!".getBytes("UTF-8")));
System.out.println("New connection success.");
}
if(sKey.isReadable()){
SocketChannel rChannel = (SocketChannel)sKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 4);
int len=-1;
try{
if((len=rChannel.read(byteBuffer))!=0){
byteBuffer.flip();
String message = new String(byteBuffer.array(),0,len, "UTF-8");
System.out.println("Client message:"+message);
byteBuffer.clear();
rChannel.write(ByteBuffer.wrap(("Copy message:"+message).getBytes("UTF-8")));
}
}catch (IOException e){
sKey.cancel();
rChannel.close();
System.out.println("Client close connection.");
}
}
}
}
}
客户端:
public static void main(String args[]) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
ManagerThread managerThread = new ManagerThread(selector, socketChannel);
ByteBuffer readBB = ByteBuffer.allocate(1024);
int len=-1;
try {
System.out.println("rely connection...");
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress(Inet4Address.getLocalHost(),30889));
} catch (IOException e) {
e.printStackTrace();
}
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isConnectable()) {
while (!socketChannel.finishConnect()) {
System.out.println("Connecting...");
}
socketChannel.register(selector,SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
try {
if ((len=channel.read(readBB)) != 0) {
System.out.println("Service message:" + new String(readBB.array(),0,len, "UTF-8"));
readBB.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (key.isWritable()) {
SocketChannel channel = (SocketChannel) key.channel();
try {
channel.write((ByteBuffer) key.attachment());
channel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
}
}
}
}
}
private static class ManagerThread implements Runnable {
private Thread thread;
private Selector selector;
private SocketChannel socketChannel;
public ManagerThread(Selector selector, SocketChannel socketChannel) {
this.selector = selector;
this.socketChannel = socketChannel;
this.thread = new Thread(this);
this.thread.start();
}
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String message = scanner.nextLine();
try {
socketChannel.register(selector, SelectionKey.OP_WRITE, ByteBuffer.wrap(message.getBytes()));
selector.wakeup();
} catch (ClosedChannelException e) {
e.printStackTrace();
}
if ("bye".equalsIgnoreCase(message)) {
break;
}
}
}
}
示例说明:
示例和上篇BIO中实现的功能相同,都是一个简单的点对点聊天工具,但实现则完全不同,下面来看下。
客户端中的ManagerThread线程实现客户端的文字输入功能,为什么要加一个线程单独实现文字输入的功能呢?上面在Selector说到select()
方法是阻塞的,如果不这样实现将会给用户在输入时造成不连贯的感觉。在客户端的main
方法中可以看到首先是在Selector实例上注册一个OP_CONNECT的事件,然后再调用connect方法连接服务端;这里除了多一步注册连接事件,其它的和BIO基本相同,都需要显示调用远程连接;但接下来的读和写就不一样了,下面是一个死循环,在这个循环中轮询的调用Selector以获取当前可用的事件,当channel连接成功后会在绑定的SelectionKey上查询到,可分别通过isConnection
isReadable
isWritable
isAcceptable
方法判断当前Key中发生了什么事件,从而做出相应的操作;在轮询的方法中可以看到每一次有效的处理都会将Key进行删除,再在读、写、连接等事件中重新注册事件,这样做的目的是为了让处理有序;另外在读与写的处理中都使用了Buffer进行操作。服务端的代码和客户端的代码类似,只有服务端的代码有监听连接的事件而已。
往期推荐:
Java BIO 原理浅析
计算机基础 文件I/O与网络I/O 概述
知识点: Java ReentrantReadWriteLock 读写锁共享锁与排他锁
知识点: Java公平锁与非公平锁 原理讲解ReentrantLock 锁的饥饿效应及解决办法