本节描述消息流和每种消息类型的语义。(每种消息的确切表示形式的详细信息请参见第 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 消息(门户变体)指定现有门户的名称(或未命名门户的空字符串)。响应是描述执行门户将返回的行(或如果门户不包含将返回行的查询,则为 NoData 消息)的 RowDescription 消息;或者如果不存在此类门户,则为 ErrorResponse。
Describe 消息(语句变体)指定现有预准备语句的名称(或未命名预准备语句的空字符串)。响应是描述语句所需的 parameters 的 ParameterDescription 消息,后跟描述当语句最终执行时将返回的行(或如果语句不会返回行,则为 NoData 消息)的 RowDescription 消息。如果不存在此类预准备语句,则发出 ErrorResponse。请注意,由于尚未发出 Bind,因此后端尚不知道要用于返回列的格式;在这种情况下,RowDescription 消息中的格式代码字段将为零。
在大多数情况下,前端应在发出 Execute 之前发出 Describe 的一个或另一个变体,以确保它知道如何解释将获得的结果。
Close 消息关闭现有的预准备语句或门户并释放资源。针对不存在的语句或门户名称发出 Close 不是错误。响应通常为 CloseComplete,但如果在释放资源时遇到某些困难,则可能是 ErrorResponse。请注意,关闭预准备语句会隐式关闭从该语句构造的所有打开的门户。
Flush 消息不会生成任何特定输出,但会强制后端传递其输出缓冲区中任何挂起的 data。如果前端希望在发出更多命令之前检查该命令的结果,则必须在除 Sync 之外的任何扩展查询命令之后发送 Flush。如果没有 Flush,后端返回的消息将合并到尽可能少的包中,以最大程度地减少网络开销。
简单的 Query 消息大致等效于使用未命名预准备语句和门户对象以及无参数的 Parse、Bind、门户 Describe、Execute、Close、Sync 系列。一个区别是它将接受查询字符串中的多个 SQL 语句,自动依次对每个语句执行 bind/describe/execute 序列。另一个区别是它不会返回 ParseComplete、BindComplete、CloseComplete 或 NoData 消息。
使用扩展查询协议允许 流水线,这意味着发送一系列查询而无需等待较早的查询完成。这减少了完成给定操作序列所需的网络往返次数。但是,用户必须仔细考虑如果其中一个步骤失败所需的 behavior,因为后面的查询已经发送到服务器。
处理此问题的一种方法是使整个查询序列成为单个事务,即将其包装在 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 视为终止复制输出模式。
在CopyData消息之间,可能穿插着NoticeResponse和ParameterStatus消息;前端必须处理这些情况,并且应该准备好处理其他异步消息类型(参见第 53.2.7 节)。否则,除CopyData或CopyDone之外的任何消息类型都可能被视为终止复制输出模式。
还有一种与复制相关的模式称为复制双向模式(copy-both),它允许高速批量数据传输到以及从服务器传输。当后端处于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加密前端/后端通信。这在攻击者可能能够捕获会话流量的环境中提供了通信安全性。有关使用SSL加密PostgreSQL会话的更多信息,请参见第 18.9 节。
要启动SSL加密连接,前端最初会发送SSLRequest消息而不是StartupMessage。然后,服务器将以包含S
或N
的单个字节进行响应,分别表示它愿意或不愿意执行SSL。如果对响应不满意,前端可能会在此处关闭连接。要继续执行S
,请执行SSL启动握手(此处未描述,是SSL规范的一部分)与服务器。如果成功,请继续发送通常的StartupMessage。在这种情况下,StartupMessage和所有后续数据都将SSL加密。要继续执行N
,请发送通常的StartupMessage并继续执行,无需加密。(或者,在N
响应后发出GSSENCRequest消息以尝试使用GSSAPI加密代替SSL.)
也是允许的。)SSL.
前端还应该准备好处理服务器发出的SSLRequest的ErrorMessage响应。前端不应将此错误消息显示给用户/应用程序,因为服务器尚未经过身份验证(CVE-2024-10977)。在这种情况下,必须关闭连接,但前端可以选择打开新的连接并继续执行,而无需请求SSL。当SSL加密可以执行时,服务器预计只会发送单个S
字节,然后等待前端启动
同样,服务器期望客户端在收到服务器对请求的单字节响应之前不要开始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加密前端/后端通信。这在攻击者可能能够捕获会话流量的环境中提供了通信安全性。有关使用GSSAPI,请参阅 第 18.10 节。
要启动GSSAPI加密连接,前端最初发送 GSSENCRequest 消息而不是 StartupMessage。然后服务器用一个包含 G
或 N
的字节进行响应,分别表示它愿意或不愿意执行GSSAPI加密。如果前端对响应不满意,则可能在此处关闭连接。要继续执行 G
,使用如 RFC 2744 中所述的 GSSAPI C 绑定或等效绑定,通过循环调用 gss_init_sec_context()
并将结果发送到服务器,从空输入开始,然后使用服务器的每个结果,直到它不返回输出。将 gss_init_sec_context()
的结果发送到服务器时,在网络字节序中以四个字节的整数形式在消息前面加上消息的长度。要继续执行 N
,请发送通常的 StartupMessage 并继续执行不加密的操作。(或者,在 N
响应后发出 SSLRequest 消息以尝试使用GSSAPI加密是可以接受的。)SSL加密代替GSSAPI.)
前端还应该准备好处理来自服务器的 GSSENCRequest 的 ErrorMessage 响应。前端不应将此错误消息显示给用户/应用程序,因为服务器尚未经过身份验证(CVE-2024-10977)。在这种情况下,必须关闭连接,但前端可以选择打开一个新的连接并继续执行而不请求GSSAPI加密。
前端还应该准备好处理服务器发出的SSLRequest的ErrorMessage响应。前端不应将此错误消息显示给用户/应用程序,因为服务器尚未经过身份验证(CVE-2024-10977)。在这种情况下,必须关闭连接,但前端可以选择打开新的连接并继续执行,而无需请求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加密的方法,但管理员可以配置服务器在身份验证检查的副产品中拒绝未加密的会话。