1. 为什么一个物联网平台要同时支持TCP和MQTT?
做物联网平台这件事,最大的误解是以为“设备联网”就等于“设备把数据发到服务器”。真正落地的时候你会发现,第一个要拍板的问题就是:设备到底怎么连上来?是走TCP裸协议直接怼二进制流,还是走MQTT这套标准的发布订阅协议?这就是我这次搭“支持TCP、MQTT协议的物联网平台”的出发点——不是二选一,而是两个都要。
先说结论:TCP是基础设施,MQTT是标准应用层协议,两者不是替代关系,而是共存关系。很多私有协议设备、单片机透传模块、PLC设备,只会用TCP长连接丢原始字节流;而大量智能硬件、手机App、云平台接入,又默认走MQTT,因为它的发布订阅模型、QoS机制、遗嘱消息实在太好用了。如果一个平台只支持其中一种,就意味着你要拒绝一半的真实设备。
为什么单一协议不行?我举个特别实际的例子。某工厂里一台老式PLC,通讯口只支持Modbus TCP,它不在乎什么主题、什么QoS,它只认识标准Modbus数据帧。另一边的环境监测传感器,则希望用MQTT发送JSON格式的温湿度数据,订阅告警主题。这两类设备要进同一个平台,协议层就必须做适配。平台本质上要做的是一个“协议网关+统一接入层”:TCP协议服务负责收原始报文,MQTT Broker负责处理标准协议订阅,上层统一转成设备与消息模型,再交给业务系统。
从架构角度看,一个可落地的双协议物联网平台通常包含这样几个部分:
- 接入层:同时监听TCP端口和MQTT端口,TCP端口处理私有协议、透传数据;MQTT端口处理标准发布订阅。
- 协议解析层:把不同设备的上行报文解析成统一的设备数据格式,比如“设备ID+属性+时间戳”。
- 设备管理层:设备注册、鉴权、在线状态管理、指令下发。
- 业务层:规则引擎、告警、数据存储、对外API。
这套东西无论是做工业场景、智慧园区,还是做物联网仿真实训平台,骨架都差不多。实训平台反而更看重“协议细节可见”:老师带着学生抓TCP三次握手包、写一个MQTT客户端、观察QoS0和QoS1的消息差异,这些实验都对双协议接入有硬需求。所以我这篇就是一个完整的设计+实操+排障记录,想自己搭平台、做毕业设计,或者带学生做实训项目的人,都可以直接参考。
2. 协议核心机制拆解:从TCP建连到MQTT报文
2.1 先补基础:TCP/IP四层模型到底在讲什么
很多新手卡在协议理解上,是因为跳过了四层模型直接去看报文。TCP/IP四层模型自上而下分别是应用层、传输层、网络层、网络接口层。每一层干的事其实很单纯:
- 应用层:决定“数据长什么样”,HTTP、MQTT、Modbus TCP都在这层。
- 传输层:决定“数据怎么可靠到达”,TCP就是这一层的代表,它管连接、管重传、管顺序。
- 网络层:决定“数据走哪条路”,IP协议负责找地址和路由。
- 网络接口层:决定“数据怎么在网线上传输”,比如以太网帧、WiFi帧。
类比一下:应用层是写信的人,决定信的内容和格式;传输层是快递公司的客服,确认你收到每一封;网络层是物流分拣中心,规划包裹从深圳到北京走哪条高速;网络接口层就是那一辆辆货车。你抓包的时候,一个数据帧里能看到以太网头、IP头、TCP头、应用数据,这就是四层模型在一帧里的直观体现。
端口号是传输层的概念,作用是区分同一台机器上的不同应用。比如服务器监听1883端口,那么所有发给这个端口的TCP数据,最终都会交给MQTT Broker处理。很多人一上来就混淆“IP地址”和“端口”,其实IP负责找到机器,端口负责找到机器上的哪个程序。
2.2 三次握手与四次挥手:Wireshark里怎么筛选
TCP三次握手是整个网络通信里最基础的机制。第一次握手:客户端发送SYN包,声明“我要建立连接”;第二次:服务端回复SYN+ACK,意思是“我收到了,也准备好连接”;第三次:客户端再回一个ACK,代表“双方都确认,可以开始传数据了”。
为什么不是两次握手?因为TCP要避免历史重复报文干扰。如果客户端第一个SYN因为网络拥塞延迟,连接建立后又发来一个旧的SYN,没有第三次握手的话,服务端无法判断这个SYN是不是当前连接要用的。三次握手让双方各自确认“我能收到你、你也能收到我”,这是建立可靠连接的最小次数。
四次挥手更直接:因为TCP是全双工的,一边断开时另一边仍可能要继续传输。第一次挥手A发FIN,表示“A的数据发完了”;第二次B回ACK,表示“收到你的FIN”;这之后B还可以继续向A发数据;等B也发完了再发FIN;A回ACK。所以你看到Wireshark里的挥手总是四个包,而不是两个。
在Wireshark里筛选三次握手,我一直用的方法是:
tcp.flags.syn==1 && tcp.flags.ack==0这个条件只筛出第一个握手包(纯SYN)。然后右键“Follow -> TCP Stream”,就能看到完整流的握手和挥手过程。如果想单看挥手:
tcp.flags.fin==1实操经验是:抓环回地址(抓本机到本机)时,Windows上默认可能抓不到,需要装Npcap并勾选“抓取环回流量”。抓包前先关掉其他占用网络的软件,否则筛选出的TCP流非常杂乱。
2.3 TCP粘包/拆包:C++服务端绕不开的坑
TCP是流式协议,发送方和接收方看到的不是“一条条消息”,而是“一串连续的字节”。发两条消息之间没有边界,接收方可能一次收到两条并在一起,也可能一条消息被拆成两次收。这就是粘包和拆包问题。
传统解法有三种:固定长度报文,比如每个报文固定64字节,不够就补零;分隔符协议,比如用\n或\r\n作为消息结束标志;长度前缀,也是最推荐的做法,报文由“4字节长度头+数据体”组成。我用C++写TCP服务时,解码器核心思路就是:
// 伪代码:长度前缀拆包 while (buffer.size() >= 4) { uint32_t len = ntohl(*(uint32_t*)buffer.data()); // 读4字节长度头 if (buffer.size() < 4 + len) break; // 数据尚未完整 std::string msg = buffer.substr(4, len); // 取出一条完整消息 handleMessage(msg); buffer.erase(0, 4 + len); // 移除已处理的部分 }这个循环处理了“一条完整消息”“半包”“多条粘包”三种情况,是典型的应用层拆包写法。做TCP接入层时,我强烈建议从上项目第一天就设计好报文格式,不要等设备端联调时再改。格式可以简单,比如“起始符+长度+类型+数据+校验”,但一定要有长度字段,这样所有设备的收发逻辑完全统一。
来说TCP和UDP的区别:TCP有连接、可靠、有流量控制,适合要求不丢数据的场景;UDP无连接、低延迟,适合音视频流和实时控制。物联网平台里,远程控制指令一般用TCP/MQTT,视频预览用UDP或RTSP,各管各的。
2.4 MQTT协议:为什么它是物联网事实标准
MQTT和TCP的关系可以这样理解:TCP是一条已经铺设好的可靠快递专线,而MQTT是这条专线上的一套邮件分拣规则。MQTT规定消息怎么分类(主题)、怎么订阅、怎么保证送达,但它本身不负责建立物理连接,连接还是靠TCP。
MQTT基于发布/订阅模型,核心角色有三个:Broker(中央服务器)、发布者、订阅者。发布者往某个主题发消息,Broker收到后转发给所有订阅了这个主题的客户端。这种解耦非常符合物联网场景:传感器不需要知道后台有哪些服务在消费数据,后台服务也不关心传感器是谁,两边都只跟Broker打交道。
MQTT的报文种类很多,但平时开发只需要关注几个关键的:
- CONNECT/CONNACK:客户端连接Broker,Broker确认。
- PUBLISH/PUBACK:发布消息,QoS1时会有回执。
- SUBSCRIBE/SUBACK:订阅主题,Broker确认。
- PINGREQ/PINGRESP:心跳保活。
- DISCONNECT:断开连接。
这里必须搞懂QoS的0/1/2。QoS0是最多一次,发出去就不管,可能出现丢失,适合实时性高但不敏感的数据,比如环境温湿度;QoS1是至少一次,Broker会回PUBACK,接收方可能收到重复消息,适合告警;QoS2是恰好一次,用四步握手确保不丢不重,代价是性能最低,适合计费等敏感数据。
还有个必须讲透的概念是遗嘱消息(Last Will)。客户端连接时可以带上一个遗嘱主题和遗嘱内容,如果它异常掉线(比如断电),Broker会自动替它发布这条遗嘱消息,相当于“死亡通知”。这在设备掉线监测里非常有用。我在实训平台里专门设计了遗嘱消息验证实验,学生的设备模拟突然断网,其他订阅端立刻收到遗嘱。
另外还要注意Clean Session(清理会话)和Keep Alive(保活机制)。Clean Session为true时,Broker不保存离线消息和订阅状态,客户端重连后恢复默认状态;为false时,Broker会保存会话,离线期间发给它的QoS1/2消息在重连后可补发。Keep Alive是客户端周期性发送PINGREQ的时间间隔,默认60秒,Broker如果在一个半周期内没收到任何报文,就会认为客户端已经掉线,进而触发遗嘱发布。
初始化连接时一组很常用的参数如下:
| 参数 | 典型值 | 说明 |
|---|---|---|
| Broker地址 | 192.168.1.100:1883 | 默认端口,TLS用8883 |
| Client ID | device_001 | 必须唯一,冲突会导致旧连接被踢下线 |
| Username/Password | admin/123456 | 平台统一鉴权 |
| Clean Session | false | 需要离线消息就设false |
| Keep Alive | 60 | 单位秒 |
| Will Topic | device_001/status | 遗嘱主题 |
| Will Message | offline | 遗嘱内容 |
3. 从0到1:平台服务端搭建与多端设备接入实操
3.1 服务端选型:Broker、TCP服务框架怎么挑
MQTT服务端有现成的Broker可以选,没必要自己从零实现。我的经验是:
| Broker | 优势 | 适用场景 |
|---|---|---|
| Mosquitto | 轻量、开源、Windows/Linux直接跑 | 本地测试、教学演示、资源受限设备 |
| EMQX | 高并发、集群、规则引擎、插件丰富 | 生产级平台、大规模设备接入 |
| RabbitMQ(开启MQTT插件) | 复用现有消息队列生态 | 已经用RabbitMQ做业务消息的场景 |
TCP接入层就要自己写了,因为涉及私有协议,没有现成的通用服务。语言选择上,C++适合性能极致但开发慢,Java(Netty)生态成熟,Go适合高并发入门快。实训平台我推荐Go或Netty,因为学生容易看懂,文档也多。不过这一次我用的是C++做TCP解码演示,因为很多工业设备服务端都是C++写的老系统,参考价值更高。
3.2 Windows本地搭建MQTT服务端:zip包安装到注册为系统服务
我这边经常需要在实训室电脑上快速搭一套MQTT环境,每次都推荐用Mosquitto,因为它直接提供Windows zip包,免安装。下载解压后,目录里有mosquitto.exe和mosquitto.conf。先用命令行启动测试:
cd C:\mosquitto mosquitto.exe -v-v是打印详细日志,启动后能看到监听1883端口的提示。默认配置只允许本机访问,要做局域网设备接入,需要改配置文件mosquitto.conf,至少加这三项:
listener 1883 0.0.0.0 allow_anonymous true persistence truelistener指定监听地址和端口,0.0.0.0表示监听所有网卡;allow_anonymous允许无账号密码连接,适合内网实验;persistence开启持久化。
命令行窗口一关服务就停了,所以需要注册成Windows服务。用管理员权限打开PowerShell或CMD:
sc create mosquitto binPath= "C:\mosquitto\mosquitto.exe -c C:\mosquitto\mosquitto.conf" start= auto启动服务:
sc start mosquitto注意binPath=后面等号和路径之间一定要有空格,这种细节坑过我好几次。注册好后,以后开机自启,设备随时可以接入。这个操作思路同样适用于把其他绿色版工具变成Windows服务,算是一个通用技巧。
3.3 TCP接入服务的关键实现:心跳、解码、收发
TCP服务端代码很轻量,但要处理的问题不少。我先用Go写一个最简版本,展示完整骨架:
func handleConn(c net.Conn) { defer c.Close() heartbeat := time.NewTicker(30 * time.Second) buf := make([]byte, 4096) for { n, err := c.Read(buf) if err != nil { log.Println("client disconnected:", c.RemoteAddr()) return } data := append(pending, buf[:n]...) for len(data) >= 4 { length := int(binary.BigEndian.Uint32(data[:4])) if len(data) < 4+length { break } msg := data[4 : 4+length] handleMessage(c, msg) data = data[4+length:] } pending = data } }这样一个服务端就具备了解粘包能力和基础连接管理。但生产环境还要做几件事:每30秒检测一次心跳,长期无数据的连接主动断开,避免“死连接”占着资源;设备上线先鉴权,比如TCP接入后第一条报文必须是设备ID+密钥,校验不通过直接关闭;下行指令要设计好,服务端可以随时向已连接的设备发送数据。
在实训平台里,我还加了一条约定:设备通过TCP接入后,每帧JSON数据必须带dev_id字段,平台根据这个字段识别设备身份。这样TCP连接和设备实体的绑定关系非常清晰,也方便后面做设备在离线统计。
3.4 MQTT客户端联调:MQTTX、Spring Boot、Android全流程
服务端搭好后,第一件事就是用MQTTX验证连通性。MQTTX是跨平台桌面客户端,填四个信息就能连:Broker地址(如192.168.1.100:1883)、Client ID、用户名密码、Clean Session。连接成功后,手动发布一条消息到test/topic,同时订阅这个主题,就能看到消息实时回显。这一步通常用来验证Broker配置有没有问题。
Spring Boot项目接入MQTT,我常用Spring Integration MQTT模块。配置文件里大致是:
spring: mqtt: url: tcp://192.168.1.100:1883 username: admin password: 123456 client-id: spring-server default-topic: device/#Java侧关键是定义工厂和发送模板。核心代码如下:
@Bean public MqttPahoClientFactory mqttClientFactory() { DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); MqttConnectOptions options = new MqttConnectOptions(); options.setServerURIs(new String[]{"tcp://192.168.1.100:1883"}); options.setCleanSession(false); options.setAutomaticReconnect(true); factory.setConnectionOptions(options); return factory; } @Bean public MqttPahoMessageHandler messageHandler() { MqttPahoMessageHandler handler = new MqttPahoMessageHandler("spring-server", mqttClientFactory()); handler.setDefaultTopic("device/status"); return handler; }Spring Boot的@ServiceActivator(inputChannel = "mqttOutboundChannel")配合上面的Handler,可以很方便地把业务消息发布到指定主题;订阅则用@MqttListener(topics = "device/#")注解接收回调。要注意client-id不能和MQTTX连着的那个客户端重复,否则会把对方踢下线,这是非常典型的“设备互踢”坑。
Android端接入MQTT,主流方案是Eclipse Paho库。在build.gradle里加依赖后,连接和订阅的核心代码并不长:
MqttClient client = new MqttClient("tcp://192.168.1.100:1883", "android-device"); MqttConnectOptions options = new MqttConnectOptions(); options.setCleanSession(true); client.connect(options); client.subscribe("device/control", 1); client.setCallback(new MqttCallback() { @Override public void messageArrived(String topic, MqttMessage message) { // 处理下行控制指令 } });需要提醒的是Android 8以上版本,网络操作不能放在主线程,要包在子线程或使用异步API。同时要申请INTERNET权限,这俩是新手最容易反的错误。
3.5 设备侧接入实战:ESP01S走TCP、Node.js动态订阅
在实训平台里我们经常用ESP01S做TCP透传实验。这个模块本身跑的是AT指令固件,连接WiFi后,通过串口发AT指令就能建TCP连接:
AT+CWMODE=1 AT+CWJAP="WiFi名称","WiFi密码" AT+CIPSTART="TCP","192.168.1.100",9500 AT+CIPSEND=14最后一行AT+CIPSEND=14是告诉模块“我后面要发14字节的数据”,发送后模块返回SEND OK。这种方法很适合低成本硬件原型,因为不需要写底层协议栈,串口发指令就行。注意手机热点做AP时,要确认电脑和ESP01S在同一个网段,电脑防火墙要放行对应端口,否则TCP连接会一直卡在建立阶段。
Node.js动态订阅MQTT在后台服务中也常见,Egg.js项目里典型的做法是应用启动后创建一个客户端,收到外部配置变更就动态subscribe新主题:
const mqtt = require('mqtt'); const client = mqtt.connect('mqtt://192.168.1.100:1883'); client.on('connect', () => { client.subscribe('device/+/online'); client.subscribe('device/+/data'); }); client.on('message', (topic, message) => { // 根据topic前缀转发到不同处理器 if (topic.endsWith('/online')) { updateDeviceStatus(topic, message.toString()); } });动态订阅的本质就是随时调用client.subscribe(),它不像TCP协议那样需要重新建连,这是MQTT非常舒服的设计。
3.6 工业协议补充:Modbus TCP、RTU转TCP与W5500
不少工业设备接入平台走的是Modbus TCP。Modbus TCP的报文结构是“MBAP头(7字节)+功能码+数据”,MBAP前两个字节是事务处理标识,第三、四字节是协议标识(0表示Modbus),第五、六字节是后续字节长度,第七字节是单元标识。常见功能码有03读保持寄存器、06写单个寄存器。做Modbus TCP服务端时,本质就是监听502端口,解析MBAP头,再去读写设备寄存器映射表。
很多老设备只有Modbus RTU串口协议,想上传到平台,常见做法是加一个边缘网关:网关一侧用RS485采集RTU数据,另一侧转为Modbus TCP上传到服务器;或者网关直接订阅MQTT,把寄存器值转成JSON发布到平台主题。这里有个经验:RTU转TCP只是“承载方式”变了,寄存器地址和功能码完全没有变,所以平台侧只需要实现标准Modbus TCP解析,不用关心设备原来是串口还是网口。
W5500是硬件TCP/IP协议栈芯片,特别适合单片机接入网络。它的优势在于TCP/IP协议栈全部由硬件处理,主控MCU不需要跑复杂协议栈,只要通过SPI接口调用Socket API即可。网上有不少freemodbus+w5500的开源方案,本质上就是在单片机上实现 Modbus TCP服务器,从站寄存器通过W5500暴露给以太网调试工具。这类方案省MCU资源、响应稳定,在工业数据采集板里非常常见。
4. 把这些能力做成实训项目:典型实验设计与教学场景
4.1 为什么实训平台必须强调抓包和协议细节
仿真实训平台的价值在于“安全地犯错”,学生可以随便改配置、乱发报文、故意断网,都不会对真实系统造成影响。我设计的实训内容不追求花哨界面,而是强调每个实验必须能看到协议层的真实表现。
实训平台最基本的架构就是一套我们前面搭建的双协议平台:一台服务器装好Mosquitto和TCP服务,几台学生机跑MQTTX和抓包工具,再加上一些虚拟传感器模拟器。学生在上面完成从TCP建连到MQTT发布的全链路实验,每一步都有日志可查、有包可抓。
4.2 实验一:环境监测数据采集与告警
模拟多台环境传感器通过MQTT周期发布温度、湿度、PM2.5到env/{deviceId}/data主题。平台规则引擎订阅这个主题,当温度超过设定阈值时,自动发布告警到env/{deviceId}/alert主题。学生可以观察整个数据流:传感器模拟器发布QoS1消息,平台日志显示收到告警,告警接收端实时弹窗。
这个实验的核心学习目标是理解主题分层和通配符:env/+/data加号匹配任意设备ID,env/#匹配后续全部分层。熟练使用这两种通配符,写订阅规则时会轻松很多。
4.3 实验二:远程开关控制与命令下发
设计一个智慧路灯场景,核心就是“下发指令”。平台端发布cmd/light/001主题,内容是{"action":"on","brightness":80},路灯模拟设备订阅这个主题。设备执行动作后,再发布result/light/001回执。整个链路既体现了MQTT双向通信,又演示了基于主题的请求/响应模式。
这个实验建议用QoS1做控制指令,避免因网络抖动导致指令丢失。另一种做法是用TCP私有协议模拟:客户端连上TCP服务后,发送固定格式指令[0xAA][0x01][0x00][0x64][0xAA]控制灯光亮度,平台解析后回复ACK帧。两个实验对比摆在一起,学生能直观看出标准MQTT和私有TCP协议在开发效率上的差别。
4.3 实验三:QoS与遗嘱消息差异化验证
QoS实验需要两台MQTTX客户端和一台模拟断电的设备。设备端以QoS0发布消息后立即断网,订阅端收不到这条消息;改用QoS1配合Clean Session=false,断网期间Broker缓存消息,重连后补发给订阅端。通过这个实验,QoS三个等级的区别就不再是死记硬背的概念。
遗嘱消息的实验更直观:设备A设置遗嘱主题device/A/status,遗嘱内容offline,连接建立后,A直接断网;平台端在另一个客户端订阅这个主题,能看到Broker主动推送了offline消息。这个实验能帮学生理解为什么设备掉线监测不能只靠“心跳超时判断”,遗嘱消息比心跳判活更及时。
4.4 实验四:Modbus TCP工业设备模拟
使用Modbus Poll或Modbus Slave工具模拟一台Modbus TCP设备,上位机发送03功能码读取寄存器,修改06功能码写入寄存器。平台服务端解析MBAP头,把寄存器值映射到设备属性,再通过MQTT转成JSON上报。这其实模拟了工业场景中“仪表-网关-平台”的完整链路:Modbus负责采集,MQTT负责上云。
也可以拓展到RTU转TCP实验:用串口工具模拟RTU从站,通过网关串口读取数据后转换成Modbus TCP数据帧,再推到平台。这样学生就能理解边缘网关在工业物联网里真正的角色是“协议翻译器”。
4.5 实训考核建议
我给学生做这套实验时,考核点设计成三层:基础层是能完成MQTT连接和消息收发,会抓包看三次握手;进阶层是能解释QoS差异,能设计主题命名规范,遇到粘包问题能自己定位和解决;挑战层是能把Modbus TCP数据接入平台并转成MQTT上云。不同层次对应不同分数,这样既照顾了基础薄弱的同学,也给能力强的学生留了发挥空间。
实训中出现最多的错误有三个:第一,Client ID重复导致互踢,刚连上就掉线;第二,防火墙没放行1883端口,外部设备连不上;第三,订阅主题写死但发布端用了不同层级,消息“消失”了。这三个问题我都会在下一节重点讲排查思路。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
下面这张表是我在多个项目里汇总出来的,遇到问题可以先按表排查:
| 现象 | 常见原因 | 快速处理 |
|---|---|---|
| MQTT客户端连接失败 | Broker没启动、端口写错、防火墙拦截 | 检查mosquitto.exe -v日志,用telnet IP 1883测试端口连通性 |
| 客户端连上就掉线 | Client ID重复互相踢 | 每个客户端使用唯一Client ID |
| 订阅收不到消息 | 发布和订阅主题不一致、通配符写错 | 用MQTTX同时订阅#观察实际消息主题 |
| TCP服务页面报端口被占用 | 上一次进程没退出 | Windows用`netstat -ano |
| 设备在线但平台显示离线 | 心跳超时、遗嘱被误触发 | 调大KeepAlive值,检查网络稳定性 |
| Docker拉取镜像超时 | 网络原因无法访问默认镜像源 | 配置可信镜像加速,重试拉取 |
| Android收不到MQTT消息 | 主线程阻塞导致回调没执行 | 确保连接和回调在子线程,检查INTERNET权限 |
浏览器访问平台接口报reset by peer | 后端服务崩溃或防火墙断开 | 查看服务端日志,用curl -v复现请求观察具体阶段 |
5.2 端口占用:见一次治一次
Windows下最典型的报错是:
error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address翻译过来就是:端口11434已经被占用,地址被绑定,不能再绑定第二次。这种情况90%是因为之前启动过服务但没关干净,或者另一个程序占用了同一个端口。
排查流程我固定三步:
netstat -ano | findstr 11434这一步能查到占用端口的PID。然后看任务管理器对应PID是什么进程,确认是不是自己残留的旧进程,如果是就结束:
taskkill /PID 12345 /F如果不想杀进程,也可以直接改配置换端口。这里强烈建议所有端口相关配置都放到配置文件里,不要在代码里硬编码,否则每次换环境都要翻代码找端口。
还有一个常见场景:ADB报daemon not running; starting now at tcp:5037 could not read ok from adb server。意思是ADB服务在5037端口启动失败,多半是5037被别的程序占了,或者多个ADB版本冲突。同样用netstat -ano | findstr 5037找到占用进程,结束后再adb kill-server && adb start-server,基本能解决。
5.3 消息“丢”了?三步定位法
MQTT消息丢失在教学和开发里都极其常见。我总结了一套三步定位法:
第一步,先看Broker日志。Mosquitto开启-v后,客户端连接、订阅、发布都会打日志,如果连日志都没有,说明消息根本没到Broker,问题在网络上。
第二步,用MQTTX开一个#通配符订阅端,观察全局消息。如果能看到消息,说明Broker工作正常,问题在具体订阅端的主题和QoS配置上。
第三步,检查QoS和Clean Session。QoS0的消息在客户端离线时直接丢弃,Clean Session=true时离线期间的QoS1消息也不会补发。所以“消息丢了”很多时候不是网络问题,而是会话策略的问题。
顺便说一句curl: (35) tcp connection reset by peer这类错误,是在请求服务时连接被对端重置。常见原因包括服务端崩溃、防火墙主动断开、TLS握手失败。排查时用curl -v看详细输出,确认连接建立到哪个阶段被断开,就能缩小范围。
5.4 一个很有用的习惯:协议一开始就带版本号
最后分享一个我踩坑踩出来的经验。设计TCP私有协议时,请求帧一定要带协议版本号字段。哪怕是1.0,也能在后续升级时通过版本号做兼容处理。我见过一个设备升级后报文格式变了,服务端没有版本判断,直接按旧格式解析,结果所有数据全部错位,排查了一整天才定位到是版本不兼容。
更推荐的做法是:TCP帧格式固定为“魔数+版本号+长度+业务类型+数据+CRC校验”。魔数用于快速校验是不是自己的设备,版本号用于协议演进,长度解决粘包,业务类型方便扩展指令,CRC保证数据完整。这个格式能应付绝大多数物联网场景。
MQTT这边同样要给消息设计规范,消息体里带msg_id字段用于去重和追踪,带timestamp字段用于判断数据新鲜度。不要偷懒只发一个裸数值,后面做数据分析和问题定位时你会感谢当初多写的这个字段。