TDengine C/C++ 连接器进阶指南

本文档面向已经熟练使用 TDengine 的开发者,聚焦 C/C++ 连接器的高阶用法与工程化落地建议:如何选择连接方式、如何把写入/查询/订阅跑到"更稳、更快、更可运维",以及一些常见的坑与规避方式。

前置条件

连接策略与部署形态

WebSocket vs 原生连接:优先级怎么定

C/C++ 连接器支持 WebSocket 连接与原生连接,两者的关键差异在基础文档中已有说明。

  • 优先推荐 WebSocket:更容易跨版本/跨环境部署,且性能接近原生连接。
  • 需要"完整能力/强版本一致"时选原生:例如需要使用 WebSocket 暂不支持的少数接口(基础文档中已列出),或你希望严格版本匹配。

:::warning

无论选择哪种方式,都要牢记:taos_options(TSDB_OPTION_DRIVER, "websocket") 必须在程序开始时调用,且只能调用一次;设置后对进程全局生效。

:::

WebSocket 多端点负载均衡与故障切换(推荐)

当你使用 taosAdapter 提供的 WebSocket 能力时,可以通过 taos_connect_with() + OPTIONSadapterList 配置多个端点,实现连接级别的负载均衡与故障切换。

c 复制代码
#include "taos.h"

static TAOS *connect_ws_with_adapters(void) {
  OPTIONS options = {0};

  // 多端点:host:port 用英文逗号分隔
  taos_set_option(&options, "adapterList", "adapter1:6041,adapter2:6041,adapter3:6041");

  // 鉴权与默认 DB
  taos_set_option(&options, "user", "root");
  taos_set_option(&options, "pass", "taosdata");
  taos_set_option(&options, "db", "power");

  // 连接重试与退避(WebSocket 专有配置)
  taos_set_option(&options, "connRetries", "5");
  taos_set_option(&options, "retryBackoffMs", "200");
  taos_set_option(&options, "retryBackoffMaxMs", "2000");

  // 可选:启用压缩(按需开启)
  taos_set_option(&options, "compression", "1");

  return taos_connect_with(&options);
}

:::note

  • adapterList 的优先级高于 ip
  • WebSocket 默认端口为 6041(基础文档的 OPTIONS 参数表中已说明)。
    :::

WebSocket TLS 与证书校验

如果你在公网/跨机房环境使用 WebSocket,建议显式评估 TLS 相关配置(wsTlsModewsTlsVersionwsTlsCa)。

  • wsTlsMode 支持从"禁用/不校验证书/校验证书但不校验主机名/校验证书且校验主机名"等多档;
  • wsTlsCa 可传 CA 文件路径或 PEM 内容,用于验证服务端证书。

具体参数与取值含义请以基础文档的 OPTIONS 参数表为准。

连接可观测性:标记 userIp/userApp/connectorInfo

在多服务、多租户或需要审计/排障的场景,建议为连接设置识别信息:

  • taos_options_connection(..., TSDB_OPTION_CONNECTION_USER_IP, ...)
  • taos_options_connection(..., TSDB_OPTION_CONNECTION_USER_APP, ...)
  • taos_options_connection(..., TSDB_OPTION_CONNECTION_CONNECTOR_INFO, ...)

这样在服务端日志与审计链路里更容易定位请求来源。

线程模型与连接池(稳定性优先)

基础文档已经给出关键提示:

  • 推荐每线程独立连接,或基于线程的连接池
  • 不推荐在多个线程间共享同一个 TAOS*,以避免连接内状态(例如 USE)互相干扰
  • 同步 API 执行过程中,避免使用类似 pthread_cancel 的强制终止方式,以免引发死锁等不可预期问题

工程建议:

  1. TAOS* 的生命周期绑定到线程(线程本地存储或显式池化)。
  2. 需要跨线程传递任务时,只传 SQL/参数,不传 TAOS*,在工作线程里拿自己的连接执行。
  3. 连接池要能"丢弃坏连接":一旦出现不可恢复错误(例如连接断开且重试失败),直接销毁并重建连接。

