简介:面向物联网开发初学者与.NET技术人员的MQTT通信实践资源,聚焦在.NET Core 3.0环境下借助MQTTnet库打通客户端与服务器(Broker)的完整链路,解决协议入门与代码落地之间的衔接问题。压缩包共77个文件,以cs源码、csproj项目文件、dll与pdb程序集、json配置及cache缓存文件为主,另含sln解决方案与少量txt说明,整体约1.15MB,可直接用Visual Studio打开运行。资源内含Mqtt.Demo解决方案,客户端与服务端分项目组织,覆盖连接、发布、订阅、消息接收处理及Broker启动监听等关键环节,源码注释较细,便于对照理解QoS级别与发布/订阅模型的实际写法。已有2568人学习下载,适合希望快速跑通MQTT通信demo并在此基础上二次开发的读者参考。
1. 从一条“设备离线”告警说起:这个 .NET Core 3.0 MQTT 双端 Demo 到底解决什么问题
去年帮一个做环境监测的小团队排查问题,现场十几台采集终端每隔几分钟就集体掉线一次,后台日志里全是连接被重置的记录。翻到最后发现,问题不在网络,也不在服务器,而是他们自己写的一个“简易 TCP 长连接”在弱网下重连逻辑写崩了。当时我给的建议很直接:别自己造轮子了,上 MQTT,用 MQTTnet 把 Client 和 Server 两端都跑一遍,先把链路打通再谈业务。这份基于 .NET Core 3.0 的 MQTT 通信 Demo,干的正是这件事——它同时给出客户端和服务端的完整可运行代码,让你不用先去啃协议规范,就能看到一条消息从发布到订阅的完整走向。物联网协议里 MQTT 之所以被大量采用,核心就是轻量、长连接、支持一对多分发,而 MQTTnet 则是 .NET 生态里把这套协议封装得比较顺手的一个库。这个项目适合两类人:一类是想快速验证设备接入链路的开发者,另一类是准备在现有系统里补一块消息通信能力、但不想从零写协议栈的工程团队。下面我按“它是什么 → 怎么跑起来 → 核心代码怎么读 → 怎么改”的顺序拆。
2. 项目结构与技术选型:为什么是 .NET Core 3.0 加 MQTTnet
2.1 整体分层与目录组织
这个 Demo 的工程组织不复杂,但该有的分层都在。典型结构大致是这样,不同人下载后可能略有差异,但核心目录跑不出这个范围:
MqttDemo/ ├── MqttDemo.Server/ # 服务端(Broker)宿主 │ ├── Program.cs # 入口,注册并启动 MQTT 服务 │ ├── MqttServerService.cs # 封装 Broker 的启动、停止、事件回调 │ └── appsettings.json # 端口、客户端上限等配置 ├── MqttDemo.Client/ # 客户端(发布者/订阅者) │ ├── Program.cs # 入口,建立连接并订阅/发布 │ ├── MqttClientService.cs # 连接管理、消息收发封装 │ └── appsettings.json # Broker 地址、ClientId、Topic 配置 └── MqttDemo.sln # 解决方案文件服务端负责把 Broker 跑起来,客户端负责连上去发消息、收消息。两边拆成独立项目的好处是,你可以只启动 Server,然后用任意第三方 MQTT 客户端(比如桌面调试工具)连进来验证,不必被自己的 Client 代码绑死。这种“Broker 独立、Client 可替换”的组织方式,在真实项目里也很常见——设备端往往不是 .NET,服务端却可以是。
2.2 技术栈与选型理由
技术栈很明确:.NET Core 3.0 作为运行时,MQTTnet 作为协议实现库。为什么选 .NET Core 3.0 而不是更老的 Framework?因为 Core 3.0 已经支持跨平台,服务端可以跑在 Linux 上,客户端也能在 Windows、Linux 甚至容器里跑,这对物联网场景很关键——现场网关多半是 Linux。为什么选 MQTTnet?它把 Broker 和 Client 都封装在同一个包里,服务端不需要额外部署 Mosquitto 之类的独立 Broker,一个控制台程序就能起一个可用的 MQTT 服务,做 Demo 和联调非常省事。
常见做法是:服务端用MqttServer或MqttFactory创建实例,客户端用MqttClient或MqttFactory创建实例,两边通过MqttServerOptionsBuilder和MqttClientOptionsBuilder配置参数。这里有个容易忽略的点:.NET Core 3.0 对应的 MQTTnet 版本较老,API 命名和现在的新版本有差异,比如早期用MqttServer直接构造,后来改成MqttFactory.CreateMqttServer()。你下载后如果发现编译不过,先看包版本,别急着改业务代码。
2.3 运行环境与依赖准备
跑之前先确认环境。需要安装 .NET Core 3.0 SDK 或更高兼容版本,然后还原依赖。命令行操作如下:
# 进入解决方案目录 cd MqttDemo # 还原 NuGet 包(MQTTnet 会在这一步拉下来) dotnet restore # 编译整个解决方案 dotnet builddotnet restore的作用是根据项目文件里的 PackageReference 把 MQTTnet 等依赖下载到本地缓存;dotnet build则是编译。如果 restore 卡住,多半是 NuGet 源配置问题,检查一下NuGet.config或全局源。编译通过后,先启动服务端,再启动客户端,顺序不能反——客户端连不上 Broker 会直接抛连接异常。
提示:.NET Core 3.0 已经停止官方支持,如果本机装的是更高版本 SDK,项目文件里的 TargetFramework 可能需要从
netcoreapp3.0调整到netcoreapp3.1或更高,改完重新 restore 即可,业务代码一般不用动。
3. 服务端拆解:一个 Broker 从启动到收发消息的完整链路
3.1 Broker 启动与端口配置
服务端的核心就是起一个 MQTT Broker,监听某个端口,等待客户端连接。关键代码通常长这样:
// 创建 MQTT 服务实例 var factory = new MqttFactory(); var mqttServer = factory.CreateMqttServer(); // 配置监听端口和连接参数 var optionsBuilder = new MqttServerOptionsBuilder() .WithDefaultEndpoint() // 使用默认端点 .WithDefaultEndpointPort(1883) // MQTT 默认端口 .WithConnectionBacklog(100) // 等待连接队列长度 .WithMaxPendingMessagesPerClient(1000); // 单客户端待处理消息上限 var options = optionsBuilder.Build(); // 注册客户端连接/断开事件 mqttServer.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(e => { Console.WriteLine($"客户端已连接: {e.ClientId}"); }); mqttServer.ClientDisconnectedHandler = new MqttServerClientDisconnectedHandlerDelegate(e => { Console.WriteLine($"客户端已断开: {e.ClientId}"); }); // 启动服务 await mqttServer.StartAsync(options); Console.WriteLine("MQTT Broker 已启动,监听 1883 端口");这段代码的逻辑是:先通过工厂创建服务实例,再用 Builder 配置端点、端口、连接队列和单客户端消息上限,最后注册连接和断开事件并启动。参数里WithDefaultEndpointPort(1883)是 MQTT 明文协议的标准端口,改端口时客户端配置要同步改;WithConnectionBacklog(100)控制的是 TCP 层等待 accept 的队列长度,设备集中上线时可以适当调大;WithMaxPendingMessagesPerClient(1000)是防止某个慢客户端把服务端内存拖垮的兜底,设太小会导致消息被丢弃,设太大又占内存,1000 是个折中值。
3.2 消息拦截与主题路由
Broker 除了转发,还经常需要在服务端做一层消息拦截,比如记录日志、做权限校验、或者对特定主题做特殊处理。MQTTnet 提供了拦截器机制:
// 拦截客户端发布的消息,可在转发前做处理 mqttServer.InterceptingPublishAsync += args => { var topic = args.ApplicationMessage.Topic; var payload = args.ApplicationMessage.Payload; Console.WriteLine($"收到发布: Topic={topic}, Payload长度={payload?.Length}"); // 示例:拒绝向系统保留主题发布 if (topic.StartsWith("$SYS/")) { args.ProcessPublish = false; // 阻止该消息继续分发 } return Task.CompletedTask; };这里InterceptingPublishAsync是发布拦截事件,args.ApplicationMessage.Topic是主题,Payload是消息体字节数组。把ProcessPublish设为false就能拦下这条消息,不再分发给订阅者。这个机制在真实项目里很有用——比如你想限制某些设备只能往自己的主题发数据,就可以在这里按 ClientId 和 Topic 做匹配校验。注意Payload是字节数组不是字符串,打印前要按约定编码转换,否则看到的是乱码。
3.3 服务端启动顺序与验证
服务端跑起来后,别急着上自己的客户端,先用一个通用 MQTT 调试工具连一下 1883 端口,确认 Broker 真的在监听。验证步骤:启动服务端 → 用调试工具填 Broker 地址和端口 → 连接 → 订阅一个主题 → 再发一条消息到该主题 → 看能否收到。这一步能排除掉大部分“客户端代码没问题但连不上”的玄学问题。如果调试工具连不上,先看服务端控制台有没有连接日志,再看防火墙是否放行了 1883 端口。血泪经验是:很多“连不上”其实是本机防火墙或云服务器安全组没开端口,跟代码一点关系没有。
4. 客户端拆解:连接、订阅、发布三段式怎么写才稳
4.1 建立连接与断线重连
客户端的第一件事是连上 Broker。核心代码:
var factory = new MqttFactory(); var client = factory.CreateMqttClient(); var options = new MqttClientOptionsBuilder() .WithTcpServer("127.0.0.1", 1883) // Broker 地址和端口 .WithClientId("demo-client-01") // 客户端唯一标识 .WithCleanSession() // 清理会话,适合 Demo .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) // 心跳间隔 .Build(); // 注册断线事件,便于观察重连触发 client.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(e => { Console.WriteLine($"连接断开: {e.Exception?.Message}"); }); await client.ConnectAsync(options); Console.WriteLine("已连接到 Broker");WithTcpServer指定地址和端口;WithClientId必须唯一,两个客户端用同一个 ClientId 会互相踢下线,这是新手最常踩的坑之一;WithCleanSession()表示不保留会话状态,重连后之前的订阅会丢失,Demo 里够用,但生产环境如果要保证离线消息不丢,得改成持久会话;WithKeepAlivePeriod是心跳周期,设太短会增加网络负担,设太长则断线发现慢,30 秒是常见取值。断线事件里能拿到异常信息,排查连接问题时先看这里。
4.2 订阅主题与消息回调
连上之后订阅主题,并注册消息接收回调:
// 订阅主题,QoS 1 表示至少送达一次 await client.SubscribeAsync(new TopicFilterBuilder() .WithTopic("sensor/temperature") .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build()); // 消息到达回调 client.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => { var topic = e.ApplicationMessage.Topic; var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); Console.WriteLine($"收到消息: Topic={topic}, Payload={payload}"); });TopicFilterBuilder用来构造订阅过滤器,WithTopic支持通配符,比如sensor/+/temperature匹配中间任意一层,sensor/#匹配多层。WithQualityOfServiceLevel设 QoS 等级:0 是最多一次、可能丢,1 是至少一次、可能重复,2 是恰好一次、开销最大。Demo 里用 1 比较稳妥。回调里Payload同样是字节数组,用Encoding.UTF8.GetString转字符串,前提是发布端也按 UTF-8 编码,两边编码不一致就会出现乱码,这个坑我在联调时见过不止一次。
4.3 发布消息与参数控制
发布消息的代码:
var message = new MqttApplicationMessageBuilder() .WithTopic("sensor/temperature") .WithPayload("{\"value\":26.5,\"unit\":\"C\"}") // 消息体,建议 JSON .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .WithRetainFlag(false) // 不保留最后一条消息 .Build(); await client.PublishAsync(message); Console.WriteLine("消息已发布");WithPayload传字符串时会按 UTF-8 编码成字节数组;WithRetainFlag(true)表示 Broker 保留这条消息,新订阅者一上来就能收到最后一条,适合“设备当前状态”这类场景,但状态频繁变化时慎用,否则容易收到过期数据。发布和订阅用同一个主题时,自己发的消息自己也能收到(如果自己订阅了),联调时别误以为是重复消息。
4.4 客户端生命周期管理
一个完整的客户端应该包含连接、订阅、发布、断开四个阶段,并且断开时要释放资源:
try { await client.ConnectAsync(options); await client.SubscribeAsync(topicFilter); await client.PublishAsync(message); // 模拟业务运行 await Task.Delay(TimeSpan.FromSeconds(10)); } catch (Exception ex) { Console.WriteLine($"客户端异常: {ex.Message}"); } finally { if (client.IsConnected) { await client.DisconnectAsync(); } client.Dispose(); }DisconnectAsync是正常断开,会通知 Broker;Dispose释放内部资源。如果不调Dispose,在频繁创建客户端的场景下可能出现连接句柄泄漏。生产环境里客户端通常是长期存活的,配合重连策略使用,而不是像 Demo 这样跑完就断。
5. 避坑与排查:联调时最容易翻车的五个点
5.1 现象:客户端连不上,报连接被拒绝
原因通常是 Broker 没启动、端口不对,或者防火墙/安全组没放行。解决顺序:先确认服务端控制台有“Broker 已启动”日志,再用telnet 127.0.0.1 1883或调试工具测端口通不通,最后检查防火墙规则。别一上来就怀疑代码。
5.2 现象:两个客户端互相踢下线
原因是 ClientId 重复。MQTT 规定同一 Broker 下 ClientId 唯一,后连的会把先连的顶掉。解决方法是给每个客户端生成唯一 ClientId,比如拼上设备编号或 GUID。这个坑在批量部署时特别常见,现场表现为“设备随机掉线”,其实是 ID 撞了。
5.3 现象:订阅了但收不到消息
先确认发布和订阅的主题字符串完全一致,包括大小写和层级分隔符;再确认 QoS 和会话设置,WithCleanSession()下重连后订阅会丢,需要在重连后重新订阅。还有一种情况是发布时RetainFlag为 true 但订阅端连上时间晚于发布,这时能收到保留消息,但如果 Broker 重启且没持久化,保留消息也会丢。
5.4 现象:消息体乱码
原因是发布端和订阅端编码不一致。发布端用 UTF-8 编码字符串,订阅端却按 GBK 解码,就会出现乱码。统一约定用 UTF-8,并且在代码里显式写Encoding.UTF8,不要依赖系统默认编码。二进制数据则不要转字符串,直接按字节处理。
5.5 现象:编译报错,找不到 MQTTnet 的某个类型或方法
原因是 MQTTnet 版本与代码不匹配。.NET Core 3.0 时期用的 MQTTnet 版本 API 和现在差异较大,比如MqttServer的创建方式、事件委托的命名都变过。解决方法是看项目文件里的 PackageReference 版本号,按该版本的 API 写;如果升级了包版本,就对照新版本文档调整创建方式和事件注册写法,别混用两套 API。
6. 二次开发改造:从 Demo 到能用的设备接入层
拿到这个 Demo 后,最直接的改造方向是把“跑通”变成“能用”。我一般会先做三件事:第一,把 ClientId 和 Topic 从硬编码改成配置驱动,appsettings.json里按设备维度配置,避免每加一台设备就改代码;第二,给客户端加重连策略,用指数退避,断线后按 1s、2s、4s 递增重试,而不是死循环猛连;第三,把消息体从裸字符串改成统一 JSON 结构,至少包含设备号、时间戳、指标值三个字段,方便后端解析入库。
再往深一层,服务端可以加认证。Demo 里是匿名连接,生产环境至少要在InterceptingPublishAsync或连接校验事件里按用户名密码或客户端证书做一层校验。MQTTnet 支持在MqttServerOptionsBuilder上配置认证相关回调,具体写法随版本不同,思路是连接建立时校验凭据,不通过就拒绝。另一个值得做的改造是持久会话:把客户端的WithCleanSession()去掉,配合固定 ClientId,让 Broker 在客户端离线期间缓存 QoS 1/2 的消息,重连后补发。这个改动对“设备短暂断网但数据不能丢”的场景很关键。
验证改造是否有效,我习惯用两个手段:一是用调试工具模拟一个慢客户端,看服务端会不会因为消息堆积把内存吃满;二是手动断网再恢复,观察客户端是否按预期重连并补收消息。下面这张表是我改造时常用的参数对照,供参考:
| 参数 | Demo 取值 | 生产建议 | 影响 |
|---|---|---|---|
| ClientId | 硬编码 | 设备号或 GUID | 重复会导致互踢 |
| CleanSession | true | false(需持久化时) | 影响离线消息保留 |
| KeepAlive | 30s | 30~60s | 断线发现速度 |
| QoS | 1 | 按业务选 0/1/2 | 可靠性与开销 |
| RetainFlag | false | 状态类消息可 true | 新订阅者能否收到最后状态 |
改造过程中最容易忽略的是资源释放和异常兜底。Demo 里try/catch/finally写得比较简,真实项目里连接、订阅、发布每一步都可能抛异常,尤其是网络抖动时。我的习惯是每个异步操作都包一层超时控制,避免某个调用卡死拖垮整个客户端。从那以后我每次接 MQTT 项目,都强制先把“断线重连 + 唯一 ClientId + UTF-8 编码”这三件事走一遍再写业务,省下来的排查时间远比多写的几行代码值。希望这份拆解能帮到你,把 Demo 跑起来只是起点,真正值钱的是你在这套链路上做的那些加固。
本文还有配套的精品资源,点击获取