侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

MQTT协议-SWOOLE和PHP下控制包的解析和打包详解

swoole  /  管理员 发布于 3年前   2319

准备工作:

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


本次解析是基于SWOOLE和PHP开发语言背景下得解析,开发环境是基于windows的docker容器。

解析包是SWOOLE官方推荐的 simps/mqtt MQTT协议解析和协程客户端包,可以通过Composer安装,也可以通过git下载。

客户端采用mqttfx,下载地址:http://www.jensd.de/apps/mqttfx/1.7.1/

端口号采用1883端口


启动服务器

1.png

2.png

客户端连接

3.png

点击Connect进行连接

5.png


解析

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

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

6.png

输出如下:

7.png


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

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

8.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函数,ord和chr。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进行按位或(|)操作即可。


转:https://www.codeshort.cn/detail/11



  • 上一条:
    php中使用帮助函数array_get()读取多维数组的值
    下一条:
    eBay 打造基于 Apache Druid 的大数据实时监控系统
  • 昵称:

    邮箱:

    1条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • swoole协程通信之数据处理协程对数据进行读写,保存到协程上下文示例(0个评论)
    • swoole redis连接池应用优化之多协程频繁访问redis(0个评论)
    • php中使用hyperf框架调用讯飞星火大模型实现国内版chatgpt功能示例(2个评论)
    • php中使用hyperf框架调用文心千帆大模型实现国内版chatgpt功能示例(0个评论)
    • 在hyperf框架中使用基于protobuf的RPC生成器实现rpc服务(1个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2017-09
    • 2020-03
    • 2020-06
    • 2021-03
    • 2021-04
    • 2021-05
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-11
    • 2022-03
    • 2022-05
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-07
    • 2023-08
    • 2023-11
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客