错误处理与资源管理:最常见的坑

统一的错误检查范式

  • taos_query():不能用返回值是否为 NULL 判断成功与否,必须检查 taos_errno(res)
  • taos_schemaless_insert*():某些情况下可能返回 NULL,仍可通过 taos_errno(NULL) 获取错误码
  • tmq_consumer_poll():返回 NULL 既可能是"超时无数据",也可能是"出错",应结合 taos_errno(NULL) 判断
c 复制代码
TAOS_RES *res = taos_query(taos, "select server_version()");
int32_t code = taos_errno(res);
if (code != 0) {
  // res 可能为 NULL,也可能非 NULL
  fprintf(stderr, "TDengine error=%d, msg=%s\n", code, taos_errstr(res));
}

taos_free_result(res);

资源释放顺序与泄漏防线

  • 查询/写入返回的 TAOS_RES*:用完必须 taos_free_result()
  • 连接 TAOS*:不再使用时 taos_close()
  • 进程退出:可调用 taos_cleanup() 清理运行环境

:::warning
taos_free_result() 之后不要再使用该结果集调用 taos_fetch_row() 等接口,否则可能崩溃。

:::

C++ RAII:把"必须释放"变成"自动释放"

在 C++ 工程里,建议用 RAII 封装 TAOS* / TAOS_RES* 的释放逻辑,减少异常路径/早返回导致的泄漏。

cpp 复制代码
#include <memory>
#include "taos.h"

struct TaosConnDeleter {
  void operator()(TAOS *p) const {
    if (p) taos_close(p);
  }
};

struct TaosResDeleter {
  void operator()(TAOS_RES *p) const {
    if (p) taos_free_result(p);
  }
};

using TaosConnPtr = std::unique_ptr<TAOS, TaosConnDeleter>;
using TaosResPtr  = std::unique_ptr<TAOS_RES, TaosResDeleter>;

static TaosResPtr query(TAOS *taos, const char *sql) {
  return TaosResPtr(taos_query(taos, sql));
}

高性能查询:大结果集与批量拉取

优先使用 taos_fetch_block() 做批量拉取

当结果集很大时,逐行 taos_fetch_row() 的函数调用开销会更明显。可以优先使用 taos_fetch_block() 批量拉取,再在应用侧遍历行。

c 复制代码
TAOS_RES *res = taos_query(taos, "select * from meters limit 10000");
if (taos_errno(res) != 0) {
  fprintf(stderr, "query failed: %s\n", taos_errstr(res));
  taos_free_result(res);
  return;
}

int num_fields = taos_num_fields(res);
TAOS_FIELD *fields = taos_fetch_fields(res);

TAOS_ROW rows = NULL;
int n = 0;
char buf[1024];

while ((n = taos_fetch_block(res, &rows)) > 0) {
  for (int i = 0; i < n; i++) {
    // rows[i] 是一行,字段类型由 fields 描述
    taos_print_row(buf, rows[i], fields, num_fields);
    // 业务处理:写日志/解析字段/转对象
  }
}

taos_free_result(res);

关注时间戳精度

  • 结果集时间戳精度可用 taos_result_precision(res) 判断(毫秒/微秒/纳秒)
  • 在做时间序列对齐、跨系统写回或序列化时,建议把精度显式带上,避免"看似正确但偏了 1000 倍"的问题

高性能写入:SQL vs STMT2 vs Schemaless

选型建议

  • SQL 直接写入:最简单;适合吞吐不敏感、写入规模不大或调试场景。
  • STMT2(参数绑定):通常是高吞吐写入的首选,避免重复 SQL 解析开销,并支持批量绑定。
  • Schemaless:适合 schema 经常变化、或者希望"写入即建表/自动演进"的场景;也适合做多源协议接入(Line Protocol/Telnet/JSON)。

