本节描述消息流和每种消息类型的语义。(每种消息的确切表示细节出现在 第 54.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()
函数返回其结果为十六进制字符串。)
MD5 加密密码的支持已弃用,将在 PostgreSQL 的未来版本中移除。有关迁移到其他密码类型的详细信息,请参阅第 20.5 节。
前端现在必须发起 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。有关详细信息,请参见 第 54.3 节。
此消息包含来自 SASL 协商上一步(AuthenticationSASL 或之前的 AuthenticationSASLContinue)的挑战数据。前端必须用 SASLResponse 消息进行响应。
SASL 认证已完成,并附带了机制特定的附加数据供客户端使用。服务器将接下来发送 AuthenticationOk 表示成功认证,或 ErrorResponse 表示失败。此消息仅在 SASL 机制指定了在完成时从服务器发送到客户端的附加数据时发送。
服务器不支持客户端请求的小协议版本,但支持协议的早期版本;此消息指示最高支持的小版本。如果客户端在启动包中请求了不支持的协议选项(即以 _pq_.
开头),也将发送此消息。
在此消息之后,认证将使用服务器指示的版本继续。如果客户端不支持较早的版本,它应立即关闭连接。如果服务器不发送此消息,则表示它支持客户端请求的协议版本和所有协议选项。
如果前端不支持服务器请求的认证方法,则应立即关闭连接。
在收到 AuthenticationOk 后,前端必须等待服务器的进一步消息。在此阶段,后端进程正在启动,前端只是一个旁观者。启动尝试仍有可能失败(ErrorResponse),或者服务器可能拒绝支持所请求的小协议版本(NegotiateProtocolVersion),但在正常情况下,后端将发送一些 ParameterStatus 消息、BackendKeyData,最后是 ReadyForQuery。
在此阶段,后端将尝试应用启动消息中提供的任何其他运行时参数设置。如果成功,这些值将成为会话默认值。错误将导致 ErrorResponse 和退出。
在此阶段,后端可能发送的消息是
此消息提供了一个前端必须保存的秘密密钥数据,以便以后能够发出取消请求。前端不应响应此消息,而应继续等待 ReadyForQuery 消息。
PostgreSQL 服务器将始终发送此消息,但一些不支持查询取消的第三方后端协议实现则已知不会发送。
此消息告知前端有关后端参数的当前(初始)设置,例如 client_encoding 或 DateStyle。前端可以忽略此消息,或记录设置以备将来使用;有关更多详细信息,请参见 第 54.2.7 节。前端不应响应此消息,而应继续等待 ReadyForQuery 消息。
启动已完成。前端现在可以发出命令。
启动失败。发送此消息后连接将关闭。
已发出警告消息。前端应显示消息,但继续等待 ReadyForQuery 或 ErrorResponse。
ReadyForQuery 消息与后端在每个命令周期后会发送的消息相同。根据前端的编码需求,可以合理地将 ReadyForQuery 视为启动一个命令周期,或者将 ReadyForQuery 视为结束启动阶段和每个后续命令周期。
简单查询周期由前端发送 Query 消息到后端启动。该消息包含一个 SQL 命令(或多个命令),以文本字符串表示。然后,后端会根据查询命令字符串的内容发送一个或多个响应消息,最后发送一个 ReadyForQuery 响应消息。ReadyForQuery 告知前端,它可以安全地发送新命令。(前端实际上不必等待 ReadyForQuery 再发出另一个命令,但这样前端必须自行负责弄清楚如果早期命令失败且已发出的后续命令成功了会发生什么。)
后端可能发送的响应消息是
SQL 命令正常完成。
后端已准备好将数据从前端复制到表中;请参见 第 54.2.6 节。
后端已准备好将数据从表中复制到前端;请参见 第 54.2.6 节。
指示将按预期返回行作为对 SELECT
、FETCH
等查询的响应。此消息的内容描述了行的列布局。随后会为返回给前端的每一行发送 DataRow 消息。
由 SELECT
、FETCH
等查询返回的行集中的一行。
已识别出空查询字符串。
发生了错误。
查询字符串的处理已完成。将发送一个单独的消息来指示这一点,因为查询字符串可能包含多个 SQL 命令。(CommandComplete 标记一个 SQL 命令的处理结束,而不是整个字符串。)无论处理是成功终止还是出错终止,都会发送 ReadyForQuery。
已发出与查询相关的警告消息。Notice 是其他响应的附加信息,即后端将继续处理命令。
对 SELECT
查询(或其他返回行集的查询,如 EXPLAIN
或 SHOW
)的响应通常由 RowDescription、零个或多个 DataRow 消息,然后是 CommandComplete 组成。到前端或从前端的 COPY
操作会调用特殊协议,如 第 54.2.6 节 所述。所有其他查询类型通常只生成一个 CommandComplete 消息。
由于查询字符串可能包含多个查询(以分号分隔),因此在后端完成处理查询字符串之前,可能会有多个此类响应序列。当整个字符串处理完毕且后端已准备好接受新查询字符串时,将发出 ReadyForQuery。
如果收到一个完全为空(除空格外无内容)的查询字符串,响应将是 EmptyQueryResponse 后跟 ReadyForQuery。
发生错误时,将发出 ErrorResponse,然后是 ReadyForQuery。ErrorResponse 将中止查询字符串的所有进一步处理(即使其中还有剩余的查询)。请注意,这可能发生在单个查询生成的响应消息序列的中间。
在简单查询模式下,检索到的值的格式始终是文本,除非给定的命令是带有 BINARY
选项声明的光标的 FETCH
。在这种情况下,检索到的值是二进制格式。RowDescription 消息中给出的格式代码指示了正在使用的格式。
前端必须准备好在期望接收任何其他类型的消息时接受 ErrorResponse 和 NoticeResponse 消息。另请参见 第 54.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
来关闭现有事务块,那么任何后续语句都将在隐式事务块中执行。反之,如果在多语句 Query 消息中出现 BEGIN
,则它将启动一个常规事务块,该事务块将仅由显式的 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
未被提交。在语义分析或之后检测到的错误,如拼写错误的表或列名,则不会产生此效果。
最后,请注意,Query 消息中的所有语句都将观察到相同的 statement_timestamp()
值,因为该时间戳仅在收到 Query 消息时更新。除了查询字符串结束先前开始的事务并开始新事务的情况外,这将导致它们都观察到相同的 transaction_timestamp()
值。
扩展查询协议将上述简单查询协议分解为多个步骤。准备阶段的结果可以重用多次,以提高效率。此外,还有其他功能可用,例如可以将数据值作为单独的参数提供,而无需直接插入查询字符串。
在扩展协议中,前端首先发送一个 Parse 消息,其中包含文本查询字符串,可选地包含一些关于参数占位符数据类型的信息,以及目标预备语句对象的名称(空字符串选择未命名预备语句)。响应是 ParseComplete 或 ErrorResponse。参数数据类型可以由 OID 指定;如果未给出,解析器将尝试以与无类型文字字符串常量相同的方式推断数据类型。
参数数据类型可以通过将其设置为零来省略,或者通过使参数类型 OID 数组比查询字符串中使用的参数符号($
n
)的数量短来省略。另一个特殊情况是,参数的类型可以指定为 void
(即 void
伪类型的 OID)。这旨在允许参数符号用于实际上是 OUT 参数的函数参数。通常没有可以用 void
参数的上下文,但如果这样的参数符号出现在函数的参数列表中,它将被有效地忽略。例如,一个像 foo($1,$2,$3,$4)
这样的函数调用,如果 $3
和 $4
被指定为 void
类型,则可以匹配一个有两个 IN 和两个 OUT 参数的函数。
Parse 消息中包含的查询字符串不能包含多个 SQL 语句;否则会报告语法错误。简单查询协议中不存在此限制,但在扩展协议中存在,因为允许预备语句或游标包含多个命令会使协议过于复杂。
如果成功创建,命名预备语句对象将一直存在直到当前会话结束,除非被显式销毁。未命名的预备语句仅在发出指定未命名语句为目标的下一个 Parse 语句之前有效。(请注意,简单的 Query 消息也会销毁未命名语句。)命名预备语句必须在被另一个 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 消息指定游标名称(空字符串表示未命名游标)以及最大结果行数(零表示“fetch all rows”)。结果行数仅对于包含返回行集的命令的游标才有意义;在其他情况下,命令总是执行到完成,并且行数被忽略。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
。服务器在第一个命令在没有 sync 的情况下结束时才会检测到这个隐式事务块。有几个 DDL 命令(如 CREATE DATABASE
)不能在事务块中执行。如果在流水线中执行其中一个,除非它是 Sync 之后的第一个命令,否则将失败。此外,成功后它将强制立即提交以保持数据库一致性。因此,在此类命令之后立即发出 Sync 除了响应 ReadyForQuery 外没有其他作用。
使用此方法时,流水线的完成必须通过计算 ReadyForQuery 消息并等待其达到发送的 Sync 数量来确定。计算命令完成响应是不可靠的,因为其中一些命令可能会被跳过,因此不会产生完成消息。
函数调用子协议允许客户端请求直接调用数据库 pg_proc
系统目录中存在的任何函数。客户端必须具有执行函数的权限。
函数调用子协议是一个遗留功能,在新代码中最好避免使用。通过设置一个执行 SELECT function($1, ...)
的预备语句,可以完成类似的结果。然后,函数调用周期可以被 Bind/Execute 替换。
函数调用周期由前端发送 FunctionCall 消息到后端启动。然后,后端会根据函数调用的结果发送一个或多个响应消息,最后发送一个 ReadyForQuery 响应消息。ReadyForQuery 告知前端,它可以安全地发送新查询或函数调用。
后端可能发送的响应消息是
发生了错误。
函数调用已完成,并返回消息中给出的结果。(请注意,函数调用协议只能处理单个标量结果,不能处理行类型或结果集。)
函数调用处理已完成。无论处理是成功终止还是出错终止,都会发送 ReadyForQuery。
已发出与函数调用相关的警告消息。Notice 是其他响应的附加信息,即后端将继续处理命令。
COPY
命令允许与服务器之间进行高速批量数据传输。复制输入(copy-in)和复制输出(copy-out)操作都会将连接切换到一种特定的子协议,直到操作完成。
复制输入模式(数据传输到服务器)在后端执行 COPY FROM STDIN
SQL 语句时启动。后端向前端发送一个 CopyInResponse 消息。前端随后应发送零个或多个 CopyData 消息,形成一个输入数据流。(消息边界不必与行边界严格对应,尽管这通常是一个合理的选择。)前端可以通过发送 CopyDone 消息(表示成功完成)或 CopyFail 消息(这将导致 COPY
SQL 语句失败并报错)来终止复制输入模式。然后,后端会恢复到 COPY
命令开始之前的连接处理模式,该模式要么是简单查询协议,要么是扩展查询协议。接下来,后端会发送 CommandComplete(如果成功)或 ErrorResponse(如果不成功)。
如果在复制输入模式期间发生后端检测到的错误(包括收到 CopyFail 消息),后端将发出一个 ErrorResponse 消息。如果 COPY
命令是通过扩展查询消息发出的,后端将丢弃前端消息,直到收到 Sync 消息,然后它会发出 ReadyForQuery 并返回正常处理。如果 COPY
命令是在简单查询消息中发出的,该消息的其余部分将被丢弃,然后发出 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 消息之间;前端必须处理这些情况,并且还应准备好处理其他异步消息类型(请参阅 第 54.2.7 节)。否则,除 CopyData 或 CopyDone 之外的任何消息类型都可能被视为终止复制输出模式。
还有一种与复制相关的模式称为 copy-both,它允许与服务器进行双向(以及)高速批量数据传输。Copy-both 模式在处于 walsender 模式的后端执行 START_REPLICATION
语句时启动。后端向前端发送一个 CopyBothResponse 消息。然后,后端和前端都可以发送 CopyData 消息,直到一方发送 CopyDone 消息。在客户端发送 CopyDone 消息之后,连接将从 copy-both 模式切换到 copy-out 模式,并且客户端不能再发送 CopyData 消息。同样,当服务器发送 CopyDone 消息时,连接将进入 copy-in 模式,并且服务器不能再发送 CopyData 消息。在双方都发送了 CopyDone 消息之后,复制模式将被终止,后端恢复到命令处理模式。如果在 copy-both 模式期间发生后端检测到的错误,后端将发出一个 ErrorResponse 消息,丢弃前端消息直到收到 Sync 消息,然后发出 ReadyForQuery 并返回正常处理。前端应将收到 ErrorResponse 视为终止双向复制;在这种情况下不应发送 CopyDone。有关通过 copy-both 模式传输的子协议的更多信息,请参阅 第 54.4 节。
CopyInResponse、CopyOutResponse 和 CopyBothResponse 消息包含通知前端每行列数以及每列使用的格式代码的字段。(在当前实现中,给定 COPY
操作的所有列将使用相同的格式,但消息设计并未假定这一点。)
在后端发送不完全由前端命令流直接触发的消息的几种情况下。前端必须随时准备处理这些消息,即使在未执行查询时也是如此。至少,在开始读取查询响应之前,应该检查这些情况。
外部活动可能会生成 NoticeResponse 消息;例如,如果数据库管理员发出“快速”数据库关闭命令,后端将在关闭连接之前发送一个 NoticeResponse 来指示此事实。因此,即使连接名义上处于空闲状态,前端也应始终准备好接收和显示 NoticeResponse 消息。
每当后端认为前端应该了解的任何参数的活动值发生变化时,都会生成 ParameterStatus 消息。最常见的情况是响应前端执行的 SET
SQL 命令,这种情况实际上是同步的——但参数状态更改也可能因为管理员更改了配置文件然后向服务器发送了 SIGHUP 信号而发生。此外,如果 SET
命令被回滚,将生成一个适当的 ParameterStatus 消息来报告当前有效值。
目前,有一个硬编码的参数集,将为这些参数生成 ParameterStatus。它们是:
application_name |
scram_iterations |
client_encoding |
search_path |
DateStyle |
server_encoding |
default_transaction_read_only |
server_version |
in_hot_standby |
session_authorization |
integer_datetimes |
standard_conforming_strings |
IntervalStyle |
TimeZone |
is_superuser |
(default_transaction_read_only
和 in_hot_standby
在 14 版之前的版本中未报告;scram_iterations
在 16 版之前的版本中未报告;search_path
在 18 版之前的版本中未报告。)请注意,server_version
、server_encoding
和 integer_datetimes
是伪参数,在启动后无法更改。此集合将来可能会更改,甚至可能变得可配置。因此,前端应简单地忽略它不理解或不关心的参数的 ParameterStatus。
如果前端发出 LISTEN
命令,那么每当为同一频道名称执行 NOTIFY
命令时,后端都会发送一个 NotificationResponse 消息(不要与 NoticeResponse 混淆!)。
目前,NotificationResponse 只能在事务之外发送,因此它不会出现在命令-响应系列中间,尽管它可能在 ReadyForQuery 之前发生。然而,设计假设这一点是不明智的。好的做法是能够在协议的任何时候接受 NotificationResponse。
在查询处理过程中,前端可能请求取消查询。取消请求不会直接发送到后端打开的连接上,原因在于实现效率:我们不希望后端在查询处理过程中不断检查前端的新输入。取消请求应该相对不频繁,因此我们使其稍微繁琐一些,以避免在正常情况下受到影响。
要发出取消请求,前端会打开一个新连接到服务器并发送一个 CancelRequest 消息,而不是通常会通过新连接发送的 StartupMessage 消息。服务器将处理此请求然后关闭连接。出于安全原因,不会对 CancelRequest 消息做出直接响应。
除非 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加密。服务器将识别那些在没有任何前置 SSLRequest 数据包的情况下立即开始SSL协商的连接。一旦建立SSL连接,服务器将期望一个常规的 startup-request 数据包,并在加密通道上继续协商。在这种情况下,任何其他加密请求都将被拒绝。此方法不适用于通用工具,因为它无法协商最佳可用连接加密或处理未加密的连接。然而,它对于服务器和客户端一起受控的环境很有用。在这种情况下,它可以避免一个往返的延迟,并允许使用依赖标准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加密的方法,但管理员可以通过身份验证检查的副产品将服务器配置为拒绝未加密的会话。
如果您在文档中看到任何不正确、与您在使用特定功能时的体验不符或需要进一步说明的内容,请使用 此表单 报告文档问题。