本文介绍MQTT协议中,固定报头中的可变长度部分的计算方式。通过提供一些例子,将其他介绍MQTT协议的文章中没有仔细说明的计算部分进行解释。
参考文章
这篇文章简要介绍了MQTT,但是没有明确提供计算过程。
MQTT简介之三(3) MQTT协议 报文的剩余长度如何计算和编码
这篇文章也介绍了MQTT,且有提供计算过程,但是是用一大段文字进行表述的,理解起来有些困难。
至于New Bing(GPT4.0)和ChatGPT 3.5,他们在做这种特殊规则下的二进制计算时,效果并不好。
如果在读完本文后你对二进制有兴趣,可以阅读这篇文章:CSAPP第二章-信息的表示与处理
如何计算可变长度?
以下是各大文章的相似描述:
MQTT协议的剩余长度字段使用了一种变长编码方案,每个字节的最高位是一个进位标志位,如果为1,表示还有后续的字节;如果为0,表示这是最后一个字节。每个字节的低七位是实际的数据位,用来表示剩余长度的一部分。因此,为了得到剩余长度的完整值,需要循环读取每个字节,并用一个乘法器来计算出总和。
并且,提供了一张表,介绍剩余长度在不同的区间中时,需要使用多少个字节数:
字节数 | 最小值 | 最大值 |
---|---|---|
1 | 0(0x00) | 127(0x7F) |
2 | 128(0x80,0x01) | 16383(0xFF,0x7F) |
3 | 16384(0x80,0x80,0x01) | 2097151(0xFF,0xFF,0x7F) |
4 | 2097152(0x80,0x80,0x80,0x01) | 268435455(0xFF,0xFF,0xFF,0x7F) |
读取过程分析
首先,我们明确MQTT协议读取数据的过程,是一个字节一个字节依次读取的。即读取完一个字节之后,再处理下一个字节。
那么为了可以动态控制可变长度部分的字节数,MQTT协议约定该部分的字节的第一位来标记“后续是否还有字节”。
明确这一点之后,我们抛开第一位,读取后七位的值,可以得到一个数。
想象一下,如果我们的可变长度res
特别长,在2097152~2684354455之间,那么我们需要4个字节进行传输。此时我们将每个字节的第一位剥离,将剩余7位取出:
用变量a
表示第一个字节后7位的数,用变量b
表示第二个字节后7位的数,c
,d
以此类推。
那么我们会有:res
= a
* 1 + b
* 128 + c
* 128 * 128 + d
* 128 * 128 * 128
或者咱们换一种写法:res
= a
* 1280 + b
* 1281 + c
* 1282 + d
* 1283
可以理解为,这是一个“公式”,即可变长度 = 各个字节除1以外的数 * 128字节序号-1 (不会打求和符号,而且也不标准,请以理解为主)
公式验算
以CSDN那篇文章的第四个例子为例,进行“公式”验算:
100000000,
100000000/128 = 781250,不等于0需要进位,
然后 781250/128=6103,不等于0再进一位,
然后 6103/128=47,不等于0再进一位
47/128=0,等于0无需再进位了
所以总体进3位,需要4个字节
第一个字节bit7是1, 第2个字节bit7是1,第3个字节bit7是1.,第4个字节bit7是0.
然后100000000%128=0,就是0x00,所以第一个字节bit0~bit6就是0x00,
然后781250%128=66,所以第2个字节bit0~bit6就是0x42,
然后 6103%128=87,所以第3个字节bit0~bit6就是0x57,
然后 47%128=47,所以第4个字节bit0~bit6就是0x2F,
然后再结合上bit7的话,就是0x80,0xC2,0xD7,0x2F
先抛开中间的计算过程,将它的目标和结果拿出来:目标100000000,结果0x80,0xC2,0xD7,0x2F
换算成二进制是:1000 0000 1100 0010 1101 0111 0010 1111
套用公式,一一计算a
, b
, c
, d
:
a
: 1000 0000 –去除开头1–> 000 0000 –得到值–> 0b
: 1100 0010 –去除开头1–> 100 0010 –得到值–> 66c
: 1101 0111 –去除开头1–> 101 0111 –得到值–> 87d
: 0010 1111 –去除开头0–> 010 1111 –得到值–> 47
然后计算,a
* 1 + b
* 128 + c
* 128 * 128 + d
* 128 * 128 * 128 = 0 + 8448 + 1425408 + 98566144 = 100000000
如此一来,验证通过。
其他情况计算
在理解了4个字节的情况之后,我们会发现,前面读到的数字乘的值会比较小,后面读到的数字乘的值会比较大。因此就很容易理解为什么其他文章说0~127的数字只需要一个字节了,因为当可变长度区仅有一个字节时,乘数是1,而一个字节抛开第一位之后剩余7位,不考虑负数的情况下最大能表示的值就是127。
如果超过127,就需要从前往后的第二个字节进行处理,因为第二个字节(除第一位的剩余7位的输)默认要乘以128。
两个字节的存储长度区间是128~16383,如果大于16383,则需要使用三个字节存储,第三个字节(除第一位的剩余7位的输)默认要乘以128*128。
理解了这个之后,就可以通过上面的推算过程,反推为什么100000000要进行多次除以128的操作。
代码实现
在mqant中,官方提供了一种读取过程,将其抄录如下:
// Read the high
multiplier := 1
for {
count_len++
pack.length += (int(temp_byte&127) * multiplier)
if temp_byte>>7 != 0 && count_len < 4 {
temp_byte, err = r.ReadByte()
if err != nil {
return
}
multiplier *= 128
} else {
break
}
}
可以看到,这里定义了变量multiplier
,其初始值是1,且读取完一个字节之后,将其自己*=128扩大,满足前述的“公式”。
总结
在TCP完成可靠传输之上,还需要应用层进行拆包解析。通过先读取包长度的方式,可以让后续数据传输时监测是否传输成功。而MQTT使用1~4个字节的方式,灵活配置可变长度,其思路值得学习。