简介:面向物联网开发初学者与.NET技术人员的MQTT通信实践资源,聚焦在.NET Core 3.0环境下借助MQTTnet库打通客户端与服务器(Broker)的完整链路,解决协议入门难、缺少可运行示例的问题。压缩包共77个文件,约1.15MB,以cs源码、csproj项目文件、sln解决方案为主,辅以dll、exe等编译产物及json配置、cache缓存文件,构成可直接打开调试的工程结构。资源内含Client与Server两个独立项目,覆盖连接建立、主题订阅、消息发布与接收处理等关键环节,源码中配有详细注释,便于对照理解发布/订阅模式与QoS机制的实际落地方式。已有2568人学习下载,适合希望快速上手MQTT协议、搭建本地通信Demo并在此基础上扩展物联网应用的开发者参考。
1. 从一份 .NET Core 3.0 的 MQTT 双端 Demo 说起:它到底能帮你省掉哪段弯路
物联网项目里最容易被低估的一环,不是传感器选型,也不是云端存储,而是设备与服务器之间那条“消息通道”怎么搭。很多人第一次接触 MQTT 时,脑子里全是“发布/订阅”“主题”“QoS”这些词,真到动手写代码,却发现连一个能跑通的 Client 和 Server 都凑不齐。这份基于 .NET Core 3.0、用 MQTTnet 实现的 MQTT 通信 Demo,恰好补的就是这一段:它同时给出客户端和服务端的完整可运行代码,让你不用在协议细节和工程骨架之间反复横跳。适合谁?适合已经会用 C# 写控制台程序、想快速把 MQTT 通道跑起来验证业务逻辑的开发者,也适合需要给团队搭一个最小可复现通信样例的工程师。它不教你从零设计协议,但能让你在半小时内看到一条消息从发布端走到订阅端。
2. MQTTnet 在 .NET Core 3.0 下的选型逻辑与工程骨架
2.1 为什么是 MQTTnet,而不是自己撸协议
MQTT 协议本身不复杂,固定头加可变头加载荷,但真正落到工程里,连接保活、会话状态、QoS 1/2 的重传与去重、遗嘱消息、主题通配符匹配,每一项单独拎出来都能写出一堆边界条件。自己从 Socket 层撸一遍,短期看是“可控”,长期看是“维护黑洞”。MQTTnet 的价值在于它把这些协议状态机封装好了,同时保留了足够的可配置项:你可以只用一个MqttFactory创建客户端,也可以在同一套库上起一个轻量 Broker。
选型时我一般看三点:一是库是否同时提供 Client 和 Server 能力,避免两端用不同实现导致行为不一致;二是是否支持 .NET Core 3.0 这个目标框架,毕竟不少老库还停在 .NET Framework;三是 API 是否足够“薄”,能让我在出问题时直接看到底层报文。MQTTnet 在这三点上都过得去,尤其是它把 Broker 也做进了同一个包,Demo 里 Server 端不需要额外装 Mosquitto 就能自洽运行,这对复现非常关键。
需要说清楚的是,MQTTnet 的 Server 端定位是“可嵌入的轻量 Broker”,适合做 Demo、做内网小规模验证、做单元测试里的假服务端,不适合直接拿去扛生产级的大规模连接。生产环境常见做法是 Client 用 MQTTnet,Broker 换成更成熟的独立服务。这个边界先立住,后面看代码才不会误用。
2.2 工程骨架:两个控制台项目加一个共享契约
这份 Demo 的结构很朴素,但朴素得合理。常见做法是拆成三个项目:一个 Server 控制台、一个 Client 控制台、一个共享的契约类库。共享库放主题常量、消息体模型和序列化辅助,避免两端各写一份字符串导致主题拼错。
# 目录结构示意(.NET Core 3.0) MqttDemo/ ├── MqttDemo.Server/ # Broker 端,托管 MQTTnet Server ├── MqttDemo.Client/ # 设备端/应用端,MQTTnet Client └── MqttDemo.Shared/ # 主题常量、消息模型建项目时目标框架写netcoreapp3.0,三个项目都引同一版本的 MQTTnet 包。这里有个血泪经验:Client 和 Server 引的 MQTTnet 版本必须一致,否则序列化行为和 API 签名对不上,编译期可能不报,运行期才炸。
dotnet new console -n MqttDemo.Server -f netcoreapp3.0 dotnet new console -n MqttDemo.Client -f netcoreapp3.0 dotnet new classlib -n MqttDemo.Shared -f netcoreapp3.0 dotnet add MqttDemo.Server package MQTTnet dotnet add MqttDemo.Client package MQTTnet命令本身没什么玄学,关键是-f netcoreapp3.0要显式指定,否则新版 SDK 默认给你拉到更高版本,后面 API 差异会让你怀疑人生。共享库里的主题常量建议用const string而不是配置文件,Demo 阶段少一层间接,排错时一眼能看到主题名。
2.3 Server 端:把 Broker 起起来的最小闭环
Server 端的核心就三件事:配置端口、注册客户端连接/断开/消息拦截事件、启动。MQTTnet 的 Server 选项里,默认端口 1883 是明文,Demo 阶段够用;如果本机已有其他 Broker 占用,改端口即可。
// MqttDemo.Server/Program.cs using MQTTnet.Server; var optionsBuilder = new MqttServerOptionsBuilder() .WithDefaultEndpoint() // 默认 1883 .WithDefaultEndpointPort(1883); var mqttServer = new MqttFactory().CreateMqttServer(optionsBuilder.Build()); // 客户端接入时打印 ClientId,方便确认谁连上来了 mqttServer.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(e => { Console.WriteLine($"[Server] client connected: {e.ClientId}"); }); // 收到任意发布消息时打印主题和载荷长度 mqttServer.ApplicationMessageNotConsumedAsync += e => { Console.WriteLine($"[Server] topic={e.ApplicationMessage.Topic}, " + $"payload={e.ApplicationMessage.Payload?.Length ?? 0} bytes"); return Task.CompletedTask; }; await mqttServer.StartAsync(); Console.WriteLine("[Server] broker started on 1883, press Enter to exit"); Console.ReadLine(); await mqttServer.StopAsync();这段代码的逻辑说明:MqttServerOptionsBuilder负责端口和端点,CreateMqttServer拿到实例后,通过事件处理器挂上观测点。ClientConnectedHandler是旧式委托写法,ApplicationMessageNotConsumedAsync是异步事件写法,两种风格在 MQTTnet 里并存,Demo 里混用不影响运行,但团队里最好统一一种,否则后面加逻辑容易漏await。参数上,WithDefaultEndpointPort改端口时记得 Client 端同步改,这是最常见的“连不上”原因之一。
2.4 Client 端:连接、订阅、发布三段式
Client 端比 Server 端多一层状态管理:连接成功后才能订阅,订阅确认后才能稳定收消息,发布则随时可做但要处理 QoS 回执。Demo 里用控制台做交互,输入主题和内容就发一条。
// MqttDemo.Client/Program.cs using MQTTnet; using MQTTnet.Client; using MQTTnet.Protocol; var factory = new MqttFactory(); var client = factory.CreateMqttClient(); var options = new MqttClientOptionsBuilder() .WithTcpServer("127.0.0.1", 1883) .WithClientId("demo-client-01") .WithCleanSession() .Build(); client.ApplicationMessageReceivedAsync += e => { var payload = System.Text.Encoding.UTF8.GetString( e.ApplicationMessage.Payload ?? Array.Empty<byte>()); Console.WriteLine($"[Client] recv topic={e.ApplicationMessage.Topic}, payload={payload}"); return Task.CompletedTask; }; await client.ConnectAsync(options); Console.WriteLine("[Client] connected"); // 订阅一个通配主题,QoS 1 await client.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() .WithTopicFilter("demo/+/data", MqttQualityOfServiceLevel.AtLeastOnce) .Build()); // 发布一条消息 var message = new MqttApplicationMessageBuilder() .WithTopic("demo/device01/data") .WithPayload("{\"temp\":26.5}") .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build(); await client.PublishAsync(message); Console.WriteLine("[Client] published"); Console.ReadLine(); await client.DisconnectAsync();逻辑说明:WithTcpServer指定 Broker 地址,WithClientId在 Demo 里写死,生产里要保证唯一,否则同 ClientId 后连的会把先连的踢掉,这是 MQTT 的协议行为,不是库的 bug。订阅用demo/+/data单层通配,能匹配demo/device01/data,但匹配不到demo/a/b/data,通配符层级要心里有数。QoS 选AtLeastOnce是 Demo 的稳妥选择,至少一次意味着可能重复,消费端要能容忍重复消息,这一点后面避坑章节会展开。
3. 把 Demo 跑起来:编译、联调与消息链路验证
3.1 启动顺序与端口占用排查
正确的启动顺序是先 Server 后 Client。Server 起来后控制台会打印监听端口,Client 再去连。如果 Client 报连接超时,第一件事不是改代码,而是看端口有没有被占。
# Windows 下查 1883 占用 netstat -ano | findstr :1883 # Linux/macOS 下查 lsof -i :1883如果发现被别的进程占了,两个选择:改 Demo 端口,或者停掉占用进程。改端口时 Server 的WithDefaultEndpointPort和 Client 的WithTcpServer第二参数要同步改,漏一个就是白忙。另一个常见现象是 Server 启动时报“地址已在使用”,本质也是端口冲突,不是权限问题,别急着用管理员身份跑。
3.2 用日志确认消息真的走通了
Demo 跑起来后,判断链路是否通,不要只看“没报错”。可靠的做法是看三处日志:Server 的client connected、Client 的connected、以及发布后 Client 自己收到的recv。因为 Client 既订阅又发布,同一条消息会回到自己,这正好形成闭环验证。
// 在 Client 发布后加一个短等待,避免控制台立刻退出看不到回显 await Task.Delay(500);这个Task.Delay不是业务逻辑,是 Demo 里的观测窗口。真实项目里消息回显靠事件驱动,不需要 sleep。如果三处日志只出现前两处,说明订阅没生效或主题不匹配;如果出现前两处但收不到回显,优先检查订阅的主题过滤器是否覆盖了发布主题,通配符写错一个字符就静默失败,这是 MQTT 的“黑匣子”特性之一。
3.3 参数对照表:改哪里、影响什么
| 参数 | 位置 | 默认值 | 改动影响 |
|---|---|---|---|
| 端口 | Server/Client | 1883 | 两端必须一致,否则连接拒绝 |
| ClientId | Client | demo-client-01 | 重复会导致互踢 |
| 主题过滤器 | Client 订阅 | demo/+/data | 不匹配则收不到消息 |
| QoS | 发布/订阅 | AtLeastOnce | 影响重复与确认开销 |
| CleanSession | Client | true | false 时保留会话状态 |
这张表建议贴在联调时的便签上,出问题按行排查,比盲改代码快得多。尤其是 ClientId 和主题过滤器两项,Demo 阶段写死没问题,一旦复制出第二个 Client 实例,互踢现象会让人误以为是网络问题。
4. 避坑与常见问题:那些跑通之后才会暴露的细节
4.1 现象:Client 连上又立刻断开
原因通常是 ClientId 冲突。MQTT 协议规定同一 ClientId 只能有一个活跃连接,后连的会把先连的顶掉,先连的那端表现为“莫名断开”。解决方式是给每个实例生成唯一 ClientId,Demo 里可以用Guid.NewGuid().ToString("N")拼接前缀,生产里用设备序列号。
4.2 现象:订阅成功但收不到消息
原因多半是主题过滤器与发布主题不匹配,或者大小写不一致。MQTT 主题是大小写敏感的,Demo/+/data和demo/+/data是两个世界。解决方式是两端共用共享库里的主题常量,杜绝手写字符串。另一个隐蔽原因是订阅时 QoS 低于发布时 QoS,某些 Broker 实现下行为有差异,Demo 阶段统一用 QoS 1 最省心。
4.3 现象:QoS 1 下消息重复消费
这不是 bug,是协议语义。至少一次投递意味着网络抖动或确认丢失时会重发。解决方式是在消费端做幂等,比如消息体里带一个业务唯一 ID,处理前查重。Demo 里消息体是简单 JSON,真实项目里这个 ID 字段要提前设计进去,事后补会很痛苦。
4.4 现象:Server 端内存缓慢上涨
原因通常是客户端断开后会话状态没清理,或者CleanSession设为 false 且没有过期机制。Demo 阶段用WithCleanSession()默认清理,长期运行的服务端要关注会话保留策略。另一个可能是消息事件里做了阻塞操作,导致内部队列堆积,事件处理器里只做轻量记录,重活丢到队列外处理。
4.5 现象:.NET Core 3.0 下包还原失败
原因可能是包源配置或 SDK 版本不匹配。先确认dotnet --list-sdks里有 3.0 对应的 SDK,再用dotnet restore单独跑一次看详细报错。如果团队里有人用更高版本 SDK 打开同一份代码,global.json里锁定 SDK 版本能避免“我这能跑你那不能跑”的扯皮。
5. 从 Demo 到可用:把双端样例改造成可验证的通信骨架
Demo 跑通只是起点,真正有价值的是把它改造成能承载业务验证的骨架。我一般做三件事:第一,把主题设计成层级清晰的命名空间,比如app/{env}/{deviceType}/{deviceId}/data,环境段和类型段让订阅端可以按需收窄;第二,把消息体从裸 JSON 换成带版本号和时间戳的信封结构,版本号让协议演进时不至于把老客户端打挂;第三,给 Client 加一个重连策略,网络闪断在真实环境里是常态,Demo 里没有重连,一断就得手动重启。
// 简易重连:连接断开事件里延迟重试 client.DisconnectedAsync += async e => { Console.WriteLine($"[Client] disconnected: {e.Exception?.Message}"); await Task.Delay(TimeSpan.FromSeconds(3)); try { await client.ConnectAsync(options); Console.WriteLine("[Client] reconnected"); } catch (Exception ex) { Console.WriteLine($"[Client] reconnect failed: {ex.Message}"); } };这段重连逻辑刻意写得朴素,没有指数退避,因为 Demo 场景下三秒固定间隔足够观察行为。真实项目里要加退避和最大重试次数,否则 Broker 宕机时所有客户端会形成重连风暴。参数上TimeSpan.FromSeconds(3)是可调的,设备数量越多,间隔越要拉开并加随机抖动。
验证改造是否成功,我习惯用一个笨办法:手动杀掉 Server 再拉起,看 Client 是否自动恢复并继续收到消息;再复制一个 Client 实例改 ClientId,看两端是否互不干扰。这两步过了,骨架基本可用。从那以后我每次把 MQTT Demo 交给别人之前,都会强制走一遍“杀服务端、复制客户端、改主题大小写”这三连测试,因为翻车往往就藏在这些看起来不会出问题的地方。希望帮到你。
本文还有配套的精品资源,点击获取