zoukankan      html  css  js  c++  java
  • 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: 
    ", 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
    
    ", convInt)
    }
    
    func testLittleEndian() {
    
        // 0000 0000 0000 0000   0000 0001 1111 1111
        var testInt int32 = 256
        fmt.Printf("%d use little endian: 
    ", 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
    
    ", 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
    ", 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语言的字节序
  • 相关阅读:
    FineReport: 参数为空选出全部值(按条件查询,空条件时直接过滤,不进行查询。。)
    BarManager菜单栏加载与菜单项点击事件详解|devExpress教程
    BarManager菜单栏加载与菜单项点击事件详解|devExpress教程
    C# 判断类型间能否隐式或强制类型转换,以及开放泛型类型转换 update
    C# 判断类型间能否隐式或强制类型转换,以及开放泛型类型转换 update
    Devexpress Chart series 点击时获取SeriesPoint的值
    Devexpress Chart series 点击时获取SeriesPoint的值
    IndexOf()、IndexOfAny()的用法 —— c#
    IndexOf()、IndexOfAny()的用法 —— c#
    Java并发问题乐观锁与悲观锁以及乐观锁的一种实现方式CAS
  • 原文地址:https://www.cnblogs.com/-wenli/p/12323809.html
Copyright © 2011-2022 走看看