一文搞定網絡編程中的BIO、NIO和AIO(從理論到代碼演示)

在學習網絡編程時,容易混淆NIO、BIO、AIO這幾個概念,同時對於阻塞和非阻塞、同步和異步的理解也較爲晦澀,本文將從最基礎的內核態/用戶態進行介紹,逐步講解在Java的IO編程中幾種不同IO操作方式及其具體實現。

1. Linux網絡IO模型介紹

在對linux網絡模型進行介紹之前,我們先來了解幾個概念,然後在對具體的模型進行介紹。

1.1 基本概念

在本節中主要介紹一下現有操作系統中的內核空間(kernel space)和用戶空間(user space)。

1.1.1 內核空間和用戶空間

對 32 位操作系統而言,它的尋址空間(虛擬地址空間,或叫線性地址空間)爲 4G(2的32次方)。也就是說一個進程的最大地址空間爲 4G。操作系統的核心是內核(kernel),它獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。爲了保證內核的安全,現在的操作系統一般都強制用戶進程不能直接操作內核。具體的實現方式基本都是由操作系統將虛擬地址空間劃分爲兩部分,一部分爲內核空間,另一部分爲用戶空間。針對 Linux 操作系統而言,最高的 1G 字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF)由內核使用,稱爲內核空間。而較低的 3G 字節(從虛擬地址 0x00000000 到 0xBFFFFFFF)由各個進程使用,稱爲用戶空間。

每個進程的 4G 地址空間中,最高 1G 都是一樣的,即內核空間。只有剩餘的 3G 才歸進程自己使用。

下圖描述了每個進程 4G 地址空間的分配情況(此圖來自互聯網):
在這裏插入圖片描述

爲什麼操作系統要劃分內核空間和用戶空間呢?

答:在 CPU 的所有指令中,有些指令是非常危險的,如果錯用,將導致系統崩潰,比如清內存、設置時鐘等。如果允許所有的程序都可以使用這些指令,那麼系統崩潰的概率將大大增加。所以,CPU 將指令分爲特權指令和非特權指令,對於那些危險的指令,只允許操作系統及其相關模塊使用,普通應用程序只能使用那些不會造成災難的指令。比如 Intel 的 CPU 將特權等級分爲 4 個級別:Ring0~Ring3。其實 Linux 系統只使用了 Ring0 和 Ring3 兩個運行級別(Windows 系統也是一樣的)。當進程運行在 Ring3 級別時被稱爲運行在用戶態,而運行在 Ring0 級別時被稱爲運行在內核態。

1.1.2 內核態和用戶態

在瞭解完內核空間和用戶空間之後,我們現在需要再瞭解一下什麼是內核態、用戶態。
簡單來說,當進程運行在內核空間時就處於內核態,而進程運行在用戶空間時則處於用戶態。

在內核態下,進程運行在內核地址空間中,此時 CPU 可以執行任何指令。運行的代碼也不受任何的限制,可以自由地訪問任何有效地址,也可以直接進行端口的訪問。
在用戶態下,進程運行在用戶地址空間中,被執行的代碼要受到 CPU 的諸多檢查,它們只能訪問映射其地址空間的頁表項中規定的在用戶態下可訪問頁面的虛擬地址,且只能對任務狀態段(TSS)中 I/O 許可位圖(I/O Permission Bitmap)中規定的可訪問端口進行直接訪問。

對於以前的 DOS 操作系統來說,是沒有內核空間、用戶空間以及內核態、用戶態這些概念的。可以認爲所有的代碼都是運行在內核態的,因而用戶編寫的應用程序代碼可以很容易的讓操作系統崩潰掉。對於 Linux 來說,通過區分內核空間和用戶空間的設計,隔離了操作系統代碼與應用程序代碼。即便是單個應用程序出現錯誤也不會影響到操作系統的穩定性,這樣其它的程序還可以正常的運行。

所以,區分內核空間和用戶空間本質上是要提高操作系統的穩定性及可用性。

1.1.3 如何從用戶空間到內核空間

