TDengine C++ 系列(5):C/C++ 连接器——连接管理与查询 API

核心目标 :掌握 taos.h 全貌,能正确管理连接生命周期,用同步/异步方式查询并处理结果集与错误。

前置知识:Part 1(3.x API 事实)、Part 4(查询 SQL);C++17 RAII 与智能指针。

验证环境:TDengine 3.4.x(3.4.1.6);taosc(taos.h);C++17;CMake;Docker(服务端)。


0. 本篇问题场景

Part 1 的最小闭环能跑了,但一上生产就暴露一串 C 接口的"地雷":

  • 服务运行几天后连接数打满,新请求全部失败------谁把连接 leak 了?
  • 查询结果遍历时类型转换写错,FLOAT 按 DOUBLE 读,数据全乱;
  • 一个多线程采集程序,几个线程共用一个连接,偶尔结果串了;
  • 长查询把请求线程卡死,想取消/超时却不知道怎么下手;
  • 想用异步方式发一批查询,回调里访问了已释放的对象,偶发崩溃。

这些问题的根源是:taosc 是一个 C 接口库,句柄与内存都要手动管理 。本篇把连接器 API 完整讲清楚,并给出一个类型安全的 RAII 封装(TdConn + TdResult),让生产代码不必直面裸 C 句柄。


1. 心智模型:句柄体系与生命周期

1.1 三个不透明句柄

taos.hTAOSTAOS_RESTAOS_STMT 都是 typedef void 的不透明指针,各自代表不同的资源:

句柄 含义 创建 释放
TAOS* 连接 taos_connect taos_close
TAOS_RES* 结果集 taos_query taos_free_result
TAOS_STMT* 预处理语句(Part 6) taos_stmt_init taos_stmt_close

纪律:谁创建谁释放;释放后句柄不可再用。RAII 封装的意义就是把这条纪律变成"编译期无法违反"。

1.2 进程级生命周期

复制代码
taos_init()         可选:进程级初始化(库加载/全局配置)
  └─ taos_connect() 创建连接(可多个)
       └─ 查询/写入...
  └─ taos_close()   关闭连接
taos_cleanup()      进程退出前清理全局资源(可选但推荐)
  • taos_init / taos_cleanup进程级的,通常进程启动/退出各调一次(或干脆不调,库会自动初始化);
  • taos_connect 每次建立一条 TCP(或 WebSocket)连接,有握手开销------高频创建连接是浪费,生产用连接池(第 4 节)。

1.3 连接方式:native vs WebSocket

