Java的BIO、NIO、AIO

NIO,BIOjava

1、序言編程

  在Java的軟件設計開發中,通訊架構是不可避免的,咱們在進行不一樣系統或者不一樣進程之間的數據交互,或者在高併發下的通訊場景都須要用到網絡通訊相關的技術。數組

  一、通訊技術總體解決的問題:服務器

  1)局域網內的通訊要求;網絡

  2)多系統間的底層消息傳遞機制;架構

  3)高併發下,大數據量的通訊場景須要;併發

  4)遊戲行業。異步

  二、在Java中,主要有三種IO模型,分別是:socket

  1)同步阻塞IO(BIO);分佈式

  2)同步非阻塞IO(NIO);

  3)異步IO(AIO)。

2、同步阻塞IO(BIO)

  Java BIO(blocking I/O):就是傳統的java io編程,其相關的類和接口在java.io包下。同步並阻塞,服務器實現模式爲一個鏈接一個線程,即每當客戶端有鏈接請求時,服務端都須要啓動一個線程進行處理,以下圖。

  BIO模型

 

  在高併發的狀況下,服務端會產生大量線程,線程間會發生競爭和上下文切換,同時要佔用棧空間和CPU資源,並且其中有些線程可能什麼事情都不會作,一直阻塞着,這些狀況都會形成服務端性能降低。

  因此BIO方式適合用於鏈接數目固定,並且比較小的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,可是程序簡單易懂。

  一、Java中的BIO分佈式分爲兩種:

  1)傳統BIO:即上圖中的一請求一應答;

  2)僞異步IO:經過線程池固定線程的最大數量,能夠防止資源的浪費。

  二、BIO編程簡單流程:

  1)服務器啓動一個ServerSocket;

  2)客戶端啓動Socket請求與服務器鏈接,默認狀況下服務器端須要對每一個客戶創建一個線程與之通訊;

  3)客戶端發出請求以後,先諮詢服務器是否有線程響應,若是沒有則會等待,或者被服務端拒絕;

  4)若是有響應,客戶端線程會等待請求結束後,再繼續執行。

  三、使用BIO進行通訊的簡單案例

  1)服務端代碼

 

public class Server {    public static void main(String[] args) throws IOException {        //服務器端開啓一個ServerSocket,並綁定6666端口
        ServerSocket ss = new ServerSocket(6666);
        System.out.println("服務器已開啓!");        while (true){
            Socket socket = ss.accept();
            System.out.println("來自" + socket.getRemoteSocketAddress() + "的鏈接");            new Handler(socket).start();
        }
    }
}//爲每一個客戶端鏈接開啓的線程class Handler extends Thread {
    Socket socket;    public Handler(Socket socket) {        this.socket = socket;
    }

    @Override    public void run() {        try (InputStream inputStream = socket.getInputStream()) {            try (OutputStream outputStream = socket.getOutputStream()) {
                handle(inputStream, outputStream);
            }
        } catch (IOException e) {            try{                //關閉socket                socket.close();
            }catch (IOException e1){

            }
        }
        System.out.println("客戶端" + socket.getRemoteSocketAddress()+ "斷開鏈接");
    }    private void handle(InputStream inputStream, OutputStream outputStream) throws IOException{        //得到一個字符輸入流
        var reader = new BufferedReader(new InputStreamReader(inputStream,
                StandardCharsets.UTF_8));        //得到一個字符輸出流
        var writer = new BufferedWriter(new OutputStreamWriter(outputStream,
                StandardCharsets.UTF_8));
        writer.write("鏈接成功!\n");
        writer.flush();        while(true){            //每次從管道中讀入一行
            String str = reader.readLine();            //當客戶端傳來"Bye"表明斷開鏈接
            if("Bye".equals(str)){
                writer.write("Bye\n");
                writer.flush();                break;
            }
            writer.write("已經收到:" + str + "\n");
            writer.flush();
        }
    }
}

 

 

  2)客戶端代碼