其實所有的系統資源管理都是在內核空間中完成的。比如讀寫磁盤文件,分配回收內存,從網絡接口讀寫數據等等。我們的應用程序是無法直接進行這樣的操作的。但是我們可以通過內核提供的接口來完成這樣的任務。比如應用程序要讀取磁盤上的一個文件,它可以向內核發起一個 “系統調用” 告訴內核:“我要讀取磁盤上的某某文件”。其實就是通過一個特殊的指令讓進程從用戶態進入到內核態(到了內核空間),在內核空間中,CPU 可以執行任何的指令,當然也包括從磁盤上讀取數據。具體過程是先把數據讀取到內核空間中,然後再把數據拷貝到用戶空間並從內核態切換到用戶態。此時應用程序已經從系統調用中返回並且拿到了想要的數據,可以開開心心的往下執行了。

對於一個進程來講,從用戶空間進入內核空間並最終返回到用戶空間,這個過程是十分複雜的。舉個例子,比如我們經常接觸的概念 “堆棧”,其實進程在內核態和用戶態各有一個堆棧。運行在用戶空間時進程使用的是用戶空間中的堆棧,而運行在內核空間時,進程使用的是內核空間中的堆棧。所以說,Linux 中每個進程有兩個棧,分別用於用戶態和內核態。

下圖簡明的描述了用戶態與內核態之間的轉換:
在這裏插入圖片描述
用戶態的進程必須切換成內核態才能使用系統的資源,通常情況下有三種方式從用戶態進入到內核態,分別是系統調用、軟中斷和硬件中斷

1.2. Linux網絡模型

Linux的內核將所有的外部設備都看做一個文件來操作,對一個文件的讀寫操作會帶哦用內核提供的系統命令,返回一個文件描述符fd。而對於一個scoket的讀寫也會有相應的描述符,描述符本質上是一個數字,指向內核中的一個結構體(文件路徑、數據區等一些屬性)。

Linux提供了5種不同的I/O模型,在這裏我們僅僅介紹其中的4種(其餘大家可以自行百度),具體如下:
(1)阻塞I/O模型:這是一種最簡單也是最常見的I/O模型,在這種情況下,所有的文件操作都是阻塞的。如圖1所示,在進程空間中調用recvfrom,其系統調用直到數據包到達且被複制到應用進程(用戶空間)的緩衝區或者發生錯誤才返回,在此期間會一直等待,因此被稱爲阻塞I/O模型。
圖1 阻塞I/O模型

(2)非阻塞I/O模型:在該模型下,recvfrom從應用層(用戶空間)到內核的時候,如果該緩衝區沒有數據的話,就直接返回一個EWOULDBLOCK錯誤,然後回進行輪詢檢查這個狀態,看內核是不是有數據到來,如圖2所示。
圖2 非阻塞I/O模型
(3)I/O複用模型:Linux提供select/poll,進程通過將一個或者多個fd傳遞給select或者poll系統調用,阻塞在select操作上,這樣select/poll可以幫助我們偵測多個fd是否處於就緒狀態。當fd就緒時,立即回調函數rollback,如圖3所示。
圖3 I/O複用模型

(4)異步I/O:該模式下,進程回告訴內核啓動每個操作,內核操作完成之後回通知應用程序,如圖4所示。
圖4 異步I/O

2. Java的I/O模型

本節中主要介紹,Java支持的幾種I/O模型,結合具體示例代碼進行講解。

2.1 傳統的BIO模型

2.1.1. BIO模型介紹

BIO (Blocking I/O):同步阻塞I/O模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。這裏使用那個經典的燒開水例子,這裏假設一個燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,纔去處理下一個水壺。但是實際上線程在等待水壺燒開的時間段什麼都沒有做。

BIO的服務端通信模型簡單來說就是:採用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後爲每個客戶端創建一個新的線程進行鏈路處理沒處理完成後,通過輸出流返回應答給客戶端,線程銷燬。即典型的一請求一應答通宵模型。

傳統的BIO模型圖如下圖所示(來源於網絡):
在這裏插入圖片描述
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的線程個數和客戶端併發訪問數呈1:1的正比關係,Java中的線程也是比較寶貴的系統資源,線程數量快速膨脹後,系統的性能將急劇下降,隨着訪問量的繼續增大,系統最終就死-掉-了

2.1.2 BIO模型代碼

本節中分別介紹BIO模型的服務端和客戶端的具體代碼。首先介紹服務端。

