
简介面向物联网与嵌入式开发者的 paho-mqtt-c 实践代码包聚焦 C 语言环境下的 MQTT 客户端通信涵盖连接 broker、发布订阅、遗嘱设置、自动重连以及 SSL/TLS 加密配置等常见需求适合需要快速理解客户端封装思路或搭建测试链路的初、中级开发者参考。压缩包共 2 个文件由 1 个 C 源文件与 1 个头文件组成体量约 2KB源文件侧重连接、收发与回调等接口实现头文件用于结构体、枚举和函数原型声明便于按模块阅读与移植。目前已有 3366 人学习下载。内容围绕客户端初始化、连接参数配置、消息发布订阅、回调注册和断线重连等环节展开可帮助读者梳理 paho-mqtt-c 的核心调用流程理解 SSL/TLS 场景下的证书与连接选项设置并从中提炼可复用的 C 语言 MQTT 封装与排错思路。1. 源码编译通过就以为成功了链接时缺 ssl 支持某开发者把 paho-mqtt-c 源码拉下来cmake一路回车make也不报错接着编译自己写好的 mqtt 客户端样例结果卡在undefined reference to SSL_CTX_new上。翻回头查编译日志才发现cmake 阶段 SSL 支持根本没有编译进去代码里那些MQTTClient_SSLOptions只是没被用到所以看起来一切正常。paho-mqtt-c 这类 C 库的坑几乎都在编译和链接这一层库名后缀差一个字母-lpaho-mqtt3cs写成-lpaho-mqtt3c程序还是能编译出来只是连 8883 端口时直接报错。这类问题在把 MQTT 接入自有网关、把设备端日志回传到内网 broker 时特别常见。如果你正准备把 paho-mqtt-c 编进项目并且必须走 ssl 双向认证那下面这套流程就按顺序执行。2. 依赖与工具链OpenSSL 版本对不上编到一半就停C 库的依赖比 Python、Node 这类语言环境更麻烦编译期缺头文件、运行期缺 so报错信息还都长得很像。paho-mqtt-c 在开 SSL 的情况下硬依赖就是 OpenSSL 的开发包和一套能用的 CMake、GCC。这三样版本如果对不上表现通常不是一句清楚的“缺依赖”而是cmake配置阶段悄悄关掉 SSL或者编译到MQTTClient.c的时候才报一大堆 openssl 符号找不到。2.1 先确认 OpenSSL 头文件和库是配套的很多人系统里装了 OpenSSL 命令行工具就以为开发环境齐了。openssl version能打出版本号只说明运行库在ssl.h和libssl.so的开发包不一定在。先跑三条命令确认# 1. 看运行库版本 openssl version -a # 2. 看开发头文件是否存在这个才是编译真正要的 ls -l /usr/include/openssl/ssl.h # 3. Debian/Ubuntu 系确认 dev 包RHEL/CentOS 系用 rpm -qa | grep openssl-devel dpkg -l | grep -E libssl-dev|libssl3三条都过了才算过关。第 2 条如果报No such file说明只装了运行库缺libssl-devDebian/Ubuntu或openssl-develRHEL/CentOS。这里有个版本取舍OpenSSL 1.1.1 和 3.x 在 paho-mqtt-c 上的编译表现不完全一样1.1.1 时代的一些老接口在 3.x 里被标记为 deprecated编译时会刷出一屏警告警告本身不影响功能但会掩盖真正的错误。我一般会在 cmake 里显式关掉把警告当错误的那类选项保证编译能走完再单独看 SSL 相关的 warning。还有一种情况更麻烦系统里同时存在自编译的 OpenSSL装在/usr/local/ssl和发行版自带的在/usr/lib/x86_64-linux-gnu。cmake默认会挑find_package(OpenSSL)找到的第一个可能挑到你不想用的那个链接出来的程序在别的机器上就跑不起来。确认方式是在 cmake 配置完成后翻一遍输出的OPENSSL_INCLUDE_DIR和OPENSSL_LIBRARIES两行跟自己预期不一致就手动指定见下一章的参数。注意不要为了省事去软链/usr/local/ssl到/usr/lib两个版本的 libssl 混在一起最容易出现编译通过、运行时段错误这种难查的问题。2.2 CMake 和 GCC 的最低要求paho-mqtt-c 的 1.3.x 系列对 CMake 的要求不算高3.10 以上基本不会卡住低于 3.5 会碰到target_link_libraries语法不兼容直接配置失败。GCC 这边用 C99 就够了GCC 4.8 之后的版本都能编。真正要注意的是 CMake 的“目录外构建”习惯不要在源码目录里直接跑cmake .中间文件会和源码混在一起后面想切Debug、Release或者切 SSL 开关只能rm -rf重来。# 检查工具链版本三项都打出版本号再往下走 cmake --version gcc --version make --version这三条输出不用记具体数字只要不是“命令未找到”就行。真正需要记住的是如果机器上的 cmake 是通过系统包管理器装的通常版本偏旧但稳定如果是自己下载的二进制包解压到/opt务必确认PATH里指向的是你要用的那个cmake否则会出现“我在终端里跑的是 3.2xsudo make install里调用的却是 3.10”的玄学现象因为sudo默认会重置PATH。2.3 装依赖这一步的具体命令把依赖补齐的动作按发行版分开照抄对应那一段就行。# Debian / Ubuntu sudo apt update sudo apt install -y build-essential cmake libssl-dev git # RHEL / CentOS / Rocky sudo yum groupinstall -y Development Tools sudo yum install -y cmake openssl-devel git # Alpine容器里编译很常见 apk add --no-cache build-base cmake openssl-dev gitbuild-essential和Development Tools这两个包组会带进 gcc、make、libc 头文件少了它们第一个报错通常是cmake: command not found或者cannot find crt1.o。libssl-dev、openssl-devel、openssl-dev是三个发行版里同一个东西的不同叫法作用都是提供ssl.h、libssl.so和libcrypto.so。装完之后建议再跑一次 2.1 里的检查命令确认头文件路径确实出现了这一步花十秒能省掉后面半小时的排查。3. 编译 paho-mqtt-c三个开关决定后面代码写不写得动依赖齐了之后编译本身只有三条命令但参数怎么给直接决定了产出的库能不能用。paho-mqtt-c 的 CMake 选项不算多其中PAHO_WITH_SSL、PAHO_BUILD_SHARED、CMAKE_INSTALL_PREFIX这三个是必须想清楚的剩下的大多是裁剪体积用的。3.1 取源码与目录外构建源码用git clone或者下载 release 包都行区别是 git 版本能拿到最新的 bugfixrelease 包更稳定并且带版本号便于复盘。不管哪种方式构建目录都放在源码外面# 取源码假设放在 ~/src 下 mkdir -p ~/src cd ~/src git clone https://github.com/eclipse/paho.mqtt.c.git cd paho.mqtt.c # 切到你需要的分支或 tag别停在默认分支上直接编 git tag --list | tail -20 # 建独立的构建目录以后想换配置直接删掉重建 mkdir -p build cd buildgit tag --list这一步经常被跳过但它决定了你后面遇到问题时能不能对得上版本。paho-mqtt-c 的 1.3.x 线在 API 上有过几次调整比如连接选项结构体里某些字段的初始化方式、TLS 1.3 的支持时间点都会让网上搜到的示例代码在你本地编不过。把版本号记在笔记里出问题时至少能判断是“代码写错”还是“版本不匹配”。构建目录分出去之后切换Release和Debug只是再建一个build-debug目录的事源码目录始终干净这也是后面回滚和对比的依据。3.2 三个 CMake 开关的实际含义配置命令长这样参数一行一个方便你按需增删cmake -S .. -B . \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX/usr/local \ -DPAHO_WITH_SSLTRUE \ -DPAHO_BUILD_SHAREDTRUE \ -DPAHO_BUILD_STATICTRUE \ -DPAHO_BUILD_SAMPLESTRUE \ -DPAHO_BUILD_DOCUMENTATIONFALSE \ -DPAHO_ENABLE_TESTINGFALSE逐项说清楚取舍。PAHO_WITH_SSL是这一篇的核心开关默认经常是关的很多人以为源码里带着 SSL 相关文件就等于 SSL 已经编译进去了实际不打开这个选项编出来的库根本不包含 TLS 代码路径代码能编译、能连 1883一换 8883 就握手失败而且失败得很安静。PAHO_BUILD_SHARED和PAHO_BUILD_STATIC建议都打开动态库方便开发和调试改代码不用重编依赖方静态库方便最终交付一个不依赖外部 so 的可执行文件尤其是往设备端、容器基础镜像里塞的时候静态链接能省掉一堆LD_LIBRARY_PATH的麻烦。CMAKE_INSTALL_PREFIX决定了make install之后头文件和库往哪放/usr/local是默认并且和系统包管理器装的库天然隔离不容易冲突如果你没有 root改成$HOME/.local也行但后面编译自己的程序时必须显式加-I和-L。PAHO_BUILD_SAMPLESTRUE建议保留它编出来的paho_c_pub、paho_c_sub在验证阶段能直接用来排除“是我代码写错还是环境有问题”。PAHO_BUILD_DOCUMENTATION需要额外依赖日常用不着关掉能少一堆配置检查。配置完成后注意盯住输出里跟 OpenSSL 有关的那几行-- Found OpenSSL: /usr/lib/x86_64-linux-gnu/libcrypto.so (found version ...)如果这一行压根没出现说明PAHO_WITH_SSLTRUE没生效或者find_package没找到 OpenSSL。这时候不要急着往下make先解决这一行。手动指定路径的写法cmake -S .. -B . \ -DOPENSSL_ROOT_DIR/usr/local/ssl \ -DOPENSSL_INCLUDE_DIR/usr/local/ssl/include \ -DOPENSSL_SSL_LIBRARY/usr/local/ssl/lib/libssl.so \ -DOPENSSL_CRYPTO_LIBRARY/usr/local/ssl/lib/libcrypto.so \ -DPAHO_WITH_SSLTRUE3.3 make 与 install 之后文件落在哪配置过了就能编# -j 后面跟核数编 C 库很快一般几十秒 make -j$(nproc) # 装到 CMAKE_INSTALL_PREFIX 指定的位置 sudo make install装完之后别急着写业务代码先把产出物点一遍知道每个文件干什么的后面链接报错时能一眼看出是缺库还是缺头文件ls -l /usr/local/lib/libpaho-mqtt3* ls -l /usr/local/include/MQTTClient.h ls -l /usr/local/include/MQTTAsync.h正常会看到libpaho-mqtt3c、libpaho-mqtt3cs、libpaho-mqtt3a、libpaho-mqtt3as四组每组可能有.so、.a和带版本号的软链。命名规则必须记牢这是后面所有链接错误的根源库名API 风格是否带 SSLlibpaho-mqtt3c同步MQTTClient否libpaho-mqtt3cs同步MQTTClient是libpaho-mqtt3a异步MQTTAsync否libpaho-mqtt3as异步MQTTAsync是字母c和a是 API 风格末尾的s才是 SSL。写-lpaho-mqtt3c编出来的程序你在代码里填再多MQTTClient_SSLOptions也没用连接 8883 会直接返回失败。当时那个undefined reference to SSL_CTX_new本质就是反过来链接了带 s 的库但库本身编的时候没开PAHO_WITH_SSL或者没带上-lssl -lcrypto。装到/usr/local之后还有一个常规动作让动态链接器认识新目录# 只在 /usr/local/lib 不在默认搜索路径时需要 echo /usr/local/lib | sudo tee /etc/ld.so.conf.d/paho.conf sudo ldconfigldconfig之后用ldconfig -p | grep paho确认一遍能看到四条记录说明注册成功。不做这一步的话程序编译能过运行时报error while loading shared libraries: libpaho-mqtt3cs.so.1看起来像玄学其实只是路径没告诉系统。4. 配置 SSL/TLSSSLOptions 每个字段都要有出处库编好了接下来是把 TLS 真正用起来。paho-mqtt-c 的 SSL 配置全在MQTTClient_SSLOptions结构体里字段不多但每一个填错的后果都不一样填少了握不上手填多了会走一些意料之外的校验分支。这一章按“证书怎么放 → 代码怎么填 → 编译怎么链”三步走。4.1 证书三件套的摆放位置与权限先明确要准备哪些文件。单向认证客户端只校验服务端只需要 CA 证书双向认证服务端也校验客户端额外要客户端证书和私钥。常见的文件有四份CA 证书签发 broker 证书的那张根证书客户端用它校验服务端身份服务端证书broker 侧配置客户端一般不直接读客户端证书双向认证时客户端出示客户端私钥和客户端证书配对绝对不能有组外读权限放到统一目录权限按下面这套来sudo mkdir -p /etc/mqtt/certs # CA 和证书可以 644私钥必须收紧 sudo chmod 644 /etc/mqtt/certs/ca.crt sudo chmod 644 /etc/mqtt/certs/client.crt sudo chmod 600 /etc/mqtt/certs/client.key # 私钥属主给运行程序的账号避免程序读不到或权限过宽 sudo chown $USER /etc/mqtt/certs/client.key这里最容易翻车的两点一是私钥权限给到 644某些 OpenSSL 版本在加载私钥时不会报错但在部分加固过的环境里会直接拒绝加载现象是握手到一半断掉日志里只有一句含糊的 SSL error二是路径用了相对路径MQTTClient_connect时程序的工作目录和你想的不一样读不到文件返回码同样是-1跟证书内容错误完全没法区分。我一般会在代码里写死绝对路径并且在启动时先access()检查一遍文件在不在把错误暴露在连 broker 之前。注意自签证书的场景下务必确认服务端证书的 SAN 里包含你连接时用的地址。用 IP 连、证书只签了域名校验一定失败这属于证书内容问题改客户端代码是绕不过去的。4.2 代码里把 SSLOptions 填对下面是一段能直接跑的最小同步客户端重点看 SSL 部分的四行赋值和结构体初始化方式#include MQTTClient.h #include stdio.h #include stdlib.h #include string.h #include unistd.h /* ssl:// 前缀才是触发 TLS 的开关写成 tcp:// 后面的 ssl 配置全部不生效 */ #define ADDR ssl://broker.example.internal:8883 #define CLIENTID demo-sub-01 #define TOPIC demo/telemetry #define QOS 1 #define TIMEOUT 10000L int main(void) { MQTTClient client; MQTTClient_connectOptions conn_opts MQTTClient_connectOptions_initializer; MQTTClient_SSLOptions ssl_opts MQTTClient_SSLOptions_initializer; MQTTClient_message pubmsg MQTTClient_message_initializer; MQTTClient_deliveryToken token; int rc; /* 持久化选 NONE联通性测试够用正式环境按需改成文件持久化 */ rc MQTTClient_create(client, ADDR, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL); if (rc ! MQTTCLIENT_SUCCESS) { printf(create failed, rc%d\n, rc); return EXIT_FAILURE; } /* 只校验服务端信任链用 CA 文件客户端证书留空 */ ssl_opts.trustStore /etc/mqtt/certs/ca.crt; ssl_opts.keyStore NULL; ssl_opts.privateKey NULL; ssl_opts.privateKeyPassword NULL; ssl_opts.enableServerCertAuth 1; ssl_opts.sslVersion MQTT_SSL_VERSION_TLS_1_2; ssl_opts.verify 1; conn_opts.keepAliveInterval 30; conn_opts.cleansession 1; conn_opts.connectTimeout 10; conn_opts.MQTTVersion MQTTVERSION_3_1_1; conn_opts.ssl ssl_opts; if ((rc MQTTClient_connect(client, conn_opts)) ! MQTTCLIENT_SUCCESS) { /* rc 是定位问题的第一手信息别只打一句 connect failed */ printf(connect failed, rc%d\n, rc); MQTTClient_destroy(client); return EXIT_FAILURE; } pubmsg.payload hello over tls; pubmsg.payloadlen (int)strlen(pubmsg.payload); pubmsg.qos QOS; pubmsg.retained 0; MQTTClient_publishMessage(client, TOPIC, pubmsg, token); MQTTClient_waitForCompletion(client, token, TIMEOUT); MQTTClient_disconnect(client, 1000); MQTTClient_destroy(client); return EXIT_SUCCESS; }几个字段必须讲清楚。trustStore是 CA 文件路径单向认证只填它就够keyStore和privateKey是双向认证才需要的一对只填其中一个OpenSSL 会报私钥和证书不匹配然后连接失败。enableServerCertAuth 1是真正让客户端校验服务端证书的开关把它设成 0连接确实能通但等于放弃了 TLS 最核心的身份认证任何能占住这个地址的服务端都能接你的数据正式环境不要这么干。verify字段在新版本里基本是历史遗留真正起作用的是enableServerCertAuth两个都写不冲突但排查问题时要认准后者。sslVersion建议显式指定MQTT_SSL_VERSION_TLS_1_2别用默认值。默认值会跟随 OpenSSL 的协商策略在服务端禁用了 TLS 1.0/1.1 的环境里某些旧版本会多花一次协商往返甚至协商失败。如果你的 broker 已经开了 TLS 1.3较新的 paho 版本提供了MQTT_SSL_VERSION_TLS_1_3但老版本里这个宏不存在用了会编译报错这就是前面强调记版本号的原因。双向认证的场景把keyStore指向client.crt、privateKey指向client.key私钥若是加密的privateKeyPassword填口令。连接地址的前缀同样关键。ssl://触发 TLStcp://是明文写成tcp://...:8883连上去broker 会直接断连客户端表现是连接后立刻connectionLost很容易被误判成网络问题。4.3 编译链接参数少一个 s 就连不上代码写完编译命令决定成败# 动态链接带 SSL 的同步库 gcc demo_sub.c -o demo_sub \ -I/usr/local/include \ -L/usr/local/lib \ -lpaho-mqtt3cs -lssl -lcrypto -lpthread # 静态链接写法库名后缀多一个 -static且必须显式带上 ssl/crypto gcc demo_sub.c -o demo_sub_static \ -I/usr/local/include \ -L/usr/local/lib \ -lpaho-mqtt3cs-static -lssl -lcrypto -lpthread -ldl-lpaho-mqtt3cs末尾的s是 SSL去掉就退化成明文库运行时连 8883 直接失败-lssl -lcrypto在动态链接时有时能靠 so 的依赖关系自动带上但静态链接一定要显式写否则会出现一大片undefined reference to SSL_*。-lpthread两个场景都不能省paho 内部有线程。静态链接多一个-ldl是因为 OpenSSL 在部分平台上会用到动态加载接口。还有一个版本差异值得记一下新版本的 paho 提供了MQTTClient_createWithOptions这类带选项的创建接口如果你的代码里用了它、链接的却是老版本库报错是undefined reference to MQTTClient_createWithOptions看着像拼写问题实际是版本不匹配。遇到这种符号找不到的情况先用nm -D /usr/local/lib/libpaho-mqtt3cs.so | grep MQTTClient_create确认库版本再决定是升库还是改代码。5. 避坑与排查编译过了不等于连得上这套环境里九成的问题集中在两个阶段编译链接期和 TLS 握手期。前者报错明确但容易被忽略后者报错含糊返回码只有-1得靠分层排查才能定位。下面按“现象 → 原因 → 解决”整理五条真实踩过的坑。5.1 编译与链接期现象一fatal error: MQTTClient.h: No such file or directory。原因通常不是没装而是装到了/usr/local/include而编译时没加-I/usr/local/include或者用了sudo装在别的前缀下。解决方式是先find / -name MQTTClient.h 2/dev/null找到真实位置把对应的-I补上也可以在编译时不加sudo用-DCMAKE_INSTALL_PREFIX$HOME/.local装到用户目录避免权限和路径两重麻烦。现象二链接时报一大堆undefined reference to SSL_CTX_new、SSL_read。原因有两种库编的时候没开PAHO_WITH_SSL或者链接时漏了-lssl -lcrypto。判断方法很简单nm -D /usr/local/lib/libpaho-mqtt3cs.so | grep SSL_connect有输出说明库里有 SSL 符号问题在链接参数没输出说明库本身就是明文版得回去重新cmake -DPAHO_WITH_SSLTRUE再装一遍。现象三程序编出来了运行时error while loading shared libraries: libpaho-mqtt3cs.so.1。原因是/usr/local/lib不在动态链接器的默认搜索路径里make install只是把文件放过去并没有注册。解决方式是写一个ld.so.conf.d下的配置文件再ldconfig或者临时用LD_LIBRARY_PATH/usr/local/lib ./demo_sub验证一下确认是这个原因之后再固化配置。临时方案只能救急别写进启动脚本里否则换个环境就失效。5.2 运行期握手与回调现象四MQTTClient_connect返回-1日志里没有任何有用信息。这是 SSL 场景最常见的表现-1把所有失败原因都吞了。按顺序排查先用openssl s_client -connect host:8883 -CAfile /etc/mqtt/certs/ca.crt -servername host确认 TLS 层能不能通这一步失败就是证书链或地址问题跟 paho 代码无关s_client通了但程序不通再看trustStore路径是否可读、enableServerCertAuth是否为 1、连接地址是不是ssl://前缀。我当时遇到的问题就是trustStore写成了相对路径程序在systemd下启动时工作目录是/文件根本读不到。现象五连上几秒就断日志里反复connection lost。原因多半在回调里。paho 的消息回调运行在库内部的网络线程上在回调里做耗时操作写数据库、同步 HTTP、大量日志刷盘会把这个线程卡住超过keepAliveInterval之后 broker 判定客户端失联并断开。解决方式是把回调里的活尽量减到最少只做数据拷贝丢进自己的队列由业务线程处理同时把keepAliveInterval设成 30 到 60 秒connectTimeout设成 10 秒左右别设成 5 秒这种激进值网络抖一下就重连。另外cleansession 1会让每次重连都丢订阅需要保留订阅状态的场景要设成 0 并配一个持久的持久化目录。6. 装完怎么验证三层验证一层都不能省装完就写业务代码是最大的隐患因为这时候出问题你分不清是环境、证书还是业务逻辑。我习惯按三层验证从下往上每一层都有明确的通过标准。第一层确认 TLS 本身能通用系统自带的openssl s_client这一层跟 paho 完全无关openssl s_client -connect broker.example.internal:8883 \ -CAfile /etc/mqtt/certs/ca.crt \ -servername broker.example.internal /dev/null看输出里三样东西Verify return code: 0 (ok)、协商出来的协议版本是 TLSv1.2 或以上、以及证书链里 CN/SAN 跟你连接的地址一致。任何一项不对先解决证书和 broker 配置不要往下走。这一步能过滤掉我遇到过的大部分“paho 连不上”的问题因为它们根本不是 paho 的问题。第二层确认 paho 的 SSL 代码路径和链接都正确用编译时带PAHO_BUILD_SAMPLESTRUE编出来的样例程序它不需要你写任何代码# 具体参数以你本地安装的样例帮助为准先看一遍 usage /usr/local/bin/paho_c_sub --help 21 | head -30 # 订阅一个测试主题观察是否稳定挂住而不是立刻退出 /usr/local/bin/paho_c_sub -h broker.example.internal -p 8883 -t demo/verify -q 1如果paho_c_sub能稳定挂住十分钟不断说明库、链接、SSL 配置这一串都是通的问题范围就缩小到你自己的代码了。如果样例也连不上回到第 5 章按返回码排查。第三层验证自己的程序和断线重连。启动程序往一个测试主题发一条消息同时在另一边订阅同一个主题确认能收到然后把 broker 停掉再拉起来观察客户端能否在retryInterval之后自动重连并恢复订阅。这一层最容易暴露cleansession和持久化配置的问题。验证项手段通过标准TLS 握手与证书链openssl s_clientVerify return code: 0paho 库与 SSL 配置paho_c_sub 样例稳定连接不主动断开业务收发自写程序 pub/sub消息内容完整、QoS 生效断线恢复重启 broker自动重连并恢复订阅从那以后我每次装这套环境都会先跑openssl s_client再编自己的代码哪怕多花两分钟也比在一个rc-1上耗一下午强。这套流程跑顺之后换机器、换 OpenSSL 版本、换容器基础镜像都只是重走一遍清单的事。希望帮到你。本文还有配套的精品资源点击获取