PHP实现对接MQTT协议

一:什么是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协议示例

  1. 发布消息

    /**

    • 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;
      }

  2. 订阅消息

    /**

    • 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);
      }
      }

相关推荐
牧羊人.33320 分钟前
动手学深度学习 01:核心组件与完整训练流程
开发语言·人工智能·深度学习
晓晓_za89866822 分钟前
GEO 搜索源码白帽合规改造:适配各大 AI 信源收录规则
java·开发语言·人工智能·性能优化·开源
一条破秋裤34 分钟前
C-数据类型
c语言·开发语言·chrome
君顾139 分钟前
从0到1课后辅导管理系统开发实战:需求文档、架构设计全复盘
java·开发语言
神仙别闹40 分钟前
基于C++ MFC RSA 实现加密程序
开发语言·c++·mfc
泷泷_1 小时前
CISP-PTE实操题(自写靶场与题类似或变型)
php·文件上传·cisp-pte·cisp·文件包含·sql注入
增量星球1 小时前
一个 Python 脚本 + LLM 怎么替代向量检索?ProjQA 技能架构原理深度解析
开发语言·python·架构·embedding·skill
探数API小喇叭1 小时前
基站查询 API 怎么用?LAC、CELLID 参数详解与实战】
java·开发语言·api·基站定位
Mr.Lu ‍1 小时前
C++开发,使用openCV API对图片进行压缩
开发语言·c++·opencv