简介:针对 VS2019 环境下 MQTT C++ 客户端开发需求,这份编译产物省去了从源码手动配置 CMake 与依赖库的繁琐流程,可直接嵌入自己的工程使用,适用于物联网设备通信、消息推送等实时消息场景。压缩包共 550 个文件,主体为 cpp 源码、h 头文件以及编译完成的 dll、lib 库,同时包含 cmake 配置、vcxproj 工程文件和 pdb 调试符号,便于查看工程结构和进行二次调试。整体约 64.21MB,已有 2979 人学习下载。除了可直接调用的动态库与静态库,资源还附带了完整源代码和示例工程,适合需要快速集成 MQTT 通信、又希望保留自定义扩展空间的中高级 C++ 开发者,既能对照学习 paho.mqtt.cpp 的封装方式,也能将其作为 Windows 项目的基础模板。包内同时保留 CMake 构建脚本,可按需重新编译或调整依赖,部署后即可通过 C++ 接口发布或订阅消息。
1. VS2019 编译完成的 paho.mqtt.cpp 库:Windows 上 C++ 接 MQTT 的捷径
在 Windows 下用 C++ 做 MQTT 接入,十有八九卡在库的编译上。paho.mqtt.cpp 是 C++ 的 MQTT 客户端库,从源码开始构建要过 CMake、要处理 pthread 依赖、要对齐运行库模式,按部就班编译一遍,少说也要半天。这份 VS2019 编译完成的 paho.mqtt.cpp 库,把头文件、静态库、动态库按平台和调试模式分目录打包好,接到 Visual Studio 2019 工程里就能直接编。适合正在写 MQTT C++ 客户端、做设备数据采集、对接消息中间件,又不想在编译链上耗时间的开发者。下面我按拆库的思路,把这套东西的结构、接入步骤和常见坑过一遍。
2. 摸清家底:paho.mqtt.cpp 的分层结构、文件组成与选型理由
动手配置之前,先得搞清楚拿到的库里各文件之间是什么关系。paho.mqtt.cpp 不是从零实现的一套协议栈,而是 paho.mqtt.c(C 语言客户端库)之上的 C++ 封装层。真正与 broker 建立 socket 连接、做 MQTT 报文编解码、处理 QoS 确认和心跳保活的,是底层的 C 库;C++ 层主要提供类型安全的 API、回调机制和 RAII 式资源管理。这意味着链接的时候只加 C++ 库的 lib 不够,底层 C 库的符号同样会被引用到,缺了就是一堆 unresolved external symbol。
2.1 同步 client 与异步 async_client:两套 API 的设计差异
paho.mqtt.cpp 对外暴露了两套核心 API。mqtt::client 是同步阻塞风格,connect、publish、subscribe、disconnect 调用返回时,操作已经完成或已经进入确定的错误状态。这类 API 适合程序结构简单、主逻辑按顺序执行的场景,比如一个采集程序每 10 秒上报一次数据,阻塞等待结果问题不大。mqtt::async_client 则把所有操作变成非阻塞调用,返回 mqtt::token 对象,可以靠 token->wait() 阻塞等待结果,也可以注册完成回调。异步客户端内部维护自己的线程池来处理消息收发,适合需要长时间在线、实时响应下行消息、或者有多个主题要并发订阅的场合。
两套 API 的名字和头文件也不同:同步的在 mqtt/client.h 里,异步的在 mqtt/async_client.h 里。写代码时最常犯的错是把异步客户端的 token 当成同步返回值去判断,导致拿到一个尚未完成的 token 直接调用 get() 而阻塞或者抛异常。理解底层结构后就不会混淆:异步 API 的返回值是“操作凭证”,不是操作结果;同步 API 的返回值才是真正的状态。这份 VS2019 编译产物里,同步和异步的实现都编进去了,不需要额外配置开关,只要在代码里 include 对应的头文件即可。
2.2 目录结构:include、lib、dll 各自承担什么职责
解压这份库之后,目录结构一般长这样:
paho.mqtt.cpp_vs2019/ ├── include/ │ ├── mqtt/ │ │ ├── client.h │ │ ├── async_client.h │ │ ├── callback.h │ │ ├── connect_options.h │ │ ├── message.h │ │ └── token.h │ └── paho/ │ └── export.h ├── lib/ │ ├── x64/ │ │ ├── paho-mqttpp3.lib │ │ ├── paho-mqttpp3d.lib │ │ ├── paho-mqtt3a.lib │ │ ├── paho-mqtt3c.lib │ │ ├── paho-mqtt3as.lib │ │ └── paho-mqtt3cs.lib │ └── x86/ │ └── ...(与 x64 同名) ├── dll/ │ ├── x64/ │ │ ├── paho-mqttpp3.dll │ │ ├── paho-mqttpp3d.dll │ │ ├── paho-mqtt3a.dll │ │ ├── paho-mqtt3c.dll │ │ └── pthreadVC2.dll │ └── x86/ │ └── ...(与 x64 同名) └── README.mdinclude 目录里是对外暴露的 C++ 头文件,mqtt/client.h 对应同步客户端,mqtt/async_client.h 对应异步客户端,mqtt/callback.h 定义回调接口。lib 目录里文件名尾部的 3 表示 MQTT 3.1.1 协议版本,d 后缀表示 Debug 版本,a 表示异步版本、c 表示同步版本。paho-mqtt3a.lib 和 paho-mqtt3c.lib 是底层 C 库的导入库,编译 C++ 代码时这两个也经常被间接引用。dll 目录里的 pthreadVC2.dll 是 Windows 下 pthread 线程接口的模拟层,paho.mqtt.c 在 Windows 上依赖它。运行程序时只要缺失其中任何一个 dll,启动就会报“找不到 xxx.dll”,排查时先对照这个清单逐项核对。
2.3 选型对比:自己写协议、纯 C 客户端和 C++ 客户端的取舍
| 维度 | 自写 socket 客户端 | 纯 C 客户端(paho.mqtt.c) | paho.mqtt.cpp |
|---|---|---|---|
| 协议完整性 | 需自己实现 CONNECT/PUBLISH/SUBSCRIBE 等全部报文 | 已实现,含 QoS 0/1/2 | 在 C 库基础上封装,协议层完整 |
| QoS 与重连 | 工作量极大 | 支持自动重连但需要自己封装调用 | 提供高级 API,set_automatic_reconnect 一行搞定 |
| 回调机制 | 自己写线程模型 | 回调接口偏向 C 风格,生命周期管理靠手动 | C++ 风格,支持 std::function、继承 callback |
| Debug 难度 | 任何异常都要从报文层面定位 | 邮件列表 / issues 较多 | 依赖 C 库,排错需要同时理解两层 |
| 工程接入成本 | 低 | 低 | 低(本资源已编译好) |
| 现代 C++ 特性 | 无 | 无 | 有 RAII、异常安全、类型安全 |
选 paho.mqtt.cpp 的核心原因是它的异步 API 和自动重连机制。处理 MQTT 长连接时,网络闪断、broker 重启是常态,C 库虽然提供自动重连选项,但使用上偏底层;C++ 封装把这些收敛成 connect_options 上的几个 setter,接入成本低很多。我之前用纯 C 库写过一段采集程序,重连逻辑要自己处理退避、会话恢复和队列重发,换到 C++ 封装后这部分代码量少了约三分之一。
3. 接进 VS2019:三步配置与第一段可运行代码
配置环境是最容易翻车的一步,很多人 configure 五分钟,链接错一上午。下面按我常用的顺序来:先确认平台位数,再配置包含目录和库目录,最后调整运行库模式。每一步都对应 VS2019 的菜单路径,照做即可。
3.1 包含目录、库目录与附加依赖项的配置路径
在 VS2019 里新建一个空项目后,右键项目名进入“属性”。先看顶部的“解决方案配置”和“解决方案平台”,确保是 x64(如果库是 x64 版本)。接着打开“C/C++ → 常规 → 附加包含目录”,把解压目录里的 include 文件夹路径填进去。然后打开“链接器 → 常规 → 附加库目录”,填入 lib 目录里对应平台的路径,例如 D:\libs\paho.mqtt.cpp_vs2019\lib\x64。最后在“链接器 → 输入 → 附加依赖项”里填入:
paho-mqttpp3.lib paho-mqttpp3d.lib paho-mqtt3a.lib paho-mqtt3c.libDebug 配置下建议填 paho-mqttpp3d.lib,Release 下填 paho-mqttpp3.lib。把四个 lib 全填上是稳妥做法,因为 C++ 层可能引用 C 层符号,多填不会出错,少填就会出现 LNK2019。需要注意附加依赖项里 lib 的名字必须和磁盘文件名完全一致,大小写不敏感但后缀不能省略。
3.2 预处理器定义与运行库模式:两个隐蔽的坑
配置好路径后,还需要在“C/C++ → 预处理器 → 预处理器定义”里加上 _WIN32_WINNT=0x0601。这个宏告诉 Windows SDK 以 Windows 7 及以上的 API 版本编译,paho 内部有时会调用较新的系统 API,不加的话某些函数声明不可见,编译时直接报找不到定义。另一个关键项是“C/C++ → 代码生成 → 运行库”,必须和库的编译方式一致。这份资源里 lib/dll 是 /MD 还是 /MT 编译,直接决定了项目里的运行库选什么。
提示:判断方法很简单,Debug 模式用 /MDd,Release 模式用 /MD。如果你的项目之前改成过 /MT 或 /MTd,在这里改回来,否则 dll 里的 CRT 符号和 exe 里的 CRT 符号各管各的,new/delete 跨模块操作大概率崩溃。这个坑在 paho 这类第三方库里特别常见,因为它的回调链横跨你的代码和库代码,内存对象在两边都可能被释放。
3.3 同步客户端最小示例:连接、发布、断开
配置完属性,写一段最简代码验证环境:
#include <mqtt/client.h> // 同步客户端头文件 #include <iostream> int main() { // 构造同步客户端:参数1是 broker 地址,参数2是客户端唯一标识 mqtt::client cli("tcp://localhost:1883", "vs2019_demo_sync"); mqtt::connect_options opts; opts.set_keep_alive_interval(30); // 30 秒心跳保活 opts.set_clean_session(true); // 连接断开后不保留离线消息 cli.connect(opts); // 阻塞直到连接建立完成 cli.publish("demo/sensor", "25.6", 1); // 发布到主题 demo/sensor,QoS 1 std::cout << "published" << std::endl; cli.disconnect(); // 主动断开 return 0; }逻辑说明:这段代码先构造 mqtt::client 对象,客户端 ID 是 vs2019_demo_sync,broker 地址指向本机 1883 端口。connect_options 里的 keep_alive_interval 表示心跳包发送间隔,超过 broker 设置的超时时间没有收到心跳就会被判定掉线。clean_session 为 true 时,broker 不缓存当前会话的订阅和离线消息,断线重连后需要重新订阅。publish 第三个参数是 QoS,0 表示最多一次,1 表示至少一次,2 表示恰好一次。QoS 1 会触发 broker 的 PUBACK 确认。
这段代码如果编译通过并且连接的是正常运行的 broker,运行后在 broker 侧能看到该客户端上线。我一般用它做环境验证,因为流程短,排错面小。如果这一步都跑不通,后面异步代码也别试了。
3.4 异步客户端与回调:订阅消息的完整形态
大部分实际项目需要订阅和接收下行消息,所以异步客户端加回调才是常态。下面这段代码展示最基本的使用方式:
#include <mqtt/async_client.h> // 异步客户端头文件 #include <iostream> #include <thread> #include <chrono> // 回调类:继承 mqtt::callback,消息到达时由库内部线程调用 class msg_callback : public virtual mqtt::callback { public: void message_arrived(mqtt::const_message_ptr msg) override { std::cout << msg->get_topic() << " => " << msg->to_string() << std::endl; } }; int main() { mqtt::async_client cli("tcp://localhost:1883", "vs2019_demo_async"); msg_callback cb; cli.set_callback(cb); // 注册回调,cb 生命周期必须覆盖 cli 的使用期 mqtt::connect_options opts; opts.set_automatic_reconnect(2, 30); // 2 秒后首次重连,后续最大间隔 30 秒 cli.connect(opts)->wait(); // 发起异步连接,wait 阻塞等待完成 cli.subscribe("demo/#", 1); // 订阅所有以 demo/ 开头的主题,QoS 1 std::cout << "subscribed, waiting..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(10)); cli.disconnect()->wait(); return 0; }逻辑说明:async_client 的 connect 返回 token 对象,wait() 会阻塞直到连接完成或失败。subscribe 同样返回 token,但这里没有等待,因为连接已完成,订阅消息会在后台排队处理。message_arrived 回调在 paho 内部线程中执行,业务逻辑里不要做耗时操作。set_callback 之后,回调对象的生命周期必须至少覆盖到客户端对象销毁,否则线程切换时访问已析构对象直接崩溃。从工程实践角度,我一般把回调对象定义在 main 函数栈上,保证 main 返回时回调仍在。
参数说明:automatic_reconnect 的两个参数是重连的最短间隔和最长间隔,单位秒,库内部会按退避策略从短到长递增。如果 broker 不可达,异步客户端不会让你的程序卡死,这也是生产环境必须用异步客户端的原因。
4. 避坑指南:链接错误、运行库冲突与断线排查
paho 库的坑多数不在协议本身,而在 Windows 的链接和运行环境。下面五条是我在几个项目里踩过的,都有明确的现象和对应的处理方式。
4.1 LNK2019 / LNK1112:lib 位数与平台不匹配
现象:编译时报 LNK2019: unresolved external symbol,或者 LNK1112: module machine type 'x64' conflicts with target machine type 'x86'。
原因:附加依赖项里填的 lib 是 x64 版本,而解决方案平台是 Win32;或者反过来。另一种可能是在附加依赖项里只填了 C++ 层的 paho-mqttpp3.lib,漏掉了底层 C 库的 paho-mqtt3a.lib / paho-mqtt3c.lib,导致 C 层符号无法解析。
解决:先把解决方案平台切到 x64 或 x86,和 lib 目录名严格对应;再把四个 lib 全部填进附加依赖项。我通常会去链接器 → 输入 → 附加依赖项的编辑框里核对一遍,确保没有拼写错误。还有一种隐蔽情况:项目里同时引用了其它库,它们之间使用了不同版本的运行时,但那是另一个问题,先按这条排查。
4.2 运行库冲突:/MTd 与 /MDd 混用导致的崩溃
现象:程序编译通过,启动正常,一旦触发消息回调就在 system("pause") 之前挂掉,调试器报 free() invalid pointer 或 new/malloc 大小不一致。
原因:你的项目选择“多线程调试 (/MTd)”,而 paho 库是 /MDd 编译的。两边 CRT 各有一份堆元数据,库返回的 std::string 在库侧分配、在你的代码里被释放,或者反过来,立刻崩溃。
解决:打开“C/C++ → 代码生成 → 运行库”,把 Debug 统一为 /MDd,Release 统一为 /MD。改完这个之后,大概率所有诡异崩溃都消失。这条经验来自某次“跑着跑着就挂在随机位置”的排查,最后发现是 /MTd 和 /MDd 混用导致的。从那以后我拿到任何 VS 编译的第三方库,第一步就是问清楚对方的运行库模式。
4.3 中文消息乱码与消息截断
现象:publish 一条“温度正常”的中文消息,订阅端显示乱码;或者消息尾部少了几个字符。原因:发送端用 std::string 直接装载默认编码的本地字符集,而 broker 和另一侧客户端预期的是 UTF-8。MQTT 报文本身就是二进制安全,协议不限制编码,但如果没有约定编码,接收端按 UTF-8 解析就会乱。截断则是用 strlen() 计算长度时遇到中文字节里的 0x00。
解决:发送前统一转成 UTF-8,并在构造消息时使用 std::string 的 size() 作为长度,而不是 strlen()。接收端 message_arrived 里拿到 msg->to_string() 后也要按 UTF-8 处理。如果发送的就是二进制数据,建议直接给主题加编码标识,比如 demo/sensor/text/utf8。血泪教训是,这种问题只有在通信双方都打印十六进制时才能看出来,别迷信控制台能正常显示。
4.4 自动重连参数设了但没生效
现象:connect_options 里调用了 set_automatic_reconnect(2, 30),运行后拔掉网线再插回,客户端没有恢复连接,或者一直卡在“重连中”不再上线。
原因:两种常见情况。第一,自动重连参数必须在 connect() 之前设置,如果先 connect 再 set_automatic_reconnect,库内部的连接状态机已经进入已连接状态,参数不会二次生效。第二,set_clean_session(true) 搭配会话过期时间过短、broker 侧 session 过期时间被设置为 0,导致重连后会话没有恢复,看起来像没有重连成功。
解决:把 set_automatic_reconnect 放在 connect_options 构造后立即调用。同时检查 broker 配置,保证 clean_session 设置与你想要的行为一致。如果想让重连后自动恢复订阅,将 clean_session 设为 false,broker 会记住订阅关系,重连后不需要重新 subscribe。需要注意的是,clean_session=false 会让 broker 缓存离线消息,消息量大会占用 broker 内存,生产环境要按需取舍。
4.5 回调生命周期与异常安全
现象:程序退出时崩溃,堆栈显示 mqtt::async_client 析构时还在调用 callback,或者 message_arrived 里抛出的异常导致线程终止。
原因:回调对象被提前销毁,或者回调函数里抛出了未捕获异常。paho 内部线程调用 message_arrived 时如果异常逸出,内部线程会终止,后续消息全部不回调,且不会给出任何日志。
解决:callback 对象定义为 cli 的同级或更外层对象,确保 cli 析构前回调一定还在。回调函数体用 try-catch 包住,任何业务异常都先捕获、记录、再返回。我一般会把回调对象封装成类成员,生命周期和客户端对象绑定。检查时也可以在析构函数里输出日志,确认析构顺序。
5. 进阶技巧:把连接参数调到“断了能回来”的状态
很多项目上线后出现的“客户端掉线后不来”问题,根源都在连接参数没调对。paho.mqtt.cpp 里最值得花时间理解的是三组参数:心跳、自动重连、会话清理。我建议每个项目都在正式跑业务前,先写一个只连接、不发布消息的测试程序,把三组参数拆开验证。
第一组是 keep_alive_interval,单位秒,默认值一般是 60。它决定客户端发送 PINGREQ 的间隔。如果 broker 的保持连接超时配置比这个值短,客户端会被误判掉线。建议设为服务器超时时间的一半。第二组是两个连接超时参数:connect_timeout 和 serverURI 列表的顺序。connect_timeout 设置建立 TCP 连接的超时,超过就放弃本次连接;如果有多个 broker 地址可以都填进去,paho 会依次尝试。第三组是 clean_session,设为 false 时 broker 会记住客户端的订阅和离线消息,重连后不需要重新订阅。但离线消息会在 broker 端堆积,消息量大的场景要评估内存占用。
验证方法很简单:程序运行后,断开网络或直接停掉 broker,观察日志。正常行为应是在 keep_alive_interval 之后发现心跳失败,触发自动重连流程,恢复连接后如果 clean_session=false 则自动恢复订阅。我自己写过一个验证脚本,每 30 秒检查一次客户端连接状态,通过一个状态主题上报在线情况。连续拔网线、恢复、再拔网线,来回测了十几次,确认重连稳定后才敢把服务放出去。
从那以后,我每次拿到别人编译的库,第一件事是确认运行库模式,第二件事就是写一个 10 行内的最小连接验证,跑通再谈业务逻辑。这套习惯帮我挡掉了很多环境问题,希望也能帮到你。
本文还有配套的精品资源,点击获取