聊一聊NIO的三大組件:Buffer、Channel和Selector

NIO是啥?

NIO是Java從JDK1.4開始引入的一系列改進版輸入輸出處理手段,也就是New IO,簡稱NIO,也有說法叫NonBlocking IO,是同步非阻塞式的IO模型,準確地說它支持阻塞非阻塞兩種模式。

筆者在NIO、BIO、AIO、同步異步、阻塞非阻塞傻傻分不清楚?一文中詳細總結了同步、非阻塞等相關概念,分析了NIO與傳統BIO的主要區別。

本篇主要介紹NIO提供的三大組件的概念及使用:Buffer,Channel,Selector。

Buffer

Buffer可以理解爲是一個容器,用於存儲數據本質是個數組,存儲的元素類型是基本類型。

無論是發送還是讀取Channel中的數據,都必須先置入Buffer。

java.nio.Buffer是一個抽象類,子類包括有除boolean外其他所有基本類型的XxBuffer,最常用的是ByteBuffer。

Buffer中的重要概念

capacity:緩衝區的容量,表示該Buffer的最大數據容量,即最多可以存儲多少數據。

limit:限制位,標記position能夠到達的最大位置,默認爲緩衝區最後一位。

position:操作位,指向即將操作位置,默認指向0。

mark:可選標記位。默認不啓用,Buffer允許直接將position定位到mark處。

他們滿足的關係:mark <= position <= limit <= capacity

ByteBuffer.allocate(capacity)爲例,說明幾個重要的過程:

  • 初始化創建HeapByteBuffer,mark = -1,position = 0,limit = cap。
  • 通過put方法向Buffer中加入數據,position++。
  • 裝入數據結束後,調用flip方法,將limit設置爲position所在位置,position置爲0,表示[position,limit]這段需要開始進行輸出了【可以使用get方法讀取數據】。
  • 輸出結束後,調用clear方法,將position置爲0,limit置爲cap,爲下一次讀取數據做好準備。

Buffer的所有子類都提供了put和get方法,對應向Buffer存入數據和從Buffe中取出數據,方式分爲以下兩種:

  • 相對:從Buffer的當前pos處開始讀取或寫入數據,然後將pos的值按處理元素的個數增加。
  • 絕對:直接根據索引向Buffer中讀取或寫入數據,不會影響pos位置本身的值。

Buffer使用Demo

    public static void main(String[] args) {

        // 創建 bytebuffer  allocate指定底層數組長度
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);

        // 添加數據
        byteBuffer.put("summerday".getBytes());

        // 獲取操作位的位置pos = 9
        System.out.println(byteBuffer.position());
        
        // 如果我想遍歷summerday呢?哪裏結束? limit = 10
        System.out.println(byteBuffer.limit());

        //反轉緩衝區 可以利用flip()代替下面兩步操作
        //將limit移到position的位置
        byteBuffer.limit(byteBuffer.position());
        
        //將pos移到0
        byteBuffer.position(0);

        // position < limit 
        // 可以利用 hasRemaining代替判斷pos和limit之間是否還有可處理元素。
        while(byteBuffer.position() < byteBuffer.limit()){
            // 獲取數據
            byte b = byteBuffer.get();
            System.out.println(b);
        }
    }


常用方法介紹

其實Buffer操作的邏輯比較簡單,每個方法操作的字段也不外乎上面介紹的幾個,下面是一些常用的方法:

設置方法

  • Buffer position(newPosition): 將pos設置爲newPosition。
  • Buffer limit(newLimit):將limit設置爲newLimit。

數據操作

  • Buffer reset:將pos置爲mark的位置。
  • Buffer rewind:將pos置爲0,取消設置的mark。
  • Buffer flip: 將limit置pos位置,pos置0。
  • Buffer clear:將position置爲0,limit置爲cap。

其他操作

    public static void main(String[] args) {

        // 如果數據已知,可以使用wrap方法創建ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.wrap("summerday".getBytes());
        // 獲取底層字節數組
        byte[] array = byteBuffer.array();
        System.out.println(new String(array));

    }

Channel

Channel概述

Channel 類似於傳統的流對象,但有些不同:

  • Channel 直接將指定文件的部分或全部直接映射 Buffer。
  • 程序不能直接訪 Channel 中的數據,包括讀取、寫入都不行,Channel只能與 Buffer 進行交互。意思是,程序要讀數據時需要先通過Buffer從Channel中獲取數據,然後從Buffer中讀取數據。
  • Channel通常可以異步讀寫,但默認是阻塞的,需要手動設置爲非阻塞。

Channel不應該通過構造器來直接創建,而是通過傳統的節點InputStream、OutputStream的getChannel()方法來返回對應的Channel,或者通過RandomAccessFile對象的getChannel方法。

