本节介绍消息流和每种消息类型的语义。(每条消息的确切表示形式的详细信息请参见 第 55.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 协商。作为响应,前端将发送包含 GSSAPI 数据流第一部分的 GSSResponse 消息。如果需要更多消息,服务器将使用 AuthenticationGSSContinue 响应。
前端现在必须启动 SSPI 协商。作为响应,前端将发送包含 SSPI 数据流第一部分的 GSSResponse。如果需要更多消息,服务器将使用 AuthenticationGSSContinue 响应。
此消息包含来自 GSSAPI 或 SSPI 协商上一步(AuthenticationGSS、AuthenticationSSPI 或之前的 AuthenticationGSSContinue)的响应数据。如果此消息中的 GSSAPI 或 SSPI 数据表明需要更多数据才能完成身份验证,前端必须将该数据作为另一个 GSSResponse 消息发送。如果此消息完成 GSSAPI 或 SSPI 身份验证,服务器将接下来发送 AuthenticationOk 以指示身份验证成功,或发送 ErrorResponse 以指示失败。
前端现在必须使用消息中列出的其中一种 SASL 机制启动 SASL 协商。前端将发送一个 SASLInitialResponse,其中包含所选机制的名称,以及对此响应的 SASL 数据流的第一部分。如果需要更多消息,服务器将使用 AuthenticationSASLContinue 响应。有关详细信息,请参见第 55.3 节。
此消息包含来自 SASL 协商(AuthenticationSASL 或之前的 AuthenticationSASLContinue)上一步的质询数据。前端必须使用 SASLResponse 消息进行响应。
SASL 身份验证已完成,并带有针对客户端的其他特定于机制的数据。服务器接下来将发送 AuthenticationOk 以指示身份验证成功,或发送 ErrorResponse 以指示失败。仅当 SASL 机制指定在完成时从服务器发送到客户端的其他数据时,才会发送此消息。
服务器不支持客户端请求的较低协议版本,但支持较早版本的协议;此消息指示最高支持的较低版本。如果客户端在启动数据包中请求了不受支持的协议选项(即以_pq_.
开头),也将发送此消息。此消息后面将紧跟一个 ErrorResponse 或一个消息,指示身份验证成功或失败。
如果前端不支持服务器请求的身份验证方法,则应立即关闭连接。
在收到 AuthenticationOk 后,前端必须等待来自服务器的更多消息。在此阶段,后端进程正在启动,而前端只是一个感兴趣的旁观者。启动尝试仍然可能失败(ErrorResponse)或服务器拒绝支持所请求的较低协议版本(NegotiateProtocolVersion),但在正常情况下,后端将发送一些 ParameterStatus 消息、BackendKeyData,最后是 ReadyForQuery。
在此阶段,后端将尝试应用启动消息中给出的任何其他运行时参数设置。如果成功,这些值将成为会话默认值。错误将导致 ErrorResponse 和退出。
此阶段后端可能发送的消息是
此消息提供前端必须保存的密钥数据,以便以后能够发出取消请求。前端不应响应此消息,但应继续侦听 ReadyForQuery 消息。
此消息通知前端后端参数的当前(初始)设置,例如 client_encoding 或 DateStyle。前端可以忽略此消息,或记录设置以供将来使用;有关更多详细信息,请参见 第 55.2.7 节。前端不应响应此消息,但应继续侦听 ReadyForQuery 消息。
启动已完成。前端现在可以发出命令。
启动失败。发送此消息后连接关闭。
已发出警告消息。前端应显示消息,但继续侦听 ReadyForQuery 或 ErrorResponse。
ReadyForQuery 消息是后端在每个命令周期后发出的相同消息。根据前端的编码需求,可以合理地将 ReadyForQuery 视为启动命令周期,或将 ReadyForQuery 视为结束启动阶段和每个后续命令周期。
简单查询周期由前端向后端发送 Query 消息发起。该消息包含表示为文本字符串的 SQL 命令(或命令)。然后,后端根据查询命令字符串的内容发送一个或多个响应消息,最后发送 ReadyForQuery 响应消息。ReadyForQuery 通知前端可以安全地发送新命令。(前端实际上不必等到 ReadyForQuery 再发出另一个命令,但前端必须负责找出如果早期命令失败而已经发出的后续命令成功会发生什么情况。)
后端可能发送的响应消息是
SQL 命令正常完成。
后端已准备好将数据从前端复制到表中;请参见 第 55.2.6 节。
后端已准备好将数据从表复制到前端;请参见 第 55.2.6 节。
表示即将返回行以响应 SELECT
、FETCH
等查询。此消息的内容描述行的列布局。接下来将为返回到前端的每一行发送一个 DataRow 消息。
由 SELECT
、FETCH
等查询返回的一组行之一。
识别出一个空查询字符串。
发生了一个错误。
查询字符串的处理已完成。发送一条单独的消息来表明这一点,因为查询字符串可能包含多个 SQL 命令。(CommandComplete 标记一个 SQL 命令的处理结束,而不是整个字符串的处理结束。)无论处理是否成功完成或发生错误,都将始终发送 ReadyForQuery。
已针对查询发出一个警告消息。通知是除了其他响应之外的,即后端将继续处理命令。
对 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 节,了解后端可能因外部事件而生成的那些消息。
推荐的做法是使用状态机样式对前端进行编码,该样式可在任何有意义的时间接受任何消息类型,而不是对消息的确切顺序进行假设。
当一个简单的查询消息包含多条 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 语句发出为止。(请注意,一个简单的 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
包装它。但是,如果希望某些命令独立于其他命令提交,则这无济于事。
扩展查询协议提供了另一种方法来管理此问题,即在依赖步骤之间省略发送同步消息。由于在发生错误后,后端会跳过命令消息,直到找到同步,这允许管道中的后续命令在较早的命令失败时自动跳过,而无需客户端使用 BEGIN
和 COMMIT
显式管理。管道中可独立提交的段可以通过同步消息分隔。
如果客户端尚未发出显式的 BEGIN
,则如果前一个步骤成功,则每个同步通常会引起一个隐式的 COMMIT
;如果前一个步骤失败,则会引起一个隐式的 ROLLBACK
。但是,有一些 DDL 命令(例如 CREATE DATABASE
)无法在事务块内执行。如果在管道中执行其中一个命令,则除非它是管道中的第一个命令,否则该命令将失败。此外,成功后,它将强制立即提交以保持数据库一致性。因此,紧跟在其中一个命令之后的同步除了响应 ReadyForQuery 之外没有其他作用。
使用此方法时,必须通过计算 ReadyForQuery 消息并等待其达到发送的同步数来确定管道是否完成。计算命令完成响应不可靠,因为某些命令可能会被跳过,因此不会生成完成消息。
函数调用子协议允许客户端请求直接调用数据库的 pg_proc
系统目录中存在的任何函数。客户端必须具有该函数的执行权限。
函数调用子协议是一个旧功能,在新代码中最好避免使用。可以通过设置执行 SELECT function($1, ...)
的预处理语句来实现类似的结果。然后,可以用绑定/执行替换函数调用周期。
函数调用周期由前端向后端发送 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。前端无法中止传输(除非关闭连接或发出取消请求),但它可以丢弃不需要的 CopyData 和 CopyDone 消息。
如果在复制输出模式期间检测到后端错误,后端将发出 ErrorResponse 消息并恢复到正常处理。前端应将收到 ErrorResponse 视为终止复制输出模式。
NoticeResponse 和 ParameterStatus 消息可能会穿插在 CopyData 消息之间;前端必须处理这些情况,并且也应为其他异步消息类型做好准备(请参阅第 55.2.7 节)。否则,任何消息类型(CopyData 或 CopyDone 除外)都可以视为终止复制输出模式。
还有另一种与复制相关的模式称为复制两者,它允许以高速批量方式向服务器传输数据和从服务器传输数据。当 walsender 模式的后端执行START_REPLICATION
语句时,将启动复制两者模式。后端向前端发送 CopyBothResponse 消息。然后,后端和前端都可以发送 CopyData 消息,直到任一端发送 CopyDone 消息。客户端发送 CopyDone 消息后,连接将从复制两者模式转到复制输出模式,并且客户端可能不会再发送任何 CopyData 消息。同样,当服务器发送 CopyDone 消息时,连接将进入复制输入模式,并且服务器可能不会再发送任何 CopyData 消息。双方都发送 CopyDone 消息后,复制模式将终止,后端将恢复到命令处理模式。如果在复制两者模式期间检测到后端错误,后端将发出 ErrorResponse 消息,丢弃前端消息,直到收到 Sync 消息,然后发出 ReadyForQuery 并返回到正常处理。前端应将收到 ErrorResponse 视为终止两个方向的复制;在这种情况下,不应发送 CopyDone。有关复制两者模式下传输的子协议的更多信息,请参阅第 55.4 节。
CopyInResponse、CopyOutResponse 和 CopyBothResponse 消息包括一些字段,这些字段会告知前端每行有多少列以及每列使用的格式代码。(在当前实现中,给定COPY
操作中的所有列都将使用相同的格式,但消息设计并不假定这一点。)
在某些情况下,后端会发送并非由前端命令流明确提示的消息。前端必须随时做好处理这些消息的准备,即使没有进行查询。至少,在开始读取查询响应之前,应该检查这些情况。
由于外部活动,可能会生成 NoticeResponse 消息;例如,如果数据库管理员命令““快速””数据库关闭,后端会在关闭连接之前发送一个 NoticeResponse 指示此事实。因此,即使连接名义上处于空闲状态,前端也应始终做好接受和显示 NoticeResponse 消息的准备。
每当后端认为前端应该了解的任何参数的活动值发生更改时,都会生成 ParameterStatus 消息。最常见的情况是对前端执行的 SET
SQL 命令做出响应,这种情况实际上是同步的,但由于管理员更改了配置文件,然后向服务器发送 SIGHUP 信号,也可能发生参数状态更改。此外,如果回滚 SET
命令,将生成适当的 ParameterStatus 消息来报告当前有效值。
目前,有一组固定的参数将生成 ParameterStatus:它们是 server_version
、server_encoding
、client_encoding
、application_name
、default_transaction_read_only
、in_hot_standby
、is_superuser
、session_authorization
、DateStyle
、IntervalStyle
、TimeZone
、integer_datetimes
和 standard_conforming_strings
。(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
。)请注意,server_version
、server_encoding
和 integer_datetimes
是伪参数,在启动后不能更改。此设置将来可能会更改,甚至可以配置。因此,前端应简单地忽略它不理解或不关心的参数的 ParameterStatus。
如果前端发出 LISTEN
命令,则每当针对同一频道名称执行 NOTIFY
命令时,后端都会发送 NotificationResponse 消息(不要与 NoticeResponse 混淆!)。
目前,NotificationResponse 只能在事务外部发送,因此不会出现在命令响应序列中,尽管它可能在 ReadyForQuery 之前出现。不过,不建议设计假设这一点的前端逻辑。良好的做法是能够在协议中的任何点接受 NotificationResponse。
在处理查询期间,前端可能会请求取消查询。取消请求不会直接在与后端的开放连接上发送,原因是实现效率:我们不希望后端在查询处理期间不断检查来自前端的新输入。取消请求应该相对不频繁,所以我们让它们稍微麻烦一些,以避免在正常情况下受到惩罚。
要发出取消请求,前端会打开到服务器的新连接并发送 CancelRequest 消息,而不是通常会通过新连接发送的 StartupMessage 消息。服务器将处理此请求,然后关闭连接。出于安全原因,不会直接回复取消请求消息。
除非 CancelRequest 消息包含连接启动期间传递给前端的相同密钥数据(PID 和密钥),否则将被忽略。如果请求与当前正在执行的后端的 PID 和密钥匹配,则当前查询的处理将被中止。(在现有实现中,这是通过向处理查询的后端进程发送特殊信号来完成的。)
取消信号可能产生或不产生任何影响——例如,如果它在后端完成查询处理后到达,那么它将不会产生任何影响。如果取消有效,它将导致当前命令过早终止并显示错误消息。
所有这一切的结果是,出于安全性和效率的原因,前端没有直接的方法来判断取消请求是否成功。它必须继续等待后端响应查询。发出取消只是提高了当前查询将很快完成的可能性,并提高了它将因错误消息而失败而不是成功的可能性。
由于取消请求是通过与服务器的新连接发送的,而不是通过常规的前端/后端通信链接发送的,因此任何进程都可以发出取消请求,而不仅仅是其查询要被取消的前端。在构建多进程应用程序时,这可能会提供额外的灵活性。它还引入了安全风险,因为未经授权的人员可能会尝试取消查询。通过要求在取消请求中提供动态生成的密钥来解决安全风险。
正常、优雅的终止过程是前端发送终止消息并立即关闭连接。收到此消息后,后端关闭连接并终止。
在极少数情况下(例如管理员命令的数据库关闭),后端可能会在没有任何前端请求的情况下断开连接。在这种情况下,后端将在关闭连接之前尝试发送错误或通知消息,说明断开连接的原因。
其他终止场景源于各种故障情况,例如一端或另一端的核心转储、通信链路丢失、消息边界同步丢失等。如果前端或后端看到连接意外关闭,则应清理并终止。如果前端不想终止自身,它可以选择通过重新联系服务器来启动新的后端。如果收到无法识别的消息类型,也建议关闭连接,因为这可能表明消息边界同步丢失。
对于正常或异常终止,任何打开的事务都会回滚,而不是提交。然而,应该注意的是,如果前端在处理非 SELECT
查询时断开连接,后端可能会在注意到断开连接之前完成查询。如果查询不在任何事务块(BEGIN
... COMMIT
序列)之外,则其结果可能会在断开连接被识别之前提交。
如果 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 响应。只有当服务器早于 PostgreSQL 中添加 SSL 支持时,才会发生这种情况。(此类服务器现在非常古老,而且可能不再存在于野外。)在这种情况下,必须关闭连接,但前端可以选择打开一个新的连接,并继续进行,而无需请求 SSL。
当可执行 SSL 加密时,服务器应仅发送单个 S
字节,然后等待前端启动 SSL 握手。如果此时有其他字节可供读取,则可能表示中间人正尝试执行缓冲区填充攻击 (CVE-2021-23222)。前端应编码为在将套接字移交给其 SSL 库之前从套接字中读取一个字节,或者在发现已读取其他字节时将其视为协议违规。
初始 SSLRequest 也可以在打开的连接中使用,以发送 CancelRequest 消息。
虽然该协议本身不为服务器提供强制 SSL 加密的方式,但管理员可以将服务器配置为拒绝未加密的会话,作为身份验证检查的副产品。
如果 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 响应。这只会发生在服务器早于 PostgreSQL 中添加 GSSAPI 加密支持的情况下。在这种情况下,必须关闭连接,但前端可以选择打开一个新的连接,并在不请求 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 加密的方法,但管理员可以将服务器配置为拒绝未加密会话作为身份验证检查的副产品。