文章背景:
两端自定义网络协议时,需要对传输的数据包格式进行约定,比如TLV格式,约定数据包头部固定几个字节传的是包头,包头的内容也需要约定。
这里讲一种包头中表示数据包大小的方式,用golang语言,语言不是问题,大意理解就行。
方案设计:
比如首先约定包最大值为60KB,
60KB = 60 * 1024B = 61440B,数据包都是字节数组,所以数据包的最大长度是61440个字节(B=Byte),uint16的最大值是65535,大于数据包的最大长度值,用uint16即可表示,uint16是16位二进制数,2个字节。也就是说,我们将用两个字节来表示数据包的大小。
具体实现方案:
我们采用位运算,将数据包的大小值,拆分为两个字节进行存储,一个高字节,一个低字节,
header := make([]byte, 2)
header[0] = byte((dataSize >> 8) & 0xff)
header[1] = byte(dataSize & 0xff)
在这段代码中,使用 (dataSz >> 8) & 0xff
将数据包大小的高字节提取出来。
原因是,数据包大小的高字节存储在整数的第二个字节(从右往左数第二个字节)。通过将数据包大小右移 8 位,将第二个字节移动到最低有效位的位置。然后,使用按位与操作符 &
和位掩码 0xff
提取字节。
在这里,右移 8 位(即 dataSz >> 8
)是基于数据包大小占用了两个字节(约定数据包长度最大值为60 * 1024 = 61440)。通过右移 8 位,我们将第二个字节移动到最低有效位的位置,以便提取。
可以将这个过程与将整数拆分为多个字节的过程进行类比。在那个过程中,我们使用右移操作符和按位与操作符将整数的各个字节提取出来。而在这个特定的代码段中,我们只需要提取第二个字节,因此使用右移 8 位即可。
综上所述,为了提取数据包大小的高字节,我们使用 (dataSz >> 8) & 0xff
中的右移 8 位操作。这样可以将第二个字节移动到最低有效位的位置,并使用按位与操作符提取字节。
0xff是16进制数,转成二进制是11111111,8个1,这个数是很特殊的值,是二进制中单个字节能表示的最大值,也就是255,再通过“&”与运算,二者都为1的情况运算结果才为1,可以使原值的低8位保留原来的值,也就达到了我们的目的,获取原值的低字节(后8位),因为总共就2个字节(16位),所以右移>>8之后,高位的8个字节变成了低位。
二次修改补充:
byte(dataSize & 0xff) 实际上 等同于 byte(dataSize),
当将 uint16
类型的数字使用 byte()
方法进行强制类型转换时,会按照以下过程进行转换:
- 首先,将
uint16
类型的数字表示为二进制形式,占用 16 位。 - 然后,将这个 16 位的二进制数截断为 8 位,只保留低 8 位。
- 最后,将这个 8 位的二进制数转换为
byte
类型,也就是一个字节。
这个过程中,只保留了原始 uint16
数字的低 8 位,高 8 位被丢弃。因此,强制类型转换后得到的 byte
类型表示了原始数字的低字节。
至此,我们将数据包长度拆分为两个字节,分别存储在header头中。
客户端接收到数据包后,因为接收到的是字节数组,所以可以直接用Index[0]和Index[1]获取到包头数据,也就是上面我们拆分的两个字节,
通过下面的位运算还原数据包长度值:
dataSize := int(header[0])<<8 | int(header[1])
代码的意思是将高字节左移8位(对应我们前面的右移),在“|”或运算,拼接低8位,从而得到完整的数据包大小的二进制表达。
综上,需要掌握的知识点有:
字节,二进制、十六进制转换,位运算