主頁 > 後端開發 > 一步一圖帶你深入剖析 JDK NIO ByteBuffer 在不同位元組序下的設計與實作

一步一圖帶你深入剖析 JDK NIO ByteBuffer 在不同位元組序下的設計與實作

2022-08-11 14:18:52 後端開發

讓我們來到微觀世界重新認識 Netty

在前面 Netty 原始碼決議系列 《聊聊 Netty 那些事兒》中,筆者帶領大家從宏觀世界詳細剖析了 Netty 的整個運轉流程,從一個網路資料包在內核中的收發程序開始聊起,總體介紹了 Netty 的 IO 執行緒模型,后面我們圍繞著這個 IO 模型又詳細介紹了整個 Reactor 模型在 Netty 中的實作,

這個宏觀流程包括:Reactor模型的創建,啟動,運轉架構,網路連接的接收和關閉,網路資料的接收和發送,利用 pipeline 對 IO 處理邏輯的編排,Netty 的優雅關閉,

Netty 的原始碼決議系列寫到這里,筆者算是帶著大家在 Netty 的宏觀世界中翱翔了一圈,但筆者還是不舍得和大家說再見,于是決定在帶領大家到 Netty 的微觀世界中一探究竟,這個系列的目的就是想讓大家從內核層面深入地搞透 Netty,

在 Netty 的微觀世界系列中,筆者會為大家講述 Netty 中的高性能組件的相關設計和實作以及應用,內容包括:

  • Netty 中的網路資料容器 ByteBuf 的整個設計體系的實作,

  • Netty 中的記憶體池設計與實作,在這個程序中,筆者會把 Linux 內核中記憶體管理子系統相關原始碼帶大家走讀一遍,讓大家從內核層面到應用層面徹底搞透徹高性能記憶體分配的原理及其實作,

  • Netty 中用于執行海量延時任務的時間輪相關設計與實作,并與 Kafka 中的時間輪設計做出詳細對比,

  • Netty 中用到的零拷貝技術在內核中的實作,

  • Netty 中用到的 MPSC (多生產者單消費者)佇列的設計與實作以及應用場景,

  • Netty 中實作無鎖化并發的關鍵組件 FastThreadLocal 的設計與實作,并詳細對比 FastThreadLocal 究竟比 JDK 中 ThreadLocal 快在了哪里,

  • 理論講完了,實踐是必不可少的,最后筆者會帶大家剖析 Netty 在各個著名中間件中是如何使用的,進一步加深大家對 Netty 的理解,

筆者的這個 Netty 微觀世界系列會涉及大量豐富的細節描述,對于喜歡細節控的同學一定不要錯過~~

寫在本文開始之前.....

本文我們開始 Netty 微觀世界系列第一部分的內容,聊聊 Netty 中的網路資料容器 ByteBuf ,對于 ByteBuf 我想大家一定不會陌生,它曾多次出現在前面的系列文章中,比如在《Netty如何高效接收網路資料 | 一文聊透ByteBuffer動態自適應擴縮容機制》和《一文搞懂Netty發送資料全流程 | 你想知道的細節全在這里》這兩篇文章中提到的 Netty 接收網路資料和發送網路資料時用到的ByteBuf,

ByteBuf 是 Netty 中的資料容器,Netty 在接收網路資料和發送網路資料時,都會首先將這些網路資料事先快取在 ByteBuf 中,然后在將它們丟給 pipeline 處理或者發送給 Socket ,這樣做的目的是防止在接收網路資料的程序中網路資料一直積壓在 Socket 的接識訓沖區中使得接識訓沖區的資料越來越多,導致對端 TCP 協議中的視窗關閉(滑動視窗),影響到了整個 TCP 通信的速度,而有了 ByteBuf,我們可以先將讀取的資料快取在 ByteBuf 中,提高 TCP 的通信能力,

而在 Netty 發送資料的時候,也可以事先將資料快取在 ByteBuf 中,如果 Socket 發送緩沖區已滿變為不可寫狀態時,由于資料我們已經快取在 ByteBuf 中了,用戶的發送執行緒不需要阻塞等待,當 Socket 發送緩沖區再次變得可寫時,Netty 會將 ByteBuf 中的資料寫入到 Socket 中,這也是 Netty 實作異步發送資料的核心所在,

而 Netty 中的 ByteBuf 底層依賴了JDK NIO 中的 ByteBuffer ,眾所周知 JDK NIO 中的 ByteBuffer 設計的非常復雜而且提供的相關 API 使用起來也很反人類,易用性不是很好,所以 Netty 的 ByteBuf 針對 JDK NIO ByteBuffer 進行了優化,再此基礎上重新設計出了一套簡潔易用的 API 出來,

熟悉筆者寫作風格的讀者朋友都知道,筆者一向是喜歡把技術的脈絡給大家鋪展開來講解,一層一層地介紹技術的演變程序,力求給大家清晰地展現出整個技術的全貌,通過技術的演變程序,我們不僅可以知道這個技術點最初的樣貌,它的優缺點是什么?瓶頸是什么?我們還可以針對這些缺點和瓶頸觸發自己的思考,如何優化?如何演變?通過這個程序的洗禮,我們才能夠對現有技術理解的清晰透徹,

根據這個思路,在介紹 Netty 的 ByteBuf 設計之前,筆者想專門用一篇文章來為大家介紹下 JDK NIO Buffer 的設計,看一下 NIO ByteBuffer 是如何設計的,它有哪些缺點,針對這些缺點,Netty 又是如何優化的,徹底理解 Netty 資料載體 ByteBuf 的前世今生,

image

1. JDK NIO 中的 Buffer

在 NIO 沒有出現之前,Java 傳統的 IO 操作都是通過流的形式實作的(包括網路 IO 和檔案 IO ),也就是我們常見的輸入流 InputStream 和輸出流 OutputStream,

但是 Java 傳統 IO 的 InputStream 和 OutputStream 的相關操作全部都是阻塞的,比如我們使用 InputStream 的 read 方法從流中讀取資料時,如果此時流中沒有資料,那么用戶執行緒就必須阻塞等待,

還有一點就是傳統的這些輸入輸出流在處理位元組流的時候一次只能處理一個位元組,這樣在處理網路 IO 的時候讀取 Socket 緩沖區中的資料效率就會很低,而且在操作位元組流的時候只能線性的處理流中的位元組,不能來回移動位元組流中的資料,這樣導致我們在處理位元組流中的資料的時候就顯得不是很靈活,

所以綜上所述,Java 傳統 IO 是面向流的,流的處理是單向,阻塞的,而且無論是從輸入流中讀取資料還是向輸出流中寫入資料都是一個位元組一個位元組來處理的,通常都是從輸入流中邊讀取資料邊處理資料,這樣 IO 處理效率就會很低,

基于上述原因,JDK1.4 引入了 NIO,而 NIO 是面向 Buffer 的,在處理 IO 操作的時候,會一次性將 Channel 中的資料讀取到 Buffer 中然后在做后續處理,向 Channel 中寫入資料也是一樣,也是需要一個 Buffer 做中轉,然后將 Buffer 中的資料批量寫入 Channel 中,這樣一來我們可以利用 Buffer 將里面的位元組資料來回移動并根據我們想要的處理方式靈活處理,

