在使用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
+
对于 uint16
、uint32
、uint64
这样的类型其实并不需要用 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
该函数返回 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))
+}
+
输出:
需要注意的是,binary.Size
只能计算固定大小变量的大小,而不能计算类型的大小。这个不同于 unsafe.Sizeof
,参见 对比unsafe.Sizeof。
Write
func Write(w io.Writer, order ByteOrder, data any) error
+
Write
函数将 data
的二进制表现形式的数据写入 w
。 data
必须是固定大小的值或者固定大小值的切片或者指向这类数据的指针。
注:
+data 类型在 Read
中只能是变量指针,在 Write
中可以是变量本身。因为 Read
需要向变量填充数据,而 Write
只是使用这个数据,所以是不是指针都没有影响。
bool
类型的值的被编码成一个字节:1
表示 true
,0
表示 false
。data
中连续的字段值根据指定的字节序被依次编码成二进制数据写入 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
+
PutUvarint
将 uint64
类型的值编码到 buf
中并返回编码后字节大小,如果 buf
空间太小,不足以容纳编码后的字节,那么将会导致本函数 panic
。所以在编码前需要确保 buf
的空间是足够的。每种数值所需要最大空间可以通过 MaxVarintLenN
来获取。
包里定义了三个常量,分别是:
const (
+ MaxVarintLen16 = 3
+ MaxVarintLen32 = 5
+ MarVarintLen64 = 10
+)
+
MaxVarintLenN
中的 N
表示整型有多少 bit
, MaxVarintLen16 = 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)
+
Uvarint
将 buf
内容解码成 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
ByteOrder
与 AppendByteOrder
都是接口类型。 它们的定义如下:
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 包内部使用。
这两种类型的变量都分别实现了两个接口 ByteOrder
和 AppendByteOrder
。如果当前字节序变量不能满足需求,可以自己定义一个类型 type MyOrder struct {}
,然后实现这两个接口。在这个包里只有 binary.Read
和 binary.Write
这两个函数用到这个变量。
总结
- binary包只能处理固定大小的类型。
- binary更倾向于易用,不适合用在有高性能需求的场景。
- Read/Write函数都是对io进行读写,如果需要处理
[]byte
类型,需要借助 bytes.NewReader
或 new(bytes.Buffer)
来处理。 - 如果要处理
struct
类型,成员必须是可导出(首字母大小)类型 - 只有 go1.19及以后版本才能使用
AppendUvarint
、AppendVarint
、AppendByteOrder
- 针对简单类型的二进制转换,可以直接调用类似
binary.BigEndian.PutUint16()
或 binary.BigEndian.Uint16
这样的函数进行编解码。如果需要在原有 buf
中进行追加,可以调用 binary.BigEndian.AppendUint16
函数来实现。