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實現高併發服務器》

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