除此之外,Nio Buffer 還提供了堆外的直接記憶體和記憶體映射相關的訪問方式,來避免記憶體之間的來回拷貝,所以即使在傳統 IO 中用到了 BufferedInputStream 也還是沒辦法和 Nio Buffer 相匹敵,

那么接下來就讓我們正式進入JDK NIO Buffer 如何設計與實作的相關主題

2. NIO 對 Buffer 的頂層抽象

JDK NIO 提供的 Buffer 其實本質上是一塊記憶體,大家可以把它簡單想象成一個陣列,JDK 將這塊記憶體在語言層面封裝成了 Buffer 的形式,我們可以通過 Buffer 對這塊記憶體進行讀取或者寫入資料,以及執行各種騷操作,

如下圖中所示,Buffer 類是JDK NIO 定義的一個頂層抽象類,對于緩沖區的所有基本操作和基礎屬性全部定義在頂層 Buffer 類中,在 Java 中一共有八種基本型別,JDK NIO 也為這八種基本型別分別提供了其對應的 Buffer 類,大家可以把這些 Buffer 類當做成對應基礎型別的陣列,我們可以利用這些基礎型別相關的 Buffer 類對陣列進行各種操作,

image

在為大家決議具體的緩沖區實作之前,我們先來看下這個緩沖區的頂層抽象類 Buffer 中到底定義規范了哪些抽象操作,具有哪些屬性,這些屬性分別是用來干什么的?先帶大家從總體上認識一下JDK NIO 中的 Buffer 設計,

2.1 Buffer 中的屬性

public abstract class Buffer {

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    
             .............
}

首先我們先來介紹下 Buffer 中最重要的這三個屬性,后面即將介紹的關于 Buffer 的各種騷操作均依賴于這三個屬性的動態變化,

image

  • capacity:這個很好理解,它規定了整個 Buffer 的容量,具體可以容納多少個元素,capacity 指標之前的元素均是 Buffer 可操作的空間,

  • position:用于指向 Buffer 中下一個可操作性的元素,初始值為 0,在 Buffer 的寫模式下,position 指標用于指向下一個可寫位置,在讀模式下,position 指標指向下一個可讀位置,

  • limit:表示 Buffer 可操作元素的上限,什么意思呢?比如在 Buffer 的寫模式下,可寫元素的上限就是 Buffer 的整體容量也就是 capacity ,capacity - 1 即為 Buffer 最后一個可寫位置,在讀模式下,Buffer 中可讀元素的上限即為上一次 Buffer 在寫模式下最后一個寫入元素的位置,也就是上一次寫模式中的 position,

  • mark:用于標記 Buffer 當前 position 的位置,這個欄位在我們對網路資料包解碼的時候非常有用,在我們使用 TCP 協議進行網路資料傳輸的時候經常會出現粘包拆包的現象,所以為了應對粘包拆包的問題,在解碼之前都需要先呼叫
    mark 方法將 Buffer 的當前 position 指標保存至 mark 屬性中,如果 Buffer 中的資料足夠我們解碼為一個完整的包,我們就執行解碼操作,如果 Buffer 中的資料不夠我們解碼為一個完整的包(也就是半包),我們就呼叫 reset 方法,將 position 還原到原來的位置,等待剩下的網路資料到來,

image

在我們理解了 Buffer 中這幾個重要屬性的含義之后,接下來我們就來看一看 JDK NIO 在 Buffer 頂層設計類中定義規范的那些抽象操作,

2.2 Buffer 中定義的核心抽象操作

本小節中介紹的這幾個關于 Buffer 的核心操作均是基于上小節中介紹的那些核心指標的動態調整實作的,

2.2.1 Buffer 的構造

構造 Buffer 的主要邏輯就是根據用戶指定的引數來初始化 Buffer 中的這四個重要屬性:mark,position,limit,capacity,它們之間的關系為:mark <= position <= limit <= capacity ,其中 mark 初始默認為 -1,position 初始默認為 0,

image

public abstract class Buffer {

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

    Buffer(int mark, int pos, int lim, int cap) {     
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }

    public final Buffer limit(int newLimit) {
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        limit = newLimit;
        if (position > limit) position = limit;
        if (mark > limit) mark = -1;
        return this;
    }

    public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;
        return this;
    }
}

2.2.2 獲取 Buffer 下一個可讀取位置

當我們在 Buffer 的讀模式下,需要從 Buffer 中讀取資料時,需要首先知道當前 Buffer 中 position 的位置,然后根據 position 的位置讀取 Buffer 中的元素,隨后 position 向后移動指定的步長 nb,

image

nextGetIndex() 方法首先獲取 Buffer 當前 position 的位置作為 readIndex 回傳給用戶,然后 position 向后移動一位,這里的步長 nb 默認為1,


    final int nextGetIndex() {                        
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }

nextGetIndex(int nb) 方法的邏輯和 nextGetIndex() 方法一樣,唯一不同的是該方法指定了position 向后移動的步長 nb,

    final int nextGetIndex(int nb) {          
        if (limit - position < nb)
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        return p;
    }

大家這里可能會感到好奇,為什么會增加一個指定 position 移動步長的 nextGetIndex(int nb) 方法呢

在《2. NIO 對 Buffer 的頂層抽象》小節的開始,我們介紹了 JDK NIO 中 Buffer 頂層設計體系,除了 boolean 這個基本型別,NIO 為幾乎所有的 Java 基本型別定義了對應的 Buffer 類,

image

假如我們從一個 ByteBuffer 中讀取一個 int 型別的資料時,我們就需要在讀取完畢后將 position 的位置向后移動 4 位,在這種情況下 nextGetIndex(int nb) 方法的步長 nb 就應該指定為 4.

   public int getInt() {
        return getInt(ix(nextGetIndex((1 << 2))));
    }

2.2.3 獲取 Buffer 下一個可寫入位置

同獲取 readIndex 的程序一樣,當我們處于 Buffer 的寫模式下,向 Buffer 寫入資料時,首先也需要獲取 Buffer 當前 position 的位置(writeIndex),當寫入元素后,position 向后移動指定的步長 nb,

同樣的道理,我們可以向 ByteBuffer 中寫入一個 int 型的資料,這時候指定的步長 nb 也是 4 ,


    final int nextPutIndex() {                        
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }

    final int nextPutIndex(int nb) {                  
        if (limit - position < nb)
            throw new BufferOverflowException();
        int p = position;
        position += nb;
        return p;
    }

2.2.4 Buffer 讀模式的切換

當我們在 Buffer 的寫模式下向 Buffer 寫入資料之后,接下來我們就需要從 Buffer 中讀取剛剛寫入的資料,由于 NIO 在對 Buffer 的設計中讀寫模式是混用一個 position 屬性,所以我們需要做讀模式的切換,

image

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

