快速入门 MQTT:从 Broker、发布订阅到双机通信

用 C 语言快速入门 MQTT:从 Broker、发布订阅到双机通信

MQTT 是一种轻量级消息协议,特别适合物联网、嵌入式设备、边缘网关和网络不稳定的场景。很多人第一次接触 MQTT 时,最容易混淆的不是 API,而是角色关系:

  • broker 到底是谁启动的?
  • subscriber 是不是先启动就能"生成" broker?
  • 两台不同机器之间,是不是互相订阅就能通信?

本文用一个最小的 C 语言示例,把 MQTT 最核心的工作方式讲清楚,并给出一套可以直接跑起来的实践路径。


一、MQTT 的核心不是"谁连谁",而是"都连到 Broker"

MQTT 里有 3 个最重要的角色:

  • Broker:消息中转服务,例如 mosquitto
  • Publisher:发布消息的一方
  • Subscriber:订阅消息的一方

很多初学者会误以为:

  • 订阅者先启动,就会让 broker 存在
  • 发布者会自动找到订阅者并直接发消息
  • 两台机器之间是彼此点对点通信

这些理解都不准确。

MQTT 的真实模型是:

  1. broker 先作为一个独立服务运行
  2. sub 连接到 broker,并订阅某个 topic
  3. pub 也连接到同一个 broker,并向某个 topic 发布消息
  4. broker 根据订阅关系,把消息转发给匹配的订阅者

所以更准确的说法不是"sub 注册 broker",而是:

sub 连接到 broker,并在 broker 上登记自己关心的 topic。


二、最小时序图

下面这张图基本可以概括 MQTT 最小工作流程:

从时序上看,有两个关键事实:

  • sub 不会创建 broker
  • pub 不是直接发给 sub,而是发给 broker

三、Broker 是谁启动的?

broker 不是 pubsub 自动创建的,它本身就是一个独立程序,通常由你手动启动,或者由系统服务自动拉起。

本文使用的是 mosquitto

在 Ubuntu 上安装:

bash 复制代码
sudo apt-get update
sudo apt-get install -y libmosquitto-dev mosquitto mosquitto-clients make

这里分别安装了:

  • mosquitto:Broker 服务
  • mosquitto-clients:命令行测试工具
  • libmosquitto-dev:C 语言开发库

broker 作为系统服务启动:

bash 复制代码
sudo systemctl start mosquitto
sudo systemctl status mosquitto --no-pager
sudo systemctl enable mosquitto

检查是否在运行:

bash 复制代码
systemctl is-active mosquitto

如果输出是 active,说明 broker 已经存在并在监听。

如果只是学习,也可以前台启动,方便直接看日志:

bash 复制代码
mosquitto -v

四、最小 C 语言工程结构

一个最小 MQTT C 工程不需要复杂框架,只要 3 个文件就够了:

  • src/pub.c:发布者
  • src/sub.c:订阅者
  • Makefile:编译脚本

编译:

bash 复制代码
make

生成:

  • build/pub
  • build/sub

五、发布者做什么?

发布者的逻辑很简单:

  1. 初始化 MQTT 库
  2. 创建客户端
  3. 连接 broker
  4. 向某个 topic 发布消息
  5. 断开连接

最小发布者代码如下:

c 复制代码
#include <mosquitto.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void print_usage(const char *program)
{
    fprintf(stderr, "usage: %s <host> <port> <topic> <message> [qos]\n", program);
}

int main(int argc, char *argv[])
{
    const char *host;
    const char *topic;
    const char *message;
    int port;
    int qos = 0;
    int rc;
    struct mosquitto *mosq = NULL;

    if (argc < 5) {
        print_usage(argv[0]);
        return 1;
    }

    host = argv[1];
    port = atoi(argv[2]);
    topic = argv[3];
    message = argv[4];

    if (argc >= 6) {
        qos = atoi(argv[5]);
    }

    mosquitto_lib_init();

    mosq = mosquitto_new(NULL, true, NULL);
    if (mosq == NULL) {
        fprintf(stderr, "failed to create mosquitto client\n");
        mosquitto_lib_cleanup();
        return 1;
    }

    rc = mosquitto_connect(mosq, host, port, 60);
    if (rc != MOSQ_ERR_SUCCESS) {
        fprintf(stderr, "connect failed: %s\n", mosquitto_strerror(rc));
        mosquitto_destroy(mosq);
        mosquitto_lib_cleanup();
        return 1;
    }

    rc = mosquitto_loop_start(mosq);
    if (rc != MOSQ_ERR_SUCCESS) {
        fprintf(stderr, "loop start failed: %s\n", mosquitto_strerror(rc));
        mosquitto_destroy(mosq);
        mosquitto_lib_cleanup();
        return 1;
    }

    sleep(1);

    rc = mosquitto_publish(
        mosq,
        NULL,
        topic,
        (int)strlen(message),
        message,
        qos,
        false);
    if (rc != MOSQ_ERR_SUCCESS) {
        fprintf(stderr, "publish failed: %s\n", mosquitto_strerror(rc));
        mosquitto_disconnect(mosq);
        mosquitto_loop_stop(mosq, false);
        mosquitto_destroy(mosq);
        mosquitto_lib_cleanup();
        return 1;
    }

    printf("published\n");
    printf("  host: %s\n", host);
    printf("  port: %d\n", port);
    printf("  topic: %s\n", topic);
    printf("  payload: %s\n", message);
    printf("  qos: %d\n", qos);

    sleep(1);

    mosquitto_disconnect(mosq);
    mosquitto_loop_stop(mosq, false);
    mosquitto_destroy(mosq);
    mosquitto_lib_cleanup();

    return 0;
}

