基于MQTTnet的C# MQTT服务端与客户端Demo实战详解 📅 发布时间:2026/9/2 2:54:39 👁 浏览次数: 简介一份用于学习MQTT协议在C#环境下落地实现的测试案例同时提供服务端与客户端Demo适合物联网开发者、C#后端工程师快速上手消息发布/订阅场景。资源内包含完整工程代码与运行依赖核心为MqttServer与MqttClient的初始化和事件处理覆盖主题订阅、QoS配置、消息发布与接收等关键环节。压缩包共2000个文件以xml配置、dll依赖、p7s签名、txt说明及cs源码为主另有sln工程、exe可执行文件与nuget包整体约42.99MB文件归类基本保留了Visual Studio工程结构便于直接加载调试。目前已有688人学习适合希望通过现成示例理解Broker与Client交互机制、并在此基础上改造物联网应用的开发者。 最近整理工业物联网项目把之前写的那个 MQTT C# demo 测试案例翻了出来重新捋了一遍。别看它叫“demo”里里外外已经把 MQTT 协议的服务端、客户端常见玩法都覆盖到了Broker 搭建、客户端连接、消息发布订阅、断线重连、QoS 验证整条链路都能跑通。如果你正在接触 MQTT或者要做 C# 上位机设备对接、数据采集、消息推送这份案例可以直接拿来做模板省掉自己从零踩坑的时间。接下来我按“整体设计 → 服务端实现 → 客户端实现 → 联调排障”的顺序把里面的细节和坑一次说清楚。1. 项目整体思路与结构设计1.1 MQTT 协议的核心概念为什么设备场景里总选它MQTT 和常见的 HTTP 请求响应不一样它是基于发布/订阅模型的轻量级消息协议。打个比方HTTP 就像你直接打电话问对方“现在几点”对方必须在线回答MQTT 更像你把问题贴到群里谁感兴趣谁就回复就算有人暂时不在线消息也会在群里留一会儿。这套模型里有三个关键角色Broker服务端、Publisher发布者、Subscriber订阅者。发布者和订阅者不直接通信所有消息都经过 Broker 中转。消息本身通过 Topic 做路由比如device/sensor/001/status支持/做层级分隔也支持单层匹配和#多层匹配通配符。既然叫“测试案例”光说概念没用关键是把这些机制落到代码里。而这个 demo 里最有价值的一点是服务端和客户端都用 C# 写你不需要装额外的语言环境就能在本地把整个协议链路研究明白。1.2 demo 的组成服务端、客户端怎么分工标题里说的“服务端、客户端”在这类项目里要拆开理解。Broker 是消息服务端实际生产环境我一般用 EMQX 或 Mosquitto而业务服务端是指订阅并处理消息的后台程序比如接收设备状态后写数据库。客户端则指发布消息的设备端或者订阅消息的展示端。这个 demo 为了单机也能跑我在服务端一侧直接内嵌了一个轻量 Broker用 MQTTnet 暴露的 Server API 实现这样不依赖任何外部安装包双击就能启动。同时 ClientDemo 可以独立发布、订阅完整模拟设备上报和后台接收两条链路。如果你手头有现成的 EMQX 或者云上 Broker把客户端里的连接地址换一下代码完全不用动。这个方案最大的好处是“零依赖”。以前在别人电脑上演示 MQTT总要现场装个 Mosquitto还得配防火墙、改配置折腾半小时。内嵌 Broker 之后演示成本降到最低学习和测试效率高很多。2. 服务端实现内嵌 Broker 与外部 Broker 切换2.1 用 MQTTnet 写一个轻量服务端内嵌 Broker服务端我用的是 NuGet 上的 MQTTnet 库当前推荐 4.x 版本。先创建一个控制台项目然后安装包Install-Package MQTTnet服务端核心代码并不复杂主要就是创建 MqttServer、配置端口、启动监听再挂几个事件观察客户端连接和消息流转using MQTTnet; using MQTTnet.Server; var options new MqttServerOptions(); options.DefaultEndpointOptions.Port 1883; var mqttServer new MqttFactory().CreateMqttServer(options); // 客户端连接成功事件 mqttServer.ClientConnectedAsync e { Console.WriteLine($[服务端] 客户端已连接: {e.ClientId}); return Task.CompletedTask; }; // 客户端断开事件 mqttServer.ClientDisconnectedAsync e { Console.WriteLine($[服务端] 客户端已断开: {e.ClientId}); return Task.CompletedTask; }; // 拦截所有发布的消息方便观察协议流转 mqttServer.InterceptingPublishAsync e { var payload e.ApplicationMessage.ConvertPayloadToString(); Console.WriteLine($[服务端] 收到消息 Client{e.ClientId}, Topic{e.ApplicationMessage.Topic}, Payload{payload}); return Task.CompletedTask; }; await mqttServer.StartAsync(); Console.WriteLine([服务端] Broker 已启动监听 1883 端口); Console.ReadLine();这段代码在 MQTTnet 4.x 下可以直接跑。其中ConvertPayloadToString()是库提供的扩展方法比手动Encoding.UTF8.GetString处理PayloadSegment省事得多强烈推荐。启动后服务端会监听 1883 端口MQTT 默认端口。如果端口被占用启动时会抛异常这时候可以用netstat -ano | findstr 1883查一下谁占用了端口。另外 Windows 防火墙首次运行可能会弹窗本地测试直接允许即可。2.2 再接一个外部 BrokerMosquitto 部署与验证虽然内嵌 Broker 很方便但我还是建议你额外部署一个 Mosquitto原因很简单生产环境几乎不会用程序内嵌 Broker你迟早要面对真实的服务端部署。而且外部 Broker 能同时接多个客户端方便验证 MQTT 一对多、多对多的消息转发。Windows 下部署 Mosquitto 非常快。去官网下载 Windows 压缩包解压后打开命令行进入目录直接运行mosquitto -v看到mosquitto version ... starting就说明 Broker 起来了默认监听 1883 端口。如果不在配置里显式关闭匿名访问默认情况下匿名客户端可以连接。demo 阶段保持匿名即可正式环境必须开用户名密码认证并且配置 TLS 加密传输。验证 Broker 通不通最直接的方式是用 Mosquitto 自带的命令行客户端。开两个终端一个订阅一个发布mosquitto_sub -t test/topic -v mosquitto_pub -t test/topic -m hello mqtt订阅端能打印出test/topic hello mqtt说明服务端工作正常。用这个方式先排掉 Broker 的锅再排查客户端代码会省很多时间。我见过不少人一上来就调 C# 代码最后发现是 Mosquitto 没启动方向完全错了。3. 客户端实现连接、发布、订阅与断线重连3.1 MQTTnet 客户端连接参数与版本注意事项客户端同样基于 MQTTnet 实现。先创建连接选项再调用ConnectAsync。连接参数是这里最容易出问题的地方我逐个说明using MQTTnet; using MQTTnet.Client; var mqttFactory new MqttFactory(); using (var client mqttFactory.CreateMqttClient()) { var options new MqttClientOptionsBuilder() .WithTcpServer(127.0.0.1, 1883) // Broker 地址和端口 .WithClientId(client-demo-001) // 客户端唯一标识 .WithCredentials(username, password) // 按需填写匿名可省略 .WithCleanSession(true) // 是否清除会话 .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) // 心跳间隔 .Build(); var result await client.ConnectAsync(options, CancellationToken.None); if (result.ResultCode MqttClientConnectResultCode.Success) { Console.WriteLine([客户端] 连接成功); } }先说ClientId这是个大坑。同一时刻相同 ClientId 只能有一个客户端在线。如果两个程序用了同一个 ClientId 连接 Broker后连接的那个会把先连接的踢下线而且被踢方不一定能立刻感知。调试时我建议把 ClientId 做成动态拼接比如加上进程 ID 或随机后缀。再说CleanSession。设为true时客户端断线后 Broker 会清除它的所有订阅和离线消息设为false时会话会被保留但前提是订阅时 QoS 大于 0。这一点在设备断网重连后能否收到离线期间的消息上影响非常大。最后提醒一句版本差异MQTTnet 从 3.x 升到 4.x事件命名变化很大。3.x 里是ApplicationMessageReceived4.x 里改成了ApplicationMessageReceivedAsync而且返回类型从 void 变成 Task。网上很多旧教程的代码直接粘到 4.x 会编译不过看到“事件不匹配”的报错先检查是不是版本问题。3.2 订阅和发布的核心代码订阅某类消息关键是注册ApplicationMessageReceivedAsync事件然后调用SubscribeAsync。收到消息后会异步触发事件我在回调里直接打印到达时间、主题和内容方便观察协议行为client.ApplicationMessageReceivedAsync e { var payload e.ApplicationMessage.ConvertPayloadToString(); Console.WriteLine($[订阅] Topic{e.ApplicationMessage.Topic}, Payload{payload}, 到达时间{DateTime.Now:HH:mm:ss.fff}); return Task.CompletedTask; }; await client.SubscribeAsync(new MqttTopicFilterBuilder() .WithTopic(device//status) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build());这里我用了device//status通配符匹配任何一层。比如device/001/status、device/002/status都能收到。如果只想收某个具体设备就写完整 Topic。Topic 设计要克制不要乱用#层级越多越难维护。发布消息时用MqttApplicationMessageBuilder构造消息体指定 Topic、Payload、QoS 和 Retain 标志var publishMessage new MqttApplicationMessageBuilder() .WithTopic(device/001/status) .WithPayload({\status\:\online\,\value\:42}) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .WithRetainFlag(false) .Build(); await client.PublishAsync(publishMessage, CancellationToken.None);我建议 Payload 统一用 JSON 字符串不管 Broker 还是客户端处理起来都清晰。不建议直接用二进制序列化后续对接其他语言或平台会有额外的兼容成本。测试发布消息和订阅消息时注意 QoS 的选择。QoS 0 最快但可能丢QoS 1 保证至少到达一次可能重复QoS 2 保证只到达一次但开销最大。实际项目里设备状态采集常用 QoS 1控制指令类建议 QoS 1 或 2直接决定消息可靠性。3.3 掉线重连与遗嘱消息的工程做法实际部署时网络抖动是常态。客户端必须做断线检测和自动重连否则设备一断网整个通道就废了。MQTTnet 4.x 里客户端暴露了DisconnectedAsync事件我是这样处理的client.DisconnectedAsync async e { Console.WriteLine($[客户端] 连接断开: {e.Reason}, 5秒后重连...); await Task.Delay(TimeSpan.FromSeconds(5)); // 不建议在这个事件里直接调用 ConnectAsync 的同一个 client 实例 // 更稳妥的做法是外层用一个重连循环或者这里重新创建 client await ReconnectAsync(); };这里要特别提醒DisconnectedAsync事件回调里不适合立刻调用ConnectAsync容易造成重连风暴。我一般会在外层维护一个while (!client.IsConnected)的重连循环配合间隔时间递增策略比如 5 秒、15 秒、30 秒直到连上为止。遗嘱消息LWT是 MQTT 里很实用的机制。比如设备异常掉电服务端想知道它“挂了”可以在连接时设置遗嘱消息.WithWillTopic(device/001/status) .WithWillPayload({\status\:\offline\}) .WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .WithWillRetain(true)设备断开时Broker 会主动发布这条遗嘱消息订阅者就能感知设备离线。注意遗嘱消息只有在网络异常断开比如心跳超时时才触发正常发送 DISCONNECT 报文则不会触发。这一点在设备保活场景里很有用。4. 联调测试与问题排查实录4.1 端到端联调步骤与效果一套完整的联调流程是这样的先启动 ServerDemo内嵌 Broker再启动两个 ClientDemo一个订阅device//status一个发布 JSON 格式的模拟状态消息同时打开 ServerDemo 控制台观察消息转发日志。我自己测试时会额外开一个 Mosquitto 订阅窗口执行mosquitto_sub -t # -v把 Broker 上所有消息都打出来做一个“旁路观察”。这样即使客户端代码有 bug也能从 Broker 侧判断消息到底有没有发出来。实测效果好于预期发布端消息发出后几十毫秒内订阅端和 ServerDemo 控制台都能看到完整消息协议转发链路清晰可见。测 QoS 2 时消息会有更高的确认开销但在这个量级下感知不到延迟差异。真正要注意的是 QoS 2 状态存储内嵌 Broker 的内存模式在重启后状态丢失这是它不适合生产的原因之一。测试完基础收发再手动把客户端进程 Kill 掉看遗嘱消息是否如预期发出。如果遗嘱保留了 Retain 标志新订阅者连上来马上就能看到当前在线状态这种“上线即得当前状态”的效果做设备管理页面时特别有用。4.2 高频踩坑与排查速查表梳理一下我在实际测试中遇到的典型问题基本都是新手常踩的现象可能原因解决办法ConnectAsync 返回 ConnectionRefusedBroker 没启动或端口不对先检查 Broker 是否监听用 telnet 或 mosquitto_pub 验证端口连接成功后立刻掉线ClientId 与在线客户端冲突换唯一 ClientId比如拼接 GUID订阅后始终收不到消息Topic 不匹配或订阅时用了通配符但发布者用了具体主题时路径错误用mosquitto_sub -t # -v观察全局消息收到消息但 Payload 乱码编码不一致统一在发送端使用 UTF-8 编码 JSON 字符串MQTTnet 事件无法编译3.x/4.x API 差异核对项目引用的 MQTTnet 版本调整代码断线后不自动重连没实现 DisconnectedAsync 或重连逻辑在事件里增加延迟重试做好退避策略Broker 启动后局域网连不上Windows 防火墙拦了 1883放行入站规则或临时关闭防火墙测试其中“连接成功后立刻掉线”是排查时间最长的一个。当时两个客户端用了同一个 ClientId后启动的客户端一连接之前的连接立刻被 Broker 断开Broker 日志里能看到client already connected之类的记录。在真实场景下设备上线时的 ClientId 策略必须设计好否则设备批量上线时可能互相挤下线。4.3 几条项目落地经验这个 demo 跑通后我又往真实项目里迁移过几次。有几个经验值得单独说一下。第一内嵌 Broker 只适合测试和演示。生产环境一定要用 EMQX 或 Mosquitto 这类独立服务端因为它们有完整的持久化、认证、监控和集群能力。程序内嵌 Broker 在进程重启时消息状态全丢这在工业场景里不可接受。第二Topic 设计一定要先规划再动手。我习惯用三到四层结构比如“项目/设备类型/设备ID/数据类型”并在文档里明确层级含义。否则项目一复杂Topic 全是临时拼的后期改都改不动。第三消息体结构统一。建议所有消息都带一个timestamp字段接收端就能判断消息是否过期。比如设备断网 10 分钟后又发来一条数据如果只看到内容不看时间很容易把旧数据当成实时数据处理。第四日志要打好。开发测试里客户端连接成功、断开原因、消息到达时间全都要打印出来。没有日志调试 MQTT 就是盲人摸象。最后说句实在话MQTT 本身协议不复杂真正难的是把连接保障、消息可靠性、Topic 规范这些边边角角想清楚。这套 C# demo 我后来在好几个项目里都直接改改就用了比如停车场车牌识别相机对接、上位机状态采集、后台消息推送。如果你也在做类似的事我建议先把这个 demo 完整跑一遍再带着它去对接真实业务逻辑会少走非常多弯路。本文还有配套的精品资源点击获取