我們看到 flip() 方法是對 Buffer 中的這四個指標做了一些調整達到了讀模式切換的目的:

  1. 將下一個可寫入位置 position 作為讀模式下的上限 limit,
  2. position設定為 0 ,這樣使得我們可以從頭開始讀取 Buffer 中寫入的資料,

2.2.5 Buffer 寫模式的切換

有讀模式的切換肯定就會有對應的寫模式切換,當我們在讀模式下以將 Buffer 中的資料讀取完畢之后,這時候如果再次向 Buffer 寫入資料的話,就需要切換到 Buffer 的寫模式下,

image

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

我們看到呼叫 clear() 方法之后,Buffer 中各個指標的狀態又回到了最初的狀態:

  1. position 位置重新指向起始位置 0 處,寫入上限 limit 重新指向了 capacity 的位置,

  2. 這時向 Buffer 中寫入資料時,就會從 Buffer 的開頭處依次寫入,新寫入的資料就會把已經讀取的那部分資料覆寫掉,

但是這里就會有一問題,當我們在讀模式下將 Buffer 中的資料全部讀取完畢時,呼叫 clear() 方法開啟寫模式,是沒有問題的,

如果我們只是讀取了 Buffer 中的部分資料,但是還有一部分資料沒有讀取,這時候,呼叫 clear() 方法開啟寫模式向 Buffer 中寫入資料的話,就會出問題,因為這會覆寫掉我們還沒有讀取的資料部分,

image

針對這種情況,我們就不能簡單粗暴的設定 position 指標了,為了保證未讀取的資料部分不被覆寫,我們就需要先將不可覆寫的資料部分移動到 Buffer 的最前邊,然后將 position 指標指向可覆寫資料區域的第一個位置,

image

由于 Buffer 是頂層設計只是負責定義 Buffer 相關的操作規范,并未定義具體的資料存盤方式,因為 compact() 涉及到移動資料,所以實作在了 Buffer 具體子類中,這里我們以 HeapByteBuffer 舉例說明:

class HeapByteBuffer extends ByteBuffer {

    //HeapBuffer中底層負責存盤資料的陣列
    final byte[] hb; 

    public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;
    }

    public final int remaining() {
        return limit - position;
    }

   final void discardMark() {                          
        mark = -1;
    }

}

2.2.6 重新讀取 Buffer 中的資料 rewind

rewind() 方法可以幫助我們重新讀取 Buffer 中的資料,它會將 position 的值重新設定為 0,并丟棄 mark,

image

    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

3. NIO Buffer 背后的存盤機制

在《2. NIO 對 Buffer 的頂層抽象》小節的開頭提到我們可以把 Buffer 簡單的看做是一個陣列,然后基于前邊介紹的四個指標:mark,position,limit,capacity 的動態調整來實作對 Buffer 的各種操作,

同時我們也提到了除了 boolean 這種基本型別之外,NIO 為其他幾種 Java 基本型別都提供了其對應的 Buffer 類,

image

而針對每一種基本型別的 Buffer ,NIO 又根據 Buffer 背后的資料存盤記憶體不同分為了:HeapBuffer,DirectBuffer,MappedBuffer,

HeapBuffer 顧名思義它背后的存盤記憶體是在 JVM 堆中分配,在堆中分配一個陣列用來存放 Buffer 中的資料,

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
    //在堆中使用一個陣列存放Buffer資料
    final byte[] hb;  
}

DirectBuffer 背后的存盤記憶體是在堆外記憶體中分配,MappedBuffer 是通過記憶體檔案映射將檔案中的內容直接映射到堆外記憶體中,其本質也是一個 DirectBuffer ,

由于 DirectBuffer 和 MappedBuffer 背后的存盤記憶體是在堆外記憶體中分配,不受 JVM 管理,所以不能用一個 Java 基本型別的陣串列示,而是直接記錄這段堆外記憶體的起始地址,

public abstract class Buffer {
    //堆外記憶體地址
    long address;
}

筆者后面還會為大家詳細講解 DirectBuffer 和 MappedBuffer,這里提前引出只是讓大家理解這三種不同型別的 Buffer 背后記憶體區域的不同,

綜上所述,HeapBuffer 背后是有一個對應的基本型別陣列作為存盤的,而 DirectBuffer 和 MappedBuffer 背后是一塊堆外記憶體做存盤,并沒有一個基本型別的陣列,

hasArray() 方法 就是用來判斷一個 Buffer 背后是否有一個 Java 基本型別的陣列做支撐,

 public abstract boolean hasArray();

如果 hasArray() 方法回傳 true,我們就可以呼叫 Object array() 方法獲取 Buffer 背后的支撐陣列,

 public abstract Object array();

其中 Buffer 中還有一個不太好理解的屬性是 offset,而這個 offset 到底是用來干什么的呢

4. Buffer 的視圖

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
    //在堆中使用一個陣列存放Buffer資料
    final byte[] hb;  
    // 陣列中的偏移,用于指定陣列中的哪一段資料是被 Buffer 包裝的
    final int offset;
}

事實上我們可以根據一段連續的記憶體地址或者一個陣列創建出不同的 Buffer 視圖出來,

image

如上圖所示,我們可以根據原生 Buffer 中的部分資料(比如圖中的未處理資料部分)創建出一個新的 Buffer 視圖出來,

這個新的視圖 Buffer 本質上也是一個 Buffer ,擁有獨立的 mark,position,limit,capacity 指標,這個四個指標會在新的 Buffer 視圖下重新被創建賦值,所以在新的視圖 Buffer 下和操作普通 Buffer 是一樣的,也可以使用 《2.2 Buffer 中定義的核心抽象操作》小節中介紹的那些方法,只不過操作的資料范圍不一樣罷了,

新的視圖 Buffer 和原生 Buffer 共享一個存盤陣列或者一段連續記憶體

站在新的視圖 Buffer 角度來說,它的存盤陣列范圍:0 - 6,所以再此視圖下 position = 0,limit = capacity = 7 ,這其實是一個障眼法,真實情況是新的視圖 Buffer 其實是復用原生 Buffer 中的存盤陣列中的 6 - 12 這塊區域,

所以在新視圖 Buffer 中訪問元素的時候,就需要加上一個偏移 offset : position + offset 才能正確的訪問到真實陣列中的元素,這里的 offset = 6,

我們可以通過 arrayOffset() 方法獲取視圖 Buffer 中的 offset,

 public abstract int arrayOffset();

以上內容就是筆者要為大家介紹的 NIO Buffer 的頂層設計,下面我們來看下 Buffer 下具體的這些實作類,對于 Buffer 視圖相關的創建和操作,筆者會把這部分內容放到具體的 Buffer 實作類中為大家介紹,這里大家只需要理解 Buffer 視圖的概念即可~~~

5. 抽象 Buffer 的具體實作類 ByteBuffer

image

通過前面小節內容的介紹,我們知道了JDK NIO Buffer 為 Java 中每種基本型別都設計了對應的 Buffer 實作(除了 boolean 型別),

而我們本系列的主題是 Netty 網路通訊框架的原始碼決議,在網路 IO 處理中出鏡率最高的當然是 ByteBuffer,所以在下面的例子中筆者均已 ByteBuffer 作為講解主線,相信大家在理解了 ByteBuffer 的整體脈絡設計之后,在看其他基本型別的 Buffer 實作就能非常容易理解,基本上大同小異,

