go語言字節序 encoding/binary


 

字節序

字節序就是多字節數據類型 (int, float 等)在內存中的存儲順序。在網絡傳輸中基於文本類型的協議(比如 JSON)和二進制協議都是字節通信,是采用字節序進行數據包的處理。

字節序可分為大端序,低地址端存放高位字節;小端序與之相反,低地址端存放低位字節。

在計算機內部,小端序被廣泛應用於現代性 CPU 內部存儲數據;而在其他場景譬如網絡傳輸和文件存儲使用大端序。

在網絡協議層操作二進制數字時約定使用大端序,大端序是網絡字節傳輸采用的方式。因為大端序最高有效字節排在首位(低地址端存放高位字節),能夠按照字典排序,所以我們能夠比較二進制編碼后數字的每個字節。

 

固定長度編碼 Fixed-length encoding

Go 中有多種類型的整型, int8, int16, int32 和 int64 ,分別使用 1, 3, 4, 8 個字節表示,我們稱之為固定長度類型 (fixed-length types)。

Go 處理固定長度字節序

Go中處理大小端序的代碼位於 encoding/binary ,包中的全局變量BigEndian用於操作大端序數據,LittleEndian用於操作小端序數據,這兩個變量所對應的數據類型都實行了ByteOrder接口:

type ByteOrder interface {
    Uint16([]byte) uint16
    Uint32([]byte) uint32
    Uint64([]byte) uint64
    PutUint16([]byte, uint16)
    PutUint32([]byte, uint32)
    PutUint64([]byte, uint64)
    String() string
}

其中,前三個方法用於讀取數據,后三個方法用於寫入數據。

上面的方法操作的都是無符號整型,如果我們要操作有符號整型的時候怎么辦呢?很簡單,強制轉換就可以了,比如這樣:

func PutInt32(b []byte, v int32) {
        binary.BigEndian.PutUint32(b, uint32(v))
}

BigEndian 和 LittleEndian 實現了 ByteOrder 接口

//BigEndian is the big-endian implementation of ByteOrder.
var BigEndian bigEndian

//LittleEndian is the little-endian implementation of ByteOrder.
var LittleEndian littleEndian

舉個例子,把固定長度的數字寫入字節切片 (byte slice),然后從字節切片中讀取到並賦值給一個變量:

// write
v := uint32(500)
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, v)

// read
x := binary.BigEndian.Uint32(buf)

 

在這里,需要注意的是使用 put 寫時要保證足夠的切片長度,另外如果從流 (stream) 讀取時要使用 io.ReadFull 確保讀取的是原始字節,而不是使用特定的 read Buffer 編碼處理過的字節。

go處理大端序和小端序的方式:

package main

import (
    "encoding/binary"
    "fmt"
    "unsafe"
)

const INT_SIZE int = int(unsafe.Sizeof(0))

//判斷我們系統中的字節序類型
func systemEdian() {
    var i int = 0x1
    bs := (*[INT_SIZE]byte)(unsafe.Pointer(&i))
    if bs[0] == 0 {
        fmt.Println("system edian is little endian")
    } else {
        fmt.Println("system edian is big endian")
    }
}

func testBigEndian() {

    // 0000 0000 0000 0000   0000 0001 1111 1111
    var testInt int32 = 256
    fmt.Printf("%d use big endian: \n", testInt)
    var testBytes []byte = make([]byte, 4)
    binary.BigEndian.PutUint32(testBytes, uint32(testInt))
    fmt.Println("int32 to bytes:", testBytes)

    convInt := binary.BigEndian.Uint32(testBytes)
    fmt.Printf("bytes to int32: %d\n\n", convInt)
}

func testLittleEndian() {

    // 0000 0000 0000 0000   0000 0001 1111 1111
    var testInt int32 = 256
    fmt.Printf("%d use little endian: \n", testInt)
    var testBytes []byte = make([]byte, 4)
    binary.LittleEndian.PutUint32(testBytes, uint32(testInt))
    fmt.Println("int32 to bytes:", testBytes)

    convInt := binary.LittleEndian.Uint32(testBytes)
    fmt.Printf("bytes to int32: %d\n\n", convInt)
}

func main() {
    systemEdian()
    fmt.Println("")
    testBigEndian()
    testLittleEndian()
}

Go 處理固定長度流 (stream processing)

binary package 提供了內置的讀寫固定長度值的流 (stream):

func Read(r io.Reader, order ByteOrder, data interface{}) error
func Write(w io.Writer, order ByteOrder, data interface{}) error

Read 通過指定類型的字節序把字節解碼 (decode) 到 data 變量中。解碼布爾類型時,0 字節 (也就是 []byte{0x00}) 為 false, 其他都為 true

package main
import (
    "bytes"
    "encoding/binary"
    "fmt"
)
func main() {
    var(
        piVar float64
        boolVar bool
    )
    piByte := []byte{0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40}
    boolByte := []byte{0x00}
    piBuffer := bytes.NewReader(piByte)
    boolBuffer := bytes.NewReader(boolByte)
    binary.Read(piBuffer, binary.LittleEndian, &piVar)
    binary.Read(boolBuffer, binary.LittleEndian, & boolByte)
    fmt.Println("pi", piVar)     // pi 3.141592653589793
    fmt.Println("bool", boolVar) // bool false
}

