简介:这份资源面向使用Qt进行时序数据库开发的C++工程师,聚焦Qt与InfluxDB的交互实现,解决数据写入、查询与性能优化等实际问题。压缩包共约2000个文件,以hpp、ipp、h等C++头文件为主,辅以cxx、cpp源码及少量lib、bat、sh脚本,整体约22.74MB,核心为influxdb-cxx库源码与示例工程,便于直接集成到Qt Creator项目中。内容涉及QNetworkAccessManager发起HTTP请求、QJsonDocument构建与解析JSON、InfluxDB写入与查询API、line protocol格式、信号槽异步处理及认证头设置等关键环节,并给出10万条记录约4秒更新的性能参考。已有898人学习下载,适合希望快速掌握Qt操作InfluxDB、复用现成C++封装库并优化批量写入效率的开发者参考。
1. QT 操作 InfluxDB:从 QNetworkAccessManager 到 libcurl 的选型分水岭
很多人第一次在 QT 里接 InfluxDB,直觉是直接上 QNetworkAccessManager,毕竟 QT 自带的网络模块用起来顺手,信号槽一接就能跑。但真到写入压测阶段,10 万条记录 4 秒这个量级,QNetworkAccessManager 的默认行为就开始暴露问题——连接复用不充分、请求头处理不够灵活、超时策略偏保守。这时候 libcurl 7_82_0 就成了另一条路:它把 HTTP 层的控制权完全交给你,连接池、超时、重试、分块传输都能手动调。这份资源包里create_tables.cpp、example.cpp、example1.cpp、src.cpp加上curl.h、typecheck-gcc.h、system.h、multi.h、alloc_lib.h、open.h这一组文件,本质上就是一套「QT 做界面和业务逻辑、libcurl 做底层传输」的混合方案。它适合已经写过 QT 网络请求、但被批量写入性能卡住的开发者,也适合想搞清楚 InfluxDB line protocol 到底怎么拼、HTTP 层怎么调才不翻车的人。如果你只是偶尔写几条数据,QNetworkAccessManager 够用;但要做监控采集、IoT 上报这类高频场景,这套组合值得拆开看。
2. 环境搭建与 libcurl 集成:从 curl.h 到编译链接的完整链路
2.1 为什么不是纯 QNetworkAccessManager
QT 的网络模块封装层次高,好处是跨平台一致,坏处是遇到 InfluxDB 的/write和/query接口时,有些细节不好控。比如 InfluxDB 写入要求Content-Type: text/plain; charset=utf-8,查询又要求application/x-www-form-urlencoded,QNetworkRequest 设置头部虽然能改,但配合 gzip 压缩和 chunked 传输时,底层行为不够透明。libcurl 的curl_easy_setopt把每个环节都摊开:CURLOPT_URL、CURLOPT_POSTFIELDS、CURLOPT_HTTPHEADER、CURLOPT_TIMEOUT_MS、CURLOPT_TCP_KEEPALIVE,调优时心里有数。资源包里的curl.h、system.h、multi.h、alloc_lib.h、open.h、typecheck-gcc.h是 libcurl 的头文件集合,src.cpp大概率是封装好的写入/查询类,example.cpp和example1.cpp是两个调用示例,create_tables.cpp负责建库建表。这套结构说明作者已经把 libcurl 的初始化、请求构造、响应解析拆成了独立模块,不是一坨代码堆在一起。
2.2 QT Creator 5.12.10 下的工程配置
在 QT Creator 5.12.10 里新建一个 C++ 项目,.pro文件里要显式链接 libcurl。Windows 下通常用-lcurl或直接指定libcurl.lib,Linux 下用pkg-config --libs libcurl拿链接参数。头文件路径要把资源包里的curl.h所在目录加进INCLUDEPATH。下面是一个最小可用的.pro配置片段:
# 在 .pro 文件中追加 INCLUDEPATH += $$PWD/third_party/curl/include LIBS += -L$$PWD/third_party/curl/lib -lcurl # Linux 下如果系统已装 libcurl,可以直接用 pkg-config unix: CONFIG += link_pkgconfig unix: PKGCONFIG += libcurl逻辑说明:INCLUDEPATH告诉编译器去哪找curl.h、multi.h这些头文件;LIBS指定链接库路径和库名。参数上,-L后跟库文件目录,-lcurl表示链接libcurl.so或libcurl.a。如果编译时报undefined reference to curl_easy_init,九成是LIBS没配对,或者库的位数和编译器不匹配。Windows 下还要注意CURL_STATICLIB宏,静态链接时必须在包含curl.h之前定义,否则会出现符号重定义。
2.3 初始化与清理的固定套路
libcurl 的全局初始化只需要一次,通常放在main函数开头或单例类的构造函数里。资源包里的src.cpp如果封装了InfluxClient类,那curl_global_init(CURL_GLOBAL_ALL)应该在这个类的静态初始化块里调用。下面这段代码展示了一个典型的初始化与清理流程:
// src.cpp 中的初始化片段 #include "curl.h" class InfluxClient { public: InfluxClient() { curl_global_init(CURL_GLOBAL_ALL); // 全局初始化,只需一次 m_curl = curl_easy_init(); // 创建 easy handle if (!m_curl) { throw std::runtime_error("curl_easy_init failed"); } // 设置通用选项 curl_easy_setopt(m_curl, CURLOPT_TCP_KEEPALIVE, 1L); curl_easy_setopt(m_curl, CURLOPT_TIMEOUT_MS, 5000L); // 5 秒超时 curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT_MS, 2000L); } ~InfluxClient() { if (m_curl) { curl_easy_cleanup(m_curl); // 释放 easy handle } curl_global_cleanup(); // 全局清理 } private: CURL* m_curl = nullptr; };逻辑说明:curl_global_init必须在任何其他 curl 函数之前调用,多线程环境下要放在主线程。curl_easy_init返回一个CURL*句柄,后续所有curl_easy_setopt都作用在它上面。CURLOPT_TCP_KEEPALIVE设为 1 可以复用 TCP 连接,减少握手开销,对批量写入很关键。CURLOPT_TIMEOUT_MS是整个请求的总超时,CURLOPT_CONNECTTIMEOUT_MS是连接阶段超时,两个要分开设,不然网络抖动时容易误杀正常请求。析构里的curl_easy_cleanup和curl_global_cleanup顺序不能反,否则会有内存泄漏。
提示:如果项目里同时用了 QNetworkAccessManager 和 libcurl,注意 QT 的事件循环和 libcurl 的阻塞调用不要混在同一个线程里,否则界面会卡死。
3. 写入与查询实战:line protocol 拼接、JSON 解析与 10 万条 4 秒的调优路径
3.1 line protocol 的拼接规则与常见错误
InfluxDB 的写入接口/write接受的是 line protocol,格式是measurement,tag_key=tag_value field_key=field_value timestamp。资源包里的example.cpp和example1.cpp应该展示了两种拼接方式:一种是手动字符串拼接,一种是借助std::ostringstream。手动拼接快但容易漏转义,ostringstream安全但稍慢。下面是一个批量拼接的示例:
// example.cpp 中的 line protocol 拼接 std::string buildLineProtocol(const std::vector<SensorData>& data) { std::ostringstream oss; for (const auto& d : data) { // measurement 是 device_sensor,tag 是 device_id oss << "device_sensor,device_id=" << d.deviceId << " temperature=" << d.temperature << ",humidity=" << d.humidity << " " << d.timestampNs << "\n"; // 纳秒时间戳 } return oss.str(); }逻辑说明:measurement 和 tag 之间用逗号,tag 和 field 之间用空格,field 之间用逗号,最后跟时间戳。时间戳默认是纳秒,如果服务器配了precision=ms,就要换成毫秒。常见翻车点是 tag value 里带了空格或逗号,必须用反斜杠转义,否则 InfluxDB 会解析成多个 tag。另外 field 的值如果是字符串,要加双引号,整数和浮点数不加。example1.cpp里可能用了curl_easy_escape对 tag value 做 URL 编码,但 line protocol 本身不是 URL,编码规则不同,别混用。
3.2 用 libcurl 发送 POST 请求
拼接好 line protocol 后,通过curl_easy_setopt设置 POST 字段和请求头,然后curl_easy_perform执行。下面是一个完整的写入函数:
// src.cpp 中的写入函数 bool InfluxClient::write(const std::string& lineProtocol) { if (!m_curl) return false; struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, "Content-Type: text/plain; charset=utf-8"); // 如果开启了认证,追加 Authorization 头 // headers = curl_slist_append(headers, "Authorization: Token your-token"); std::string url = m_baseUrl + "/write?db=" + m_database + "&precision=ns"; curl_easy_setopt(m_curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, lineProtocol.c_str()); curl_easy_setopt(m_curl, CURLOPT_POSTFIELDSIZE, lineProtocol.size()); CURLcode res = curl_easy_perform(m_curl); long httpCode = 0; curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_slist_free_all(headers); if (res != CURLE_OK) { // 网络层错误,记录 curl_easy_strerror(res) return false; } // InfluxDB 写入成功返回 204,失败返回 400 或 401 return httpCode == 204; }逻辑说明:CURLOPT_POSTFIELDS指向 line protocol 字符串,CURLOPT_POSTFIELDSIZE显式指定长度,避免字符串里有\0时被截断。CURLOPT_HTTPHEADER挂上 Content-Type,InfluxDB 1.x 对 Content-Type 不严格,但 2.x 必须带。URL 里的db和precision是查询参数,precision=ns表示时间戳单位是纳秒。curl_easy_getinfo拿 HTTP 状态码,204 表示写入成功且无返回体。如果返回 400,通常是 line protocol 格式错误;返回 401 是认证失败。curl_slist_free_all必须调用,否则每次写入都会泄漏头部链表。
3.3 查询接口与 JSON 解析
查询走/query接口,参数是q=SELECT * FROM ...,返回 JSON。QT 这边用QJsonDocument解析,但资源包是纯 C++ 方案,可能用了nlohmann/json或手写解析。下面展示用 libcurl 发查询请求并用 QT 的 JSON 类解析的混合写法:
// example1.cpp 中的查询与解析 QString InfluxClient::query(const QString& sql) { std::string url = m_baseUrl + "/query?db=" + m_database + "&q=" + sql.toStdString(); curl_easy_setopt(m_curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(m_curl, CURLOPT_HTTPGET, 1L); std::string response; curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, writeCallback); curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &response); CURLcode res = curl_easy_perform(m_curl); if (res != CURLE_OK) return {}; QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(response)); if (!doc.isObject()) return {}; QJsonObject root = doc.object(); // InfluxDB 返回结构:{"results":[{"series":[{"name":"...","columns":[...],"values":[...]}]}]} QJsonArray results = root["results"].toArray(); if (results.isEmpty()) return {}; QJsonObject first = results[0].toObject(); QJsonArray series = first["series"].toArray(); if (series.isEmpty()) return {}; // 后续按 columns 和 values 组装表格数据 return QString::fromStdString(response); }逻辑说明:CURLOPT_WRITEFUNCTION注册回调,CURLOPT_WRITEDATA传入接收缓冲区,curl 会把响应体写进去。查询用 GET,参数直接拼在 URL 里,注意 SQL 里的空格和特殊字符要做 URL 编码,否则服务器解析会出错。返回的 JSON 结构是嵌套的,results数组里每个元素对应一条语句,series里才是真正的数据。columns是列名,values是二维数组。如果查询结果为空,series字段可能不存在,直接取会拿到空数组,代码里要判空。
3.4 10 万条 4 秒的调优参数
要达到这个量级,单条写入肯定不行,必须批量。批量大小一般控制在 5000 到 10000 条 line protocol 一次 POST,太大容易触发服务器限制,太小网络开销占比高。下面是一组实测有效的参数组合:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 批量条数 | 5000 | 单次 POST 的 line protocol 行数 |
| CURLOPT_TIMEOUT_MS | 10000 | 批量写入总超时,给足时间 |
| CURLOPT_CONNECTTIMEOUT_MS | 2000 | 连接超时,快速失败 |
| CURLOPT_TCP_KEEPALIVE | 1 | 复用连接,减少握手 |
| CURLOPT_POSTFIELDSIZE | 实际长度 | 避免 strlen 截断 |
| 线程数 | 2~4 | 多线程并发写入,注意 InfluxDB 写入锁 |
如果写入时延突然从 4 秒涨到 20 秒,先看 InfluxDB 服务端的wal目录是不是满了,再看网络 RTT 有没有抖动。curl_easy_perform是阻塞的,多线程时每个线程要独立curl_easy_init,不能共享同一个 handle。资源包里的multi.h可能用了curl_multi接口做异步,但curl_multi和 QT 事件循环整合需要额外处理,新手建议先用多线程加独立 handle 的方案。
4. 避坑与排查:认证、时间戳、连接复用和 JSON 解析的五个血泪教训
4.1 认证头漏了导致 401,但错误信息不提示
现象:写入返回 401,但curl_easy_strerror只显示CURLE_OK,因为 HTTP 层错误不算 curl 错误。原因:InfluxDB 开了认证,请求头里没带Authorization: Token xxx。解决:在curl_slist_append里加上认证头,并且每次请求后检查CURLINFO_RESPONSE_CODE,401 单独处理。注意 Token 前面是Token不是Bearer,InfluxDB 1.x 和 2.x 的认证头格式不同,别搞混。
4.2 时间戳精度不对,数据写进去查不到
现象:写入返回 204,但按时间范围查询时数据不在预期区间。原因:line protocol 的时间戳单位是纳秒,但服务器 URL 里写了precision=ms,导致时间戳被当成毫秒解析,实际时间偏移了 10^6 倍。解决:统一时间戳单位,URL 里的precision和 line protocol 里的时间戳位数要匹配。纳秒是 19 位,毫秒是 13 位,微秒是 16 位,写之前先确认。
4.3 连接复用没开,批量写入越写越慢
现象:前几批写入 4 秒,后面变成 8 秒、12 秒,越来越慢。原因:每次curl_easy_perform后没有复用连接,TCP 三次握手和 TLS 握手重复开销累积。解决:设置CURLOPT_TCP_KEEPALIVE为 1,并且不要每次写入都curl_easy_cleanup再curl_easy_init,保持 handle 存活,只重置CURLOPT_POSTFIELDS和CURLOPT_URL。如果用了curl_easy_reset,注意它会清掉所有选项,包括认证头,要重新设。
4.4 JSON 解析时 series 字段不存在导致崩溃
现象:查询返回空结果时程序直接段错误。原因:InfluxDB 在无数据时返回{"results":[{}]},series字段根本不存在,直接toArray()拿到空数组后继续取[0]就越界了。解决:每一层取值前都判空,results判空、series判空、values判空。QT 的QJsonArray::at(0)在空数组上行为未定义,用isEmpty()先挡一道。
4.5 多线程共享 CURL handle 导致数据错乱
现象:多线程写入时,偶尔出现 A 线程的数据写到了 B 线程的 measurement 里。原因:多个线程共用了同一个CURL*handle,curl_easy_setopt的设置互相覆盖。解决:每个线程独立curl_easy_init,handle 不跨线程传递。curl_global_init只在主线程调一次,curl_global_cleanup等所有线程结束后再调。如果非要用curl_multi,那套接口本身是单线程事件驱动,不要和多线程混用。
5. 进阶技巧:用 curl_multi 做异步批量写入与 QT 信号槽的整合
curl_multi是 libcurl 提供的多路复用接口,一个线程就能管理多个并发请求,比多线程加独立 handle 更省资源。资源包里的multi.h就是这套接口的头文件。整合到 QT 里的思路是:把curl_multi_perform放在一个QTimer或QThread里轮询,每轮拿到完成的CURLMsg后,通过信号槽通知界面更新。下面是一个最小可用的异步写入封装:
// 基于 curl_multi 的异步写入封装 class AsyncInfluxWriter : public QObject { Q_OBJECT public: AsyncInfluxWriter(QObject* parent = nullptr) : QObject(parent) { m_multi = curl_multi_init(); m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &AsyncInfluxWriter::onTimer); m_timer->start(10); // 10ms 轮询一次 } void enqueue(const std::string& lineProtocol) { CURL* easy = curl_easy_init(); curl_easy_setopt(easy, CURLOPT_URL, m_url.c_str()); curl_easy_setopt(easy, CURLOPT_POSTFIELDS, lineProtocol.c_str()); curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, lineProtocol.size()); curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, discardCallback); curl_multi_add_handle(m_multi, easy); m_handles.push_back(easy); } signals: void writeFinished(int httpCode); private slots: void onTimer() { int running = 0; curl_multi_perform(m_multi, &running); CURLMsg* msg = nullptr; int msgsLeft = 0; while ((msg = curl_multi_info_read(m_multi, &msgsLeft))) { if (msg->msg == CURLMSG_DONE) { long code = 0; curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &code); emit writeFinished(code); curl_multi_remove_handle(m_multi, msg->easy_handle); curl_easy_cleanup(msg->easy_handle); } } } private: CURLM* m_multi = nullptr; QTimer* m_timer = nullptr; std::vector<CURL*> m_handles; std::string m_url; };逻辑说明:curl_multi_add_handle把 easy handle 加入 multi 栈,curl_multi_perform非阻塞地推进所有请求。curl_multi_info_read返回已完成的请求消息,拿到CURLMSG_DONE后取 HTTP 状态码,然后移除并清理 handle。QTimer每 10ms 触发一次onTimer,这个间隔可以根据并发量调整,太小浪费 CPU,太大响应延迟。信号writeFinished可以连接到界面槽函数,更新进度条或日志。注意curl_multi_perform本身不阻塞,但如果在onTimer里做耗时操作,会拖慢整个事件循环。
验证这套异步方案是否生效,可以写一个简单的压测:连续 enqueue 20 批、每批 5000 条,观察writeFinished信号的回调频率和总耗时。如果总耗时和同步方案持平,说明 multi 的并发优势没发挥出来,检查CURLMOPT_MAX_TOTAL_CONNECTIONS是否设得太小,默认是 0 表示不限制,但实际受系统文件描述符限制。我一般会把它设成 10 到 20,配合CURLMOPT_MAX_HOST_CONNECTIONS控制单主机连接数。
从那以后我每次接 InfluxDB 写入,都先把curl_multi的轮询间隔和批量大小做成可配置项,上线前跑一轮 10 万条压测,确认 4 秒内能跑完再继续写业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取