下面我們就來正式開始 ByteBuffer 的介紹~~~

在前邊《3. NIO Buffer 背后的存盤機制》小節的介紹中,我們知道 NIO 中的 ByteBuffer 根據其背后記憶體分配的區域不同,分為了:HeapByteBuffer,MappedByteBuffer,DirectByteBuffer 這三種型別,

而這三種型別的 ByteBuffer 肯定會有一些通用的屬性以及方法,所以 ByteBuffer 這個類被設計成了一個抽象類,用來封裝這些通用的屬性和方法作為 ByteBuffer 這個基本型別 Buffer 的頂層規范,

image

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
    // Buffer背后的陣列
    final byte[] hb;  
    // 陣列 offset,用于創建 Buffer 視圖                
    final int offset;
    // 標識 Buffer 是否是只讀的
    boolean isReadOnly;                

    ByteBuffer(int mark, int pos, int lim, int cap,  
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

    ByteBuffer(int mark, int pos, int lim, int cap) { 
        this(mark, pos, lim, cap, null, 0);
    }

}

ByteBuffer 中除了之前介紹的 Buffer 類中定義的四種重要屬性之外,又額外定義了三種屬性;

image

  1. byte[] hb:ByteBuffer 中背后依賴的用于存盤資料的陣列,該欄位只適用于 HeapByteBuffer ,而 DirectByteBuffer 和 MappedByteBuffer 背后依賴于堆外記憶體,這塊堆外記憶體的起始地址存盤于 Buffer 類中的 address 欄位中,

  2. int offset:ByteBuffer 中的記憶體偏移,用于創建新的 ByteBuffer 視圖,詳情可回看《4. Buffer 的視圖》小節,

  3. boolean isReadOnly:用于標識該 ByteBuffer 是否是只讀的,

5.1 創建具體存盤型別的 ByteBuffer

創建 DirectByteBuffer:

    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

創建 HeapByteBuffer:

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

由于 MappedByteBuffer 背后涉及到的原理比較復雜(雖然 API 簡單),所以筆者后面會有一篇專門講解 MappedByteBuffer 的文章,為了不使本文過于復雜,這里就不列出了,

5.2 將位元組陣列映射成 ByteBuffer

經過前邊的介紹,我們知道 Buffer 其實本質上就是一個陣列,在 Buffer 中封裝了一些對這個陣列的便利操作方法,既然 Buffer 已經為陣列操作提供了便利,所以大家基本都不會愿意去直接操作原生位元組陣列,這樣一來將一個原生位元組陣列映射成一個 ByteBuffer 的需求就誕生了,

    public static ByteBuffer wrap(byte[] array, int offset, int length) {
        try {
            return new HeapByteBuffer(array, offset, length);
        } catch (IllegalArgumentException x) {
            throw new IndexOutOfBoundsException();
        }
    }

ByteBuffer 中的 wrap 方法提供了這樣的映射實作,該方法可以將位元組陣列全部映射成一個 ByteBuffer,或者將位元組陣列中的部分位元組資料靈活映射成一個 ByteBuffer ,

  • byte[] array:需要映射成 ByteBuffer 的原生位元組陣列 array,

  • int offset:用于指定映射之后 Buffer 的 position, position = offset,注意此處的 offset 并不是 Buffer 視圖中的 offset ,

  • int length:用于計算映射之后 Buffer 的 limit, limit = offset + length,capacity = array,length,

映射后的 ByteBuffer 中 Mark = -1,offset = 0,此處的 offset 才是 Buffer 視圖中的 offset,

    HeapByteBuffer(byte[] buf, int off, int len) { // package-private
        super(-1, off, off + len, buf.length, buf, 0);
    }

image

以上介紹的 wrap 映射方法是根據用戶自己指定的 position 和 limit 對原生位元組陣列進行靈活映射,當然 NIO 中還提供了一個方法是直接對原生位元組陣列 array 進行默認全部映射,映射之后的Buffer :position = 0,limit = capacity = array.length,


 public static ByteBuffer wrap(byte[] array) {
        return wrap(array, 0, array.length);
    }

5.3 定義 ByteBuffer 視圖相關操作

在前邊《4. Buffer 的視圖》小節的介紹中,筆者介紹頂層抽象類 Buffer 中定義的 offset 屬性的時候,我們提到過這個 offset 屬性就是用來創建 Buffer 視圖的,在該小節中筆者其實已經將 Buffer 創建視圖的相關原理和程序已經給大家詳細的介紹完了,而視圖創建的相關操作就定義在 ByteBuffer 這個抽象類中,分別為 slice() 方法和 duplicate() 方法,

這里還是需要再次和大家強調的是我們基于原生 ByteBuffer 創建出來新的 ByteBuffer 視圖其實是 NIO 設計的一個障眼法,原生的 ByteBuffer 和它的視圖 ByteBuffer 其實本質上共用的是同一塊記憶體,對于 HeapByteBuffer 來說這塊共用的記憶體就是 JVM 堆上的一個位元組陣列,而對于 DirectByteBuffer 和 MappedByteBuffer 來說這塊共用的記憶體是堆外記憶體中的同一塊記憶體區域,

ByteBuffer 的視圖本質上也是一個 ByteBuffer,原生的 ByteBuffer 和它的視圖 ByteBuffer 擁有各自獨立的 mark,position,limit,capacity 指標,只不過背后依靠的記憶體空間是一樣的,所以在視圖 ByteBuffer 做的任何內容上的改動,原生 ByteBuffer 是看得見的,同理在原生 ByteBuffer 上做的任何內容改動,視圖 ByteBuffer 也是看得見的,它們是相互影響的,這點大家需要注意,

5.3.1 slice()

 public abstract ByteBuffer slice();

呼叫 slice() 方法創建出來的 ByteBuffer 視圖內容是從原生 ByteBufer 的當前位置 position 開始一直到 limit 之間的資料,也就是說通過 slice() 方法創建出來的視圖里邊的資料是原生 ByteBuffer 中還未處理的資料部分,

image

如上圖所屬,呼叫 slice() 方法創建出來的視圖 ByteBuffer 它的存盤陣列范圍:0 - 6,所以再此視圖下 position = 0,limit = capacity = 7,這其實是一個障眼法,真實情況是新的視圖 ByteBuffer 其實是復用原生 ByteBuffer 中的存盤陣列中的 6 - 12 這塊區域(未處理的資料部分),

所以在視圖 ByteBuffer 中訪問元素的時候,就需要 position + offset 來訪問才能正確的訪問到真實陣列中的元素,這里的 offset = 6,

下面是 HeapByteBuffer 中關于 slice() 方法的具體實作:

class HeapByteBuffer extends ByteBuffer {

    public ByteBuffer slice() {
        return new HeapByteBuffer(hb,
                                        -1,
                                        0,
                                        this.remaining(),
                                        this.remaining(),
                                        this.position() + offset);
    }

}

5.3.2 duplicate()