public class Client {    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 6666);        try (InputStream inputStream = socket.getInputStream()){            try (OutputStream outputStream = socket.getOutputStream()){
                handle(inputStream, outputStream);
            }
        }catch (IOException e){            try{
                socket.close();
            }catch (IOException e1){

            }
        }

    }    private static void handle(InputStream inputStream, OutputStream outputStream) throws IOException{
        var reader = new BufferedReader(new InputStreamReader(inputStream,
                StandardCharsets.UTF_8));
        var writer = new BufferedWriter(new OutputStreamWriter(outputStream,
                StandardCharsets.UTF_8));
        Scanner in = new Scanner(System.in);
        System.out.println("<<<" + reader.readLine());        while (true){
            System.out.print(">>>");
            String str = in.nextLine();
            writer.write(str);
            writer.newLine();
            writer.flush();
            String resp = reader.readLine();
            System.out.println("<<<" + resp);            if("Bye".equals(str)){                break;
            }
        }
    }
}

  3)啓動服務器端,再啓動客戶端,使用客戶端與服務端進行通訊

   Server的控制檯:

服務器已開啓!
來自/127.0.0.1:51871的鏈接
客戶端/127.0.0.1:51871斷開鏈接

    Client的控制檯:

<<<鏈接成功!>>>你好<<<已經收到:你好>>>在嗎<<<已經收到:在嗎>>>吃了嗎<<<已經收到:吃了嗎>>>Bye<<<Bye

 

 3、同步非阻塞IO(NIO)

  Java NIO(New IO):也稱java non-blocking IO,是從java1.4版本開始引入的一個新IO API,能夠代替標準的java IO API。NIO與原來的IO具備一樣的做用和目的,可是使用的方式徹底不一樣,NIO支持面向緩衝區的、基於通道的IO操做。NIO將以更加高效的方式進行文件的讀寫操做。NIO能夠理解爲非阻塞IO,傳統IO的read和write只能阻塞執行,線程在讀寫IO期間不能幹其餘事。而NIO則能夠配置socket爲非阻塞模式。

  Java NIO的阻塞模式,使一個線程從某通道發送請求或者讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可用,則什麼都不會獲取,而不是保持線程阻塞,因此直至數據變得可用讀取以前,該線程能夠繼續作其餘事情。

   工做示意圖以下:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

  一、BIO 和 NIO 的區別?

  1)BIO 以流的方式處理數據,而 NIO 以塊的方式處理數據;

  2)BIO 是阻塞的, NIO 是非阻塞的

  3)BIO 基於字節流和字符流進行操做,而 NIO 基於 Channel(通道)和 Buffer(緩衝區)進行操做,數據老是從通道讀取到緩衝區,或者從緩衝區寫入到通道中。

  二、NIO 的三大組件

  1)Buffer 緩衝區:緩衝區本質是一塊能夠寫入數據,而後能夠從中讀取數據的內存。這塊內存被包裝成了 NIO Buffer 對象,並提供了一組方法,用來方便對該快的訪問。

   Buffer 就像一個數組,能夠保存多個相同類型的數據。根據數據類型不一樣,有如下經常使用的 Buffer 子類:ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer。

  建立方法以下:

static XxxBuffer allocate(int capacity) : 建立一個容量爲capacity 的 XxxBuffer 對象

   基本屬性:

  •   容量(capacity):做爲一個內存塊,Buffer具備必定的固定大小,也稱爲「容量」,緩衝區不能爲負,而且建立後不能更改。
  •   限制(limit):表示緩衝區中能夠操做的數據的大小(位於limit後的數據不能進行讀寫)。緩衝區的Buffer也不能爲負數,而且不能大於其容量。在寫入模式下,limit大於capacity。在讀取模式下,limit等於寫入的數據量。
  •   位置(position):下一個要讀取或者寫入的數據的索引。緩衝區的位置不能爲負,而且不能大於limit。
  •   標記(mark)與重置(reset):標記是一個索引,經過Buffer中獲得mark()方法指定Buffer中的一個特定的position,以後能夠調用reset()方法恢復到這個position。

   watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

     常見方法:

Buffer clear() 清空緩衝區並返回對緩衝區的引用
Buffer flip() 爲 將緩衝區的界限設置爲當前位置,並將當前位置重置爲 0int capacity() 返回 Buffer 的 capacity 大小boolean hasRemaining() 判斷緩衝區中是否還有元素int limit() 返回 Buffer 的界限(limit) 的位置
Buffer limit(int n) 將設置緩衝區界限爲 n, 並返回一個具備新 limit 的緩衝區對象
Buffer mark() 對緩衝區設置標記int position() 返回緩衝區的當前位置 position
Buffer position(int n) 將設置緩衝區的當前位置爲 n , 並返回修改後的 Buffer 對象int remaining() 返回 position 和 limit 之間的元素個數
Buffer reset() 將位置 position 轉到之前設置的 mark 所在的位置
Buffer rewind() 將位置設爲爲 0, 取消設置的 mark

    讀取操做:

