MQTT-第二节(SWOOLE和PHP下控制包的解析和打包)
日期 2021-07-10 14:50:35
阅读 25

准备工作

在开篇进行叙述解析过程之前,这里进行一些准备工作un。

  • 本次解析是基于SWOOLE和PHP开发语言背景下得解析,开发环境是基于windows的docker容器。
  • 解析包是SWOOLE官方推荐的 simps/mqtt MQTT协议解析和协程客户端包,可以通过Composer安装,也可以通过git下载。
  • 客户端采用mqttfx,下载地址:http://www.jensd.de/apps/mqttfx/1.7.1/
  • 端口号采用1883端口

启动服务器

image.png

image.png

客户端连接

image.png

点击Connect进行连接

image.png

解析

下面以Connect控制包类型解析进行说明,其他自行推导解析过程

首先打印一下,连接的控制包,修改代码后,重新启动服务器,并重新进行客户端连接

image.png

输出如下:

image.png

从上图可以看出,接受到的是一个UTF-8字符编码的字符串,下面的数组是解析后的数组,可以看到类型,协议名称MQTT,协议等级等信息。MQTT具体的Connect控制包的相关结构,请参考MQTT中文网

打开V3解析类,这里先具体分析getType,getRemaining,再分析Connect解析。

image.png

    public static function getType(string $data): int
    {
         return ord($data[0]) >> 4;
    }

    private static function getRemainingLength(string $data, ?int &$headBytes): int
    {
        $headBytes = $multiplier = 1;
        $value = 0;
        do {
            if (!isset($data[$headBytes])) {
                throw new LengthException('Malformed Remaining Length');
            }
            $digit = ord($data[$headBytes]);
            $value += ($digit & 127) * $multiplier;
            $multiplier *= 128;
            ++$headBytes;
        } while (($digit & 128) != 0);

        return $value;
    }

    public static function getRemaining(string $data): string
    {
        $remainingLength = static::getRemainingLength($data, $headBytes);

        return substr($data, $headBytes, $remainingLength);
    }

我们知道,MQTT控制包的第一个字节,7-4位是MQTT控制包类型,3-0位是标识信息,所以getType是获取控制包的类型,这里需要先介绍两个PHP函数,ordchr。ord() 函数返回字符串中第一个字符的 ASCII 值。chr() 函数从指定 ASCII 值返回字符,并且ASCII 值可被指定为十进制值、八进制值或十六进制值。八进制值被定义为带前置 0,十六进制值被定义为带前置 0x。

这里ord($data[0])的结果为int(16),是一个10进制的值,由于7-4位表示MQTT控制包类型,所以,我们需要右移4位(>>),去掉3-0位的信息,来获取相应的值,因为>>相当于除以2,<<相当于乘以2,所以右移4位的结果为1,正好对应MQTT控制包类型Connect的值。

剩余长度是从第二个字节开始,所以getRemainingLength函数的$headBytes的初始值为1,这有别于MQTT-第一节(协议)中的$i的0,其他都类似,getRemaining函数中的substr($data, $headBytes, $remainingLength); $headBytes就是可变包头和载荷的开始索引,remainingLength是剩余长度,substr是截取字符串函数,这里截取的是可变包头和载荷的数据。

下面解析Connect可变包头和载荷信息

    public static function string(string &$remaining): string
    {
        $length = unpack('n', $remaining)[1];
        if ($length + 2 > strlen($remaining)) {
            throw new LengthException("unpack remaining length error, get {$length}");
        }
        $string = substr($remaining, 2, $length);
        $remaining = substr($remaining, $length + 2);

        return $string;
    }

    public static function shortInt(string &$remaining): int
    {
        $tmp = unpack('n', $remaining);
        $remaining = substr($remaining, 2);

        return $tmp[1];
    }

    public static function connect(string $remaining): array
    {
        $protocolName = UnPackTool::string($remaining);
        $protocolLevel = ord($remaining[0]);
        $cleanSession = ord($remaining[1]) >> 1 & 0x1;
        $willFlag = ord($remaining[1]) >> 2 & 0x1;
        $willQos = ord($remaining[1]) >> 3 & 0x3;
        $willRetain = ord($remaining[1]) >> 5 & 0x1;
        $passwordFlag = ord($remaining[1]) >> 6 & 0x1;
        $userNameFlag = ord($remaining[1]) >> 7 & 0x1;
        $remaining = substr($remaining, 2);
        $keepAlive = UnPackTool::shortInt($remaining);
        $clientId = UnPackTool::string($remaining);
        if ($willFlag) {
            $willTopic = UnPackTool::string($remaining);
            $willMessage = UnPackTool::string($remaining);
        }
        $userName = $password = '';
        if ($userNameFlag) {
            $userName = UnPackTool::string($remaining);
        }
        if ($passwordFlag) {
            $password = UnPackTool::string($remaining);
        }
        $package = [
            'type' => Types::CONNECT,
            'protocol_name' => $protocolName,
            'protocol_level' => $protocolLevel,
            'clean_session' => $cleanSession,
            'will' => [],
            'user_name' => $userName,
            'password' => $password,
            'keep_alive' => $keepAlive,
            'client_id' => $clientId,
        ];
        if ($willFlag) {
            $package['will'] = [
                'qos' => $willQos,
                'retain' => $willRetain,
                'topic' => $willTopic,
                'message' => $willMessage,
            ];
        } else {
            unset($package['will']);
        }

        return $package;
    }

