news 2026/10/19 15:16:43

基于QMQTT的Qt客户端MQTT通信实战:订阅发布与Broker集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于QMQTT的Qt客户端MQTT通信实战:订阅发布与Broker集成

做 C++/Qt 客户端开发的人,迟早会碰到一个需求:让程序和远端服务保持通信,实时收发数据。最常见的落地方案,就是把 MQTT 协议接进 Qt 工程里,实现客户端订阅和发布。这个方案在嵌入式设备管理、桌面工具、工业上位机里都特别常见,而且一旦基础设施搭好,后续扩展几乎是无痛的。这篇文章不会讲太多空泛的协议理论,就按我从零开始接一个基于 QMQTT 的客户端库、跑通订阅发布、再被各种奇葩问题折磨之后总结出来的套路来写。适合正在用 Qt 写客户端、想快速加一套消息实时通道的人,也适合刚接触 MQTT 但不太清楚怎么落地的朋友。

1. 为什么是 MQTT + Qt,而不是其他方案

1.1 先想清楚通信问题:轮询和长连接的区别

很多新手一开始会问:我们的需求不就是客户端和服务器通信吗,用 HTTP 轮询不行吗?行,但是效率差很多。MQTT 是长连接协议,连接建立之后,服务器能主动把消息推给客户端,不需要客户端反复地问。这个“推送”的机制对实时性很关键。

轮询的方案是客户端定时问服务器“有没有新消息”,单位时间能问多少次,实时性就有多大的天花板。设备多的时候,几十上百个终端同时轮询,数据量一上来,带宽和服务器资源都在空耗。而发布订阅模式最大的优势,是消息一到 Broker 就立刻被转发给所有订阅者,中间没有请求-响应的来回,也没有重复无效的数据。

MQTT 属于经典的发布/订阅模式,发布者和订阅者都是客户端,彼此之间不需要知道对方是谁。中间人叫 Broker,也就是消息代理。客户端 A 往主题sensor/temp发布一条消息,所有订阅了这个主题的客户端都会收到。Broker 的作用就像快递中转站,消息一到,立刻分发给订阅者。这个模型做多端联动非常简单,不用关心对端在哪里、是否在线,符合物联网和桌面工具里非常典型的异步协作场景。

1.2 Qt 做客户端为什么顺手

Qt 本身是跨平台 UI 框架,但网络模块 QtNetwork 也非常成熟。基于 Qt 封装的 MQTT 客户端库,把协议最底层的数据包编码、解码、确认、心跳机制都封装好了,暴露出来的基本都是信号槽接口。比如连接成功、订阅成功、收到消息,都是一个个信号。这意味着我们甚至不必反复去抠 MQTT 包里每个字节的格式,就能把功能跑起来。

更重要的是,Qt 的信号槽机制在跨线程、事件循环里处理得很好。收到的消息可以直接触发界面刷新,也可以派发到工作线程做耗时处理。相比裸 TCP 或者直接使用 C SDK,QMQTT 的接入成本低很多。我见过不少工程一开始用纯 C 的客户端库,后来为了消息分发、线程安全和 UI 联动,越写越复杂,最后还是回到 Qt 封装这条路上来。

1.3 客户端库怎么选:对比几条可行路径

这里说三条主流路径,方便你按自己情况选:

第一条,是使用官方 Qt MQTT 模块。这个模块需要自己根据 Qt 版本拉取源码构建,并且和你的 Qt 版本、编译器匹配,授权上还要留意 GPL 和商业授权条款的差异,适合愿意折腾的人。

第二条,是使用第三方库 QMQTT。它底层基于 QtNetwork,API 完全是 Qt 风格,源码集成非常方便,实现了 MQTT 3.1.1 的常用功能,比如订阅、发布、遗嘱、认证、QoS 1/2,对大多数项目完全够用。这是我最推荐的方式。

第三条,是自己实现协议层。这就不太建议了。MQTT 数据包虽然不复杂,但可变长度编码、消息 ID、QoS 状态机、重发机制写起来非常容易出 bug,而且调试周期很长,不划算。

