PostgreSQL 网络报文体系深度解析
一、引言
PostgreSQL 使用一种基于消息的协议用于前端和后端(服务器和客户机)之间通讯。该协议在 TCP/IP 和 Unix 域套接字上实现,端口号 5432 已在 IANA 注册为使用该协议的常用端口,但实际上任何非特权端口号都可以使用。这套协议体系是理解 PostgreSQL 通信机制的核心,也是开发数据库驱动、中间件和代理的基础。本文将以协议 3.0 版本(PostgreSQL 7.4 及更高版本中实现)为主线,深度剖析 PostgreSQL 的网络报文体系。
二、协议基础架构
2.1 整体架构
PostgreSQL 的前端/后端协议是一种客户端-服务器协议,采用 消息驱动 的通信模式。为了有效地为多个客户端提供服务,服务器为每个客户端派生一个新的“后端”进程。在目前的实现中,在检测到到来的连接请求后,立即创建一个新的子进程,这种架构使得协议处理相对简单透明。
网络协议是通信双方必须遵守的一组约定,包括 语法、语义和时序 三个要素。语法规定数据和控制信息的组织结构;语义定义数据传递的信息内容;时序规范数据和控制信息的发送顺序。
2.2 通用消息帧格式
除启动消息外,所有协议消息都遵循统一的帧格式:
+----------+-------------+--------------------+
| Byte1 | Int32 | 消息体 |
| 消息类型 | 消息长度 | (根据消息类型变化) |
+----------+-------------+--------------------+- Byte1:消息类型标识符,使用单个 ASCII 字符标记消息类型(如
'Q'表示 Query 消息) - Int32:消息剩余部分的长度(以字节为单位),包括长度字段自身,但不包括消息类型字节
- 消息体:长度不定,格式由具体消息类型决定
历史遗留:客户端发送的第一条消息(启动消息)没有初始的消息类型字节
2.3 字节序
PostgreSQL 协议在传输多字节整数时统一采用 大端序(网络字节序),而 MySQL 协议采用小端序,这是两者在底层设计上的一个重要区别。
2.4 防丢失同步机制
为了避免与消息流丢失同步信息,服务器和客户端通常将整个消息读取到缓冲区中(利用字节计数),然后才尝试处理其内容。这样在处理内容时,若发现错误就更容易恢复。在极少数情况下(例如没有足够内存缓冲消息),接收端可以利用字节计数判断需要跳过多少输入后才能继续读取消息。
三、报文消息类型体系
3.1 消息类型全景
PostgreSQL 协议定义了大量的消息类型,按照方向和功能可分为以下几类:
| 分类 | 方向 | 主要消息类型 | 说明 |
|---|---|---|---|
| 启动认证 | F→B | StartupMessage | 启动消息(无类型字节) |
| 认证 | B→F | Authentication* | 多种认证请求消息 |
| 简单查询 | F→B | Query | 文本查询字符串 |
| 扩展查询 | F→B | Parse, Bind, Execute, Describe, Close | 分步执行查询 |
| 查询响应 | B→F | RowDescription, DataRow, CommandComplete, EmptyQueryResponse | 返回结果集 |
| 消息通知 | B→F | NoticeResponse, ErrorResponse, ParameterStatus | 异步通知 |
| 同步控制 | B→F | ReadyForQuery | 前端可发送命令的状态指示 |
| 事务控制 | F→B | Sync | 同步点 |
| 函数调用 | F→B | FunctionCall | 调用函数(已不推荐) |
| COPY 协议 | F↔B | CopyInResponse, CopyOutResponse, CopyData, CopyDone, CopyFail | 批量数据传输 |
| 复制协议 | F↔B | START_REPLICATION, XLogData, PrimaryKeepalive | 流复制相关 |
| 取消请求 | F→B | CancelRequest | 取消正在执行的查询(无类型字节) |
| 终止 | F→B | Terminate | 正常关闭连接 |
3.2 通用长度计数规则
在消息帧中,Int32 长度字段计算方式如下:
总消息长度 = 4 (长度字段自身) + 消息体长度注意这个长度 不包含 开头的 Byte1 消息类型字节,因此接收端可以用以下方式解析消息边界:先读取 1 字节得到消息类型,再读取 4 字节得到长度 L,然后继续读取 L 字节作为消息体,之后下一个 Byte1 就是下一条消息的类型。
3.3 错误与通知消息的字段结构
错误消息(ErrorResponse)和通知消息(NoticeResponse)采用结构化的字段格式,每个字段类型有一个单字节标识符,且每个字段类型在一条消息中最多出现一次:
| 标识符 | 字段名 | 说明 | 是否必须 |
|---|---|---|---|
S | Severity | 严重级别(ERROR/FATAL/PANIC/WARNING/NOTICE等) | 是 |
C | Code | SQLSTATE 错误码(如 42703 表示 undefined_column) | 是 |
M | Message | 主要人类可读错误消息 | 是 |
D | Detail | 可选的详细信息 | 否 |
H | Hint | 可选的建议提示 | 否 |
P | Position | 错误光标位置(字符串索引,从 1 开始) | 否 |
p | Internal Position | 内部命令中的错误位置 | 否 |
q | Internal Query | 失败的内部生成命令文本 | 否 |
W | Where | 错误发生的上下文(如 PL/pgSQL 调用栈) | 否 |
F | File | 报告错误的源文件名 | 否 |
L | Line | 源文件行号 | 否 |
R | Routine | 报告错误的例程名 | 否 |
客户端负责根据自身需要格式化显示信息,应适当处理消息中的换行符。
四、会话生命周期报文交互
4.1 总体状态机
协议的通信过程主要分为 **启动阶段(Startup Phase)**和 正常操作阶段(Normal Operation Phase):
[TCP 连接建立]
↓
[启动阶段] —— SSL 协商 → 认证流程 → 参数状态传递
↓
[正常操作阶段] —— 查询执行 → 结果返回 → (循环)
↓
[终止阶段] —— 发送 Terminate 或连接关闭除了初始的启动请求外,启动阶段的其余部分由服务器驱动。在正常操作中,前端发送查询和命令,后端返回结果和响应。少数情况(如 NOTIFY)后端会主动发送消息,但绝大多数情况由前端请求驱动。
4.2 启动阶段报文详解
4.2.1 SSL 协商
如果客户端希望使用 SSL 加密连接,在发送启动消息前先发送一个 SSLRequest 消息(与启动消息类似,没有消息类型字节)。服务器响应 'S' 表示接受 SSL 连接请求,响应 'N' 表示拒绝,随后客户端和服务器进行 SSL 握手。握手完成后,重新开始协议流程。
SSLRequest 消息格式:
Int32 8 (消息长度,包含自身)
Int32 80877103 (SSL 请求码)4.2.2 StartupMessage 格式
启动消息没有消息类型字节,直接以长度字段开头:
Int32 消息长度(包含自身)
Int32 协议版本号
String 参数名(如 "user")
String 参数值(如 "postgres")
String 参数名(如 "database")
String 参数值(如 "mydb")
...
String "" (参数列表结束标记)协议版本号采用 PG_PROTOCOL(major, minor) 编码
#define PG_PROTOCOL(m,n) (((m) << 16) | (n))例如版本 3.0 表示为 196608(3 << 16 | 0)。启动消息可以包含运行时参数的其他设置,如 client_encoding、TimeZone、application_name 等。
4.2.3 认证流程
服务器收到启动消息后,根据 pg_hba.conf 配置决定认证方式,并发送相应的认证请求消息。服务器支持的认证方式包括:
| 认证方式 | 说明 | 交互轮数 |
|---|---|---|
| AuthenticationOk | 无需认证,直接成功 | 0 |
| AuthenticationCleartextPassword | 明文密码认证 | 1 请求 + 1 响应 |
| AuthenticationMD5Password | MD5 加盐密码认证 | 1 请求 + 1 响应 |
| AuthenticationSASL | SASL 认证(SCRAM-SHA-256) | 多轮 |
| AuthenticationGSSAPI | GSSAPI 认证 | 多轮 |
| AuthenticationSSPI | SSPI 认证(Windows) | 多轮 |
对于 GSSAPI、SSPI 和 SASL,可能需要多次交换数据包才能完成认证。认证周期以服务器拒绝连接(ErrorResponse)或发送 AuthenticationOk 结束。
4.2.4 MD5 密码计算方式
当收到 AuthenticationMD5Password 消息时,客户端需要计算 MD5 密码响应:
-- 假设密码为 'secret', 用户名为 'postgres'
salt = AuthenticationMD5Password 消息中的 4 字节随机盐
md5_hash = md5(password + username) -- 十六进制字符串
response = 'md5' + md5(md5_hash + salt)4.2.5 启动完成后的消息序列
认证通过后,服务器发送以下消息序列,表示启动阶段完成:
1. ParameterStatus (可能多条) —— 服务器参数状态,如 server_version、client_encoding 等
2. BackendKeyData —— 包含后端进程 ID(PID)和取消密钥
3. ReadyForQuery —— 表示服务器准备接收命令BackendKeyData 消息格式:
Byte1 'K'
Int32 12
Int32 后端进程ID
Int32 取消密钥这个密钥用于后续安全地取消查询,只有原始客户端知道密钥,才能发起取消请求。
ReadyForQuery 消息的事务状态指示符:
'I':空闲(idle),不在事务块中'T':在事务块中(in a transaction block)'E':在失败的事务块中(in a failed transaction block),需要执行 ROLLBACK 恢复
4.3 正常操作阶段报文交互
4.3.1 Simple Query 协议
在简单查询协议中,前端只发送一个文本查询字符串,后端立即解析并执行。
Query 消息格式(前端 → 后端):
Byte1 'Q'
Int32 消息长度(包含自身)
String 查询字符串Simple Query 响应序列(后端 → 前端):
对于返回行集的查询(如 SELECT):
- RowDescription:描述结果集的列信息(列名、类型 OID、类型大小等)
- DataRow(可能多条):每条消息包含一行数据
- CommandComplete:执行完成,包含标签(如
SELECT 10表示返回 10 行) - ReadyForQuery:服务器可以接收下一条命令
对于不返回行集的命令(如 INSERT、UPDATE、DELETE):
- CommandComplete(直接)
- ReadyForQuery
对于空查询(空字符串或仅分号):
- EmptyQueryResponse
- ReadyForQuery
RowDescription 消息体结构:
Int16 列数
For each column:
String 列名
Int32 表 OID(如果是表达式结果则为 0)
Int16 属性编号(1-based,如果不是表列则为 0)
Int32 数据类型 OID
Int16 类型大小(字节数)
Int32 类型修饰符
Int16 格式代码(0=text,1=binary)DataRow 消息体结构:
Int16 列数
For each column:
Int32 列值长度(-1 表示 NULL)
Byte[n] 列值内容(如果长度 != -1)CommandComplete 消息体标签示例:
SELECT 42:SELECT 返回 42 行INSERT 0 3:INSERT 插入 3 行UPDATE 5:UPDATE 更新 5 行DELETE 3:DELETE 删除 3 行CREATE TABLE:DDL 命令
4.3.2 Extended Query 协议
扩展查询协议将查询处理分割为多个步骤:解析(Parse)、绑定(Bind)和执行(Execute),从而提供灵活性和性能提升。
Parse → Bind → Execute ───┐
↑ ↑ │
│ └─ Describe │
└─ Describe │
↓
(可重复 Execute) Sync → ReadyForQueryParse 消息(消息类型 'P')的格式:
Byte1 'P'
Int32 消息长度
String 预备语句名称(空字符串表示未命名)
String 查询字符串(可包含 $1, $2 等参数占位符)
Int16 参数类型 OID 数量
Int32[] 参数类型 OID(每个 OID 4 字节)Bind 消息(消息类型 'B')的格式:
Byte1 'B'
Int32 消息长度
String 目标 Portal 名称(空字符串表示未命名)
String 源预备语句名称
Int16 参数格式码数量
Int16[] 参数格式码(0=text, 1=binary)
Int16 参数值数量
For each parameter:
Int32 参数值长度(-1 表示 NULL)
Byte[] 参数值内容
Int16 结果列格式码数量
Int16[] 结果列格式码Execute 消息(消息类型 'E'):
Byte1 'E'
Int32 消息长度
String Portal 名称
Int32 最大行数(0 表示所有行)Sync 消息(消息类型 'S'):
Byte1 'S'
Int32 4Sync 消息用于清空扩展查询的消息队列,并强制后端返回 ReadyForQuery。
格式码(Format Code)说明:
- 0:文本格式(text)
- 1:二进制格式(binary)
自 PostgreSQL 7.4 起,协议仅支持这两种格式,但为未来扩展预留了空间。客户端可以为每个传输的参数值和查询结果的每一列指定格式代码。
ParseComplete 消息(消息类型 '1'):
Byte1 '1'
Int32 4BindComplete 消息(消息类型 '2'):
Byte1 '2'
Int32 4Describe 消息(消息类型 'D'):
Byte1 'D'
Int32 消息长度
Byte1 'S' (预备语句) 或 'P' (Portal)
String 对象名称Describe 消息用于获取预备语句的参数类型信息(返回 ParameterDescription)或 Portal 的结果列信息(返回 RowDescription)。
4.4 终止阶段
会话的终止通常由前端选择,但后端也可在某些情况下强制终止。无论哪种情况,当后端关闭连接时,它将回滚所有打开的(未完成的)事务后退出。
Terminate 消息(前端 → 后端):
Byte1 'X'
Int32 4发送 Terminate 后,前端应关闭连接,不应再等待任何响应。
五、高级子协议报文体系
5.1 COPY 协议
COPY 协议用于高效的大规模数据导入/导出,独立于常规查询协议运行。COPY 协议有两种变体:
- COPY FROM:数据从前端流向后端(导入)
- COPY TO:数据从后端流向前端(导出)
COPY 协议的消息序列:
COPY FROM 流程(前端发送数据):
前端: Query "COPY table FROM STDIN"
后端: CopyInResponse ('G')
前端: CopyData ('d') x N
前端: CopyDone ('c') 或 CopyFail ('f')
后端: CommandComplete + ReadyForQueryCOPY TO 流程(后端发送数据):
前端: Query "COPY table TO STDOUT"
后端: CopyOutResponse ('H')
后端: CopyData ('d') x N
后端: CopyDone ('c')
后端: CommandComplete + ReadyForQueryCopyData 消息格式:
Byte1 'd'
Int32 消息长度
Byte[] 实际数据CopyDone 消息格式:
Byte1 'c'
Int32 4CopyFail 消息格式(仅在 COPY FROM 中使用):
Byte1 'f'
Int32 消息长度
String 错误消息5.2 取消请求协议
取消请求是一种特殊机制,允许客户端在不关闭连接的情况下中止正在执行的查询。取消请求有自己独特的消息格式,因为它在独立的连接上发送。
CancelRequest 消息格式(没有消息类型字节):
Int32 16
Int32 80877102 (取消请求码)
Int32 后端进程ID
Int32 取消密钥取消请求码定义为 PG_PROTOCOL(1234, 5678),确保不会与任何协议版本号冲突。
取消请求使用原始连接的连接参数(包括加密要求),但不需要认证,因为取消请求本身不涉及身份验证,发送后立即关闭连接。
libpq 提供的取消接口:
PQcancelCreate():准备取消连接PQcancelBlocking():阻塞方式发送取消请求PQcancelStart()/PQcancelPoll():非阻塞方式发送取消请求
取消请求的成功发送不保证命令会被取消。如果取消生效,被取消的命令将提前终止并返回错误结果。
5.3 流复制协议
流复制协议是 PostgreSQL 高可用架构的核心,分为 物理流复制 和 逻辑流复制 两种。
5.3.1 物理流复制协议
要启动物理流复制,前端在启动消息中发送 replication 参数,值为 true(或 on、yes、1),告诉后端进入 walsender 模式。
启动成功后,后端进入复制模式,前端可以发送复制命令:
IDENTIFY_SYSTEM:获取系统标识START_REPLICATION:开始流式传输 WALCREATE_REPLICATION_SLOT/DROP_REPLICATION_SLOT:管理复制槽
复制协议消息不嵌入消息类型字节,因为流复制协议自身提供了消息长度。
5.3.2 逻辑流复制协议
逻辑流复制协议建立在物理流复制协议的原语之上,通过 START_REPLICATION SLOT slot_name LOGICAL 命令启动。
逻辑复制协议的关键特性:
- 逐个发送事务:一对 Begin 和 Commit 消息之间的所有消息属于同一事务
- 流式传输大型事务:使用 Stream Start → ... → Stream Stop 分段传输
- 支持 DML 消息:Insert、Update、Delete
- Relation 缓存:Relation 消息仅在关系定义首次出现时发送,后续 DML 复用缓存
逻辑复制协议支持多个协议版本:
- 版本 1:基础版本
- 版本 2(服务器 ≥ 14):支持流式传输大型进行中事务
- 版本 3(服务器 ≥ 15):支持两阶段提交
- 版本 4(服务器 ≥ 16):支持并行应用大型事务流
5.4 异步通知(NOTIFY/LISTEN)
PostgreSQL 提供了发布/订阅机制,支持异步通知。
LISTEN 和 UNLISTEN 通过普通 Query 协议执行。当后端执行 NOTIFY 时,主动向前端发送 NotificationResponse 消息。
NotificationResponse 消息格式:
Byte1 'A'
Int32 消息长度
Int32 后端进程 ID
String 通知条件名称
String 负载数据(额外信息)异步通知可以在启动阶段后的任何时间发生,后端会主动推送。
六、源码实现分析
6.1 libpq 客户端库
libpq 是 PostgreSQL 的 C 语言客户端库,为所有客户端数据库连接提供基础。它实现了 PostgreSQL 前端/后端协议,处理连接管理、认证、查询执行和客户端与服务器之间的数据传输。
libpq 核心组件:
| 组件 | 用途 | 关键成员 |
|---|---|---|
| PGconn | 连接状态 | pghost, pgport, dbName, pguser |
| 网络状态 | Socket 和地址 | sock, laddr, raddr |
| 认证状态 | 认证方法和凭据 | auth_req_received, sasl_state |
| SSL/TLS | 加密状态 | ssl, ssl_in_use |
| 协议状态 | 版本和状态 | pversion, asyncStatus |
| 缓冲区 | I/O 数据 | inBuffer, outBuffer |
6.2 协议底层实现(pqpacket.c)
pqpacket.c 是理解协议最低层部分的模块,负责数据包的接收和发送。该模块的核心工作是处理 主机到网络字节序的转换,而数据格式转换则由其他模块负责。
核心函数:
PacketReceive:接收数据包PacketSend:发送数据包
libpq 实际上提供了两套通信方式:
- socket(标准 TCP/IP 通信,实现在
pqcomm.c) - shm_mq(共享内存消息队列,用于本地进程间通信,实现在
pqmq.c)
6.3 中间件协议解析实现
由于 PostgreSQL 协议的开放性,许多中间件和代理工具实现了协议解析能力:
- pgwire(Rust):实现了 PostgreSQL Wire Protocol,提供构建兼容 PostgreSQL 服务器的 API
- He3Proxy:基于 PG 协议构建,需要解析 PG 通信协议以支持连接池和负载均衡等功能
- pgproto3:Go 语言的 PostgreSQL 协议实现,包含 backend.go(后端协议编解码)、frontend.go(前端协议编解码)和 message.go(消息定义)
- PgDog:网络代理,可查看客户端和 PostgreSQL 之间的每一个字节,理解 SQL 并推断查询路由
七、协议最佳实践与陷阱
7.1 常见问题
消息边界同步丢失:这是协议实现中最棘手的问题。应始终使用长度字段读取完整消息后再处理,而不是流式解析。
启动消息的特殊性:启动消息没有消息类型字节,需要特殊处理。
大端序 vs 小端序:跨平台实现时务必注意字节序转换。
扩展查询的状态管理:扩展查询是有状态的协议,需要正确管理预备语句(prepared statement)和 Portal 的生命周期。
异步操作的复杂性:非阻塞 I/O 需要仔细处理部分读取的情况,
pqpacket.c的大部分复杂性都是为了确保非阻塞 I/O 的正确处理。
7.2 性能优化建议
- 对于批量操作,优先使用 Extended Query + Execute 多次,而不是重复发送 Parse 和 Bind
- 大量数据导入/导出时使用 COPY 协议,避免逐行 INSERT
- 利用 Pipeline 模式 批量发送多个查询,减少网络往返
- 对于大型结果集,考虑设置合适的 Fetch 大小,分批获取
八、总结
PostgreSQL 的网络报文体系是一套设计精巧、功能完备的通信协议。其核心设计思想可以总结为:
- 统一消息帧格式:除启动消息和取消请求外,所有消息遵循
[类型字节][长度][消息体]的统一结构,简化了协议解析 - 服务器驱动启动阶段:除了初始启动请求,启动阶段的后续流程由服务器控制,保证了安全性和一致性
- 灵活的双查询协议:Simple Query 提供简单易用性,Extended Query 提供高性能和预编译能力
- 完备的子协议体系:COPY、流复制、异步通知等子协议覆盖了各种应用场景
- 开放的实现生态:libpq 提供标准实现,pgwire、pgproto3 等第三方库实现了多种语言的协议支持
理解 PostgreSQL 的报文体系,不仅是深入掌握 PostgreSQL 本身的关键,也是开发数据库驱动、中间件、代理以及构建 PostgreSQL 兼容系统的基石。随着 PostgreSQL 的持续演进,协议也在不断完善和扩展,为现代数据库应用提供了坚实的基础。
本文基于 PostgreSQL 协议 3.0 版本撰写,适用于 PostgreSQL 7.4 及以上版本。如需了解早期协议版本,请参考对应版本的 PostgreSQL 文档。