维度 native(默认) WebSocket
端口 6030(直连 taosd) 6041(经 taosAdapter)
开启方式 默认 taos_options(TSDB_OPTION_DRIVER, "websocket"),且须在程序开头调用一次
特点 原生协议、性能最优 防火墙友好、与 REST 同口;适合受限网络
支持状态 C/C++/Python/Rust 官方长期支持 官方推荐方向(Java/C#/Go/ODBC 的 native 已弃用)

本系列正文默认 native;WebSocket 在 6.3 给接入方式。

1.4 线程安全边界(先记住结论)

  • 连接(TAOS*)可以多线程共享:官方支持同一连接被多个线程并发调用;
  • 结果集(TAOS_RES*)不能跨线程共享 :每个线程用自己 taos_query 得到的结果集,混用是未定义行为;
  • 预处理语句(TAOS_STMT*)建议单线程使用:绑定/批量状态是有序的,多线程共用会交错(Part 6 详述,推荐每线程独立 stmt)。

2. 最小可运行示例:类型安全的结果集遍历

2.1 裸 API 的典型查询循环

cpp 复制代码
// 1) 执行查询(3.x 直接返回结果集)
TAOS_RES* res = taos_query(conn, sql);
if (taos_errno(res) != 0) {                    // 必须检查,失败不返回 NULL
    fprintf(stderr, "查询失败: %s\n", taos_errstr(res));
    taos_free_result(res);
    return;
}

// 2) 取元数据
int n = taos_num_fields(res);
TAOS_FIELD* fields = taos_fetch_fields(res);   // {char name[65]; int8_t type; int32_t bytes;}

// 3) 逐行遍历(TAOS_ROW 是 void**,每列按类型 memcpy 解引用)
TAOS_ROW row;
while ((row = taos_fetch_row(res)) != nullptr) {
    for (int i = 0; i < n; ++i) {
        if (row[i] == nullptr) { /* NULL */ continue; }
        switch (fields[i].type) {
            case TSDB_DATA_TYPE_TIMESTAMP: {
                int64_t v; memcpy(&v, row[i], sizeof(v)); /* ... */ break;
            }
            case TSDB_DATA_TYPE_FLOAT: {
                float v; memcpy(&v, row[i], sizeof(v)); /* ... */ break;
            }
            // ... DOUBLE / INT / BIGINT / BINARY / NCHAR ...
        }
    }
}
taos_free_result(res);   // 4) 必须释放

裸写的问题:释放靠自觉、类型靠手写 switch、NULL 判断分散------正是生产事故的来源。下面封装。

2.2 封装 TdResult(ems-lab 的查询工具)

cpp 复制代码
// td_result.h(本系列 Part 5 引入,Part 4 的 Q1-Q5 都靠它跑)
class TdResult {
public:
    explicit TdResult(TAOS_RES* res) : res_(res) {}
    ~TdResult() { if (res_) taos_free_result(res_); }
    TdResult(const TdResult&) = delete;
    TdResult& operator=(const TdResult&) = delete;

    int num_fields() const { return taos_num_fields(res_); }
    const TAOS_FIELD* fields() const { return taos_fetch_fields(res_); }

    // 取下一行;false = 结束
    bool next() { row_ = taos_fetch_row(res_); return row_ != nullptr; }

    bool is_null(int col) const { return row_[col] == nullptr; }

    int64_t get_int64(int col) const {
        int64_t v; std::memcpy(&v, row_[col], sizeof(v)); return v;
    }
    double get_double(int col) const {
        double v; std::memcpy(&v, row_[col], sizeof(v)); return v;
    }
    const char* get_string(int col) const {
        return static_cast<const char*>(row_[col]);
    }
    // get_float / get_int / get_bool 同理,按 fields()[col].type 分发可再封装一层

private:
    TAOS_RES* res_;
    TAOS_ROW  row_ = nullptr;
};

配套 TdConn::query 返回裸 TAOS_RES*(Part 1 已有),调用方包一层 TdResult 即获得 RAII:

cpp 复制代码
TdConn conn(config_from_env());
conn.exec("USE ems");
TdResult res(conn.query(
    "SELECT tbname, _wstart, avg(power) FROM telemetry "
    "WHERE ts >= '2026-08-01 00:00:00.000' AND ts < '2026-08-01 01:00:00.000' "
    "PARTITION BY tbname INTERVAL(5m)"));
while (res.next()) {
    printf("%s\t%lld\t%.3f\n", res.get_string(0),
           (long long)res.get_int64(1), res.get_double(2));
}
// 析构自动 taos_free_result

3. 机制拆解:同步、异步、错误与取消

3.1 同步查询流程与内存

同步流程 = taos_query → 检查 errno → 遍历 → 释放。结果集数据默认全部缓冲在客户端内存taos_fetch_row 从缓冲取),所以:

  • 超大结果集(如全量导出)要考虑内存:用分页 LIMIT流式/块式读取
  • taos_fetch_block / taos_fetch_raw_block 按列块批量取数,适合大批量处理(性能好于逐行)。

3.2 异步查询:taos_query_a

cpp 复制代码
// 回调签名:void (*)(void* param, TAOS_RES* res, int code)
void query_cb(void* param, TAOS_RES* res, int code) {
    // code < 0 失败;== 0 无结果;> 0 受影响行数
    // 查询型 SQL 需要继续取数:内部再调 taos_fetch_rows_a(res, cb2, param)
    ...
}

taos_query_a(conn, sql, query_cb, param);

异步要点:

  • 回调运行在库内部线程,不要在里面做阻塞/耗时操作;
  • param 指向的对象生命周期要覆盖到回调结束------use-after-free 高发点(5.4 实验);
  • 结果集同样要 taos_free_result

3.3 错误码体系

