资讯详情

资讯详情

树莓派轻量MQTT服务:C#实现的嵌入式消息通信方案

简介本资源是一套基于.NET Core 3.0实现MQTT协议通信的完整开发实践包面向物联网应用开发者、C#后端工程师及学习轻量级消息中间件集成的中高级程序员解决在跨平台环境中快速构建MQTT客户端与服务器Broker的核心问题。压缩包共77个文件含7个核心C#源码文件如Form1.cs、Program.cs、2个可执行项目工程.csproj、1个解决方案.sln以及DLL依赖库、JSON配置、PDB调试符号等辅助文件结构清晰便于理解客户端发布/订阅逻辑与服务端消息路由机制。资源包仅1.15MB精简实用已获2570人学习下载。读者可直接运行Mqtt.Demo中的Client与Server双示例结合详尽注释掌握MQTTnet 2.7.5.0库的连接配置、QoS消息发布、主题订阅及事件回调处理全流程是深入理解IoT设备通信落地的高价值实操素材。1. 一个跑在树莓派上的轻量 MQTT 服务3 分钟就能把温湿度传感器数据推到网页仪表盘你手头有个 ESP32 或树莓派 Pico接了 DHT22 温湿度模块想把数据实时传到局域网另一台电脑的网页上画曲线——但又不想搭 Node-RED、不碰 Python 的 asyncio 黑匣子、更不想啃 Eclipse Mosquitto 的 C 编译玄学。这个基于 .NET Core 3.0 的 MQTT 项目就是为你写的它自带 ServerBroker和 Client 两个可直接运行的控制台程序Server 不依赖任何外部服务Client 支持发布/订阅双模式所有代码用 C# 写得干净利落连Program.cs都没藏私。它不是教学 Demo而是某物联网实验室真实压测过的通信底座——在 4 核 ARM64 设备上稳定承载 200 终端、QoS1 消息吞吐超 800 条/秒。适合嵌入式开发者、工业网关调试员、以及被 Java Spring Boot MQTT Starter 依赖版本搞崩溃过三次的后端同学。别急着翻文档先dotnet run起来5 行命令之后你就能看到“Hello from MQTT Client”出现在 Server 控制台里。2. 技术栈与架构设计为什么选 MQTTnet 而不是自己手撸协议解析这个项目没有造轮子而是把 MQTTnet 这个成熟、轻量、纯 .NET 实现的库用到了极致。MQTTnet 不是简单封装 System.Net.Sockets它内部实现了完整的 MQTT 3.1.1 协议状态机、会话持久化抽象、内存/文件两种默认存储策略还预留了自定义IStorage接口。更重要的是它对 .NET Core 3.0 的IHostedService和ILoggerT做了原生适配——这意味着 Server 可以作为后台服务长期运行日志能直接对接 Serilog 或 ConsoleLogger不用再写一堆胶水代码。整个项目结构极简只有三个核心工程MqttDemo.sln ├── MqttServer/ # Broker 主体HostedService 内存会话管理 TCP 监听 ├── MqttClient/ # 双模式客户端PublishMode发传感器数据 / SubscribeMode收告警 └── Shared/ # 公共实体MqttMessageDto、TopicRule、ConnectionConfig提示Shared 工程里没有业务逻辑只放 DTO 和配置类。这是为后续扩展留的伏笔——比如你要加 TLS 支持只需在 Shared 里新增TlsOptions类Server 和 Client 就能共享同一套证书加载逻辑避免配置散落在两处。2.1 Server 架构从 TCP 连接到消息路由的四层链路Broker 启动后实际走的是这样一条链路HostedService 层MqttServerHostedService实现IHostedService在StartAsync()中启动IMqttServer实例网络层MqttFactory.CreateMqttServer()默认使用TcpServerAdapter监听0.0.0.0:1883每个新连接触发OnClientConnected事件会话层连接建立后自动创建MqttClientSession其ClientId作为内存字典 Key 存入ConcurrentDictionarystring, IMqttClientSession路由层当 Client 发送PUBLISH包Server 解析 Topic如sensor/room1/temperature按通配符规则sensor//temperature或sensor/#匹配所有订阅者逐个投递。关键不在“怎么连”而在“怎么不丢”。MQTTnet 的IMqttServerOptionsBuilder允许你精细控制WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(30))防僵尸连接WithMaxPendingMessagesPerClient(1000)防恶意 Client 塞爆内存WithConnectionValidator(...)可插拔认证比如校验 JWT Token 或白名单 IP。2.2 Client 模式拆解PublishMode 与 SubscribeMode 的行为差异Client 工程通过命令行参数切换模式# 发布模式每 2 秒向 topic sensor/livingroom/temp 推送 JSON dotnet run --project MqttClient/ -- -m publish -t sensor/livingroom/temp -i 2 # 订阅模式监听 sensor/#收到消息就打印并触发本地告警 dotnet run --project MqttClient/ -- -m subscribe -t sensor/#PublishMode的核心是MqttClient.PublishAsync()调用链// MqttClient/PublishMode.cs var message new MqttApplicationMessageBuilder() .WithTopic(topic) .WithPayload(JsonSerializer.Serialize(data, _jsonOptions)) .WithQualityOfService(MqttQualityOfService.ExactlyOnce) // QoS2 .WithRetain(false) .Build(); await _mqttClient.PublishAsync(message, CancellationToken.None);注意WithQualityOfService参数QoS0 是“发了就忘”QoS1 是“至少一次”带 PUBACKQoS2 是“恰好一次”PUBREC/PUBREL/PUBCOMP 三段握手。项目默认设为 QoS1 —— 它在可靠性与性能间取得最佳平衡实测丢包率 0.002%而 QoS2 在高并发下会显著增加延迟。SubscribeMode则依赖事件驱动// MqttClient/SubscribeMode.cs _mqttClient.ApplicationMessageReceivedAsync e { var payload Encoding.UTF8.GetString(e.ApplicationMessage.Payload); var dto JsonSerializer.DeserializeMqttMessageDto(payload, _jsonOptions); Console.WriteLine($[{DateTime.Now:HH:mm:ss}] {e.ApplicationMessage.Topic}: {payload}); if (dto.Value 35 e.ApplicationMessage.Topic.Contains(temperature)) TriggerLocalAlarm(); // 例如点亮 LED 或写入本地日志 };这里的关键是ApplicationMessageReceivedAsync事件——它不是轮询而是由 MQTTnet 底层 Socket 收到PUBLISH包后主动触发毫秒级响应。你不需要手动while(true) { client.Receive() }这是 .NET Core 异步 I/O 的红利。2.3 Shared 工程DTO 与配置如何支撑跨模块一致性Shared 工程看似简单却是项目可维护性的命脉。它定义了MqttMessageDto统一消息体结构含Topic、Value、Timestamp、Unit四个字段强制序列化时忽略 null 值ConnectionConfigServer 地址、端口、ClientId、用户名密码等支持从appsettings.json自动绑定TopicRule用于 Server 端做 Topic 白名单校验例如new TopicRule(sensor//temperature, read)。MqttMessageDto的设计有讲究public class MqttMessageDto { [JsonPropertyName(topic)] public string Topic { get; set; } [JsonPropertyName(value)] public double Value { get; set; } [JsonPropertyName(ts)] public long Timestamp { get; set; } DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); [JsonPropertyName(unit)] public string Unit { get; set; } C; }Timestamp字段设为long而非DateTime是为了跨平台兼容JavaScript Date.now() 也是毫秒时间戳Unit默认C但允许 Client 发送F或%Server 不做转换只做透传——语义交给上层应用处理Broker 保持无状态。3. 快速运行指南从零开始跑通 Server Client 双向通信别被 “.NET Core 3.0” 吓住它对运行环境要求极低。只要你的机器装了 .NET SDK 3.1注意3.0 已 EOL但项目兼容 3.1/5.0/6.0就能跑。Windows/macOS/Linux 全支持树莓派 ARM32/ARM64 也验证通过。3.1 环境准备确认 SDK 版本与项目还原首先检查 .NET SDK 是否就位dotnet --list-sdks # 输出应包含类似3.1.426 [/usr/share/dotnet/sdk] # 若无请去 https://dotnet.microsoft.com/download/dotnet/3.1 下载 SDK 3.1.xLTS 版本克隆或解压项目后进入根目录执行dotnet restore # 此命令会读取 *.csproj 中的 PackageReference下载 MQTTnet 3.1.0 及其依赖 # 注意项目锁定 MQTTnet 3.1.0因为 4.0 已转向 .NET 5与 Core 3.0 不兼容提示如果你看到NU1102: Unable to find package MQTTnet...错误大概率是 NuGet 源被重定向。临时切回官方源dotnet nuget add source https://api.nuget.org/v3/index.json -n official3.2 启动 Server监听 1883 端口并观察日志在终端 A 中执行cd MqttServer/ dotnet run你会看到类似输出info: MqttServerHostedService[0] Starting MQTT server on port 1883... info: MqttServerHostedService[0] MQTT server started successfully. info: MqttServerHostedService[0] Listening on 0.0.0.0:1883此时 Server 已就绪。它默认启用内存会话InMemorySessionStorage不保存历史消息符合轻量定位。若需断线重连后接收离线消息需自行实现IStorage并注入见第 5 章。3.3 启动 Client发布一条测试消息并验证接收新开终端 B执行cd MqttClient/ dotnet run -- -m publish -t test/topic -p Hello from .NET Core MQTT几秒后回到终端 A你应该看到info: MqttServerHostedService[0] Client test-client published to test/topic: Hello from .NET Core MQTT再试订阅模式终端 C 中运行dotnet run -- -m subscribe -t test/topic此时终端 C 会打印[14:22:35] test/topic: Hello from .NET Core MQTT双向链路已通。接下来你可以把-p参数换成真实传感器数据比如dotnet run -- -m publish -t sensor/kitchen/humidity -p {\value\:65.2,\unit\:\%\,\ts\:1715638955000}Server 会原样转发SubscribeMode Client 会自动反序列化为MqttMessageDto对象。3.4 验证工具用 mosquitto_sub/mosquitto_pub 做第三方交叉验证为了证明这不是“自家兄弟互吹”我们用业界标准工具验证互通性。安装 mosquittoUbuntu/Debiansudo apt update sudo apt install mosquitto-clients在 Server 运行状态下另开终端执行# 用 mosquitto 订阅 mosquitto_sub -h localhost -p 1883 -t test/topic -v # 在另一个终端用 mosquitto 发布 mosquitto_pub -h localhost -p 1883 -t test/topic -m From mosquitto!如果mosquitto_sub终端成功打印test/topic From mosquitto!说明该项目完全遵循 MQTT 3.1.1 协议与任何标准 Broker/Client 兼容。这是项目落地可信度的硬指标。4. 常见问题排查那些让你卡住 2 小时的典型翻车现场刚跑起来时最容易在几个地方栽跟头。这些不是文档遗漏而是 .NET Core 网络编程和 MQTT 协议细节交织产生的“隐性坑”。我把它们列出来每条都附真实复现步骤和绕过方案。4.1 现象Server 启动报错System.Net.Sockets.SocketException: Permission denied原因Linux/macOS 下1024 以下端口需要 root 权限。MQTT 默认端口 1883 属于特权端口。解决方案一推荐改用非特权端口修改MqttServer/appsettings.jsonMqttServerOptions: { Port: 18830 }然后 Client 连接时指定-p 18830方案二用sudo dotnet run不推荐安全风险方案三给 dotnet 二进制文件授 CAP_NET_BIND_SERVICE 权限高级用户。4.2 现象Client 连接 Server 失败日志显示Unable to connect to remote server原因Server 默认绑定0.0.0.0:1883但 Client 代码中ConnectionConfig.Server默认值是localhost。如果 Server 运行在树莓派上而 Client 在 Windows 上运行localhost指向的是 Windows 本机而非树莓派 IP。解决显式传入 Server IPdotnet run -- -m publish -s 192.168.1.123 -p test或修改MqttClient/appsettings.json中的Server字段为树莓派局域网 IP。4.3 现象SubscribeMode Client 收不到消息但 Server 日志显示published to xxx原因MQTT 的 Topic 匹配是大小写敏感的且和#通配符有严格语法。例如sensor/room1/temp不会被sensor/Room1/temp订阅到sensor/不会匹配sensor/room1/temp因为只匹配单级sensor/#才匹配多级。解决用mosquitto_sub -t sensor/# -v先验证 Broker 是否真发出消息检查 Client 订阅的 Topic 字符串是否与 Publisher 完全一致包括大小写、斜杠结尾在SubscribeMode.cs中加一行日志Console.WriteLine($Subscribed to: {topic}); // 确认传入的 topic 值4.4 现象发布大量消息后Client 报OperationCanceledException或 Server CPU 暴涨原因默认MqttClientOptions的KeepAlivePeriod是 60 秒但未设置CleanSession true。当 Client 频繁断连重连Server 会为每个ClientId创建新会话旧会话内存不释放最终 OOM。解决在 Client 初始化时显式设置var options new MqttClientOptionsBuilder() .WithTcpServer(config.Server, config.Port) .WithClientId(config.ClientId) .WithCredentials(config.Username, config.Password) .WithCleanSession(true) // 关键每次连接都清空旧会话 .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) .Build();或在 Server 端启用会话过期需自定义IStorage见第 5 章。4.5 现象JSON 反序列化失败SubscribeMode报JsonException: The input does not contain any JSON tokens原因Publisher 发送的 payload 是纯字符串如25.6但SubscribeMode期望MqttMessageDto对象。两者类型不匹配。解决Publisher 必须发送合法 JSON 对象不能只发数字或字符串✅ 正确{value:25.6,unit:C,ts:1715638955000}❌ 错误25.6或25.6在SubscribeMode.cs中加防御性判断try { var dto JsonSerializer.DeserializeMqttMessageDto(payload, _jsonOptions); // 处理 dto } catch (JsonException ex) { Console.WriteLine($Invalid JSON payload: {payload}. Error: {ex.Message}); return; }5. 二次开发实战给 Server 加持久化存储与 TLS 支持现在 Server 是纯内存的断电即失Client 通信明文不适合生产环境。这两项改造是项目从 Demo 迈向可用的分水岭。我不会教你抄文档而是给出可直接粘贴的代码块并说明每一行为什么这么写。5.1 为 Server 添加文件级会话持久化让离线消息不丢失MQTTnet 提供FileSessionStorage但它默认只存会话元数据ClientId、CleanSession 标志不存未确认的 QoS1/QoS2 消息。我们要补全这个缺口。第一步在MqttServer/工程中新增PersistentSessionStorage.cs// MqttServer/PersistentSessionStorage.cs public class PersistentSessionStorage : IStorage { private readonly string _basePath; private readonly ILoggerPersistentSessionStorage _logger; public PersistentSessionStorage(string basePath, ILoggerPersistentSessionStorage logger) { _basePath Path.GetFullPath(basePath); _logger logger; Directory.CreateDirectory(_basePath); } public async TaskIMqttClientSession LoadClientSessionAsync(string clientId, CancellationToken cancellationToken) { var path Path.Combine(_basePath, ${clientId}.session.json); if (!File.Exists(path)) return null; try { var json await File.ReadAllTextAsync(path, cancellationToken); return JsonSerializer.DeserializeMqttClientSession(json, new JsonSerializerOptions { PropertyNameCaseInsensitive true }); } catch (Exception ex) { _logger.LogWarning(ex, Failed to load session for {ClientId}, clientId); return null; } } public async Task StoreClientSessionAsync(IMqttClientSession session, CancellationToken cancellationToken) { var path Path.Combine(_basePath, ${session.ClientId}.session.json); var json JsonSerializer.Serialize(session, new JsonSerializerOptions { WriteIndented true }); await File.WriteAllTextAsync(path, json, cancellationToken); } // 其他方法LoadMessage, StoreMessage暂留空实现因本项目暂不需离线消息队列 public TaskIMqttApplicationMessage LoadMessageAsync(string messageId, CancellationToken cancellationToken) Task.FromResultIMqttApplicationMessage(null); public Task StoreMessageAsync(IMqttApplicationMessage message, CancellationToken cancellationToken) Task.CompletedTask; }第二步在MqttServerHostedService.cs的StartAsync()中注入该存储// MqttServer/MqttServerHostedService.cs protected override async Task StartAsync(CancellationToken cancellationToken) { var options new MqttServerOptionsBuilder() .WithDefaultEndpointPort(_config.Port) .WithStorage(new PersistentSessionStorage(./sessions, _logger)) // ← 新增这行 .Build(); _server MqttFactory.CreateMqttServer(options); await _server.StartAsync(cancellationToken); }现在每次 Client 断连其会话含订阅列表、QoS1 待确认消息 ID都会写入./sessions/{ClientId}.session.json。重启 Server 后Client 重连时能自动恢复订阅关系。注意FileSessionStorage不处理消息体本身只存会话状态。若需真正离线消息如 Client 断线期间发布的消息在重连后推送需实现StoreMessageAsync/LoadMessageAsync并配合IMqttServerOptions.WithPersistentSessions(true)。本项目未实现因会显著增加磁盘 IO与“轻量”定位冲突。5.2 为 Client 和 Server 同时启用 TLS用自签名证书搞定加密通信明文 MQTT 在局域网尚可但一旦暴露在公网或企业内网必须上 TLS。.NET Core 3.0 原生支持SslStreamMQTTnet 也提供了WithTcpServerSecure方法。第一步生成自签名证书Linux/macOS# 生成私钥和证书有效期 365 天 openssl req -x509 -newkey rsa:2048 -keyout mqtt.key -out mqtt.crt -days 365 -nodes -subj /CNlocalhost # 合并为 pfxClient 需要 openssl pkcs12 -export -in mqtt.crt -inkey mqtt.key -out mqtt.pfx -passout pass:123456第二步修改 Server 启动逻辑监听 8883 端口并加载证书// MqttServer/MqttServerHostedService.cs var options new MqttServerOptionsBuilder() .WithDefaultEndpointPort(_config.Port) // 仍用 1883 .WithDefaultEndpointTlsOptions(tls { tls.CertificateFilePath ./mqtt.pfx; tls.CertificatePassword 123456; tls.CertificateValidationHandler (cert, chain, errors) true; // 开发期跳过证书校验 }) .Build();第三步Client 连接时启用 TLS// MqttClient/Program.cs var options new MqttClientOptionsBuilder() .WithTcpServerSecure(config.Server, config.Port) // ← 关键用 WithTcpServerSecure 替代 WithTcpServer .WithClientId(config.ClientId) .WithTls(tls { tls.UseTls true; tls.CertificateValidationHandler (cert, chain, errors) true; }) .Build();最后Client 连接命令变为dotnet run -- -m publish -s localhost -p 8883 -t test/tls -p encrypted此时 Wireshark 抓包将看不到明文 Topic 和 Payload只有 TLS 握手流量。证书校验逻辑CertificateValidationHandler在生产环境必须改为校验 CA 签名但开发阶段设为true可避免因证书链不完整导致连接失败——这是血泪经验宁可先通再安别一上来就卡在证书上。5.3 改造 Checklist上线前必做的五件事事项操作验证方式1. 端口收敛Server 关闭 1883 明文端口只开 8883或用防火墙限制 1883 仅局域网访问netstat -tuln | grep :18832. ClientID 唯一性确保每个物理设备有唯一ClientId如 MAC 地址哈希避免会话冲突查看 Server 日志中Client xxx connected的 clientId3. 日志分级将ILogger的LogLevel从Information调至Warning减少高频 debug 日志刷屏日志量下降 90% 以上4. 连接池复用Client 不要每次发消息都新建MqttFactory.CreateMqttClient()应单例复用_mqttClient观察进程句柄数是否稳定5. 心跳保活设置WithKeepAlivePeriod(TimeSpan.FromSeconds(30))防 NAT 超时断连持续运行 2 小时确认无意外断连从那以后我每次给嵌入式设备写 MQTT Client都强制走一遍这五件事 checklist —— 尤其是ClientId唯一性和心跳保活踩过太多次坑。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →