PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

54.2. 消息流 #

54.2.1. 启动连接流程
54.2.2. 简单查询流程
54.2.3. 扩展查询流程
54.2.4. 流水线处理
54.2.5. 函数调用
54.2.6. COPY 数据传输操作
54.2.7. 异步操作
54.2.8. 取消正在执行的请求
54.2.9. 连接终止
54.2.10. SSL 会话加密
54.2.11. GSSAPI 会话加密

本节描述消息流以及每种消息类型的语义。 (每种消息的确切表示形式的细节见 Section 54.7。) 根据连接的状态,存在几种不同的子协议:启动、查询、函数调用、 COPY 和终止。 对于异步操作(包括通知响应和命令取消)也有特殊的规定, 它们可以在启动阶段之后的任何时刻发生。

54.2.1. 启动连接流程 #

要开始一个会话,前端打开一个到服务器的连接并发送一条启动消息。 这条消息包括用户名以及用户想连接的数据库名; 它还标明要使用的特定协议版本。(启动消息可以选择性地包含 运行时参数的额外设置。) 服务器随后会利用这些信息以及其配置文件的内容 (例如 pg_hba.conf)来确定 该连接是否临时可接受,以及需要什么额外的认证(如果有)。

服务器随后会发送一条适当的认证请求消息, 前端必须以适当的认证响应消息(例如口令)回复。 对于除 GSSAPI、SSPI 和 SASL 之外的所有认证方法, 至多只有一次请求和一次响应。在某些方法中, 根本不需要来自前端的响应,因此也就不会发生认证请求。 对于 GSSAPI、SSPI 和 SASL, 可能需要多轮分组交换才能完成认证。

认证循环以下述方式结束:服务器要么拒绝该连接尝试 (ErrorResponse),要么发送 AuthenticationOk。

在这一阶段服务器可能发出的消息有:

ErrorResponse

连接尝试已被拒绝。 服务器随后立即关闭该连接。

AuthenticationKrb5

前端现在必须与服务器进行一次 Kerberos V5 认证对话(这里不描述,它是 Kerberos 规范的一部分)。 如果成功,服务器以 AuthenticationOk 响应, 否则以 ErrorResponse 响应。已不再支持这一方式。

AuthenticationCleartextPassword

前端现在必须发送一条 PasswordMessage, 其中包含明文形式的口令。如果这是正确的口令, 服务器以 AuthenticationOk 响应, 否则以 ErrorResponse 响应。

AuthenticationMD5Password

前端现在必须发送一条 PasswordMessage,其中包含 (带用户名的)口令经 MD5 加密后再用 AuthenticationMD5Password 消息中指定的 4 字节随机盐 再次加密的结果。如果这是正确的口令, 服务器以 AuthenticationOk 响应, 否则以 ErrorResponse 响应。实际的 PasswordMessage 可以在 SQL 中计算为 concat('md5', md5(concat(md5(concat(password, username)), random-salt)))。 (请记住 md5() 函数以十六进制字符串 返回其结果。)

AuthenticationOk

认证交换已成功完成。

AuthenticationKerberosV5

前端现在必须与服务器进行一次 Kerberos V5 认证对话(这里不描述,它是 Kerberos 规范的一部分)。 如果成功,服务器以 AuthenticationOk 响应, 否则以 ErrorResponse 响应。已不再支持这一方式。

AuthenticationCleartextPassword

前端现在必须发送一条 PasswordMessage, 其中包含明文形式的口令。如果这是正确的口令, 服务器以 AuthenticationOk 响应, 否则以 ErrorResponse 响应。

AuthenticationMD5Password

前端现在必须发送一条 PasswordMessage,其中包含 (带用户名的)口令经 MD5 加密后再用 AuthenticationMD5Password 消息中指定的 4 字节随机盐 再次加密的结果。如果这是正确的口令, 服务器以 AuthenticationOk 响应, 否则以 ErrorResponse 响应。实际的 PasswordMessage 可以在 SQL 中计算为 concat('md5', md5(concat(md5(concat(password, username)), random-salt)))。 (请记住 md5() 函数以十六进制字符串 返回其结果。)

Warning

对 MD5 加密口令的支持已被弃用,并将在 PostgreSQL 的未来版本中被移除。 有关迁移到其他口令类型的详情, 参阅 Section 20.3.2。

AuthenticationGSS

前端现在必须发起一次 GSSAPI 协商。 作为响应,前端将发送一条 GSSResponse 消息, 其中包含 GSSAPI 数据流的第一部分。如果还需要更多消息, 服务器将以 AuthenticationGSSContinue 响应。

AuthenticationSSPI

前端现在必须发起一次 SSPI 协商。 作为响应,前端将发送一条 GSSResponse, 其中包含 SSPI 数据流的第一部分。如果还需要更多消息, 服务器将以 AuthenticationGSSContinue 响应。

AuthenticationGSSContinue

这条消息包含 GSSAPI 或 SSPI 协商前一步 (AuthenticationGSS、AuthenticationSSPI 或一条先前的 AuthenticationGSSContinue)的响应数据。 如果这条消息中的 GSSAPI 或 SSPI 数据表明完成认证还需要更多数据, 前端必须把那些数据作为另一条 GSSResponse 消息发送。 如果这条消息完成了 GSSAPI 或 SSPI 认证, 服务器接下来会发送 AuthenticationOk 表示认证成功, 或发送 ErrorResponse 表示失败。

AuthenticationSASL

前端现在必须发起一次 SASL 协商, 使用该消息中列出的 SASL 机制之一。作为响应, 前端将发送一条 SASLInitialResponse, 其中包含所选机制的名称以及 SASL 数据流的第一部分。 如果还需要更多消息,服务器将以 AuthenticationSASLContinue 响应。详情见 Section 54.3。

AuthenticationSASLContinue

这条消息包含 SASL 协商前一步 (AuthenticationSASL 或一条先前的 AuthenticationSASLContinue)的挑战数据。 前端必须以一条 SASLResponse 消息响应。

AuthenticationSASLFinal

SASL 认证已完成,并带有给客户端的额外机制相关数据。 服务器接下来会发送 AuthenticationOk 表示认证成功, 或发送 ErrorResponse 表示失败。 只有当 SASL 机制规定在完成时要从服务器向客户端 发送额外数据时,才会发送这条消息。

NegotiateProtocolVersion

服务器不支持客户端请求的次协议版本,但支持该协议的更早版本; 这条消息给出所支持的最高次版本。 如果客户端在启动包中请求了不受支持的协议选项 (即以 _pq_. 开头的选项), 也会发送这条消息。

在这条消息之后,认证将继续使用服务器指明的版本进行。 如果客户端不支持更旧的版本,它应当立即关闭连接。 如果服务器没有发送这条消息,则表示它支持客户端请求的 协议版本以及所有协议选项。

如果前端不支持服务器请求的认证方法, 那么它应当立即关闭连接。

在收到 AuthenticationOk 之后,前端必须等待来自服务器的更多消息。 在这一阶段,一个后端进程正在被启动,而前端只是一个感兴趣的旁观者。 启动尝试仍有可能失败(ErrorResponse), 或服务器拒绝支持所请求的次协议版本(NegotiateProtocolVersion), 但在正常情况下,后端会发送一些 ParameterStatus 消息、 BackendKeyData,最后是 ReadyForQuery。

在这一阶段,后端会尝试应用启动消息中给出的任何额外的 运行时参数设置。如果成功,这些值将成为会话默认值。 出错会导致 ErrorResponse 并退出。

在这一阶段后端可能发出的消息有:

BackendKeyData

这条消息提供秘密密钥数据,如果前端希望以后能发出取消请求, 就必须保存它。前端不应响应这条消息, 而应继续监听 ReadyForQuery 消息。

ParameterStatus

这条消息向前端告知后端参数的当前(初始)设置, 例如 client_encoding 或 DateStyle。 前端可以忽略这条消息,也可以记录这些设置供以后使用; 更多细节见 Section 54.2.7。 前端不应响应这条消息,而应继续监听 ReadyForQuery 消息。

ReadyForQuery

启动完成。前端现在可以发出命令。

ErrorResponse

启动失败。服务器在发送这条消息之后会关闭连接。

NoticeResponse

发出了一条警告消息。前端应当显示该消息, 但应继续监听 ReadyForQuery 或 ErrorResponse。

ReadyForQuery 消息与后端在每个命令周期之后发出的那条消息相同。 根据前端的编码需要,把 ReadyForQuery 视为一个命令周期的开始, 或把它视为启动阶段和每个后续命令周期的结束,都是合理的。

54.2.2. 简单查询流程 #

一个简单查询周期由前端向后端发送一条 Query 消息发起。 这条消息包含以文本字符串表达的 SQL 命令(或若干命令)。 然后,后端根据查询命令字符串的内容发送一条或多条响应消息, 最后发送一条 ReadyForQuery 响应消息。ReadyForQuery 告知前端它可以安全地发送新命令。 (实际上前端没有必要在发出另一条命令之前等待 ReadyForQuery, 但那时前端就必须自己负责弄清楚:如果较早的命令失败 而已经发出的较晚命令成功了,会发生什么。)

The possible response messages from the backend are:

CommandComplete

一条 SQL 命令正常完成。

CopyInResponse

后端已准备好把数据从前端复制到一张表;见 Section 54.2.6。

CopyOutResponse

后端已准备好把数据从一张表复制到前端;见 Section 54.2.6。

RowDescription

表明即将响应某个 SELECT、FETCH 等查询而返回行。这条消息的内容描述了这些行的列布局。 在它之后,每一条要返回给前端的行都会跟着一条 DataRow 消息。

DataRow

SELECT、FETCH 等查询 返回的行集合中的一行。

EmptyQueryResponse

识别到一个空的查询字符串。

ErrorResponse

发生了一个错误。

ReadyForQuery

对查询字符串的处理已完成。之所以单独发送一条消息来表明这一点, 是因为查询字符串可能包含多条 SQL 命令。 (CommandComplete 标记的是一条 SQL 命令处理的结束, 而不是整个字符串的结束。) 无论处理是成功终止还是因错误而终止, 都总是会发送 ReadyForQuery。

NoticeResponse

就该查询发出了一条警告消息。Notice 是对其他响应的补充, 也就是说,后端会继续处理该命令。

对 SELECT 查询(或其他返回行集合的查询, 例如 EXPLAIN 或 SHOW)的响应 通常由 RowDescription、零条或多条 DataRow 消息以及最后的 CommandComplete 组成。 与前端之间的 COPY 会启用 Section 54.2.6 中描述的特殊协议。 所有其他类型的查询通常只产生一条 CommandComplete 消息。

由于一个查询字符串可能包含多个查询(以分号分隔), 在后端完成对查询字符串的处理之前, 可能会出现多个这样的响应序列。 当整个字符串处理完毕、后端准备好接受新的查询字符串时, 才会发出 ReadyForQuery。

