上一篇博客中我们介绍了Java中的NIO模型,而JDK1.7之后升级NIO类库,也就是NIO2.0.Java正式提供了异步IO操作,同时提供了与UNIX网络编程事件驱动IO相对应的AIO。NIO(non-block IO)指的是同步非阻塞IO,AIO(Asynchronous IO)则是异步非阻塞IO。我们先来了解一些基础知识,最后再用AIO来设计一个服务器。
服务器往客户端发送数据的过程主要有以下四步:
1、服务器调用read()方法,由用户态转为内核态,把磁盘的数据读到Read Buffer(内核缓存区)上;
2、read()方法返回,把Read Buffer(内核缓存区)的数据读到Buffer上,服务器由内核态转为用户态;之后对Buffer中的数据做进一步处理;
3、数据处理完之后,服务器调用send()方法,由用户态转为内核态,把Buffer中的数据读到Socket Buffer上;
4、将数据读到NIC Buffer(NetWork Interface Card,网卡的缓存区),send()方法返回,服务器由内核态转为用户态;
如果想更详细地了解IO的知识,可以看下这篇博文(https://www.jianshu.com/p/9b2922c10129)
NIO,又称同步IO,当应用程序发出一个IO请求后,它需要主动去检查这个IO请求是否完成。
AIO,又称异步IO,当应用程序发出一个IO请求后,就不再管它了,当这个IO请求完成之后,操作系统主动通知应用程序来做后续的处理。
举个例子:你在家里烧开水,你把水放进去烧以后就去看电视了。如果是普通的烧水壶,你每隔一段时间都要去看一眼水烧开了没有,这就是同步IO;但是如果是响水壶,你只要安安静静地看你的电视,等水壶一响,你就知道水烧开了,这就是异步IO。
NIO采用Reactor的设计模式,而AIO采用Proactor的设计模式。
Reactor和Proactor最主要的区别就是数据的读取和写入Buffer的操作由谁来完成。
对于Reactor而言,它被激活时仅表示当前SocketChannel中有数据来了,应用程序需要自己把SocketChannel中的数据搬到Buffer里面;
而Proactor被激活时则表示SocketChannel中有数据来了,并且我已经帮你把它搬到Buffer里面了,应用程序可以直接对Buffer中的数据进行处理。
NIO采用了Reactor的模式,读写由应用程序自己进行,适合用于连接数目多且连接短的场景中
AIO采用了Proactor,读写操作由内核完成,适合用于连接数目多且连接长的场景中
设想这么一个场景,我们现在需要往磁盘中取数据进行处理,并把处理后的结果发送给客户端。
定义一个类A,实现read()、process()和send()三个方法,在线程中一次调用这三个方法。但是计算很复杂,主线程就会一直阻塞在process()方法中。
我们定义了两个类,其中类A包含read()和send()方法,类B包含process()方法。首先我们启动主线程读取数据,数据读取结束后我们就启动新线程实例化类B,调用它的process()处理数据,主线程就可以做其他事了。等到b.process()方法执行结束后,再主动回调a的send()方法即可。
关于Java回调模式,奉上一篇通俗易懂的博文(《java回调函数详解》)
我们根据上面的原理来分析一下AIO的过程
A、首先建立一个AsynchronusSocketChannel,绑定端口号,并调用它的connect()方法尝试连接服务器,并指明connect()方法的回调类为ConnectCompletionHandler;
B、连接服务器得到两种返回值。如果连接失败则调用AcceptCompletionHandler类的failed()方法进行后续处理,如果成功,则调用ConnectCompletionHandler类的completion()方法进行后续处理。
A、建立一个AsynchronousServerSocketChannel,绑定端口号,调用accept()方法等待客户端连接,并指明accept()方法的回调类为AcceptCompletionHandler;
B、判断连接是否成功,如果失败就调用AcceptCompletionHandler的failed()方法,如果成功就调用AcceptCompletionHandler的completed()方法;
C、如果调用了AcceptCompletionHandler的completed()方法,即连接成功时。服务端总共进行了三个操作。一个是建立AsynchronousSocketChannel与当前的客户端建立连接(调用completed()方法之前),一个是调用AsynchronousServerSocketChannel.accept()继续处理其他的客户端连接(调用completed()方法时),最后一个是调用AsychronousSocketChannel.read()进行客户端消息的读操作,并指明回调类为ReadCompletionHandler
D、如果AsynchronousSocketChannel接收到数据,在接收完数据后,会调用ReadCompletionHandler.completed()方法对消息进行处理
A、所有方法的回调结果都由相应的CompletionHandler类的进行处理,eg.accept()方法由AcceptCompletionHandler类(实现了CompletionHandler接口的类)进行处理。
package aioserver;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TimeServer {
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();
}
}
package aioserver;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;
public class AsyncTimeServerHandler implements Runnable{
private int port;
/*
* CountDownLatch类位于java.util.concurrent包下,
* 利用它可以实现类似计数器的功能。比如有一个任务A,
* 它要等待其他4个任务执行完毕之后才能执行,
* 此时就可以利用CountDownLatch来实现这种功能了。
*/
CountDownLatch latch;
AsynchronousServerSocketChannel asychronousServerSocketChannel;
public AsyncTimeServerHandler(int port) {
this.port = port;
try {
//1、创建一个异步的服务器通道AsynchronousServerSocketChannel,并绑定端口号
asychronousServerSocketChannel = AsynchronousServerSocketChannel.open();
asychronousServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("The time server is start in port : " + port);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void run() {
//初始化CountDownLantch为1,在完成一组正在执行的操作之前,允许当前线程一直阻塞
//这里是为了防止服务端执行完成退出
//在实际的项目应用中,不需要启动独立的线程来处理AsynchronousServerSocketChannel?
latch = new CountDownLatch(1);
doAccept();
//等待线程执行完毕
try {
//await()和wait()的区别
latch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void doAccept() {
//1、调用accept()方法,并指明回调类为AcceptCompletionHandler,不过这里用了匿名内部类,没有定义出具体的类名
asychronousServerSocketChannel.accept(this, new CompletionHandler() {
//2、判断是否连接成功,成功就调用completed()方法
@Override
public void completed(AsynchronousSocketChannel result,
AsyncTimeServerHandler attachment) {
//3、调用accept()方法,监听其他的客户端连接
attachment.asychronousServerSocketChannel.accept(attachment, this);
//链路建立成功之后,服务端需要接收客户端的请求消息,
//创建新的ByteBuffer,预分配1M的缓冲区。
ByteBuffer buffer = ByteBuffer.allocate(1024);
//3、调用AsynchronousSocketChannel的read方法进行异步读操作,并指明回调类为ReadCompletionHandler
result.read(buffer, buffer, new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
exc.printStackTrace();
attachment.latch.countDown();
}
});
}
}
package aioserver;
import java.nio.ByteBuffer;
import java.nio.channels.CompletionHandler;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.channels.AsynchronousSocketChannel;
public class ReadCompletionHandler implements CompletionHandler {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel channel) {
//将AsynchronousSocketChannel通过参数传递到ReadCompletion Handler中当作成员变量来使用
//主要用于读取半包消息和发送应答。本例程不对半包读写进行具体说明
if (this.channel == null)
this.channel = channel;
}
//4、读取消息结束,调用ReadCompletionHandler.completed()方法进行处理
@Override
public void completed(Integer result, ByteBuffer attachment) {
//读取到消息后的处理,首先对attachment进行flip操作,为后续从缓冲区读取数据做准备。
attachment.flip();
//根据缓冲区的可读字节数创建byte数组
byte[] body = new byte[attachment.remaining()];
attachment.get(body);
try {
//通过new String方法创建请求消息,对请求消息进行判断,
//如果是"QUERY TIME ORDER"则获取当前系统服务器的时间,
String req = new String(body, "UTF-8");
System.out.println("The time server receive order : " + req);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
//调用doWrite方法发送给客户端。
doWrite(currentTime);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
private void doWrite(String currentTime) {
if (currentTime != null && currentTime.trim().length() > 0) {
//首先对当前时间进行合法性校验,如果合法,调用字符串的解码方法将应答消息编码成字节数组,
//然后将它复制到发送缓冲区writeBuffer中,
byte[] bytes = (currentTime).getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
//最后调用AsynchronousSocketChannel的异步write方法。
//正如前面介绍的异步read方法一样,它也有三个与read方法相同的参数,
//在本例程中我们直接实现write方法的异步回调接口CompletionHandler。
channel.write(writeBuffer, writeBuffer,
new CompletionHandler() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
//对发送的writeBuffer进行判断,如果还有剩余的字节可写,说明没有发送完成,需要继续发送,直到发送成功。
if (buffer.hasRemaining())
channel.write(buffer, buffer, this);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
//关注下failed方法,它的实现很简单,就是当发生异常的时候,对异常Throwable进行判断,
//如果是I/O异常,就关闭链路,释放资源,
//如果是其他异常,按照业务自己的逻辑进行处理,如果没有发送完成,继续发送.
//本例程作为简单demo,没有对异常进行分类判断,只要发生了读写异常,就关闭链路,释放资源。
try {
channel.close();
} catch (IOException e) {
// ingnore on close
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
说明:本文代码部分主要来自《netty权威指南一书》
C10k系列文章:
《C10k破局(一)——线程池和消息队列实现高并发服务器》
《C10k破局(二)——Java NIO实现高并发服务器(一张图看懂Java NIO)》
《C10破局(三)——Java AIO实现高并发服务器》