一:什么是MQTT协议
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一个轻量级、发布‑订阅 (Publish‑Subscribe) 模式的即时通讯协议,基于 TCP/IP。专门为带宽有限、设备算力低、网络不稳定的物联网设备设计。
二:MQTT核心模型
MQTT一共有三种角色
- 发布者 (Publisher):发送消息的设备 / 程序(传感器、单片机、后端服务)
- Broker(消息服务器 / 代理):中转站,接收消息,转发给订阅者 - 常见 Broker:Mosquitto、EMQX、VerneMQ、RabbitMQ (MQTT 插件)
- 订阅者 (Subscriber):接收消息的设备 / 程序(APP、后端、大屏、其他物联网设备)
三:MQTT协议优缺点
1. 优点
- 数据包极小,头部最小仅 2 字节,流量消耗低,适合单片机、4G / 窄带物联网
- 异步解耦,设备不需要长轮询
- 支持断线重连、遗嘱、QoS,适配不稳定网络
- 跨平台:PHP、Java、Python、小程序、单片机都有客户端库
2. 缺点
- 不适合海量大文件传输,只适合短消息、指令、状态
- Broker 是单点,需要做集群高可用
- 默认无消息持久化,掉线没收到的消息会丢失(需开启会话持久化 clean‑session=false)
四:使用mosquitto实现MQTT的发布与订阅
1. 安装mosquitto
Ubuntu/Debian安装
# 更新源
sudo apt update
# 安装 mosquitto 服务端 + 客户端工具
sudo apt install mosquitto mosquitto-clients -y
CentOS 7/Rocky/AlmaLinux/RHEL安装
sudo yum install epel-release -y
sudo yum install mosquitto mosquitto-clients -y
启动mosquitto
sudo systemctl start mosquitto
sudo systemctl enable mosquitto
systemctl status mosquitto
2. mosquitto配置(看情况配置)
配置文件路径
/etc/mosquitto/mosquitto.conf
/etc/mosquitto/conf.d/ # 推荐把自定义配置放这里新建文件
创建MQTT账户密码
# 创建密码文件
sudo mosquitto_passwd -c /etc/mosquitto/passwd mqttuser
会提示输入两次密码。
再新建 /etc/mosquitto/conf.d/auth.conf
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
重启服务
sudo systemctl restart mosquitto
3. 发布消息
mosquitto_pub -h 【IP】 -p 【端口】 -u 【用户名】 -P 【密码】 -t "【主题 Topic】" -m "【发布的消息】"
4. 订阅消息
mosquitto_sub -h 【IP】 -p 【端口号】 -u 【用户名】 -P 【密码】 -t "【主题 Topic】"
五:PHP对接MQTT协议示例
-
发布消息
/**
-
MQTT 消息发布
-
@param string $top MQTT 主题
-
@param array $message 发布消息体
-
@return mixed
*/
public function release(topic, message)
{
message = is_array(message) ? json_encode(message, JSON_UNESCAPED_UNICODE) : message;$host = '127.0.0.1';//IP
$port = '14001';//端口
$username = 'xxx';//用户名
$password = 'xxx';//密码
config = [ 'host' => host,
'port' => port, 'username' => username,
'password' => password, 'topic' => topic,
'message' => $message,
];try {
this->publishMqttMessage(config);return true;} catch (\Throwable $e) {
return false;
}
}
/**
-
发布 MQTT 消息
-
@param array $config
-
@throws \RuntimeException
*/
protected function publishMqttMessage(array config) { address = sprintf('tcp://%s:%d', config['host'], config['port']);
socket = @stream_socket_client(address, errno, errstr, 5, STREAM_CLIENT_CONNECT);
if (socket === false) { throw new \RuntimeException("connect {address} failed: [{errno}] {errstr}");
}stream_set_timeout($socket, 5);
try {
this->writeMqttPacket(socket, this->buildMqttConnectPacket( 'platform_' . uniqid(), config['username'],
$config['password'],
));$connack = $this->readMqttPacket($socket); $this->assertMqttConnack($connack); $this->writeMqttPacket($socket, $this->buildMqttPublishPacket($config['topic'], $config['message'])); $this->writeMqttPacket($socket, chr(0xE0) . chr(0x00));} finally {
fclose($socket);
}
}
/**
-
CONNACK: payload[0]=session present, payload[1]=return code (0=accepted)
-
@param array $connack
-
@throws \RuntimeException
*/
protected function assertMqttConnack(array $connack)
{if ($connack['type'] !== 2) {
throw new \RuntimeException('invalid CONNACK packet');
}returnCode = this->mqttPayloadByte(connack['payload'], 1, 1); if (returnCode !== 0) {
throw new \RuntimeException('MQTT broker rejected connection, code ' . $returnCode);
}
}
/**
-
@param string $payload
-
@param int $offset
-
@param int $default
-
@return int
*/
protected function mqttPayloadByte(payload, offset, default = 0) { if (!is_string(payload) || !isset(payload[offset])) {
return $default;
}return ord(payload[offset]);
}
/**
- @param string $string
- @return string
*/
protected function encodeMqttString(string) { return pack('n', strlen(string)) . $string;
}
/**
-
@param string $clientId
-
@param string $username
-
@param string $password
-
@return string
*/
protected function buildMqttConnectPacket(clientId, username, password) { variableHeader = this->encodeMqttString('MQTT'); variableHeader .= chr(4);
variableHeader .= chr(0xC2); variableHeader .= pack('n', 60);payload = this->encodeMqttString(clientId); payload .= this->encodeMqttString(username);
payload .= this->encodeMqttString($password);body = variableHeader . $payload;
return chr(0x10) . this->encodeMqttRemainingLength(strlen(body)) . $body;
}
/**
-
@param resource $socket
-
@param string packet */ protected function writeMqttPacket(socket, packet) { written = fwrite(socket, packet);
if (written === false || written !== strlen($packet)) {
throw new \RuntimeException('failed to write MQTT packet');
}
}
/** -
@param resource $socket
-
@return array{type:int,payload:string}
*/
protected function readMqttPacket(socket) { fixedHeader = fread(socket, 1); if (fixedHeader === false || $fixedHeader === '') {
throw new \RuntimeException('failed to read MQTT packet header');
}remainingLength = this->decodeMqttRemainingLength(socket); payload = '';
if (remainingLength > 0) { payload = fread(socket, remainingLength);
if (payload === false || strlen(payload) !== $remainingLength) {
throw new \RuntimeException('failed to read MQTT packet payload');
}
}return [
'type' => ord(fixedHeader) >> 4, 'flags' => ord(fixedHeader) & 0x0F,
'payload' => $payload,
];
}
/** -
@param resource $socket
-
@return int
*/
protected function decodeMqttRemainingLength(socket) { multiplier = 1;
$value = 0;do {
byte = fread(socket, 1);
if (byte === false || byte === '') {
throw new \RuntimeException('failed to decode MQTT remaining length');
}$digit = ord($byte); $value += ($digit & 127) * $multiplier; $multiplier *= 128; $hasMore = ($digit & 128) !== 0;} while ($hasMore);
return $value;
}
/**
-
@param int $length
-
@return string
*/
protected function encodeMqttRemainingLength(length) { encoded = '';
do {
digit = length % 128;
length = intdiv(length, 128);
if (length > 0) { digit |= 0x80;
}
encoded .= chr(digit);
} while ($length > 0);return $encoded;
}
/**
-
@param string $topic
-
@param string $message
-
@return string
*/
protected function buildMqttPublishPacket(topic, message)
{
body = this->encodeMqttString(topic) . message;return chr(0x30) . this->encodeMqttRemainingLength(strlen(body)) . $body;
}
-
-
订阅消息
/**
-
MQTT 订阅监听
-
@param string $topic MQTT 主题
-
@param int timeout 监听超时秒数,0 表示持续监听直到 Ctrl+C */ public function subscribe(topic, timeout = 0) { host = '127.0.0.1';//IP
$port = '14001';//端口
$username = 'xxx';//用户名
$password = 'xxx';//密码
config = [ 'host' => host,
'port' => port, 'username' => username,
'password' => password, 'topic' => topic,
'timeout' => (int) $timeout,
];try {
this->subscribeMqttMessage(config);return true;} catch (\Throwable $e) {
return false;
}
}
/**
-
订阅 MQTT 主题并持续接收消息
-
@param array $config
-
@throws \RuntimeException
*/
protected function subscribeMqttMessage(array config) { address = sprintf('tcp://%s:%d', config['host'], config['port']);
socket = @stream_socket_client(address, errno, errstr, 5, STREAM_CLIENT_CONNECT);
if (socket === false) { throw new \RuntimeException("connect {address} failed: [{errno}] {errstr}");
}stream_set_blocking($socket, true);
if (config['timeout'] > 0) { stream_set_timeout(socket, $config['timeout']);
}try {
this->writeMqttPacket(socket, this->buildMqttConnectPacket( 'platform_sub_' . uniqid(), config['username'],
$config['password'],
));$connack = $this->readMqttPacket($socket); $this->assertMqttConnack($connack); $this->writeMqttPacket($socket, $this->buildMqttSubscribePacket($config['topic'])); $suback = $this->readMqttPacket($socket); $this->assertMqttSuback($suback); while (!feof($socket)) { if ($config['timeout'] > 0) { $read = [$socket]; $ready = stream_select($read, $write, $except, $config['timeout']); if ($ready === 0) { break; } if ($ready === false) { throw new \RuntimeException('failed to wait for MQTT message'); } } $packet = $this->readMqttPacket($socket); if ($packet['type'] === 3) { $publish = $this->parseMqttPublishPacket($packet['payload'], $packet['flags']); $data = $publish['message'];//订阅消息 //TODO 业务代码操作 continue; } if ($packet['type'] === 13) { $this->writeMqttPacket($socket, chr(0xD0) . chr(0x00)); } }} finally {
@fwrite(socket, chr(0xE0) . chr(0x00)); fclose(socket);
}
}
/**
-
@param string $payload
-
@param int $flags
-
@return array{topic:string,message:string}
*/
protected function parseMqttPublishPacket(payload, flags)
{
qos = (flags >> 1) & 0x03;
offset = 0; topicLength = unpack('n', substr(payload, offset, 2))[1];
offset += 2; topic = substr(payload, offset, topicLength); offset += $topicLength;if (qos > 0) { offset += 2;
}return [
'topic' => topic, 'message' => substr(payload, $offset),
];
}
/**
-
SUBACK: payload[0..1]=packet id, payload[2]=granted QoS (0x80=failure)
-
@param array $suback
-
@throws \RuntimeException
*/
protected function assertMqttSuback(array suback) { if (suback['type'] !== 9) {
throw new \RuntimeException('invalid SUBACK packet');
}returnCode = this->mqttPayloadByte(suback['payload'], 2, 0x80); if (returnCode === 0x80) {
throw new \RuntimeException('MQTT broker rejected subscription');
}
}
/**
-
@param string $payload
-
@param int $offset
-
@param int $default
-
@return int
*/
protected function mqttPayloadByte(payload, offset, default = 0) { if (!is_string(payload) || !isset(payload[offset])) {
return $default;
}return ord(payload[offset]);
}
/**
-
@param string $topic
-
@param int $packetId
-
@param int $qos
-
@return string
*/
protected function buildMqttSubscribePacket(topic, packetId = 1, qos = 0) { body = pack('n', packetId) . this->encodeMqttString(topic) . chr(qos);return chr(0x82) . this->encodeMqttRemainingLength(strlen(body)) . $body;
}
/**
- @param string $string
- @return string
*/
protected function encodeMqttString(string) { return pack('n', strlen(string)) . $string;
}
/**
-
@param int $length
-
@return string
*/
protected function encodeMqttRemainingLength(length) { encoded = '';
do {
digit = length % 128;
length = intdiv(length, 128);
if (length > 0) { digit |= 0x80;
}
encoded .= chr(digit);
} while ($length > 0);return $encoded;
}
/**
-
@param resource $socket
-
@param string packet */ protected function writeMqttPacket(socket, packet) { written = fwrite(socket, packet);
if (written === false || written !== strlen($packet)) {
throw new \RuntimeException('failed to write MQTT packet');
}
}
/** -
@param string $clientId
-
@param string $username
-
@param string $password
-
@return string
*/
protected function buildMqttConnectPacket(clientId, username, password) { variableHeader = this->encodeMqttString('MQTT'); variableHeader .= chr(4);
variableHeader .= chr(0xC2); variableHeader .= pack('n', 60);payload = this->encodeMqttString(clientId); payload .= this->encodeMqttString(username);
payload .= this->encodeMqttString($password);body = variableHeader . $payload;
return chr(0x10) . this->encodeMqttRemainingLength(strlen(body)) . $body;
}
/**
-
@param resource $socket
-
@return array{type:int,payload:string}
*/
protected function readMqttPacket(socket) { fixedHeader = fread(socket, 1); if (fixedHeader === false || $fixedHeader === '') {
throw new \RuntimeException('failed to read MQTT packet header');
}remainingLength = this->decodeMqttRemainingLength(socket); payload = '';
if (remainingLength > 0) { payload = fread(socket, remainingLength);
if (payload === false || strlen(payload) !== $remainingLength) {
throw new \RuntimeException('failed to read MQTT packet payload');
}
}return [
'type' => ord(fixedHeader) >> 4, 'flags' => ord(fixedHeader) & 0x0F,
'payload' => $payload,
];
}
/**
-
@param resource $socket
-
@return int
*/
protected function decodeMqttRemainingLength(socket) { multiplier = 1;
$value = 0;do {
byte = fread(socket, 1);
if (byte === false || byte === '') {
throw new \RuntimeException('failed to decode MQTT remaining length');
}$digit = ord($byte); $value += ($digit & 127) * $multiplier; $multiplier *= 128; $hasMore = ($digit & 128) !== 0;} while ($hasMore);
return $value;
}
/**
-
CONNACK: payload[0]=session present, payload[1]=return code (0=accepted)
-
@param array $connack
-
@throws \RuntimeException
*/
protected function assertMqttConnack(array $connack)
{if ($connack['type'] !== 2) {
throw new \RuntimeException('invalid CONNACK packet');
}returnCode = this->mqttPayloadByte(connack['payload'], 1, 1); if (returnCode !== 0) {
throw new \RuntimeException('MQTT broker rejected connection, code ' . $returnCode);
}
}
-