发布者的本质可以概括成一句话:

把一段数据作为 payload 发布到指定的 topic


六、订阅者做什么?

订阅者的工作流程是:

  1. 初始化 MQTT 库
  2. 创建客户端
  3. 连接 broker
  4. 在连接成功后执行订阅
  5. 持续等待 broker 转发消息

最小订阅者代码如下:

c 复制代码
#include <mosquitto.h>
#include <stdio.h>
#include <stdlib.h>

struct app_config {
    const char *topic;
    int qos;
};

static void print_usage(const char *program)
{
    fprintf(stderr, "usage: %s <host> <port> <topic> [qos]\n", program);
}

static void on_connect(struct mosquitto *mosq, void *userdata, int rc)
{
    struct app_config *config = userdata;
    int sub_rc;

    if (rc != 0) {
        fprintf(stderr, "connect callback failed: %s\n", mosquitto_connack_string(rc));
        return;
    }

    printf("connected, subscribing to topic: %s\n", config->topic);

    sub_rc = mosquitto_subscribe(mosq, NULL, config->topic, config->qos);
    if (sub_rc != MOSQ_ERR_SUCCESS) {
        fprintf(stderr, "subscribe failed: %s\n", mosquitto_strerror(sub_rc));
    }
}

static void on_message(
    struct mosquitto *mosq,
    void *userdata,
    const struct mosquitto_message *message)
{
    (void)mosq;
    (void)userdata;

    printf("message arrived\n");
    printf("  topic: %s\n", message->topic);
    printf("  payload: %.*s\n", message->payloadlen, (const char *)message->payload);
    printf("  qos: %d\n", message->qos);
    printf("\n");
}

int main(int argc, char *argv[])
{
    const char *host;
    int port;
    int rc;
    struct app_config config;
    struct mosquitto *mosq = NULL;

    if (argc < 4) {
        print_usage(argv[0]);
        return 1;
    }

    setvbuf(stdout, NULL, _IONBF, 0);

    host = argv[1];
    port = atoi(argv[2]);
    config.topic = argv[3];
    config.qos = 0;

    if (argc >= 5) {
        config.qos = atoi(argv[4]);
    }

    mosquitto_lib_init();

    mosq = mosquitto_new(NULL, true, &config);
    if (mosq == NULL) {
        fprintf(stderr, "failed to create mosquitto client\n");
        mosquitto_lib_cleanup();
        return 1;
    }

    mosquitto_connect_callback_set(mosq, on_connect);
    mosquitto_message_callback_set(mosq, on_message);

    rc = mosquitto_connect(mosq, host, port, 60);
    if (rc != MOSQ_ERR_SUCCESS) {
        fprintf(stderr, "connect failed: %s\n", mosquitto_strerror(rc));
        mosquitto_destroy(mosq);
        mosquitto_lib_cleanup();
        return 1;
    }

    printf("waiting for messages on %s:%d ...\n", host, port);

    rc = mosquitto_loop_forever(mosq, -1, 1);
    if (rc != MOSQ_ERR_SUCCESS) {
        fprintf(stderr, "loop failed: %s\n", mosquitto_strerror(rc));
        mosquitto_destroy(mosq);
        mosquitto_lib_cleanup();
        return 1;
    }

    mosquitto_destroy(mosq);
    mosquitto_lib_cleanup();

    return 0;
}

这段代码最关键的地方在于:

  • 订阅不是"等发布者来找我"
  • 而是"我先连接 broker,并告诉 broker 我关心哪个 topic"

七、单机运行:先把最小链路跑通

假设 broker 就在当前机器上,地址是 localhost:1883

先编译:

bash 复制代码
make

第一个终端运行订阅者:

bash 复制代码
./build/sub localhost 1883 learn/demo

第二个终端运行发布者:

bash 复制代码
./build/pub localhost 1883 learn/demo "hello from c mqtt"

如果一切正常,订阅端会输出:

text 复制代码
waiting for messages on localhost:1883 ...
connected, subscribing to topic: learn/demo
message arrived
  topic: learn/demo
  payload: hello from c mqtt
  qos: 0

这表示整条消息链路已经打通:

  • sub 成功连接 broker
  • sub 成功订阅 topic
  • pub 成功向 broker 发布消息
  • broker 成功把消息转发给 sub

八、两台不同机器怎么通信?

这是 MQTT 初学阶段最容易误解的问题。

错误想象通常是这样的:

text 复制代码
机器A(pub) ---> 机器B(sub)