所以接下来的内容都围绕 QMQTT 这套方案展开。QMQTT 在社区里已经是很通用的叫法,你在工程里用到的头文件是qmqtt/client.h,命名空间是QMQTT,核心类是Client。

2. 环境准备:依赖、工程配置和本地 Broker

2.1 准备 Qt 开发环境和编译器

我这边用的开发环境是 Qt 5.15 及以上的长期支持版本,理论上 Qt 6 也可以用,API 差别不大。编译器方面,Windows 上使用 MinGW 或 MSVC 都可以,但这里有一个比较容易踩的坑:如果你的 QMQTT 库是预先编译成 DLL 的,那么 DLL 的编译器和 Qt 版本必须和你的工程完全一致,否则运行时会直接崩溃,或者链接阶段报各种莫名其妙的错误。

最省事的方法是直接以源码方式集成,把 QMQTT 的源码放到工程目录下,和工程一起编译。这样做可以避免很多 ABI 兼容性的坑。如果你一定要使用预编译库,记得把 Debug 和 Release 版本分开,命名上用qmqttd和qmqtt区分,不然很容易在 Debug 下链接到 Release 库,然后出现各种看着像“内存被踩坏”的诡异崩溃。

2.2 获取和集成 QMQTT 源码

从代码托管平台拉一份 QMQTT 源码下来,目录结构大概是这样的:

mqtt-demo/ qmqtt/ include/qmqtt/ 头文件目录 src/ 源码目录 qmqtt.pri 工程包含文件 mqtt-demo.pro 你的工程文件

集成方式很简单,在工程文件.pro里加一行include就能把它链进你的工程:

QT += core network CONFIG += c++11 TARGET = mqtt_demo TEMPLATE = app include(./qmqtt/qmqtt.pri)

如果你的工程还要做界面,记得加上QT += gui widgets。看情况决定是否引入C++17,QMQTT 本身的代码风格比较保守,C++11 完全够用。

2.3 准备一个可用的 MQTT Broker

MQTT 客户端必须连接一个 Broker,本地测试当然也要有一个 Broker 在运行。常见的开源 Broker 装好后默认监听 1883 端口,同时自带命令行工具,可以拿来发布消息和订阅消息,方便验证。装好之后,先启动 Broker,再用工具确认 1883 端口能被访问。

这一步如果嫌麻烦,也可以先用公共测试 Broker,但因为设备内网、数据安全和调试体验的考虑,我还是建议本地搭一个。启动之后用系统自带工具看一眼端口,比如在命令行里执行类似telnet 127.0.0.1 1883的探测,能通说明 Broker 已经就绪。后面代码里默认连接的地址就是127.0.0.1,端口1883。

3. 核心功能落地:订阅、发布与消息处理的完整代码

3.1 建立连接:先设置参数再连接

回归到代码本身,第一个任务是创建客户端对象。QMQTT 的基本套路是先设置地址、端口、客户端 ID 等信息,再调用连接方法。客户端 ID 在同一个 Broker 上应该尽量唯一,如果两个客户端用同一个 ID 连接同一个 Broker,后连接的一方会把先连接的一方踢下线。这个现象在排查时特别常见,很多人以为是程序不稳定,查了半天才发现是“撞 ID”了。

QMQTT::Client client; client.setHost(QStringLiteral("127.0.0.1")); client.setPort(1883); client.setClientId(QStringLiteral("qt-demo-01")); client.setUsername(QStringLiteral("mqtt_user")); client.setPassword(QStringLiteral("secret")); client.setKeepAlive(30); client.connectToHost();

这里有一个很多人会忽略的点:QMQTT::Client 直接依赖 Qt 的事件循环。如果你用 QCoreApplication 做控制台程序,一定不要忘了 main 函数最后调用app.exec(),否则连接信号永远不会触发,程序看起来像“没反应”。实际项目中很大比例的“连不上”都出在这种地方,不是网络问题,是事件循环没有跑起来。

3.2 观察连接状态:connected 和 disconnected 信号

连接是异步的,不能一调用connectToHost()就立刻发布消息。正确的做法是监听connected信号,在连接建立成功后再进行订阅和发布。同时监听disconnected信号,这样断线时可以提示用户,也可以安排自动重连,或者把状态写进日志。