如果收到一个完全空的查询字符串(除空白外没有任何内容), 响应是 EmptyQueryResponse 后跟 ReadyForQuery。

出错时,会发出 ErrorResponse 后跟 ReadyForQuery。 ErrorResponse 会让查询字符串的所有后续处理被中止 (即使其中还有更多查询)。注意, 这可能发生在单个查询所生成的消息序列的中途。

在简单 Query 模式下,检索到的值的格式总是文本, 除非给定的命令是对用 BINARY 选项声明的 游标执行 FETCH。在那种情况下, 检索到的值是二进制格式。RowDescription 消息中给出的格式代码 表明了所使用的格式。

无论前端何时在期待任何其他类型的消息, 它都必须准备好接受 ErrorResponse 和 NoticeResponse 消息。 关于后端可能因外部事件而产生的消息, 另见 Section 54.2.7。

推荐的做法是把前端编码成一种状态机风格, 在任何可能有意义的时候接受任何消息类型, 而不是把对确切消息序列的假设硬编码进去。

54.2.2.1. 简单查询中的多语句处理 #

当一条简单 Query 消息包含多于一条 SQL 语句 (以分号分隔)时,这些语句会作为一个单一事务执行, 除非其中包含了显式的事务控制命令来强制不同的行为。 例如,如果这条消息包含

INSERT INTO mytable VALUES(1);
SELECT 1/0;
INSERT INTO mytable VALUES(2);

那么 SELECT 中的除零失败将强制回滚第一条 INSERT。此外, 由于在第一个错误处就会放弃对消息的执行, 第二条 INSERT 根本不会被尝试。

如果消息中包含的是

BEGIN;
INSERT INTO mytable VALUES(1);
COMMIT;
INSERT INTO mytable VALUES(2);
SELECT 1/0;

那么第一条 INSERT 会被显式的 COMMIT 命令提交。第二条 INSERT 和该 SELECT 仍会被当作一个单一事务,因此除零失败会回滚第二条 INSERT,但不会回滚第一条。

这一行为是这样实现的:让多语句 Query 消息中的语句在一个 隐式事务块中运行, 除非已有某个显式事务块供它们运行。 隐式事务块与常规事务块的主要区别在于, 隐式块会在 Query 消息结束时自动关闭: 如果没有错误则以隐式提交关闭,如果出错则以隐式回滚关闭。 这类似于一条语句单独执行时(不在事务块中) 发生的隐式提交或回滚。

如果会话由于先前某条消息中的 BEGIN 已经处于一个事务块中,那么无论 Query 消息包含一条语句还是多条, 它都只是继续那个事务块。但是,如果 Query 消息包含一个 关闭现有事务块的 COMMIT 或 ROLLBACK,那么其后的任何语句 都会在一个隐式事务块中执行。 反过来,如果一条多语句 Query 消息中出现了一个 BEGIN,那么它会开始一个常规事务块, 该块只会被一个显式的 COMMIT 或 ROLLBACK 终止, 无论那出现在这条 Query 消息中还是更晚的消息中。 如果 BEGIN 出现在一些已作为隐式事务块执行的 语句之后,那些语句不会立即被提交;实际上, 它们会被追溯地纳入这个新的常规事务块。

出现在隐式事务块中的 COMMIT 或 ROLLBACK 会照常执行并关闭该隐式块; 不过会发出一条警告,因为前面没有 BEGIN 的 COMMIT 或 ROLLBACK 可能代表一个错误。如果后面还有更多语句, 会为它们启动一个新的隐式事务块。

隐式事务块中不允许使用保存点, 因为它们会与在出错时自动关闭块的行为相冲突。

请记住,无论可能存在哪些事务控制命令, Query 消息的执行都会在第一个错误处停止。 因此,例如在单条 Query 消息中给出

BEGIN;
SELECT 1/0;
ROLLBACK;

时,会话将留在一个失败的常规事务块中, 因为除零错误之后根本执行不到 ROLLBACK。 需要再执行一条 ROLLBACK 才能把会话恢复到可用状态。

值得注意的另一个行为是:在对查询字符串执行任何部分之前, 就会对整个查询字符串做初始的词法和语法分析。 因此,靠后的语句中的简单错误(例如拼错的关键字) 可能会阻止任何语句的执行。 通常这对用户是不可见的,因为作为隐式事务块执行时, 所有语句反正都会被回滚。不过, 在尝试用多语句 Query 完成多个事务时它可能是可见的。 例如,如果我们的上一个示例因拼写错误变成

BEGIN;
INSERT INTO mytable VALUES(1);
COMMIT;
INSERT INTO mytable VALUES(2);
SELCT 1/0;

那么任何语句都不会被执行,其可见的区别是第一条 INSERT 不会被提交。 在语义分析阶段或更晚才检测到的错误 (例如拼错的表名或列名)则没有这种效果。

最后要注意,Query 消息中的所有语句都会观察到相同的 statement_timestamp() 值, 因为该时间戳只在收到 Query 消息时更新。 这也会导致它们都观察到相同的 transaction_timestamp() 值, 除非查询字符串结束了先前启动的事务并开始了一个新事务。

54.2.3. 扩展查询流程 #

扩展查询协议把上述简单查询协议分解为多个步骤。 准备步骤的结果可以被多次重用以提高效率。 此外,还可以使用一些额外的特性, 例如可以把数据值作为单独的参数提供, 而不必把它们直接插入查询字符串。

