《BIO、NIO、AIO非阻塞通信实例》
NIO与阻塞式通信区别
传统的阻塞式通信中ServerSocket创建时可以与自身一个端口绑定,然后用accept监听客户端的请求,当接收到客户端的Socket请求时,服务器也对应产生一个Socket。客户端要新建一个Socket只需要知道服务器的地址和端口。双方建立socket后,通过getInputStream和getOutputStream即可读写该Socket。阻塞式通信在程序执行输入、输出操作后,会在操作返回之前一直阻塞该线程,所以要求服务器必须为每个客户端提供一个独立线程来处理,服务器要处理多个客户端请求,通常都是采用多线程工作方式,将一个socket传入处理该客户的线程中,一个线程的栈内存大小根据操作系统默认区间一般为64Kb到1M。在活动链接小于1000情况时,利用线程池,可以采用阻塞式通信,但是线程数过千,整个JVM内存都会被吃掉一般。
NIO 非阻塞式通信可以来开发高性能的网络服务器,非阻塞式通信是基于通道Channel和缓冲区Buffer进行读写的,而非字节流和字符流。非阻塞式通信引入了Selectors选择器的概念用于监听多个通道的事件,服务器上所有的Channel包括ServerSocketChannel和SocketChannel都要向Selector注册,当任何一个或者多个Channel具有可用的IO操作时,该Selector的selector()方法都会返回大于0的整数,并通过selectedKeys()方法返回这些Channel对应的selectionKey集合。NIO中的ServerSocketChannel不像ServerSocket可以直接监听某个端口,必须要先调用open()静态方法返回一个SeverSocketChannel实例,然后在使用它的bind()方法进行端口监听,还要设置它的非阻塞模式注册到指定的Selector。
NIO在编程时难度会比BIO大很多,并且如果考虑“半包读”和“半包写”,代码会更加复杂。但即使如此,NIO应用更加广泛:优点有1、客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后序结果,不需要像之前的客户端那么被同步阻塞。2、SocketChannel的读写操作都是异步的,如果没有可读写的操作它不会同步等待,直接返回,这样I/O通信线程就可以处理其他链路,而不需要同步等待这个链路可用。3、线程模型的优化,由于JDK的Selector在linux等主流操作系统上通过epoll实现,它没有连接句柄的限制而只限制与操作系统对单个进程的句柄限制或者操作系统最大句柄树限制,意味着一个Selector线程可以同时处理上万个客户端连接,并且性能不会随着客户端的增加而线性下降。因此,非常适合做高性能、高负载的网络服务器。
非阻塞式到底非阻塞在哪里呢?传统阻塞式IO的瓶颈在于不能处理过多的连接,阻塞式IO就是在进行读写的时候调用了某个方法,如read()或write()方法 在该方法执行完之前,会一直等待,直到该方法执行完毕,所以把时间都花在等待操作请求上。非阻塞IO处理连接是异步的,当某个连接发送请求到服务器,服务器把这个连接请求当作一个请求"事件",并把这个"事件"分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还。这样一个线程就可以异步的处理多个事件。
NIO通信IO工作方式原理
NIO采用多路复用器Selector作为通信的基础,简单来讲,Selector会不断轮询注册在上面的Channel,如果某个Channel上面发送了读、写时间那么这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获得就绪的Channnel集合,再根据响应Channel发送的事件进行处理。NIO采用内存映射文件的方式处理输入输出,Java NIO并且提供了与标准IO不同的IO工作方式:
- Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
- Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
- Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
Channel 和 Buffer :所有的 IO 在NIO 中都从一个Channel 开始。Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。Channel和Buffer有好几种类型。一些主要Channel的实现有FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。Java NIO里关键的Buffer实现有ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer,Buffer覆盖了你能通过IO发送的基本数据类型:byte, short, int, long, float, double 和 char。
Channel提供了map()方法,可以通过map方法将数据映射到Buffer中。Buffer中有三个重要的概念,capacity、界限limit、位置position,capacity是Buffer的容量,limit是不可读写的缓冲区索引,位置position是指明了下一个可以读出或者写入的缓冲区位置索引,position之前表示已经读写过的位置。Buffer中有两个重要的方法,flip()和clear(),flip()为从Buffer中取出数据做好准备,而clear()为再次向Buffer中写入数据做准备。当Buffer装入数据结束,即此时position描述的是写入到的位置,调用Buffer的flip()方法,可将limit设置成position位置,将position设为0,这样可使Buffer的读写指针移动到开始位置。调用clear()方法,将position设置为0,limit设置为capacity,为再次写入做好准备。
Selector :Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。 这是在一个单线程中使用一个Selector处理3个Channel的图示:
AIO通信编程
NIO2.0不需要通过多路复用器对注册的通道进行轮询操作,而是引入了异步通道的概念,并且提供了异步文件通道和异步套接字通道的实现。异步通道通过java.util.concurrent.Future类来表示异步操作的结果,AIO运用了大量的JDK系统回调实现,所以相对难理解。
AIO服务端创建过程
步骤一:首先创建一个异步的时间服务器处理类AsyncTimeServerHandler,AsyncTimeServerHandler实现Runnable接口。在AsyncTimeServerHandler的构造方法中,要首先创建一个异步的服务端通道AsynchronousServerSocketChannel,然后调用其bind方法绑定监听端口,如果端口合法且没被占用,则绑定成功。
public class AsyncTimeServerHandler implements Runnable {
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() {
......
}
public void doAccept(){
.....
}
}
步骤二:在时间服务器处理类AsyncTimeServerHandler的run方法中,初始化一个CountDownLatch对象,作用是在完成一组正在执行的操作之前,允许当前的线程一直阻塞,在本例中,我们让线程执行阻塞,防止服务端执行完成退出。其中 doAccept();是用于接收客户端的连接,由于是异步操作,我们可以通过传递一个CompletionHandler<AsynchronousSocketChannel,?super A>类型的handler实例来接收accept操作成功的通知消息,本例我满通过AcceptCompletionHandler实例作为handler接收通知消息。
public class AsyncTimeServerHandler implements Runnable {
public AsyncTimeServerHandler(int port) {
....
}
@Override
public void run() {
//CountDownLatch作用是完成一组正在执行的操作之前,允许当前的线程一直阻塞,
//实际项目中不需要独立启动一个线程来处理的
latch=new CountDownLatch(1);
doAccept();
try{
latch.await();
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void doAccept(){
asynchronousServerSocketChannel.accept(this,new AcceptCompletionHandler());
}
}
步骤三:AcceptCompletionHandler 中,我们通过attachment获取成员变量AsynchronousSocketChannel ,同时还要继续调用它的accept方法,再次调用原因在于AsynchronousSocketChannel 的accept方法调用后,如果有新的客户端连接接入,系统将回调我们传入的CompletetionHandler实例的completed方法,表示新的客户端已经连接成功。因为一个AsynchronousServerSocketChannel可以接受成千上万个客户端,所以继续调用它的accept()方法,接收其他客户端连接,最终形成一个循环。每当接收一个客户读连接成功后,再一步接收新的客户端连接。服务端预分配1MB的缓冲区,通过调用AsynchronousSocketChannel 的read方法进行异步读写操作。
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel,AsyncTimeServerHandler> {
@Override
public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment) {
//再次让asynchronousServerSocketChannel对象调用accept方法是因为:
//调用AsynchronousServerSocketChannel的accept方法后,如果有新的客户端接入,
// 系统将回调我们传入的CompletionHandler实例的completed方法,表示新客户端连接成功。
// 因为AsynchronousServerSocketChannel可以接受成千上万个客户端,所以需要继续调用它的accept方法,
// 接受其他客户端连接,最终形成一个环;每当一个客户端连接成功后,再异步接受新的客户端连接
attachment.asynchronousServerSocketChannel.accept(attachment,this);
ByteBuffer readBuffer=ByteBuffer.allocate(1024);
result.read(readBuffer,readBuffer,new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
exc.printStackTrace();
attachment.latch.countDown();
}
}
步骤四:在异步read方法中,需要传入1、ByteBuffer 接收缓冲区用于接收异步Channel的数据包。2、attachment:异步Channel携带的附件,通知回调时候作为入参使用。3、CompletetionHandler<Integer,?super A>:接收通知回调业务的Hander,在本例中为ReadCompletionHandler。于是,我们要重写ReadCompletionHandler,ReadCompletionHandler的构造方法中传入了AsynchronousSocketChannel 当做成员变量,主要作为用于读取半包消息和发送应答。
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel socketChannel;
public ReadCompletionHandler(AsynchronousSocketChannel socketChannel) {
if (this.socketChannel == null) {
this.socketChannel = socketChannel;
}
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
......
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
.....
}
private void doWrite(String currentTime) {
...
}
}
步骤五:在读取到消息后,执行completed函数,首先对attachment进行flip操作,为后序从缓冲区读取数据做准备,根据缓冲区的可读字节创建byte数组,然后new String方法创建请求消息,根据请求消息进行业务逻辑实现。在doWrite操作中,首先根据对当前时间进行合法性校验,如果合法,调用字符串的解码方法将应答消息编码成字节数组,然后将其复制到发送缓冲区writeBuffer中,最后调用AsynchronousSocketChannel异步write方法。正如上一步的read()方法一样,它 也有三个与read方法相同的参数。最后的failed方法实现很简单,当异常发生时对Throwable进行逻辑判断,如果是I/O异常,则关闭链路是否自愿,如果是其他异常根据自己的逻辑进行处理。
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel socketChannel;
......
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] body = new byte[attachment.remaining()];
attachment.get(body);
try {
String request = new String(body, "UTF-8");
System.out.println("The time server receive order : " + request);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(request) ? new Date().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();
final ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
socketChannel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
//如果没有发送完继续发送
if (attachment.hasRemaining()) {
socketChannel.write(attachment, attachment, this);
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
socketChannel.close();
} catch (IOException e) {
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
AIO客户端创建过程
客户端异步时间服务器客户端AsyncTimeClientHandler 实现了CompletionHandler、Runnable接口。
步骤一:构造方法,首先通过AsynchronousSocketChannel的open方法创建一个AsynchronousSocketChannel 对象。
public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
private AsynchronousSocketChannel socketChannel;
private String host;
private int port;
private CountDownLatch latch;
public AsyncTimeClientHandler(String host,int port){
this.host=host;
this.port=port;
try {
socketChannel=AsynchronousSocketChannel.open();
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public void run() {
......
}
@Override
public void completed(Void result, AsyncTimeClientHandler attachment) {
......
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
......
}
}
步骤二:在run()方法中,创建CountDownLatch进行等待,防止异步操作没有执行完成线程就退出。在connect方法中发起异步操作,其中传入attachment,回调通知是作为入参被传递,调用者可以自定义。传入CompletionHandler<void ,?super A>hander,异步操作回调通知接口,有调用者自己实现。在本例中AsyncTimeClientHandler 实现了CompletionHandler接口 ,所以参数都是本身。
public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
....
@Override
public void run() {
latch=new CountDownLatch(1);
socketChannel.connect(new InetSocketAddress(host,port),this,this);
try {
latch.await();
}catch (InterruptedException e){
e.printStackTrace();
}
try {
socketChannel.close();
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public void completed(Void result, AsyncTimeClientHandler attachment) {
......
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
......
}
}
步骤三:异步连接成功后方法回调complete方法,我们创建消息请求体,对其进行编码,然后复制到发送缓冲区writeBuffer中,调用AsynchronousSocketChannel的write方法进行异步写,与服务端类似,write方法进行异步写,实现一个匿名的CompletionHandler类用于写操作完成后的回调,如果发送缓冲区有尚未发送的字节,那么异步发送,如果已经发送完成,则执行异步读取操作。
public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
....
@Override
public void completed(Void result, AsyncTimeClientHandler attachment) {
byte[] request="QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer=ByteBuffer.