Write 是 Read 的逆過程,直接看例子比較直觀:
package main
import (
    "bytes"
    "encoding/binary"
    "fmt"
    "math"
)
func main() {
    buf := new(bytes.Buffer)
    var pi float64 = math.Pi
    err := binary.Write(buf, binary.LittleEndian, pi)
    if err != nil {
        fmt.Println("binary.Write failed:", err)
    }
    fmt.Printf("% x", buf.Bytes()) // 18 2d 44 54 fb 21 09 40
}
在實際編碼中,面對復雜的數據結構,可考慮使用更標准化高效的協議,比如 Protocol Buffer。

 

可變長度編碼 Variable-length encoding

固定長度編碼對存儲空間的占用不靈活,比如一個 int64 類型范圍內的值,當值較小時就會產生比較多的 0 字節無效位,直至達到 64 位。使用可變長度編碼可限制這種空間浪費。

原理
可變長度編碼理想情況下值小的數字占用的空間比值大的數字少,有多種實現方案,Go Binary 實現方式和 protocol buffer encoding 一致,具體原理如下:

每個字節的首位存放一個標識位,用以表明是否還有跟多字節要讀取及剩下的七位是否真正存儲數據。標識位分別為 0 和 1

1 表示還要繼續讀取該字節后面的字節
0 表示停止讀取該字節后面的字節
一旦所有讀取完所有的字節,每個字節串聯的結果就是最后的值。舉例說明:數字 53 用二進制表示為 110101 ,需要六位存儲,除了標識位還剩余七位,所以在標識位后補 0 湊夠七位,最終結果為 00110101。標識位 0 表明所在字節后面沒有字節可讀了,標識位后面的 0110101 保存了值。

再來一個大點的數字舉例,1732 二進制使用 11011000100 表示,實際上只需使用 11 位的空間存儲,除了標識位每個字節只能保存 7 位,所以數字 1732 需要兩個字節存儲。第一個字節使用 1 表示所在字節后面還有字節,第二個字節使用 0 表示所在字節后面沒有字節,最終結果為:10001101 01000100

go處理可變長度的字節序
函數 putVarint() 和 putUvarint() 把可變長值寫到內存字節切片中

func PutVarint(buf []byte, x int64) int
func PutUvarint(buf []byte, x uint64) int

 

這兩個函數把 x 編碼到 buf 中並返回寫入 buf 中字節的長度,如果 buf 初始化長度過小(比 x 還要小)函數就會 panic , 建議使用 binary.MaxVarintLen64 常量確保出現 panic 的情況。

package main
import (
    "encoding/binary"
    "fmt"
)
func main() {
    buf := make([]byte, binary.MaxVarintLen64)
    for _, x := range []int64{-65, 1, 2, 127, 128, 255, 256} {
        n := binary.PutVarint(buf, x)
        fmt.Print(x, "輸出的可變長度為:", n, ",十六進制為:")
        fmt.Printf("%x\n", buf[:n])
    }
}
-65輸出的可變長度為:2,十六進制為:8101
1輸出的可變長度為:1,十六進制為:02
2輸出的可變長度為:1,十六進制為:04
127輸出的可變長度為:2,十六進制為:fe01
128輸出的可變長度為:2,十六進制為:8002
255輸出的可變長度為:2,十六進制為:fe03
256輸出的可變長度為:2,十六進制為:8004

函數 Varint() 和 Uvarint() 把字節碼轉為十進制。 

func Varint(buf []byte) (int64, int)
func Uvarint(buf []byte) (uint64, int)
package main
import (
    "encoding/binary"
    "fmt"
)
func main() {
    inputs := [][]byte{
        []byte{0x81, 0x01},
        []byte{0x7f},
        []byte{0x03},
        []byte{0x01},
        []byte{0x00},
        []byte{0x02},
        []byte{0x04},
        []byte{0x7e},
        []byte{0x80, 0x01},
    }
    for _, b := range inputs {
        x, n := binary.Varint(b)
        if n != len(b) {
            fmt.Println("Varint did not consume all of in")
        }
        fmt.Println(x) // -65,-64,-2,-1,0,1,2,63,64,
    }
}

go處理可變長度字節流數據 Decoding from a byte stream

binary 包提供了兩個函數從字節流中讀取到可變長度值。

func ReadVarint(r io.ByteReader) (int64, error)
func ReadUvarint(r io.ByteReader) (uint64, error)

 

 

總結

二進制協議 (Binary protocol) 高效地在底層處理數據通信,字節序決定字節輸出的順序、通過可變長度編碼壓縮數據存儲空間。理解了 Encoding/binary 庫之后,我們可以繼續深入理解當前一些主流的二進制協議。

 

全文整理於:

字節序及 Go encoding/binary 庫:https://zhuanlan.zhihu.com/p/35326716https://www.jianshu.com/p/1deed9012440

 

go語言的字節序


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM