帶你完全掌握Java NIO(總結分享)

2022-03-31 13:00:46
本篇文章給大家帶來了關於的相關知識,其中主要介紹了NIO的相關問題,包括了NIO核心、BIO與NIO比較、通過NIO實現簡單的伺服器端使用者端通訊,希望對大家有幫助。

推薦學習:《》

一、Java思維導圖

在這裡插入圖片描述

二、I/O模型

I/O模型的本質是用什麼樣的通道進行資料的傳送和接收,很大程度上決定了程式通訊的效能。
Java共支援三種網路程式設計模型:BIO、NIO、AIO

  • BIO:同步並阻塞,服務實現模式為一個連線一個執行緒,即使用者端有一個連線請求時,伺服器端就需要啟動一個執行緒進行處理。

  • NIO: 同步非阻塞,伺服器實現模式為一個執行緒處理多個請求連線,即使用者端傳送的請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求就進行處理。

  • AIO:非同步非阻塞,AIO引入非同步通道的概念,採用了Proactor模式,簡化了程式編寫,有效的請求才啟動執行緒,它的特點是先由作業系統完成後才通知伺服器端。

三、BIO、NIO、AIO應用場景

  • BIO方式適用於連線數目比較小且固定的架構,這種方式對伺服器資源要求比較高, 並行侷限於應用中,JDK1.4以前的唯一選擇,但程式簡單易理解。

  • NIO方式適用於連線數目多且連線比較短(輕操作)的架構,比如聊天伺服器,彈幕 系統,伺服器間通訊等。程式設計比較複雜,JDK1.4開始支援。

  • AIO方式使用於連線數目多且連線比較長(重操作)的架構,比如相簿伺服器,充分 呼叫OS參與並行操作,程式設計比較複雜,JDK7開始支援

四、BIO程式設計簡單流程

  • 伺服器端啟動一個ServerSocket;

  • 使用者端啟動Socket對伺服器進行通 信,預設情況下伺服器端需要對每 個客戶 建立一個執行緒與之通訊;

  • 使用者端發出請求後, 先諮詢伺服器 是否有執行緒響應,如果沒有則會等 待,或者被拒絕;

  • 如果有響應,使用者端執行緒會等待請 求結束後,在繼續執行;

五、NIO核心

NIO 有三大核心部分:Selector(選擇器)、Channel(通道)、Buffer(緩衝區)。
NIO是面向緩衝區,或者說面向塊程式設計,資料讀取到一個 它稍後處理的緩衝區,需要時可在緩衝區中前後移動,這就 增加了處理過程中的靈活性,使用它可以提供非阻塞式的高伸縮性網路。
HTTP2.0使用了多路複用的技術,做到同一個連線並行處理多個請求,而且並行請求 的數量比HTTP1.1大了好幾個數量級。
簡而言之,NIO可以一個執行緒處理多個請求。

六、BIO與NIO比較

  • BIO 以流的方式處理資料,而 NIO 以塊的方式處理資料,塊 I/O 的效率比流 I/O 高很多;

  • BIO 是阻塞的,NIO 則是非阻塞的;

  • BIO基於位元組流和字元流進行操作,而 NIO 基於 Channel(通道)和 Buffer(緩衝區)進 行操作,資料總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇器)用於監聽多個通道的事件(比如:連線請求,資料到達等),因 此使用單個執行緒就可以監聽多個使用者端通道。

七、NIO 三大核心原理示意圖

在這裡插入圖片描述
流程圖說明:

  • Selector 對應一個執行緒, 一個執行緒對應多個channel(連線);

  • 該圖反應了有三個channel 註冊到 該selector //程式;

  • 每個channel 都會對應一個Buffer;

  • 程式切換到哪個channel 是有事件決定的, Event 就是一個重要的概念;

  • Selector 會根據不同的事件,在各個通道上切換;

  • Buffer 就是一個記憶體塊 , 底層是有一個陣列;

  • 資料的讀取寫入是通過Buffer, 這個和BIO , BIO 中要麼是輸入流,或者是 輸出流, 不能雙向,但是NIO的Buffer 是可以讀也可以寫, 需要 flip 方法切換;

  • channel 是雙向的, 可以返回底層作業系統的情況, 比如Linux , 底層的作業系統 通道就是雙向的;

八、緩衝區(buffer)

緩衝區本質上是一個可以讀寫資料的記憶體塊,可以理解成是一個 容器物件(含陣列),該物件提供了一組方法,可以更輕鬆地使用記憶體塊,,緩衝區對 象內建了一些機制,能夠跟蹤和記錄緩衝區的狀態變化情況。Channel 提供從檔案、 網路讀取資料的渠道,但是讀取或寫入的資料都必須經由 Buffer。
在 NIO 中,Buffer 是一個頂層父類別,它是一個抽象類。

1、常用Buffer子類一覽

  • ByteBuffer,儲存位元組資料到緩衝區;

  • ShortBuffer,儲存字串資料到緩衝區;

  • CharBuffer,儲存字元資料到緩衝區;

  • IntBuffer,儲存整數資料到緩衝區;

  • LongBuffer,儲存長整型資料到緩衝區;

  • DoubleBuffer,儲存小數到緩衝區;

  • FloatBuffer,儲存小數到緩衝區;

