用 C 语言快速入门 MQTT:从 Broker、发布订阅到双机通信
MQTT 是一种轻量级消息协议,特别适合物联网、嵌入式设备、边缘网关和网络不稳定的场景。很多人第一次接触 MQTT 时,最容易混淆的不是 API,而是角色关系:
broker到底是谁启动的?subscriber是不是先启动就能"生成" broker?- 两台不同机器之间,是不是互相订阅就能通信?
本文用一个最小的 C 语言示例,把 MQTT 最核心的工作方式讲清楚,并给出一套可以直接跑起来的实践路径。
一、MQTT 的核心不是"谁连谁",而是"都连到 Broker"
MQTT 里有 3 个最重要的角色:
Broker:消息中转服务,例如mosquittoPublisher:发布消息的一方Subscriber:订阅消息的一方
很多初学者会误以为:
- 订阅者先启动,就会让 broker 存在
- 发布者会自动找到订阅者并直接发消息
- 两台机器之间是彼此点对点通信
这些理解都不准确。
MQTT 的真实模型是:
broker先作为一个独立服务运行sub连接到broker,并订阅某个topicpub也连接到同一个broker,并向某个topic发布消息broker根据订阅关系,把消息转发给匹配的订阅者
所以更准确的说法不是"sub 注册 broker",而是:
sub 连接到 broker,并在 broker 上登记自己关心的 topic。
二、最小时序图
下面这张图基本可以概括 MQTT 最小工作流程:

从时序上看,有两个关键事实:
sub不会创建brokerpub不是直接发给sub,而是发给broker
三、Broker 是谁启动的?
broker 不是 pub 或 sub 自动创建的,它本身就是一个独立程序,通常由你手动启动,或者由系统服务自动拉起。
本文使用的是 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/pubbuild/sub
五、发布者做什么?
发布者的逻辑很简单:
- 初始化 MQTT 库
- 创建客户端
- 连接 broker
- 向某个 topic 发布消息
- 断开连接
最小发布者代码如下:
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。
六、订阅者做什么?
订阅者的工作流程是:
- 初始化 MQTT 库
- 创建客户端
- 连接 broker
- 在连接成功后执行订阅
- 持续等待 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成功连接 brokersub成功订阅 topicpub成功向 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/demohome/livingroom/temprobot/rk3568/status
Payload
Payload 是消息内容,例如:
"hello""26.5"- JSON 字符串
QoS
QoS 表示消息可靠性等级:
0:尽力而为1:至少一次2:只有一次
入门阶段先使用 QoS 0 就足够了。
Broker
Broker 是消息中转站,它的职责是接收消息并按订阅关系转发,而不是处理业务逻辑。
十二、本文的实践总结
用一句话总结 MQTT:
pub 和 sub 都不是直接互相通信,而是都连接到同一个 broker,由 broker 负责消息中转。
所以一个最小的学习路径应该是:
- 先启动
mosquitto - 再运行
sub连接 broker 并订阅 topic - 再运行
pub连接 broker 并发布消息 - 观察 broker 把消息转发给订阅者
如果这条链路已经跑通,下一步就可以继续学习:
- JSON 消息格式
- QoS 1 / QoS 2
- 断线重连
- retain 消息
- will 消息
- 设备状态上报
- 设备控制指令下发
从工程实践角度看,MQTT 最值得学习的地方其实不是 API 本身,而是它背后的解耦思想:
发送方不需要知道接收方是谁,只需要把消息交给 broker;接收方也不需要知道发送方是谁,只需要订阅自己关心的 topic。正是这种低耦合,让 MQTT 非常适合物联网系统。