在扩展协议中,前端首先发送一条 Parse 消息, 其中包含一个文本查询字符串、可选的关于参数占位符数据类型的信息, 以及一个目标预备语句对象的名字(空字符串选择未命名的预备语句)。 响应是 ParseComplete 或 ErrorResponse。 参数数据类型可以用 OID 指定;如果不给定, 解析器会按照处理无类型的字符串字面值常量的方式尝试推断数据类型。

Note

可以通过把参数数据类型设置为零来让它保持未指定, 也可以通过让参数类型 OID 数组比查询字符串中使用的参数符号 ($n)数量更短来做到。 另一个特例是参数的类型可以指定为 void (即 void 伪类型的 OID)。 这是为了允许参数符号被用于实际上是 OUT 参数的函数参数。 通常不存在可以使用 void 参数的上下文, 但如果这样的参数符号出现在函数的参数列表中,它会被有效地忽略。 例如,形如 foo($1,$2,$3,$4) 的函数调用 可以匹配一个有两个 IN 参数和两个 OUT 参数的函数, 只要 $3 和 $4 被指定为 void 类型。

Note

Parse 消息中包含的查询字符串不能包含多于一条 SQL 语句, 否则会报告语法错误。简单查询协议中没有这一限制, 但扩展协议中有,因为允许预备语句或门户包含多条命令 会给协议带来不必要的复杂性。

如果成功创建,命名的预备语句对象会存续到当前会话结束, 除非被显式销毁。未命名的预备语句只存续到下一条 以未命名语句为目标的 Parse 语句被发出为止。 (注意,一条简单 Query 消息也会销毁未命名语句。) 命名的预备语句必须先被显式关闭,才能被另一条 Parse 消息重定义, 但对未命名语句没有这个要求。 命名的预备语句也可以在 SQL 命令层面创建和访问, 使用的是 PREPARE 和 EXECUTE。

一旦预备语句存在,就可以用一条 Bind 消息让它准备好执行。 Bind 消息给出源预备语句的名字(空字符串表示未命名的预备语句)、 目标门户的名字(空字符串表示未命名的门户), 以及要用于预备语句中出现的任何参数占位符的值。 所提供的参数集必须与预备语句所需的参数匹配。 (如果你在 Parse 消息中声明了任何 void 参数, 请在 Bind 消息中为它们传 NULL 值。) Bind 还指定查询返回的任何数据所使用的格式; 该格式可以整体指定,也可以逐列指定。 响应是 BindComplete 或 ErrorResponse。

Note

文本输出与二进制输出之间的选择由 Bind 中给出的格式代码决定, 与所涉及的 SQL 命令无关。使用扩展查询协议时, 游标声明中的 BINARY 属性是无关的。

查询规划通常在处理 Bind 消息时发生。 如果预备语句没有参数,或者会被重复执行, 服务器可能会保存创建好的计划,并在后续针对同一预备语句的 Bind 消息中重用它。不过,只有当它发现可以创建一个通用计划、 且其效率不会比依赖所提供特定参数值的计划差太多时才会这样做。 就协议而言,这一过程是透明的。

如果成功创建,命名的门户对象会存续到当前事务结束, 除非被显式销毁。未命名的门户会在事务结束时销毁, 或者在下一条以未命名门户为目标的 Bind 语句被发出时立即销毁。 (注意,一条简单 Query 消息也会销毁未命名门户。) 命名的门户必须先被显式关闭,才能被另一条 Bind 消息重定义, 但对未命名门户没有这个要求。 命名的门户也可以在 SQL 命令层面创建和访问, 使用的是 DECLARE CURSOR 和 FETCH。

一旦门户存在,就可以用一条 Execute 消息执行它。 Execute 消息指定门户名(空字符串表示未命名的门户)以及 一个最大结果行计数(零表示“抓取所有行”)。 结果行计数只对包含返回行集合命令的门户有意义; 在其他情况下,命令总是会执行完毕,行计数会被忽略。 对 Execute 的可能响应与上文中通过简单查询协议发出的查询相同, 区别在于 Execute 不会导致 ReadyForQuery 或 RowDescription 被发出。

如果 Execute 在门户执行完成之前终止 (由于达到了非零的结果行计数),它会发送一条 PortalSuspended 消息; 这条消息的出现告诉前端应当对同一门户再发出一次 Execute 以完成该操作。指示源 SQL 命令完成的 CommandComplete 消息 在门户的执行完成之前不会被发送。因此, 一个 Execute 阶段总是会以下列消息之一的出现而终止: CommandComplete、EmptyQueryResponse(如果门户是由一个空查询字符串 创建的)、ErrorResponse 或 PortalSuspended。

在每一系列扩展查询消息完成时,前端应当发出一条 Sync 消息。 这条无参数的消息会让后端在不在 BEGIN/COMMIT 事务块内时 关闭当前事务(“关闭”的含义是:无错误则提交, 出错则回滚)。然后会发出一条 ReadyForQuery 响应。 Sync 的目的是为错误恢复提供一个重新同步点。 当在处理任何扩展查询消息期间检测到错误时, 后端会发出 ErrorResponse,然后读取并丢弃消息直到遇到一条 Sync, 接着发出 ReadyForQuery 并返回正常的消息处理。 (但请注意,如果在正在处理 Sync 时检测到错误, 则不会发生跳过 —— 这保证了每条 Sync 有且只有一条 ReadyForQuery 被发送。)

Note

Sync 不会导致用 BEGIN 打开的事务块被关闭。 由于 ReadyForQuery 消息包含事务状态信息, 可以检测到这种情况。