2、buffer四大屬性

  • mark:標記

  • position:位置,下一個要被讀或寫的元素的索引, 每次讀寫緩衝區資料時都會改變改值, 為下次讀寫作準備。

  • limit:表示緩衝區的當前終點,不能對緩衝區 超過極限的位置進行讀寫操作。且極限 是可以修改的

  • capacity:容量,即可以容納的最巨量資料量;在緩 衝區建立時被設定並且不能改變。

在這裡插入圖片描述

3、buffer常用api

JDK1.4時,引入的api

  • public final int capacity( )//返回此緩衝區的容量
  • public final int position( )//返回此緩衝區的位置
  • public final Buffer position (int newPositio)//設定此緩衝區的位置
  • public final int limit( )//返回此緩衝區的限制
  • public final Buffer limit (int newLimit)//設定此緩衝區的限制
  • public final Buffer mark( )//在此緩衝區的位置設定標記
  • public final Buffer reset( )//將此緩衝區的位置重置為以前標記的位置
  • public final Buffer clear( )//清除此緩衝區, 即將各個標記恢復到初始狀態,但是資料並沒有真正擦除, 後面操作會覆蓋
  • public final Buffer flip( )//反轉此緩衝區
  • public final Buffer rewind( )//重繞此緩衝區
  • public final int remaining( )//返回當前位置與限制之間的元素數
  • public final boolean hasRemaining( )//告知在當前位置和限制之間是否有元素
  • public abstract boolean isReadOnly( );//告知此緩衝區是否為唯讀緩衝區

JDK1.6時引入的api

  • public abstract boolean hasArray();//告知此緩衝區是否具有可存取的底層實現陣列
  • public abstract Object array();//返回此緩衝區的底層實現陣列
  • public abstract int arrayOffset();//返回此緩衝區的底層實現陣列中第一個緩衝區元素的偏移量
  • public abstract boolean isDirect();//告知此緩衝區是否為直接緩衝區

在這裡插入圖片描述

九、通道(channel)

1、基本介紹

(1)NIO的通道類似於流

  • 通道可以同時進行讀寫,而流只能讀或者只能寫;
  • 通道可以實現非同步讀寫資料
  • 通道可以從緩衝讀資料,也可以寫資料到緩衝

(2)BIO 中的 stream 是單向的,例如 FileInputStream 對 象只能進行讀取資料的操作,而 NIO 中的通道 (Channel)是雙向的,可以讀操作,也可以寫操作。
(3)Channel在NIO中是一個介面
(4)常用的 Channel 類有:FileChannel、 DatagramChannel、ServerSocketChannel 和 SocketChannel。ServerSocketChanne 類似 ServerSocket , SocketChannel 類似 Socket。
(5)FileChannel 用於檔案的資料讀寫, DatagramChannel 用於 UDP 的資料讀寫, ServerSocketChannel 和 SocketChannel 用於 TCP 的資料讀寫。

2、FileChannel

FileChannel主要用來對本地檔案進行 IO 操作,常見的方法有:

  • read,從通道讀取資料並放到緩衝區中

  • write,把緩衝區的資料寫到通道中

  • transferFrom,從目標通道 中複製資料到當前通道

  • transferTo,把資料從當 前通道複製給目標通道

3、關於Buffer 和 Channel的注意事項和細節

  • ByteBuffer 支援型別化的put 和 get, put 放入的是什麼資料型別,get就應該使用 相應的資料型別來取出,否則可能有 BufferUnderflowException 異常。

  • 可以將一個普通Buffer 轉成唯讀Buffer。

  • NIO 還提供了 MappedByteBuffer, 可以讓檔案直接在記憶體(堆外的記憶體)中進 行修改, 而如何同步到檔案由NIO 來完成。

  • NIO 還支援 通過多個 Buffer (即 Buffer 陣列) 完成讀寫操作,即 Scattering 和 Gathering。

十、Selector(選擇器)

1、基本介紹

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一個執行緒,處理多個的使用者端連 接,就會使用到Selector(選擇器)。

  • Selector 能夠檢測多個註冊的通道上是否有事件發生,如果有事件發生,便獲取事件然 後針對每個事件進行相應的處理。這樣就可以只用一個單執行緒去管理多個 通道,也就是管理多個連線和請求。

  • 只有在 連線/通道 真正有讀寫事件發生時,才會進行讀寫,就大大地減少 了系統開銷,並且不必為每個連線都建立一個執行緒,不用去維護多個執行緒。

  • 避免了多執行緒之間的上下文切換導致的開銷。

2、selector的相關方法

  • open();//得到一個選擇器物件

  • select(long timeout);//監控所有註冊的通道,當其 中有 IO 操作可以進行時,將 對應的 SelectionKey 加入到內部集合中並返回,引數用來 設定超時時間

  • selectedKeys();//從內部集合中得 到所有的 SelectionKey。