Buffer 全部子類提供了兩個用於數據操做的方法:get()put() 方法
取獲取 Buffer中的數據
get() :讀取單個字節
get(byte[] dst):批量讀取多個字節到 dst 中
get(int index):讀取指定索引位置的字節(不會移動 position)
    
放到 入數據到 Buffer 中 中
put(byte b):將給定單個字節寫入緩衝區的當前位置
put(byte[] src):將 src 中的字節寫入緩衝區的當前位置
put(int index, byte b):將指定字節寫入緩衝區的索引位置(不會移動 position)

  2)通道(channel)

  通道(Channel):由java.nio.channels包下定義的,表示IO源與目標打開的鏈接。Channel相似於傳統的「流」。只不過Channel自己不能直接訪問數據,Channel只能與Buffer進行交互。

  NIO通道與流的區別:

  •   通道能夠同時進行讀寫,而流只能讀或者只能寫。
  •   通道能夠實現異步讀取數據。
  •   通道能夠從緩衝中讀數據,也能夠寫數據到緩衝。

  Channel在NIO中是一個接口,經常使用的Channel實現類有:

  •   FileChannel:用於讀取、寫入、映射和操做文件的通道。
  •   DatagramChannel:經過UDP讀寫網絡中的數據通道。
  •   SocketChannel:經過TCP讀取網絡中的數據。
  •   ServerSocketChannel:能夠監聽新進來的TCP鏈接,對每一個新進來的鏈接都會建立一個SocketChannel。

  3)選擇器(Selector)

  選擇器(Selector)是SelectableChannle對象的多路複用器,Selector能夠同時監控多個SelectorableChannel的IO情況,利用Selector可使一個單獨的線程管理多個Channel。Selector是NIO非阻塞的核心,其能檢測多個註冊的通道上是否有事件發生,若是有事件發生,便獲取事件,而後針對每一個事件做出相應的處理。

  經過Selector.open()方法建立一個Selector:

 

Selector selector = Selector.open();

   4)入門案例

 

/**
  客戶端 */public class Client {    public static void main(String[] args) throws Exception {        //1. 獲取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));        //2. 切換非阻塞模式
        sChannel.configureBlocking(false);        //3. 分配指定大小的緩衝區
        ByteBuffer buf = ByteBuffer.allocate(1024);        //4. 發送數據給服務端
        Scanner scan = new Scanner(System.in);        while(scan.hasNext()){
            String str = scan.nextLine();
            buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())                    + "\n" + str).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }        //5. 關閉通道        sChannel.close();
    }
}/**
 服務端 */public class Server {    public static void main(String[] args) throws IOException {        //1. 獲取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();        //2. 切換非阻塞模式
        ssChannel.configureBlocking(false);        //3. 綁定鏈接
        ssChannel.bind(new InetSocketAddress(9999));        //4. 獲取選擇器
        Selector selector = Selector.open();        //5. 將通道註冊到選擇器上, 而且指定「監聽接收事件」        ssChannel.register(selector, SelectionKey.OP_ACCEPT);        //6. 輪詢式的獲取選擇器上已經「準備就緒」的事件
        while (selector.select() > 0) {
            System.out.println("輪一輪");            //7. 獲取當前選擇器中全部註冊的「選擇鍵(已就緒的監聽事件)」
            Iteratorit = selector.selectedKeys().iterator();            while (it.hasNext()) {                //8. 獲取準備「就緒」的是事件
                SelectionKey sk = it.next();                //9. 判斷具體是什麼事件準備就緒
                if (sk.isAcceptable()) {                    //10. 若「接收就緒」,獲取客戶端鏈接
                    SocketChannel sChannel = ssChannel.accept();                    //11. 切換非阻塞模式
                    sChannel.configureBlocking(false);                    //12. 將該通道註冊到選擇器上                    sChannel.register(selector, SelectionKey.OP_READ);
                } else if (sk.isReadable()) {                    //13. 獲取當前選擇器上「讀就緒」狀態的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();                    //14. 讀取數據
                    ByteBuffer buf = ByteBuffer.allocate(1024);                    int len = 0;                    while ((len = sChannel.read(buf)) > 0) {
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }                //15. 取消選擇鍵 SelectionKey                it.remove();
            }
        }
    }
}
相關文章
相關標籤/搜索