而由 duplicate() 方法創建出來的視圖相當于就是完全復刻原生 ByteBuffer,它們的 offset,mark,position,limit,capacity 變數的值全部是一樣的,這里需要注意雖然值是一樣的,但是它們各自之間是相互獨立的,用于對同一位元組陣列做不同的邏輯處理,

public abstract ByteBuffer duplicate();

image

下面是 HeapByteBuffer 中關于 duplicate() 方法的具體實作:

class HeapByteBuffer extends ByteBuffer {

    public ByteBuffer duplicate() {
        return new HeapByteBuffer(hb,
                                        this.markValue(),
                                        this.position(),
                                        this.limit(),
                                        this.capacity(),
                                        offset);
    }

}

5.3.3 asReadOnlyBuffer()

public abstract ByteBuffer asReadOnlyBuffer();

通過 asReadOnlyBuffer() 方法我們可以基于原生 ByteBuffer 創建出一個只讀視圖,對于只讀視圖的 ByteBuffer 只能讀取不能寫入,對只讀視圖進行寫入操作會拋出 ReadOnlyBufferException 例外,

下面是 HeapByteBuffer 中關于 asReadOnlyBuffer() 方法的具體實作:

class HeapByteBuffer extends ByteBuffer {

   public ByteBuffer asReadOnlyBuffer() {

        return new HeapByteBufferR(hb,
                                     this.markValue(),
                                     this.position(),
                                     this.limit(),
                                     this.capacity(),
                                     offset);
    }

}

NIO 中專門設計了一個只讀 ByteBufferR 視圖類,它的 isReadOnly 屬性為 true,

class HeapByteBufferR extends HeapByteBuffer {

   protected HeapByteBufferR(byte[] buf,
                                   int mark, int pos, int lim, int cap,
                                   int off)
    {
        super(buf, mark, pos, lim, cap, off);
        this.isReadOnly = true;

    }

}

5.4 定義 ByteBuffer 讀寫相關操作

ByteBuffer 中定義了四種針對 Buffer 讀寫的基本操作方法,由于 ByteBuffer 這個抽象類是一個頂層設計類,只是規范定義了針對 ByteBuffer 操作的基本行為,它并不負責具體資料的存盤,所以這四種基本操作方法會在其具體的實作類中實作,這個我們后面會一一介紹,這里只是向大家展示 NIO 針對 ByteBuffer 的頂層設計,


 //從ByteBuffer中讀取一個位元組的資料,隨后position的位置向后移動一位
 public abstract byte get();

 //向ByteBuffer中寫入一個位元組的資料,隨后position的位置向后移動一位
 public abstract ByteBuffer put(byte b);

 //按照指定index從ByteBuffer中讀取一個位元組的資料,position的位置保持不變
 public abstract byte get(int index);

 //按照指定index向ByteBuffer中寫入一個位元組的資料,position的位置保持不變
 public abstract ByteBuffer put(int index, byte b);

ByteBuffer 類中除了定義了這四種基本的讀寫操作,還依據這四個基本操作衍生出了幾種通用操作,下面筆者來為大家介紹下這幾種通用的操作:

1. 將 ByteBuffer中的位元組轉移到指定的位元組陣列 dst 中

  • offset:dst 陣列存放轉移資料的起始位置,

  • length:從 ByteBuffer 中轉移位元組數,

   public ByteBuffer get(byte[] dst, int offset, int length) {
         //檢查指定index的邊界,確保不能越界
        checkBounds(offset, length, dst.length);
        //檢查ByteBuffer是否有足夠的轉移位元組
        if (length > remaining())
            throw new BufferUnderflowException();
        int end = offset + length;
        // 從當前ByteBuffer中position開始轉移length個位元組 到dst陣列中
        for (int i = offset; i < end; i++)
            dst[i] = get();
        return this;
    }

2. 將指定位元組陣列 src 中的資料轉移到 ByteBuffer中

  • offset:從位元組陣列中的 offset 位置處開始轉移,

  • length:向 ByteBuffer轉移位元組個數,

    public ByteBuffer put(byte[] src, int offset, int length) {
        //檢查指定index的邊界,確保不能越界
        checkBounds(offset, length, src.length);
        //檢查ByteBuffer是否能夠容納得下
        if (length > remaining())
            throw new BufferOverflowException();
        int end = offset + length;
        //從位元組陣列的offset處,轉移length個位元組到ByteBuffer中
        for (int i = offset; i < end; i++)
            this.put(src[i]);
        return this;
    }

在為大家介紹完 ByteBuffer 的抽象設計之后,筆者相信大家現在已經對 NIO 的 ByteBuffer 有了一個整體上的認識,

接下來的內容,筆者將會為大家詳細介紹之前多次提到的這三種 ByteBuffer 的具體實作型別:

image

讓我們從 HeapByteBuffer 開始,HeapByteBuffer 的相關實作最簡單最容易理解的,我們會在 HeapByteBuffer 的介紹中,詳細介紹 Buffer 操作的實作,理解了 HeapByteBuffer 的相關實作,剩下的 Buffer 實作類就更容易理解了,都是大同小異,

6. HeapByteBuffer 的相關實作

image

經過前邊幾個小節的介紹,大家應該對 HeapByteBuffer 的結構很清楚了,HeapByteBuffer 背后主要是依賴于 JVM 堆中的一個位元組陣列 byte[] hb,

在這個 JVM 堆中的位元組陣列的基礎上,實作了在 Buffer 類和 ByteBuffer類中定義的抽象方法,

6.1 HeapByteBuffer 的構造

在 HeapByteBuffer 的構造程序中首先就會根據用戶指定的 Buffer 容量 cap,在 JVM 堆中創建一個容量大小為 cap 的位元組陣列出來作為 HeapByteBuffer 底層存盤資料的容器,


class HeapByteBuffer extends ByteBuffer {