获取方式 用途
taos_errno(res) 查询/结果错误码;连接失败时传 NULL
taos_errstr(res) 错误信息字符串;传 NULL 获取连接错误信息
taos_stmt_errstr(stmt) stmt 专用错误(Part 6)
TSDB_CODE_* 常量 错误码命名常量,如 TSDB_CODE_... 系列

常见错误类型与排查方向:

text 复制代码
连接失败(网络不可达/端口错)      → 检查 6030/6041、服务状态、防火墙
表不存在 / Database not specified → 检查 USE 与表名、库名
语法错误                          → 用 taos CLI 复跑同一条 SQL 定位
资源不足(内存/连接数打满)        → 检查连接泄漏、查询并发、BUFFER 配置
超时                              → 查询超时配置、语句扫描量

习惯 :所有 TdException 都带 code 与 SQL 上下文(Part 1 的封装已做),日志里能一眼定位。

3.4 超时与取消

  • 查询超时:服务端 queryTimeout 配置 + 客户端等待超时;
  • 主动取消:taos_query_cancel(conn)(结合异步使用,长查询超时中断)。

4. ems-lab 工程实战:查询服务封装

4.1 从裸 API 到三层封装

复制代码
TdResult(RAII 结果集 + 类型化取值)     ← 本篇
TdConn::query/exec(错误即异常)          ← Part 1
业务查询函数(Q1-Q5 类型化结果)           ← 各篇示例

4.2 连接池:别让生产代码裸奔

高频业务下每条查询都 taos_connect 不现实。最小连接池(Part 12 会演进出完整版)的原则:

  • 预建 N 条连接(默认 = 线程数);
  • 每条连接固定归属一个线程(天然规避共享);
  • 线程退出时 taos_close + 归还/销毁。
cpp 复制代码
// 概念版:线程局部连接(thread_local)
thread_local std::unique_ptr<TdConn> t_conn;
TdConn& conn() {
    if (!t_conn) t_conn = std::make_unique<TdConn>(config_from_env());
    return *t_conn;
}

连接数、vgroup 数与并发的关系在 Part 9/11 展开;这里先建立"每线程一条、用后归还"的正确形态。

4.3 用封装跑 Q1-Q5

把 Part 4 的核心查询集全部改为 TdResult 遍历,并加行数与值断言 (Q1 必须 288 行、Q2 量纲手算一致),作为 CI 回归。查询封装稳定后,Part 6-8 全部建立在它之上。


5. 失败实验与根因

5.1 连接泄漏:连接数打满

cpp 复制代码
for (int i = 0; i < 10000; ++i) {
    TAOS* c = taos_connect(...);   // 忘了 taos_close
    taos_query(c, "SELECT 1");
}
// 进程连接数持续增长 → 服务端拒绝新连接

现象:一段时间后新连接全部失败,SHOW DNODES / 连接监控打满。根因:每循环一条连接未释放。修法 :RAII(TdConn)或连接池;排查 :监控连接数曲线,配合 taosKeeper(Part 10)。

5.2 结果集未释放

cpp 复制代码
TAOS_RES* res = taos_query(conn, sql);
// 忘了 taos_free_result,循环 10 万次

现象:客户端内存持续增长,最终 OOM。根因:结果集在客户端缓冲未释放。修法TdResult 保证析构释放。

5.3 跨线程共享结果集

cpp 复制代码
// 线程 A 查询
TAOS_RES* res = taos_query(connA, sqlA);
// 把 res 交给线程 B 遍历 ------ ❌

现象:偶发数据错乱/崩溃。根因:TAOS_RES* 不能跨线程共享。修法 :谁查询谁遍历;跨线程传的是解析后的数据(复制到业务结构),不是句柄。

5.4 异步回调 use-after-free

cpp 复制代码
void cb(void* param, TAOS_RES* res, int code) {
    auto* ctx = static_cast<Ctx*>(param);   // 可能已析构!
}
// 发起异步查询后 ctx 被提前释放

现象:偶发崩溃,难以复现。根因:回调线程访问已析构对象。修法param 指向的生命周期覆盖到回调完成(用共享所有权/队列持有),回调内不阻塞。


6. 版本与环境差异

