上周有个做农业大棚的老哥找我,说他那套 ESP8266 采集温湿度的玩意儿,折腾了三个礼拜,数据一直上不了云。我问他卡在哪,他说 MQTT 连 broker 没问题,串口打印也正常,就是没地想:数据发出去以后放哪儿?自己拿台旧笔记本开个 MQTT 加 MySQL,停电就全完;买个云服务器吧,光配环境、写接口、防注入这一套下来,人先累趴了。这其实是很多单片机玩家的真实处境——传输这一段打通了,存储和分发那一段又断了。
单片机MQTT传输数据,为什么最后还是卡在服务端?
先说个很多人没意识到的误区:MQTT 本身不是数据库。它是个消息总线,负责把 payload 从 A 搬到 B,搬完就完事。你发一条 {"temp":26.5} 到一个 topic,如果那一刻没有订阅者在线上,这条消息要么被 broker 丢掉,要么靠 retained 留一条最新的,历史数据你一条也翻不出来。所以「MQTT 上云」这句话,真正的难点从来不在单片机侧,而在消息落地之后:谁存、怎么存、怎么按顺序取、怎么防止重复。
自己搭这一层,成本远比想象中高。你要有公网 IP 或内网穿透,要有 MySQL 表结构设计,要写鉴权,要防 SQL 注入,还要处理「设备重启后补发导致重复写入」这种细碎问题。对个人开发者和小团队来说,这些活干完,项目本身的业务逻辑反而没时间写了。我一般的建议是:传输归传输,存储分发交给专门的轻量接口,两头各干各的,别硬凑。
先理清:MQTT负责传输,云递数据负责存储与分发
把职责拆开看就清楚了。MQTT 负责「送到」,云递数据负责「存住 + 按顺序发出去」。云递数据是一个轻量级云端数据存储与分发平台,不用你买服务器和数据库,通过 HTTP/JSON 接口就能让软件、网站、脚本、硬件设备存取数据。对单片机来说,它就是个「云端的收纳箱 + 取件柜」:你往里投,它按投递顺序排队;你来取,取走即标记已读,不会重复下发。
两者不是二选一的关系。你可以继续用 MQTT 做设备间的实时联动,同时把需要留档、需要被后台按顺序消费的数据,通过一个 HTTP 请求转存到云递数据。任何能发 HTTP 请求的语言和设备都能接入,ESP8266/ESP32、Python、PHP、curl、PowerShell 都行。下面这张表把两种路径的差别摆一下,你就知道什么时候该用哪个:
| 对比项 | 纯 MQTT Broker | 云递数据 HTTP 接口 |
|---|---|---|
| 核心职责 | 消息实时转发 | 数据存储与队列式分发 |
| 历史数据 | 默认不保留,需额外落库 | 写入即留存,控制台可查 |
| 消费语义 | 订阅者各自收,无已读概念 | FIFO 读取,取走即标记已读 |
| 重复写入 | 需自行去重 | 内容完全相同自动哈希去重 |
| 部署成本 | 需自备 broker 与数据库 | 注册即用,也可私有化部署 |
| 单片机接入 | 需 MQTT 库与稳定长连接 | 一个 HTTP POST 即可 |
我的经验是:设备侧网络不稳、又需要留档的场景,HTTP 短连接反而更省心,断了重发就行;对实时性要求高的控制指令,MQTT 更合适。两者搭配,各司其职。
分步实现:从注册APIKey到ESP8266跑通第一个上报
第一步,注册账号,后台会直接给你一个 APIKey,这是你所有接口调用的通行证,别外传。第二步,规划数据名(dataset),比如 dht11_room1,不同设备、不同传感器用不同数据名,后面读取时按数据名拉取才不串。第三步,才是写代码。
云递数据的推送接口只有一个:POST http://你的域名/api/push.php,JSON 请求体里带 api_key、dataset 和 data,data 可以是对象、数组,也可以是一段纯文本。api_key 也可以放在 X-Api-Key 请求头里传。ESP8266 上我用的是 Arduino 的 ESP8266HTTPClient,代码不长,直接能跑:
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
const char* ssid = "你的WiFi名";
const char* password = "你的WiFi密码";
const char* apiUrl = "http://你的域名/api/push.php";
const char* apiKey = "你的APIKey";
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi已连接");
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
WiFiClient client;
HTTPClient http;
http.begin(client, apiUrl);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-Api-Key", apiKey); // 密钥走请求头,比放URL里安全
// 模拟采集,实际换成你的传感器读数
float temp = 26.5;
float humi = 61.2;
String body = "{\"dataset\":\"dht11_room1\",\"data\":{\"temp\":";
body += String(temp, 1);
body += ",\"humi\":";
body += String(humi, 1);
body += "}}";
int code = http.POST(body);
Serial.print("HTTP状态码: ");
Serial.println(code);
if (code > 0) {
Serial.println(http.getString());
}
http.end();
}
delay(60000); // 一分钟上报一次,按需调整
}
有几个细节值得提醒。一是 WiFiClient 要作为参数传给 http.begin,新版库不加这个会编译不过;二是 delay 别设太短,一分钟一次对温湿度足够了,也省得触发接口限流;三是如果设备是老程序,不想拼 JSON,也可以用兼容写法:http://你的域名/api/push.php?api_key=你的APIKey&dataset=yyy 配合纯文本请求体,直接存一段字符串,改造老代码时很省事。
烧录进去,串口看到状态码 200,再去控制台看,数据名下面应该已经多了一条记录。到这一步,「上云最后一公里」算是通了。
分步实现:用读取接口做FIFO指令下发与分页拉取
存进去只是上半场。很多场景需要反过来:后台或另一台设备把指令写进某个数据名,单片机定时来取,取到就执行。这正是读取接口的用武之地——POST http://你的域名/api/pull.php,按写入顺序 FIFO 读取,取走即标记已读,不会重复下发,还支持分页和按数据名读取。这个「取走即已读」的语义,天然适合做指令队列,不用担心同一条指令被执行两遍。
下面这段是 ESP8266 侧轮询指令的写法,每 10 秒去拉一次,取到指令就打印出来:
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
const char* pullUrl = "http://你的域名/api/pull.php";
const char* apiKey = "你的APIKey";
void pollCommand() {
if (WiFi.status() != WL_CONNECTED) return;
WiFiClient client;
HTTPClient http;
http.begin(client, pullUrl);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-Api-Key", apiKey);
// 按数据名读取,page/limit 控制分页
String body = "{\"dataset\":\"cmd_room1\",\"page\":1,\"limit\":5}";
int code = http.POST(body);
if (code == 200) {
String resp = http.getString();
Serial.println("拉取结果: " + resp);
// 这里解析 JSON,根据返回内容执行开关灯等动作
// 因为取走即标记已读,同一批指令不会被重复下发
} else {
Serial.print("拉取失败, 状态码: ");
Serial.println(code);
}
http.end();
}
void setup() {
Serial.begin(115200);
WiFi.begin("你的WiFi名", "你的WiFi密码");
while (WiFi.status() != WL_CONNECTED) delay(500);
}
void loop() {
pollCommand();
delay(10000);
}
关于分页,我的建议是:单片机侧把 limit 设小一点,比如 5 到 10,一次别贪多,避免内存吃紧解析失败;后台消费侧可以设大一些,提高吞吐。分页参数按你的实际接口返回结构来解析,别写死下标。另外,指令数据名和上报数据名一定要分开,比如上报用 dht11_room1,下信用 cmd_room1,否则队列会互相干扰。
运行效果:控制台里看到条数、容量与已读状态
代码跑起来之后,最有说服力的验证就是看控制台。登录后台,你能看到每个数据名下的条数、占用容量,以及每条记录的已读状态。设备每分钟推一条,条数就往上走;后台调一次 pull,对应记录的状态变成已读,再调一次不会重复下发。数据还能下载、编辑、清空、删除,调试阶段清空一下重新来,比手动删表省事得多。
我自己的排查习惯是:先看条数对不对得上设备上报次数,再看容量增长是否异常(异常往往是重复写入或 payload 过大),最后看已读状态是否符合消费节奏。这三项对上了,链路基本就是健康的。如果条数涨得比预期快,先别怀疑平台,回头看看设备是不是在断网重连后疯狂补发。
常见坑与注意事项:密钥泄露、重复写入与会员到期
密钥泄露是最常见也最要命的一个。APIKey 写在固件里,固件一旦流出就等于把仓库钥匙给了别人。能放请求头就别拼在 URL 上,URL 会进日志、会被截图;固件别随便分享;发现异常立刻在后台换密钥重新烧录。推送接口写了鉴权、PDO 预处理防注入、CSRF 防护、登录防爆破和接口限流,但钥匙在你手里,别自己弄丢。
重复写入方面,平台写入时会自动规范化并计算哈希去重,内容完全相同只保留第一条。这意味着设备重连补发同样的数据,不会把队列撑爆。但要注意「完全相同」这个前提,如果你在 payload 里带了时间戳,那每次内容都不一样,自然不会去重——这是设计上的取舍,不是 bug,想利用去重就别塞随机字段。
会员到期这块很多人担心。新用户注册送 3 天会员免费试用,正式会员是月卡 19 元、年卡 99 元,卡密充值,YK 开头月卡、NK 开头年卡,多张可叠加、到期顺延。到期后数据保留不删除,只是暂停推送和读取,续费就恢复。所以别到期了发现设备发不上去就以为数据没了,续上就回来了。另外邀请好友开月卡,推荐人得 10 天,开年卡得 30 天,天数以后台配置为准,续费每次都奖、自动到账。对数据敏感、想完全自主可控的团队,云递数据也支持私有化部署到自己的内网或公网服务器,架构是 Nginx + PHP + MySQL。
常见问题
单片机上云一定要自己买服务器和数据库吗?
不一定。如果你只是要存数据、按顺序取数据,用现成的 HTTP 接口就够了,不用买服务器也不用装数据库。真到了对数据主权有硬要求、或者要部署在内网的场景,再考虑私有化部署,那是另一回事。
MQTT和云递数据是什么关系,需要二选一吗?
不是二选一。MQTT 负责实时传输,云递数据负责存储与按序分发,两者可以并存。设备实时联动走 MQTT,需要留档和被后台顺序消费的数据转存到云递数据,各干各擅长的事。
数据推送上去后重复发送会不会产生多条记录?
内容完全相同的数据会被哈希去重,只保留第一条,所以设备重连补发同样的内容不会产生多条。但如果 payload 里带了时间戳等每次都不同的字段,那就会被视为不同内容,各自留存。
读取接口取走的数据还能再取一次吗?
读取是按写入顺序 FIFO 消费的,取走即标记已读,不会重复下发。所以取到之后要在本地落好,别指望再拉一次还能拿到同一批。需要留底的话,控制台里数据是可以下载的。
会员到期后已经存的数据会丢失吗?
不会。到期后数据保留不删除,只是暂停推送和读取,续费后恢复。卡密支持多张叠加、到期顺延,续费前先确认好到期时间,别让设备在空窗期白上报就行。
开始使用
本文的代码示例都可以直接在云递数据上运行:注册后即可获得专属 APIKey,在 开发文档 中查看推送、读取接口的完整说明;需要长期使用可以在 会员中心 用卡密开通月卡或年卡;更多实战内容可以浏览技术博客的其他文章。