在使用golang的过程需要进行NBNS的报文解析,经调研发现NBNS的报文大都是定长报文,并且需要处理这些报文也不需要太高的性能,所有就决定使用 encoding/binary包来进行编解码。学习过程中对这个包内每个函数都进行尝试并整理出笔记如下。

Binary 包实现了简单的数字和字节序列之间的转换以及变量的编码与解码。

数字的翻译是通过读写固定大小的值来进行的。固定大小的值要么是固定大小的算术类型(bool,int8,uint8,int16,flaot32,complex64,...) 要么是包含固定大小值的数组或结构体。

vaint 函数使用变长编码对单个整型值进行编、解码。值越小,需要的字节越少。具体规范可以参考 https://developers.google.com/protocol-buffers/docs/encoding

本包更倾向于简单的实现而不是更高的性能。如果客户需要高性能序列化,尤其是大型数据结构,应该看看更高级的解决方案,例如 encoding/gob 包 或者 protocol buffers

函数

Read

func Read(r io.Readr, order ByteOrder, data any) error

Read 函数从 r 中读取结构化的二进制数据到 data 中。也就是说 Read 是一个 反序列化(解码) 的过程。 data 必须是一个有固定大小值的指针类型或者是有固定大小值的切片(可以是int32,但不能是int类型)。从 r 读取的字节使用指定的字节顺序解码并写入 data 中连续的字段。在解码 bool 值时, 值为 0x00 的字节被解码成 false 其它任何非 0x00 字节都被解码成 true 。当将数据读取到体中时,字段名是空白(_)的,对应该字段的数据将被跳过。例如空白字段名称可以用来处理 padding 。结构体中的字段如果不是空白字段,都必须是导出字段(字段名首字母必须大写),否则会导致 Read 崩溃(panic: reflect: reflect.Value.SetInt using value obtained using unexported field)。

只有当没有任何字节被读取的时候,error 的值才是 EOF。如果 Read 函数读取了一部分字节,但是没有读取全部所需的字节时发生了 EOF,那么Read函数就会返回ErrUnexpectedEOF


package main

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

func main() {
	var u16l uint16
	var u16b uint16
	b := []byte{0x12, 0x34}
	r := bytes.NewReader(b)
	err := binary.Read(r, binary.LittleEndian, &u16l)
	if err != nil {
		fmt.Println("binary.Read failed:", err)
		return
	}
	r.Reset(b)
	err = binary.Read(r, binary.BigEndian, &u16b)
	if err != nil {
		fmt.Println("binary.Read failed:", err)
		return
	}
	fmt.Printf("littleEndian: 0x%x, bigEndian: 0x%x\n", u16l, u16b)
}

output:

littleEndian: 0x3412, bigEndian: 0x1234

对于 uint16uint32uint64 这样的类型其实并不需要用 Read 函数来进行解码,可以直接调用 binary.LittleEndian.Uint16() 这样的函数来进行处理:

package main

import (
	"encoding/binary"
	"fmt"
)

func main() {

	b := []byte{0x12, 0x34}
	u16l := binary.LittleEndian.Uint16(b)
	u16b := binary.BigEndian.Uint16(b)
	fmt.Printf("littleEndian: 0x%x, bigEndian: 0x%x\n", u16l, u16b)
}

输出为:

littleEndian: 0x3412, bigEndian: 0x1234

另外还有一点需要注意的是,上面的代码仅针对无符号类型的,如果是有符号类型的数那么该如何处理?其实可以简单的通过强制类型转换就能完成。如,var i16 int16; i16 = int16(binary.LittleEndian.Uint16(b))

对于结构体类型或字节类型通常更适合用 Read 函数来进行处理:

package main

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

type Info struct {
	A int16
	B uint32
	_ uint8
	C [3]byte
}

func main() {
	var inf Info
	b := []byte{
		0x12, 0x34,
		0x22, 0x34, 0x56, 0x78,
		0xff,
		0xaa, 0xbb, 0xcc,
		0x00, 0x00,
	}
	r := bytes.NewReader(b)
	fmt.Printf("struct len = %d, bytes = %d\n", binary.Size(inf), len(b))
	err := binary.Read(r, binary.BigEndian, &inf)

	fmt.Printf("Info: %x, err = %v\n", inf, err)
}

输出:

struct len = 10, bytes = 12
Info: {1234 22345678 0 aabbcc}, err = <nil>

由上面输出可以知道,字节流中的 0xff 被跳过了(结构成员对应的位置为0值),字节流的大小超过要解析结构大小不会产生错误。如果字节流比要读取的结构大小还要小,那么就会报错误 unexpected EOF

另外,如果结构体成员中有任何一个字段的类型不是固定大小的,那么 Read 函数就是报错: invalid type 。同时 Size 函数返回值是 -1

Size

func Size(v any) int

该函数返回 Write 将对 v 编码成多少个字节,v 必须是一个固定大小的值,或者是固定大小值的切片,或者是指向上述类型的指针。如果 v 不是上面这些类型,那么本函数将返回 -1

package main