维度 说明
3.4.0 驱动兼容 社区版/企业版客户端驱动互不兼容,按发行版安装对应 taosc
native 弃用范围 Java/C#/Go/ODBC 的 native 连接已弃用(计划 2027-01-01 停用);C/C++ native 官方继续支持
taos_options 3.x 支持 TSDB_OPTION_DRIVER/TIMEZONE/CONFIGDIR 等;WebSocket 模式须程序开头调用一次
异步回调 回调线程模型各版本一致;taos_fetch_rows_a 名称与 2.x 不同
Windows 链接 taos.lib;注意 taos.h 的字符编码与 CLI 输出(UTF-8)

7. 测试与验收

7.1 自动化断言

  • 连接失败路径:错误端口抛 TdException(已有 test_conn 用例);
  • 查询断言:Q1 行数 = 288、Q2 量纲手算一致(用 TdResult 断言);
  • 内存:跑 test_conn 于 ASan 下无泄漏(连接、结果集、stmt 全覆盖)。

7.2 本篇验收清单

  • 能画出 taos 三个句柄(TAOS / TAOS_RES / TAOS_STMT)的生命周期;
  • 能说出 native(6030)与 WebSocket(6041)的差异与 C++ 的选型结论;
  • TdResult 封装完成,Q1-Q5 全部改为类型化遍历并加断言;
  • 复现 4 个失败实验(连接泄漏、结果集未释放、跨线程共享结果集、异步 use-after-free)并解释根因;
  • ASan 下 test_conn 无泄漏告警;
  • 知道线程安全边界:连接可共享、结果集不可、stmt 单线程。

8. 常见误区

误区 事实
"taos_query 失败返回 NULL,判空即可" 3.x 返回带错误的结果集,必须 taos_errno(res) 判断
"连接可以随便跨线程共享" 连接可共享;结果集不能;stmt 建议单线程
"异步回调很安全,随便访问外部对象" 回调在库内部线程,param 生命周期必须覆盖到回调完成
"结果集不释放也没事,进程会回收" 长生命周期进程内存持续增长,最终 OOM
"每次查询都新建连接,反正便宜" 握手有代价,生产必须连接池/线程局部连接
"错误码没用,看错误信息就行" code 用于程序化处理与监控,信息用于人读,两者都要保留

9. 本篇小结

连接器层是 C++ 项目里"裸 C 接口"与"生产代码"之间的桥,本篇的核心纪律:

  1. 句柄纪律:TAOS / TAOS_RES / TAOS_STMT 谁创建谁释放,RAII 封装把纪律变成约束;
  2. 生命周期 :进程级 init/cleanup、连接级 connect/close、结果级 query/free_result 三层清晰;
  3. 线程边界:连接可共享、结果集不可共享、stmt 单线程------写并发代码前先背下来;
  4. 错误与异步taos_errno/errstr 双通道取证;taos_query_a 的回调生命周期是 use-after-free 高发区;
  5. 查询封装TdResult 让 Part 4 的 Q1-Q5 变成可断言、可回归的代码。

至此,读路径(连接 + 查询)已经完整。下一步是性能主角:写路径------参数绑定批量写入,把"每秒写几十万点"变成现实。

10. 官方资料

相关推荐
MC皮蛋侠客1 小时前
TDengine C++ 系列(8):流式计算与最新值缓存——库内实时处理
c++·缓存·tdengine
天空'之城1 小时前
C 语言工业级通用组件手写 27:配置参数管理组件
c语言·eeprom·参数管理·工业级组件·掉电存储
YSL0701241 小时前
指针习题讲解
c语言
*.✧屠苏隐遥(ノ◕ヮ◕)ノ*.✧9 小时前
C++学习:基础知识的掌握
c++·visualstudio
wuyk55510 小时前
第2章:六步换相原理全解+STM32工程实战
c语言·开发语言·stm32·单片机·嵌入式硬件
C++ 老炮儿的技术栈15 小时前
从 Qt Designer 属性编辑器的层级可以看到继承链
c语言·数据库·c++·qt·sqlite·visual studio
new_zhou16 小时前
C++ 项目 AI 协作指南(Windows / MSVC 环境)
c++·人工智能·windows
水饺编程17 小时前
AT&T 汇编语言学习笔记开篇语
c语言·汇编·c++
Iruoyaoxh17 小时前
类和对象~
开发语言·c++