第4章:MQTT协议入门:发布/订阅模型,搭建本地Mosquitto Broker,实现灯光的远程开关

好,咱们继续往前走。

上一章我们把硬件点亮了,也调通了PWM调光。但说实话,那只是单机版。你想想看,如果每次开关灯都要跑到设备跟前去按按钮,那还叫什么智能家居?

这一章,我们来解决远程控制的问题。核心就是MQTT协议。

4.1 为什么是MQTT?不是HTTP?

很多刚入行的朋友会问我:「为什么不用HTTP?RESTful API不是挺成熟的吗?」

嗯,这个问题我当年也纠结过。我在一个早期的物联网项目里,确实尝试过用HTTP做设备控制。结果呢?设备在弱网环境下频繁断连,每次重连都要重新握手,功耗还高得吓人。后来我彻底转向了MQTT。

MQTT和HTTP最大的区别在于:

特性 HTTP MQTT
通信模型 请求/响应 发布/订阅
连接开销 每次请求都要TCP握手 长连接,一次握手
实时性 需要轮询 服务器主动推送
协议开销 头部大(几百字节) 最小仅2字节头部
适用场景 Web应用、API调用 物联网、传感器、移动端

说白了,MQTT就是为物联网而生的。它轻量、省电、支持双向通信。你想想看,一个ESP8266模块,用MQTT发一条控制指令,整个报文可能才十几字节。换成HTTP?光头部就几百字节了。

4.2 发布/订阅模型:核心概念

MQTT的核心思想,就是「发布/订阅」模型。我习惯把它比作一个微信群。

  • Broker(代理):就是微信群。它不生产消息,只负责转发。
  • Publisher(发布者):在群里说话的人。他发一条消息到群里。
  • Subscriber(订阅者):在群里听消息的人。他只关心自己感兴趣的话题。
  • Topic(主题):就是话题标签。比如#灯光控制、#温度上报。

举个例子:

你的手机App是发布者,它发了一条消息到主题 home/livingroom/light/set,内容是 {"state": "ON"}。你的ESP8266订阅了这个主题,Broker就把这条消息推送给它。ESP8266收到后,执行开灯操作。

关键点:发布者和订阅者不需要知道对方的存在。它们只跟Broker打交道。这就实现了「解耦」——你换手机App,不影响设备;你换设备,也不影响App。

4.3 Topic的设计:别小看这个

Topic的设计,说实话,比你想的重要得多。我在一个项目中吃过亏——一开始随便起了几个Topic名字,结果设备多了以后,管理起来一团糟。

我建议你遵循这样的层级结构:

home/<房间>/<设备类型>/<设备ID>/<属性>

比如:

  • home/livingroom/light/001/state —— 上报灯的状态
  • home/livingroom/light/001/set —— 接收控制指令
  • home/livingroom/temperature —— 上报温度

MQTT还支持通配符:

  • + 匹配单层:home/+/light/001/set 匹配所有房间的灯
  • # 匹配多层:home/livingroom/# 匹配客厅所有设备

我的习惯:Topic命名全部用小写字母,单词之间用斜杠分隔。不要用空格、中文、特殊字符。否则后面解析的时候会踩坑。

4.4 搭建本地Mosquitto Broker

好,理论说完了,咱们动手。我选择Mosquitto,因为它轻量、稳定、开源。你在树莓派、Ubuntu、甚至Windows上都能跑。

在Ubuntu/Debian上安装:

sudo apt update
sudo apt install mosquitto mosquitto-clients

启动服务:

sudo systemctl start mosquitto
sudo systemctl enable mosquitto  # 开机自启

验证是否运行:

sudo systemctl status mosquitto

看到 active (running) 就对了。

测试一下:

打开两个终端窗口。

终端1(订阅者):

mosquitto_sub -h localhost -t "test/topic"

终端2(发布者):

mosquitto_pub -h localhost -t "test/topic" -m "Hello MQTT"

你会看到终端1收到了消息。嗯,就是这么简单。

注意:默认配置下,Mosquitto允许匿名连接,且不加密。这在局域网内调试没问题。但如果要暴露到公网,一定要配置用户名密码和TLS加密。我后面会专门讲安全配置。

4.5 实现灯光的远程开关

现在,我们把MQTT和之前的灯光硬件结合起来。

硬件接线回顾:

  • ESP8266 GPIO4 → LED正极(通过220Ω电阻)
  • LED负极 → GND

代码实现:

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

// WiFi配置
const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";

// MQTT Broker配置
const char* mqtt_server = "192.168.1.100";  // 你的Broker IP
const int mqtt_port = 1883;

// Topic定义
const char* topic_state = "home/livingroom/light/001/state";
const char* topic_set = "home/livingroom/light/001/set";

WiFiClient espClient;
PubSubClient client(espClient);

// LED引脚
const int ledPin = 4;

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);
  
  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("连接到 ");
  Serial.println(ssid);
  
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("WiFi 已连接");
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("收到消息 [");
  Serial.print(topic);
  Serial.print("] ");
  
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);
  
  // 解析控制指令
  if (String(topic) == topic_set) {
    if (message == "ON") {
      digitalWrite(ledPin, HIGH);
      client.publish(topic_state, "ON");
      Serial.println("灯已打开");
    } else if (message == "OFF") {
      digitalWrite(ledPin, LOW);
      client.publish(topic_state, "OFF");
      Serial.println("灯已关闭");
    }
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("尝试连接MQTT...");
    if (client.connect("ESP8266_Light_001")) {
      Serial.println("已连接");
      client.subscribe(topic_set);
      client.publish(topic_state, "OFF");
    } else {
      Serial.print("失败,rc=");
      Serial.print(client.state());
      Serial.println(" 5秒后重试");
      delay(5000);
    }
  }
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

代码要点:

  • client.setCallback(callback) —— 注册回调函数,收到消息时自动触发
  • client.subscribe(topic_set) —— 订阅控制主题
  • client.publish(topic_state, "ON") —— 发布状态,让App知道灯已经开了
  • client.loop() —— 必须放在loop里,保持MQTT连接活跃

4.6 测试远程开关

上传代码到ESP8266后,打开串口监视器。你会看到它先连WiFi,再连MQTT Broker。

然后,在电脑上执行:

mosquitto_pub -h 192.168.1.100 -t "home/livingroom/light/001/set" -m "ON"

灯亮了!再发一条:

mosquitto_pub -h 192.168.1.100 -t "home/livingroom/light/001/set" -m "OFF"

灯灭了。

你还可以订阅状态主题,看看设备有没有正确回复:

mosquitto_sub -h 192.168.1.100 -t "home/livingroom/light/001/state"

避坑指南:我曾经遇到过一个问题——灯能开不能关。查了半天,发现是回调函数里用 == 比较字符串,但收到的payload末尾有换行符。后来改用 message.trim() 就好了。嗯,细节决定成败。

4.7 本章小结

这一章我们做了三件事:

  1. 理解了MQTT的发布/订阅模型,以及它为什么适合物联网
  2. 在本地搭建了Mosquitto Broker
  3. 用ESP8266实现了灯光的远程开关

你现在已经可以坐在沙发上,用电脑或者手机App远程控制家里的灯了。下一章,我们会加入调光功能,让灯光不只是开关,还能调节亮度。而且,我们会用上MQTT的QoS机制,保证指令不丢失。

准备好了吗?我们继续。