JDK1.7 提供了全新的异步NIO模式。称为:NIO2.0或AIO。该模式引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供两种方式获取获取操作结果。分别是:
- 通过java.util.concurrent.Future类来表示异步操作的结果;
- CompletionHandler接口的实现类作为操作完成的回调。
NIO2.0的异步套接字通道是真正的异步非阻塞I/O,它对应UNIX网络编程中的事件驱动I/O(AIO),它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。
public class SocketServiceCb {
AsynchronousServerSocketChannel asynchronousServerSocketChannel;
CountDownLatch latch;
public void start() {
try {
asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
asynchronousServerSocketChannel.bind(new InetSocketAddress("127.0.0.1", 17777));
} catch (IOException e) {
e.printStackTrace();
}
latch = new CountDownLatch(1);
doAccept();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAccept() {
asynchronousServerSocketChannel.accept(this, new AccessCompleteHandler());
}
class AccessCompleteHandler implements CompletionHandler {
@Override
public void completed(AsynchronousSocketChannel result, SocketServiceCb attachment) {
// 当我们调用AsynchronousServerSocketChannel的accept方法后,如果有新的客户端连接接入,
// 系统将回调我们传入的CompletionHandler实例的completed方法,表示新的客户端已经接入成功,
// 因为一个AsynchronousServerSocket Channel可以接收成千上万个客户端,
// 所以我们需要继续调用它的accept方法,接收其他的客户端连接,
// 最终形成一个循环。每当接收一个客户读连接成功之后,再异步接收新的客户端连接。
attachment.asynchronousServerSocketChannel.accept(attachment, this);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.flip();
//参数1 ByteBuffer dst:接收缓冲区,用于从异步Channel中读取数据包;
//参数2 A attachment:异步Channel携带的附件,通知回调的时候作为入参使用。即回调方法的第二个参数
//参数3 CompletionHandler<Integer,? super A>:接收通知回调的业务handler,本例程中为ReadCompletionHandler。
result.read(byteBuffer, byteBuffer, new ReadCompleteHandler(result));
}
@Override
public void failed(Throwable exc, SocketServiceCb attachment) {
}
}
class ReadCompleteHandler implements CompletionHandler {
private AsynchronousSocketChannel channel;
public ReadCompleteHandler(AsynchronousSocketChannel result) {
this.channel = result;
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
//flip操作,为后续从缓冲区读取数据做准备
System.out.print("数据大小为:"+attachment.remaining());
byte[] bytes = new byte[attachment.remaining()];
attachment.get(bytes);
try {
System.out.print("服务器接收:" + new String(bytes, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
doWrite("12312");
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void doWrite(String data) {
ByteBuffer byteBuffer = ByteBuffer.wrap(data.getBytes());
byteBuffer.flip();
channel.write(byteBuffer, byteBuffer, new CompletionHandler() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if (attachment.hasRemaining()) {
channel.write(attachment, attachment, this);
}else {
latch.countDown();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
}
CompletionHandler接口即为回调,它有两个方法,执行成功的回调和异常回调,分别如下。
- public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment);
- public void failed(Throwable exc, AsyncTimeServerHandler attachment)。
CountDownLatch latch;
@Test
public void testNIOCallBack() throws IOException {
final AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
latch = new CountDownLatch(1);
channel.connect(new InetSocketAddress("127.0.0.1", 17777),null, new CompletionHandler() {
@Override
public void completed(Object result, Object attachment) {
System.out.print("链接成功");
//final ByteBuffer byteBuffer = ByteBuffer.wrap("12312414".getBytes());
byte[] bytes = "1231231231".getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(bytes);
byteBuffer.flip();
channel.write(byteBuffer, byteBuffer, new CompletionHandler() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if(attachment.hasRemaining()){
channel.write(attachment, attachment, this);
}else {
System.out.print("发送成功");
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
channel.read(readBuffer, readBuffer, new CompletionHandler() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
byte[] bytes = new byte[attachment.remaining()];
attachment.get(bytes);
System.out.print("接受信息:"+new String(bytes));
latch.countDown();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
JDK底层通过线程池ThreadPoolExecutor来执行回调通知,异步回调通知类由sun.nio.ch.AsynchronousChannelGroupImpl实现,它经过层层调用,最终回调com.phei.netty.aio.AsyncTimeClientHandler$1.completed方法,完成回调通知。由此我们也可以得出结论:异步Socket Channel是被动执行对象,我们不需要像NIO编程那样创建一个独立的I/O线程来处理读写操作。对于AsynchronousServerSocket Channel和AsynchronousSocketChannel,它们都由JDK底层的线程池负责回调并驱动读写操作。
Future模式服务器端的代码
public class SocketServiceAIO {
private static ExecutorService executorService;
private static AsynchronousServerSocketChannel serverSocketChannel;
static class ChannelWorker implements Callable {
private CharBuffer charBuffer;
private CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
private AsynchronousSocketChannel channel;
ChannelWorker(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public String call() throws Exception {
final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读取请求
while (channel.read(byteBuffer).get() != -1) {
byteBuffer.flip();
charBuffer = decoder.decode(byteBuffer);
String request = charBuffer.toString().trim();
System.out.println("客户端请求:" + request);
ByteBuffer outByteBuffer = ByteBuffer.wrap("请求收到".getBytes());
Future future = channel.write(outByteBuffer);
future.get();
if (byteBuffer.hasRemaining()) {
byteBuffer.compact();
} else {
byteBuffer.clear();
}
}
channel.close();
return "OK";
}
}
private static void init() throws IOException {
executorService = Executors.newCachedThreadPool(Executors.defaultThreadFactory());
serverSocketChannel = AsynchronousServerSocketChannel.open();
if (serverSocketChannel.isOpen()) {
serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 17777));
} else {
throw new RuntimeException("通道未打开");
}
}
private static void start() {
System.out.println("等待客户端请求...");
while (true) {
//接收客户端请求
Future future = serverSocketChannel.accept();
try {
//获取请求
AsynchronousSocketChannel channel = future.get();
//提交给线程池
executorService.submit(new ChannelWorker(channel));
} catch (Exception ex) {
ex.printStackTrace();
System.err.println("服务器关闭");
executorService.shutdown();
while (!executorService.isTerminated()) {
}
break;
}
}
}
public static void startTCPService() {
try {
init();
} catch (IOException e) {
e.printStackTrace();
System.out.println("AIO初始化失败");
}
try {
start();
} catch (Exception e) {
e.printStackTrace();
System.out.println("AIO初始化失败");
}
}
}
Future客户端代码:
@Test
public void test() {
try {
start();
} catch (Exception e) {
e.printStackTrace();
System.out.println("AIO初始化失败");
}
}
private void start() throws IOException, ExecutionException, InterruptedException {
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
if (socketChannel.isOpen()) {
socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
socketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
Future future = socketChannel.connect(new InetSocketAddress("127.0.0.1", 17777));
Object connect = future.get();
if(connect != null){
throw new RuntimeException("链接失败");
}
} else {
throw new RuntimeException("通道未打开");
}
//发送数据
Future future = socketChannel.write(ByteBuffer.wrap("我是客户端".getBytes()));
future.get();
//读取服务器的发送的数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读取服务器
while(socketChannel.read(byteBuffer).get() != -1){
byteBuffer.flip();
CharBuffer charBuffer = Charset.defaultCharset().newDecoder().decode(byteBuffer);
System.out.println("服务器说:"+charBuffer.toString().trim());
byteBuffer.clear();
}
}
至此,我们学习了在java中几种不同的网络编程模式。这里总是一下:
1.异步非阻塞I/O
即本节所述的JDK 1.7 AIO。很多人喜欢将JDK1.4提供的NIO框架称为异步非阻塞I/O,但是,如果严格按照UNIX网络编程模型和JDK的实现进行区分,实际上它只能被称为非阻塞I/O,不能叫异步非阻塞I/O。因为它是基于Selctor的select/poll模型实现,它是基于I/O复用技术的非阻塞I/O,不是异步I/O。在JDK1.5 update10和Linux core2.6以上版本,Sun优化了Selctor的实现,它在底层使用epoll替换了select/poll,上层的API并没有变化,可以认为是JDK NIO的一次性能优化,但是它仍旧没有改变I/O的模型。
由JDK1.7提供的NIO2.0,新增了异步的套接字通道,它是真正的异步I/O,在异步I/O操作的时候可以传递信号变量,当操作完成之后会回调相关的方法,异步I/O也被称为AIO。
2.多路复用器/选择器 Selector
在前面的章节我们介绍过Java NIO的实现关键是多路复用I/O技术,多路复用的核心就是通过Selector来轮询注册在其上的Channel,当发现某个或者多个Channel处于就绪状态后,从阻塞状态返回就绪的Channel的选择键集合,进行I/O操作。由于多路复用器是NIO实现非阻塞I/O的关键
3.伪异步I/O
伪异步I/O的概念完全来源于实践。在JDK NIO编程没有流行之前,为了解决Tomcat通信线程同步I/O导致业务线程被挂住的问题,大家想到了一个办法:在通信线程和业务线程之间做个缓冲区,这个缓冲区用于隔离I/O线程和业务线程间的直接访问,这样业务线程就不会被I/O线程阻塞。而对于后端的业务侧来说,将消息或者Task放到线程池后就返回了,它不再直接访问I/O线程或者进行I/O读写,这样也就不会被同步阻塞。像这样通过线程池做缓冲区的做法来解决一连接一线程问题,习惯于称它为伪异步I/O,而官方并没有伪异步I/O这种说法,请大家注意。
4.同步阻塞IO
即最简单,也最好理解,一个Socket链接,开启一个线程去处理。
如果客户端并发连接数不多,服务器的负载也不重,那就完全没必要选择NIO做服务端,毕竟非阻塞的处理IO是比阻塞IO的响应时间要慢一些(涉及多线程的上下文切换等问题);如果是相反情况,那就要考虑选择合适的NIO框架进行开发。但并建议直接利用JDK的NIO来开发。
开发出高质量的NIO程序并不是一件简单的事情,除去NIO固有的复杂性和BUG不谈,作为一个NIO服务端,需要能够处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写、网络拥塞等情况。由于NIO还涉及到Reactor模式,如果你没有足够的NIO网络编程和多线程编程经验积累,一个NIO框架的稳定往往需要半年甚至更长的时间。更为糟糕的是,一旦在生产环境中发生问题,往往会导致跨节点的服务调用中断,严重的可能会导致整个集群环境都不可用,需要重启服务器,这种非正常停机会带来巨大的损失。
从可维护性角度看,由于NIO采用了异步非阻塞编程模型,而且是一个I/O线程处理多条链路,它的调试和跟踪非常麻烦,特别是生产环境中的问题,我们无法进行有效的调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大。
由于上述原因,在大多数场景下,不建议大家直接使用JDK的NIO类库,除非你精通NIO编程或者有特殊的需求。在绝大多数的业务场景中,我们可以使用NIO框架Netty来进行NIO编程,它既可以作为客户端也可以作为服务端,同时支持UDP和异步文件传输,功能非常强大。
后续笔者将记录Netty的学习笔记