2.1.2.1 BIO模式服務端

代碼如下:

package bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * created by LMR on 2020/5/19
 */
public class TimeServer {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        ServerSocket server = null;
        try {
            server = new ServerSocket(port);
            System.out.println("The time server is start in port : " + port);
            Socket socket = null;
            while (true){
                socket = server.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }

        }finally {
            if (server != null){
                System.out.println("The time server close");
                server.close();
                server = null;
            }
        }
    }
}

TimeServer設置監聽端口,默認值爲8080,如果端口沒有被佔用,創建ServerSocket實例,服務器端監聽成功。然後通過一個無限循環來監聽客戶端的連接,如果沒有客戶端接入,則主線程一直等待,阻塞在ServerSocket的accept操作上。當有新的客戶端接入時,則創建一個線程來處理當前連接,在創建線程時傳入TimeServerHandler對象,來處理事件,下面給出TimeServerHandler類的代碼:

package bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * created by LMR on 2020/5/19
 */
public class TimeServerHandler implements Runnable{

    private Socket socket;

    public TimeServerHandler(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {

        BufferedReader in = null;
        PrintWriter out = null;

        try {
            in = new BufferedReader(new InputStreamReader(
                    this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(), true);
            String currentTime = null;
            String body = null;
            while (true){
                body = in.readLine();
                if (body == null){
                    break;
                }
                System.out.println("The time server receive order : " + body);
                currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                        new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
                out.println(currentTime);
            }
        }catch (IOException e){
                if (in != null){
                    try {
                        in.close();
                    }catch (IOException e1){
                        e1.printStackTrace();
                    }

                }
                if (out != null){
                    out.close();
                    out = null;
                }
                if (socket != null){
                    try {
                        socket.close();
                    }catch (IOException e1){
                        e1.printStackTrace();
                    }
                    socket = null;
                }
        }
    }
}

代碼中通過BufferedReader讀物一行,如果已經讀取到了輸入流的末尾,則返回值爲null,退出循環,如果是非空值,則對該值進行判斷,如果消息爲請求查詢時間的指令,則通過PrintWriter的println函數發送消息給客戶端,最後退出循環,並釋放資源。

2.1.2.2. BIO模式客戶端

下面介紹客戶端代碼,具體如下:

package bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * created by LMR on 2020/5/19
 */
public class TimeClient {

    public static void main(String[] args) {
        int port = 8080;
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;

        try {
            socket = new Socket("127.0.0.1", port);
            in = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            out.println("QUERY TIME ORDER");
            System.out.println("Send order 2 server succeed");
            String line = in.readLine();
            System.out.println("Now is : " + line);
        }catch (IOException e){

        }finally {
            if (out != null){
                out.close();
                out = null;
            }

            if (in != null){
                try {
                    in.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
                in = null;
            }
            if (socket != null){
                try {
                    socket.close();
                }catch (IOException e1){
                    e1.printStackTrace();
                }
                socket = null;
            }
        }
    }
}

客戶端的代碼與服務端的代碼類似,但更爲簡單。在這裏就不再進行解釋,我們運行代碼,首先啓動服務端,然後再啓動客戶端,運行結果如下圖所示:
服務端:一直等待客戶端連接
在這裏插入圖片描述
客戶端:處理完之後就退出
在這裏插入圖片描述

2.2 NIO模型

2.2.1 NIO模型介紹

NIO (New I/O):同時支持阻塞與非阻塞模式,但這裏我們以其同步非阻塞I/O模式來說明,那麼什麼叫做同步非阻塞?如果還拿燒開水來說,NIO的做法是叫一個線程不斷的輪詢每個水壺的狀態,看看是否有水壺的狀態發生了改變,從而進行下一步的操作。

下面我們對NIO模型中涉及到的一些基本概念進行介紹。
(1)緩衝區 Buffer
Buffer是一個對象,包含一些要寫入或者讀出的數據。在NIO庫中,所有數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的;在寫入數據時,也是寫入到緩衝區中。任何時候訪問NIO中的數據,都是通過緩衝區進行操作。緩衝區實際上是一個數組,並提供了對數據結構化訪問以及維護讀寫位置等信息。
具體的緩存區有這些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他們實現了相同的接口:Buffer。
(2)通道 Channel
我們對數據的讀取和寫入要通過Channel,它就像水管一樣,是一個通道。通道不同於流的地方就是通道是雙向的,可以用於讀、寫和同時讀寫操作。底層的操作系統的通道一般都是全雙工的,所以全雙工的Channel比流能更好的映射底層操作系統的API。

Channel主要分兩大類:
SelectableChannel:用戶網絡讀寫
FileChannel:用於文件操作

後面代碼會涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子類。
(3)多路複用器 Selector
Selector是Java NIO 編程的基礎。Selector提供選擇已經就緒的任務的能力:Selector會不斷輪詢註冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。一個Selector可以同時輪詢多個Channel,因爲JDK使用了epoll()代替傳統的select實現,所以沒有最大連接句柄1024/2048的限制。所以,只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。

2.2.2. NIO模型代碼

2.2.2.1 NIO模型服務端

再給出NIO模型服務端代碼之前先給出NIO模型服務端的通信序列圖(來源於《Netty權威指南》)
在這裏插入圖片描述
NIO模型的服務端的主要步驟如下:

  1. 打開ServerSocketChannel,用於監聽客戶端的連接,它是所有客戶端連接的父管道;
  2. 綁定監聽端口,設置連接爲非阻塞模式;
  3. 創建Reactor線程,創建多路複用器並啓動線程;
  4. 將ServerSocketChannel註冊到Reactor線程的多路複用器Selector上,監聽ACCEPT事件;
  5. 多路複用器在線程run方法的無限循環體內輪詢準備就緒的Key;
  6. 多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP三次握手,建立物理鏈路;
  7. 設置客戶端鏈路爲非阻塞模式;
  8. 將新接入的客戶端連接註冊到Reactor線程的多路複用器上,監聽讀操作,用於讀取客戶端發送的網絡消息;
  9. 異步讀取客戶端請求消息到緩衝區;
  10. 對Buffer編解碼,處理半包消息,將解碼成功的消息封裝成Task;
  11. 將應答消息編碼爲Buffer,調用SocketChannel的write將消息異步發送給客戶端。

下面給出2.1節中相同功能的時間服務器的NIO實現,具體實現步驟可能與上述描述的不同。

package nio;

/**
 * created by LMR on 2020/5/19
 */
public class TimeServer {

    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0){
            try {
                port = Integer.valueOf(args[0]);

            } catch (NumberFormatException e){

            }
        }
        MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
        new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
    }
}

服務端啓動類與前面的基本相同,不同在於在NIO實現中我們創建了一個MultiplexerTimeServer類來輪詢多路複用器selector,可以處理多個客戶端的併發接入,下面看該類的具體代碼。

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * created by LMR on 2020/5/19
 */
public class MultiplexerTimeServer implements Runnable {

    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private volatile boolean stop;

