55.2. 消息流 #
本节描述消息流以及各种消息类型的语义(每种消息的精确格式见第 55.7 节)。根据连接所处的状态不同,存在若干不同的子协议:启动、查询、函数调用、COPY 以及终止。对于异步操作(包括通知响应和命令取消)还有专门规定,它们可能在启动阶段结束后的任何时刻发生。
55.2.1. 启动 #
要开始一个会话,前端会打开到服务器的连接并发送一条启动消息。该消息包含用户名以及用户希望连接的数据库名;它还指明要使用的协议版本。(启动消息也可以选择性地包含运行时参数的附加设置。)随后服务器会结合这些信息以及配置文件(例如 pg_hba.conf)的内容,初步判断是否接受该连接,以及需要何种额外认证(如果需要)。
接着服务器会发送适当的认证请求消息,前端必须以适当的认证响应消息进行回应(例如密码)。对于除 GSSAPI、SSPI 和 SASL 之外的认证方法,最多只会有一次请求和一次响应。在某些方法中,前端根本不需要发送响应,因此也不会出现认证请求。对于 GSSAPI、SSPI 和 SASL,则可能需要多轮报文交换才能完成认证。
认证周期要么以服务器拒绝连接(ErrorResponse)结束,要么以 AuthenticationOk 结束。
在这个阶段,服务器可能发送以下消息:
- ErrorResponse
连接请求被拒绝。然后服务器马上关闭连接。
- 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()函数返回的是十六进制字符串。)- AuthenticationGSS
前端现在必须发起一次 GSSAPI 协商。前端将发送一条带有 GSSAPI 数据流第一部分的 GSSResponse 消息作为响应。如果还需要进一步的消息,服务器会响应 AuthenticationGSSContinue。
- AuthenticationSSPI
前端现在必须发起一次 SSPI 协商。前端将发送一条带有 SSPI 数据流第一部分的 GSSResponse 作为响应。如果还需要进一步的消息,服务器会响应 AuthenticationGSSContinue。
- AuthenticationGSSContinue
该消息包含前一步 GSSAPI 或 SSPI 协商(AuthenticationGSS、AuthenticationSSPI 或前一个 AuthenticationGSSContinue)的响应数据。如果该消息中的 GSSAPI 或 SSPI 数据表明完成认证还需要更多数据,前端必须把所需数据作为另一条 GSSResponse 发送。如果该消息已完成 GSSAPI 或 SSPI 认证,服务器接下来会发送 AuthenticationOk 表示认证成功,或发送 ErrorResponse 表示认证失败。
- AuthenticationSASL
前端现在必须发起一次 SASL 协商,使用该消息中列出的一种 SASL 机制。前端将发送一条 SASLInitialResponse,其中包含所选机制的名称以及 SASL 数据流的第一部分。如果还需要进一步的消息,服务器会响应 AuthenticationSASLContinue。详见第 55.3 节。
- AuthenticationSASLContinue
该消息包含 SASL 协商前一步(AuthenticationSASL 或前一个 AuthenticationSASLContinue)的挑战数据。前端必须用一条 SASLResponse 消息响应。
- AuthenticationSASLFinal
SASL 认证已完成,并附带了面向客户端的额外机制相关数据。服务器接下来会发送 AuthenticationOk 表示认证成功,或发送 ErrorResponse 表示认证失败。只有当 SASL 机制规定在完成时要从服务器向客户端发送附加数据时,才会发送该消息。
- NegotiateProtocolVersion
服务器不支持客户端请求的协议次版本,但支持更早的协议版本;此消息指明其所支持的最高次版本。如果客户端在启动包中请求了不受支持的协议选项(即以
_pq_.开头的选项),也会发送此消息。此消息后面会跟随一条 ErrorResponse 或一条指示认证成功或失败的消息。
如果前端不支持服务器要求的认证方式,那么它应该马上关闭连接。
在收到 AuthenticationOk 消息之后,前端必须继续等待来自服务器的后续消息。在这个阶段,一个后端进程正在启动,而前端只是旁观者。启动尝试仍可能失败(ErrorResponse),服务器也可能拒绝支持所请求的协议次版本(NegotiateProtocolVersion);但通常情况下,后端会发送一些 ParameterStatus 消息、BackendKeyData,以及最后的 ReadyForQuery。
在这个阶段,后端会尝试应用启动消息中给出的任何额外运行时参数设置。如果成功,这些值就会成为会话的默认值。发生错误则会导致 ErrorResponse 并退出。
这个阶段来自后端的可能消息是:
- BackendKeyData
该消息提供密钥数据。如果前端希望稍后发送取消请求,就必须保存这些数据。前端不应响应该消息,而应继续等待 ReadyForQuery 消息。
- ParameterStatus
该消息告知前端后端参数的当前(初始)设置,例如 client_encoding 或 DateStyle。前端可以忽略这些信息,也可以记录下来供后续使用;详见第 55.2.7 节。前端不应响应该消息,而应继续等待 ReadyForQuery 消息。
- ReadyForQuery
启动成功,前端现在可以发出命令。
- ErrorResponse
启动失败,在发送完这个消息之后连接被关闭。
- NoticeResponse
已发出一条警告消息。前端应显示该消息,但仍应继续等待 ReadyForQuery 或 ErrorResponse。
ReadyForQuery 消息与后端在每个命令周期结束后发出的消息是同一个。前端可根据自身编码需要,将 ReadyForQuery 视为一个命令周期的开始,或视为启动阶段以及后续每个命令周期的结束。
55.2.2. 简单查询 #
一个简单查询周期由前端向后端发送一条 Query 消息来启动。该消息包含一条或多条以文本字符串表示的 SQL 命令。后端随后会根据查询命令串的内容向前端发送一条或多条响应消息,最后再发送一条 ReadyForQuery 响应消息。ReadyForQuery 通知前端它现在可以安全地发送新的命令。(实际上,前端并不一定非要等到收到 ReadyForQuery 才发送下一条命令,但这样一来,前端就必须自己处理“先前命令失败而后续已发出的命令成功”这种情况。)
后端可能返回的响应消息有:
- CommandComplete
一条 SQL 命令正常完成。
- CopyInResponse
后端已准备好把数据从前端复制到表中;参见第 55.2.6 节。
- CopyOutResponse
后端已准备好把数据从表中复制到前端;参见第 55.2.6 节。
- RowDescription
表示即将返回行作为对
SELECT、FETCH等查询的响应。此消息的内容描述了行的列布局。该消息之后,每个返回给前端的行都对应一条 DataRow 消息。- DataRow
由
SELECT、FETCH等查询返回的一组行中的一个。- EmptyQueryResponse
识别出了一条空查询字符串。
- ErrorResponse
发生了一个错误。
- ReadyForQuery
查询字符串的处理已完成。发送一个单独的消息来指示这一点,因为查询字符串可能包含多个 SQL 命令。(CommandComplete 标记了一个 SQL 命令的处理结束,而不是整个字符串的结束。)无论处理是成功还是出现错误,都将始终发送 ReadyForQuery。
- NoticeResponse
与查询相关的警告消息已发出。通知是其他响应的补充,即后端将继续处理命令。
SELECT 查询(或其他返回行集的查询,如 EXPLAIN 或 SHOW)的响应通常包含 RowDescription、零条或多条 DataRow 消息,以及最后的 CommandComplete。在前端与服务器之间执行 COPY 输入或输出时,会使用第 55.2.6 节所述的特殊协议。所有其他类型的查询通常只产生一条 CommandComplete 消息。
由于查询字符串可能包含若干条查询(以分号分隔),因此在后端完成整个查询字符串的处理之前,可能会出现多个这样的响应序列。只有在整个字符串处理完毕且后端已准备好接受新的查询字符串时,才会发出 ReadyForQuery 消息。
如果收到的是一条完全为空的查询字符串(除了空白字符之外没有任何内容),响应就是一条 EmptyQueryResponse,后面跟着 ReadyForQuery。
一旦发生错误,就会发出一条 ErrorResponse 消息,后面跟着 ReadyForQuery。ErrorResponse 会中止该查询字符串中后续所有处理(即使其中还包含其他查询)。请注意,这种情况可能发生在处理单条查询所生成的消息序列中途。
在简单查询模式中,取回值的格式总是文本,除非给出的命令是在使用 BINARY 选项声明的游标上执行 FETCH。在这种情况下,取回的值将采用二进制格式。RowDescription 消息中给出的格式代码会告诉我们使用的是哪种格式。
当前端正在等待其他类型的消息时,也必须准备好接收 ErrorResponse 和 NoticeResponse 消息。参见第 55.2.7 节,了解后端因外部事件而可能生成的消息。
建议以状态机的方式编写前端,使其能够在任何合理的时机接收相应类型的消息,而不把消息确切顺序的假设写死在代码中。
55.2.2.1. 一个简单查询中的多条语句 #
当一个简单查询消息中包含多于一条 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 消息还是后面的一个 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(),除非查询串恰好结束了一个已开始的事务并开启了新事务。
55.2.3. 扩展查询 #
扩展查询协议把上文描述的简单查询协议拆分成多个步骤。准备步骤的结果可以重复使用,从而提高效率。此外,它还提供了额外特性,例如可以把数据值作为独立参数提供,而不必直接插入查询字符串中。
在扩展协议中,前端首先发送一条 Parse 消息,其中包含文本查询字符串、可选的参数占位符数据类型信息,以及目标预备语句对象的名称(空字符串表示未命名预备语句)。响应要么是 ParseComplete,要么是 ErrorResponse。参数数据类型可以用 OID 指定;如果未给出,解析器会像处理无类型字面字符串常量那样尝试推断其数据类型。
注意
一个参数的数据类型可以通过设为零,或让参数类型 OID 数组短于查询字符串中参数符号($n)的数量来保持未指定。另一个特例是,参数类型可以指定为 void(即伪类型 void 的 OID)。这样做是为了允许参数符号用于那些实际上是 OUT 参数的函数参数。通常并不存在可使用 void 参数的上下文,但如果这样的参数符号出现在函数参数列表中,它实际上会被忽略。例如,像 foo($1,$2,$3,$4) 这样的函数调用,如果 $3 和 $4 被指定为 void 类型,就可能匹配一个带有两个 IN 参数和两个 OUT 参数的函数。
注意
Parse 消息中的查询字符串不能包含多于一条 SQL 语句;否则会报告语法错误。这个限制在简单查询协议中并不存在,但在扩展协议中必须如此,因为若允许预备语句或 portal 包含多条命令,会使协议变得过于复杂。
如果成功创建了一个有名的预备语句对象,它会一直持续到当前会话结束,除非被显式销毁。未命名预备语句只会持续到下一条把未命名语句作为目标的 Parse 消息发出为止。(注意,简单 Query 消息也会销毁未命名语句。)有名预备语句在被另一条 Parse 消息重新定义之前必须显式关闭,但未命名语句则无此要求。有名预备语句也可以在 SQL 命令级别通过 PREPARE 和 EXECUTE 来创建和访问。
一旦预备语句存在,就可以用 Bind 消息把它准备为可执行状态。Bind 消息给出源预备语句的名称(空字符串表示未命名预备语句)、目标 portal 的名称(空字符串表示未命名 portal),以及预备语句中所有参数占位符应使用的值。所提供的参数集必须与预备语句所需参数相匹配。(如果你在 Parse 消息中声明了任何 void 参数,那么在 Bind 消息中应为它们传递 NULL 值。)Bind 还会指定查询返回数据所使用的格式;格式既可以统一指定,也可以按列指定。响应要么是 BindComplete,要么是 ErrorResponse。
注意
输出采用文本还是二进制格式,由 Bind 中给出的格式代码决定,而与涉及的 SQL 命令无关。在使用扩展查询协议时,游标声明中的 BINARY 属性并不起作用。
通常会在处理 Bind 消息时进行查询规划。如果预备语句没有参数,或者会被重复执行,服务器可能会保存生成的计划,并在后续针对同一预备语句的 Bind 消息中重用它。不过,只有当它发现可以创建一个效率并不比依赖具体参数值的计划差很多的通用计划时,才会这样做。就协议而言,这一切都是透明的。
如果成功创建了一个有名 portal 对象,它会一直持续到当前事务结束,除非被显式销毁。未命名 portal 会在事务结束时销毁,或者在下一条把未命名 portal 作为目标的 Bind 消息发出时立即销毁。(注意,简单 Query 消息也会销毁未命名 portal。)有名 portal 在被另一条 Bind 消息重新定义之前必须显式关闭,而未命名 portal 则无此要求。有名 portal 也可以在 SQL 命令级别通过 DECLARE CURSOR 和 FETCH 来创建和访问。
一旦 portal 存在,就可以使用 Execute 消息执行它。Execute 消息指定 portal 的名称(空字符串表示未命名 portal)以及一个最大的结果行计数(零表示“取回全部行”)。结果行计数只对包含返回行集命令的 portal 有意义;在其他情况下,命令总会执行到完成,而行计数会被忽略。Execute 的可能响应与通过简单查询协议发出的查询相同,只是 Execute 不会导致后端发送 ReadyForQuery 或 RowDescription。
如果 Execute 在 portal 执行完成之前终止(因为达到了非零的结果行计数),它会发送一条 PortalSuspended 消息;该消息表明前端应当针对同一个 portal 再发出一条 Execute 消息,以完成此次操作。在 portal 执行完成之前,不会发送表示源 SQL 命令结束的 CommandComplete 消息。因此,一个 Execute 阶段总会以下列消息中的恰好一条结束:CommandComplete、EmptyQueryResponse(如果 portal 是从空查询字符串创建的)、ErrorResponse 或 PortalSuspended。
每一组扩展查询消息完成后,前端都应发送一条 Sync 消息。这条无参数消息会让后端关闭当前事务,如果当前事务并不处在 BEGIN/COMMIT 事务块内(这里的“关闭”指的是:无错误则提交,有错误则回滚)。随后会发送一条 ReadyForQuery 响应。Sync 的目的是为错误恢复提供一个重新同步点。如果在处理任何扩展查询消息时检测到错误,后端会发出 ErrorResponse,然后持续读取并丢弃消息,直到遇到 Sync,再发送 ReadyForQuery 并回到正常的消息处理流程。(但要注意,如果是在处理 Sync 期间检测到错误,则不会跳过任何消息,这样就能保证每条 Sync 恰好对应一条 ReadyForQuery。)
注意
Sync 不会关闭由 BEGIN 打开的事务块。之所以能够检测出这种情况,是因为 ReadyForQuery 消息中包含事务状态信息。
除了这些基本的、必需的操作之外,在扩展查询协议里还有几种可选的操作可以使用。
Describe 消息(portal 变体)指定一个现有 portal 的名称(或者用空字符串表示未命名 portal)。响应要么是一条 RowDescription 消息,用于描述执行该 portal 时将返回的行;要么是一条 NoData 消息,如果该 portal 不包含会返回行的查询;要么是 ErrorResponse,如果不存在这样的 portal。
Describe 消息(语句变体)指定一个现有预备语句的名称(或者用空字符串表示未命名预备语句)。响应是一条 ParameterDescription 消息,用于描述该语句所需的参数,随后是一条 RowDescription 消息,用于描述该语句最终执行时将返回的行(如果该语句不返回行,则为 NoData 消息)。如果不存在这样的预备语句,则返回 ErrorResponse。请注意,由于还没有发出 Bind,后端尚不知道返回列将使用什么格式;因此在这种情况下,RowDescription 消息中的格式代码字段将为零。
提示
在大多数场景下,前端都应在发出 Execute 之前先发送某一种 Describe 变体,以确保它知道应如何解释接收到的结果。
Close 消息会关闭一个现有的预备语句或 portal,并释放相关资源。针对不存在的语句或 portal 发出 Close 并不算错误。响应通常是 CloseComplete,但如果在释放资源时遇到困难,也可能返回 ErrorResponse。请注意,关闭预备语句会隐式关闭所有基于该语句构造出的打开 portal。
Flush 消息本身不会产生任何特定输出,但会强制后端发送其输出缓冲区中所有尚待发送的数据。如果前端希望在发出更多命令之前检查某条扩展查询命令的结果,那么除了 Sync 之外,任何扩展查询命令之后都必须发送 Flush。如果没有 Flush,后端返回的消息会尽量合并成最少的数据包,以降低网络开销。
注意
简单 Query 消息大致等价于一串 Parse、Bind、portal Describe、Execute、Close、Sync 操作,它们使用未命名预备语句和未命名 portal 对象,并且不带参数。不同之处在于,简单 Query 会接受包含多条 SQL 语句的查询字符串,并依次自动为每条语句执行绑定、描述和执行序列;另一个区别是它不会返回 ParseComplete、BindComplete、CloseComplete 或 NoData 消息。
55.2.4. 管道化 #
扩展查询协议允许管道化,即连续发送一系列查询,而不等待先前查询完成。这减少了完成一组操作所需的网络往返次数。不过,用户必须仔细考虑其中某个步骤失败时所需的行为,因为后续查询已经在发往服务器的途中。
处理这个问题的一种方式是让整组查询成为单个事务,即用 BEGIN ... COMMIT 包围它们。不过,如果希望其中某些命令独立于其他命令提交,这种方式就无法满足要求。
扩展查询协议提供了另一种处理方式,即省略相互依赖的步骤之间的 Sync 消息。发生错误后,后端会跳过命令消息,直到遇到 Sync,因此当前面的命令失败时,管道中的后续命令便会自动跳过,而不必由客户端用 BEGIN 和 COMMIT 显式管理。管道中可独立提交的各段可以用 Sync 消息分隔。
如果客户端没有发出显式的 BEGIN,那么每个 Sync 通常会在前序步骤成功时引发隐式的 COMMIT,失败时引发隐式的 ROLLBACK。不过,少数 DDL 命令(例如 CREATE DATABASE)不能在事务块中执行。如果这类命令在管道中执行,除非它是管道中的第一条命令,否则会失败。此外,它成功后会强制立即提交,以保持数据库一致性。因此,紧随这类命令之后的 Sync 除了返回 ReadyForQuery 外,不产生其他效果。
使用这种方式时,必须统计 ReadyForQuery 消息的数量,并等待该数量达到已发送的 Sync 消息数量,才能确定管道已完成。统计命令完成响应并不可靠,因为某些命令可能被跳过,因而不会产生完成消息。
55.2.5. 函数调用 #
函数调用子协议允许客户端请求直接调用数据库 pg_proc 系统目录中的任意函数。客户端必须具有该函数的执行权限。
注意
函数调用子协议是一个遗留的特性,在新代码里可能最好避免用它。类似的结果可以通过设置一个执行 SELECT function($1, ...) 的预备语句得到。这样函数调用周期就可以用 Bind/Execute 代替。
函数调用周期由前端向后端发送一条 FunctionCall 消息来启动。后端随后根据函数调用的结果发送一条或多条响应消息,最后发送一条 ReadyForQuery 响应消息。ReadyForQuery 告知前端,可以安全地发送新的查询或函数调用。
来自后端的可能的响应消息是:
- ErrorResponse
发生了一个错误。
- FunctionCallResponse
函数调用完成并且在消息中返回一个结果(请注意函数调用协议只能处理单个标量结果,不能处理行类型或者结果集合)。
- ReadyForQuery
函数调用处理完成。ReadyForQuery 将总是被发送,不管是成功完成处理还是发生一个错误。
- NoticeResponse
发出了一条有关该函数调用的警告信息。通知是附加在其他响应上的,也就是说,后端将继续处理该命令。
55.2.6. COPY 操作 #
COPY 命令允许在服务器和客户端之间进行高速大批量数据传输。拷贝入和拷贝出操作每个都把连接切换到一个独立的子协议中,并且持续到操作结束。
拷贝入模式(向服务器传输数据)在后端执行 COPY FROM STDIN SQL 语句时启动。后端会向前端发送一条 CopyInResponse 消息。随后前端应发送零条或多条 CopyData 消息,构成一条输入数据流。(消息边界与行边界之间没有任何对应关系要求,尽管让它们对齐通常是合理的选择。)前端可以通过发送 CopyDone 消息来结束拷贝入模式(允许成功结束),也可以发送 CopyFail 消息(这会使 COPY 语句以错误失败)。然后后端会恢复到 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 语句时启动的。后端发出一个 CopyOutResponse 消息给前端,后面跟着零或者多个 CopyData 消息(总是每行一个),然后跟着 CopyDone。然后后端回退到它在 COPY 开始之前的命令处理模式,然后发送 CommandComplete。前端不能中止传输(除非是关闭连接或者发出一个 Cancel 请求),但是它可以抛弃不需要的 CopyData 和 CopyDone 消息。
在拷贝出模式中,如果后端检测到错误,那么它将发出一个 ErrorResponse 消息并且回到正常的处理。前端应该把收到 ErrorResponse 当作终止拷贝出模式的标志。
NoticeResponse 和 ParameterStatus 消息可能穿插在 CopyData 消息之间;前端必须处理这些情况,并应准备好处理其他异步消息类型(参见第 55.2.7 节)。除此之外,可以将任何除 CopyData 或 CopyDone 以外的消息类型视为拷贝出模式的终止标志。
还有另一种与拷贝相关的模式,称为双向拷贝(copy-both),它允许高速批量地向服务器发送数据以及从服务器接收数据。当处于 WAL 发送进程模式的后端执行 START_REPLICATION 语句时,会启动双向拷贝模式。后端会向前端发送一条 CopyBothResponse 消息。此后,前端和后端都可以发送 CopyData 消息,直到任一方发送 CopyDone 消息。客户端发送 CopyDone 后,连接会从双向拷贝模式切换到拷贝出模式,客户端也不得再发送 CopyData。类似地,当服务器发送 CopyDone 后,连接会进入拷贝入模式,服务器也不得再发送 CopyData。当双方都发送完 CopyDone 后,拷贝模式结束,后端恢复到原先的命令处理模式。如果双向拷贝模式期间发生后端检测到的错误,后端会发出 ErrorResponse,丢弃前端消息直到收到 Sync,然后发出 ReadyForQuery 并返回正常处理。前端应将收到 ErrorResponse 视为双向拷贝终止的信号;在这种情况下不应再发送 CopyDone。关于在双向拷贝模式上传输的子协议,见第 55.4 节。
CopyInResponse、CopyOutResponse 和 CopyBothResponse 消息包含一些字段,用于告知前端每行的列数以及每列所使用的格式代码。(在当前实现中,同一次 COPY 操作的所有列都使用相同格式,但消息设计并不作此假设。)
55.2.7. 异步操作 #
在若干情况下,后端会发送并非由前端命令流直接触发的消息。前端必须随时准备处理这些消息,即使当前并未处于查询过程中。至少,在开始读取查询响应之前应检查这些情况。
NoticeResponse 消息可能因外部活动而产生;例如,如果数据库管理员发起一次“快速”数据库关闭,后端会在关闭连接之前发送一条 NoticeResponse 说明这一事实。因此,前端应始终准备好接收并显示 NoticeResponse 消息,即使连接表面上处于空闲状态。
只要后端认为前端应当知晓的某个参数的当前有效值发生变化,就会生成 ParameterStatus 消息。最常见的情况是响应前端执行的 SET 命令,这种情况实际上是同步的;但也可能是管理员修改了配置文件,然后向服务器发送 SIGHUP 信号,从而导致参数状态发生变化。同样,如果某条 SET 命令被回滚,也会生成适当的 ParameterStatus 消息,用于报告当前生效的值。
目前,会为一组固定的参数生成 ParameterStatus,参数如下:
application_name | is_superuser |
client_encoding | scram_iterations |
DateStyle | server_encoding |
default_transaction_read_only | server_version |
in_hot_standby | session_authorization |
integer_datetimes | standard_conforming_strings |
IntervalStyle | TimeZone |
(8.0 之前的版本不报告 server_encoding、TimeZone 和 integer_datetimes;8.1 之前的版本不报告 standard_conforming_strings;8.4 之前的版本不报告 IntervalStyle;9.0 之前的版本不报告 application_name;14 之前的版本不报告 default_transaction_read_only 和 in_hot_standby;16 之前的版本不报告 scram_iterations。)注意,server_version、server_encoding 和 integer_datetimes 是启动后不能改变的伪参数。这组参数将来可能变化,甚至可能变为可配置。因此,前端应直接忽略其不理解或不关心的参数的 ParameterStatus。
如果前端发出 LISTEN 命令,那么每当针对同一通道名执行 NOTIFY 命令时,后端都会发送一条 NotificationResponse 消息(不要与 NoticeResponse 混淆)。
注意
目前,NotificationResponse 只能在一个事务外面发送,因此它将不会在一个命令响应序列中间出现,但是它可能正好在 ReadyForQuery 之前出现。不过,在前端逻辑中做上述假设是不明智的。好的做法是在协议的任何点上都可以接受 NotificationResponse。
55.2.8. 取消正在处理的请求 #
在一条查询正在处理的时候,前端可以请求取消该查询。这种取消请求不是直接通过打开的连接发送给后端的,这么做是因为实现的效率:我们不希望后端在处理查询的过程中不停地检查前端来的输入。取消请求应该相对而言比较少见,所以我们把取消做得稍微笨拙一些,以便不影响正常状况的性能。
要发出取消请求,前端会新建到服务器的连接,并发送 CancelRequest 消息,而不是新连接通常发送的 StartupMessage 消息。服务器处理该请求后便会关闭连接。出于安全原因,服务器不会直接回复取消请求消息。
除非 CancelRequest 消息包含在连接启动过程中传递给前端的相同的密钥数据(PID 和密钥),否则它将被忽略。如果该请求匹配当前运行着的后端的 PID 和密钥,则中止当前查询的处理(目前的实现里采用的方法是向正在处理该查询的后端进程发送一个特殊的信号)。
取消信号可能有效,也可能无效;例如,如果它在后端已经处理完查询之后才到达,就不会起作用。如果取消生效,当前命令就会以一条错误消息提前终止。
这么做是对安全性和效率通盘考虑的结果,前端没有直接的方法获知一个取消请求是否成功。它必须继续等待后端对查询的响应。发出一个取消仅仅是增加了当前查询快些结束的可能性,同时也增加了当前查询会伴随着一条错误消息失败而不是成功执行的可能性。
由于取消请求是通过一条新的连接发送给服务器,而不是通过常规的前端/后端通信链路发送,因此发出取消请求的可以是任意进程,而不一定是待取消查询所属的那个前端。这为构建多进程应用提供了额外的灵活性,同时也带来了安全风险,因为未授权用户可能会尝试取消查询。通过要求在取消请求中提供动态生成的密钥,可以缓解这一安全风险。
55.2.9. 终止 #
通常优雅的终止过程是前端发送一条 Terminate 消息并且立刻关闭连接。一旦收到消息,后端马上关闭连接并且终止。
在少数情况下(比如一个管理员命令数据库关闭),后端可能在没有任何前端请求的情况下断开连接。在这种情况下,后端将在它断开连接之前尝试发送一个错误或者通知消息给出断开的原因。
其他终止场景来自各种故障,例如任一端发生 core dump、通信链路中断、消息边界同步丢失等。如果前端或后端看到连接意外关闭,就应清理并终止。若前端不想自行终止,也可以重新联系服务器以启动一个新的后端。如果收到无法识别的消息类型,同样建议关闭连接,因为这通常意味着消息边界同步已经丢失。
不管是正常还是不正常的终止,任何打开的事务都会回滚而不是提交。不过,我们应该注意的是如果一个前端在一个非 SELECT 查询正在处理的时候断开,那么后端很可能在发现断开之前先完成查询的处理。如果查询处于任何事务块之外(BEGIN ... COMMIT 序列),那么其结果可能在发现连接断开之前被提交。
55.2.10. SSL 会话加密 #
如果编译 PostgreSQL 时启用了 SSL 支持,那么前端/后端通信就可以使用 SSL 加密。这为攻击者可能截获会话流量的环境提供了通信安全性。有关使用 SSL 加密 PostgreSQL 会话的更多信息,请参阅第 19.9 节。
要发起一条使用 SSL 加密的连接,前端首先发送 SSLRequest 消息,而不是 StartupMessage。随后服务器会响应一个包含 S 或 N 的单字节,分别表示它愿意或不愿意执行 SSL 加密。如果前端对该响应不满意,此时可以关闭连接。若要在收到 S 后继续,就先与服务器完成 SSL 启动握手(这里不做描述,它属于 SSL 规范的一部分)。如果成功,再继续发送通常的 StartupMessage。在这种情况下,StartupMessage 以及后续所有数据都会被 SSL 加密。若要在收到 N 后继续,则发送通常的 StartupMessage,并在不使用加密的情况下继续。(另外,可以在 N 响应之后发出 GSSENCRequest 消息,尝试使用 GSSAPI 加密代替 SSL。)
前端也应准备处理服务器对 SSLRequest 返回的 ErrorMessage。前端不应将该错误展示给用户/应用,因为此时服务器尚未完成认证(CVE-2024-10977)。遇到这种情况必须关闭连接,但前端可以选择重新建立连接并在不请求 SSL 的情况下继续。
当可以执行 SSL 加密时,服务器应仅发送单个 S 字节,然后等待前端启动 SSL 握手。如果此时有其他可读取的字节,则很可能意味着中间人正在尝试执行缓冲区填充攻击(CVE-2021-23222)。前端应该编写代码,要么从套接字中恰好读取一个字节,然后将套接字交给所用的 SSL 库,要么在发现已经读取到额外的字节时将其视为协议违规。
如果建立连接是为了发送 CancelRequest 消息,也可以先发送 SSLRequest。
虽然协议本身没有提供让服务器强制使用 SSL 加密的方法,但管理员可以配置服务器,使其在认证检查中拒绝未加密的会话。
55.2.11. GSSAPI 会话加密 #
如果 PostgreSQL 构建时启用了 GSSAPI 支持,就可以使用 GSSAPI 对前端/后端通信进行加密。这为攻击者可能截获会话流量的环境提供了通信安全性。有关使用 GSSAPI 加密 PostgreSQL 会话的详细信息,请参阅第 19.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 加密代替 GSSAPI。)
前端也应准备处理服务器对 GSSENCRequest 返回的 ErrorMessage。前端不应将该错误展示给用户/应用,因为此时服务器尚未完成认证(CVE-2024-10977)。遇到这种情况必须关闭连接,但前端可以选择重新建立连接并在不请求 GSSAPI 加密的情况下继续。
当 GSSAPI 加密可用时,服务器应只发送单个 G 字节,然后等待前端启动 GSSAPI 握手。此时如果还有其他可读字节,很可能意味着中间人正在尝试执行缓冲区填充攻击(CVE-2021-23222)。前端应编写代码,要么从套接字中恰好读取一个字节后再把套接字交给其 GSSAPI 库,要么在发现自己已经读到额外字节时将其视为协议违规。
如果建立连接是为了发送 CancelRequest 消息,也可以先发送 GSSENCRequest。
一旦成功建立 GSSAPI 加密连接,就应使用 gss_wrap() 加密通常的 StartupMessage 以及后续所有数据,并在实际加密负载前附上 gss_wrap() 结果长度,该长度以网络字节序的四字节整数表示。请注意,服务器只接受来自客户端、长度小于 16kB 的加密数据包。客户端应使用 gss_wrap_size_limit() 确定符合该限制的未加密消息大小,较大的消息则应拆分为多次 gss_wrap() 调用。典型分段是 8kB 的未加密数据,对应得到略大于 8kB 但明显小于 16kB 的加密数据包。通常可以认为,服务器不会向客户端发送大于 16kB 的加密数据包。
虽然协议本身没有提供让服务器强制使用 GSSAPI 加密的方法,但管理员可以配置服务器,使其在认证检查中拒绝未加密的会话。
报告文档问题
阅读 上游文档. 通过 PostgreSQL 文档反馈表单.