import (
	"encoding/binary"
	"fmt"
)

type Info1 struct { // size = 2 + 4 = 6
	A int16
	B int32
}

type Info2 struct { // size = 4 + 32 = 36
	A int32
	B [32]byte
}

type Info3 struct { // size = 2 * 6 + 36 = 48
	A [2]Info1
	B Info2
}

// 匿名内嵌体
type Info4 struct { // size = 6 + 4 = 10
	Info1
	C int32
}

type Info5 struct { // size = -1, 因为 int 大小不固定
	A int
	B uint8
}

func main() {
	var inf1 Info1
	var inf2 Info2
	var inf3 Info3
	var inf4 Info4
	var inf5 Info5

	fmt.Println(binary.Size(inf1))
	fmt.Println(binary.Size(inf2))
	fmt.Println(binary.Size(inf3))
	fmt.Println(binary.Size(inf4))
	fmt.Println(binary.Size(inf5))
}

输出:

6
36
48
10
-1

需要注意的是,binary.Size 只能计算固定大小变量的大小,而不能计算类型的大小。这个不同于 unsafe.Sizeof ,参见 对比unsafe.Sizeof

Write

func Write(w io.Writer, order ByteOrder, data any) error

Write 函数将 data 的二进制表现形式的数据写入 wdata 必须是固定大小的值或者固定大小值的切片或者指向这类数据的指针。

注: data 类型在 Read 中只能是变量指针,在 Write 中可以是变量本身。因为 Read 需要向变量填充数据,而 Write 只是使用这个数据,所以是不是指针都没有影响。

bool 类型的值的被编码成一个字节:1 表示 true0 表示 falsedata 中连续的字段值根据指定的字节序被依次编码成二进制数据写入 w 中。对结构体进行编码时,如果字段名称是空白 (_ ),那么 0 值将被写入 w 中。

package main

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

type NameQuery struct {
	Name  [16]byte
	Type  uint16
	Class uint16
}

func main() {
	nq := NameQuery{
		Name:  [16]byte{'a', 'b'},
		Type:  0x1234,
		Class: 0xcd,
	}
	buf := new(bytes.Buffer)
	err := binary.Write(buf, binary.BigEndian, nq)
	if err != nil {
		fmt.Println("binary.Write failed:", err)
	}
	fmt.Printf("% x", buf.Bytes())
}

输出:

61 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 12 34 00 cd

PutUvarint

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

PutUvarintuint64 类型的值编码到 buf 中并返回编码后字节大小,如果 buf 空间太小,不足以容纳编码后的字节,那么将会导致本函数 panic 。所以在编码前需要确保 buf 的空间是足够的。每种数值所需要最大空间可以通过 MaxVarintLenN 来获取。

包里定义了三个常量,分别是:

const (
	MaxVarintLen16 = 3
	MaxVarintLen32 = 5
	MarVarintLen64 = 10
)

MaxVarintLenN 中的 N 表示整型有多少 bitMaxVarintLen16 = 3 表示 16bit 的整型在变长编码时最多可以编码成 3 个字节。其它两个类型依此类推。

注: 如果 buf 空间不足,假设,需要3个字节,而 buf 只有 2 个字节空间,那么报错内容如下: panic: runtime error: index out of range [2] with length 2

package main

import (
	"encoding/binary"
	"fmt"
)

func main() {
	buf := make([]byte, binary.MaxVarintLen64)

	for _, x := range []uint64{1, 2, 127, 128, 255, 256, 32767, 32768, 65535, 65536} {
		n := binary.PutUvarint(buf, x)
		fmt.Printf("Encode x = %5d into [% x] with %d bytes\n", x, buf, n)
	}
}

输出:

Encode x =     1 into [01 00 00 00 00 00 00 00 00 00] with 1 bytes
Encode x =     2 into [02 00 00 00 00 00 00 00 00 00] with 1 bytes
Encode x =   127 into [7f 00 00 00 00 00 00 00 00 00] with 1 bytes
Encode x =   128 into [80 01 00 00 00 00 00 00 00 00] with 2 bytes
Encode x =   255 into [ff 01 00 00 00 00 00 00 00 00] with 2 bytes
Encode x =   256 into [80 02 00 00 00 00 00 00 00 00] with 2 bytes
Encode x = 32767 into [ff ff 01 00 00 00 00 00 00 00] with 3 bytes
Encode x = 32768 into [80 80 02 00 00 00 00 00 00 00] with 3 bytes
Encode x = 65535 into [ff ff 03 00 00 00 00 00 00 00] with 3 bytes
Encode x = 65536 into [80 80 04 00 00 00 00 00 00 00] with 3 bytes

Uvarint

func Uvarint(buf []byte) (uint64, int)

Uvarintbuf 内容解码成 uint64,并返回解码后的数值和读取 buf 的字节数,如果发生错误,那么解码的值是 0 同时读取的字节 n <= 0 , 这将表示:

	n == 0: buf 空间太小
	n < 0: 解码后的值大于64位(溢出),`-n` 表示已经读取的字节。
package main

import (
	"encoding/binary"
	"fmt"
)