除了这些基本的必需操作之外,扩展查询协议还有一些可选操作可用。

Describe 消息(门户变体)指定一个现有门户的名字 (未命名门户则用空字符串)。响应是一条 RowDescription 消息, 描述执行该门户将返回的行; 如果该门户不包含会返回行的查询,则是一条 NoData 消息; 如果不存在这样的门户,则是 ErrorResponse。

Describe 消息(语句变体)指定一个现有预备语句的名字 (未命名的预备语句则用空字符串)。响应是一条 ParameterDescription 消息,描述该语句所需的参数, 后跟一条 RowDescription 消息,描述该语句最终执行时将返回的行 (如果该语句不会返回行,则是一条 NoData 消息)。 如果不存在这样的预备语句,则会发出 ErrorResponse。 注意,由于 Bind 尚未发出,后端还不知道返回列将使用的格式; 在这种情况下,RowDescription 消息中的格式代码字段将为零。

Tip

在大多数场景下,前端应当在发出 Execute 之前先发出 两种变体之一的 Describe,以确保它知道如何解释将要得到的结果。

Close 消息关闭一个现有的预备语句或门户并释放资源。 对不存在的语句名或门户名发出 Close 并不是一个错误。 响应通常是 CloseComplete,但如果在释放资源时遇到困难, 也可能是 ErrorResponse。注意,关闭一条预备语句 会隐式关闭所有由该语句构建的打开的门户。

Flush 消息不会导致生成任何特定的输出, 但它会强制后端交付其输出缓冲区中所有待处理的数据。 在除 Sync 之外的任何扩展查询命令之后, 如果前端希望在发出更多命令之前检查该命令的结果, 就必须发送一条 Flush。没有 Flush 时, 后端返回的消息会被合并成尽可能少的分组,以减少网络开销。

Note

简单 Query 消息大致等价于以下系列:Parse、Bind、门户 Describe、 Execute、Close、Sync,使用未命名的预备语句和门户对象,不带参数。 一个区别是它可以接受查询字符串中的多条 SQL 语句, 并自动依次为每一条执行绑定/描述/执行序列。 另一个区别是它不会返回 ParseComplete、BindComplete、 CloseComplete 或 NoData 消息。

54.2.4. 流水线处理 #

使用扩展查询协议可以实现流水线, 也就是在不等待较早查询完成的情况下发送一系列查询。 这减少了完成给定一系列操作所需的网络往返次数。 不过,用户必须仔细考虑其中某一步失败时所需的行为, 因为较晚的查询届时已经在发往服务器的途中了。

处理这个问题的一种方式是让整个查询序列成为一个单一事务, 也就是把它包在 BEGIN ... COMMIT 中。但是, 如果希望其中一些命令独立于其他命令提交,这样做就没有帮助了。

扩展查询协议提供了另一种管理这一问题的方法, 即在相互依赖的步骤之间省略 Sync 消息。 由于出错后后端会跳过命令消息直到找到 Sync, 这使得当流水线中较早的命令失败时, 较晚的命令可以被自动跳过,而客户端不必用 BEGIN 和 COMMIT 显式管理这一点。可以独立提交的流水线片段 可以用 Sync 消息分隔开。

如果客户端没有发出显式的 BEGIN, 则会启动一个隐式事务块,并且每条 Sync 通常都会导致: 如果前面的步骤成功,则隐式 COMMIT; 如果失败,则隐式 ROLLBACK。 只有当第一条命令结束而没有同步时,服务器才会检测到 这个隐式事务块。有少数 DDL 命令 (例如 CREATE DATABASE) 不能在事务块内执行。如果流水线中执行了其中之一, 除非它是 Sync 之后的第一个命令,否则它会失败。 此外,成功时会强制立即提交以保持数据库一致性。 因此,紧跟在这类命令后面的 Sync 除了响应 ReadyForQuery 之外没有任何作用。

使用这种方法时,必须通过统计 ReadyForQuery 消息的数量、 等待其达到已发送 Sync 的数量,来判断流水线是否完成。 统计命令完成响应是不可靠的, 因为某些命令可能被跳过,从而不会产生完成消息。

54.2.5. 函数调用 #

函数调用子协议允许客户端直接调用数据库的 pg_proc 系统目录中存在的任何函数。 客户端必须拥有该函数的执行权限。

Note

函数调用子协议是一个遗留特性,在新代码中最好避免使用。 通过建立一条执行 SELECT function($1, ...) 的预备语句可以获得类似的结果。 然后可以用 Bind/Execute 取代函数调用周期。

函数调用周期由前端向后端发送一条 FunctionCall 消息发起。 然后,后端根据函数调用的结果发送一条或多条响应消息, 最后发送一条 ReadyForQuery 响应消息。ReadyForQuery 告知前端它可以安全地发送新的查询或函数调用。

后端可能发出的响应消息有:

ErrorResponse

发生了一个错误。

FunctionCallResponse

函数调用已完成,并返回了该消息中给出的结果。 (注意,函数调用协议只能处理单个标量结果, 不能处理行类型或结果集合。)

ReadyForQuery

函数调用的处理已完成。无论处理是成功终止 还是因错误而终止,都总是会发送 ReadyForQuery。

NoticeResponse

就该函数调用发出了一条警告消息。Notice 是对其他响应的补充, 也就是说,后端会继续处理该命令。

54.2.6. COPY 数据传输操作 #