3、注意事項

NIO中的 ServerSocketChannel功能類似ServerSocket,SocketChannel功能類 似Socket。

十一、通過NIO實現簡單的伺服器端使用者端通訊

1、伺服器端

package com.nezha.guor.nio;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.*;import java.util.Iterator;public class NioServer {
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private static final int PORT = 8080;

    public NioServer() {
        try {
            //獲得選擇器
            selector = Selector.open();
            serverSocketChannel =  ServerSocketChannel.open();
            //繫結埠
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            //設定非阻塞模式
            serverSocketChannel.configureBlocking(false);
            //將該ServerSocketChannel 註冊到selector
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        }catch (IOException e) {
            System.out.println("NioServer error:"+e.getMessage());
        }
    }

    public void listen() {

        System.out.println("監聽執行緒啟動: " + Thread.currentThread().getName());
        try {
            while (true) {
                int count = selector.select();
                if(count > 0) {
                    //遍歷得到selectionKey集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();

                        if(key.isAcceptable()) {
                            SocketChannel sc = serverSocketChannel.accept();
                            sc.configureBlocking(false);
                            sc.register(selector, SelectionKey.OP_READ);
                            System.out.println(sc.getRemoteAddress() + " 上線 ");
                        }
                        //通道傳送read事件,即通道是可讀的狀態
                        if(key.isReadable()) {
                            getDataFromChannel(key);
                        }
                        //當前的key 刪除,防止重複處理
                        iterator.remove();
                    }
                } else {
                    System.out.println("等待中");
                }
            }
        }catch (Exception e) {
            System.out.println("listen error:"+e.getMessage());
        }
    }

    private void getDataFromChannel(SelectionKey key) {
        SocketChannel channel = null;
        try {
            channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            int count = channel.read(buffer);
            //根據count的值做處理
            if(count > 0) {
                String msg = new String(buffer.array());
                System.out.println("來自使用者端: " + msg);

                //向其它的使用者端轉發訊息(排除自己)
                sendInfoToOtherClients(msg, channel);
            }
        }catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 離線了");
                //取消註冊
                key.cancel();
            }catch (IOException ex) {
                System.out.println("getDataFromChannel error:"+ex.getMessage());
            }
        }finally {
            try {
                channel.close();
            }catch (IOException ex) {
                System.out.println("channel.close() error:"+ex.getMessage());
            }
        }
    }

    //轉發訊息給其它客戶(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self ) throws  IOException{
        System.out.println("伺服器轉發訊息中...");
        System.out.println("伺服器轉發資料給使用者端執行緒: " + Thread.currentThread().getName());
        //遍歷 所有註冊到selector 上的 SocketChannel,並排除 self
        for(SelectionKey key: selector.keys()) {
            Channel targetChannel = key.channel();

            //排除自己
            if(targetChannel instanceof  SocketChannel && targetChannel != self) {
                SocketChannel dest = (SocketChannel)targetChannel;
                //將資訊儲存到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //將buffer資料寫入通道
                dest.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        //建立伺服器物件
        NioServer nioServer = new NioServer();
        nioServer.listen();
    }}

2、使用者端

package com.nezha.guor.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.Scanner;public class NioClient {
    private final int PORT = 8080; //伺服器埠
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    public NioClient() throws IOException {
        selector = Selector.open();
        socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
        //設定非阻塞
        socketChannel.configureBlocking(false);
        //將channel註冊到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");
    }

    //向伺服器傳送訊息
    public void sendInfo(String info) {
        info = username + " 說:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        }catch (IOException e) {
            System.out.println("sendInfo error:"+e.getMessage());
        }
    }

    //讀取從伺服器端回覆的訊息
    public void readInfo() {
        try {
            int readChannels = selector.select();
            if(readChannels > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if(key.isReadable()) {
                        //得到相關的通道
                        SocketChannel sc = (SocketChannel) key.channel();
                        //得到一個Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //讀取
                        sc.read(buffer);
                        //把讀到的緩衝區的資料轉成字串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                }
                iterator.remove(); //刪除當前的selectionKey, 防止重複操作
            } else {
                System.out.println("沒有可以用的通道...");
            }
        }catch (Exception e) {
            System.out.println("readInfo error:"+e.getMessage());
        }
    }

    public static void main(String[] args) throws Exception {
        NioClient nioClient = new NioClient();
        new Thread() {
            public void run() {
                while (true) {
                    nioClient.readInfo();
                    try {
                        Thread.currentThread().sleep(2000);
                    }catch (InterruptedException e) {
                        System.out.println("sleep error:"+e.getMessage());
                    }
                }
            }
        }.start();

        //傳送資料給伺服器端
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLine()) {
            nioClient.sendInfo(scanner.nextLine());
        }
    }}

3、控制檯輸出

在這裡插入圖片描述
在這裡插入圖片描述

推薦學習:《》

以上就是帶你完全掌握Java NIO(總結分享)的詳細內容,更多請關注TW511.COM其它相關文章!