上面从connect函数进行分析,UnPackTool::string($remaining) 这段调用是为了解析协议名称,这里先具体分析string函数,$length = unpack(‘n’, $remaining)[1]可以看到这里有个unpack函数,这个函数的作用是从二进制字符串中按照特定的类型对数据进行解包,因为Connect的可变包头开始是两个字节的整型数据值,并且是16位大端序列,所以解析类型使用"n",后面的数据如果是UTF-8字符串类型,则使用"C*“,如果是32整型数据值,则使用"L”。接着分析string函数,得到了$length就可以获取,16位整型数据值后面的数据,$remainning剩余长度,这里再次截取到获取该数据之后的长度。获取了协议名称,下面一个字节是协议等级,协议等级之后的字节是连接标识,这里重点分析。

Figure 3.4 - Connect Flag bits

|Bit        |7                |6              |5              |4  |3       |2          |1              |0
|           |User Name Flag   |Password Flag  |Will Retain    |Will QoS    |Will Flag  |Clean Session  |Reserved
|byte 8     |X                |X              |X              |X  |X       |X          |X              |0

这个字节0位是保留位;1位是Clean Session,指明了会话状态的处理方式;2位是Will Flag;4-3位是Will Qos,表示发布Will Message时使用QoS的等级;5位是Will Retain,表示Will Message在发布之后是否需要保留;6位是Password Flag,表示是否设置密码;7位是User Name Flag,表示是否设置了用户名。

这里在继续解析之前,要分析&的一个使用技巧,如果一个值为0000 1111,转换成10进制为15,8+4+2+1,任何一个小于15的值与15进行按位与(&),都等于15。例如,9&15=9,这是因为15的3-0位都是1,所以任何小于15的值都包含在15内,结果就是其本身。

Clean Session是1位,最大代表的值为0x01,这里的0x01就相当于上面的15,是最大数,ord($remaining[1])右移一位,7位补零,6-1是User Name Flag到Will Flag的值,依照按位与的规则,0x01的7-1位都为0,所以7-1位结果为0,相当于0位就是Clean Session本身的值。同理可以依次得出Will Flag,Will QoS,Will Retain,Password Flag,User Name Flag的值,需要强调的是,Will QoS占有两位,所以最大为0000 0011,转换成10进制为3,Will QoS的值可是是0(0x00),1(0x01),2(0x02),一定不会是3(0x03)。所以$willQos = ord($remaining[1]) >> 3 & 0x3这里和0x3进行&运算而不是0x1。

连接标识解析完毕以后,解析解析Keep Alive,由于Keep Alive前面有两个字节(16位)的整型数据值,所以使用shortInt方法进行解析。Keep Alive之后是载荷的解析,方法与上面相同,依次解析即可,这里不再赘述。

其他控制包类型的解析参照上面的方法即可,具体解析的内容需要对照文档,具体解析不再赘述。

打包

MQTT有解析,就有打包的过程,具体过程是MQTT解析的逆过程,simps/mqtt中有详细的打包过程,这里只重点强调一下上面提到的连接标识的打包。

默认连接标识$connectFlags设置为0,0000 0000,如果设置了Clean Session,说明位1应该设置为1,于是$cleanSession左移一位,即$cleanSession << 1,得到0000 0010,然后将$connectFlags与$cleanSession进行按位或(|)操作,将二者合二为一,其他值,比如Will Flag,Will QoS,Will Retain等过程相同,再左移到相应位置再与$connectFlags进行按位或(|)操作即可。

总结

以上就是MQTT控制包在SWOOLE和PHP环境下得解析和打包原理。

更多:

MQTT-第一节(协议)

MQTT-第二节(SWOOLE和PHP下控制包的解析)