QObject::connect(&client, &QMQTT::Client::connected, [&client](){ qDebug() << "connected to broker"; client.subscribe(QStringLiteral("sensor/temp"), 1); });

还要补充一个细节:连接过程中如果不小心设置了错误的认证信息,QMQTT 会直接抛出disconnected,并且连接不会重试。这时候要看 Broker 日志确认是认证失败还是网络问题。

3.3 订阅主题:明确告诉 Broker 你想要什么

订阅的本质是告诉 Broker:“我对这个主题感兴趣,以后有消息就发给我。”QMQTT 里调用subscribe方法即可,第一个参数是主题,第二个参数是 QoS 等级。这里填 1,表示至少送达一次。

client.subscribe(QStringLiteral("sensor/temp"), 1);

订阅不一定立刻生效,Broker 有时会延迟返回确认。如果需要严谨的业务流,可以监听subscribed信号,在收到订阅确认后再继续后面的逻辑:

QObject::connect(&client, &QMQTT::Client::subscribed, [](const QString &topic, quint8 qos){ qDebug() << "subscribe ok:" << topic << qos; });

实际项目里,我一般会在connected信号里批量订阅多个主题,然后用一个列表把“需要订阅的主题”保存起来,方便断线重连时重新订阅。这样后面不需要每次改代码,只需维护一个订阅清单。

3.4 处理收到的消息:received 信号和 Message 对象

收到消息会触发received信号,参数是QMQTT::Message对象。这个对象里有主题、QoS、保留标志、消息内容等字段。尤其要注意,payload 是 QByteArray,所以除了文本数据,也可以用来传图片、文件等二进制内容。

QObject::connect(&client, &QMQTT::Client::received, [](const QMQTT::Message &message) { qDebug() << "topic:" << message.topic() << "payload:" << QString::fromUtf8(message.payload()); });

这里建议强制约定 payload 使用 UTF-8 编码。MQTT 本身对消息内容没有任何编码要求,如果发布端用的是 GBK,接收端用 UTF-8 解析就乱码了。编码问题属于业务流程约定,不是协议包带来的问题,所以团队协作时一定要统一。

3.5 发布消息:向主题写入一条数据

发布的核心就是一句话:指定主题、指定消息、指定 QoS。QMQTT 也支持 retain 标志,retain 为 true 时,如果当前没有订阅者,Broker 会保留最后一条消息,等新的订阅者上线时自动推送给它。

client.publish(QStringLiteral("cmd/device01"), QStringLiteral("{\"action\":\"reboot\"}").toUtf8(), 1);

很多业务都会把消息按 JSON 格式组织,payload 直接用 QByteArray 存 JSON 字符串即可。订阅端解析时用你熟悉的 JSON 库,不需要额外处理什么。如果你要传二进制大文件,建议拆包或换别的通道,MQTT 本身就不是为大批量文件传输设计的,硬塞反而容易把 Broker 弄出内存问题。

3.6 一个能直接编译的最小 Demo

这里是完整的最小示例,连接成功后订阅两个主题,启动 3 秒后向一个主题发布消息。直接复制到工程里就能跑通:

#include <QCoreApplication> #include <QTimer> #include <QDebug> #include "qmqtt/client.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QMQTT::Client client; client.setHost(QStringLiteral("127.0.0.1")); client.setPort(1883); client.setClientId(QStringLiteral("qt-demo-01")); client.setKeepAlive(30); QObject::connect(&client, &QMQTT::Client::connected, [&]() { qDebug() << "connected to broker"; client.subscribe(QStringLiteral("sensor/temp"), 1); client.subscribe(QStringLiteral("cmd/device01"), 1); }); QObject::connect(&client, &QMQTT::Client::received, [](const QMQTT::Message &message) { qDebug() << "message received" << "topic:" << message.topic() << "payload:" << QString::fromUtf8(message.payload()); }); QTimer::singleShot(3000, [&]() { client.publish(QStringLiteral("cmd/device01"), QStringLiteral("{\"action\":\"reboot\"}").toUtf8(), 1); }); client.connectToHost(); return app.exec(); }