但 MQTT 默认不是点对点直连模型。

更常见、也更正确的部署方式是:

text 复制代码
机器A(pub/sub) ---> Broker所在机器
机器B(pub/sub) ---> Broker所在机器

也就是说:

  • 两台机器不是互相订阅对方
  • 它们是都连接到同一个 broker
  • topic 是登记在 broker 上的,不是登记在另一台机器上

示例

假设你的虚拟机 IP 是 192.168.64.7,并且这台虚拟机运行着 mosquitto

方案 1:虚拟机订阅,本机发布

虚拟机上运行:

bash 复制代码
./build/sub 192.168.64.7 1883 learn/demo

本机上运行:

bash 复制代码
./build/pub 192.168.64.7 1883 learn/demo "hello from mac"
方案 2:本机订阅,虚拟机发布

本机上运行:

bash 复制代码
./build/sub 192.168.64.7 1883 learn/demo

虚拟机上运行:

bash 复制代码
./build/pub 192.168.64.7 1883 learn/demo "hello from rk3568"

无论哪种方式,只要双方都连接到了同一个 broker 地址,就可以互相通信。


九、远程连接 Broker 时要检查什么?

如果不同机器之间连不上,通常先检查下面 3 件事。

1. Broker 是否启动

bash 复制代码
systemctl is-active mosquitto

2. 1883 端口是否在监听

bash 复制代码
ss -lnt | grep 1883

3. Broker 是否允许外部连接

学习阶段可以使用一份最小配置:

conf 复制代码
listener 1883 0.0.0.0
allow_anonymous true

然后前台启动:

bash 复制代码
mosquitto -c /tmp/mosquitto.conf -v

这个配置的意思是:

  • 监听所有网卡地址
  • 暂时允许匿名连接

这适合本地实验,但不适合生产环境。


十、几个最容易混淆的点

1. Subscriber 不会创建 Broker

sub 只是客户端,它必须先连接一个已经存在的 broker。

2. Publisher 不会自动发现 Subscriber

pub 只知道 broker 地址,并不知道谁在订阅。

3. 没有订阅者时,发布不一定报错

很多情况下,发布操作依然成功,只是没有任何订阅者收到消息。

4. Broker 和 Broker 不会自动互通

如果要让两个 broker 之间互联,需要额外配置 bridge 或集群方案,这不是 MQTT 的默认行为。


十一、初学 MQTT 最应该先掌握的 4 个概念

Topic

Topic 是消息分类路径,例如:

  • learn/demo
  • home/livingroom/temp
  • robot/rk3568/status

Payload

Payload 是消息内容,例如:

  • "hello"
  • "26.5"
  • JSON 字符串

QoS

QoS 表示消息可靠性等级:

  • 0:尽力而为
  • 1:至少一次
  • 2:只有一次

入门阶段先使用 QoS 0 就足够了。

Broker

Broker 是消息中转站,它的职责是接收消息并按订阅关系转发,而不是处理业务逻辑。


十二、本文的实践总结

用一句话总结 MQTT:

pubsub 都不是直接互相通信,而是都连接到同一个 broker,由 broker 负责消息中转。

所以一个最小的学习路径应该是:

  1. 先启动 mosquitto
  2. 再运行 sub 连接 broker 并订阅 topic
  3. 再运行 pub 连接 broker 并发布消息
  4. 观察 broker 把消息转发给订阅者

如果这条链路已经跑通,下一步就可以继续学习:

  • JSON 消息格式
  • QoS 1 / QoS 2
  • 断线重连
  • retain 消息
  • will 消息
  • 设备状态上报
  • 设备控制指令下发

从工程实践角度看,MQTT 最值得学习的地方其实不是 API 本身,而是它背后的解耦思想:

发送方不需要知道接收方是谁,只需要把消息交给 broker;接收方也不需要知道发送方是谁,只需要订阅自己关心的 topic。正是这种低耦合,让 MQTT 非常适合物联网系统。

相关推荐
LCG元2 小时前
STM32实战:基于STM32F103的MQTT协议通信(EMQ X Broker)
stm32·单片机·嵌入式硬件
zmj3203242 小时前
51单片机
单片机·嵌入式硬件·51单片机
zmj3203243 小时前
MCS-51单片机
单片机·嵌入式硬件·51单片机
小柯博客3 小时前
从零开始打造 OpenSTLinux 6.6 Yocto 系统 - STM32MP2(基于STM32CubeMX)(八)
c语言·git·stm32·单片机·嵌入式硬件·嵌入式·yocto
421!12 小时前
GPIO工作原理以及核心
开发语言·单片机·嵌入式硬件·学习
cmpxr_16 小时前
【单片机】STM32的启动流程(Keil)
stm32·单片机·嵌入式硬件
广药门徒16 小时前
嵌入式常用通信协议速率对比及布线要点全解析
单片机·嵌入式硬件
cmpxr_18 小时前
【单片机】RAM和ROM
单片机·嵌入式硬件
信息安全专家20 小时前
sigmastar SSD222D编译问题总结2-dash问题
linux·嵌入式硬件·dash