本节描述消息流和每种消息类型的语义。(每种消息的确切表示形式的详细信息请参见第 53.7 节。)根据连接状态,有几种不同的子协议:启动、查询、函数调用、COPY
和终止。还有用于异步操作(包括通知响应和命令取消)的特殊规定,这些操作可以在启动阶段之后的任何时间发生。
要开始会话,前端打开与服务器的连接并发送启动消息。此消息包括用户名和用户要连接的数据库的名称;它还标识要使用的特定协议版本。(可选地,启动消息可以包括运行时参数的其他设置。)然后,服务器使用此信息及其配置文件(例如 pg_hba.conf
)的内容来确定连接是否在初步上可接受,以及需要哪些其他身份验证(如果有)。
然后,服务器发送适当的身份验证请求消息,前端必须使用适当的身份验证响应消息(例如密码)进行回复。对于除 GSSAPI、SSPI 和 SASL 之外的所有身份验证方法,最多只有一个请求和一个响应。在某些方法中,根本不需要前端的任何响应,因此不会发生身份验证请求。对于 GSSAPI、SSPI 和 SASL,可能需要多次交换数据包才能完成身份验证。
身份验证周期以服务器拒绝连接尝试(ErrorResponse)或发送 AuthenticationOk 结束。
在此阶段,服务器可能发送的消息是
连接尝试已被拒绝。然后,服务器立即关闭连接。
身份验证交换已成功完成。
现在,前端必须参与与服务器的 Kerberos V5 身份验证对话(此处未描述,是 Kerberos 规范的一部分)。如果成功,服务器将以 AuthenticationOk 响应,否则将以 ErrorResponse 响应。此方法不再受支持。
前端现在必须发送包含纯文本格式密码的 PasswordMessage。如果密码正确,服务器将以 AuthenticationOk 响应,否则将以 ErrorResponse 响应。
前端现在必须发送包含通过 MD5 加密(带有用户名)的密码的 PasswordMessage,然后使用 AuthenticationMD5Password 消息中指定的 4 字节随机盐再次加密。如果密码正确,服务器将以 AuthenticationOk 响应,否则将以 ErrorResponse 响应。实际的 PasswordMessage 可以在 SQL 中计算为 concat('md5', md5(concat(md5(concat(password, username)), random-salt)))
。(请记住,md5()
函数将其结果作为十六进制字符串返回。)
前端现在必须启动 GSSAPI 协商。前端将发送 GSSResponse 消息,其中包含 GSSAPI 数据流的第一部分,以响应此消息。如果需要更多消息,服务器将以 AuthenticationGSSContinue 响应。
前端现在必须启动 SSPI 协商。前端将发送 GSSResponse,其中包含 SSPI 数据流的第一部分,以响应此消息。如果需要更多消息,服务器将以 AuthenticationGSSContinue 响应。
此消息包含来自 GSSAPI 或 SSPI 协商上一步(AuthenticationGSS、AuthenticationSSPI 或之前的 AuthenticationGSSContinue)的响应数据。如果此消息中的 GSSAPI 或 SSPI 数据表明需要更多数据才能完成身份验证,则前端必须将该数据作为另一个 GSSResponse 消息发送。如果通过此消息完成 GSSAPI 或 SSPI 身份验证,则服务器将接下来发送 AuthenticationOk 以指示身份验证成功,或发送 ErrorResponse 以指示失败。
前端现在必须使用消息中列出的 SASL 机制之一启动 SASL 协商。前端将发送 SASLInitialResponse,其中包含所选机制的名称,以及 SASL 数据流的第一部分,以响应此消息。如果需要更多消息,服务器将以 AuthenticationSASLContinue 响应。有关详细信息,请参见第 53.3 节。
此消息包含来自 SASL 协商上一步(AuthenticationSASL 或之前的 AuthenticationSASLContinue)的质询数据。前端必须使用 SASLResponse 消息进行响应。
SASL 身份验证已完成,并带有客户端的其他特定于机制的数据。服务器将接下来发送 AuthenticationOk 以指示身份验证成功,或发送 ErrorResponse 以指示失败。仅当 SASL 机制指定在完成时将其他数据从服务器发送到客户端时,才发送此消息。
服务器不支持客户端请求的次要协议版本,但支持该协议的早期版本;此消息指示支持的最高次要版本。如果客户端在启动数据包中请求不支持的协议选项(即,以 _pq_.
开头),也会发送此消息。此消息之后将是 ErrorResponse 消息或指示身份验证成功或失败的消息。
如果前端不支持服务器请求的身份验证方法,则应立即关闭连接。
在收到 AuthenticationOk 后,前端必须等待服务器的进一步消息。在此阶段,正在启动后端进程,而前端只是一个感兴趣的旁观者。启动尝试仍然可能失败 (ErrorResponse) 或服务器可能拒绝支持请求的次要协议版本 (NegotiateProtocolVersion),但在正常情况下,后端将发送一些 ParameterStatus 消息、BackendKeyData,最后发送 ReadyForQuery。
在此阶段,后端将尝试应用在启动消息中给出的任何其他运行时参数设置。如果成功,这些值将成为会话默认值。错误会导致 ErrorResponse 并退出。
在此阶段,后端可能发送的消息是
此消息提供密钥数据,如果前端稍后要能够发出取消请求,则必须保存此数据。前端不应响应此消息,而应继续侦听 ReadyForQuery 消息。
此消息通知前端有关后端参数的当前(初始)设置,例如 client_encoding 或 DateStyle。前端可以忽略此消息,或记录设置以供将来使用;有关更多详细信息,请参见第 53.2.7 节。前端不应响应此消息,而应继续侦听 ReadyForQuery 消息。
启动已完成。前端现在可以发出命令。
启动失败。发送此消息后,连接将关闭。
已发出警告消息。前端应显示该消息,但继续侦听 ReadyForQuery 或 ErrorResponse。
ReadyForQuery 消息与后端在每个命令周期后将发出的消息相同。根据前端的编码需求,将 ReadyForQuery 视为开始命令周期是合理的,或者将 ReadyForQuery 视为结束启动阶段和每个后续命令周期也是合理的。
前端向后端发送 Query 消息来启动一个简单的查询周期。该消息包括一个或多个 SQL 命令,以文本字符串形式表示。然后,后端根据查询命令字符串的内容发送一个或多个响应消息,最后发送 ReadyForQuery 响应消息。ReadyForQuery 通知前端,它可以安全地发送新命令。(前端实际上没有必要在发出另一个命令之前等待 ReadyForQuery,但前端必须承担责任,以弄清楚如果较早的命令失败,而已经发出的较晚命令成功会发生什么。)
后端可能发送的响应消息是
SQL 命令正常完成。
后端已准备好将数据从前端复制到表中;请参见第 53.2.6 节。
后端已准备好将数据从表复制到前端;请参见第 53.2.6 节。
指示即将返回行以响应 SELECT
、FETCH
等查询。此消息的内容描述行的列布局。之后,将为返回到前端的每一行发送 DataRow 消息。
由 SELECT
、FETCH
等查询返回的一组行中的一行。
识别到空查询字符串。
发生错误。
查询字符串的处理已完成。发送单独的消息来指示这一点,因为查询字符串可能包含多个 SQL 命令。(CommandComplete 标记一个 SQL 命令处理的结束,而不是整个字符串。)无论处理成功终止还是发生错误,都将始终发送 ReadyForQuery。
已发出与查询相关的警告消息。通知是对其他响应的补充,即后端将继续处理命令。
对 SELECT
查询(或其他返回行集的查询,例如 EXPLAIN
或 SHOW
)的响应通常包括 RowDescription、零个或多个 DataRow 消息,然后是 CommandComplete。与前端之间的 COPY
调用特殊的协议,如第 53.2.6 节中所述。所有其他查询类型通常仅生成 CommandComplete 消息。
由于查询字符串可能包含多个查询(以分号分隔),因此在后端完成查询字符串的处理之前,可能会有多个此类响应序列。当整个字符串都已处理完毕并且后端准备好接受新的查询字符串时,将发出 ReadyForQuery。
如果收到完全为空(除了空格之外没有其他内容)的查询字符串,则响应为 EmptyQueryResponse,后跟 ReadyForQuery。
如果发生错误,则会发出 ErrorResponse,后跟 ReadyForQuery。ErrorResponse 会中止对查询字符串的所有后续处理(即使其中还剩下更多查询)。请注意,这可能发生在单个查询生成的消息序列的中途。
在简单查询模式下,检索到的值的格式始终为文本,除非给定的命令是通过 BINARY
选项声明的游标的 FETCH
。在这种情况下,检索到的值采用二进制格式。RowDescription 消息中给出的格式代码指示正在使用哪种格式。
前端必须准备好在期望任何其他类型的消息时接受 ErrorResponse 和 NoticeResponse 消息。另请参阅第 53.2.7 节,了解后端可能由于外部事件而生成的消息。
建议的做法是以状态机样式编写前端代码,使其在任何时候都能接受任何类型的消息,而不是硬性假设消息的确切顺序。
当一个简单的 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
来关闭现有事务块,那么任何后续语句都将在隐式事务块中执行。相反,如果 BEGIN
出现在多语句 Query 消息中,那么它将启动一个常规事务块,该事务块将仅由显式的 COMMIT
或 ROLLBACK
终止,无论它是否出现在此 Query 消息或之后的某个消息中。如果 BEGIN
遵循一些作为隐式事务块执行的语句,则这些语句不会立即提交;实际上,它们会被追溯到新的常规事务块中。
出现在隐式事务块中的 COMMIT
或 ROLLBACK
将正常执行,关闭隐式块;但是,由于没有先前的 BEGIN
的 COMMIT
或 ROLLBACK
可能代表错误,因此会发出警告。如果后面还有更多语句,则将为它们启动一个新的隐式事务块。
隐式事务块中不允许使用保存点,因为它们会与在任何错误时自动关闭块的行为冲突。
请记住,无论是否存在任何事务控制命令,Query 消息的执行都会在第一个错误时停止。因此,例如给定
BEGIN; SELECT 1/0; ROLLBACK;
在单个 Query 消息中,会话将留在失败的常规事务块内,因为在除零错误之后无法到达 ROLLBACK
。将需要另一个 ROLLBACK
才能将会话恢复到可用状态。
另一个值得注意的行为是,在执行任何查询字符串之前,都会对整个查询字符串进行初始词法和语法分析。因此,后续语句中的简单错误(例如拼写错误的关键字)可能会阻止执行任何语句。这通常对用户不可见,因为在作为隐式事务块完成时,语句无论如何都会回滚。但是,当尝试在多语句 Query 中执行多个事务时,它可能会变得可见。例如,如果一个错别字将我们之前的示例变成
BEGIN; INSERT INTO mytable VALUES(1); COMMIT; INSERT INTO mytable VALUES(2); SELCT 1/0;
那么将不会运行任何语句,从而导致第一个 INSERT
未提交的可见差异。在语义分析或之后检测到的错误,例如拼写错误的表或列名,不会产生此影响。
扩展查询协议将上述简单的查询协议分解为多个步骤。准备步骤的结果可以重复使用多次以提高效率。此外,还提供了其他功能,例如可以将数据值作为单独的参数提供,而不是必须直接将其插入到查询字符串中。
在扩展协议中,前端首先发送 Parse 消息,该消息包含文本查询字符串,可以选择包含有关参数占位符数据类型的信息,以及目标预处理语句对象的名称(空字符串选择未命名的预处理语句)。响应是 ParseComplete 或 ErrorResponse。参数数据类型可以通过 OID 指定;如果未给出,解析器会尝试以与处理未类型化的文字字符串常量相同的方式推断数据类型。
可以通过将其设置为零或通过使参数类型 OID 数组短于查询字符串中使用的参数符号 ($
n
) 的数量来使参数数据类型保持未指定。另一个特殊情况是将参数的类型指定为 void
(即,void
伪类型的 OID)。这是为了允许参数符号用于实际上是 OUT 参数的函数参数。通常,没有可以使用 void
参数的上下文,但是如果这样的参数符号出现在函数的参数列表中,则实际上会忽略它。例如,如果 $3
和 $4
被指定为具有 void
类型,则诸如 foo($1,$2,$3,$4)
之类的函数调用可以匹配具有两个 IN 和两个 OUT 参数的函数。
Parse 消息中包含的查询字符串不能包含多个 SQL 语句;否则会报告语法错误。此限制在简单查询协议中不存在,但在扩展协议中存在,因为允许预处理语句或门户包含多个命令会过度复杂化协议。
如果成功创建,则命名的预处理语句对象将持续到当前会话结束,除非显式销毁。未命名的预处理语句仅持续到发出下一个将未命名语句指定为目标的 Parse 语句。(请注意,简单的 Query 消息也会销毁未命名语句。)必须显式关闭命名的预处理语句,然后才能由另一个 Parse 消息重新定义,但这对于未命名的语句不是必需的。也可以使用 PREPARE
和 EXECUTE
在 SQL 命令级别创建和访问命名的预处理语句。
预处理语句存在后,可以使用 Bind 消息将其准备好执行。Bind 消息给出源预处理语句的名称(空字符串表示未命名的预处理语句)、目标门户的名称(空字符串表示未命名的门户)以及要用于预处理语句中存在的任何参数占位符的值。提供的参数集必须与预处理语句所需的参数集匹配。(如果在 Parse 消息中声明了任何 void
参数,请在 Bind 消息中为它们传递 NULL 值。)Bind 还指定用于查询返回的任何数据的格式;可以整体或按列指定格式。响应是 BindComplete 或 ErrorResponse。
文本和二进制输出之间的选择由 Bind 中给出的格式代码确定,而与所涉及的 SQL 命令无关。使用扩展查询协议时,游标声明中的 BINARY
属性无关紧要。
查询计划通常在处理 Bind 消息时发生。如果预处理语句没有参数,或者被重复执行,则服务器可能会保存创建的计划,并在后续相同预处理语句的 Bind 消息期间重复使用该计划。但是,只有当它发现可以创建一个通用计划,并且该计划的效率并不比依赖于提供的特定参数值的计划低得多时,它才会这样做。就协议而言,这种情况是透明发生的。
如果成功创建,则命名的门户对象将持续到当前事务结束,除非显式销毁。未命名的门户在事务结束时被销毁,或者在发出下一个将未命名门户指定为目标的 Bind 语句时被销毁。(请注意,简单的 Query 消息也会销毁未命名门户。)必须显式关闭命名的门户,然后才能由另一个 Bind 消息重新定义,但这对于未命名的门户不是必需的。也可以使用 DECLARE CURSOR
和 FETCH
在 SQL 命令级别创建和访问命名的门户。
一旦存在游标,就可以使用 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。)
Sync 不会导致使用 BEGIN
打开的事务块被关闭。可以检测到这种情况,因为 ReadyForQuery 消息包含事务状态信息。
除了这些基本必需的操作之外,还有几个可选操作可以与扩展查询协议一起使用。
Describe 消息(游标变体)指定现有游标的名称(或未命名游标的空字符串)。响应是描述通过执行游标将返回的行的 RowDescription 消息;如果游标不包含将返回行的查询,则为 NoData 消息;如果不存在此类游标,则为 ErrorResponse。
Describe 消息(语句变体)指定现有预处理语句的名称(或未命名预处理语句的空字符串)。响应是描述语句所需的参数的 ParameterDescription 消息,后跟描述语句最终执行时将返回的行的 RowDescription 消息(如果语句不返回行,则为 NoData 消息)。如果不存在此类预处理语句,则发出 ErrorResponse。请注意,由于尚未发出 Bind,后端尚不知道要用于返回列的格式;在这种情况下,RowDescription 消息中的格式代码字段将为零。
在大多数情况下,前端应在发出 Execute 之前发出 Describe 的一种或另一种变体,以确保它知道如何解释它将获得的结果。
Close 消息关闭现有的预处理语句或游标并释放资源。对不存在的语句或游标名称发出 Close 不是错误。响应通常是 CloseComplete,但如果释放资源时遇到一些困难,则可能是 ErrorResponse。请注意,关闭预处理语句会隐式关闭从该语句构造的任何打开的游标。
Flush 消息不会导致生成任何特定输出,但会强制后端传递其输出缓冲区中任何待处理的数据。在任何扩展查询命令(Sync 除外)之后必须发送 Flush,如果前端希望在发出更多命令之前检查该命令的结果。如果没有 Flush,后端返回的消息将组合成最少数量的数据包,以最大限度地减少网络开销。
简单的 Query 消息大致等效于使用未命名的预处理语句和游标对象且没有参数的 Parse、Bind、游标 Describe、Execute、Close、Sync 系列。一个区别是它将在查询字符串中接受多个 SQL 语句,自动为每个语句连续执行绑定/描述/执行序列。另一个区别是它不会返回 ParseComplete、BindComplete、CloseComplete 或 NoData 消息。
使用扩展查询协议允许流水线,这意味着发送一系列查询而无需等待较早的查询完成。这减少了完成给定系列操作所需的网络往返次数。但是,如果其中一个步骤失败,用户必须仔细考虑所需的行为,因为稍后的查询已经发送到服务器。
解决这个问题的一种方法是使整个查询系列成为一个事务,即将其包装在 BEGIN
... COMMIT
中。但是,如果希望某些命令独立于其他命令提交,则此方法无济于事。
扩展查询协议提供了另一种管理此问题的方法,即省略在依赖步骤之间发送 Sync 消息。由于在发生错误后,后端将跳过命令消息,直到找到 Sync,因此当较早的命令失败时,允许自动跳过流水线中的后续命令,而无需客户端使用 BEGIN
和 COMMIT
显式管理。流水线中可独立提交的段可以用 Sync 消息分隔。
如果客户端没有发出显式的 BEGIN
,那么每个 Sync 通常会在前面的步骤成功时导致隐式的 COMMIT
,如果它们失败则导致隐式的 ROLLBACK
。但是,有一些 DDL 命令(例如 CREATE DATABASE
)不能在事务块内执行。如果在流水线中执行其中一个命令,则除非它是流水线中的第一个命令,否则它将失败。此外,成功后它将强制立即提交以保持数据库一致性。因此,紧跟在这些命令之后的 Sync 除了响应 ReadyForQuery 外没有其他作用。
当使用此方法时,必须通过计算 ReadyForQuery 消息并等待其达到发送的 Sync 数量来确定流水线的完成。计数命令完成响应不可靠,因为某些命令可能会被跳过,因此不会生成完成消息。
函数调用子协议允许客户端请求直接调用数据库的 pg_proc
系统目录中存在的任何函数。客户端必须具有该函数的执行权限。
函数调用子协议是一个遗留功能,最好在新代码中避免使用。可以通过设置执行 SELECT function($1, ...)
的预处理语句来完成类似的结果。然后,可以使用 Bind/Execute 替换函数调用周期。
函数调用周期由前端向后端发送 FunctionCall 消息启动。然后,后端会根据函数调用的结果发送一个或多个响应消息,最后发送 ReadyForQuery 响应消息。ReadyForQuery 通知前端,它可以安全地发送新查询或函数调用。
后端可能发送的响应消息是
发生错误。
函数调用已完成并返回消息中给出的结果。(请注意,函数调用协议只能处理单个标量结果,而不是行类型或结果集。)
函数调用处理已完成。无论处理成功终止还是出现错误,都将始终发送 ReadyForQuery。
已发出与函数调用相关的警告消息。通知是对其他响应的补充,即后端将继续处理命令。
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 消息之间;前端必须处理这些情况,并且应准备好处理其他异步消息类型(请参阅第 53.2.7 节)。否则,除 CopyData 或 CopyDone 之外的任何消息类型都可能被视为终止复制输出模式。
还有一种与复制相关的模式称为 copy-both,它允许高速批量数据传输到服务器和从服务器传输。当 walsender 模式下的后端执行 START_REPLICATION
语句时,会启动 copy-both 模式。后端会向前端发送 CopyBothResponse 消息。然后,后端和前端都可以发送 CopyData 消息,直到任何一方发送 CopyDone 消息。在客户端发送 CopyDone 消息后,连接会从 copy-both 模式变为 copy-out 模式,客户端可能不会再发送任何 CopyData 消息。类似地,当服务器发送 CopyDone 消息时,连接会进入 copy-in 模式,服务器可能不会再发送任何 CopyData 消息。双方都发送 CopyDone 消息后,复制模式终止,后端恢复到命令处理模式。如果在 copy-both 模式期间后端检测到错误,后端将发出 ErrorResponse 消息,丢弃前端消息,直到收到 Sync 消息,然后发出 ReadyForQuery 并返回正常处理。前端应将接收到 ErrorResponse 视为终止双向复制;在这种情况下不应发送 CopyDone。有关通过 copy-both 模式传输的子协议的更多信息,请参阅第 53.4 节。
CopyInResponse、CopyOutResponse 和 CopyBothResponse 消息包含字段,用于通知前端每行中的列数以及每列正在使用的格式代码。(在当前实现中,给定 COPY
操作中的所有列将使用相同的格式,但消息设计并未假定这一点。)
在几种情况下,后端会发送并非由前端的命令流专门提示的消息。前端必须随时准备好处理这些消息,即使没有进行查询时也是如此。至少,应该在开始读取查询响应之前检查这些情况。
由于外部活动,可能会生成 NoticeResponse 消息;例如,如果数据库管理员命令 “快速” 关闭数据库,后端将在关闭连接之前发送一个 NoticeResponse,指示这一事实。因此,即使连接名义上处于空闲状态,前端也应始终准备好接收和显示 NoticeResponse 消息。
每当后端认为前端应该知道的任何参数的活动值发生更改时,都会生成 ParameterStatus 消息。最常见的情况是,这是响应前端执行的 SET
SQL 命令而发生的,这种情况实际上是同步的——但参数状态更改也可能发生,因为管理员更改了配置文件,然后向服务器发送了 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 |
(default_transaction_read_only
和 in_hot_standby
在 14 之前的版本中未报告;scram_iterations
在 16 之前的版本中未报告。)请注意,server_version
、server_encoding
和 integer_datetimes
是启动后无法更改的伪参数。此集合将来可能会更改,甚至可能变为可配置的。因此,前端应简单地忽略它不理解或不关心的参数的 ParameterStatus。
如果前端发出 LISTEN
命令,则每当为同一通道名称执行 NOTIFY
命令时,后端将发送 NotificationResponse 消息(不要与 NoticeResponse 混淆!)。
目前,NotificationResponse 只能在事务之外发送,因此它不会发生在命令-响应序列的中间,但它可能会发生在 ReadyForQuery 之前。但是,设计假设这一点的前端逻辑是不明智的。良好的做法是能够在协议中的任何点接受 NotificationResponse。
在处理查询期间,前端可能会请求取消查询。出于实现效率的原因,取消请求不会直接在与后端的开放连接上发送:我们不希望后端在查询处理期间不断检查前端的新输入。取消请求应该相对不频繁,因此我们使它们稍微麻烦一些,以避免在正常情况下受到惩罚。
要发出取消请求,前端会打开与服务器的新连接并发送 CancelRequest 消息,而不是通常通过新连接发送的 StartupMessage 消息。服务器将处理此请求,然后关闭连接。出于安全原因,不会对取消请求消息进行直接回复。
除非 CancelRequest 消息包含连接启动期间传递给前端的相同密钥数据(PID 和密钥),否则该消息将被忽略。如果请求与当前正在执行的后端的 PID 和密钥匹配,则将中止当前查询的处理。(在现有实现中,这是通过向正在处理查询的后端进程发送特殊信号来完成的。)
取消信号可能有效也可能无效——例如,如果它在后端完成查询处理后到达,那么它将不起作用。如果取消有效,则会导致当前命令因错误消息而提前终止。
所有这一切的结果是,出于安全和效率的原因,前端无法直接知道取消请求是否成功。它必须继续等待后端响应查询。发出取消只会提高当前查询将很快完成的可能性,并提高它将因错误消息而失败而不是成功的可能性。
由于取消请求是通过与服务器的新连接发送的,而不是通过常规的前端/后端通信链路发送的,因此可以由任何进程发出取消请求,而不仅仅是要取消其查询的前端。这在构建多进程应用程序时可能会提供额外的灵活性。它也引入了安全风险,未经授权的人员可能会尝试取消查询。安全风险通过要求在取消请求中提供动态生成的密钥来解决。
正常的、优雅的终止过程是前端发送 Terminate 消息并立即关闭连接。收到此消息后,后端将关闭连接并终止。
在极少数情况下(例如管理员命令的数据库关闭),后端可能会在没有任何前端请求的情况下断开连接。在这种情况下,后端会尝试发送错误或通知消息,说明断开连接的原因,然后再关闭连接。
其他终止情况来自各种失败情况,例如一端或另一端的核心转储、通信链路丢失、消息边界同步丢失等。如果前端或后端看到连接意外关闭,它应该清理并终止。如果前端不想自行终止,可以选择通过重新联系服务器来启动新的后端。如果收到无法识别的消息类型,也建议关闭连接,因为这可能表示消息边界同步丢失。
对于正常或异常终止,任何打开的事务都会回滚,而不是提交。但是应该注意的是,如果前端在处理非 SELECT
查询时断开连接,后端可能会在注意到断开连接之前完成查询。如果查询在任何事务块(BEGIN
... COMMIT
序列)之外,那么它的结果可能会在识别到断开连接之前提交。
如果 PostgreSQL 是使用SSL支持构建的,则可以使用SSL加密前端/后端通信。这在攻击者可能能够捕获会话流量的环境中提供了通信安全。有关使用SSL加密 PostgreSQL 会话的更多信息,请参阅第 18.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 库之前,从套接字读取一个字节,或者如果发现已读取其他字节,则将其视为协议违规。
同样,服务器希望客户端在收到服务器对SSL请求的单个字节响应之前,不要开始SSL协商。如果客户端立即开始SSL协商而不等待接收到服务器响应,则可以减少一个往返的连接延迟。但是,这样做会付出代价,即无法处理服务器对SSL请求发送否定响应的情况。在这种情况下,服务器将直接断开连接,而不是继续使用 GSSAPI 或未加密的连接或协议错误。
初始 SSLRequest 也可以用于打开要发送 CancelRequest 消息的连接。
另一种启动SSL加密的方法可用。服务器将识别立即开始SSL协商而没有任何先前的 SSLRequest 数据包的连接。建立SSL连接后,服务器将期望一个正常的启动请求数据包,并在加密通道上继续协商。在这种情况下,任何其他加密请求都将被拒绝。这种方法不适用于通用工具,因为它无法协商可用的最佳连接加密或处理未加密的连接。但是,它对于服务器和客户端一起控制的环境非常有用。在这种情况下,它可以避免一个往返的延迟,并允许使用依赖标准SSL连接的网络工具。当使用SSL在这种类型的连接中,客户端需要使用 RFC 7301 定义的 ALPN 扩展来防止协议混淆攻击。PostgreSQL 协议在 IANA TLS ALPN 协议 ID 注册表中注册为 "postgresql"。
虽然协议本身不提供服务器强制SSL加密的方法,但管理员可以配置服务器,使其拒绝未加密的会话,作为身份验证检查的副产品。
如果 PostgreSQL 是使用GSSAPI支持构建的,则可以使用GSSAPI加密前端/后端通信。这在攻击者可能能够捕获会话流量的环境中提供了通信安全。有关使用GSSAPI,请参阅 第 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加密而不是GSSAPI.)
前端还应该准备好处理来自服务器的对 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加密的方法,但管理员可以配置服务器,使其拒绝未加密的会话,作为身份验证检查的副产品。
如果您在文档中发现任何不正确、与您使用特定功能的体验不符或需要进一步澄清的地方,请使用此表单报告文档问题。