Skip to main content
原生协议是 ClickHouse 客户端和服务器通过 TCP 进行通信时使用的二进制、面向连接的协议。它承载 SQL 查询、结果数据、INSERT 载荷、执行遥测以及错误信号。它也是命令行客户端、C++ 以及大多数第三方原生驱动所使用的底层协议。 本页介绍协议本身:数据包分帧、连接状态机、版本协商,以及每一种非 Block 消息的消息体。Data 家族数据包中的字节 (即 Block、其列以及各类型的编码) 属于另一部分内容,记录在 Native Format 规范中。
配套规范本页是这组成对规范中的一部分,与配套的 Native Format 规范一同发布。两份规范分工明确:本页负责数据包和传输层;Native Format 规范负责 Data 家族数据包内部的字节。
该协议始终具有以下几个特性:它是二进制的,并且按位置解析;除 BlockInfo 内部外,没有字段标签,因此只要有一个字节错位,后续所有内容都会失去同步。它是有状态的,并且每个 TCP 连接一次只处理一个查询——不存在多路复用。定长整数采用小端序。

概览

在线上传输的每条消息都以一个 VarUInt 数据包类型编码开头,后面跟着一个消息体,其形态取决于该编码以及协商出的协议版本。 一个连接会经历三个阶段——先进行一次性握手,然后进行任意次数的 PingQuery 交换,最后关闭: 原生 TCP 协议始终以 Native 格式传输表格数据,而不会理会 SQL 中任何 FORMAT 子句。重新格式化为 RowBinaryCSVJSON 等格式是客户端的工作,这一步会在其解码 Native 块之后完成。 (HTTP interface 则是另一条代码路径,确实会遵循 FORMAT 子句;这里不作讨论。)

安全

传输安全 (TLS)

TLS 位于传输层,处于协议层之下。启用后,整个 TCP 数据流都会被加密;而无论是否使用 TLS,协议消息在字节级别上都完全一致。

身份验证

身份验证在握手阶段进行,也就是在 ClientHello 消息中完成。userpassword 字段会以明文字符串形式传输,因此需要依靠传输层加密 (TLS) 来保护传输中的凭据。 从协议版本 54466 开始,支持 SSH 质询-响应身份验证——请参阅 SSH 质询-响应身份验证

服务器间密钥

对于分布式查询执行,服务器之间会通过证明自己知晓某个共享密钥来相互进行身份验证,而无需在传输过程中直接发送该密钥。每个 Query 都会在 Query 的字段 4 中携带一个 32 字节的 SHA-256 auth_hash,它根据 salt、nonce、已配置的密钥以及查询内容计算得出,接收服务器会重新计算并进行比对。此功能受 INTERSERVER_SECRET 功能开关 (v54441) 控制。外部客户端在此处始终发送空字符串。请参阅服务器间身份验证

版本控制与功能开关

版本协商

客户端和服务端都会在握手期间声明其支持的最高协议版本。协商后的版本取两者中较小者:
此后的每条消息都会使用协商出的版本来决定传输时包含哪些字段。

功能开关

每项功能都以引入它的协议版本作为标识;当协商出的版本大于或等于该版本号时,该功能即处于生效状态。
当某项功能处于生效状态时,其字段必须出现在传输数据中。该协议严格按位置解析,因此如果省略受功能开关控制的字段,就会破坏其后每个字段对应的字节流。

功能列表

数据包封装

在线上传输的每条消息,无论哪个方向,其外层结构都相同:
完整的数据包类型表见数据包类型参考 数据包类型是 VarUInt,而不是定宽字节。对于小于 128 的值,VarUInt 产生的仍然是相同的单个字节,但实现必须使用 VarUInt 编码,以确保当未来的数据包类型达到 128 或更大时仍能保持兼容。 消息参考仅说明每个数据包的 包体 —— 即位于数据包类型代码之后的字节。字段编号从 1 开始,包体中的第一个字段编号为 1。

分块帧封装 (v54470+)