Channel中最常用的三種方法:

  • map():將Channel對應的部分或全部數據映射成ByteBuffer。
  • read():從Buffer中讀取數據。
  • write():向Buffer中寫入數據。

RandomAccessFile#getChannel

下面是個簡單的示例,通過RandomAccessFile的getChannel方法:

    public static void main(String[] args) throws FileNotFoundException {

        RandomAccessFile file = new RandomAccessFile("D://b.txt", "rw");
        // 獲取RandomAccessFile對應的channel
        try (FileChannel fileChannel = file.getChannel()) {
            ByteBuffer buf = ByteBuffer.allocate(48);
            int byteRead = fileChannel.read(buf);
            while(byteRead != -1){
                System.out.println("read " + byteRead);
                buf.flip();
                while (buf.hasRemaining()){
                    System.out.println((char) buf.get());
                }
                buf.clear();
                byteRead = fileChannel.read(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

SocketChannel與ServerSocketChannel

Java爲Channel接口根據不同功能,提供了不同的實現類,比如我們下面的示例:支持TCP網絡通信的SocketChannel和ServerSocketChannel。

public class Client {

    public static void main(String[] args) throws IOException {
        // 開啓客戶端的channel
        SocketChannel sc = SocketChannel.open();
        // 手動設置爲非阻塞模式
        sc.configureBlocking(false);
        // 發起連接
        sc.connect(new InetSocketAddress(8081));
        // 手動判斷保證連接的建立
        while(!sc.isConnected()){
            // 如果多次連接都沒有臉上,會認爲此次連接無法建立
            sc.finishConnect();
        }
        // 發送數據
        sc.write(ByteBuffer.wrap("hello , i am client".getBytes()));
        // 關閉通道
        sc.close();
    }
}


public class Server {

    public static void main(String[] args) throws IOException {
        // 開啓服務器端的通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 監聽端口號
        ssc.bind(new InetSocketAddress(8081));
        // 手動設置爲非阻塞模式
        ssc.configureBlocking(false);
        // 接收連接
        SocketChannel sc = ssc.accept();
        // 保證連接
        while (sc == null) {
            sc = ssc.accept();
        }
        // 讀取數據
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        sc.read(buffer);
        byte[] bs = buffer.array();
        System.out.println(new String(bs, 0, buffer.position()));
        // 關流
        ssc.close();

    }
}

Selector

NIO實現非阻塞IO的其中關鍵組件之一就是Selector多路複用選擇器,可以註冊多個Channel到一個Selector中。Selector可以不斷執行select操作,判斷這些註冊的Channel是否有已就緒的IO事件,如可讀,可寫,網絡連接已完成等。

一個線程通過使用一個Selector管理多個Channel。

public class Server {

    public static void main(String[] args) throws IOException {

        // 開啓服務端的通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 設置非阻塞
        ssc.configureBlocking(false);

        // 綁定端口
        ssc.bind(new InetSocketAddress(8081));

        // 開啓選擇器
        Selector selector = Selector.open();
        // 將通道註冊到選擇器上
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            // 選擇已註冊的通道
            selector.select();
            // 獲取選擇通道的事件
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 接收
                if (key.isAcceptable()) {
                    // 從事件中獲取通道
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    // 接收連接
                    SocketChannel sc = channel.accept();
                    // 設置非阻塞
                    sc.configureBlocking(false);
                    // 註冊讀 + 寫事件
                    sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                }
                // 讀
                if (key.isReadable()) {
                    // 獲取通道
                    SocketChannel sc = (SocketChannel) key.channel();
                    // 讀取數據到buffer
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    sc.read(buffer);
                    // 反轉緩衝區
                    buffer.flip();
                    System.out.println(new String(buffer.array(), 0, buffer.limit()));
                    // 在同一通道上註冊,將會將之前註冊的事件給註冊
                    // 註銷read事件
                    sc.register(selector, key.interestOps() ^ SelectionKey.OP_READ);
                }
                // 寫
                if (key.isWritable()) {
                    // 獲取通道
                    SocketChannel sc = (SocketChannel) key.channel();
                    sc.write(ByteBuffer.wrap("hello client, i am server!".getBytes()));
                    // 註銷write事件
                    sc.register(selector, key.interestOps() ^ SelectionKey.OP_WRITE);
                }
            }
            iterator.remove();
        }
    }

}


public class Client {

    public static void main(String[] args) throws IOException {

        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress(8081));
        sc.write(ByteBuffer.wrap("hello ! i am client !".getBytes()));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        sc.read(buffer);
        System.out.println(new String(buffer.array(), 0, buffer.position()));

    }
}

參考閱讀

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