1. 程式人生 > >java網路程式設計之AIO/NIO2.0(三)

java網路程式設計之AIO/NIO2.0(三)

概念理解

      AIO程式設計,在NIO基礎之上引入了非同步通道的概念,並提供了非同步檔案和非同步套接字通道的實現,從而在真正意義上實現了非同步非阻塞,之前我們學習的NIO只是非阻寒而並非非同步。而AIO它不需要通過多路複用器對註冊的通道進行輪詢操作即可實現非同步讀寫,從而簡化了NIO程式設計模型。也可以稱之為NIO2.0,這種模式才真正的屬於我們非同步非阻寒的模型。

案例

Server.java

public class Server {
    //執行緒池
    private ExecutorService executorService;
    //執行緒組
    private
AsynchronousChannelGroup threadGroup; //伺服器通道 public AsynchronousServerSocketChannel assc; public Server(int port){ try { //建立一個快取池 executorService = Executors.newCachedThreadPool(); //建立執行緒組 threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1
); //建立伺服器通道 assc = AsynchronousServerSocketChannel.open(threadGroup); //進行繫結 assc.bind(new InetSocketAddress(port)); System.out.println("server start , port : " + port); //進行阻塞 assc.accept(this, new ServerCompletionHandler()); //一直阻塞 不讓伺服器停止
Thread.sleep(Integer.MAX_VALUE); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(8765); } }

上面的程式碼都有註釋,學了NIO相信這個步驟你也有所瞭解,不瞭解NIO的可以看我的上一篇文章。

ServerCompletionHandler.java

public class ServerCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Server> {

    @Override
    public void completed(AsynchronousSocketChannel asc, Server attachment) {
        //當有下一個客戶端接入的時候 直接呼叫Server的accept方法,這樣反覆執行下去,保證多個客戶端都可以阻塞
        attachment.assc.accept(attachment, this);
        read(asc);
    }

    private void read(final AsynchronousSocketChannel asc) {
        //讀取資料
        ByteBuffer buf = ByteBuffer.allocate(1024);
        asc.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer resultSize, ByteBuffer attachment) {
                //進行讀取之後,重置標識位
                attachment.flip();
                //獲得讀取的位元組數
                System.out.println("Server -> " + "收到客戶端的資料長度為:" + resultSize);
                //獲取讀取的資料
                String resultData = new String(attachment.array()).trim();
                System.out.println("Server -> " + "收到客戶端的資料資訊為:" + resultData);
                String response = "伺服器響應, 收到了客戶端發來的資料: " + resultData;
                write(asc, response);
            }
            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                exc.printStackTrace();
            }
        });
    }

    private void write(AsynchronousSocketChannel asc, String response) {
        try {
            ByteBuffer buf = ByteBuffer.allocate(1024);
            buf.put(response.getBytes());
            buf.flip();
            asc.write(buf).get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void failed(Throwable exc, Server attachment) {
        exc.printStackTrace();
    }

}

這裡ServerCompletionHandler必須實現CompletionHandler介面

CompletionHandler有兩個方法,分別是:

1) public void completed(AsynchronousSocketChannel asc, Server attachment)

2) public void failed(Throwable exc, Server attachment)

      下面我們分別對這兩個介面的實現進行分析:首先看completed介面的實現,我們從attachment獲取成員變數AsynchronousServerSocketChannel,然後繼續呼叫它的accept方法。可能讀者在此可能會心存疑惑,既然已經接收客戶端成功了,為什麼還要再次呼叫accept方法呢?原因是這樣的:當我們呼叫AsynchronousServerSocketChannel的accept方法後,如果有新的客戶端連線接入,系統將回調我們傳入的CompletionHandler例項的completed方法,表示新的客戶端已經接入成功,因為一個AsynchronousServerSocketChannel可以接收成千上萬個客戶端,所以我們需要繼續呼叫它的accept方法,接收其它的客戶端連線,最終形成一個迴圈。每當接收一個客戶讀連線成功之後,再非同步接收新的客戶端連線。

Client.java

public class Client implements Runnable{

    //建立客戶端通道
    private AsynchronousSocketChannel asc ;

    public Client() throws Exception {
        //開啟通道
        asc = AsynchronousSocketChannel.open();
    }

    public void connect(){
        //連線通道
        asc.connect(new InetSocketAddress("127.0.0.1", 8765));
    }

