// +---------------------------------------------------------------------- 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]; } }