CHUNKED_PROTOCOL 功能协商完成后 (参见握手) ,线路上传输的每个数据包都会使用分块帧进行封装。这种封装是按方向分别进行的:client→server 和 server→client 会分别协商,最终可能采用不同的模式 (分块或无帧封装) 。 每个数据包在线路上的布局:
每个 chunk 的线格式:
VarUInt 数据包类型位于分块流 内部:它是数据包载荷的第一个字节 (即第一个 chunk 的第一个字节) ,而不是在分帧之前单独提前发送的一个字节。每个数据包的 chunk 载荷都是来自数据包封装的完整 [VarUInt packet_type_code][message body]。如果客户端把数据包类型放在分块流之外,对端就会把这个类型字节当作 u32 chunk 大小的第一个字节来读取,导致连接失去同步。 如果写入端的缓冲区在数据包中途写满,单个数据包可以拆分到多个 chunk 中;拆分点可以出现在任何位置,包括数据包类型的 VarUInt 内部。读取端会拼接各个 chunk 载荷,并将末尾的 4 字节零值视为透明的数据包边界——它会将其消费掉,但不会把它暴露给负责读取数据包消息体的逻辑。 没有消息体的数据包仍然会被封装:像 PingPong 这样的单字节数据包,在协商启用分块后会变成 [u32 size = 1][0x04][u32 0]。本页其他地方任何“在线路上是单字节”的描述,指的都是分块前的形式。 协商。 ServerHello 和 Addendum 各自携带两个 String 字段,每个方向一个,取值来自 {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}
  • chunked / notchunked 是严格模式:该方向要求必须精确使用该模式。
  • _optional 变体是灵活的:它们接受对端选择的任意模式。
每个方向的最终协商值按双方成对计算: 在客户端一侧,客户端的 SEND 偏好会与服务端的 RECV 偏好协商,反之亦然。 时序。 这些协商字符串通过未分帧的线路传输:ClientHello → ServerHello (服务端偏好) → Addendum (客户端的协商结果值) 。分帧模式切换适用于 Addendum 被刷出 之后 发送的每一个字节。Addendum 本身、ClientHello 和 ServerHello 始终不分帧。

连接生命周期

在任何时刻,连接都只会处于以下四种状态之一:HANDSHAKEREADYREADING_RESPONSE,或已终止。由于该协议不支持多路复用,如果客户端在尚未读取完上一个响应之前就发送新请求,就会导致传输中的字节交错,从而破坏数据流。

状态

顺畅路径沿直线向下推进——HANDSHAKE → READY → READING_RESPONSE → READY——其中 Ping/Pong 会形成自循环,而所有失败分支最终都会汇入唯一的 Terminated 终态。

握手阶段

进行身份验证并协商协议版本。每个连接只会发生一次,并且先于任何其他操作。 TCP 连接刚刚建立,双方尚未交换任何消息。流程如下:
  1. 客户端发送 ClientHello,其中包含其支持的最高协议版本。
  2. 客户端读取响应,并根据数据包类型进行分发处理:
  3. 如果 negotiated_version ≥ 54458 (ADDENDUM 功能) ,客户端会发送一个 Addendum。这一决定基于协商后的版本,而不是客户端声明的版本。
成功时,连接会进入 READY;发生任何错误时,连接都会终止。

Ping 阶段

一种应用层的存活检查,独立于 TCP keepalive。成功完成一次 Ping/Pong 往返即可确认 TCP 连接在两个方向上都保持存活,并且服务器能够正常响应。Ping 是无状态的,与任何查询都不关联,因此多个连续的 Ping 彼此独立。 READY 开始,流程如下:
  1. 客户端发送 Ping
  2. 客户端读取响应:

查询阶段

