NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供以下两种方式获取操作结果。

  • 通过java.util.concurrent.Future类来表示异步操作的结果;
  • 在异步操作的时候传入一个java.nio.channels;

CompletionHandler接口的实现类作为操作完成的回调。


NIO2.0的异步套接字通道是真正的异步非阻塞I/O,对应于UNIX网络编程中的事件驱动I/O(AIO).它不需要通过多路复用器(selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。


下面通过代码来熟悉NIO2.0AIO的相关类库,仍旧以时间服务器为例程进行讲解。


AIO创建的TimeServer源码分析


public class TimerServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
//
}
}

AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
new Thread(timeServer,"AIO-AsyncTimeServerHandler-001").start();
}
}

代码最后,创建yi异步的sh时间器处理类,然后启动线程将AsyncTimeServerHandler拉起,代码如下。


public class AsyncTimeServerHandler implements Runnable {
private int port;
CountDownLatch latch;
AsynchronousServerSocketChannel asynchronousServerSocketChannel;

public AsyncTimeServerHandler(int port) {
this.port = port;
try {
asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("The time server is start in port:" + port);
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void run() {
latch = new CountDownLatch(1);
doAccept();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void doAccept() {
asynchronousServerSocketChannel.accept(this,new AcceptCompletionHandler());
}

在最后d的doAccept()方法中,用于接收客户端的连接,由于是yi异步操作,我们可以传递一个CompletionHandler<AsynchronousSocketChannel,? super A>类型的handler实例接收accept操作成功的通知消息。本例程中我们通过 AcceptCompletionHander实例作为handler来接收通知消息,下面继续对AcceptCompletionHandler进行分析。


public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel,AsyncTimeServerHandler> {
@Override
public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment) {
attachment.asynchronousServerSocketChannel.accept(attachment, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
result.read(buffer, buffer, new ReadCompletionHandler(result));
}

@Override
public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
exc.printStackTrace();
attachment.latch.countDown();
}
}


CompletionHandler有如下两个方法,分别如下。


  • public void completed(AsynchronousSocketChannel result,AsyncTimeServerHandler attachment);
  • public void failed(Throwable exc,AsyncTimeServerHandler attachment);

下面分别对这两个接口的实现进行分析。首先看completed接口的实现,我们从AsyncTimeServerHandler获取成员变量AsynchronousServerSocketChannel,然后继续调用它的accept方法。有的读者可能会心存疑惑:既然已经接收客户端成功了,为什么还要再次调用accept方法呢?原因是这样的:调用AsynchronousServerSocketChannel的accept方法后,如果有新的客户端连接接入,系统将回调我们传入的CompletionHandler实例的completed方法,表示新的客户端已经接入成功。因为一个AsynchronousServerSocketChannel可以接收成千上万个客户端,所以需要继续调用它的accept方法,接收其他的客户端连接,最终形成一个循环。每当接收一个客户读连接成功之后,再异步接收新的客户端连接。





链路建立成功后,服务端需要接收客户端的请求消息,在accept()方法之后创建新的ByteBuffer,预分配1k的缓冲区,在通过AsynchronousSocketChannel的read方法进行异步读操作。下面我们看下异步read方法的参数。



  • ByteBuffer dst:接收缓冲区,用于从异步Channel中读取数据包;
  • A attachment:异步Channel携带的附件,通知回调的时候作为入参使用;
  • CompletionHandler<Integer,? super A>:接收通知回调的业务Handler,在本例程中为ReadCompletionHandler.

下面我们继续对ReadCompletionHandler进行分析。



public class ReadCompletionHandler implements CompletionHandler<Integer,ByteBuffer> {
private AsynchronousSocketChannel channel;

public ReadCompletionHandler(AsynchronousSocketChannel channel) {
if (this.channel == null) {
this.channel = channel;
}
}

@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] body = new byte[attachment.remaining()];
attachment.get(body);
try {
String req = new String(body, "UTF-8");
System.out.println("The time server receive order:" + req);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(currentTime);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}

private void doWrite(String currentTime) {
if (currentTime != null && currentTime.trim().length() > 0) {
byte[] bytes = (currentTime).getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();

channel.write(writeBuffer, writeBuffer,new CompletionHandler<Integer, ByteBuffer>() {

@Override
public void completed(Integer result, ByteBuffer buffer) {
//如果没有发送完成,继续发送
if (buffer.hasRemaining()) {
channel.write(buffer, buffer, this);
}
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}


至此,服务端的代码就写完成了,后面我们来看看客户端的源码分析。



public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
//
}
}

new Thread(new AsyncTimeClientHandler("127.0.0.1",port),"AIO-AsyncTimeClientHandler-001").start();
}
}

最后一行通过一个独立的I/O线程创建异步时间服务器客户端Handler。在实际项目中,我们不需要独立的线程创建异步连接对象,因为底层都是通过JDK的系统回调实现的,我们会抓去线程堆栈给大家演示。



public class AsyncTimeClientHandler implements CompletionHandler<Void,AsyncTimeClientHandler>,Runnable {
private AsynchronousSocketChannel client;
private String host;
private int port;
private CountDownLatch latch;

public AsyncTimeClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
client = AsynchronousSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void run() {
latch = new CountDownLatch(1);
client.connect(new InetSocketAddress(host, port), this, this);
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void completed(Void result, AsyncTimeClientHandler attachment) {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
client.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (buffer.hasRemaining()) {
client.write(buffer, buffer, this);
}else {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
client.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body;
try {
body = new String(bytes, "UTF-8");
System.out.println("Now is:" + body);
latch.countDown();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}

@Override
public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

由于在AsyncTimeClientHandler中大量使用了匿名内部类,所以代码看起来稍微有点复杂,下面我们对主要代码进行分析。




  • A attachment:AsynchronousSocketChannel的附件,用于回调通知时作为入参被传递,调用者可以自定义;
  • CompletionHandler<Void,? super A> handler:异步操作回调通知接口,由调用者实现。

在本例程中,这两个参数都使用AsyncTimeClientHandler类本身,因为它实现了CompletionHandler接口。


接下来我们看y异步连接成功之后d的方法回调——completed方法。第一行创建req请求消息体,对其进行编码,然后复制到发送缓冲区writeBuffer中,调用AsynchronousSocketChannel的write方法进行异步写。与服务端类似,我们可以实现CompletionHandler<Integer,ByteBuffer>接口用于写操作完成后的回调。下面的completed方法如果发送缓冲区中仍有尚未发送的字节,将继续异步发送,如果已经发送完成,则执行异步读取操作。



之后的read方法是客户端yi异步读取时间服务器服务端应答消息的处理逻辑。由于read操作是异步的,所以我们通过匿名内部类实现CompletionHandler<Integer,ByteBuffer>接口,当读取完成被JDK回调时,构造应答消息,然后打印结果。



当发生异常时,关闭链路,同时调用CountDownLatch的countDown方法让AsyncTimeClientHandler线程执行完毕,客户端退出执行。



需要指出的是,正如之前的NIO例程,我们并没有完整的处理网络的半包读写,在对例程进行功能测试的时候没有问题。但是,如果对代码稍加改造,进行压力或者性能测试,就会发现输出结果存在问题。



由于半包读写会作为专门的小节Netty的应用和源码分析章节进行详细讲解。


下面我们运行AIO版本的时间服务器程序,并通过打印线程堆栈的方法看下回调异步Channel CompletionHandler的调用情况。


server端:



AIO编程_ide



client端:



AIO编程_客户端_02



下面继续看下JDK异步回调CompletionHander的线程执行堆栈。



AIO编程_客户端_03




从“Thread-2”线程堆栈中可以发现,JDK底层通过线程池ThreadPoolExecutor来执行回调通知,异步回调通知类由sun.nio.ch.AsynchronousChannelGroupImpl实现,它经过层层调用,最终回调com.phei.netty.aio.AsyncTimeClientHandler$1.completed方法,完成回调通知。由此我们可以得出结论:异步Socket Channel是被动执行对象,我们不需要像NIO编程那样创建一个独立的I/O线程来处理读写操作。对于AsynchronousServerSocketChannel和AsynchronousSocketChannel,它们都由底层的线程池负责回调并驱动读写操作。正因为如此,基于NIO2.0新的异步非阻塞Channel进行编程比NIO编程更为简单。