STMT2 的关键复用点:Prepare 一次,Bind/Exec 多次

基础文档已经给出 STMT2 的典型流程:

  1. taos_stmt2_init()
  2. taos_stmt2_prepare()
  3. 多次 taos_stmt2_bind_param() + taos_stmt2_exec()
  4. taos_stmt2_close()

工程上最重要的是:

  • 同一条 SQL,尽量复用 prepare 结果(只换绑定数据,避免反复解析)。
  • 批次大小要"按行大小"调优:每行很大时批次不要太大;每行很小可适当加大批次。
  • 出错后建议重建 stmt2 上下文 :基础文档已明确"不建议继续在当前环境上下文下工作",可通过 taos_stmt2_error() 查看原因。

STMT2 高效写入模式与异步执行

taos_stmt2_init()TAOS_STMT2_OPTION 支持:

  • 高效写入模式:将 singleStbInsertsingleTableBindOnce 设为 true
  • 异步执行:设置回调函数 asyncExecFn 与回调参数 userdata

:::warning

启用异步执行时,务必确保绑定数据涉及的内存(表名、tags、列值缓冲区等)在回调触发前仍然有效;不要把栈上临时变量地址直接作为绑定数据长期使用。

:::

Schemaless:用 raw/ttl/reqid 扩展接口提升工程可控性

在 Schemaless 进阶使用中,常见诉求包括:

  • 批量更大、更省拷贝 :使用 _raw 版本,按 lines + len 传递整块缓冲区
  • 链路追踪 :使用 _with_reqid 版本带上 reqid
  • 自动建表 TTL :使用 _ttl 版本控制自动建表的生存时间
c 复制代码
#include "taos.h"
#include <stdint.h>
#include <stdio.h>

static void schemaless_insert_raw_demo(TAOS *taos) {
  // 示例:多行 Line Protocol 拼接成一段缓冲区
  // 注意:这里用 \n 分隔多行;真实工程里建议用更高效的缓冲区构造方式
  char lines[] =
      "meters,location=beijing groupid=1 current=10.1,voltage=220i32,phase=0.33 1700000000000\n"
      "meters,location=beijing groupid=1 current=10.2,voltage=221i32,phase=0.34 1700000001000\n";

  int32_t totalRows = 0;
  int64_t reqid = 123456789;

  TAOS_RES *res = taos_schemaless_insert_raw_with_reqid(
      taos,
      lines,
      (int)sizeof(lines) - 1,
      &totalRows,
      TSDB_SML_LINE_PROTOCOL,
      TSDB_SML_TIMESTAMP_MILLI_SECONDS,
      reqid);

  if (taos_errno(res) != 0) {
    fprintf(stderr, "schemaless failed: %s\n", taos_errstr(res));
  } else {
    printf("schemaless ok, totalRows=%d\n", totalRows);
  }

  taos_free_result(res);
}

TMQ(数据订阅)进阶:消费语义、提交与消息类型

关键约束与建议

  • tmq_consumer_poll()每个消费者只能单线程调用(基础文档已说明)。
  • "处理-提交"顺序决定语义:
    • 至少一次(at-least-once) :处理成功后再 tmq_commit_sync()
    • 至多一次(at-most-once):先提交再处理(可能丢数据,谨慎)
  • 订阅延迟较大或需要消费离线数据时,务必确保数据库的 WAL 保留策略满足需求(例如 WAL 的额外保留时长/大小相关配置)。

识别消息类型:数据/元数据/自动建表

tmq_get_res_type(msg) 会返回 TMQ_RES_DATA/TMQ_RES_TABLE_META/TMQ_RES_METADATA 等类型。

  • 只关心数据行:仅处理 TMQ_RES_DATA
  • 需要感知表结构变化/自动建表:同时处理元数据类型

推荐消费循环(带手动提交)

下面示例基于快速上手里的配置项(group.idtd.connect.usertd.connect.passauto.offset.reset),增加了:

  • 消息类型判断
  • 处理后提交 offset
  • 释放消息结果集
