转自:http://blog.insanecoder.top/javazhong-bio-niohe-aioshi-yong-yang-li/
上文中分析了阻塞,非阻塞,同步和异步概念上的区别以及各种IO模型的操作流程,本篇文章将主要介绍Java中BIO,NIO和AIO三种IO模型如何使用。需要注意的是,本文中所提到的所有样例都是在一个server对应一个client的情况下工作的,如果你想扩展为一个server服务多个client,那么代码需要做相应的修改才能使用。另外,本文只会讲解server端如何处理,客户端的操作流程可以仿照服务端进行编程,大同小异。文章最后给出了源码的下载地址。
在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的编程的模型比较简单,这里就写这么多,需要下载源代码的可以到文章末尾。
BIO的编程模型简单易行,但是缺点也很明显。由于采用的是同步阻塞IO的模式,所以server端要为每一个连接创建一个线程,一方面,线程之间在进行上下文切换的时候会造成比较大的开销,另一方面,当连接数过多时,可能会造成服务器崩溃的现象产生。
为了解决这个问题,在JDK 1.4的时候,引入了NIO(New IO)的概念。NIO主要由三个部分组成,即Channel,Buffer和Selector。Channel可以跟BIO中的Stream类比,不同的是Channel是可读可写的。当和Channel进行交互的时候需要Buffer的支持,数据可以从Buffer写到Channel中,也可以从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之前,一定要注意把对应的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处于空闲状态,都是可写的。这个我也从实际的程序中论证了。
在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