COPY 命令允许与服务器之间进行高速的批量数据传输。 复制进入和复制传出操作各自把连接切换到一个独特的子协议中, 该子协议一直持续到操作完成。

当后端执行一条 COPY FROM STDIN SQL 语句时, 会启动复制进入模式(向服务器传输数据)。后端会向前端 发送一条 CopyInResponse 消息。然后,前端应当发送零条或多条 CopyData 消息,构成一个输入数据流。 (消息边界与行边界没有任何必然关系,尽管那通常是一个合理的选择。) 前端可以通过发送 CopyDone 消息(允许成功终止) 或 CopyFail 消息(这会导致 COPY SQL 语句 以错误失败)来终止复制进入模式。然后,后端会恢复到 COPY 开始之前所处的命令处理模式, 即简单或扩展查询协议。接下来它会发送 CommandComplete (如果成功)或 ErrorResponse(如果失败)。

如果在复制进入模式期间发生由后端检测到的错误 (包括收到一条 CopyFail 消息),后端会发出一条 ErrorResponse 消息。 如果 COPY 命令是通过一条扩展查询消息发出的, 后端此时会丢弃前端消息,直到收到一条 Sync 消息, 然后发出 ReadyForQuery 并返回正常处理。 如果 COPY 命令是在一条简单 Query 消息中发出的, 该消息的剩余部分会被丢弃,并发出 ReadyForQuery。 无论哪种情况,前端随后发出的任何 CopyData、CopyDone 或 CopyFail 消息都会被直接丢弃。

后端会忽略在复制进入模式期间收到的 Flush 和 Sync 消息。 收到任何其他非复制消息类型都构成一个错误, 会如上所述中止复制进入状态。 (对 Flush 和 Sync 的例外是为了方便那些总是在 Execute 消息之后 发送 Flush 或 Sync、而不检查要执行的命令是否是 COPY FROM STDIN 的客户端库。)

当后端执行一条 COPY TO STDOUT SQL 语句时, 会启动复制传出模式(从服务器传输数据)。后端会向前端 发送一条 CopyOutResponse 消息,后跟零条或多条 CopyData 消息 (总是每行一条),再后跟 CopyDone。然后,后端会恢复到 COPY 开始之前所处的命令处理模式, 并发送 CommandComplete。前端无法中止这次传输 (除非关闭连接或发出一个 Cancel 请求), 但它可以丢弃不需要的 CopyData 和 CopyDone 消息。

如果在复制传出模式期间发生由后端检测到的错误, 后端会发出一条 ErrorResponse 消息并恢复到正常处理。 前端应当把收到 ErrorResponse 视为复制传出模式的终止。

NoticeResponse 和 ParameterStatus 消息有可能穿插在 CopyData 消息之间;前端必须处理这些情况, 并且也应当为其他异步消息类型做好准备 (见 Section 54.2.7)。除此之外, CopyData 或 CopyDone 之外的任何消息类型 都可以被当作复制传出模式的终止。

还有一种与 COPY 相关的模式,称为 copy-both(双向复制), 它允许与服务器之间双向进行高速的批量数据传输。 当处于 walsender 模式的后端执行一条 START_REPLICATION 语句时, 会启动 copy-both 模式。后端会向前端发送一条 CopyBothResponse 消息。然后,后端和前端都可以发送 CopyData 消息,直到任意一端发送一条 CopyDone 消息。 客户端发送 CopyDone 消息之后,连接会从 copy-both 模式 进入复制传出模式,客户端不得再发送任何 CopyData 消息。 类似地,当服务器发送 CopyDone 消息时, 连接会进入复制进入模式,服务器不得再发送任何 CopyData 消息。 在双方都发送了 CopyDone 消息之后,复制模式终止, 后端恢复到命令处理模式。如果在 copy-both 模式期间 发生由后端检测到的错误,后端会发出一条 ErrorResponse 消息, 丢弃前端消息直到收到一条 Sync 消息, 然后发出 ReadyForQuery 并返回正常处理。 前端应当把收到 ErrorResponse 视为两个方向上的复制的终止; 在这种情况下不应发送任何 CopyDone。 关于通过 copy-both 模式传输的子协议的更多信息见 Section 54.4。

CopyInResponse、CopyOutResponse 和 CopyBothResponse 消息 包含一些字段,用于告知前端每行的列数以及每列使用的格式代码。 (就目前的实现而言,给定 COPY 操作中的 所有列都使用相同的格式,但消息设计并未做此假设。)

54.2.7. 异步操作 #

在若干情况下,后端会发送一些并非由前端命令流特别引发的消息。 前端必须随时准备好处理这些消息,即使当前没有在执行查询。 至少,应当在开始读取查询响应之前检查这些情况。

NoticeResponse 消息有可能因外部活动而产生; 例如,如果数据库管理员命令执行一次“快速”数据库关闭, 后端会在关闭连接之前发送一条 NoticeResponse 来表明这一事实。 因此,前端应当始终准备好接受并显示 NoticeResponse 消息, 即使连接名义上处于空闲状态。

只要后端认为前端应当知晓的任何参数的活动值发生变化, 就会产生 ParameterStatus 消息。最常见的情况是, 这是对前端执行的 SET SQL 命令的响应, 这种情况实际上是同步的 —— 但参数状态变化也可能因为 管理员更改了配置文件然后向服务器发送 SIGHUP 信号而发生。 另外,如果一条 SET 命令被回滚, 也会产生一条适当的 ParameterStatus 消息来报告当前的有效值。

目前,有一个硬编码的参数集合,会为它们产生 ParameterStatus。它们是:

