核心目标 :掌握
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.h 里 TAOS、TAOS_RES、TAOS_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 接口"与"生产代码"之间的桥,本篇的核心纪律:
- 句柄纪律:TAOS / TAOS_RES / TAOS_STMT 谁创建谁释放,RAII 封装把纪律变成约束;
- 生命周期 :进程级
init/cleanup、连接级connect/close、结果级query/free_result三层清晰; - 线程边界:连接可共享、结果集不可共享、stmt 单线程------写并发代码前先背下来;
- 错误与异步 :
taos_errno/errstr双通道取证;taos_query_a的回调生命周期是 use-after-free 高发区; - 查询封装 :
TdResult让 Part 4 的 Q1-Q5 变成可断言、可回归的代码。
至此,读路径(连接 + 查询)已经完整。下一步是性能主角:写路径------参数绑定批量写入,把"每秒写几十万点"变成现实。