    public MultiplexerTimeServer(int port){
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("The time server is start in port : " + port);
        }catch (IOException e){
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop(){
        this.stop = true;

    }

    @Override
    public void run() {

        while (!stop){
            try {
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()){
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    }catch (IOException e){
                        if (key != null){
                            key.cancel();;
                            if (key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Throwable t){
                t.printStackTrace();
            }
        }
        if (selector != null){
            try {
                selector.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException{
        if (key.isValid()){
            //處理新接入的請求消息
            if (key.isAcceptable()){
                //接受新的連接
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                //添加新的連接到selector
                sc.register(selector, SelectionKey.OP_READ);
            }
            if (key.isReadable()){
                //讀數據
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0){
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("The time server receive order : " + body);
                    String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
                            System.currentTimeMillis()).toString() : "BAD ORDER";
                    doWrite(sc, currentTime);
                }else if (readBytes < 0){
                    //關閉鏈路
                    key.cancel();
                    sc.close();
                }else
                {
                    ;
                }
            }
        }


    }

    private void doWrite(SocketChannel channel, String response) throws IOException{
        if (response != null && response.trim().length() > 0){
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }
}

代碼解析後續補充。。。。。

2.2.2.2 NIO模型客戶端

同樣在這裏首先給出NIO模型客戶端的通信序列圖(來源於《Netty權威指南》):
在這裏插入圖片描述
客戶端代碼:

package nio;

/**
 * created by LMR on 2020/5/19
 */
public class TimeClient {

    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0){
            port = Integer.valueOf(args[0]);
        }
        new Thread(new TimeClientHandle("127.0.0.1", port), "TimeCLient-001").start();

    }
}

客戶端處理類TimeClientHandle代碼:

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * created by LMR on 2020/5/19
 */
public class TimeClientHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;


    public TimeClientHandle(String host, int port){
        this.host = host == null ? "127.0.0.1" : host;
        this.port = port;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        }catch (IOException e){
            e.printStackTrace();System.exit(1);
        }


    }
    @Override
    public void run() {
        try {
            doConnect();
        }catch (IOException e){
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop){
            try {
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()){
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    }catch (Exception e){
                        if (key != null){
                            key.cancel();
                            if (key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
                System.exit(1);
            }
        }
        //多路複用器關閉後,所有註冊在上面的Channel和Pipe等資源都會被自動去註冊並關閉,所以不需要重複示方資源
        if (selector != null){
            try {
                selector.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException{
        if (key.isValid()){
            //判斷是否連接成功
            SocketChannel sc = (SocketChannel) key.channel();
            if (key.isConnectable()){
                if (sc.finishConnect()){
                    sc.register(selector, SelectionKey.OP_READ);
                    doWrite(sc);
                }else {
                    System.exit(1);
                }
            }
            if (key.isReadable()){
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0){
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("Now is : " + body);
                    this.stop = true;
                } else if (readBytes < 0){
                    //關閉鏈路
                    key.cancel();
                    sc.close();
                } else {
                    ;
                }
            }
        }
    }


    private void doConnect() throws IOException{
        //如果直接連接成功,則註冊到多路複用器上,發送請求消息,讀取應答數據
        if (socketChannel.connect(new InetSocketAddress(host, port))){
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("**************");
            doWrite(socketChannel);
        }else {
            System.out.println("------------");
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }
    private void doWrite(SocketChannel sc) throws IOException{
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        sc.write(writeBuffer);
        if (!writeBuffer.hasRemaining()){
            System.out.println("Send order 2 server succeed");
        }
    }
}

運行結果如下:
服務端:

在這裏插入圖片描述

客戶端:
在這裏插入圖片描述

2.3 AIO模型

2.3.1 AIO模型介紹

AIO ( Asynchronous I/O):異步非阻塞I/O模型。異步非阻塞與同步非阻塞的區別在哪裏?異步非阻塞無需一個線程去輪詢所有IO操作的狀態改變,在相應的狀態改變後,系統會通知對應的線程來處理。對應到燒開水中就是,爲每個水壺上面裝了一個開關,水燒開之後,水壺會自動通知我水燒開了。

2.3.2 AIO模型代碼

2.3.2.1 AIO模型服務端代碼

package aio;

/**
 * created by LMR on 2020/5/19
 */
public class TimeServer {
    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0){
            try {
                port = Integer.valueOf(args[0]);

            } catch (NumberFormatException e){

            }
        }

        new Thread(new AsyncTimeServerHandler(port), "AIO-MAsyncTimeServerHandler-001").start();
    }
}

AIO模型服務端處理類AsyncTimeServerHandler代碼如下:

package aio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.CountDownLatch;

/**
 * created by LMR on 2020/5/19
 */
public class AsyncTimeServerHandler implements Runnable {

    private int port;

    CountDownLatch latch;
    AsynchronousServerSocketChannel asynchronousServerSocketChannel;

    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() {
        latch = new CountDownLatch(1);
        doAccept();
        try {
            latch.await();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public void doAccept(){
        asynchronousServerSocketChannel.accept(this, new AcceptCompletionHandler());
    }
}

在收到客戶端發來請求時,服務端進行相應的操作,具體實現類爲AcceptCompletionHandler,代碼如下:

package aio;

import java.nio.ByteBuffer;

import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * created by LMR on 2020/5/19
 */
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {



    @Override
    public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment) {

        attachment.asynchronousServerSocketChannel.accept(attachment,this);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        result.read(buffer, buffer, new ReadCompletionHandler(result));

    }


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

ReadCompletionHandler代碼如下:

package aio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * created by LMR on 2020/5/19
 */
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {

  private AsynchronousSocketChannel channel;

  ReadCompletionHandler(AsynchronousSocketChannel channel){
      if (this.channel == null)
         this.channel = channel;
  }
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        attachment.flip();
        byte[] body = new byte[attachment.remaining()];
        attachment.get(body);
        try {
            String req = new String(body, "UTF-8");
            System.out.println("The time server receive order : " + req);
            String curTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new java.util.Date(
                    System.currentTimeMillis()
            ).toString() : "BAD ORDER";
            doWrite(curTime);
        }catch (UnsupportedEncodingException e){
            e.printStackTrace();
        }
    }
    private void  doWrite(String currentTime){
      if (currentTime != null && currentTime.trim().length() > 0){
          byte[] bytes = (currentTime).getBytes();
          ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
          writeBuffer.put(bytes);
          writeBuffer.flip();
          channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
              @Override
              public void completed(Integer result, ByteBuffer attachment) {
                  //如果沒有發送,繼續發送
                  if (attachment.hasRemaining()){
                      channel.write(writeBuffer, writeBuffer, this);
                  }
              }

              @Override
              public void failed(Throwable exc, ByteBuffer attachment) {
                    try {
                        channel.close();
                    }catch (IOException e){

                    }
              }
          });
      }
    }


    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
            try {
                this.channel.close();
            }catch (IOException e){

            }
    }
}

2.3.2.2 AIO模型客戶端代碼

package aio;

/**
 * created by LMR on 2020/5/19
 */
public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {

            }
        }
        new Thread(new AsyncTimeClientHandler("127.0.0.1", port), "AIO-AsyncTimeClientHandler-001").start();
    }
}