application_namescram_iterations
client_encodingsearch_path
DateStyleserver_encoding
default_transaction_read_onlyserver_version
in_hot_standbysession_authorization
integer_datetimesstandard_conforming_strings
IntervalStyleTimeZone
is_superuser 

(14 之前的版本不会报告 default_transaction_read_only 和 in_hot_standby;16 之前的版本不会报告 scram_iterations;18 之前的版本不会报告 search_path。) 注意, server_version、 server_encoding 和 integer_datetimes 是伪参数,在启动之后不会变化。 这个集合将来可能变化,甚至可能变得可配置。 因此,对于它不理解或不关心的参数, 前端应当直接忽略其 ParameterStatus。

如果前端发出一条 LISTEN 命令, 那么只要对同一个频道名执行了 NOTIFY 命令, 后端就会发送一条 NotificationResponse 消息 (不要与 NoticeResponse 混淆!)。

Note

目前,NotificationResponse 只能在事务之外发送, 因此它不会出现在命令-响应序列的中间, 尽管它可能恰好出现在 ReadyForQuery 之前。 不过,把前端逻辑设计成依赖这一点是不明智的。 良好的做法是能够在协议的任何一点接受 NotificationResponse。

54.2.8. 取消正在执行的请求 #

在处理一个查询的过程中,前端可能会请求取消该查询。 出于实现效率的考虑,取消请求不会直接在通向该后端的打开连接上发送: 我们不希望后端在查询处理期间不停地检查来自前端的新输入。 取消请求应当相对少见,所以我们让它略微繁琐一些, 以避免在正常情况下带来性能损失。

要发出取消请求,前端会打开一条到服务器的新连接, 并发送一条 CancelRequest 消息,而不是通常在新建连接上 发送的 StartupMessage 消息。服务器会处理这个请求,然后关闭连接。 出于安全原因,对取消请求消息不会做任何直接回复。

除非 CancelRequest 消息包含在连接启动期间传给前端的 相同密钥数据(PID 和秘密密钥),否则它会被忽略。 如果该请求与某个当前正在执行的后端的 PID 和秘密密钥匹配, 当前查询的处理就会被中止。 (在现有实现中,这是通过向正在处理查询的后端进程 发送一个特殊信号来完成的。)

取消信号可能起作用,也可能不起作用 —— 例如,如果它到达时后端已经完成了查询的处理, 那么它将没有任何作用。如果取消生效, 其结果是当前命令被提前终止并给出一条错误消息。

上述一切的最终结果是:出于安全和效率两方面的原因, 前端没有办法直接得知一个取消请求是否成功。 它必须继续等待后端对查询做出响应。 发出取消请求只是提高了当前查询很快结束的概率, 以及它以错误消息失败而不是成功的概率。

由于取消请求是通过一条新建的连接发送到服务器的, 而不是通过常规的前端/后端通信链路发送的, 因此任何进程都可以发出取消请求,而不仅仅是其查询 要被取消的那个前端。这在构建多进程应用时 可能带来额外的灵活性。它也带来了一个安全风险: 未授权的人可能试图取消查询。 通过要求在取消请求中提供一个动态生成的秘密密钥, 可以应对这一安全风险。

54.2.9. 连接终止 #

正常、优雅的终止流程是:前端发送一条 Terminate 消息 并立即关闭连接。收到这条消息后,后端会关闭连接并终止。

在少数情况下(例如管理员命令的数据库关闭), 后端可能在没有任何前端请求的情况下断开连接。 在这种情况下,后端会在关闭连接之前尽力发送一条错误或通知消息, 说明断开的原因。

其他终止场景来自各种失败情况, 例如某一端发生核心转储、通信链路丢失、 消息边界同步丢失等。无论前端还是后端, 只要看到连接被意外关闭,就应当进行清理并终止。 如果前端不想自行终止,它也可以选择重新联系服务器 来启动一个新的后端。如果收到了无法识别的消息类型, 也建议关闭连接,因为这很可能意味着消息边界同步已丢失。

无论是正常终止还是异常终止,任何打开的事务都会被回滚,而不是提交。 不过应当注意,如果前端在一个非 SELECT 查询的处理期间断开连接,后端很可能在注意到断开之前 就已经完成了查询。如果该查询在任何事务块 (BEGIN ... COMMIT 序列) 之外,那么它的结果可能会在断开被识别之前就被提交。

54.2.10. SSL 会话加密 #

如果 PostgreSQL 在构建时启用了 SSL 支持,则前端/后端通信可以 使用 SSL 加密。在攻击者可能能够捕获会话流量的 环境中,这提供了通信安全性。关于用 SSL 加密 PostgreSQL 会话的更多信息,见 Section 18.9。

要发起一条 SSL 加密的连接, 前端最初发送的是一条 SSLRequest 消息,而不是 StartupMessage。 然后服务器会用一个字节来响应,其中包含 S 或 N,分别表示它愿意或不愿意执行 SSL。如果前端对响应不满意, 它可以在此时关闭连接。若收到 S 后要继续, 则与服务器执行一次 SSL 启动握手 (这里不描述,它是 SSL 规范的一部分)。 如果成功,则继续发送常规的 StartupMessage。在这种情况下, StartupMessage 以及所有后续数据都会被 SSL 加密。若收到 N 后要继续, 则发送常规的 StartupMessage 并在不加密的情况下继续。 (另外,也允许在收到 N 响应之后 发出一条 GSSENCRequest 消息,尝试改用 GSSAPI 加密。)