运行后如果看到 connected 日志,再看到自己发布并订阅的消息回调打印出来,说明整个链路已经通了。有一个细节值得注意:在上面的 Demo 里,程序自己订阅了cmd/device01,又往这个主题发消息,所以会收到自己发布的那条消息。这在某些业务里是有意的设计,比如用来确认指令已经到达 Broker,但如果你不想收到自己的消息,主题设计上要做隔离,或者加入发送者 ID 过滤。

3.7 封装一个可复用的 MqttClient 类

Demo 验证完,下一步就是把它封装成一个可复用的类,避免每个界面里都塞一堆连接、订阅的逻辑。思路比较简单,把 QMQTT::Client 包在你的 MqttClient 里面,对外只暴露订阅、发布、消息回调这三个核心接口:

class MqttClient : public QObject { Q_OBJECT public: explicit MqttClient(QObject *parent = nullptr); void connectBroker(const QString &host, int port, const QString &clientId); void subscribe(const QString &topic, quint8 qos = 1); void publish(const QString &topic, const QByteArray &payload, quint8 qos = 1); signals: void messageReceived(const QString &topic, const QByteArray &payload); private: QMQTT::Client m_client; };

封装的核心是内部把received信号转成自己的messageReceived,同时维护一个订阅列表,在connected时重新订阅。这样界面层只需要跟着信号走,不关心 MQTT 连接状态机,业务代码干净很多。

4. 容易忽视的细节:QoS、遗嘱消息、主题设计与断线重连

4.1 QoS 等级到底怎么选

QoS(Quality of Service)是 MQTT 里最基础也最容易选错的概念。通俗地讲,它描述的是消息从发布者到订阅者之间的送达保证程度:

QoS语义生活化类比适合场景
0最多一次,可能丢失平邮,不保价高频传感器数据,丢了就丢了,下一轮还会来
1至少一次,可能重复挂号信,可能收到多次控制指令、告警通知,一般选择这个
2恰好一次,不重复视频会议握手协议,确保只读一次订单、计费类,消息重复代价极高

很多开发者在刚开始时喜欢把所有的消息都设置成 QoS 2,觉得越可靠越好。实际上 QoS 每升一级,网络交互都复杂很多,会出现更多确认包和状态存储,吞吐量会下降,延迟也会增加。大多数业务用 QoS 1 就够了,只有涉及资金、订单、高精度计数这类场景,才值得付出 QoS 2 的代价。

4.2 遗嘱消息:让设备“失联”也能被感知

遗嘱消息是 MQTT 里一个非常实用的功能。设备突然断电、网络断开时,其他终端需要感知到这件事。Broker 不会立刻发现 TCP 断连,可能要等心跳超时,但它一旦确认客户端异常断开,就会代替该客户端发布一条遗嘱消息。

QMQTT 里配置遗嘱很简单,在连接之前设置好:

client.setWillTopic(QStringLiteral("device/status")); client.setWillMessage(QStringLiteral("offline")); client.setWillQos(1);

这里要特别说明:正常客户端调用disconnect()主动断开时,Broker 不会发遗嘱消息;只有非正常断开,比如网线被拔掉、程序崩溃、心跳超时,遗嘱消息才会被触发。如果你想让设备正常下电时也发一条“离线”消息,需要在主动断开前自己手动发布一次。

4.3 主题命名与通配符规则

主题是分级的,用/分隔,例如house/room01/temp。MQTT 提供了两个非常常用的通配符:

  • +表示匹配单层,比如house/+/temp可以匹配house/room01/temp、house/room02/temp。
  • #表示匹配多层,比如house/#可以匹配house/room01/temp,也可以匹配house/room01本身。

建议不要以/开头的主题,那样会引入空层级,容易混乱;也不要用$开头的主题作为业务主题,$SYS这类前缀是 Broker 内部预留的。主题设计一般按 站点/设备/传感器类型/属性 来分层,比如siteA/dev03/sensor/temp。这样后续扩展时,无论是按站点订阅还是按设备订阅,都有足够的灵活性,也不会出现跨级订阅遗漏。

4.4 断线自动重连

