encoding/binary
回想区
- 为什么除了encoding/json,我们还需要encoding/binary?文本协议和二进制协议区别是什么?
- 什么是字节序?能否举一个例子说明字节序不同带来的变化?
- 为什么要引入小端法?优点是什么?
- 什么是网络字节序?
- 如何从字节切片中读取一个定长整数?
- 如何将一个定长整数写入到字节切片中?
- 如何从比特流中读取一个定长整数?
- 如何从比特流中写入一个定长整数?
- 什么是不定长编码?
encoding/binary
使用的不定长编码协议是怎么样的? - 如何从字节切片读取一个不定长整数?
- 如何将一个整数按照不定长编码的形式写入到字节切片中?
- 如何从比特流中读取一个不定长整数?
-
什么时候我们需要encoding/binary?
-
什么是二进制协议?与文本协议区别在哪里?
文本协议为了能够可读,约束了适用字符在ASCII或者unicode。
比方说数字26,在文本协议下需要用两个字节分别表示”2“和”6“,而在二进制协议下只需要使用一个字节——*0x1A。*因此二进制协议节省了内存, 除此之外,二进制协议可以被计算机直接理解。
-
什么是字节序?
字节序是指二进制数据在内存中的存储顺序。以数字”287454020“为例,在十六进制表示下为0x11223344。最高字节是0x11,最低字节是0x44。
如果我们在存储的时候,按照
11 22 33 44
来存储,那么就是大端法。而如果按照44 33 22 11
来存储,那么就是小端法。 -
为什么会使用小端法?
可能很多人奇怪,为什么会有小端法这种看起来比较别扭的形式呢?事实上,很多CPU都使用小端法,其原因在于使用小端法可以让我们在不移动字节的前提下改变数字类型大小。
比方说对于
0x11223344
,现在占用了四个字节(int32),如果我们希望转换为8个字节(int64)。在大端法下为00 00 00 00 11 22 33 44
,我们需要迁移四个字节;而小端法下为44 33 22 11 00 00 00 00
,我们不需要迁移。 -
什么是网络字节序?
遗憾的是,虽然计算机系统常常使用小端法。但是网络传输约定了使用大端法,因此大端法常常也称之为网络字节序。
之所以需要提及该点,是因为我们需要记住在传输之前进行必要的字节序转换。
定长编码
定长编码也就是对int8,int16.,int32,int64等有着固定长度的数字类型编码
encoding包中常常会区分”处理比特流“和“处理字节切片”
编码至字节切片
type ByteOrder interface {
Uint16([]byte) uint16
Uint32([]byte) uint32
Uint64([]byte) uint64
PutUint16([]byte, uint16)
PutUint32([]byte, uint32)
PutUint64([]byte, uint64)
String() string
}
如上述代码所述,ByteOrder可以实现对数字的编码和解码。 encoding/binary
包中有两个实现, BigEndian
和 LittleEndian
,显然他们分别对应大端法和小端法。
比方说如果我们想要用大端法对 uint32 500
做编码,我们可以使用下面的代码
v := uint32(500)
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, v)
值得注意的是,如果提供的buffer过小,那么Put操作会panic
当然我们也可以使用 Uint32
进行解码
x := binary.BigEndian.Uint32(buf)
如果提供的buffer过小同样会发生panic。除此之外,如果使用的是比特流,那么一定要使用 io.ReadFull()
来确保不会对部分buffer解码
处理比特流
encoding/binary
包提供了两个函数来处理向比特流读取和写入定长值。分别是 Read()
和 Write()
Read函数的定义如下
func Read(r io.Reader, order ByteOrder, data interface{}) error
data必须是一个定长类型的指针或者切片。Read函数会检查data的具体类型来决定该读取多少字节,然后用传入的 order
来决定写入的字节序。该函数支持所有定长的integer, float, complex数字类型。
在内部,go使用类型转换来实现。但是对于复杂类型如切片和结构体, Read
不得不回退到使用基于反射的解码器来寻找解码器, Read
会忽略所有空白字段( _
,通常是为了对齐), Read
要求结构体的除了 _
以外的所有字段都必须是导出的,否则会panic。如果没有字节被读取,那么会返回 EOF
,如果读取了部分字节遇到 EOF
,那么返回 ErrUnexpectedEOF
实践上,如果我们需要编码复杂结构,我们应该优先考虑专业高效的协议如protocal buffer等。
另一方面, Write
函数则实现了完全相反的功能:会根据data的类型来写入writer
func Write(w io.Writer, order ByteOrder, data interface{}) error
不定长编码
定长编码的问题在于它会消耗太多的空间。如果我们的绝大多数数值都是小数字,那为什么我们需要使用int64呢?如果你也面临这样的问题,一个好方法是使用不定长编码
不定长编码怎么运作?
不定长编码的思路是对于小数字应该使用更少的空间,对此有着不同的协议。而 encoding/binary
使用与protocol buffers完全相同的实现。
该编码方式会用第一个位来表示是不是有着更多的数据,剩下七位来存储真正的数据。如果第一位为1,那么需要读取下一个字节,如果是0,那么说明已经是该数的最后一个字节了(不需要在读取)。找到完整的字节后将所有字节的7个位连起来就能找到对应值。
比方说对于数字53,二进制表达是 110101
,因为只有六位,所以可以直接用一个字节表示 00110101
。而对于1732,二进制表达是 11011000100
,需要11位存储,因此可以用两个字节来表达 10001101 01000100
。
编码至字节切片
我们可以使用两个函数来实现编码至字节切片的目的
func PutVarint(buf []byte, x int64) int
func PutUvarint(buf []byte, x uint64) int
这两个函数会将x编码至buf然后返回使用的字节数,如果buf过小那么会触发panic。如果想要避免触发panic,我们可以在声明buf的时候使用到常量 MaxVarintLen16
, MaxVarintLen32
, MaxVarintLen64
对应的,我们可以使用下面的两个函数来实现解码
func Varint(buf []byte) (int64, int)
func Uvarint(buf []byte) (uint64, int)
这两个函数都会解码数据到最大的有符号和无符号整数,第二个返回值返回读取的字节长度。
通常来说,使用 Varint
和 Uvarint
可能会出现两种错误:在读取到有前导0的字节前buffer结束(表示没有读到最后一个字节),此时会返回0。读取到的字节数超过64位,此时会返回一个负数
从字节流中解码
比较奇怪的是, encoding/binary
包提供了从字节流中解码不定长编码的函数,但是没有提供向字节流写入不定长编码的方法。
我们可以使用两个函数来实现解码字节流
func ReadVarint(r io.ByteReader) (int64, error)
func ReadUvarint(r io.ByteReader) (uint64, error)
这两个函数实现的作用类似于 Varint
和 Uvarint
只不过是从字节流中拉取数据。在读取过程中发生的错误会传递给error,而如果读取的编码超过64位,跟 Varint
不一样的是,不会返回负数而是返回overflow 错误