客户端提交一条 SQL 语句;服务器以流式方式返回结果块和执行遥测信息。响应由一系列数据包组成,并且恰好以一个 EndOfStreamException 结束。 READY 开始,流程如下: 如果在任意阶段发生错误,服务端会发送 Exception 而不是 EndOfStream,从而终止查询。
  1. 客户端发送带有唯一 query_id (通常为 UUID) 的 Query
  2. 客户端发送所有外部表,然后发送空的 Data 标记。空 Data 数据包的字段为 table_name = ""num_columns = 0num_rows = 0。服务端在收到此标记之前不会开始执行查询。
  3. 客户端进入 READING_RESPONSE,并刷写其写入缓冲区。
  4. 客户端在循环中读取响应数据包,并按类型分发处理:
收到 EndOfStream 或已处理的 Exception 后,连接会返回 READY。如果发生协议违规或 I/O 错误,连接会被终止。
num_rows == 0 这种情况很容易让新实现踩坑。零行块是边界标记或 schema 头,而不是流结束信号。只有 EndOfStreamException 才会结束响应。

INSERT 阶段

INSERT 阶段是在查询阶段的基础上增加了两次额外的交互。客户端提交一条 INSERT 语句;服务器返回一个描述目标表的 schema 块;客户端随后以流式方式发送包含这些行的 Data packets,再发送空的 Data 标记;最后,服务器以 EndOfStreamException 结束。 READY 状态开始,SQL 采用如下形式的 INSERTINSERT INTO <table> [(<cols>)] VALUES —— 不包含内联的 VALUES (...) 字面量,因为行数据是通过 Data packets 传输的。流程如下:
  1. 客户端发送 查询,并将 body 设为 INSERT SQL。
  2. 客户端发送所有外部表 (这种情况在 INSERT 中较少见) 。与 查询 phase 不同,这里不会发送空的 Data 标记。INSERT查询 数据包会连同待发送的数据一起发出,因此表示数据结束的空数据块会推迟到步骤 5;如果在 schema 块之前发送它,服务器会将其视为行流结束,从而以 0 行完成 INSERT,随后再把第一个真实的行数据包解析为一个游离的顶层数据包。
  3. 客户端持续读取元数据包 (TableColumns、Progress、ProfileInfo、Log、ProfileEvents) ,直到读到 schema Data 数据包——这是一个 0 行但包含完整列结构 (名称和类型) 的 Block。schema 块就是约定:客户端接下来发送的行必须符合这些列的形态。
  4. 客户端发送一个或多个数据块。对于每个块,它都会先写入 VarUInt(ClientPacket::Data = 2),然后写入表示空外部表名称的 String(""),接着写入 Block。列类型必须按位置与 schema 块中的列对齐。
  5. 客户端发送输入结束标记:一个带空 Block (0 列、0 行) 的 Data 数据包。
  6. 客户端持续读取响应流,直到 EndOfStream (成功) 或 Exception (失败) 。
异步 INSERT (v54484+) 。 当查询带有 async_insert = 1 时,服务器会将这些行放入队列,并作为某个批次的一部分进行刷写。在协商版本 ≥ 54484 (PROGRESS_IN_ASYNC_INSERT) 时,一旦刷写完成,服务器会额外发出一个 Progress 数据包,紧接着发送该次 insert 的 ProfileEvents,然后是 EndOfStream。在 54484 以下,服务器会跳过这个尾部的 Progress。该数据包是一个普通的 Progress;由于服务器在合并写入计数前会重置查询管道,因此其中的增量实际上只包含已用时间,而写入行数和字节统计则通过随附的 ProfileEvents 传递给客户端。对于已经在步骤 6 中处理交错 Progress 的客户端,只需再接受一个额外的数据包即可。 连接在收到 EndOfStream 或已处理的 Exception 后会返回 READY。协议违规和 I/O 错误会终止连接。

消息参考

各字段按 wire 顺序列出。Type 列使用:
  • VarUInt — 可变长度无符号整数 (参见 VarUInt) 。
  • String — 以 VarUInt 为前缀的字节序列 (参见 String) 。
  • UInt8Int32 等 — 固定宽度的小端序整数。
  • Bool — 单个字节,0x000x01
