IoT 系列第 1 篇。在《09_无线通信》里我们解决了数据怎么飞出去这一篇往上抬一层看整张地图。适用人群玩过 STM32 或 ESP32、能读传感器、能连 Wi-Fi但一被问你这套东西架构是怎样的就答不上来的同学。不需要服务器经验Docker 命令我会从 0 给。读完你能得到一张能默出来的端-边-云三层架构图面试画在白板上不心虚“为什么中间非要多一个网关”——四个能折算成钱或可靠性的理由常见软硬件的对号入座STM32/ESP32/树莓派/EMQX/云平台各站哪一层一次完整数据流的六步走知道你的字节过了几道手协议选型视角MQTT / CoAP / HTTP / LwM2M 不是四选一是各管一段半小时在本机跑起来的最小端边云ESP32 EMQX Docker Node-RED。没看过无线通信那几篇也能懂的核心传感器只负责把物理量变成数字数字要变成别人能用的信息中间还隔着链路、协议、网关、平台四层。本文讲的就是这四层怎么分工。一、我曾经以为IoT ESP32 连 Wi-Fi 发个 HTTP大二那年我做了个教室温湿度监测ESP32 读 SHT30每 10 秒POST一个 JSON 到我自己写的 Flask 接口网页能看曲线。当时我觉得物联网不过如此。后来两个看起来差不多的需求把我打醒了。第一个是学校试验田的土壤墒情。田里没 Wi-Fi、没市电要太阳能 电池活一学期。我原来的方案直接作废一台设备开一个 HTTP 长连光 TCP 握手的开销都比一包数据大。而且 20 个节点各自直连我的云服务器20 条 TCP 连接 24 小时挂着——云是我自己的钱包也是我自己的。第二个是智慧路灯。要求是天黑自动亮、人走过加亮、断网了也要亮。我第一反应是云端写个规则呗老师问我一句校园网断了你的路灯是不是就全瞎了我答不上来。这类断网也要能工作的需求本质上是要求判断逻辑不在云上而在路灯旁边。这两个需求把我逼出了设备直连云的单一思维。真实的 IoT 系统里设备和云之间几乎总是夹着一层东西它叫边Edge。加上它之后原本无解的问题突然有解了田里 20 个电池节点不用各自连云用 BLE 或 LoRa 甩给田埂上那个有电有网的网关统一上行——省电、省流量、省连接数路灯的联动规则写在本地网关里断网只是上不了报灯照样亮上行数据先在网关滤波、聚合、只传变化量流量费直接降一个数量级。我没有量产经验这些是我在课设和啃文档过程中整理的。但端-边-云不是我发明的它是行业通用语言——你去面试白板上画出来的那张图应该跟本文图 1 同一个骨架。二、前置概念表先把这几个词对齐术语白话解释一句话记住它端Device / Node真正接触物理世界的板子采集数据或执行动作大多数端节点压根不会直接上网也上不起网边Edge端和云之间的中转 处理站。有电、有网、算力比端强它不是转发器是会思考的中转站云Cloud远端服务器集群存储、分析、下发、对接业务长处是算力和全局视角短处是离设备太远BrokerMQTT 的消息中转服务器按主题转发设备之间从不直连都在跟 Broker 说话网关Gateway协议转换器把 BLE/Modbus/Zigbee 翻译成 MQTT/HTTP网关是边最常见的物理形态边还包括网关里跑的逻辑上行 / 下行设备→云叫上行遥测云→设备叫下行命令上行求稳下行求快主题TopicMQTT 里消息的地址agri/gw01/telemetry用/分层主题设计好不好决定以后加设备痛不痛苦⚠️ 最容易搞混的一对边是逻辑层网关是具体设备。一个树莓派上跑 Mosquitto Node-RED 联动脚本它既是网关硬件也是边逻辑。阿里云的边缘计算实例则是把边做成软件塞进别的机器——层还在只是不再是一台看得见的盒子。三、端-边-云三层全景图[图 1] IoT 系统端-边-云三层全景面试白板上画的那张 ┌──────────────────────────── 云 CLOUD ────────────────────────────┐ │ ┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ 物联网平台 │ │ 规则引擎 │ │ 业务应用 │ │ │ │ EMQX / 阿里云IoT │──│ 阈值告警 │──│ 大屏 / App / MES │ │ │ │ 华为云 / OneNET │ │ 数据清洗 │ │ 时序数据库 │ │ │ └─────────────────┘ └──────────────┘ └──────────────────┘ │ └────────┼─────────────────────────────────────────────────────────┘ │ MQTT over TLS1883 / 8883 │ ↑ 这一段是广域网会断、会慢、按流量收费 ═════════╪══════════════════════════════════════════════════════════ │ 园区 / 现场局域网通常免费、快但地理范围有限 ┌──────────────────────────── 边 EDGE ────────────────────────────┐ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 边缘网关树莓派 / ESP32-S3 / 工控机 / 工业网关 │ │ │ │ ① 协议转换 BLE / Modbus / Zigbee / LoRa ⇄ MQTT │ │ │ │ ② 本地 Broker可选Mosquitto / EMQX断网也能内部通信 │ │ │ │ ③ 边缘计算 滑动滤波、越限判断、聚合上报、单位换算 │ │ │ │ ④ 断网续传 本地队列 SQLite / 文件缓存联网后补传 │ │ │ │ ⑤ 本地联动 温度30 就开风扇不依赖云 │ │ │ └───────────────────────────────────────────────────────────┘ │ └──────▲──────────────▲─────────────────────▲─────────────────────┘ │ │ │ BLE / Zigbee RS-485 / Modbus LoRa / 私有射频 ┌──────┴───────┐ ┌────┴───────┐ ┌──────┴────────┐ │ 温湿度节点 │ │ 电表 / 水表 │ │ 土壤墒情节点 │ │ STM32传感器 │ │ 工业仪表 │ │ 电池 太阳能 │ └──────────────┘ └────────────┘ └───────────────┘ 端 DEVICES数量多、资源少、大多不通 IP3.1 三层各自该干什么、不该干什么判断一个功能放哪层我的土办法是问三个问题要不要全局信息能不能容忍延迟断网时还要不要工作层该它干的不该它干的典型软硬件端采样、执行、本地安全联锁、低功耗休眠Stop / Deep Sleep、基础自检复杂规则判断、长连接、TLS 握手、大数据缓存STM32F1/F4/L4/U5 按功耗算力选、ESP32-C3/S3自带 Wi-Fi/BLE、nRF52纯 BLE边协议转换、聚合/滤波、本地联动、断网续传、本地告警、固件分发跨站点全局分析、长期历史存储树莓派/香橙派Linux Docker、ESP32-S3轻量网关、工业网关RS-485/DI/DO、跑 Mosquitto / EMQX / Node-RED云海量存储、跨设备关联分析、用户权限、OTA 版本管理、对接业务毫秒级实时控制、单点高频联动阿里云 IoT / 华为云 / OneNET托管自建EMQX / Mosquitto 时序库TDengine/InfluxDB 规则引擎⚠️平台差异同样叫边ESP32-S3 和树莓派不是一个量级。ESP32-S3 几百 KB RAM、无 MMU、跑 FreeRTOS让它做本地 Broker 一周缓存是想多了树莓派是瓦级功耗、GB 级内存能干的多得多但费电、贵、要管系统。选硬件前先算清楚要缓存多少“要不要跑容器”别上来就上树莓派也别硬扛。云端选型托管平台的好处是接入、鉴权、影子、OTA 都给你做好了坏处是被绑死、按消息数收费、数据过别人手。自建全在自己手里、本地就能练手代价是鉴权/高可用/监控自己扛。我的建议是先自建把 MQTT 搞明白再学平台的封装——顺序反了平台的物模型“影子”Topic 类你一个都看不懂。四、为什么非要多一个边四个理由逐个能算账如果你以后只记得一句话我希望是这句边不是为了多一层显得专业它解决的都是能折算成钱或可靠性的问题。4.1 省钱带宽和云成本假设一个大棚有50 个节点每 30 秒上报一次 100 字节方案 A50 个节点各自直连云每个都开 TCP TLS 每包实际开销 ≈ 100 B 载荷 40 B TCP/IP 头 ~30 B MQTT 头 TLS 记录头 ≈ 200 B 往上 每天50 × 2880 × 200 B ≈ 28.8 MB/天 ≈ 864 MB/月服务器维持 50 条 TLS 长连接 方案 B节点走 BLE/LoRa 给网关网关每 5 分钟聚合发一包约 2 KB 每天288 × 2 KB ≈ 0.58 MB/天 ≈ 17 MB/月服务器只维持 1 条连接 → 流量降到约 1/50连接数从 50 降到 1数字是估算的但数量级是对的聚合 批量上传是边缘层最直白的省钱方式。4.2 省电把最费电的活从电池节点身上拿走《低功耗》那几篇的公式平均电流 Σ(各状态电流 × 停留时间) ÷ 周期。一次 MQTT 上报要 3~5 秒的高电流含建连、TLS 握手而一次 BLE 广播只要几十毫秒。把射频发射时长砍掉一两个数量级续航直接翻倍。所以我那个发 HTTP的 ESP32 教室节点用市电毫无问题一换电池架构就得改电池端只做短距离低功耗通信联网的重活交给有市电的网关。4.3 断网续传最容易被忽视也最容易在验收时翻车我第一次做户外项目时写的是采样 → 连 Wi-Fi → 发 HTTP → 失败就丢弃。只要那 5 分钟网络抖一下数据就永久没了。验收时对方问我昨天下午 3 点到 4 点的数据呢我只能说那会儿断网了。[图 2] 断网续传的基本结构 采样 ── 打时间戳 ── 本地环形队列内存 掉电保存到 Flash/SQLite │ 联网───────┤ 是 ───────── 批量上行 → 收到确认 → 删除队列条目 否 ───────── 继续攒到上限就按策略丢最旧的三个要点每条都是踩过的① 时间戳要在采样那一刻打不是上传时打——否则断网一小时后补传的数据全挤在同一时刻曲线是错的② 队列要有上限别把 Flash 写穿和《17_存储》的写平衡、掉电保护是同一个问题③ 补传要分批限速——断网两小时后一恢复灌几千条进去轻则卡死自己重则被 Broker 限流踢下线。4.4 本地联动呼应我在《项目实战_02》做的 BLE 网关我在《项目实战_02智能家居环境网关——ESP32 做 BLE 中心 本地联动》里干的事用本篇的语言重述一遍[图 3] 同一个项目两种架构 (a) 我最初的云上联动想法 传感器 ──Wi-Fi── 云规则引擎 ── 下发命令 ── 执行器 断网 全瘫每次联动绕一圈延迟几百毫秒到几秒 (b) 实际做成的本地联动边在起作用 传感器 ──BLE── ESP32 网关【规则温度28℃ 且 有人 → 开风扇】── 执行器 └──MQTT── 云只负责记录和远程手动控制 断网 只是上不了报联动照常延迟是毫秒级这个经历让我彻底理解了边判断逻辑离被控对象越近系统越可靠。云是全局视角和记忆力不是反射神经——你不会指望大脑皮层负责膝跳反射。五、数据是怎么流过去的一次上行 一次下行[图 4] 一次完整闭环六个阶段 ① 采集 传感器 → 数字量I2C/SPI/ADC ② 本地处理 标定、单位换算、滑动平均、剔除跳变、越限标记 ③ 协议封装 组 JSON/CBOR加设备 ID、时间戳、序号、校验 ④ 上行 MQTT PUBLISH(QoS 1) → Wi-Fi/4G/LoRa → 网关 → Broker ⑤ 平台 解析、鉴权、入库、规则引擎判定、更新设备影子 ⑥ 下行控制 PUBLISH 到 cmd 主题 → 设备校验执行 → 回报新状态回到 ① 关键⑥ 执行完必须回报形成闭环。否则 App 上永远是已发送 你不知道设备到底开没开——而这个回报本身就是一次上行。⚠️ 第 ③ 步新手最常踩把所有东西塞进一个主题。比如device/data里一会儿温度一会儿电量一会儿心跳靠解析 payload 区分。后果是没法用主题过滤云端规则写得又臭又长、retained 消息也没法用。主题要按用途分层不是按设备打包。六、协议选型不是四选一是各管一段这是我最想纠正的误解。教程常把 MQTT/CoAP/HTTP/LwM2M 摆一排让你选一个但真实架构里它们常常同时出现只是在不同的段上。[图 5] 协议是分段的不是互斥的 端 ──────────── 边 ──────────── 云 ────────── 业务 BLE GATT MQTT over TLS HTTPS / AMQP / Kafka Modbus RTU 也可能直接 CoAP数据库驱动 Zigbee / LoRa 私有帧 └─ 根本不是 IP 网络 └─ 这一段才是 └─ 跟嵌入式关系不大 轮不到 MQTT MQTT 的主战场 是后端的事所以听到这设备用什么协议第一个该问的是从哪儿到哪儿。协议跑在报文开销模型该用的地方别用的地方MQTTTCP(TLS)固定头最小2 B发布/订阅长连接支持推送边↔云主力遥测 下行命令有市电设备电池 NB-IoT 的极低频上报心跳本身就费电见《无线_03》CoAPUDP固定头4 B请求/响应像精简 HTTP电池设备 极受限网络NB-IoT/LoRa 直连需要服务端主动推命令、对丢包零容忍的业务HTTP(S)TCP文本头动辄几百字节请求/响应短连接偶尔取配置、下载固件、对接 Web 后端高频遥测头比载荷大下行实时控制LwM2M通常在 CoAP 之上同 CoAP面向资源如/3/0/1设备管理远程配置、固件升级、生命周期当数据上报协议用——那不是它的主场一句话记忆MQTT 管来回传数据CoAP 管极省电地传一包HTTP 管跟 Web 世界打交道LwM2M 管管理设备本身。成熟平台里这四个往往同时在跑。6.1 主题命名现在多花 5 分钟以后省 5 天{产品}/{设备ID}/{方向}/{用途} agri/gw01/up/telemetry 上行周期遥测 agri/gw01/up/event 上行事件/告警 agri/gw01/up/reply 上行命令执行结果闭环 agri/gw01/down/cmd 下行命令 agri/gw01/status 状态配合 retained 遗嘱 订阅侧agri/gw01/down/#这台的下行 agri//up/telemetry所有设备遥测四条规矩① 不以/开头会产生空层级② 主题里不放动态值时间戳、随机数一律进 payload否则主题树爆炸③ 全小写 短横线别混大小写④ 主题里不放敏感信息它是明文的鉴权靠用户名/密码或证书。七、动手半小时搭一个最小端边云这套我自己在笔记本上跑通过不需要买云、不需要公网 IP全在本机 Docker 里。第 1 步起一个本地 BrokerEMQXdockerrun-d--nameemqx\-p1883:1883\# MQTT over TCP-p8083:8083\# MQTT over WebSocket-p8084:8084\# MQTT over WSS-p8883:8883\# MQTT over TLS-p18083:18083\# DashboardWeb 管理页emqx/emqx:latestdockerlogs-femqx# 看日志确认起来了浏览器打开http://localhost:18083默认账号密码admin/public首次登录会要求改掉默认密码。⚠️ 后面让 ESP32 连这个 Broker 时地址不能填localhost或127.0.0.1——那是 ESP32 自己。要用电脑在局域网里的 IPifconfig/ipconfig查。这是ESP32 连不上 EMQX的头号原因第二号是防火墙没放行 1883。第 2 步先用命令行验证 Broker 通了sudoaptinstall-ymosquitto-clients# macOS: brew install mosquitto# 终端 A订阅模拟云/应用一侧mosquitto_sub-h192.168.1.100-tagri/#-v# 终端 B发布模拟设备一侧mosquitto_pub-h192.168.1.100-tagri/gw01/up/telemetry\-m{temp:26.5,humi:61,ts:1756000000}-q1终端 A 应立刻打印出这一条。意义先把云侧打通再调设备这样设备联不上时你能确定问题在设备侧。第 3 步ESP32 上报最小可用版/* ESP32 / ESP-IDF v5.x最小 MQTT 上报 * 前置Wi-Fi 已连接并拿到 IPesp_wifi 或 example_connect 均可 * 配置细节见本系列下一篇《MQTT 深度实战》 */#includestdbool.h#includestring.h#includemqtt_client.h#includeesp_log.hstaticconstchar*TAGapp;staticesp_mqtt_client_handle_ts_clientNULL;staticvoidmqtt_event_handler(void*handler_args,esp_event_base_tbase,int32_tevent_id,void*event_data){esp_mqtt_event_handle_tevent(esp_mqtt_event_handle_t)event_data;switch((esp_mqtt_event_id_t)event_id){caseMQTT_EVENT_CONNECTED:ESP_LOGI(TAG,broker 已连接);esp_mqtt_client_subscribe(event-client,agri/gw01/down/cmd,1);break;caseMQTT_EVENT_DATA:{/* ⚠️ event-topic / event-data 不以 \0 结尾必须按长度拷贝 */chartopic[64];inttlenevent-topic_len;if(tlen(int)sizeof(topic))tlen(int)sizeof(topic)-1;memcpy(topic,event-topic,(size_t)tlen);topic[tlen]\0;ESP_LOGI(TAG,收到 [%s] qos%d : %.*s,topic,event-qos,event-data_len,event-data);/* 执行完记得往 .../up/reply 回报一次形成闭环 */break;}caseMQTT_EVENT_DISCONNECTED:ESP_LOGW(TAG,连接断开esp-mqtt 默认会自动重连);break;default:break;}}voidmqtt_app_start(void){constesp_mqtt_client_config_tcfg{.broker.address.urimqtt://192.168.1.100,/* 换成你的局域网 IP */.credentials.client_idesp32-gw01,/* 同 Broker 内必须唯一 */.session.keepalive60,/* 秒 */.network.reconnect_timeout_ms5000,.network.timeout_ms10000,};s_clientesp_mqtt_client_init(cfg);if(s_clientNULL){ESP_LOGE(TAG,mqtt init 失败);return;}esp_mqtt_client_register_event(s_client,ESP_EVENT_ANY_ID,mqtt_event_handler,NULL);esp_mqtt_client_start(s_client);}/* 上报一包遥测在采集任务里调用 */booltelemetry_publish(floattemp,floathumi,int64_tts_ms){if(s_clientNULL)returnfalse;charpayload[128];intnsnprintf(payload,sizeof(payload),{\temp\:%.1f,\humi\:%.1f,\ts\:%lld},(double)temp,(double)humi,(longlong)ts_ms);if(n0||n(int)sizeof(payload))returnfalse;/* 格式化失败或截断 *//* QoS 1要等 PUBACK。返回 0 的 msg_id 才算进了发送队列 */intmsg_idesp_mqtt_client_publish(s_client,agri/gw01/up/telemetry,payload,n,1,0);if(msg_id0){ESP_LOGW(TAG,publish 失败: %d-2 outbox 满发的比网络快,msg_id);returnfalse;}returntrue;}第 4 步用 Node-RED 当那个云上应用npminstall-gnode-rednode-red# 打开 http://localhost:1880# 或docker run -d -p 1880:1880 --name nodered nodered/node-red拖四个节点连成一条线这就是云层在干的事解析 → 存储 → 展示。┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ mqtt in │──│ json │──│ function │──│ debug / │ │ agri/# │ │ 解析字符串 │ │ 提取字段 │ │ chart │ └──────────┘ └──────────┘ └──────────┘ └─────────┘mqtt in 节点填 Server192.168.1.100:1883、Topicagri/#、QoS 1function 节点只写一行// Node-RED function 节点内JavaScriptmsg.payload{value:msg.payload.temp,ts:msg.payload.ts};returnmsg;点 Deploy再用mosquitto_pub发一次debug 面板应立刻出现数据。到这一步你已跑通完整的端 → 云 → 应用链路——只不过边目前是空的。第 5 步把边加回来做对比实验不聚合ESP32 每 2 秒发一包去 EMQX Dashboard 看消息速率加上边在 ESP32 里做 10 秒滑动窗口只有温度变化超 0.5 ℃ 或超过 30 秒没发才上报对比 Dashboard 上的「消息数 / 分钟」。你会看到一个数量级的差别且数据几乎没有信息损失——这就是 4.1 那笔账的实物版。八、新手必踩的 8 个坑#坑现象 / 后果正确做法1所有设备直连云不考虑网关连接数爆炸、流量费高、电池撑不过一周先问有没有市电、数量多少10 个以上电池节点就该考虑边2ESP32 里 Broker 地址填localhost/127.0.0.1连不上日志一片超时填电脑的局域网 IP确认防火墙放行 18833断网时直接丢数据网络一抖这段时间数据永久缺失本地队列 采样时打时间戳 队列有上限 补传限速4时间戳在上传时打不是采样时打补传的历史数据全挤同一时刻曲线是假的采样那一刻就打或维护序号由平台还原5所有消息塞进一个主题云端规则写不动、没法过滤、retained 用不了按{产品}/{设备}/{方向}/{用途}分层一用途一主题6主题以/开头或把时间戳拼进主题多出空层级主题树爆炸撑爆 Broker 内存不以/开头主题只放稳定标识动态值进 payload7下行命令发出去就不管App 显示已发送不知设备有没有执行设备执行完回报.../up/reply形成闭环8边这层硬件选型拍脑袋树莓派做电池场景几小时没电ESP32 硬扛一周缓存撑不住先算缓存多大、要不要跑容器、功耗预算多少再选九、动手练一练按顺序做。第 3 步和第 5 步是故意搞破坏——只有亲眼见过故障现象以后才认得出它。练习 1把本机端边云跑通30 分钟按第七章做完。验收标准ESP32 上电后 Node-RED 的 debug 面板持续收到数据EMQX Dashboard 的 Clients 页能看到esp32-gw01在线。练习 2用命令行手动模拟设备和应用不开 ESP32只用两个终端互发。要求你能说出这一包从发布者 → Broker → 订阅者经过哪些环节并指出 Broker 在里面的作用。练习 3故意试错掐断网络看数据会不会丢让 ESP32 正常上报然后直接关掉电脑 Wi-Fi或docker stop emqx等 30 秒再恢复。没有本地队列的话这 30 秒的数据不见了而且日志里可能什么都看不出来。然后加一个最简单的队列哪怕只是内存里的环形数组 补传再试一次。意义这是能演示的 demo和能交付的系统之间的分水岭。练习 4改主题结构体会过滤的威力先把所有数据都发到agri/data在 Node-RED 里写过滤逻辑再改成agri/gw01/up/telemetry的分层结构用agri//up/telemetry订阅。对比两种写法下 Node-RED 流程的复杂度你就明白为什么要分层。练习 5故意试错把 Broker 地址写错学会看错误把.broker.address.uri改成mqtt://192.168.1.999重新烧录看串口日志里MQTT_EVENT_ERROR的error_type然后对比两种情况IP 不存在是传输层TCP失败且反复重试IP 存在但端口没开如填 1884是连接被拒重连间隔由reconnect_timeout_ms决定。意义以后遇到设备离线你能第一时间分清是网络不通还是Broker 不认我而不是盲目重启。小结端-边-云是三次分工端接触物理世界边就地解决本地该解决的事云负责全局和记忆。边的价值能折算成钱和可靠性省带宽聚合、省电重活从电池节点拿走、断网续传数据不丢、本地联动断网也能控。每条都能讲出一个具体场景。协议是分段的端↔边常常根本不是 IP 网络边↔云才是 MQTT 的主场。别问MQTT 和 CoAP 哪个好要问这一段用哪个。主题设计要趁早{产品}/{设备}/{方向}/{用途}一用途一主题动态值进 payload。现在偷懒以后要还。下行必须有回报否则你的系统永远只有开环。参考MQTT 3.1.1 / 5.0 协议规范OASISEMQX 官方文档Docker 部署与 Dashboard 端口乐鑫 ESP-IDF 编程指南中 esp-mqtt 组件的配置说明。文中协议开销与流量估算为典型值实际请以抓包Wireshark或 Broker 侧统计为准。下一篇MQTT 深度实战——QoS、保留消息、遗嘱与订阅树ESP32 对接 Broker