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语言的字节序
posted @ 2020-02-17 21:37  -零  阅读(8978)  评论(0编辑  收藏  举报