Role 列说明每个字段由谁使用:
  • client — 由外部客户端设置。
  • inter-server — 仅对服务器之间的通信有意义;外部客户端写入默认值。
  • universal — 两者都会使用。
这些表仅记录每个数据包的包体,即位于数据包类型代码之后的部分。

ClientHello (数据包类型 0)

客户端 → 服务端。TCP 连接建立后发送的第一条消息。

ServerHello (packet type 0)

Server → Client。对 ClientHello 在身份验证成功后的响应。 Rulepassword_complexity_rules 中的一个元素: 该列表反映 server operator 配置的密码策略,仅起提示作用——server 不会在握手期间强制执行这些规则。提供密码修改/设置功能的客户端可利用这些规则,在将不合规密码发送给 server 之前先提示错误。
为限制恶意或配置错误的 server 导致的 resource 消耗,请将解码后的 count 上限设为 256 个 entries,并将每个 patternmessage String 的上限设为 4096 字节。对于未配置密码策略的 server,count0 (后面没有任何成对项) 是常见情况。

附加信息 (无数据包类型)

客户端 → 服务器,受 ADDENDUM (v54458) 控制。在握手交换完成后立即发送。它不是一种独立的数据包类型——这些字段会以原始形式直接在传输中发送,前面不带数据包类型字节前缀。 分块帧格式的切换会在此附加信息写出后生效——附加信息本身不带帧封装。

Ping (数据包类型 4)

客户端 → 服务器。无消息体——在分块成帧之前,该数据包仅为单个字节 0x04;协商启用分块后,该字节会成为一个块的单字节载荷 (参见 分块成帧) 。

Pong (数据包类型 4)

服务器 → 客户端。无消息体——在采用分块帧之前,该数据包仅为单个字节 0x04;协商启用分块传输后,该字节会作为某个分块的单字节载荷 (参见分块成帧) 。

Exception (数据包类型 2)

服务器 → 客户端。当服务器在任意阶段发生错误时发送。

查询 (数据包类型 1)

客户端 → 服务器。

ClientInfo (嵌入在 查询 中)

客户端 → 服务器,嵌入在 查询 体 (字段 2) 中。受 CLIENT_INFO (v54032) 控制。 (ClientInfo 中的某些字段受更高版本控制,详见下方各字段说明。)
依赖 interface 的布局 (字段 7–12) 上面的字段 7–12 属于 TCP 分支。当 query_interface (字段 6) 不是 TCP 时,这些字段会被替换为另一种 wire 布局——并不只是可选省略,因此解码器必须根据字段 6 进行分支处理。
  • query_interface = 2 (HTTP) :此时写入的是由 server 转发的 HTTP request 信息——http_method (UInt8) 、http_user_agent (String) ,然后是 forwarded_for (String,受 X_FORWARDED_FOR_IN_CLIENT_INFO v54443 控制) 和 http_referer (String,受 REFERER_IN_CLIENT_INFO v54447 控制) 。此时不存在 os_user/client_hostname/client_name/version_*/protocol_version 这些字段。
  • 任何其他 interface:既不写入任何 TCP 字段 (7–12) ,也不写入任何 HTTP 字段;stream 会直接继续写入 quota_key
经过这个分支后,布局会重新合流:对所有 interface,后面都会跟着 quota_key (字段 13) 和 distributed_depth (字段 14) ;随后仅对 TCP 写入 version_patch (字段 15) 。这个分支主要影响 inter-server 流量,即发起方 server 转发原本通过 HTTP 到达的查询时。如果解码器始终按 TCP 字段读取,就会误读这类数据包——把 http_methodhttp_user_agent 当作 quota_key
OpenTelemetry 编码 (字段 16) :

服务器间身份验证

Query 的第 4 个字段 (auth_hash) 不是在线路上传输的共享集群密钥。发送原始密钥不仅会导致身份验证失败,还会泄露密钥。相反,作为服务器间客户端的服务端会使用加盐的 SHA-256 哈希来证明自己知道该密钥:
  1. 进入服务器间模式。 发起连接的服务端会在 ClientHello 中表明这一点:user 字段是服务器间标记,password 为空。然后,它会在同一个 ClientHello 数据包中,紧接 user/password 字段之后再附加两个字符串——cluster 名称,以及一个新生成的 32 字节 salt (随机值的 encodeSHA256) 。服务端会在发送 ServerHello 之前读取这两个字符串,因此客户端必须预先写入它们;如果先等待 ServerHello,就会发生死锁,因为服务端会阻塞并等待读取这两个字符串。
  2. 获取 nonce。 当协商了 INTERSERVER_SECRET_V2 (v54462) 时,ServerHello 会携带一个 8 字节的 UInt64 nonce。
  3. 计算哈希。 对于每个非 InitialQuery 的 Query 数据包,客户端会将 encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles) 写入第 4 个字段——即一个 32 字节摘要。 (nonce 是其十进制字符串形式,仅在协商版本 ≥ v54462 时存在;external_roles 仅在协商了 INTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472) 时附加。) 对于 InitialQuery,或者在未配置集群密钥时,客户端则会写入空字符串。
  4. 验证。 服务端会以 32 字节上限读取第 4 个字段,并使用自己持有的集群密钥副本重新计算相同的拼接内容;如果摘要不同,则会拒绝该连接。
外部 (非服务器间) 客户端永远不会进入此模式,并且始终发送空的 auth_hash

设置

以内联方式编码在 Query body 的 settings 列表中 (Query 数据包的第 3 个字段) 。无论协商出的版本是什么,该列表都始终存在,并以一个 key 为空的 Setting 结尾——即单个 VarUInt 0,后面不再跟任何 flags 或 value。只有单个 setting 的编码方式取决于协商版本,并受 SETTINGS_SERIALIZED_AS_STRINGS (v54429) 控制。 v54429+ (STRINGS_WITH_FLAGS) — 每个 setting 都是如下所示的三元组: key 为空时,字段 2 和 3 不存在。 Pre-54429 (BINARY) — 每个 setting 的编码形式为 [String key][特定类型的二进制值]不会写入 flags 字段,value 也会按该 setting 的原生二进制形式编码 (例如定宽整数或带长度前缀的字符串) ,而不是编码为十进制/文本字符串。该列表仍以空 key 结尾。以低于 54429 的协商版本为目标的 client,必须读写这种二进制形式,而不是上面的三元组。 (自定义 setting 属于例外:无论哪种编码,它们始终都带有 flags 和字符串 value。) flags 字段包含:
  • 0x01Important:该 setting 会影响查询结果,旧版 peer 不得静默忽略它。
  • 0x02Custom:用户定义的自定义 setting。
  • 0x0c — 一个 2 位层级 字段,而非独立 flag:0x00 = Production,0x04 = Obsolete,0x08 = Experimental,0x0c = Beta。应读取完整 2 位 (flags & 0x0c) ——如果简单测试 flags & 0x04,会把 Beta (0x0c) 误判为 Obsolete。
  • 0x80HotReload (无需重启即可重载 config;在 flags 枚举中定义,主要见于协调 settings) 。

参数

查询参数,用于参数化查询,例如 SELECT {x:UInt64}。其编码方式与设置了 Custom 标志 (0x02) 的 Setting 完全相同,并同样以空 key 作为结束标记。
参数值应是该值的 SQL 表示形式,而不是原始字面量。String 类型的参数在传递时必须预先用单引号括起来 (例如,{name:String} 的值应为 'Alice',而不是 Alice) ;否则服务器的值解析器会将其拒绝。

Data (数据包类型 1 server→client,数据包类型 2 client→server)