c 复制代码
#include "taos.h"
#include <stdio.h>

static void consume_loop(void) {
  tmq_conf_t *conf = tmq_conf_new();
  tmq_conf_set(conf, "group.id", "group1");
  tmq_conf_set(conf, "td.connect.user", "root");
  tmq_conf_set(conf, "td.connect.pass", "taosdata");
  tmq_conf_set(conf, "auto.offset.reset", "earliest");

  char errstr[512] = {0};
  tmq_t *tmq = tmq_consumer_new(conf, errstr, (int32_t)sizeof(errstr));
  tmq_conf_destroy(conf);

  if (!tmq) {
    fprintf(stderr, "tmq_consumer_new failed: %s\n", errstr);
    return;
  }

  tmq_list_t *topics = tmq_list_new();
  tmq_list_append(topics, "topic_meters");
  if (tmq_subscribe(tmq, topics) != 0) {
    fprintf(stderr, "tmq_subscribe failed\n");
    tmq_list_destroy(topics);
    tmq_consumer_close(tmq);
    return;
  }
  tmq_list_destroy(topics);

  for (;;) {
    TAOS_RES *msg = tmq_consumer_poll(tmq, 1000);
    if (!msg) {
      // 可能是超时无数据,也可能出错:按需结合 taos_errno(NULL) 做监控/告警
      continue;
    }

    tmq_res_t tp = tmq_get_res_type(msg);
    if (tp == TMQ_RES_DATA) {
      // 消费数据:msg 的读取方式与 taos_query 返回的结果集一致
      TAOS_ROW row;
      while ((row = taos_fetch_row(msg))) {
        // 业务处理...
      }

      // 处理成功后提交(至少一次语义)
      int32_t rc = tmq_commit_sync(tmq, msg);
      if (rc != 0) {
        fprintf(stderr, "tmq_commit_sync failed: %s\n", tmq_err2str(rc));
      }
    } else {
      // 元数据/自动建表等类型:按需处理或忽略
      // 你也可以通过 tmq_get_table_name/tmq_get_topic_name 等接口做路由
    }

    taos_free_result(msg);
  }

  // 实际工程里需要有退出条件
  // tmq_unsubscribe(tmq);
  // tmq_consumer_close(tmq);
}

:::warning

上例中省略了退出条件与信号处理;实际工程建议引入"可中断的循环 + 有序退出"(先 unsubscribe,再 close)。

:::


关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。

相关推荐
不惑_1 小时前
通俗理解经典CNN架构:VGGNet
人工智能·神经网络·cnn
没学上了1 小时前
MNIST
人工智能
fpcc1 小时前
跟我学C++中级篇——Linux中文件和链接及重定向
linux·c++
audyxiao0012 小时前
人工智能顶级期刊PR论文解读|HCRT:基于相关性感知区域的混合网络,用于DCE-MRI图像中的乳腺肿瘤分割
网络·人工智能·智慧医疗·肿瘤分割
零售ERP菜鸟2 小时前
IT价值证明:从“成本中心”到“增长引擎”的确定性度量
大数据·人工智能·职场和发展·创业创新·学习方法·业界资讯
Fcy6482 小时前
C++ set&&map的模拟实现
开发语言·c++·stl
叫我:松哥2 小时前
基于大数据和深度学习的智能空气质量监测与预测平台,采用Spark数据预处理,利用TensorFlow构建LSTM深度学习模型
大数据·python·深度学习·机器学习·spark·flask·lstm
童话名剑3 小时前
目标检测(吴恩达深度学习笔记)
人工智能·目标检测·滑动窗口·目标定位·yolo算法·特征点检测
木卫四科技3 小时前
【木卫四 CES 2026】观察:融合智能体与联邦数据湖的安全数据运营成为趋势
人工智能·安全·汽车
你怎么知道我是队长8 小时前
C语言---枚举变量
c语言·开发语言