    public void write(String request){
        try {
            //往服務端發內容
            asc.write(ByteBuffer.wrap(request.getBytes())).get();
            //讀取服務端的響應
            read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void read() {
        ByteBuffer buf = ByteBuffer.allocate(1024);
        try {
            asc.read(buf).get();
            buf.flip();
            byte[] respByte = new byte[buf.remaining()];
            buf.get(respByte);
            System.out.println(new String(respByte,"utf-8").trim());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //做個死迴圈讓這個程式不要停下來
        while(true){

        }
    }

    public static void main(String[] args) throws Exception {
        Client c1 = new Client();
        c1.connect();

        Client c2 = new Client();
        c2.connect();

        Client c3 = new Client();
        c3.connect();

        new Thread(c1, "c1").start();
        new Thread(c2, "c2").start();
        new Thread(c3, "c3").start();

        Thread.sleep(1000);

        c1.write("c1 aaa");
        c2.write("c2 bbbb");
        c3.write("c3 ccccc");
    }

}

服務端執行結果:

這裡寫圖片描述

客戶端執行結果:

這裡寫圖片描述

客戶端執行後服務端的響應結果:

這裡寫圖片描述

我們來看一下AsynchronousSocketChannel 底層實現了 讀寫操作 如下方法:

public abstract Future write(ByteBuffer src, long position);

public abstract Future read(ByteBuffer dst);

      我們發現AIO不需要像NIO程式設計那樣建立一個獨立的IO執行緒處理讀寫操作,對於AsynchronousServerSocketChannel和AsynchronousSocketChannel,它們都由JDK底層的執行緒池負責回撥並驅動讀寫操作。正因為如此,基於NIO2.0新的非同步非阻塞Channel進行程式設計比NIO程式設計更簡單。

BIO NIO AIO總結

      好!到這目前為止 BIO NIO AIO(NIO2.0)都說完了,來總結一下他們之間的區別:

BIO:同步阻塞

NIO:同步非阻塞

AIO(NIO2.0):非同步非阻塞

      IO (BIO)和NIO/NIO2.0的區別:其本質就是阻塞和非阻塞的區別

      阻塞概念:應用程式在獲取網路資料的時候,如果網路傳輸資料很慢,那麼程式就一直等著,直到傳輸完畢為止。

      非阻塞概念;應用程式直接可以獲取己經準備就緒好的資料,無需等待。

      同步和非同步:同步和非同步一般是面向作業系統與用程式對操作的層面上來區別的。

      同步時,應用程式會直接參與IO讀寫操作,並且我們的應用程式會直接阻塞到某一個方法上,直到資料準備就緒;或者採用輪詢的策略實時檢查資料的就緒狀態,如果就緒獲取資料。

      非同步時,則所有IO讀寫操作交給作業系統處理。與我們的應用程式沒有直接關係,我們程式不需要關心IO讀寫。當作系統完成了IO讀寫操作時,會給我們應用程式傳送通知,我們的應用程式直接拿走資料即可。

      同步說的是你的server伺服器端的執行方式

      阻塞說的是具體的技術,接收資料的方式、狀態(IO、NIO)

      NIO/NIO2.0自我感覺比起BIO好多了,但是 NIO/NIO2.0 API繁雜,用起來我個人感覺有點噁心,不易理解。開發出高質量的NIO/NIO2.0程式並不是一件簡單的事情,除去NIO固有的複雜性和BUG不談,作為一個NIO/NIO2.0服務端需要能夠處理網路的閃斷、客戶端的重複接入、客戶端的安全認證、訊息的編解碼、半包讀寫等等,如果你沒有足夠的NIO/NIO2.0程式設計經驗積累,一個NIO/NIO2.0框架的穩定往往需要半年甚至更長的時間。更為糟糕的是一旦在生產環境中發生問題,往往會導致跨節點的服務呼叫中斷,嚴重的可能會導致整個叢集環境都不可用,需要重啟伺服器,這種非正常停機會帶來巨大的損失。

不選擇JAVA原生NIO/NIO2.0程式設計的原因

1) NIO的類庫和API繁雜,使用麻煩,你需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;

2) 需要具備其它的額外技能做鋪墊,例如熟悉Java多執行緒程式設計,因為NIO程式設計涉及到Reactor模式,你必須對多執行緒和網路程式設計非常熟悉,才能編寫出高質量的NIO程式;

3) 可靠效能力補齊,工作量和難度都非常大。例如客戶端面臨斷連重連、網路閃斷、半包讀寫、失敗快取、網路擁塞和異常碼流的處理等等,NIO程式設計的特點是功能開發相對容易,但是可靠效能力補齊工作量和難度都非常大;

4) JDK NIO的BUG,例如臭名昭著的epoll bug,它會導致Selector空輪詢,最終導致CPU 100%。官方聲稱在JDK1.6版本的update18修復了該問題,但是直到JDK1.7版本該問題仍舊存在,只不過該bug發生概率降低了一些而已,它並沒有被根本解決。

      好!說到這相信大家也知道我的用心,我們可以使用NIO框架Netty來進行NIO程式設計,它既可以作為客戶端也可以作為服務端,同時支援UDP和非同步檔案傳輸,功能非常強大。

後面我會寫專門的文章來介紹Netty的使用。