func main() {
	inputs := [][]byte{
		{0x01},
		{0x02},
		{0x7f},
		{0x80, 0x01},
		{0xff, 0x01},
		{0x80, 0x02},
		{0x80, 0x80}, // n = 0, 不完整的编码,还需要后面有最高位不为1的字节。
		{0x08, 0x80}, // n = 1, 后续有不需要的字节。
		{0x88, 0x80, 0x88, 0x80, 0x88, 0x80, 0x88, 0x80, 0x88, 0x80, 0x77}, // n = -11, 数值太大
	}
	for _, b := range inputs {
		x, n := binary.Uvarint(b)
		if n != len(b) {
			fmt.Printf("Uvarint did not consume all of in when decoding:[% x], x = %d, n = %d\n", b, x, n)
		} else {
			fmt.Printf("0x%x\n", x)
		}
	}
}

输出:

0x1
0x2
0x7f
0x80
0xff
0x100
Uvarint did not consume all of in when decoding:[80 80], x = 0, n = 0
Uvarint did not consume all of in when decoding:[08 80], x = 8, n = 1
Uvarint did not consume all of in when decoding:[88 80 88 80 88 80 88 80 88 80 77], x = 0, n = -11

PutVarint

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

本函数功能同PutUvarint,只是编码的整型类型是 int64

AppendUvarint

func AppendUvarint(buf []byte, x uint64) []byte

AppendUvarint 将由 PutUvarint 生成的 x 的变长编码追加到 buf 中, 并返回扩展后的 buf

AppendVarint

func AppendVarint(buf []byte, x int64) []byte

本函数功能同 AppendUvarint ,只是针对 int64 类的进行编码追加。

ReadUvarint

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

本函数功能同 Uvarint(),只是读取数据是通过 io.ByteReader 进行,而不是直接传入 []byte 类型的参数。如果没能读取到任何字节,那么 error 的值是 EOF ,如果已经读取了部分数据然后遇到 EOF并且没有读取到所有需要的数据,那么将返回 io.ErrUnexpectedEOF

package main

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

func main() {
	inputs := [][]byte{
		{0x80, 0x02}, // 正常解析
		{0x80, 0x80}, // 还需要数据,但已经没有数据可以读取
		{0x08, 0x08}, // 有多余的数据
		{0x88, 0x80, 0x88, 0x80, 0x88, 0x80, 0x88, 0x80, 0x88, 0x80, 0x77}, // 数值太大溢出
	}
	for _, b := range inputs {

		x, err := binary.ReadUvarint(bytes.NewReader(b))
		if err != nil {
			fmt.Printf("Uvarint did not consume all of in when decoding:[% x], x = %d, err = %v\n", b, x, err)
		} else {
			fmt.Printf("0x%x\n", x)
		}
	}
}

输出:

0x100
Uvarint did not consume all of in when decoding:[80 80], x = 0, err = unexpected EOF
0x8
Uvarint did not consume all of in when decoding:[88 80 88 80 88 80 88 80 88 80 77], x = 576495938823127048, err = binary: varint overflows a 64-bit integer

ReadVarint

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

本函数功能同 ReadUvarint。与 ReadUvarint 的区别是本函数是解析 int64 类型的值。

ByteOrder与AppendByteOrder

ByteOrderAppendByteOrder 都是接口类型。 它们的定义如下:

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

type AppendByteOrder interface {
    AppendUint16([]byte, uint16) []byte
    AppendUint32([]byte, uint32) []byte
    AppendUint64([]byte, uint64) []byte
    String() string
}

包里有两个变量,一个是变量是 BigEndian 另一个是 LittleEndian 。 这两个变量都是定义成 struct {} 空结构体的类型,只要用这类型变量实现接口函数即可,而不用关心这个变量具体的值。

BigEndian 变量类型是 bigEndian ,相应的 LitlleEndian 的类型是 littleEdian 。这两个变量都是大写的,可以导出使用,而这两个变量的类型都是小写的,只能在 binary 包内部使用。

这两种类型的变量都分别实现了两个接口 ByteOrderAppendByteOrder 。如果当前字节序变量不能满足需求,可以自己定义一个类型 type MyOrder struct {} ,然后实现这两个接口。在这个包里只有 binary.Readbinary.Write 这两个函数用到这个变量。

总结

  1. binary包只能处理固定大小的类型。
  2. binary更倾向于易用,不适合用在有高性能需求的场景。
  3. Read/Write函数都是对io进行读写,如果需要处理 []byte 类型,需要借助 bytes.NewReadernew(bytes.Buffer) 来处理。
  4. 如果要处理 struct 类型,成员必须是可导出(首字母大小)类型
  5. 只有 go1.19及以后版本才能使用 AppendUvarintAppendVarintAppendByteOrder
  6. 针对简单类型的二进制转换,可以直接调用类似 binary.BigEndian.PutUint16()binary.BigEndian.Uint16 这样的函数进行编解码。如果需要在原有 buf 中进行追加,可以调用 binary.BigEndian.AppendUint16 函数来实现。