Java中BIO,NIO和AIO使用样例

上文中分析了阻塞,非阻塞,同步和异步概念上的区别以及各种IO模型的操作流程,本篇文章将主要介绍Java中BIO,NIO和AIO三种IO模型如何使用。需要注意的是,本文中所提到的所有样例都是在一个server对应一个client的情况下工作的,如果你想扩展为一个server服务多个client,那么代码需要做相应的修改才能使用。另外,本文只会讲解server端如何处理,客户端的操作流程可以仿照服务端进行编程,大同小异。文章最后给出了源码的下载地址。

BIO(Blocking I/O)

在Java中,BIO是基于流的,这个流包括字节流或者字符流,但是细心的同学可能会发现基本上所有的流都是单向的,要么只读,要么只写。在实际上编程时,在对IO操作之前,要先获取输入流或输出流,然后对输入流读或对输出流写即完成实际的IO读写操作。 首先需要新建一个ServerSocket对象监听特定端口,然后当有客户端的连接请求到来时,在服务器端获取一个Socket对象,用来进行实际的通信。

ServerSocket serverSocket = new ServerSocket(PORT);  
Socket socket = serverSocket.accept();  

获取到Socket对象后,通过这个Socket对象拿到输入流和输出流就可以进行相应的读写操作了。

DataInputStream in = new DataInputStream(socket.getInputStream());  
DataOutputStream out = new DataOutputStream(socket.getOutputStream());  

由于BIO的编程的模型比较简单,这里就写这么多,需要下载源代码的可以到文章末尾。

NIO(New I/O, or Nonblocking I/O)

BIO的编程模型简单易行,但是缺点也很明显。由于采用的是同步阻塞IO的模式,所以server端要为每一个连接创建一个线程,一方面,线程之间在进行上下文切换的时候会造成比较大的开销,另一方面,当连接数过多时,可能会造成服务器崩溃的现象产生。

为了解决这个问题,在JDK 1.4的时候,引入了NIO(New IO)的概念。NIO主要由三个部分组成,即ChannelBufferSelectorChannel可以跟BIO中的Stream类比,不同的是Channel是可读可写的。当和Channel进行交互的时候需要Buffer的支持,数据可以从Buffer写到Channel中,也可以从Channel中读到Buffer中,他们的关系如下图。 channel&buffer 以SocketChannel为例,Channel和Buffer交互的例子如下。ByteBuffer是Buffer的一种实现,在使用ByteBuffer之前,需要为其分配空间,然后调用Channel的read方法将数据写入Buffer中,在完成后,在使用Buffer中的数据之前需要调用Buffer的flip方法。Buffer中有个position常量,记录当前操作数据的位置,当向Buffer中写数据时,position会记录当前写的位置,当写操作完成后,flip会把position至为0,这样读取Buffer中的数据时,就会从0开始了。另外需要注意的是处理完Buffer中的数据后需要调用clear方法将Buffer清空。向Channel中写数据的操作比较简单,这里不再赘述。

// Read data from channel to buffer
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();  
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);  
while (socketChannel.read(byteBuffer) > 0) {  
    byteBuffer.flip();
    while(byteBuffer.hasRemaining()){
        System.out.print((char) byteBuffer.get());
    }
    byteBuffer.clear();
}

// Write data to channel from buffer
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));  

NIO中另一个重要的组件是Selector,Selector可以用来检查一个或多个Channel是否有新的数据到来,这种方式可以实现在一个线程中管理多个Channel的目的,示意图如下。
selector 在使用selector之前,一定要注意把对应的Channel配置为非阻塞。否则在注册的时候会抛异常。

serverSocketChannel.configureBlocking(false);  

然后调用select函数,select是个阻塞函数,它会阻塞直到某一个操作被激活。这个时候可以获取一系列的SelectionKey,通过这个SelectionKey可以判断其对应的Channel可进行的操作(可读,可写或者可接受连接),然后进行相应的操作即可。这里还要注意一个问题就是在判断完可执行的操作后,需要将这个SelectionKey从集合中移除

selector.select();

Set<SelectionKey> selectionKeys = selector.selectedKeys();  
Iterator<SelectionKey> iterator = selectionKeys.iterator();

while (iterator.hasNext()) {  
    SelectionKey selectionKey = iterator.next();

    if (!selectionKey.isValid())
        continue;

    if (selectionKey.isAcceptable()) {
        // ready for accepting        
    } else if (selectionKey.isReadable()) {
        // ready for reading                   
    } else if (selectionKey.isWritable()) {
        // ready for writing
    }

    iterator.remove();
}

NIO这里最后一个问题是,什么时候Channel可写,这个问题困扰了我很久,经过从网上查资料最后得出的结论是,只要这个Channel处于空闲状态,都是可写的。这个我也从实际的程序中论证了。

AIO(Asynchronous I/O)

在JDK 1.7时,Java引入了AIO的概念,AIO还是基于Channel和Buffer的,不同的是它是异步的。用户线程把实际的IO操作以及数据拷贝全部委托给内核来做,用户只要传递给内核一个用于存储数据的地址空间即可。内核处理的结果通过两种方式返回给用户线程。一是通过Future对象,另外一种是通过回调函数的方式,回调函数需要实现CompletionHandler接口。这里只给出通过回调方式处理数据的样例,其中关键的步骤已经在程序中添加了注释。

// 创建AsynchronousServerSocketChannel监听特定端口,并设置回调AcceptCompletionHandler
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));  
serverSocketChannel.accept(serverSocketChannel, new AcceptCompletionHandler());

// 监听回调,当用连接时会触发该回调
private static class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel> {  
    @Override
    public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 注册read请求以及回调ReadCompletionHandler
        result.read(byteBuffer, result, new ReadCompletionHandler(byteBuffer, "client"));
        // 递归监听
        attachment.accept(attachment, this);
    }
    @Override
    public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
        // 递归监听
        attachment.accept(attachment, this);
    }
}

// 读取数据回调,当有数据可读时触发该回调
public class ReadCompletionHandler  implements CompletionHandler<Integer, AsynchronousSocketChannel> {  
    private ByteBuffer byteBuffer;
    private String remoteName;
    public ReadCompletionHandler(ByteBuffer byteBuffer, String remoteName) {
        this.byteBuffer = byteBuffer;
        this.remoteName = remoteName;
    }
    @Override
    public void completed(Integer result, AsynchronousSocketChannel attachment) {
        if (result <= 0)
            return;

        byteBuffer.flip();
        System.out.println("[" + this.remoteName + "] " + new String(byteBuffer.array()));

        byteBuffer.clear();
        // 递归监听数据
        attachment.read(byteBuffer, attachment, this);
    }

    @Override
    public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
        byteBuffer.clear();
        // 递归监听数据
        attachment.read(byteBuffer, attachment, this);
    }
}

上面给出了BIO,NIO以及AIO在Java中的使用的部分程序,并且分析了其中关键步骤的使用及其需要注意的事项。

需要源码的同学可以到这里下载

参考

Java NIO Tutorial

Shaohang Zhao

Read more posts by this author.