如果你最近在做物联网、设备上报、消息推送、边缘网关,或者只是想给系统加一条“轻量、低延迟、天然支持发布订阅”的通道,那 MQTT 依然是一个非常稳的选择。
问题在于,很多人学 MQTT 时会遇到两个很烦的现实:
协议概念懂了,但落到代码里,不知道服务端和客户端到底怎么配。
好不容易连上了,接下来订阅、发布、重连、主题路由、WebSocket 接入,又是一堆零散知识点。
这篇文章就不讲空洞概念了。我们直接基于 TouchSocket.Mqtt,把一套完整的 MQTT 使用方式串起来:从 TCP 服务端、TCP 客户端,到插件收消息、主题路由、WebSocket MQTT,再到几个非常实用的踩坑提醒。
整篇文章内容,基于仓库里的这些真实代码整理而来:
examples/Mqtt/MqttConsoleApp/Program.cs
examples/Mqtt/MqttTopicHandlerConsoleApp/Program.cs
examples/Mqtt/MqttWebSocketConsoleApp/Program.cs
handbook/docs/mqttservice.mdx
handbook/docs/mqttclient.mdx
Src/TouchSocket.Mqtt/*
官网与文档首页:https://touchsocket.net/
GitHub:https://github.com/RRQM
Gitee:https://gitee.com/RRQM_Home
NuGet 包名:TouchSocket.Mqtt
安装命令很直接:
dotnet add package TouchSocket.Mqtt
从当前工程源码可以看到,TouchSocket.Mqtt 支持的目标框架包括:
net462
netstandard2.0
netstandard2.1
net6.0
net8.0
net10.0
这意味着它不只是适合新项目,对不少历史项目也比较友好。
基于当前源码和示例来看,TouchSocket.Mqtt 不是一个“只能发发消息”的轻包装,它提供的是一套相对完整的 MQTT 能力:
支持 TCP 和 WebSocket 两种接入方式
同时覆盖 MqttTcpService 与 MqttTcpClient
支持 MQTT 3.1.1 和 5.0+
支持标准的订阅、取消订阅、发布、QoS
提供插件机制,方便监听连接和消息生命周期
提供 UseMqttTopicHandler 做主题路由封装
支持断线重连插件
服务端可以主动广播消息
如果你已经在用 TouchSocket 体系做网络开发,那这套 MQTT 能力接入成本会非常低,写起来也符合 TouchSocket 一贯的配置风格。
在这套库里,你可以把几个核心角色记住:
MqttTcpService 这是基于 TCP 的 MQTT 服务端,也就是 Broker。
MqttTcpClient 这是基于 TCP 的 MQTT 客户端,用来连接 Broker、订阅主题、发布消息。
HttpService + AddMqttWebSocketService + UseMqttWebSocket 这是基于 WebSocket 的 MQTT 服务端组合,适合浏览器或前端场景。
MqttWebSocketClient 这是基于 WebSocket 的 MQTT 客户端。
IMqttService 服务端统一能力接口,核心是 MqttBroker。
IMqttClient 客户端统一能力接口,核心方法包括:
PublishAsync
SubscribeAsync
UnsubscribeAsync
PingAsync
理解这几个角色后,下面的代码就很顺了。
如果你只是想先把 Broker 立起来,最简单的方式就是直接使用 MqttTcpService。
using TouchSocket.Core;
using TouchSocket.Mqtt;
var service = new MqttTcpService();
var config = new TouchSocketConfig()
.SetListenIPHosts(7789)
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
});
await service.SetupAsync(config);
await service.StartAsync();
Console.WriteLine("MQTT 服务端已启动,监听 tcp://127.0.0.1:7789");
这段代码对应的思路非常清晰:
创建 MqttTcpService
用 TouchSocketConfig 配监听端口
注入日志
SetupAsync
StartAsync
对于入门来说,这已经足够了。
如果你之前写过原生 Socket 或手动拼 Broker,你会明显感受到这种写法的轻松程度。
服务端起来之后,我们再连接一个客户端。
using TouchSocket.Core;
using TouchSocket.Mqtt;
var client = new MqttTcpClient();
var config = new TouchSocketConfig()
.SetRemoteIPHost("tcp://127.0.0.1:7789")
.SetMqttConnectOptions(options =>
{
options.ClientId = Guid.NewGuid().ToString("N");
options.UserName = "TestUser";
options.Password = "TestPassword";
options.ProtocolName = "MQTT";
options.Version = MqttProtocolVersion.V311;
options.KeepAlive = 60;
options.CleanSession = true;
})
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
});
await client.SetupAsync(config);
await client.ConnectAsync();
Console.WriteLine("客户端已连接");
这里最值得注意的是 SetMqttConnectOptions。
它就是整个 MQTT 连接阶段的核心配置入口。根据源码中的 MqttConnectOptions,常用字段包括:
ClientId
UserName
Password
Version
KeepAlive
CleanSession
WillFlag
WillTopic
WillPayload
UserProperties
SessionExpiryInterval
ReceiveMaximum
MaximumPacketSize
也就是说,如果你后面要用 MQTT 5.0 的高级能力,大多数情况下也是从这里展开。
连上 Broker 之后,下一步通常就是订阅。
TouchSocket.Mqtt 的订阅模型比较标准:你先构造 SubscribeRequest,再包装成 MqttSubscribeMessage,最后执行 SubscribeAsync。
var topic1 = "sensor/temperature";
var topic2 = "sensor/humidity";
var subscribeRequest1 = new SubscribeRequest(topic1, QosLevel.AtLeastOnce);
var subscribeRequest2 = new SubscribeRequest(topic2, QosLevel.AtMostOnce);
var mqttSubscribeMessage = new MqttSubscribeMessage(subscribeRequest1, subscribeRequest2);
var mqttSubAckMessage = await client.SubscribeAsync(mqttSubscribeMessage);
foreach (var item in mqttSubAckMessage.ReturnCodes)
{
Console.WriteLine($"订阅结果: {item}");
}
这段 API 有两个优点:
能一次订阅多个主题
每个主题可以单独指定 QoS
如果你需要更细粒度的 MQTT 5.0 订阅控制,SubscribeRequest 还支持这些高级参数:
NoLocal
RetainAsPublished
RetainHandling
也就是说,它不只是“能订阅”,而是预留了比较完整的 MQTT 订阅语义。
订阅完成之后,自然就是发布消息。
最常用的写法如下:
using System.Text;
var payload = Encoding.UTF8.GetBytes("Hello TouchSocket MQTT");
var message = new MqttPublishMessage(
"sensor/temperature",
false,
QosLevel.AtLeastOnce,
payload);
await client.PublishAsync(message);
这里 4 个关键参数分别代表:
主题名 TopicName
是否保留消息 retain
QoS 等级
消息体 payload
如果你还在区分 QoS 的使用场景,可以简单这么记:
AtMostOnce 追求吞吐,不太在乎偶发丢失
AtLeastOnce 大多数业务场景的均衡选择
ExactlyOnce 代价最高,适合对重复与丢失都极其敏感的场景
在 TouchSocket.Mqtt 里,消息接收主要不是靠你手写轮询,而是通过插件机制来接。
这也是这套库非常顺手的一点。
如果你只关心“收到了一条真正的业务消息”,那就监听 IMqttReceivedPlugin。
using TouchSocket.Core;
using TouchSocket.Mqtt;
internal class MqttClientReceivedPlugin : PluginBase, IMqttReceivedPlugin
{
public async Task OnMqttReceived(IMqttSession client, MqttReceivedEventArgs e)
{
var mqttMessage = e.MqttMessage;
Console.WriteLine("收到发布消息: " + mqttMessage.TopicName);
Console.WriteLine("QoS: " + mqttMessage.QosLevel);
Console.WriteLine("Payload: " + Encoding.UTF8.GetString(mqttMessage.Payload.FirstSpan));
await e.InvokeNext();
}
}
如果你不只关心发布消息,还想看到订阅、取消订阅、确认报文等,就用 IMqttReceivingPlugin。
internal class MqttClientReceivingPlugin : PluginBase, IMqttReceivingPlugin
{
public async Task OnMqttReceiving(IMqttSession client, MqttReceivingEventArgs e)
{
Console.WriteLine("收到 MQTT 报文类型: " + e.MqttMessage.MessageType);
await e.InvokeNext();
}
}
这两个插件的区别,建议你直接记一句话:
IMqttReceivedPlugin 看“业务消息”
IMqttReceivingPlugin 看“所有 MQTT 报文”
服务端的收消息方式和客户端高度一致,也是通过插件做。
例如,服务端监听所有 MQTT 报文:
internal class MqttServerReceivingPlugin : PluginBase, IMqttReceivingPlugin
{
public async Task OnMqttReceiving(IMqttSession client, MqttReceivingEventArgs e)
{
switch (e.MqttMessage)
{
case MqttSubscribeMessage subscribeMessage:
foreach (var item in subscribeMessage.SubscribeRequests)
{
Console.WriteLine($"客户端订阅主题: {item.Topic}, QoS: {item.QosLevel}");
}
break;
case MqttUnsubscribeMessage unsubscribeMessage:
foreach (var topic in unsubscribeMessage.TopicFilters)
{
Console.WriteLine($"客户端取消订阅: {topic}");
}
break;
}
await e.InvokeNext();
}
}
如果你只想在服务端接收发布消息本身,同样可以用 IMqttReceivedPlugin。
internal class MqttServerReceivedPlugin : PluginBase, IMqttReceivedPlugin
{
public async Task OnMqttReceived(IMqttSession client, MqttReceivedEventArgs e)
{
Console.WriteLine("服务端收到消息: " + e.MqttMessage.TopicName);
await e.InvokeNext();
}
}
这种设计非常适合做这些事情:
审计某个客户端订阅了哪些主题
记录发布轨迹
做权限校验
做消息监控
对接业务日志系统
很多人以为 MQTT Broker 只能被动转发客户端消息,实际上在 TouchSocket.Mqtt 里,服务端自己也可以主动发消息。
关键入口就是 IMqttService.MqttBroker。
MqttTcpService 本身就实现了 IMqttService,所以你可以直接这样写:
using System.Buffers;
using System.Text;
var broker = service.MqttBroker;
await broker.ForwardMessageAsync(
new MqttArrivedMessage(
"system/broadcast",
QosLevel.AtLeastOnce,
false,
new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("服务器主动广播"))));
这在下面这些场景里非常有价值:
服务端推送系统公告
服务端主动下发控制命令
后台任务完成后主动通知设备
从数据库或消息队列读取内容后转发给 MQTT 客户端
作为服务端,除了收发消息,你通常还需要能看到当前有哪些客户端在线,以及必要时主动断开某个客户端。
当前示例里已经给出了非常直接的写法:
foreach (var client in service.Clients)
{
Console.WriteLine(client.Id);
}
如果要踢掉某个客户端:
foreach (var client in service.Clients)
{
if (client.Id == "123")
{
await client.CloseAsync("normal close");
}
}
这意味着你可以很容易做出基础的 Broker 管理能力,比如:
在线客户端列表
指定客户端断开
简单的后台运维页面
如果你看过 examples/Mqtt/MqttTopicHandlerConsoleApp/Program.cs,会发现它演示了一个非常实用的能力:UseMqttTopicHandler。
这是我觉得这套 MQTT 代码里最适合写业务的一个点。
它的作用可以概括成一句话:
连接成功后自动订阅配置好的主题,并在收到消息后按主题规则自动分发到对应处理器。
换句话说,你不用每次都手写:
连接成功后手动订阅
收到消息后自己写 if/else 判断主题
你可以直接这样声明式配置:
var subscriber = new MqttTcpClient();
await subscriber.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7791")
.SetMqttConnectOptions(options =>
{
options.ClientId = "subscriber-" + Guid.NewGuid().ToString("N");
options.CleanSession = true;
options.KeepAlive = 60;
options.Version = MqttProtocolVersion.V311;
})
.ConfigurePlugins(a =>
{
a.UseMqttTopicHandler<MqttTcpClient>(opt =>
{
opt.AddSubscription("home/temperature", async (client, msg) =>
{
var text = Encoding.UTF8.GetString(msg.Payload.FirstSpan);
Console.WriteLine($"收到消息 topic={msg.TopicName}, payload={text}");
await Task.CompletedTask;
});
});
}));
根据源码 MqttSubscriptionManagementPlugin<TClient> 的实现,它至少帮你做了两件事:
在 OnMqttConnected 后自动发起订阅
在 OnMqttReceived 时自动按主题匹配并路由到处理器
这就让你的代码结构会非常清晰:
连接配置是连接配置
主题声明是主题声明
消息处理是消息处理
而不是全堆在一个大方法里。
UseMqttTopicHandler 还有一个很适合写文章和业务示例的点,就是它支持 MQTT 的主题通配符。
a.UseMqttTopicHandler<MqttTcpClient>(opt =>
{
opt.AddSubscription("home/+/temperature", async (client, msg) =>
{
Console.WriteLine($"命中单级通配符: {msg.TopicName}");
await Task.CompletedTask;
});
});
这个规则可以匹配:
home/room1/temperature
home/room2/temperature
但不能匹配:
home/room1/device1/temperature
因为 + 只匹配一层。
a.UseMqttTopicHandler<MqttTcpClient>(opt =>
{
opt.AddSubscription("home/#", async (client, msg) =>
{
Console.WriteLine($"命中多级通配符: {msg.TopicName}");
await Task.CompletedTask;
});
});
这个规则就宽松很多,可以匹配:
home/temperature
home/room1/humidity
home/floor1/room2/light
如果你的设备主题层级很多,# 会非常好用。
在 UseMqttTopicHandler 模式下,多主题订阅的体验也很好。
a.UseMqttTopicHandler<MqttTcpClient>(opt =>
{
opt.AddSubscription("sensor/temperature", async (client, msg) =>
{
Console.WriteLine("温度: " + Encoding.UTF8.GetString(msg.Payload.FirstSpan));
await Task.CompletedTask;
})
.AddSubscription("sensor/humidity", async (client, msg) =>
{
Console.WriteLine("湿度: " + Encoding.UTF8.GetString(msg.Payload.FirstSpan));
await Task.CompletedTask;
})
.AddSubscription("sensor/pressure", QosLevel.AtLeastOnce, async (client, msg) =>
{
Console.WriteLine("气压: " + Encoding.UTF8.GetString(msg.Payload.FirstSpan));
await Task.CompletedTask;
});
});
这里有两个特别顺手的地方:
可以链式调用 AddSubscription
可以为单个主题单独指定 QoS
这已经很接近真实业务系统里的写法了。
如果你的 MQTT 不只是给设备用,还想让 Web 前端直接接进来,那么 WebSocket 版本就非常重要。
在 TouchSocket.Mqtt 里,WebSocket MQTT 的服务端不是直接用 MqttTcpService,而是基于 HttpService 组合出来的。
using TouchSocket.Core;
using TouchSocket.Http;
using TouchSocket.Mqtt;
var service = new HttpService();
await service.SetupAsync(new TouchSocketConfig()
.SetListenIPHosts("tcp://127.0.0.1:1883")
.ConfigureContainer(a =>
{
a.AddMqttWebSocketService();
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.UseMqttWebSocket("/mqtt");
}));
await service.StartAsync();
Console.WriteLine("WebSocket MQTT 服务端已启动: ws://127.0.0.1:1883/mqtt");
这段代码里有两个必须记住的动作:
容器中注册 AddMqttWebSocketService()
插件中启用 UseMqttWebSocket("/mqtt")
少任何一个,WebSocket MQTT 都起不来。
var client = new MqttWebSocketClient();
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("ws://127.0.0.1:1883/mqtt")
.SetMqttConnectOptions(options =>
{
options.ClientId = Guid.NewGuid().ToString("N");
options.UserName = "admin";
options.Password = "123456";
options.ProtocolName = "Mqtt";
options.Version = MqttProtocolVersion.V500;
options.KeepAlive = 60;
options.CleanSession = true;
}));
await client.ConnectAsync();
如果你后续要和浏览器、H5、大屏或者前端控制台集成,这个模式会非常自然。
基于 WebSocket 模式时,HttpService 本身不是 IMqttService,所以如果你想主动广播消息,需要先从容器里解析出 MQTT 服务实例。
示例代码已经给出了标准写法:
var mqttService = service.Resolver.Resolve<IMqttWebSocketService>();
var mqttBroker = mqttService.MqttBroker;
拿到 mqttBroker 后,就可以像 TCP 服务端那样主动广播消息。
示例代码里还有一个很值得单独拿出来讲的点,就是重连插件。
TCP 客户端示例:
config.ConfigurePlugins(a =>
{
a.UseReconnection<MqttTcpClient>(options =>
{
options.PollingInterval = TimeSpan.FromSeconds(5);
options.UseMqttCheckAction(
activeTimeSpan: TimeSpan.FromSeconds(10),
pingInterval: TimeSpan.FromSeconds(3));
});
});
WebSocket 客户端示例:
config.ConfigurePlugins(a =>
{
a.UseReconnection<MqttWebSocketClient>(options =>
{
options.PollingInterval = TimeSpan.FromSeconds(10);
options.UseMqttCheckAction();
});
});
这个能力为什么重要?
因为在示例源码里已经明确写了一个很容易被忽略的事实:
KeepAlive 只是把数值传给服务端,客户端本身不会仅凭这个字段自动发送心跳包。
也就是说,如果你希望客户端真正具备“断线重连 + 心跳保活”的能力,就应该把重连插件启起来,而不是只设置一个 KeepAlive = 60 然后安心睡觉。
这是一条非常实用的经验。
除了收消息插件,连接生命周期也可以通过插件来感知。
例如:
internal class MqttConnectionPlugin : PluginBase, IMqttConnectingPlugin, IMqttConnectedPlugin
{
public async Task OnMqttConnecting(IMqttSession client, MqttConnectingEventArgs e)
{
Console.WriteLine("Client Connecting");
await e.InvokeNext();
}
public async Task OnMqttConnected(IMqttSession client, MqttConnectedEventArgs e)
{
Console.WriteLine("Client Connected");
await e.InvokeNext();
}
}
如果你要做下面这些功能,这类插件就非常适合:
登录日志
连接审计
统计在线时长
限制重复连接
自定义认证前后的埋点
当前文档和源码中可以看到的常用插件接口包括:
IMqttConnectingPlugin
IMqttConnectedPlugin
IMqttClosingPlugin
IMqttClosedPlugin
IMqttReceivingPlugin
IMqttReceivedPlugin
如果你想进一步发挥 MQTT 5.0 的能力,MqttConnectOptions 里这些字段值得优先关注:
UserProperties 用来带业务元数据
AuthenticationMethod 自定义认证方式名
AuthenticationData 认证附加数据
MaximumPacketSize 限制包大小
ReceiveMaximum 限制并发接收 QoS 1/2 消息数量
SessionExpiryInterval 会话过期时间
TopicAliasMaximum 主题别名能力
如果你的系统走的是更严格的设备认证、复杂的会话控制、或者高并发设备连接,这些配置会逐渐变得重要。
如果你准备真正上手,而不是只看文章,最推荐从仓库里的这 3 个 demo 开始:
适合先理解最基础的 TCP MQTT 用法:
启动 MqttTcpService
创建 MqttTcpClient
订阅主题
发布消息
配置插件
配置断线重连
适合理解更贴近业务开发的主题路由模型:
UseMqttTopicHandler
精确主题订阅
+ 单级通配符
# 多级通配符
多主题订阅
如果你以后要做“设备消息分发到不同业务处理器”,这个 demo 很值得多看几遍。
适合理解 WebSocket MQTT:
HttpService
AddMqttWebSocketService
UseMqttWebSocket("/mqtt")
MqttWebSocketClient
从容器解析 IMqttWebSocketService
如果你准备让浏览器直接接 MQTT,这个 demo 基本就是入口样板。
最后,把几个容易忽略但真的会影响开发体验的问题单独列出来。
这是最值得提醒的一条。
只设置:
options.KeepAlive = 60;
并不意味着客户端一定会自动发心跳保活。想要比较完整的保活与断线重连行为,请结合 UseReconnection(...).UseMqttCheckAction(...)。
WebSocket 版必须同时具备:
HttpService
AddMqttWebSocketService()
UseMqttWebSocket("/mqtt")
少一环都不对。
如果你的主题一开始就不少,建议不要等到几十个主题后再重构,直接上 UseMqttTopicHandler 会省很多事。
如果你只想处理业务消息,就优先用 IMqttReceivedPlugin。
如果你要做协议监控、审计、调试,再去用 IMqttReceivingPlugin。
插件职责一混,后面很容易越写越乱。
如果你打算把 TouchSocket.Mqtt 真正用于项目,我建议一开始就按下面这个分层来组织:
MqttTcpService 或 HttpService + WebSocket MQTT
插件做日志、认证、审计
MqttBroker 做主动广播
MqttTcpClient 或 MqttWebSocketClient
SetMqttConnectOptions 配身份与版本
UseReconnection 配保活与重连
统一约定主题命名
用 UseMqttTopicHandler 管理主题与处理器映射
尽量把消息反序列化、校验、落库封装进独立处理器
这样后面不管你是接 10 台设备,还是接 10000 台设备,代码结构都不至于太散。
如果只用一句话来评价 TouchSocket.Mqtt,我的看法是:
它不是那种只让你“连上就算完事”的 MQTT 包,而是一套比较完整、并且很适合继续往业务里延展的组件方案。
你可以先从最基础的 MqttTcpService + MqttTcpClient 跑起来,再逐步上:
插件化收消息
服务端主动广播
断线重连
UseMqttTopicHandler
WebSocket MQTT
MQTT 5.0 高级配置
这样学习路径是顺的,项目代码也会更稳。
如果你准备马上动手,建议从这条路线开始:
先跑 MqttConsoleApp
再跑 MqttTopicHandlerConsoleApp
最后看 MqttWebSocketConsoleApp
等你把这三个 demo 跑明白,TouchSocket.Mqtt 的核心用法基本就真的进脑子了。