   HeapByteBuffer(int cap, int lim) {      
        super(-1, 0, lim, cap, new byte[cap], 0);
   }

}

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {

    ByteBuffer(int mark, int pos, int lim, int cap,   
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

}

還有我們《5.2 將位元組陣列映射成 ByteBuffer》小節介紹的用于將原生位元組陣列映射成 ByteBuffer 的 wrap 方法中用到的建構式:

    public static ByteBuffer wrap(byte[] array, int offset, int length) {
        try {
            return new HeapByteBuffer(array, offset, length);
        } catch (IllegalArgumentException x) {
            throw new IndexOutOfBoundsException();
        }
    }
    HeapByteBuffer(byte[] buf, int off, int len) { 
        super(-1, off, off + len, buf.length, buf, 0);
    }

以及我們在《5.3 定義 ByteBuffer 視圖相關操作》小節介紹的用于創建 ByteBuffer 視圖的兩個方法 slice() 和 duplicate() 方法中用到的建構式:

   protected HeapByteBuffer(byte[] buf,
                                   int mark, int pos, int lim, int cap,
                                   int off)
    {
        super(mark, pos, lim, cap, buf, off);
    }

6.2 從 HeapByteBuffer 中讀取位元組

6.2.1 根據 position 的位置讀取一個位元組

  • 首先會通過《2.2.2 獲取 Buffer 下一個可讀取位置》小節介紹的 nextGetIndex() 方法獲取當前 HeapByteBuffer 中的 position 位置,根據 position 的位置讀取位元組,

  • 為了兼容 Buffer 視圖的相關操作,定位讀取位置 position 都會加上 offset,原生 Buffer 中的 offset = 0,

  • 通過 position + offset 確定好訪問 Index 之后,就是陣列的普通操作了,直接通過這個 Index 從 hb 位元組陣列中獲取位元組,隨后 Buffer 中的 position 向后移動一個位置,

class HeapByteBuffer extends ByteBuffer {

    protected final byte[] hb;
    protected final int offset;

    public byte get() {
        return hb[ix(nextGetIndex())];
    }
   // 確定訪問 index 
   protected int ix(int i) {
        return i + offset;
    }
}

6.2.2 根據指定的 Index 讀取一個位元組

我們除了可以根據 Buffer 的 position 位置讀取位元組,還可以指定具體的 Index 來從 Buffer 中讀取位元組:

  • 檢查 Index 是否超出 Buffer 的邊界范圍,通過檢查之后 Index + offset 確定讀取位置,

注意這個方法讀取位元組之后,position 的位置是不會改變的,

public byte get(int i) {
        return hb[ix(checkIndex(i))];
    }

6.2.3 將 HeapByteBuffer 中的位元組轉移到指定的位元組陣列中

這個方法其實筆者在《5.4 定義 ByteBuffer 讀寫相關操作》小節中介紹 ByteBuffer 的頂層規范設計時已經提到過了,由于 ByteBuffer 只是一個抽象類負責頂層操作規范的定義,本身并不具備具體存盤資料的能力,所以在 ByteBuffer 中只是提供了一個通用的實作,ByteBuffer 中的實作是通過在一個for () {....} 回圈中不停的根據原生 Buffer 中的 position 指標(前邊介紹的 get() 方法)遍歷底層陣列并一個一個的拷貝到目標位元組陣列 dst 中,這樣的拷貝操作無疑是效率低下的,

而在 HeapByteBuffer 這個具體的 ByteBuffer 實作類中已經定義了具體的存盤方式,所以根據具體的存盤方式能夠做一下拷貝上的優化:

    public ByteBuffer get(byte[] dst, int offset, int length) {
        checkBounds(offset, length, dst.length);
        if (length > remaining())
            throw new BufferUnderflowException();
        System.arraycopy(hb, ix(position()), dst, offset, length);
        position(position() + length);
        return this;
    }

HeapByteBuffer 中對于拷貝位元組陣列中的資料使用了 System.arraycopy 方法,該方法在 JVM 中是一個 intrinsic method,是經過 JVM 編譯器特殊優化的,比通過 JNI 呼叫 native 方法的性能還要高,

利用 System.arraycopy 方法將 HeapByteBuffer 中的位元組資料從 position 開始,拷貝 length 個位元組到目標位元組陣列 dst 中,

6.3 向HeapByteBuffer中寫入位元組

6.3.1 根據 position 的位置寫入一個位元組

  • 首先會通過《2.2.3 獲取 Buffer 下一個可寫入位置》小節中介紹的 nextPutIndex() 方法獲取當前 HeapByteBuffer 中的 position 位置,根據position的位置寫入位元組,

  • 通過 position + offset 定位到寫入位置 Index,然后向 HeapByteBuffer 底層的位元組陣列 hb 直接寫入位元組資料,隨后 position 向后移動一個位置,


    public ByteBuffer put(byte x) {
        hb[ix(nextPutIndex())] = x;
        return this;
    }

   protected int ix(int i) {
        return i + offset;
    }

6.3.2 根據指定的 Index 寫入一個位元組

注意通過這個方法根據指定 Index 寫入位元組之后,position 的位置是不會改變的,

    public ByteBuffer put(int i, byte x) {
        hb[ix(checkIndex(i))] = x;
        return this;
    }

6.3.3 將指定位元組陣列轉移到 HeapByteBuffer 中

同理和《6.2.3 將 HeapByteBuffer 中的位元組轉移到指定的位元組陣列中》小節中介紹的相關方法一樣,HeapByteBuffer 也是采用了 JVM 中的 System.arraycopy 方法(intrinsic method )從而更加高效地進行位元組陣列的拷貝操作,

從位元組陣列 src 中的 offset 位置開始拷貝 length 個位元組到 HeapByteBuffer中


   public ByteBuffer put(byte[] src, int offset, int length) {

        checkBounds(offset, length, src.length);
        if (length > remaining())
            throw new BufferOverflowException();
        System.arraycopy(src, offset, hb, ix(position()), length);
        position(position() + length);
        return this;
    }


HeapByteBuffer 背后依靠的位元組陣列存盤的是一個一個的位元組,以上操作全部針對的是單個位元組來的,所以并不需要考慮位元組序的影響,但是如果我們想從 HeapByteBuffer 中讀取寫入一個 int 或者一個 double 型別的資料,那么我們就需要考慮位元組序的問題了,

在介紹如何從 HeapByteBuffer 中讀取或者寫入一個指定基本型別資料之前,筆者先來為大家介紹一下:

  • 到底什么是位元組序?

  • 為什么會有位元組序的存在?

  • 位元組序對 Buffer 的操作會有什么影響?

7. 位元組序

談起位元組序來大家可能都會有這樣的感觸就是記了忘,忘了記,記了又忘,所以為了讓大家清晰地理解位元組序并且深深地刻入腦海中,筆者挖空心思終于想出了一個生活中的例子來為大家說明位元組序,

筆者平時有健身的習慣,已經堅持擼鐵四年多了,為了給身體補充蛋白質增加肌肉量,每天打底至少 15 個雞蛋,所以剝雞蛋就成為了筆者日常的一個重要任務,

image

那么問題來了,在我們剝雞蛋的時候,我們到底是該從雞蛋大的一端剝起還是從雞蛋小的的一端剝起呢?

這還真是一個問題,有的人喜歡從小端剝起,但是筆者習慣從大端開始剝起,于是就有了大端-小端的剝法,

image

既然剝雞蛋有大端-小端的分歧在,那么在計算機網路傳輸資料時也會存在這樣的問題,計算機中是怎么扯出大端-小端的分歧呢?請耐心聽筆者接著講下去~~

我們都知道在計算機中存盤資料,字符編碼以及網路中傳輸資料時都是通過一個 bit 一個 bit 組成的 010101 這樣的二進制形式傳輸存盤的,由于本系列的主題是關于網路 IO 的處理,所以筆者這里以網路傳輸中的位元組序舉例:

比如現在我們要傳輸一個 int 型的整數 5674 到對端主機中,int 型的變數 5674 對應的二進制是 1011000101010 ,如下圖所示:

image

剝雞蛋的分歧在于是從大的一端開始剝還是從小的一端開始剝,從大的一端開始剝我們叫做大端剝法,而從小的一端開始剝我們叫做小端剝法,

同樣的道理,我們在網路傳輸二進制資料的時候也有分歧:我們是從二進制的高位開始傳輸呢(圖中綠色區域)?還是從二進制的低位開始傳輸呢(圖中黃色區域)?

如果我們從二進制資料的高位(類比雞蛋的大端)開始傳輸我們就叫大端位元組序,如果我們從二進制的低位(類比雞蛋的小端)開始傳輸就叫小端位元組序

網路協議采用的是大端位元組序傳輸

好了,現在關于網路傳輸位元組的順序問題,我們闡述清楚了,那么接下來我們看下當網路位元組傳輸到對端時,對端如何接收?

當網路位元組按照大端位元組序傳輸到對端計算機時,對端會在作業系統的堆中開辟一塊記憶體用來接收網路位元組,而在作業系統的虛擬記憶體布局中,堆空間的地址增長方向是從低地址向高地址增長,而堆疊空間的地址是從高地址向低地址增長,

image

現在我們假設如果當網路位元組傳輸到對端計算機中,我們在對端使用 HeapByteBuffer 去接收網路位元組(這里只是假設,實踐上都是使用 DirectByteBuffer ),經過前邊內容的介紹我們知道,HeapByteBuffer 背后其實依靠一個位元組陣列來存盤位元組,如圖中所示,位元組陣列從索引 0 開始到索引 6 它們在記憶體中的地址是從低地址到高地址,

理解了這些,下面我們就來看下位元組在不同位元組序下是如何接收存盤的,

7.1 大端位元組序

image

如圖中所示,在大端位元組序下 int 型變數 5674 它的位元組高位被存盤在了位元組陣列中的低地址中,位元組的低位被存盤在位元組陣列的高地址中,這就是大端位元組序,也是比較符合人類的直觀感受,

7.2 小端位元組序

image

然而在小端位元組序下,int 型變數 5674 它的位元組高位被存盤在了位元組陣列中的高地址中,位元組的低位被存盤在位元組陣列的低地址中,這就是小端位元組序,正好和正常人類直觀感受是相反的,


到現在,我想大家應該最起碼從概念上知道什么是大端位元組序?什么是小端位元組序了吧?

下面筆者在帶大家到實戰中,再去體驗一把大端位元組序和小端位元組序的不同,徹底讓大家理解清楚,

8. 向 HeapByteBuffer 中寫入指定基本型別

HeapByteBuffer 背后是一個在 JVM 堆中開辟的一個位元組陣列,里邊存放的是一個一個的位元組,當我們以單個位元組的形式操作 HeapByteBuffer 的時候并沒有什么問題,可是當我們向 HeapByteBuffer 寫入一個指定的基本型別資料時,比如寫入一個 int 型 (占用 4 個位元組),寫入一個 double 型 (占用 8 個位元組),就必須要考慮位元組序的問題了,

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {

   boolean bigEndian = true;
   boolean nativeByteOrder = (Bits.byteOrder() == ByteOrder.BIG_ENDIAN);

}

我們可以強制網路協議傳輸使用大端位元組序,但是我們無法強制主機中采用的位元組序,所以我們需要經常在網路 IO 場景下做一些位元組序的轉換作業,

JDK NIO ByteBuffer 默認的位元組序為大端模式,我們可以通過 NIO 提供的操作類 Bits 獲取主機位元組序 Bits.byteOrder(),或者直接獲取 NIO ByteBuffer 中的 nativeByteOrder 欄位判斷主機位元組序:true 表示主機位元組序為大端模式,false 表示主機位元組序為小端模式,

當然我們也可以通過 ByteBuffer 中的 order 方法來指定我們想要的位元組序:


    public final ByteBuffer order(ByteOrder bo) {
        bigEndian = (bo == ByteOrder.BIG_ENDIAN);
        nativeByteOrder =
            (bigEndian == (Bits.byteOrder() == ByteOrder.BIG_ENDIAN));
        return this;
    }

下面筆者就帶大家分別從大端模式和小端模式下來看一下如何向 HeapByteBuffer 寫入一個指定基本型別的資料,我們以 int 型資料舉例,假設要寫入的 int 值 為 5674,

8.1 大端位元組序

class HeapByteBuffer extends ByteBuffer {

    public ByteBuffer putInt(int x) {
        Bits.putInt(this, ix(nextPutIndex(4)), x, bigEndian);
        return this;
    }
}

首先我們會獲取當前 HeapByteBuffer 的寫入位置 position,因為我們需要寫入的是一個 int 型的資料,所以當寫入完畢之后 position 的位置需要向后移動 4 位,nextPutIndex 方法的邏輯筆者在之前的內容中已經詳細介紹過了,這里不在贅述,

class Bits { 

    static void putInt(ByteBuffer bb, int bi, int x, boolean bigEndian) {
        if (bigEndian)
            // 采用大端位元組序寫入 int 資料
            putIntB(bb, bi, x);
        else
            // 采用小端位元組序寫入 int 資料
            putIntL(bb, bi, x);
    }

    static void putIntB(ByteBuffer bb, int bi, int x) {
        bb._put(bi    , int3(x));
        bb._put(bi + 1, int2(x));
        bb._put(bi + 2, int1(x));
        bb._put(bi + 3, int0(x));
    }
}

大家看到了嗎,這里就是按照我們之前介紹的大端位元組序,從 int 值 5674 的二進制高位位元組到低位位元組依次寫入 HeapByteBuffer中位元組陣列的低地址中,

這里的 int3(x) 方法就是負責獲取寫入資料 x 的最高位位元組,并將最高位位元組(下圖中綠色部分)寫入位元組陣列中的低地址中(下圖中對應綠色部分),

同理 int2(x),int1(x),int0(x) 方法依次獲取 x 的次高位位元組,依次寫入位元組陣列中的低地址中,

image

那么我們如何依次獲得一個 int 型資料的高位位元組呢?大家接著跟著筆者往下走~

8.1.1 int3(x) 獲取 int 型最高位位元組

class Bits { 

 private static byte int3(int x) { return (byte)(x >> 24); }

}

image

8.1.2 int2(x) 獲取 int 型次高位位元組

class Bits { 

 private static byte int2(int x) { return (byte)(x >> 16); }

}

image

8.1.3 int1(x) 獲取 int 型第三高位位元組

class Bits { 

 private static byte int1(int x) { return (byte)(x >> 8); }

}

image

8.1.4 int0(x) 獲取 int 型最低位位元組

class Bits { 

 private static byte int0(int x) { return (byte)(x      ); }

}

image

最終 int 型變數 5764 按照大端位元組序寫入到 HeapByteBuffer之后的位元組陣列結構如下:

image

8.2 小端位元組序

在我們徹底理解了大端位元組序的操作之后,小端位元組序的相關操作就很好理解了,

    static void putIntL(ByteBuffer bb, int bi, int x) {
        bb._put(bi + 3, int3(x));
        bb._put(bi + 2, int2(x));
        bb._put(bi + 1, int1(x));
        bb._put(bi    , int0(x));
    }

根據我們之前介紹的小端位元組序的定義,在小端模式下二進制資料的高位是存盤在位元組陣列中的高地址中,二進制資料的低位是存盤在位元組陣列中的低地址中,

image

9. 從 HeapByteBuffer 中讀取指定基本型別

當我們清楚了在不同的位元組序下如何向 HeapByteBuffer 中寫入指定基本型別資料的程序之后,那么在不同位元組序下向 HeapByteBuffer 讀取指定基本型別資料的程序,我想大家就能很容易理解了,

我們還是以 int 型資料舉例,假設要從 HeapByteBuffer 中讀取一個 int 型的資料,

首先我們還是獲取當前 HeapByteBuffer 中的讀取位置 position,從 position 位置開始讀取四個位元組出來,然后通過這四個位元組組裝成一個 int 資料回傳,

class HeapByteBuffer extends ByteBuffer {

    public int getInt() {
        return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
    }

}

class Bits { 

  static int getInt(ByteBuffer bb, int bi, boolean bigEndian) {
        return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ;
    }

}

我們還是先來介紹大端模式下的讀取程序:

9.1 大端位元組序

class Bits { 

    static int getIntB(ByteBuffer bb, int bi) {
        return makeInt(bb._get(bi    ),
                       bb._get(bi + 1),
                       bb._get(bi + 2),
                       bb._get(bi + 3));
    }

}

image

由于在大端模式下,二進制資料的高位是存放于位元組陣列中的低地址中,我們需要從位元組陣列中的低地址中依次讀取二進制資料的高位出來,

然后我們從高位開始依次組裝 int 型資料,正好和寫入程序相反,

    static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
        return (((b3       ) << 24) |
                ((b2 & 0xff) << 16) |
                ((b1 & 0xff) <<  8) |
                ((b0 & 0xff)      ));
    }

9.2 小端位元組序

image

class Bits { 

    static int getIntL(ByteBuffer bb, int bi) {
        return makeInt(bb._get(bi + 3),
                       bb._get(bi + 2),
                       bb._get(bi + 1),
                       bb._get(bi    ));
    }

}

而在小端模式下,我們則需要先從位元組陣列中的高地址中將二進制資料的高位依次讀取出來,然后在從高位開始依次組裝 int 型資料,

在筆者介紹完了關于 int 資料的讀寫程序之后,相信大家可以很輕松的理解其他基本型別在不同位元組序下的讀寫操作程序了,

10. 將 HeapByteBuffer 轉換成指定基本型別的 Buffer

在《2. NIO 對 Buffer 的頂層抽象》小節一開始就介紹到,NIO 其實為我們提供了多種基本型別的 Buffer 實作,

image

NIO 允許我們將 ByteBuffer 轉換成任意一種基本型別的 Buffer,這里我們以轉換 IntBuffer 為例說明:

class HeapByteBuffer extends ByteBuffer {

    public IntBuffer asIntBuffer() {
        int size = this.remaining() >> 2;
        int off = offset + position();
        return (bigEndian
                ? (IntBuffer)(new ByteBufferAsIntBufferB(this,
                                                             -1,
                                                             0,
                                                             size,
                                                             size,
                                                             off))
                : (IntBuffer)(new ByteBufferAsIntBufferL(this,
                                                             -1,
                                                             0,
                                                             size,
                                                             size,
                                                             off)));
    }

}

