177 lines
6.2 KiB
PHP
177 lines
6.2 KiB
PHP
<?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];
|
||
}
|
||
}
|