两个方向均使用此类型。它承载结果块、INSERT 数据、外部表以及数据结束标记。 传输格式是对称的——两个方向都会在块前包含一个 table_name 前缀。只有数据包类型字节不同。
数据结束标记是指 Block 为空的数据包——即 0 列和 0 行——与 table_name 的值无关。只有当解码后的块为空 (block.empty()) 时,服务器才会将客户端的 Data 数据包视为终止符;带有 table_name = "" 且块非空的数据包只是普通的行数据包,并非终止符。因此,INSERT 行流由一系列非空 Data 块组成,最后以一个空的 Data 块结束。 有关块的各种变体及其含义,请参见 块变体

Progress (数据包类型 3)

服务器 → 客户端。在查询执行期间定期发送。所有字段均为 VarUInt,并且每个数据包携带的都是自上一个 Progress 数据包以来的增量,而非累计总数。发送前,服务器会读取其计数器,并以原子方式将其重置为零,同时将 elapsed_ns 计算为自上次发送以来的时间差。因此,客户端必须在本地累加后续收到的数据包,才能得到持续更新的总数——如果把某个数据包当作绝对值,一旦收到多个数据包,进度显示就会回跳或少计。

ProfileInfo (数据包类型 6)

服务器 → 客户端。每个查询发送一次,通常在执行接近结束时发送。

Totals (数据包类型 7)

服务器 → 客户端。对于包含 WITH TOTALS 的查询,会发送此数据包。其传输格式与 Data 完全一致:一个 table_name 字符串 (始终为空) ,后面跟着一个块。不同之处仅在于数据包类型字节。

极值 (数据包类型 8)

服务器 → 客户端。在启用 extremes 设置时发送。传输格式与 Data 完全相同。该块恰好包含 2 行:第 0 行保存每一列的最小值,第 1 行保存每一列的最大值。

日志 (数据包类型 10)

服务器 → 客户端。当查询存在活动的日志队列时,会发送此数据包 (由 send_logs_level 设置控制;参见日志流式传输) 。 其封装和消息体格式与 Data 相同。该块的 num_columns = 8 为固定值,并具有预定义的 schema。每条日志记录对应一行,分布在全部 8 列中;单个日志数据包可携带多行。
这 8 列的顺序必须严格如下:

ProfileEvents (数据包类型 14)

Server → Client。携带每个查询的性能计数器。 其封套和 body 格式与 Data 相同。该块的 num_columns = 6 为固定值,并具有预定义的 schema。每个事件对应一行。
这 6 列:
value 列的元素类型在不同数据包之间并不是固定的——旧版服务器会输出 UInt64,新版则会输出 Int64。应从块头读取该列的类型字符串,而不要假定其位宽固定不变。

TableColumns (数据包类型 11)

Server → Client,由 COLUMN_DEFAULTS_METADATA (v54410) 控制。server 会在 INSERT schema 块之前发送该数据包,用于携带列默认值元数据,但仅当协商版本 ≥ 54410 启用了 input_format_defaults_for_omitted_fields setting 时才会发送。低于 54410 时,该数据包绝不会发送,因此较旧的 client 不得 等待它——schema Data 块会直接到来。v54410+ 的 client 应准备好处理任意一种顺序:先收到可选的 TableColumns,然后是 schema 块。
v54481+ 中的压缩 body当协商版本 ≥ 54481 (COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS) 时,server 会通过同一条可选压缩的输出路径写入这两个字段,因此当查询设置了 compression = true 时,整个 TableColumns body (external_table + columns_description) 都位于 compression frame 内;client 会通过对应的解压流读取它。当查询未启用压缩时,body 会完全按上表所示,以未压缩形式直接在 wire 上传输。这一点对 INSERT schema 响应尤为重要:如果 client 仅对 LogProfileEvents 切换压缩处理,而未对 TableColumns 做同样处理,那么在启用查询压缩时就会误读响应。

TimezoneUpdate (数据包类型 17)