IntBuffer 底層其實依托了一個 ByteBuffer,當我們向 IntBuffer 讀取一個 int 資料時,其實是從底層依托的這個 ByteBuffer 中讀取 4 個位元組出來然后組裝成 int 資料回傳,

class ByteBufferAsIntBufferB extends IntBuffer {

    protected final ByteBuffer bb;

    public int get() {
        return Bits.getIntB(bb, ix(nextGetIndex()));
    }
}
class Bits { 

    static int getIntB(ByteBuffer bb, int bi) {
        return makeInt(bb._get(bi    ),
                       bb._get(bi + 1),
                       bb._get(bi + 2),
                       bb._get(bi + 3));
    }

    static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
        return (((b3       ) << 24) |
                ((b2 & 0xff) << 16) |
                ((b1 & 0xff) <<  8) |
                ((b0 & 0xff)      ));
    }

}

同理,我們向 IntBuffer 中寫入一個int資料時,其實是想底層依托的這個 ByteBuffer 寫入 4 個位元組,

IntBuffer 底層依托的這個 ByteBuffer ,會根據位元組序的不同分為:ByteBufferAsIntBufferB(大端實作)和 ByteBufferAsIntBufferL(小端實作),

在我們詳細介紹完 HeapByteBuffer 的實作之后,筆者這里就不在為大家詳細介紹 ByteBufferAsIntBufferB 和 ByteBufferAsIntBufferL 了,操作全部是一樣的,感興趣的大家可以自行查看一下,

總結

image

本文我們以 JDK NIO Buffer 中最簡單的一個實作類 HeapByteBuffer 為主線從 NIO 對 Buffer 的頂層抽象設計開始從整體上為大家介紹了 Buffer 的設計,

在這個程序中,我們可以體會到 NIO 對 Buffer 的設計還是比較復雜的,尤其是我們針對裸 NIO 進行編程的時候會有非常多的反人類操作,一不小心就會出錯,

比如:用于 Buffer 讀模式切換 flip() 方法,寫模式切換的 clear() 方法和 compact() 方法以及用于重新處理 Buffer 中資料的 rewind() 方法,在我們使用這些方法處理位元組資料的時候需要時刻清楚 Buffer 中的資料分布情況,一不小心就會造成資料的覆寫和丟失,

后面我們又介紹了 Buffer 中視圖的概念和相關操作 slice() 方法和 duplicate() 方法,以及關于視圖 Buffer 和原生 Buffer 之間的區別和聯系,

我們以 HeapByteBuffer 為例,介紹了 NIO Buffer 相關頂層抽象方法的實作,并再次基礎上更進一步介紹了在不同位元組序下 ByteBuffer 相關的讀取寫入操作的詳細程序,

最后我們介紹了 ByteBuffer 與相關指定基本型別 Buffer (比如 IntBuffer,LongBuffer)在不同位元組序下的轉換,

另外我們還穿插介紹了:到底什么是位元組序? 為什么會有位元組序的存在? 位元組序對 Buffer 的操作會有什么影響?

因為 HeapByteBuffer 足夠簡單,所以利用它能夠把整個 NIO 對 Buffer 的設計與實作串聯起來,但是根據 Buffer 背后的存盤機制不同,還有 DirectByteBuffer 和 MappedByteBuffer ,它們的 API 在使用上基本和 HeapByteBuffer 是一致的,但是它們背后涉及到的原理卻是非常復雜的(尤其是 MappedByteBuffer),

所以筆者后面會單獨寫兩篇文章來詳細分別為大家介紹 DirectByteBuffer 和 MappedByteBuffer 背后涉及到的復雜原理,目的是讓大家不僅會使用而且還要把它們背后涉及到的復雜原理徹底搞透徹弄清楚,要知其然并且還要知其所以然~~~

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/501576.html

標籤:Java

上一篇:binary與進制轉換

下一篇:大頂堆的實作(基于陣列存盤的完全二叉樹)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more