AIO模型客戶端處理類AsyncTimeClientHandler代碼:

package aio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;

/**
 * created by LMR on 2020/5/19
 */
public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {

    private AsynchronousSocketChannel client;
    private String host;
    private int port;

    private CountDownLatch latch;

    public AsyncTimeClientHandler(String host, int port)
    {
        this.host = host;
        this.port = port;
        try {
            client = AsynchronousSocketChannel.open();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //防止異步操作沒有執行完成線程就退出
        latch = new CountDownLatch(1);
        client.connect(new InetSocketAddress(host, port), this, this);
        try {
            latch.await();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        try {
            client.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    @Override
    public void completed(Void result, AsyncTimeClientHandler attachment) {
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        //異步寫
        client.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                if (attachment.hasRemaining()){
                    client.write(writeBuffer,writeBuffer,this);
                }else {
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    //異步讀取服務端數據
                    client.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            buffer.flip();
                            byte[] bytes = new byte[buffer.remaining()];
                            buffer.get(bytes);
                            String body;
                            try {
                                body = new String(bytes, "UTF-8");
                                System.out.println("Now is : " + body);
                                latch.countDown();
                            }catch (UnsupportedEncodingException e){
                                e.printStackTrace();
                            }
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer attachment) {
                            try {
                                client.close();
                                latch.countDown();
                            }catch (IOException e){

                            }
                        }
                    });
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {

                try {
                    client.close();
                    latch.countDown();
                }catch (IOException e){

                }
            }
        });

    }


    @Override
    public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
        exc.printStackTrace();
        try {
            client.close();
            latch.countDown();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

運行截圖:
服務端:
在這裏插入圖片描述
客戶端:
在這裏插入圖片描述

3. 幾種I/O模型的對比

此圖來源於網絡:
在這裏插入圖片描述

寫在後面的話:
由於寫博客的時間比較倉促,文中很多代碼沒有註釋,一些代碼也沒有介紹流程,在後續會逐漸補上。文中的代碼均是參考《netty權威指南》實現,一句一句自己巧的,全文當作自己學習的一個筆記,如有侵權還希望聯繫我刪除。

參考博客和書籍:
https://www.cnblogs.com/blackjoyful/p/11534985.html
https://www.jb51.net/article/131810.htm
https://www.cnblogs.com/sparkdev/p/8410350.html
《Netty權威指南》

如果喜歡的話希望點贊收藏,關注我,將不間斷更新博客。

希望熱愛技術的小夥伴私聊,一起學習進步

來自於熱愛編程的小白

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