本文介绍MQTT协议中,固定报头中的可变长度部分的计算方式。通过提供一些例子,将其他介绍MQTT协议的文章中没有仔细说明的计算部分进行解释。

参考文章

MQTT 5.0 报文(Packets)入门指南

这篇文章简要介绍了MQTT,但是没有明确提供计算过程。

MQTT简介之三(3) MQTT协议 报文的剩余长度如何计算和编码

这篇文章也介绍了MQTT,且有提供计算过程,但是是用一大段文字进行表述的,理解起来有些困难。

至于New Bing(GPT4.0)和ChatGPT 3.5,他们在做这种特殊规则下的二进制计算时,效果并不好。

如果在读完本文后你对二进制有兴趣,可以阅读这篇文章:CSAPP第二章-信息的表示与处理

如何计算可变长度?

以下是各大文章的相似描述:

MQTT协议的剩余长度字段使用了一种变长编码方案,每个字节的最高位是一个进位标志位,如果为1,表示还有后续的字节;如果为0,表示这是最后一个字节。每个字节的低七位是实际的数据位,用来表示剩余长度的一部分。因此,为了得到剩余长度的完整值,需要循环读取每个字节,并用一个乘法器来计算出总和。

并且,提供了一张表,介绍剩余长度在不同的区间中时,需要使用多少个字节数:

字节数最小值最大值
10(0x00)127(0x7F)
2128(0x80,0x01)16383(0xFF,0x7F)
316384(0x80,0x80,0x01)2097151(0xFF,0xFF,0x7F)
42097152(0x80,0x80,0x80,0x01)268435455(0xFF,0xFF,0xFF,0x7F)

读取过程分析

首先,我们明确MQTT协议读取数据的过程,是一个字节一个字节依次读取的。即读取完一个字节之后,再处理下一个字节。

那么为了可以动态控制可变长度部分的字节数,MQTT协议约定该部分的字节的第一位来标记“后续是否还有字节”。

明确这一点之后,我们抛开第一位,读取后七位的值,可以得到一个数。

想象一下,如果我们的可变长度res特别长,在2097152~2684354455之间,那么我们需要4个字节进行传输。此时我们将每个字节的第一位剥离,将剩余7位取出:

用变量a表示第一个字节后7位的数,用变量b表示第二个字节后7位的数,cd以此类推。

那么我们会有: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 –得到值–> 0
  • b: 1100 0010 –去除开头1–> 100 0010 –得到值–> 66
  • c: 1101 0111 –去除开头1–> 101 0111 –得到值–> 87
  • d: 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个字节的方式,灵活配置可变长度,其思路值得学习。