前端还应当准备好处理服务器对 SSLRequest 响应的 ErrorMessage。 前端不应向用户/应用显示这条错误消息,因为服务器尚未被认证 (CVE-2024-10977)。 在这种情况下必须关闭连接, 但前端可以选择打开一条新连接并在不请求 SSL 的情况下继续。

当可以执行 SSL 加密时, 服务器应当只发送单个 S 字节, 然后等待前端发起 SSL 握手。 如果此时还有额外的字节可供读取,那很可能意味着 一个中间人正在尝试进行缓冲区填充攻击 (CVE-2021-23222)。 前端的编码应当是:要么在把套接字交给其 SSL 库之前 从套接字中精确读取一个字节, 要么在发现自己读到了额外字节时将其视为违反协议。

同样,服务器期望客户端在收到服务器对 SSL 请求的单字节响应之前, 不要开始 SSL 协商。如果客户端 不等待服务器响应就已收到便立即开始 SSL 协商, 它可以减少一个往返的连接延迟。然而这样做的代价是 无法处理服务器对 SSL 请求给出否定响应的情况。 在这种情况下,服务器不会继续 GSSAPI 或未加密连接或协议错误, 而是直接断开连接。

一条初始的 SSLRequest 也可以用于为了发送 CancelRequest 消息 而打开的连接。

还有第二种发起 SSL 加密的替代方式。 服务器会识别那些不做任何先前的 SSLRequest 分组 就立即开始 SSL 协商的连接。 一旦 SSL 连接建立, 服务器会期待一个正常的启动请求分组, 并在加密信道上继续协商。在这种情况下, 任何其他加密请求都会被拒绝。对于通用工具而言 不推荐这种方法,因为它无法协商出可用的最佳连接加密方式, 也无法处理未加密的连接。不过,对于服务器和客户端 被一起管控的环境,它很有用。在这种情况下, 它省去了一个往返的延迟,并允许使用依赖标准 SSL 连接的网络工具。 以这种方式使用 SSL 连接时, 要求客户端使用 RFC 7301 定义的 ALPN 扩展,以防范协议混淆攻击。 PostgreSQL 协议是 "postgresql",已在 IANA TLS ALPN Protocol IDs 注册表中注册。

虽然协议本身没有提供让服务器强制使用 SSL 加密的方法, 但管理员可以把服务器配置为在认证检查过程中 拒绝未加密的会话。

54.2.11. GSSAPI 会话加密 #

如果 PostgreSQL 在构建时启用了 GSSAPI 支持,则前端/后端通信可以 使用 GSSAPI 加密。在攻击者可能能够捕获 会话流量的环境中,这提供了通信安全性。 关于用 GSSAPI 加密 PostgreSQL 会话的更多信息, 见 Section 18.10。

要发起一条 GSSAPI 加密的连接, 前端最初发送的是一条 GSSENCRequest 消息,而不是 StartupMessage。 然后服务器会用一个字节来响应,其中包含 G 或 N,分别表示它愿意或不愿意执行 GSSAPI 加密。如果前端对响应不满意, 它可以在此时关闭连接。若收到 G 后要继续, 则按照 RFC 2744 或等效方式使用 GSSAPI C 绑定,在一个循环中调用 gss_init_sec_context() 执行 GSSAPI 初始化,并把结果发送给服务器: 从空输入开始,然后使用来自服务器的每个结果, 直到它不再返回输出。把 gss_init_sec_context() 的结果发送给服务器时, 要以网络字节序的四字节整数在前面加上消息的长度。 若收到 N 后要继续, 则发送常规的 StartupMessage 并在不加密的情况下继续。 (另外,也允许在收到 N 响应之后 发出一条 SSLRequest 消息,尝试改用 SSL 加密。)

前端还应当准备好处理服务器对 GSSENCRequest 响应的 ErrorMessage。 前端不应向用户/应用显示这条错误消息,因为服务器尚未被认证 (CVE-2024-10977)。 在这种情况下必须关闭连接, 但前端可以选择打开一条新连接并在不请求 GSSAPI 加密的情况下继续。

当可以执行 GSSAPI 加密时, 服务器应当只发送单个 G 字节, 然后等待前端发起 GSSAPI 握手。 如果此时还有额外的字节可供读取,那很可能意味着 一个中间人正在尝试进行缓冲区填充攻击 (CVE-2021-23222)。 前端的编码应当是:要么在把套接字交给其 GSSAPI 库之前 从套接字中精确读取一个字节, 要么在发现自己读到了额外字节时将其视为违反协议。

一条初始的 GSSENCRequest 也可以用于为了发送 CancelRequest 消息 而打开的连接。

一旦 GSSAPI 加密成功建立, 就用 gss_wrap() 加密常规的 StartupMessage 以及所有后续数据,并在实际加密载荷之前, 以网络字节序的四字节整数加上 gss_wrap() 结果的长度。 注意,服务器只接受客户端发来的小于 16kB 的加密分组; 客户端应使用 gss_wrap_size_limit() 来确定在此限制之内能容纳的未加密消息的大小, 更大的消息应拆分为多次 gss_wrap() 调用。 典型的分段是 8kB 的未加密数据, 产生的加密分组略大于 8kB,但远在 16kB 的上限之内。 可以预期服务器发送给客户端的加密分组不会超过 16kB。

虽然协议本身没有提供让服务器强制使用 GSSAPI 加密的方法, 但管理员可以把服务器配置为在认证检查过程中 拒绝未加密的会话。