深入理解NIO三大核心

NIO三大核心

  • 在NIO中有三個核心對象
  • 緩衝區(Buffer)
  • 選擇器(Selector)
  • 通道(channel)

什麼是緩衝區Buffer?

  • 緩衝區實際上是一個容器對象,本質上是一個數組
  • 在NIO庫中,幾乎所有的數據都是使用緩衝區處理的
  • 在進行讀寫操作的時候,首先會經過緩衝區
  • 在面向流的IO系統中,所有的數據都是 讀寫 到Stream對象中

類繼承關係

  • 在NIO庫中,頂層抽象類Buffer定義了緩衝區的規範。它提供了各個基本數據類型對應的buffer
    在這裏插入圖片描述

工作原理

  • 在緩衝區中有三個重要的屬性
  • position capacity limit
postiton:指定下一個將要被寫入或者讀取的元素索引,調用put/get方法時更新,初始化爲0
limit   :指定剩餘數據容量/剩餘可存儲數據空間
capacity:可以存儲在緩衝區的最大數據容量
  • 一個簡單示例
public class BufferPlay {

    public static void main(String[] args) throws Exception {
        //  文件IO處理   使用文件輸入流讀取文件  把數據從磁盤讀取到內存—>內核緩衝區->進程緩衝區
        FileInputStream fis = new FileInputStream("filePath");
        //   創建文件的操作管道
        FileChannel channel = fis.getChannel();
        //  分配一個大小固定的緩衝區,即一個固定長度的byte數組
        ByteBuffer buffer = ByteBuffer.allocate(10);
        output("初始化", buffer);
        //  進行一次讀操作
        channel.read(buffer);
        output("調用channel.read()", buffer);
        //  鎖定操作範圍
        buffer.flip();
        output("調用buffer.flip()", buffer);
        //  判斷有無可讀數據
        while (buffer.remaining() > 0) {
            byte b = buffer.get();
        }
        output("調用buffer.get()", buffer);
        //  復位
        buffer.clear();
        output("調用buffer.clear()", buffer);
        // 關閉管道
        channel.close();
    }

    private static void output(String string, ByteBuffer buffer) {
        System.out.println(string + " : ");
        // 容量,數組大小
        System.out.print("capacity: " + buffer.capacity() + ", ");
        // 當前操作數據所在的位置,也可以叫做遊標
        System.out.print("position: " + buffer.position() + ", ");
        // 鎖定值,flip,數據操作範圍索引只能在position - limit 之間
        System.out.println("limit: " + buffer.limit());
        System.out.println();
    }

}
//  運行結果
初始化 : 
capacity: 10, position: 0, limit: 10

調用channel.read() : 
capacity: 10, position: 8, limit: 10

調用buffer.flip() : 
capacity: 10, position: 0, limit: 8

調用buffer.get() : 
capacity: 10, position: 8, limit: 8

調用buffer.clear() : 
capacity: 10, position: 0, limit: 10
  • 圖解

在這裏插入圖片描述

緩衝區分配

public class BufferWrap {

    public void myMethod() {

        /*  調用 allocate(int i) 方法相當於創建了一個指定大小的數組,並封裝爲緩衝區對象
         *  也可以使用  wrap(byte[] arr) 方法,把一個現有的數組封裝爲緩衝區對象
         *
         */
        //  分配指定大小的緩衝區
        ByteBuffer allocate = ByteBuffer.allocate(10);

        //  定義一個 Byte 數組  容量爲10
        byte[] arr = new byte[10];
        ByteBuffer result = ByteBuffer.wrap(arr);
    }
}

緩衝區分片

public class BufferSlice {

    /*
     *    緩衝區分片:在現有緩衝區上切出一片來作爲一個新的緩衝區,即子緩衝區
     *               現有緩衝區與字緩衝區在底層數組層面是數據共享的
     *               子緩衝區相當於現有緩衝區的一個視圖窗口
     */

    public static void main(String[] args) {
        //  分配一個緩衝區大小爲10的數組
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //  向緩衝區中寫入數據  0-9
        for (int i = 0; i < buffer.capacity(); ++i) {
            buffer.put((byte) i);
        }
        //  創建子緩衝區  設置字緩衝區的位置 爲 原緩衝區位置的  3-6  容量爲4
        buffer.position(3);
        buffer.limit(7);
        ByteBuffer slice = buffer.slice();
        // 改變子緩衝區的內容
        for (int i=0; i<slice.capacity(); ++i) {
            byte b = slice.get( i );
            b *= 10;
            slice.put(i, b);
        }
        //  重新設置  position  和  limit  的位置,方便讀取驗證數據
        buffer.position(0);
        buffer.limit(buffer.capacity());

        while (buffer.remaining() > 0) {
            System.out.print(buffer.get()+" ");
        }
    }
}

只讀緩衝區

  • 只讀緩衝區不允許寫入數據
//  創建一個緩衝區
ByteBuffer buffer = ByteBuffer.allocate(10);
//  創建一個只讀緩衝區-從原有緩衝區基礎上覆制
ByteBuffer readOnly = buffer.asReadOnlyBuffer();
  • 只讀緩衝區與原緩衝區共享數據,原緩衝區內容發生變化,只讀緩衝區內容隨之變化

  • 只讀緩衝區不能轉換爲常規緩衝區,修改只讀緩衝區的內容會報異常

