C10破局(三)——Java AIO实现高并发服务器

上一篇博客中我们介绍了Java中的NIO模型,而JDK1.7之后升级NIO类库,也就是NIO2.0.Java正式提供了异步IO操作,同时提供了与UNIX网络编程事件驱动IO相对应的AIO。NIO(non-block IO)指的是同步非阻塞IO,AIO(Asynchronous IO)则是异步非阻塞IO。我们先来了解一些基础知识,最后再用AIO来设计一个服务器。

一、IO处理模型

服务器往客户端发送数据的过程主要有以下四步:

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和AIO的区别

1、同步与异步

NIO,又称同步IO,当应用程序发出一个IO请求后,它需要主动去检查这个IO请求是否完成。

AIO,又称异步IO,当应用程序发出一个IO请求后,就不再管它了,当这个IO请求完成之后,操作系统主动通知应用程序来做后续的处理。

举个例子:你在家里烧开水,你把水放进去烧以后就去看电视了。如果是普通的烧水壶,你每隔一段时间都要去看一眼水烧开了没有,这就是同步IO;但是如果是响水壶,你只要安安静静地看你的电视,等水壶一响,你就知道水烧开了,这就是异步IO。

2、设计模式

NIO采用Reactor的设计模式,而AIO采用Proactor的设计模式。

Reactor和Proactor最主要的区别就是数据的读取和写入Buffer的操作由谁来完成。

对于Reactor而言,它被激活时仅表示当前SocketChannel中有数据来了,应用程序需要自己把SocketChannel中的数据搬到Buffer里面;

而Proactor被激活时则表示SocketChannel中有数据来了,并且我已经帮你把它搬到Buffer里面了,应用程序可以直接对Buffer中的数据进行处理。

3、适用场景

NIO采用了Reactor的模式,读写由应用程序自己进行,适合用于连接数目多且连接短的场景中

AIO采用了Proactor,读写操作由内核完成,适合用于连接数目多且连接长的场景中

三、AIO原理

1、Java回调模式

设想这么一个场景,我们现在需要往磁盘中取数据进行处理,并把处理后的结果发送给客户端。

(1)、传统方法

定义一个类A,实现read()、process()和send()三个方法,在线程中一次调用这三个方法。但是计算很复杂,主线程就会一直阻塞在process()方法中。

(2)、异步调用

我们定义了两个类,其中类A包含read()和send()方法,类B包含process()方法。首先我们启动主线程读取数据,数据读取结束后我们就启动新线程实例化类B,调用它的process()处理数据,主线程就可以做其他事了。等到b.process()方法执行结束后,再主动回调a的send()方法即可。

关于Java回调模式,奉上一篇通俗易懂的博文(《java回调函数详解》

2、AIO原理图

我们根据上面的原理来分析一下AIO的过程

(1)、客户端

A、首先建立一个AsynchronusSocketChannel,绑定端口号,并调用它的connect()方法尝试连接服务器,并指明connect()方法的回调类为ConnectCompletionHandler;

B、连接服务器得到两种返回值。如果连接失败则调用AcceptCompletionHandler类的failed()方法进行后续处理,如果成功,则调用ConnectCompletionHandler类的completion()方法进行后续处理。

(2)、服务端

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()方法对消息进行处理

(3)注意点

A、所有方法的回调结果都由相应的CompletionHandler类的进行处理,eg.accept()方法由AcceptCompletionHandler类(实现了CompletionHandler接口的类)进行处理。

四、代码实现

1、服务器

(1)、TimeServer类

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();
	}
}

(2)、AsyncTimeServerHandler类

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<AsynchronousSocketChannel, AsyncTimeServerHandler>() {
			//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();
            }
        });
        
	}
	

}

(3)、ReadCompletionHandler类

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<Integer, ByteBuffer> {
    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<Integer, ByteBuffer>() {
                        @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实现高并发服务器》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章