Server → Client,由 TIMEZONE_UPDATES (v54464) 控制。它只在一个地方发送:input 表函数的初始化器中 (即形如 INSERT INTO <table> SELECT ... FROM input('<structure>') 的查询,会从客户端流式传输行) 。服务器发送输入 schema 的 Data 块后 (见INSERT 阶段) ,会立即发出 TimezoneUpdate,携带查询上下文当前的 session_timezone,以便客户端用相同的时区解析接下来要发送的行。对于查询执行过程中任意的 SET session_timezone 变更,服务器不会发送此数据包;它也不会用这个数据包告诉客户端如何格式化后续返回的结果块。 该数据包只会到达一次:紧接在输入 schema 块之后,且在客户端开始发送行块之前。即使解码器忽略 TimezoneUpdate,也必须继续读取后面的 String,以保持线协议对齐。

SSH 质询-响应身份验证 (packet types 11, 12, 18)

SSH_AUTHENTICATION (v54466) 控制,且默认不启用,需显式选择启用。当 ClientHello 发送 user = " SSH KEY AUTHENTICATION " + <real_user> (包含前导和尾随空格) 以及 password = "" 时,连接会进入 SSH 流程。服务器会读取此前缀,将其剥离以还原真实用户名,然后切换到质询-响应模式。 此流程会替代密码身份验证,并且质询-响应交换发生在 ServerHello 之前——服务器会推迟发送 Hello 回复,直到身份验证成功:
  1. 客户端发送带有 SSH 标记前缀且密码为空的 ClientHello。
  2. 客户端发送 SSHChallengeRequest (packet 11) 。此时服务器 尚未发送 ServerHello——它会先处理身份验证,并在此阻塞等待该 packet。
  3. 服务器回复 SSHChallenge,携带随机字节 (packet 18) 。
  4. 客户端构建待签名字符串,并对该字符串进行签名,而不是对原始 challenge 进行签名,然后发送携带签名的 SSHChallengeResponse (packet 12) 。签名消息是以下四个部分按字节拼接的结果,不带任何分隔符,并且严格按以下顺序排列:
  5. 服务器使用该用户已注册的 public key 验证签名,并重建相同的 decimal(protocol_version) + default_database + user + challenge 字符串。成功后,它会发送 ServerHello——与密码流程中的回复相同——随后 handshake 将正常继续 (Addendum 等) ;失败时,它会返回 Exception 并终止连接。仅对原始 challenge 字节签名的客户端将无法通过身份验证。
这与密码握手的顺序相反:这里是 ServerHello 紧接在 ClientHello 之后。在 SSH 认证下,ServerHello 会在签名验证完成前暂不发送,因此在看到任何 ServerHello 之前,SSH 质询-响应会先交错插入握手过程。
不使用 SSH 认证的外部客户端永远不会看到数据包 11、12 或 18——除非用户通过用户名此前缀显式选择启用,否则这些数据包不会在线路上传输。

数据包类型参考

客户端 → 服务器

服务器 → 客户端

配置

本节介绍会影响原生协议连接形态的可调参数: 下方的默认值反映的是较新的服务器版本;它们可能因版本和部署而异。

传输层设置

套接字选项

超时

这些超时的嵌套关系如下:
操作系统的 keepalive 会最先生效,并且可能在内核层静默检测到失效的对端。应用程序的接收超时是下一道防线。空闲超时则是最后一道手段,用于回收长时间未使用的连接。

连接限制

只要连接持续定期发出查询,就可以无限期保持存活。只有空闲连接会在一小时后被回收,且默认不设最大生命周期限制。

应用层设置

这些设置会随每个查询一起,通过Query 数据包的 settings 列表传递。它们会改变服务器在线上传输的数据内容,或其分帧方式。

压缩

Query 数据包 (字段 6) 中的 compression 标志位用于开启或关闭压缩;这些设置用于选择开启压缩时使用的编解码器。

日志流

send_logs_level 设为除 "none" 之外的任意值时,服务器会在查询执行期间发送 日志 数据包。

进度报告

这是目标最小值,而非严格的最大值:如果查询生成工作的速度不够快,服务器发送 Progress 数据包的频率可能会更低。