直接緩衝區

  • 爲加快IO速度,使用一種特殊方式爲其分配內存的緩衝區
  • 操作系統在進行IO操作之前,會嘗試跳過中間緩衝區,直接將內容分配給直接緩衝區
ByteBuffer buffer = ByteBuffer.allocateDirect();

內存映射

  • 內存映射是一種讀寫文件數據的方法
  • 速度優於常規基於流或者通道的IO
  • 通過使文件中的數據出現在內存數組中來完成,一般只有文件中實際讀寫的內容部分纔會映射到內存中
public class BufferMapped {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("filePath", "rw");
        FileChannel channel = raf.getChannel();

        //  把緩衝區和文件系統進行映射關聯
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 8);
        //  修改數據——執行完畢在對應文件中查看
        map.put(1, (byte) 100);
        map.put(7, (byte) 101);

        raf.close();
        channel.close();
    }
}

選擇器Selector

傳統的Server/Client

  • 傳統的Server/Client交互基於TPR (Thread per Request)
  • 服務器會爲每一個客戶端請求建立一個線程,由該線程負責處理一個用戶請求
  • 請求數量很多的情況下,系統難以承受

NIO模型中採用的方式

  • NIO中的非阻塞IO,採用了基於Reactor的工作模式,IO調用不會被阻塞
  • 通過註冊特定的IO事件,如可讀數據到達、新的套接字連接等。發生特定事件時,系統返回通知
  • 實現非阻塞IO的核心對象就是選擇器Selector
  • Selector是註冊各種IO事件的地方,對不同事件發生做出相對響應

圖片引用自咕泡學院Tom老師的課堂筆記
圖片引用自咕泡學院Tom老師的課堂筆記

  • 當有事件發生時,可以從Selector中獲取相應的SelectionKey

  • 從Selectionkey中可以找到事件發生的具體SelectableChannel

  • SelecttableChannel處理完畢,可對key重新進行註冊

  • 代碼示例

public class NioServerSocket {

    /**
     * selector 選擇器
     */
    private Selector selector;

    /**
     * 緩衝區
     */
    private ByteBuffer buffer = ByteBuffer.allocate(1024);

    private int port = 8080;

    public NioServerSocket(int port) {
        //  初始化   輪詢器
        try {
            this.port = port;
            ServerSocketChannel server = ServerSocketChannel.open();

            //  socket 服務綁定到目標  IP:PORT  默認爲本地IP即 localhost
            server.bind(new InetSocketAddress(this.port));

            //NIO:BIO的升級版本   兼容BIO  NIO模型默認採用阻塞式
            //  設置false:非阻塞
            server.configureBlocking(false);

            //  可以接收請求了
            this.selector = Selector.open();

            // ON_ACCEPT :設置爲可接收請求狀態
            server.register(this.selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void listen() {
        System.out.println("listen on "+ this.port + "");
        //輪詢主線程
        try {
            while (true) {
                //  接收所有的請求  select()方法會阻塞,直到拿到一個key
                this.selector.select();
                Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
                //  不斷地迭代
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                //  同步體現:每次只能拿到一個key,每次只能處理一種狀態
                while (iterator.hasNext()) {
                    //  每一個key代表一種狀態
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    process(key);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void process(SelectionKey key) throws IOException {
        //  針對每一種 key 的狀態給一個反應
        if(key.isAcceptable()) {
            ServerSocketChannel severChannel = (ServerSocketChannel) key.channel();
            SocketChannel channel = severChannel.accept();
            channel.configureBlocking(false);
            //  當數據準備就緒的時候,將狀態改爲可讀
            key = channel.register(this.selector, SelectionKey.OP_READ);
        } else if (key.isReadable()){
            //  key.channel  從多路複用器中拿到客戶端的引用
            //  多路複用體現在,key.channel 每一次拿到的對象都是同一個引用
            SocketChannel channel = (SocketChannel) key.channel();
            int length = channel.read(this.buffer);
            if (length > 0) {
                this.buffer.flip();
                String content = new String(this.buffer.array(), 0, length);
                key = channel.register(this.selector, SelectionKey.OP_WRITE);
                key.attach(content);
                System.out.println("讀取內容:"+content);
            }
        } else if (key.isWritable()) {
            SocketChannel channel = (SocketChannel) key.channel();
            String content = (String) key.attachment();
            channel.write(ByteBuffer.wrap(("輸出"+content).getBytes()));
            channel.close();
        }
    }

    public static void main(String[] args) {
        new NioServerSocket(8080).listen();
    }

}

通道Channel

  • 數據通過Buffer對象來處理
  • 數據 和 Buffer對象交互中,通過通道Channel來完成

NIO中Channel結構

  • NIO中所有的通道對象都實現了Channel接口

在這裏插入圖片描述

Channel的作用

  • 使用NIO讀寫數據
從  InputStream 中獲取 Channel
創建Buffer
將數據通過Channle 讀/寫 到Buffer
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章