真实环境里网络抖动、Broker 重启都难免,所以客户端一定要有断线重连的能力。我的做法是:

  1. 在connected信号里记录已连接状态,同时把订阅列表清空并重新订阅。
  2. 在disconnected信号里启动一个 QTimer,延时后重新连接。
  3. 所有订阅主题维护在一个QList<QPair<QString, quint8>>里,每次重连成功后统一重订阅。
  4. 为避免 Broker 被不断的重连请求打爆,可以做指数退避,比如重连间隔从 1 秒、2 秒、4 秒、8 秒逐步增加,封顶 30 秒。

伪代码大概是这样的:

void MqttClient::onDisconnected() { if (m_retryCount < 10) { int delay = qMin(30000, 1000 << m_retryCount); QTimer::singleShot(delay, this, [this]() { m_client.connectToHost(); }); ++m_retryCount; } }

重连成功的瞬间,不要忘记把m_retryCount清零,不然下次断线又会从很长的退避周期开始。

5. 常见问题与排查技巧实录

5.1 “连接不上 Broker”的排查顺序

连接不上这个问题最让人头疼,因为可能的原因太多了。我通常按下面的顺序排查:

  1. 确认 Broker 进程真的在运行,端口监听正常。
  2. 确认 IP 和端口是否匹配,127.0.0.1和局域网 IP 要区分清楚。
  3. 确认客户端 ID 没有冲突,同一个 ID 会把另一个客户端踢下线。
  4. 确认认证信息是否正确,一些 Broker 默认开启匿名访问,另一些则强制认证。
  5. 确认 Qt 事件循环是否在跑,main 函数是否最后调用了app.exec()。
  6. 确认你连接的 QMQTT 库和工程是否使用相同 Qt 版本和编译器。

第 5 点最容易误导人。很多程序运行起来没有任何报错,但连接就是不通,最后发现是事件循环没有进入等待状态,异步信号一直没有被处理。

5.2 “已经连上了,却一直收不到消息”

如果连接是成功的,却收不到订阅的消息,重点检查这几点:

  1. 主题是否完全一致。MQTT 主题区分大小写,Sensor/temp和sensor/temp不是同一个主题。
  2. 通配符是否用对了位置。sensor/#能匹配sensor/temp,但不能匹配sensor本身。
  3. 消息是否在订阅之后才发布。对于 QoS 0 的消息,如果发布时订阅者不在线,这条消息直接丢弃,不会补发。
  4. 发送方和接收方是否连的是同一个 Broker,有没有连到不同实例。
  5. 代码里是否创建了多个 Client 对象,比如界面里一个对象,业务逻辑里又 new 了一个,订阅和接收分别在两个对象上。

这类问题一般通过看两边的日志和 Broker 的连接日志就能定位。最简单有效的验证方式是:用一个 Broker 自带的订阅工具去订阅同样的主题,看有没有消息过来,以此判断问题出在代码还是 Broker 端。

5.3 “消息收到了,但是重复、乱序或者出现乱码”

这三个问题其实是不同层面的:

重复消息很大概率是 QoS 1 的正常行为。QoS 1 保证到达,但不保证不重复。如果你需要精确一次,要么升级到 QoS 2,要么在自己业务层做幂等去重,比如在 payload 里带全局唯一 ID。

乱序问题一般有两种情况。MQTT 协议本身不保证跨主题的消息顺序,如果你订阅多个主题,不能假设先到时先处理。同一个主题下,QoS 1 的消息通常能保持顺序,但如果你使用多线程处理消息,顺序被打乱也正常,需要自己在业务层加序号校验。

乱码问题基本就是编码问题。payload 是二进制安全的,但两端必须要约定统一编码,最稳妥的做法是统一 UTF-8。如果你发现从设备端收上来的中文乱码,先去检查发布端用什么编码,而不是在接收端反复做编码转换。

5.4 “程序退出时崩溃”

这个问题的常见场景有两个。第一个是在槽函数里直接delete了 Client 对象,但当前信号处理还没结束,后面又在访问同一个对象,于是野指针。处理方法是用deleteLater()延迟释放,或者先把连接断开等待disconnected信号再清理。第二个是 Client 对象的生命周期比事件循环短,比如在主函数里声明了一个栈上的 Client,最后 return 时事件循环还没有正常结束,导致析构时的资源竞争。

