clickhouse-c 是一个面向 ClickHouse 原生协议的仅头文件 C 客户端。
其源代码以及各头文件的参考文档位于 GitHub 仓库。
与更高层的客户端不同,它是有意只提供尽可能少的封装。核心头文件会通过你提供的 I/O 回调,对 Native 格式的块进行解码和编码。套接字、TLS 上下文、内存分配器、重试机制以及连接池都由你自行管理。因此它足够小,便于嵌入:仅包含 clickhouse.h 即可,除 libc 外不会引入任何链接时依赖项。
该库仍在积极开发中。v1 可解码 ClickHouse 的核心类型。
如有局限或缺失的功能,请通过 issue tracker 反馈。
但也请理解,该库有意在设计上省略了部分功能。
该库不做什么
- HTTP 协议。直接封装 libcurl 以使用 HTTP interface。
- DNS 解析、端点故障转移、连接池、重试和退避。
- TLS 上下文生命周期。OpenSSL 后端使用的是你已建立连接的
SSL。 - 线程。每个
chc_client按设计都是单线程的。 - 库内部的异步 I/O。阻塞式 client 会同步调用
chc_io.read。如果需要一个 自身不执行任何 I/O 的事件循环 client,请使用 ioless client。
库的组织方式
clickhouse-c 以一组扁平的头文件形式发布。每个头文件同时包含声明和实现,
并由哨兵宏保护。请按构建需要选择相应的头文件。
| Header | Purpose | Link flags |
|---|---|---|
clickhouse.h | Core:类型、错误、内存分配器、I/O vtable、类型名解析器、块读取器和写入器 | — |
clickhouse-client.h | TCP 数据包循环:Hello、Query、Data、EndOfStream、Exception、Progress、Pong | — |
clickhouse-async.h | 无 I/O 客户端:由调用方提交字节驱动的相同数据包循环,不使用套接字 | — |
clickhouse-compression.h | 压缩帧布局、CityHash128、编解码器分发,以及 LZ4/ZSTD 适配器 | -llz4 -lzstd |
clickhouse-posix-io.h | 基于阻塞 read(2)/write(2) 的 I/O 后端 | — |
clickhouse-openssl.h | 基于 SSL_read/SSL_write 的 I/O 后端 | -lssl -lcrypto |
必需的服务器设置
将其添加到你的项目中
CHC_IMPLEMENTATION 并包含实现;
其他所有编译单元都只包含相同的头文件,以获得声明。
clickhouse.h 之前定义 CHC_PROVIDE_STDLIB_ALLOC,即可使用 chc_alloc_stdlib。
为 clickhouse-compression.h 定义 CHC_NO_LZ4 或 CHC_NO_ZSTD,即可去除对 lz4/zstd 的依赖项。
通过 TCP 连接
chc_io,然后将其传给
chc_client_init,它会同步执行 Hello 握手。该库不负责 DNS、
故障转移、重连或连接池化——这些都需要由调用方处理。
chc_client 都是单线程的,并封装一个连接。库会同步调用 chc_io
回调;至于这些回调在底层具体使用什么机制 (epoll、io_uring、
WaitLatchOrSocket) ,则由你决定。
运行查询
CHC_PKT_END_OF_STREAM。使用 chc_client_send_query_ex 可
附带所需的服务器级设置;而基础版 chc_client_send_query 会发送一个
空的设置列表,并继承服务器的默认设置。
CHC_PKT_EXCEPTION 数据包的形式到达,而不是作为
chc_client_recv_packet 的非 OK 返回值。只有传输层故障才会返回非 OK。结果中的第一个 CHC_PKT_DATA
数据包是一个描述 schema 且包含零行的头部块;后面跟着的是数据块。
chc_packet_clear 会释放该数据包的块或异常——如果要改为自行接管其所有权,请先将数据包中的这些字段设为 NULL。
读取列数据
chc_column_layout 返回的物理布局,
你需要根据它进行分派处理;其声明类型来自 chc_block_column_type。复合布局可以嵌套,因此
读取 Nullable(Array(String)) 意味着先拆开 Nullable,遍历数组偏移量,然后
切分字符串数据。
| 布局 | 访问器 |
|---|---|
CHC_COL_FIXED | chc_column_fixed_data(c, &elem_size) — n_rows * elem_size 个小端字节序字节 |
CHC_COL_STRING | chc_column_string_data(c), chc_column_string_offsets(c) — offsets[i] 是第 i 行在主机字节序中的结束位置 (不含) ;第 0 行从 0 开始 |
CHC_COL_NULLABLE | chc_column_null_map(c) (每行一个字节,1 = NULL) ,chc_column_nullable_inner(c) |
CHC_COL_ARRAY | chc_column_array_offsets(c) (累计结束位置) ,chc_column_array_values(c);Map 解码为 Array(Tuple(K, V)) |
CHC_COL_TUPLE | chc_column_tuple_arity(c), chc_column_tuple_child(c, i) — 每个子列都具有相同的行数 |
CHC_COL_LOW_CARDINALITY | chc_column_lc_key_size(c) (1/2/4/8) ,chc_column_lc_keys(c),chc_column_lc_dict(c);字典槽位 0 是默认值 |
CHC_COL_FIXED 数据在线上传输时采用 小端字节序;在 big-endian 主机上,你需要自行对多字节整数进行字节交换。
偏移量和 LowCardinality 键在解码时就已经转换为主机字节序。
UUID 由两个 小端字节序 的 UInt64 半部分组成,IPv4 是一个 4 字节的 小端字节序 整数,而 IPv6 采用 network byte order。
DateTime64 tick 以 UTC 为准——类型中的 timezone 仅是元数据。
从不受信任的对端摄取数据时,在遍历每一列之前都要调用 chc_column_validate 进行校验。
chc_block_read 不会验证跨字段约束,例如数组偏移量和 LowCardinality 键,因此伪造的块可能会导致读取越过内部列的边界。
插入数据
chc_block_builder 构建一个块,然后将其交给 chc_client_send_data。该构建器记录的是
指针而不是复制数据,因此各列的 slab 必须在发送完成前一直有效。一次 INSERT 会先发送查询,
等待服务器的头部块,再发送一个或多个数据块,最后发送一个空块来
终止 stream。
chc_block_builder_append_fixed 接受 n_rows * elem_size 个小端字节序字节;
chc_block_builder_append_string 接受基于主机字节序、作用于紧凑 slab 的累计结束偏移量 (不包含终点) 。让 builder 经由 chc_client_send_data 而不是更底层的
chc_block_write 发送,可使 client 根据协商后的 revision 设置块选项并应用
压缩。
压缩
chc_client_opts 中传入压缩模式和一个已填充的 编解码器。客户端会解压传入的
Data 包,并压缩传出的数据包。压缩请求头内置了 LZ4 和 ZSTD 适配器;
每个 init 只会填充各自的槽位,因此若要同时支持两者,请把它们都调用一次。
chc_codec;
其 vtable 在 clickhouse-compression.h 中声明。
TLS
clickhouse-openssl.h 基于 SSL_read/SSL_write 提供了一个 chc_io backend。OpenSSL 需要由你自行驱动:
该库不会创建 SSL_CTX、验证证书、设置 SNI,或调用 SSL_connect /
SSL_shutdown。在 chc_io.read 触发之前,握手必须已经完成。
check_cancel 回调,该回调会在两次读取之间轮询;并且可通过 chc_openssl_io_set_deadline / chc_posix_io_set_deadline 设置读取截止时间。
Ioless (异步) 客户端
clickhouse-async.h 是面向事件循环的 TCP 客户端 ioless 变体。它完全不会操作
套接字:你提交已接收的字节,取出它需要发送的字节,并自行驱动 epoll、
io_uring 或 WaitLatchOrSocket。其选项、数据包类型和块构建器与阻塞式客户端
相同。
chc_async_client_init 不执行 I/O,也不会阻塞。之后的握手会以可恢复的
状态机方式运行,每次发送和接收也都是如此。当解析超出你已提交的字节时,
调用会返回 CHC_WOULD_BLOCK,而不是发生阻塞——提交更多入站字节后再次调用,
parser 会从块中间继续解析。
pump 会在两个方向上传输字节。对于 Outbound,chc_async_pending_out 会返回一个指向已排队字节的指针及其长度;当套接字接收了其中一部分后,用该数量调用 chc_async_consume_out,部分写入也是可以的。对于 Inbound,将从套接字读取的数据传给 chc_async_submit。发送操作永远不会阻塞,也不会施加背压,因此要留意待发送数据的长度,并在其增长过大时停止继续发送。
一个可用的 liburing driver 位于
test/test_async_uring.c。
内存与分配器
chc_alloc vtable,因此内存分配会遵循宿主所采用的机制。
clickhouse.h 之前定义 CHC_PROVIDE_STDLIB_ALLOC,并调用 chc_alloc_stdlib() 以使用
基于标准 malloc 的分配器。
错误和服务器异常
CHC_OK (0) 或非零的 CHC_ERR_* 代码。该代码即返回值;由
调用方在栈上分配的 chc_err 保存便于人类阅读的错误消息。该库绝不会在堆上分配
错误对象。
chc_err 失败。它们会以
CHC_PKT_EXCEPTION 的形式在数据包 stream 中传输,并携带 server 的 code、display_text 和 stack_trace。请仅将
chc_err 检查用于传输、协议和解码失败。
支持的数据类型
Int8–Int256,UInt8–UInt256Float32,Float64,BFloat16BoolDecimal32,Decimal64,Decimal128,Decimal256Date,Date32,DateTime,DateTime64,Time,Time64String,FixedString(N)UUID,IPv4,IPv6Enum8,Enum16Nullable(T),Array(T),Tuple(...),Map(K, V),Nested(...)LowCardinality(T)IntervalQBit(...)Point,Ring,Polygon,MultiPolygonSimpleAggregateFunction(f, T),解码后为其内部的TJSON和Object('json'),在字符串序列化下会被解码为String列 (见下文)
output_format_native_write_json_as_string=1 时,JSON 和 Object('json') 才会被解码。
每一行都会以 CHC_COL_STRING 列中的一个 JSON 文档形式传入,因此可由字符串访问器读取;
构建器则使用 chc_block_builder_append_json_string 写入相同的形态。任何其他 JSON
序列化版本都会返回 CHC_ERR_TYPE,并注明该设置。
Variant、Dynamic、AggregateFunction 目前尚不能解码,返回 CHC_ERR_TYPE;
可作为回退方案,在服务端将其 CAST 为 String。