本节描述消息流以及每种消息类型的语义。(每种消息的精确表示细节见 第 53.7 节。)根据连接的状态,有几种不同的子协议:启动、查询、函数调用、COPY
和终止。此外,还为异步操作(包括通知响应和命令取消)提供了特殊规定,这些操作可以在启动阶段之后随时发生。
要开始一个会话,前端会打开与服务器的连接并发送一个启动消息。此消息包含用户名和用户要连接的数据库的名称;它还标识要使用的特定协议版本。(可选地,启动消息可以包含用于运行时参数的附加设置。)然后,服务器会使用这些信息以及其配置文件(如 pg_hba.conf
)的内容来确定连接是否暂时可接受,以及需要什么额外的身份验证(如果有)。
然后,服务器会发送一个适当的身份验证请求消息,前端必须用一个适当的身份验证响应消息(如密码)进行回复。对于除了 GSSAPI、SSPI 和 SASL 之外的所有身份验证方法,最多只有一个请求和一个响应。在某些方法中,前端不需要任何响应,因此不会发生身份验证请求。对于 GSSAPI、SSPI 和 SASL,可能需要多次交换数据包才能完成身份验证。
身份验证循环以服务器拒绝连接尝试(ErrorResponse)或发送 AuthenticationOk 结束。
服务器在此阶段可能发送的消息是
连接尝试已被拒绝。服务器会立即关闭连接。
身份验证交换已成功完成。
前端现在必须与服务器进行 Kerberos V5 身份验证对话(此处未描述,是 Kerberos 规范的一部分)。如果成功,服务器会回复 AuthenticationOk,否则会回复 ErrorResponse。这不再受支持。
前端现在必须发送一个 PasswordMessage,其中包含以明文形式的密码。如果这是正确的密码,服务器会回复 AuthenticationOk,否则会回复 ErrorResponse。
前端现在必须发送一个 PasswordMessage,其中包含使用 MD5 加密的密码(带用户名),然后使用 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 节,了解后端可能由于外部事件而生成的消息。
推荐做法是以状态机风格对前端进行编码,这样它就能在任何时间接受任何类型的消息(只要它在逻辑上可能出现),而不是对消息的精确序列进行假设。
当简单查询消息包含多个 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
,但不会回滚第一个。
此行为是通过在多语句查询消息中运行语句以实现的,隐式事务块 除非存在一些显式事务块供它们运行。隐式事务块和常规事务块之间的主要区别在于,隐式块在查询消息结束时会自动关闭,如果未出现错误则隐式提交,如果出现错误则隐式回滚。这类似于单独执行语句时(当不在事务块中时)发生的隐式提交或回滚。
如果会话已处于事务块中(由于之前消息中的 BEGIN
),则查询消息将继续该事务块,无论消息包含一个语句还是多个语句。但是,如果查询消息包含一个 COMMIT
或 ROLLBACK
关闭现有事务块,则所有后续语句将在隐式事务块中执行。相反,如果 BEGIN
出现在多语句查询消息中,则它将启动一个常规事务块,该块将仅由显式 COMMIT
或 ROLLBACK
终止,无论它出现在此查询消息中还是稍后的消息中。如果 BEGIN
位于已作为隐式事务块执行的一些语句之后,则这些语句不会立即提交;实际上,它们会追溯性地包含到新的常规事务块中。
出现在隐式事务块中的 COMMIT
或 ROLLBACK
将按正常方式执行,关闭隐式块;但是,将发出警告,因为没有先前的 BEGIN
的 COMMIT
或 ROLLBACK
可能代表错误。如果还有更多语句,将为它们启动一个新的隐式事务块。
在隐式事务块中不允许使用保存点,因为它们会与在任何错误发生时自动关闭块的行为冲突。
请记住,无论可能存在哪些事务控制命令,查询消息的执行都会在第一个错误处停止。因此,例如,给定
BEGIN; SELECT 1/0; ROLLBACK;
在单个查询消息中,会话将保留在失败的常规事务块中,因为在除零错误后无法到达 ROLLBACK
。另一个 ROLLBACK
将需要恢复会话到可用状态。
另一个值得注意的行为是,在执行任何语句之前,会对整个查询字符串进行初始词法和语法分析。因此,后续语句中的简单错误(例如拼写错误的关键字)可能会阻止执行任何语句。这通常对用户不可见,因为所有语句都会在作为隐式事务块执行时回滚。但是,当尝试在多语句查询中执行多个事务时,它可能是可见的。例如,如果错字将我们之前的示例改为
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 语句为止。(请注意,简单查询消息也会销毁未命名语句。)命名准备语句必须在被另一个 Parse 消息重新定义之前显式关闭,但对未命名语句来说这不是必需的。命名准备语句也可以在 SQL 命令级别创建和访问,使用 PREPARE
和 EXECUTE
。
一旦准备语句存在,就可以使用 Bind 消息将其准备好执行。Bind 消息给出源准备语句的名称(空字符串表示未命名准备语句)、目标门户的名称(空字符串表示未命名门户)以及用于准备语句中存在的任何参数占位符的值。提供的参数集必须与准备语句所需的参数集匹配。(如果您在 Parse 消息中声明了任何 void
参数,请在 Bind 消息中为它们传递 NULL 值。)Bind 还指定了用于查询返回的任何数据的格式;可以整体或按列指定格式。响应是 BindComplete 或 ErrorResponse。
文本和二进制输出之间的选择由 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。)
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 语句,并自动依次为每个语句执行 bind/describe/execute 序列。另一个区别是它不会返回 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 相关的模式,称为复制双向模式,它允许高速批量数据传输到 和从服务器。当处于 walsender 模式下的后端执行 START_REPLICATION
语句时,将启动复制双向模式。后端向前端发送一个 CopyBothResponse 消息。然后,后端和前端都可能发送 CopyData 消息,直到任一端发送 CopyDone 消息。客户端发送 CopyDone 消息后,连接从复制双向模式变为复制输出模式,客户端不再发送任何 CopyData 消息。类似地,服务器发送 CopyDone 消息后,连接进入复制输入模式,服务器不再发送任何 CopyData 消息。两端都发送了 CopyDone 消息后,复制模式将终止,后端恢复到命令处理模式。如果在复制双向模式期间后端检测到错误,则后端将发出 ErrorResponse 消息,丢弃前端消息,直到收到 Sync 消息,然后发出 ReadyForQuery 并返回正常处理。前端应将收到 ErrorResponse 视为终止双向复制;在这种情况下不应发送 CopyDone。有关在复制双向模式上传输的子协议的更多信息,请参见 第 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来加密前端/后端通信。这在攻击者可能能够捕获会话流量的环境中提供通信安全性。有关使用加密 PostgreSQL 会话的更多信息SSL,请参见 第 18.9 节。
要启动一个SSL加密的连接,前端最初发送 SSLRequest 消息而不是 StartupMessage。然后,服务器将以包含 S
或 N
的单个字节进行响应,分别表示它愿意或不愿意执行SSL。如果前端对响应不满意,则可能在此时关闭连接。要在 S
之后继续,请执行SSL启动握手(此处未描述,是SSL与服务器进行 (specification) 规格说明。如果成功,继续发送常规的 StartupMessage。在这种情况下,StartupMessage 和所有后续数据将SSL- 加密。若要继续执行 N
后面的操作,请发送常规 StartupMessage 并继续执行,无需加密。(或者,在 N
响应后发出 GSSENCRequest 消息以尝试使用GSSAPI加密而不是SSL.)
前端还应准备处理服务器对 SSLRequest 的 ErrorMessage 响应。这只会发生在服务器早于添加SSL对 PostgreSQL 的支持的情况下。(此类服务器现在非常古老,并且可能在现实世界中不存在。)在这种情况下,必须关闭连接,但前端可以选择打开新的连接并继续执行,而无需请求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 协议是“postgresql”,在 IANA TLS ALPN 协议 ID 注册表中注册。
虽然协议本身没有为服务器提供强制SSL加密的方法,但管理员可以配置服务器以拒绝未加密会话,作为身份验证检查的副产品。
如果 PostgreSQL 使用GSSAPI支持构建,则可以使用GSSAPI来加密前端/后端通信。这在攻击者可能能够捕获会话流量的环境中提供通信安全性。有关使用加密 PostgreSQL 会话的更多信息GSSAPI,参见 第 18.10 节。
要启动一个GSSAPI- 加密连接,前端最初发送 GSSENCRequest 消息而不是 StartupMessage。然后服务器用一个包含 G
或 N
的单个字节进行响应,分别表示它愿意或不愿意执行GSSAPI加密。如果前端对响应不满意,则可能会在此处关闭连接。若要继续执行 G
后面的操作,请使用如 RFC 2744 中所述的 GSSAPI C 绑定或等效绑定,执行GSSAPI初始化,方法是在循环中调用 gss_init_sec_context()
并将结果发送到服务器,从空输入开始,然后使用来自服务器的每个结果,直到它不返回输出为止。当将 gss_init_sec_context()
的结果发送到服务器时,请在网络字节顺序中将消息长度作为四个字节的整数附加到实际加密有效负载之前。若要继续执行 N
后面的操作,请发送常规 StartupMessage 并继续执行,无需加密。(或者,在 N
响应后发出 SSLRequest 消息以尝试使用SSL加密而不是GSSAPI.)
前端还应准备处理服务器对 GSSENCRequest 的 ErrorMessage 响应。这只会发生在服务器早于添加GSSAPI加密支持到 PostgreSQL 的情况下。(此类服务器现在非常古老,并且可能在现实世界中不存在。)在这种情况下,必须关闭连接,但前端可以选择打开新的连接并继续执行,而无需请求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加密的方法,但管理员可以配置服务器以拒绝未加密会话,作为身份验证检查的副产品。
如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符,或者需要进一步说明,请使用 此表格 报告文档问题。