解决办法其实很简单:把 Client 的生命周期和主窗口或者核心业务对象绑定在一起,退出时先切断信号槽连接,再主动disconnect(),最后让事件循环自然退出。这样比在析构函数里做各种“急救”要稳得多。

5.5 常见问题速查表

现象可能原因处理思路
一直重连但连不上Broker 未启动 / 端口不对 / 认证失败先确认 Broker 和端口,再检查账号密码
连接后立刻被踢下线客户端 ID 冲突换唯一客户端 ID
订阅失败主题非法 / 权限不足检查主题格式和 Broker ACL 配置
收不到消息主题不匹配 / 订阅晚于发布 / QoS 0 丢弃统一主题约定,必要时用 QoS 1
消息重复到达QoS 1 正常语义业务层幂等或改用 QoS 2
中文乱码payload 编码不统一统一使用 UTF-8
退出崩溃Client 生命周期管理不当断开连接再释放,使用 deleteLater

最后再分享一个我自己的体会:MQTT 这块,技术难点从来不在协议本身,而在于消息能不能按约定到达正确的人手里。所以主题设计、编码约定、QoS 选择这些一定要在项目初期定好规范,后期改动成本很大。我手里的这个例子,最初是为了做一个带 Web 态的监控面板跑通的。顺着这个思路扩展,你还能把收到的消息写进时序数据库做趋势分析,或者接一个消息桥接,让桌面客户端、网页端甚至手机端共用一套主题,改动反而最少。先把这份 Demo 跑通,然后再一点点把业务塞进来,你会发现这套通信底座其实比预想的要稳得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 15:14:36

汽车电子硬件窗口期:电动化智能化重塑需求、赛道与能力

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 15:13:32

从帧到包:链路层与局域网通信排查实战指南

干网络排障这行&#xff0c;总有新手拿着抓包软件问我&#xff1a;这显示的到底是“包”还是“帧”&#xff1f;还有人会把“链路层”和“网络层”混在一起分析&#xff0c;结果过滤条件写错、判断方向跑偏&#xff0c;查了半天才发现看的是不同层面的东西。这个困惑太常见了&a…

作者头像 李华
网站建设 2026/10/19 15:11:58

TransXNet实战:用D-Mixer替代注意力机制,高效完成图像分类微调

简介&#xff1a;围绕TransXNet在图像分类任务中的实战应用&#xff0c;资源核心是使用transxnet_t模型完成植物分类任务&#xff0c;并在数据集上实现了96%以上的准确率。TransXNet在ImageNet-1K上相比Swin-T有更高的top-1准确率和更低的计算成本&#xff0c;具备较好的模型扩…

作者头像 李华
网站建设 2026/10/19 15:11:23

安卓手机连电脑没反应?USB传文件从原理到排查全攻略

插上USB线&#xff0c;手机弹出充电提示&#xff0c;电脑这边却什么反应都没有——这是我被问过最多的问题之一。用USB线把文件从Android传到PC&#xff0c;听起来是再基础不过的操作&#xff0c;但实际做起来总会冒出各种奇怪现象&#xff1a;电脑认不出设备、复制到一半断线、…

作者头像 李华
网站建设 2026/10/19 15:09:42

Arduino U8g2库详解:跨屏兼容的嵌入式单色显示屏驱动方案

简介&#xff1a;Arduino_u8g2库是面向嵌入式初学者与物联网开发者的高性能图形显示解决方案&#xff0c;专为Arduino平台优化&#xff0c;解决OLED、LCD及ePaper等多类显示屏在资源受限微控制器上的驱动与图形渲染难题&#xff0c;广泛适用于智能硬件原型、交互艺术装置、低功…

作者头像 李华
网站建设 2026/10/19 15:06:56

Spring Boot配置加密实战:Jasypt加解密命令与密钥管理

先说说我为什么要把这篇文章写下来。之前的某次项目交接中&#xff0c;我接手了一套遗留系统&#xff0c;打开application.yml一看&#xff0c;数据库账号密码、Redis密码、第三方支付密钥清清楚楚躺在里面&#xff0c;甚至还有生产环境的配置。当时后背一凉——这意味着任何一…

作者头像 李华