结果封装

异步 INSERT

分布式链路追踪

不在此范围内的设置

这些设置有时会被误认为是协议级设置,但它们控制的是 SQL 执行、存储或 CPU 使用,而不是线上传输行为。协议实现不需要对它们做特殊处理。
  • max_threads — 查询执行期间的并行度。
  • max_memory_usage — 单个查询的内存上限。
  • max_block_size, preferred_block_size_bytes — 查询处理期间的内部块大小;线上传输的块不受这些设置影响。
  • compile_expressions — JIT 编译;仅影响 CPU。
  • async_insert_max_data_size — 服务端队列缓冲区。
  • 所有 input_format_*output_format_* 设置,除了 input_format_native_* / output_format_native_* 家族 —— 非 native 设置用于选择或调整其他格式 (例如通过 HTTP) ,不会改变原生协议的 Data 块。
*_native_* 设置是个例外:它们会改变原生 TCP Data 块中的字节,因此协议实现必须将其考虑在内。output_format_native_encode_types_in_binary_format 会将列的 type 字段从文本字符串切换为二进制类型编码,output_format_native_write_json_as_string 会将 JSON 列输出为 String,而 output_format_native_use_flattened_dynamic_and_json_serialization 则会选择 FLATTENED Dynamic/JSON 布局。由于这些设置影响的是块体而非数据包封装,因此它们在 Native Format 规范中定义——请参见列在线路上的布局带版本的类型

术语表

Cancel — 由客户端发起的数据包 (类型 3) ,用于中止正在运行的查询。本页未对此作详细说明。 客户端数据结束标记 — 客户端发送的空 Data 数据包 (0 列、0 行) ,用于关闭输入流。其所在位置因查询类型而异:
  • 普通查询 (SELECT 等) : 在 Query 数据包以及所有外部表 Data 数据包之后发送,用于表示“没有更多外部数据”。随后服务器开始执行。
  • INSERT 客户端不会发送 schema 之前的标记。服务器会先发送 schema 块,客户端再流式传输其行 Data 块,最后才发送空 Data 数据包来终止行流。如果在 schema 块之前发送空标记,服务器会将其视为行已立即结束,从而导致数据丢失。
特性 — 在特定协议版本中引入的一项线格式变更。当协商后的版本大于或等于该特性对应的版本时生效。参见版本控制与特性门控 Inter-server — 某个字段的角色标签,仅在服务器到服务器的分布式查询中有意义。外部客户端会写入默认值 (通常为空字符串、0 或 false) 。 协商版本min(client_version, server_version),在握手期间计算得出。它决定了在连接的整个生命周期内哪些特性处于激活状态。 数据包 — 一条线上传输消息:以 VarUInt 数据包类型代码开头,后接一个 body,其格式取决于类型。参见数据包封装 数据包类型代码 — 数据包开头的 VarUInt,用于标识其格式。目前 0–18 的值已分配。参见数据包类型参考 响应流 — 服务器在查询期间发出的数据包序列。其长度不定,并且恰好以一个 EndOfStream (成功) 或 Exception (失败) 结束。参见查询阶段 Schema 块 — INSERT 阶段中服务器发送的头部块 (即一个有列但 0 行的 Block) ,用于在客户端发送数据前声明预期的列形态。 Settings 列表 — Query body 中由 (key, flags, value) 元组组成的序列,以空 key 终止。它承载每个查询的应用层配置。参见 Setting StageQuery 数据包中的一个 VarUInt 字段 (字段 5) ,用于控制服务器将查询执行到什么程度。外部客户端通常发送 2 (Complete) ;分布式查询和序列化查询计划会使用更高的值。完整的线传输取值请参见 Query 的字段 5。 终止符 — 用于结束流的数据包。Query 响应以 EndOfStream (成功) 或 Exception (失败) 结束。客户端的输入流则以空 Data 标记结束。
Last modified on June 25, 2026