ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity MQTT服务层封装指南:从连接混乱到模块化设计

Unity MQTT服务层封装指南:从连接混乱到模块化设计 在Unity项目里接入MQTT一开始图省事直接在某个MonoBehaviour里new一个MqttClient连接、订阅、消息处理全堆在同一个脚本里。等业务功能多起来你就知道什么叫灾难连接状态没人统一管多个面板各连各的断线重连逻辑散落在三个脚本里回调里直接操作UI还时不时报线程错误。后来我花了两天时间把这块彻底重构按照“配置、连接、订阅发布”三个模块拆出了一个独立服务层代码变得非常清爽。这篇就把设计思路和可落地的实现方案完整写出来希望对准备用Best MQTT v3但又不想把项目搞乱的人有帮助。1. 为什么要把MQTT封装成服务层而不是直接在MonoBehaviour里写先聊聊动机。很多人第一次用Best MQTT v3就是拖一个Demo场景把Broker地址一填跑通了就完事。可一旦你把它往正式项目里放尤其是数字孪生、设备远程控制、多人联动业务这类场景直接调用库和封装服务层的差别会立刻显现出来。1.1 不封装的典型乱象最常见的写法是这个套路某个Panel的脚本里持有MqttClient在Start方法里Connect在OnMessage方法里解析消息直接更新面板上的文本。第二个面板也需要连接就复制一份连接代码第三个面板需要订阅另一个主题又复制一份。于是产生几个问题。连接实例满天飞每个脚本都有自己的MqttClientBroker连接数被无意义拉满而且同一个客户端断线后还能不能复用一个实例根本没人关注。消息处理逻辑与UI强耦合协议一旦变化要改的脚本可能是五六个文件。更隐蔽的是生命周期问题MonoBehaviour销毁时连接没有释放后台线程还在跑设备交互界面一关程序里就多了一个幽灵连接。这些问题的根源都一样MQTT的完整生命周期连接、重连、订阅、发布、消息分发没有被集中管理。服务层就是解决这个问题的标准做法它把通信细节收敛到一个类里其他业务脚本只面向服务层提供的接口不直接碰库API。1.2 服务层管什么不管什么服务层不是把所有代码都塞进一个类里而是划分边界。我的划分原则是连接状态、发布订阅、消息分发是服务层的事业务逻辑怎么处理消息、UI怎么展示数据是业务层的事。具体来说服务层要负责这些加载并校验连接配置Broker地址、端口、ClientId、账号密码、心跳间隔等建立连接、断线自动重连、主动断开维护主题订阅注册表统一管理所有订阅关系提供Publish、Subscribe、Unsubscribe的简单方法把收到的消息去重、序列化解析并派发给对应业务模块用事件或回调把连接状态变化通知出去不该服务层管的事情消息的具体业务含义、UI刷新、数据存储。业务层拿到的是已经解析好的消息对象而不是原始字节流这样订阅方代码会非常干净。1.3 三大模块划分总览整个服务层我分成三个模块对应三条核心链路。配置模块负责读取和校验MQTT连接参数同时管理订阅主题前缀等约定。连接模块负责连接生命周期对外暴露Connected、Disconnected、Reconnecting等事件。订阅发布模块负责把所有业务方的Subscribe/Publish请求统一转化成底层库调用并处理消息回调分发。三个模块之间单向依赖配置模块不依赖其他模块连接模块依赖配置模块订阅发布模块依赖连接模块因为只有在连接可用时才能执行发送和订阅。这样一来修改配置不会影响连接逻辑更换Broker地址只需要改配置文件业务层完全不感知。2. 配置模块把连接信息从代码里拆出去配置模块是整个服务层的地基。很多人觉得配置不就是几个字段嘛写死在代码里最简单。问题是你迟早要面对开发环境、测试环境、生产环境三种Broker地址或者客户现场要改IP。到那时候再翻代码找硬编码字符串效率就太低了。2.1 先梳理清楚需要哪些配置项我整理了一份实际项目里常用的配置字段按用途分成了三组。连接参数BrokerHost、BrokerPort、ClientId、Username、Password、UseTLS、KeepAliveInterval、ConnectTimeout。这些是MQTT建立连接最基础的参数。行为参数AutoReconnect、ReconnectInterval、MaxReconnectAttempts如果重连次数超过阈值要告警。这些决定了连接断了之后整个服务层怎么表现。主题参数TopicPrefix所有订阅主题统一加上前缀、WillTopic、WillMessage遗嘱消息设备掉线时让Broker广播出去。如果你的项目里有多套设备类型这个前缀设计很有用。配置项不是越多越好多了会增加使用者的理解成本。我一般只留上面这些其他库特有参数用默认值不暴露给配置层。2.2 选ScriptableObject还是JSON在Unity里做配置通常有两个选择ScriptableObject和JSON文件。两种我都用过说说实际感受。ScriptableObject的好处是可视化在Inspector里直接填字段还能做下拉选项和枚举校验适合项目经理或者策划去改配置。坏处是Unity工程里改配置会生成.asset文件多人协作时容易产生冲突。JSON的好处是纯文本可以在运行时从服务器拉取也可以在构建后放到StreamingAssets里修改不需要进Unity编辑器。坏处是没有可视化编辑得自己写解析和校验逻辑。我的建议是纯客户端项目用ScriptableObject代码简单、类型安全需要远程更新配置的项目用JSON。如果是“服务层”这个设计目标我更推荐ScriptableObject因为配置本身就是开发期的东西不是运行期热更的数据。2.3 可用的配置类和校验逻辑下面是我常用的配置类代码。注意我没有用UnityEngine的序列化特性依赖太重直接用了System.Serializable方便和JSON互转。using System; using UnityEngine; [Serializable] public class MqttConnectionConfig { [Header(连接参数)] public string brokerHost 127.0.0.1; public int brokerPort 1883; public string clientId UnityClient; public string username; public string password; public bool useTls; public int keepAliveInterval 60; public int connectTimeout 5; [Header(重连参数)] public bool autoReconnect true; public int reconnectInterval 3; public int maxReconnectAttempts 5; [Header(主题参数)] public string topicPrefix ; public string willTopic ; public string willMessage ; }校验逻辑可以放在服务层加载配置时统一进行。主要检查几个点brokerHost不能为空否则直接报错port范围在1-65535之间clientId不能为空如果为空则自动生成一个GUID前缀字符串keepAliveInterval和connectTimeout必须大于0如果启用了TLS要检查证书相关配置是否完整这些校验看起来琐碎但在现场部署时能省掉很多排查时间。我之前遇到过一次连不上Broker查了半天发现是端口被写成了18830这种手滑错误有校验的话启动阶段就能发现。2.4 多环境配置切换的技巧如果项目要跑多个环境我建议在配置文件里加一个环境枚举然后用条件编译或者运行时选择来加载不同配置。public enum MqttEnvironment { Dev, Test, Production }在ScriptableObject里留一个Environment字段服务层启动时根据这个字段加载对应的配置资产。如果用的是JSON方案就直接维护三个JSON文件运行时按名字加载。这个成本很低但能让部署阶段不用改代码。3. 连接模块管好生命周期才能不崩不退连接模块是整个服务层最需要谨慎的部分因为MQTT连接是长连接状态变化多而且Unity的MonoBehaviour生命周期和网络连接生命周期交织在一起很容易出幺蛾子。3.1 连接状态机从Disconnected到Connected怎么走我习惯把连接模块的内部状态定义成一个枚举整个状态迁移是单向走下去的。public enum MqttConnectionState { Disconnected, Connecting, Connected, Reconnecting, Disconnecting }连接流程是这样的调用Connect方法后进入Connecting成功后进入Connected如果连接过程中网络错误进入Reconnecting前提是开启了AutoReconnect重连成功回到Connected失败则保持Reconnecting并进入下一次重连计时主动调用Disconnect则进入Disconnecting完成后回到Disconnected。用状态机的最大好处是其他模块可以随时查询当前连接状态UI也能根据状态显示“连接中/已连接/重连中”。避免出现连接已经断了界面上还显示在线的情况。3.2 连接和断开要写成异步安全的接口Best MQTT v3底层是基于异步socket的所以连接和断开都是异步操作。在服务层里我建议把所有异步逻辑用一个轻量级抽象包起来对外提供统一的异步接口。如果你用的是C#的async/await可以直接这样设计public async Taskbool ConnectAsync() { if (currentState MqttConnectionState.Connected || currentState MqttConnectionState.Connecting) { return true; } SetState(MqttConnectionState.Connecting); try { var result await mqttClient.ConnectAsync(); if (result) { SetState(MqttConnectionState.Connected); return true; } else { SetState(MqttConnectionState.Disconnected); return false; } } catch (Exception e) { Debug.LogError($[MqttService] 连接异常: {e.Message}); SetState(MqttConnectionState.Disconnected); return false; } }这里有个很重要的点不要在回调里直接改UIUnity的UI操作必须回到主线程。Best MQTT的底层回调可能来自socket线程直接操作UnityObject会抛异常。我的做法是让连接状态事件统一通过一个主线程调度器派发比如用Unity的SynchronizationContext或者直接丢给一个每帧Update处理的队列。3.3 心跳、重连、超时参数怎么调很多人忽略心跳参数但它在实际项目里决定了掉线能否被及时感知。KeepAliveInterval表示客户端在这段时间内向Broker发送一次PINGREQ报文如果Broker在1.5倍时间内没收到任何报文就会判定客户端掉线。在局域网环境KeepAlive设60秒问题不大但在公网或移动网络环境网络抖动可能让连接在空闲时被路由器踢掉建议设短一点比如20到30秒。重连间隔也不是越短越好设成1秒会在弱网环境下产生大量无效请求反而加重手机功耗。我一般设3秒起步每次重连失败后按指数退避最大不超过30秒这样服务端压力可控用户也能感知到系统在努力恢复连接。private int nextReconnectInterval; private void ScheduleReconnect() { nextReconnectInterval Mathf.Min(nextReconnectInterval * 2, 30); }每次重连成功后把nextReconnectInterval重置为初始值。3.4 Unity生命周期和连接模块的配合服务层最好不要继承MonoBehaviour因为纯C#类更容易做单元测试和生命周期管理。但Unity的协程、InvokeRepeating这些功能用不上所以重连计时器我一般用UniTask的UniTask.Delay或者自己写一个简单的定时器管理器。如果你的项目不想引入UniTask可以考虑让服务层持有MonoBehaviour的引用用协程来执行延迟重连。我的建议是尽量让服务层保持纯C#用一个主线程调度器来执行所有延迟回调这样服务层可以被独立测试也方便将来移植到服务器端跑仿真逻辑。4. 订阅发布模块用主题路由把业务解耦连接建立起来之后真正的业务通信就靠订阅发布模块了。这个模块看起来只是包装一下库的Subscribe和Publish方法但要做得好用还得考虑主题管理、消息分发方式、QoS选择这些实际问题。4.1 主题命名设计从第一天就别乱起名MQTT的主题是层级结构的用斜杠分隔比如factory/line1/machine3/status。主题设计直接影响后期维护成本和Broker的权限管理所以我建议在项目开始就定义一套主题规范。我常用的规范是第一段是业务域第二段是设备或房间标识第三段是消息类型。总长度控制在三到四段太深的层级会让通配符订阅变得赘余太浅又容易主题冲突。如果在配置模块里设置了TopicPrefix那所有实际订阅的主题都会自动加上前缀后缀。这样同一个App连到不同Broker时可以只改配置不用改代码。4.2 订阅注册表统一管理哪些主题被订阅了一个项目里可能有多个业务脚本各自订阅主题。如果不做统一管理断线重连之后就麻烦了服务层重连成功但之前订阅过的主题全部失效业务脚本还得自己重新订阅一遍。这个体验非常差。所以我设计了一个订阅注册表让所有业务脚本通过服务层的Subscribe方法注册主题服务层内部记录这些订阅关系。当连接成功时自动把所有已注册的主题重新订阅一遍当业务脚本调用Unsubscribe时再从注册表里移除对应项。private Dictionarystring, MqttSubscription subscriptionRegistry new Dictionarystring, MqttSubscription(); public void Subscribe(string topic, ActionMqttMessage handler, byte qos 0) { subscriptionRegistry[topic] new MqttSubscription(topic, handler, qos); if (currentState MqttConnectionState.Connected) { mqttClient.Subscribe(topic, qos); } }这个设计的好处是业务脚本只需要关心自己的业务逻辑不用关心连接重连细节。服务层会在合适的时候自动恢复订阅。4.3 消息分发事件、UnityEvent还是消息中心收到消息后怎么分发给业务脚本我试过几种方案最终选择用C#事件为主配合一个轻量级的消息封装。Best MQTT v3的原始回调会把消息以字节数组或字符串形式返回。如果直接让业务脚本去解析那每个订阅方都要知道消息的序列化规则耦合又上来了。我定义了一个统一的MqttMessage类包含Topic、PayLoad、QoS、Retain字段订阅方拿到的就是这个对象自己再去反序列化成对应的业务数据。分发方式上最纯粹的是C#的event订阅方用注册用-注销。这样性能好类型安全。如果项目里已经用了UnityEvent或者一个全局消息中心也可以把MqttService封装成消息源但我不建议把MQTT服务层和UI用的消息中心强耦合因为服务层理论上不关心消息是给UI还是给逻辑层。4.4 QoS怎么选才合理QoS是MQTT里最容易让人迷惑的概念。简单说QoS 0最多发一次可能丢QoS 1至少发一次可能重复QoS 2恰好发一次性能开销最大。在实时控制类业务里命令下发建议用QoS 1这样可以容忍重复但不能容忍丢失。状态上报类业务比如传感器数据如果数据本身带有时间戳用QoS 0就够了丢一条也没事下一条很快会来。业务层要有幂等处理能力因为即使是QoS 1也可能收到重复消息。5. 实操实现一个最小可落地的MqttService理论讲完了下面给一套完整的、可以直接复制的精简实现。这套代码基于Best MQTT v3的库API但底层类名在不同版本里可能略有差异你以自己项目引用的版本为准核心逻辑是一致的。5.1 文件组织我建议把服务层放到独立的目录避免和场景里的MonoBehaviour混在一起。Assets/Scripts/MqttService/ MqttService.cs // 服务层入口 MqttConnectionConfig.cs // 配置类 MqttConnectionState.cs // 状态枚举 MqttMessage.cs // 消息封装 IMqttService.cs // 服务层接口接口的好处是可以做依赖注入或者单元测试如果项目复杂度不高也可以先不要接口直接用静态类或单例。5.2 服务层接口我先把对外公开的接口定下来这样后续实现不会跑偏。public interface IMqttService { MqttConnectionState CurrentState { get; } event ActionMqttConnectionState OnStateChanged; event ActionMqttMessage OnMessageReceived; Taskbool ConnectAsync(); Task DisconnectAsync(); void Subscribe(string topic, ActionMqttMessage handler, byte qos 0); void Unsubscribe(string topic); Taskbool PublishAsync(string topic, string payload, byte qos 0, bool retain false); Taskbool PublishAsync(string topic, byte[] payload, byte qos 0, bool retain false); }这个接口覆盖了三个模块连接方法、订阅发布方法、状态事件。业务脚本只需要依赖这个接口不需要关心底层是怎么连接的。5.3 核心实现连接与重连核心实现的骨架是连接管理部分。因为代码量比较大我只展示最关键的部分。public class MqttService : IMqttService { private MqttConnectionConfig config; private MqttClient mqttClient; private MqttConnectionState currentState; private int reconnectAttempts; private bool isDisposing; public MqttConnectionState CurrentState currentState; public event ActionMqttConnectionState OnStateChanged; public event ActionMqttMessage OnMessageReceived; public MqttService(MqttConnectionConfig config) { this.config config; this.currentState MqttConnectionState.Disconnected; } private void SetState(MqttConnectionState newState) { currentState newState; OnStateChanged?.Invoke(newState); } public async Taskbool ConnectAsync() { if (currentState MqttConnectionState.Connected || currentState MqttConnectionState.Connecting) { return true; } SetState(MqttConnectionState.Connecting); reconnectAttempts 0; try { mqttClient new MqttClient( config.brokerHost, config.brokerPort, config.useTls, null, null, 0); mqttClient.CustomConnectMethod null; var connected await mqttClient.ConnectAsync(); if (connected) { reconnectAttempts 0; SetState(MqttConnectionState.Connected); RestoreSubscriptions(); return true; } else { SetState(MqttConnectionState.Disconnected); ScheduleReconnect(); return false; } } catch (Exception e) { Debug.LogError($[MqttService] ConnectAsync failed: {e.Message}); SetState(MqttConnectionState.Disconnected); ScheduleReconnect(); return false; } } private void ScheduleReconnect() { if (!config.autoReconnect || isDisposing) return; if (reconnectAttempts config.maxReconnectAttempts) return; reconnectAttempts; var delay config.reconnectInterval; MainThreadDispatcher.Delay(() ConnectAsync(), delay); } }这里要注意Best MQTT的MqttClient构造参数在不同版本里有区别比如TLS证书参数、WebSocket配置等。我提供的是常见形式实际接入时对应查一下库的API文档。5.4 订阅恢复与消息回调订阅恢复是重连后最重要的动作。我的实现里维护了一个订阅字典重连成功后遍历一遍重新调用库的Subscribe。private Dictionarystring, ActionMqttMessage subscribers new Dictionarystring, ActionMqttMessage(); private void RestoreSubscriptions() { foreach (var kv in subscribers) { mqttClient.Subscribe(kv.Key, 0); } } public void Subscribe(string topic, ActionMqttMessage handler, byte qos 0) { subscribers[topic] handler; if (currentState MqttConnectionState.Connected) { mqttClient.Subscribe(topic, qos); } } public void Unsubscribe(string topic) { subscribers.Remove(topic); if (currentState MqttConnectionState.Connected) { mqttClient.Unsubscribe(topic); } }消息回调那边Best MQTT会提供一个OnMessage或OnPublish事件。我在事件里解析出Topic和Payload包装成MqttMessage然后通过主线程调度器回到主线程再回调业务方。private void OnMqttMessageReceived(string topic, byte[] payload, byte qos, bool retain) { var message new MqttMessage { Topic topic, Payload payload, Qos qos, Retain retain }; MainThreadDispatcher.ExecuteOnMainThread(() { OnMessageReceived?.Invoke(message); if (subscribers.TryGetValue(topic, out var handler)) { handler?.Invoke(message); } else { // 处理通配符订阅的匹配逻辑 TryDispatchWithWildcard(topic, message); } }); }注意通配符匹配不能简单地用Dictionary查找。如果业务方订阅了factory//status那实际收到的factory/line1/status并不能从字典直接命中。我在实现里加了一个通配符匹配方法把每个订阅主题先按层级拆开再逐层匹配。这个逻辑不难但性能敏感时要注意缓存匹配结果。5.5 主线程调度器上面代码里出现了MainThreadDispatcher这个类在Unity里可以这样实现最简单的方案就是在每个MonoBehaviour的Update里驱动一个并发队列。public class MainThreadDispatcher : MonoBehaviour { private static MainThreadDispatcher instance; private static ConcurrentQueueAction actions new ConcurrentQueueAction(); public static void ExecuteOnMainThread(Action action) { actions.Enqueue(action); } private void Update() { while (actions.TryDequeue(out var action)) { action?.Invoke(); } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void Initialize() { if (instance null) { var go new GameObject(MainThreadDispatcher); instance go.AddComponentMainThreadDispatcher(); DontDestroyOnLoad(go); } } }这个方案足够简单也够用。如果你项目里已经有UniTask或者Reactive也可以用它们行替代但核心思想是一致的网络回调线程产出的逻辑必须排队回到主线程执行。5.6 如何注入到业务脚本不搞复杂的依赖注入框架最简单的方式是做一个静态入口或者把MqttService实例挂到GameObject上。public class MqttServiceBootstrap : MonoBehaviour { [SerializeField] private MqttConnectionConfig config; private MqttService mqttService; private async void Start() { mqttService new MqttService(config); await mqttService.ConnectAsync(); } public IMqttService Service mqttService; }其他业务脚本通过查找Bootstrap拿到服务层引用就能调用Subscribe、Publish等方法。如果想更进一步可以在启动时把服务层实例注册到一个简单的服务定位器里运行时代码里直接MqttServiceLocator.Get()这样业务脚本不需要到处拖引用。6. 常见问题与排查技巧实录最后这部分是干活多年的心得都是踩过坑之后沉淀下来的。我按实际频率从高到低列一下希望能帮你少走弯路。6.1 问题速查表现象可能原因处理办法连接一直失败日志提示超时Broker地址填错或者端口不对先用MQTT客户端工具手动连一下确认地址端口可用连接成功但收不到消息订阅主题和发布主题不一致或者权限不足检查主题是否带前缀检查Broker的ACL配置收到消息重复执行QoS 1导致重复投递业务侧做幂等处理比如加消息ID去重断线后重连成功但订阅失效没有恢复订阅注册表重连成功后调用RestoreSubscriptionsUI操作报“Thread access”错误在socket回调线程直接更新UI所有回调统一走主线程调度器频繁掉线日志有Ping timeout心跳间隔太大或网络不稳定把KeepAliveInterval调小检查网络环境发布消息时提示“not connected”连接状态判断不准确发布前统一检查CurrentState并做失败重发布策略旧订阅没有取消重复注册业务脚本卸载时没有调用Unsubscribe在OnDestroy里统一注销订阅6.2 线程问题不要在回调里直接动Unity对象这是Unity MQTT最容易踩的坑。Best MQTT底层事件可能在socket线程上触发如果你在事件回调里直接修改Text组件或者Instantiate一个GameObjectUnity会非常不客气地抛异常。我的建议是所有从MQTT回调进入业务层的路径都在主线程调度器排队执行。宁可多一两帧延迟也不要冒线程错误的风险。尤其在做设备控制类项目时用户看到延迟一两帧根本无感但闪退或者UI卡死感应很强烈。6.3 内存泄漏和事件未注销服务层存在一个很大的隐患事件累积。如果业务脚本在OnEnable里订阅服务层的事件但OnDisable时忘了注销那么即使场景切走了服务层持有的委托引用还会让这个业务脚本无法被GC回收造成内存持续增长。我建议在业务脚本里用一种安全的注册模式。用一个字段持有处理函数在OnEnable里在OnDisable里-。另外服务层内部也要在Dispose时清空所有订阅注册表断开连接释放MqttClient避免程序退出时线程还在后台跑。6.4 调试MQTT工具链推荐写服务层的时候强烈建议准备一个MQTT调试工具箱我平时用这几个本机或局域网环境调试我会起一个本地的Mosquitto Broker再用MQTTX或者MQTT Explorer这两个图形化客户端查看主题、发布消息。MQTT Explorer特别适合查看消息实时流动因为它按主题树展示。线上环境测试可以用CloudMQTT或者公共测试Broker但生产环境不要用公共Broker数据安全没法保证。调试自己的服务层时还有一个技巧是在服务层里加日志开关输出连接状态变化、订阅/发布请求、收到的消息内容。这样线上排查问题不用打断点看日志就能判断是服务层的问题还是业务逻辑的问题。6.5 报文抓包如果遇到底层库行为无法解释就抓包。Broker如果跑在Linux上直接用tcpdump抓1883端口如果跑在Windows上可以用Wireshark。MQTT是明文协议抓包可以直接看到CONNECT、SUBSCRIBE、PUBLISH报文内容。我能用抓包确认客户端是否发出了PINGREQ、订阅请求的Topic是否带上了莫名前缀、遗嘱消息是否生效。之前有个环境一直连不上服务端也查不到连接请求抓包发现端口变成1883之外的某端口——因为我配置模块里有个字段类型写错了int被默认成0最后连接被发到了端口0。这种问题不看报文很难定位。6.6 服务层扩展方向这套服务层设计好之后扩展能力就体现出来了。我后来在它的基础上加了几个功能成本都很低。消息序列化辅助类把JSON序列化和反序列化收敛到统一入口业务方传对象不用关心字符串。消息过滤管道在分发前对消息做统一处理比如记录日志、统计消息量、过滤没有业务意义的系统性消息。订阅关系持久化把业务方的订阅记录保存到本地下次启动时自动恢复。如果项目需要还可以给服务层加一套模拟模式在没有真实Broker的环境下用本地内存队列模拟发布订阅方便在开发机上跑UI联调。这些扩展都不是推翻重来而是在服务层内部加插件或者回调。这就是一开始花时间把结构拆清楚的最大回报。我自己实际项目里用这套方案跑了两个版本从最开始在家电控制App里用到后来在数字孪生产品里对接设备数据流中间几乎没有出现因为MQTT连接引发的大事故。最直观的感受是团队成员不再需要理解MQTT协议细节他们只面对几个简单的方法出问题看服务层的日志就行。如果你现在正在被一个越改越乱的MQTT模块折磨我建议你按这个思路试着重构一次尤其是在业务还在早期的时候拆这个服务层的成本比后期低太多了。
返回列表