Files
YwxAppThink/addon/mqttbroker/protocol/Mqtt.php
T

177 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace addon\mqttbroker\protocol;
/**
* MQTT 协议编解码器(Workerman 标准协议类)
*
* 遵循 Workerman 自定义协议规范(https://www.workerman.net/doc/workerman/protocols/how-protocols.html):
* - 必须实现 input() / decode() / encode() 三个静态方法;
* - 无需强制继承 ProtocolInterface,只要类包含这三个静态方法即可;
* - 通过 $worker->protocol = Mqtt::class 绑定,Workerman 在收到数据时自动:
* input() 判定一个完整包的长度(>0 包长 / 0 继续等待 / -1 协议错误断开)
* decode() 解析完整包,结果作为 $data 传入 onMessage
* 业务侧 $connection->send($data) 时自动调用 encode() 打包。
*
* 解码后数据结构(供 Broker 使用):
* ['cmd' => int, 'flags' => int, 'body' => string]
* - cmd :MQTT 控制报文类型(高 4 位固定头)
* - flags:固定头低 4 位(PUBLISH 时为 dup/qos/retain
* - body :变长头 + 负载(已剔除固定头与剩余长度字段)
*
* 编码时同样接收上述数组,也可直接传原始字符串(透传)。
*/
class Mqtt
{
/* ---------- MQTT 控制报文类型 ---------- */
const CMD_CONNECT = 1;
const CMD_CONNACK = 2;
const CMD_PUBLISH = 3;
const CMD_PUBACK = 4;
const CMD_PUBREC = 5;
const CMD_PUBREL = 6;
const CMD_PUBCOMP = 7;
const CMD_SUBSCRIBE = 8;
const CMD_SUBACK = 9;
const CMD_UNSUBSCRIBE = 10;
const CMD_UNSUBACK = 11;
const CMD_PINGREQ = 12;
const CMD_PINGRESP = 13;
const CMD_DISCONNECT = 14;
const CMD_AUTH = 15;
/**
* 检查包的完整性,返回当前包在 buffer 中的总长度
* @param string $buffer 当前收到的数据缓冲
* @param mixed $connection Workerman 传入的连接对象(此处未使用)
* @return int >0 完整包长度 | 0 数据不足需等待 | -1 协议错误(断开连接)
*/
public static function input(string $buffer, $connection = null): int
{
$len = strlen($buffer);
if ($len < 2) {
return 0; // 至少 1 字节固定头 + 1 字节长度
}
$first = ord($buffer[0]);
$cmd = ($first >> 4) & 0x0F;
if ($cmd < 1 || $cmd > 15) {
return -1; // 非法控制报文类型
}
// 解码剩余长度(变长整数,最多 4 字节)
$value = 0;
$multiplier = 1;
$i = 0;
$pos = 1;
do {
if ($pos + $i >= $len) {
return 0; // 长度字节尚未收全
}
$byte = ord($buffer[$pos + $i]);
$value += ($byte & 0x7F) * $multiplier;
$multiplier *= 128;
$i++;
if ($i > 4) {
return -1; // 剩余长度字段超过 4 字节,协议错误
}
} while (($byte & 0x80) !== 0);
$headerLen = 1 + $i; // 固定头(1) + 剩余长度字段(i)
if ($len - $headerLen < $value) {
return 0; // 包体尚未收全
}
return $headerLen + $value;
}
/**
* 解包:把完整包解析为结构化数组,作为 onMessage 的 $data
* @param string $buffer 一个完整 MQTT 报文(含固定头)
* @param mixed $connection
* @return array ['cmd'=>int,'flags'=>int,'body'=>string]
*/
public static function decode(string $buffer, $connection = null): array
{
if ($buffer === '') {
return ['cmd' => 0, 'flags' => 0, 'body' => ''];
}
$first = ord($buffer[0]);
$cmd = ($first >> 4) & 0x0F;
$flags = $first & 0x0F;
// 解码剩余长度,定位包体起始
$value = 0;
$multiplier = 1;
$i = 0;
$pos = 1;
do {
$byte = ord($buffer[$pos + $i]);
$value += ($byte & 0x7F) * $multiplier;
$multiplier *= 128;
$i++;
} while (($byte & 0x80) !== 0);
$body = substr($buffer, $pos + $i, $value);
return ['cmd' => $cmd, 'flags' => $flags, 'body' => $body];
}
/**
* 打包:把结构化数组编码为可发送字节流
* @param array|string $data 编码结构 ['cmd'=>int,'flags'=>int,'body'=>string] 或原始字符串
* @param mixed $connection
* @return string
*/
public static function encode($data, $connection = null): string
{
if (is_string($data)) {
return $data; // 兼容直接发送原始字节
}
$cmd = (int) ($data['cmd'] ?? 0);
$flags = (int) ($data['flags'] ?? 0);
$body = (string) ($data['body'] ?? '');
$fh = chr(($cmd << 4) | ($flags & 0x0F));
return $fh . self::encodeRemainingLength(strlen($body)) . $body;
}
/**
* 编码 MQTT 剩余长度(变长整数,最大 4 字节)
*/
public static function encodeRemainingLength(int $len): string
{
$bytes = '';
do {
$byte = $len % 128;
$len = intdiv($len, 128);
if ($len > 0) {
$byte |= 0x80;
}
$bytes .= chr($byte);
} while ($len > 0);
return $bytes;
}
/**
* 解码 MQTT 剩余长度,返回 [值, 占用字节数]
*/
public static function decodeRemainingLength(string $buffer, int $pos = 0): array
{
$value = 0;
$multiplier = 1;
$i = 0;
do {
$byte = ord($buffer[$pos + $i]);
$value += ($byte & 0x7F) * $multiplier;
$multiplier *= 128;
$i++;
if ($i > 4) {
break;
}
} while (($byte & 0x80) !== 0);
return [$value, $pos + $i];
}
}