Postgresql 中文操作指南
55.2. Message Flow #
本节介绍每种消息类型的消息流和语义。(每条消息的确切表示的详细信息均显示在 Section 55.7 中。)连接状态取决于几种不同的子协议:启动、查询、函数调用、COPY 和终止。对于异步操作(包括通知响应和命令取消),还有特殊条款,这些操作可以在启动阶段后随时发生。
55.2.1. Start-up #
为了开始会话,前端打开与服务器的连接并发送启动消息。此消息包括用户以及用户想要连接的数据库的名称;它还标识要使用的特定协议版本。(启动消息可以选择包括运行时参数的其他设置。)然后,服务器使用此信息和它的配置文件(如 pg_hba.conf)的内容,来确定连接是否初步可接受,以及需要什么其他身份验证(如果有)。
然后,服务器发送一个适当的身份验证请求消息,前端必须用一个适当的身份验证响应消息(如密码)进行回复。对于除 GSSAPI、SSPI 和 SASL 之外的所有身份验证方法,最多一个请求和一个响应。在某些方法中,前端根本不需要任何响应,所以不会发生身份验证请求。对于 GSSAPI、SSPI 和 SASL,可能需要多次交换数据包才能完成身份验证。
身份验证周期在服务器拒绝连接尝试(错误响应)或发送身份验证确定后结束。
此阶段中服务器可能的消息包括:
-
ErrorResponse
-
连接尝试已被拒绝。然后服务器立即关闭连接。
-
-
AuthenticationOk
-
身份验证交换已成功完成。
-
-
AuthenticationKerberosV5
-
前端现在必须参加与服务器的 Kerberos V5 身份验证对话框(此处未描述,为 Kerberos 规范的一部分)。如果成功,服务器将用身份验证确定响应,否则将用错误响应响应。此方法不再受支持。
-
-
AuthenticationCleartextPassword
-
前端现在必须发送包含明文形式密码的 PasswordMessage。如果这是正确的密码,服务器将用身份验证确定响应,否则将用错误响应响应。
-
-
AuthenticationMD5Password
-
前端现在必须发送包含密码(带用户名)的 PasswordMessage,该密码使用 MD5 加密,然后使用身份验证 MD5 密码消息中指定的 4 字节随机盐再次加密。如果这是正确的密码,服务器将用身份验证确定响应,否则将用错误响应响应。实际 PasswordMessage 可在 SQL 中计算 concat('md5', md5(concat(md5(concat(password, username)), random-salt)))。(请记住 md5() 函数以十六进制字符串形式返回其结果。)
-
-
AuthenticationGSS
-
前端现在必须启动 GSSAPI 协商。前端将发送包含 GSSAPI 数据流第一部分的 GSSResponse 消息以响应此消息。如果需要更多消息,服务器将用身份验证 GSS 继续响应。
-
-
AuthenticationSSPI
-
前端现在必须启动 SSPI 协商。前端将发送包含 SSPI 数据流第一部分的 GSSResponse 以响应此消息。如果需要更多消息,服务器将用身份验证 GSS 继续响应。
-
-
AuthenticationGSSContinue
-
此消息包含 GSSAPI 或 SSPI 协商上一步的响应数据(身份验证 GSS、身份验证 SSPI 或上一个身份验证 GSS 继续)。如果此消息中的 GSSAPI 或 SSPI 数据指示完成身份验证需要更多数据,前端必须将该数据作为另一个 GSSResponse 消息发送。如果通过此消息完成了 GSSAPI 或 SSPI 身份验证,服务器接下来将发送身份验证确定以指示成功身份验证或发送错误响应以指示失败。
-
-
AuthenticationSASL
-
前端现在必须使用消息中列出的某种 SASL 机制发起 SASL 协商。前端将发送 SASLInitialResponse,其中包含已选机制的名称,以及作为对此进行响应的 SASL 数据流的第一部分。如果需要更多消息,服务器将使用 AuthenticationSASLContinue 响应。有关详细信息,请参见 Section 55.3。
-
-
AuthenticationSASLContinue
-
此消息包含 SASL 协商上一步的质询数据(身份验证 SASL 或上一个身份验证 SASL 继续)。前端必须用 SASLResponse 消息响应。
-
-
AuthenticationSASLFinal
-
SASL 身份验证已完成,其中包含附加的针对客户端的特定于机制的数据。服务器接下来将发送身份验证确定以指示成功身份验证或发送错误响应以指示失败。仅当 SASL 机制指定在完成时从服务器发送到客户端的其他数据时,才会发送此消息。
-
-
NegotiateProtocolVersion
-
服务器不支持客户端请求的微小协议版本,但支持早期版本的协议;此消息指示最高支持的微小版本。如果客户端在启动数据包中请求了不支持的协议选项(即以 _pq._ 开头),也将发送此消息。此消息之后将是错误响应或指示身份验证成功或失败的消息。
-
如果前端不支持服务器请求的身份验证方法,则应立即关闭连接。
接收到身份验证确定后,前端必须等待服务器的进一步消息。在此阶段正在启动一个后端进程,前端只是一个感兴趣的旁观者。启动尝试仍然可能失败(错误响应),或者服务器拒绝支持所请求的微小协议版本(协商协议版本),但在正常情况下后端将发送一些参数状态消息、后端密钥数据,最后是准备查询。
在此阶段,后端将尝试应用在启动消息中给出的任何其他运行时参数设置。如果成功,这些值将成为会话默认值。错误将导致错误响应并退出。
此阶段中来自后端的可能消息包括:
-
BackendKeyData
-
此消息提供前端必须保存的密钥数据,如果以后想要发出取消请求。前端不应对此消息进行响应,但应继续侦听 ReadyForQuery 消息。
-
-
ParameterStatus
-
此消息通知前端后端参数的当前(初始)设置,例如 client_encoding 或 DateStyle。前端可以忽略此消息,或记录设置以备将来使用;有关更多详细信息,请参见 Section 55.2.7。前端不应对此消息进行响应,但应继续监听 ReadyForQuery 消息。
-
-
ReadyForQuery
-
启动已完成。前端现在可发出命令。
-
-
ErrorResponse
-
启动失败。发送此消息后关闭连接。
-
-
NoticeResponse
-
已发出警告消息。前端应显示消息,但继续侦听 ReadyForQuery 或 ErrorResponse。
-
ReadyForQuery 消息与后端在每个命令周期后发出的消息相同。根据前端的编码需要,合理地将 ReadyForQuery视为启动命令周期,或将 ReadyForQuery视为结束启动阶段和每个后续命令周期。
55.2.2. Simple Query #
一个简单的查询周期由前端向后端发送 Query 消息启动。该消息包括一个以文本字符串形式表示的 SQL 命令(或命令)。然后,后端根据查询命令字符串的内容发送一条或多条响应消息,最后发送一个 ReadyForQuery 响应消息。ReadyForQuery 告知前端它可以安全地发送新命令。(实际上,在发出另一个命令之前,前端不必等待 ReadyForQuery,但前端必须承担弄清如果前面的命令失败且已经发出的后续命令成功时会发生什么情况的责任。)
后端可能的响应消息是:
-
CommandComplete
-
SQL 命令正常完成。
-
-
CopyInResponse
-
后端准备好将数据从前端复制到表中;请参见 Section 55.2.6。
-
-
CopyOutResponse
-
后端准备好将数据从表中复制到前端;请参见 Section 55.2.6。
-
-
RowDescription
-
指示即将返回行以响应 SELECT、FETCH 等查询。此消息的内容描述了行的列布局。接下来是 DataRow 消息以返回到前端的每行。
-
-
DataRow
-
SELECT、FETCH 等查询返回的一组行之一。
-
-
EmptyQueryResponse
-
识别到一个空查询字符串。
-
-
ErrorResponse
-
An error has occurred.
-
-
ReadyForQuery
-
查询字符串的处理已完成。发送一个独立的消息来表明这一点,因为查询字符串可能包含多个 SQL 命令。(CommandComplete 标记处理一个 SQL 命令的结束,而不是整个字符串的结束。)无论处理成功终止还是出错,都始终会发送 ReadyForQuery。
-
-
NoticeResponse
-
已针对查询发出警告消息。除了其他响应,通知也是如此,即后端将继续处理命令。
-
对 SELECT 查询(或其他返回行集的查询,例如 EXPLAIN 或 SHOW)的响应通常包括 RowDescription、零个或更多个 DataRow 消息,然后是 CommandComplete。到前端或从前端的 COPY 会调用特殊协议,如 Section 55.2.6 中所述。所有其他查询类型通常只生成 CommandComplete 消息。
由于一个查询字符串可以包含多个查询(用分号分隔),因此在后端完成处理查询字符串之前,可能会有多个这样的响应序列。当整个字符串已处理完毕并且后端准备接受新的查询字符串时,发出 ReadyForQuery。
如果接收到一个完全为空的(除空格外无其他内容)的查询字符串,响应是 EmptyQueryResponse 接着是 ReadyForQuery。
如果遇到错误,则会发出 ErrorResponse,后跟 ReadyForQuery。ErrorResponse 会中止对查询字符串的所有进一步处理(即使其中仍有更多查询)。请注意,这可能发生在由单个查询生成的系列消息期间的某个时间点。
在简单查询模式中,检索到的值格式始终为文本,除非给定的命令是使用 BINARY 选项声明的光标 FETCH。在这种情况下,检索到的值将采用二进制格式。RowDescription 消息中给出的格式代码告诉正在使用哪种格式。
每当前端期望任何其他类型的消息时,它都必须做好接受 ErrorResponse 和 NoticeResponse 消息的准备。另请参见 Section 55.2.7,了解后端可能由于外部事件而生成的消息。
推荐的做法是使用状态机样式对前端进行编码,该样式可以在任何时刻接受任何消息类型(前提是这有意义),而不是将有关消息的确切顺序的假设编入其中。
55.2.2.1. Multiple Statements in a Simple 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,但不会回滚第一个。
通过在 implicit transaction block 中运行多语句查询消息中的语句,来实现此行为,除非有某个显式的事务块供它们运行。隐式事务块和常规事务块之间的主要区别在于,隐式事务块会在查询消息的末尾自动关闭,如果没有任何错误,则会隐式提交;如果出现错误,则会隐式回滚。这类似于为自身执行的语句(当不在事务块中时)发生的隐式提交或回滚。
如果会话已处于事务块中,这是由于之前某个消息中的 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 无法提交的明显差异。语义分析或之后检测到的错误,例如拼写错误的表或列名,不会产生此效果。
55.2.3. Extended Query #
扩展查询协议将上述简单查询协议分解为多个步骤。准备步骤的结果可以多次重复使用,以提高效率。此外,还提供了附加功能,例如将数据值作为单独参数提供而不是必须直接插入到查询字符串中的可能性。
在扩展协议中,前端首先发送一个 Parse 消息,该消息包含一个文本查询字符串(可选地包含有关参数占位符的数据类型的一些信息),以及一个目标已准备好的语句对象的名称(空字符串选择未命名的已准备好的语句)。响应要么是 ParseComplete,要么是 ErrorResponse。可以通过 OID 指定参数数据类型;如果没有给出,解析器将尝试以与处理未键入的字符串常量相同的方式推断数据类型。
Note
可以将参数数据类型留空,方法是将其设置为零,或将参数类型 OID 数组弄得比查询字符串中使用的参数符号数量 ($__n) 短。另一特殊情况是,可以指定参数的类型为 void(即 void 伪类型的 OID)。这旨在允许将参数符号用于实际上是 OUT 参数的函数参数。通常情况下,void 参数没有可用于的上下文,但如果这样的参数符号出现在函数的参数列表中,它将被有效忽略。例如,像 foo($1,$2,$3,$4) 这样的函数调用可以与具有两个 IN 和两个 OUT 自变量的函数相匹配,如果 $3 和 $4 被指定为类型 void。
Note
包含在解析消息中的查询字符串不能包含多条 SQL 语句;否则将报告一个语法错误。此限制在简单查询协议中不存在,但在扩展协议中存在,这是因为允许准备语句或门户包含多条命令会使协议变得过于复杂。
如果成功创建,则命名的已准备好的语句对象将持续到当前会话结束,除非显式销毁。未命名的已准备好的语句仅持续到发出将未命名的语句指定为目标的下一个 Parse 语句为止。(请注意,简单的查询消息也会销毁未命名的语句。)在可以使用另一个 Parse 消息重新定义命名的已准备好的语句之前,必须显式关闭它们,但对于未命名的语句则不需要。还可以使用 PREPARE 和 EXECUTE 在 SQL 命令级别创建和访问命名的已准备好的语句。
准备语句准备完成后,可以使用关联消息为其准备好执行。该关联消息提供源准备语句的名称(空字符串表示未命名准备语句),目标门户名称(空字符串表示未命名门户)以及准备语句中出现的所有参数占位符要使用的值。提供的参数设置必须与准备语句所需的相同。(如果您在解析消息中声明任何 void 参数,则在关联消息中为其传递 NULL 值。)关联消息还指定用于查询返回的所有数据的格式;该格式可以全部指定,也可以按列指定。响应为关联完成或错误响应。
Note
文本和二进制输出之间的选择是由 Bind 中给定的格式代码决定的,与所涉及的 SQL 命令无关。在使用扩展查询协议时,游标声明中的 BINARY 属性是不相关的。
在处理关联消息时,通常会进行查询计划。如果准备语句没有参数,或者重复执行,则服务器可能会保存创建的计划并在后续关联消息中为同一个准备语句重复使用该计划。然而,服务器仅当发现可以创建一个通用计划时,这个通用的计划的效率不会远低于依赖于所提供的特定参数值计划的效率。从协议而言,这是透明的。
如果成功创建,则命名门户对象持续到当前事务结束,除非明确销毁。未命名门户在事务结束时销毁,或者在指定未命名门户作为目标的下一个关联语句发出后立即销毁。(请注意,简单的查询消息也会销毁未命名门户。)在由其他关联消息重新定义之前,必须明确关闭命名门户,但这对于未命名门户不是必须的。命名门户也可以使用 DECLARE CURSOR 和 FETCH 在 SQL 命令级别创建和访问。
创建门户后,可以使用执行消息执行该门户。执行消息指定门户名称(空字符串表示未命名门户)和最大结果行数(零表示“获取所有行”)。结果行数仅对包含返回行集的命令的门户有意义;在其他情况下,该命令总是执行到完成,并且忽略行数。执行的可行响应与通过简单查询协议发出的查询的描述相同,只是执行不会导致发出准备好查询或行描述。
如果执行在完成门户的执行前终止(由于达到非零结果行数),它将发送门户暂停消息;此消息的出现告诉前端,应该针对同一个门户发出另一个执行消息来完成操作。表示完成源 SQL 命令的命令完成消息在门户执行完成之前不会发送。因此,执行阶段总是以出现以下消息之一的方式终止:命令完成、空查询响应(如果门户是从空查询字符串创建的)、错误响应或门户暂停。
在每个系列的扩展查询消息完成时,前端都应该发出同步消息。这个无参数消息导致后端关闭当前事务,如果它不在 BEGIN/COMMIT 事务块内(“关闭”的意思是如果没有错误则提交,如果有错误则回滚)。然后发出准备好查询响应。同步的目的是为错误恢复提供重新同步点。在处理任何扩展查询消息时检测到错误时,后端会发出错误响应,然后读取并丢弃消息直到到达同步,然后发出准备好查询并返回到正常消息处理。(但请注意,如果在 while 处理同步时检测到错误,不会跳过任何内容 - 这确保对每次同步只发送一次准备好查询。)
Note
Sync 不会导致使用 BEGIN 打开的事务块关闭。因为 ReadyForQuery 消息包含事务状态信息,所以可以检测到这种情况。
除了这些基本的必需操作,扩展查询协议还可以使用其他几个可选操作。
描述消息(门户变量)指定现有门户的名称(或针对未命名门户的空字符串)。响应是行描述消息,描述执行门户后返回的行;或者如果没有包含将返回行的查询的门户,则为无数据消息;或者如果没有该门户,则为错误响应。
描述消息(语句变量)指定现有准备语句的名称(或未命名准备语句的空字符串)。响应是参数描述消息,描述语句需要的参数,然后是行描述消息,描述最终执行语句时将返回的行(或者如果语句不会返回行,则为无数据消息)。如果没有该准备语句,则会发出错误响应。请注意,由于关联尚未发出,后端还不知道将用于返回列的格式;在这种情况下,行描述消息中的格式代码字段将为零。
Tip
在大多数情况下,前端应该在发出执行之前发出描述的一个或另一个变量,以确保它知道如何解释它将获得的结果。
关闭消息关闭现有准备语句或门户并释放资源。针对不存在的语句或门户名称发出关闭不是错误。响应通常是关闭完成,但如果在释放资源时遇到一些困难,可能是错误响应。请注意,关闭准备语句意味着会隐式关闭从该语句构建的任何开放门户。
刷新消息不会导致生成任何特定输出,但会强制后端传送其输出缓冲区中保留的任何数据。前端如果想在发送更多命令前检查该命令的结果,则必须在除了同步消息之外的任何扩展查询命令之后发送刷新消息。如果不刷新,则后端返回的消息将组合成尽可能少的包,以最大程度减少网络开销。
Note
简单的 Query 消息大约等同于使用未命名准备好的语句和门户对象且无参数的 Parse、Bind、portal Describe、Execute、Close、Sync 系列。不同的是,它会在查询字符串中接受多个 SQL 语句,并自动依次执行每个语句的 bind/describe/execute 序列。另一个不同之处是,它不会返回 ParseComplete、BindComplete、CloseComplete 或 NoData 消息。
55.2.4. Pipelining #
使用扩展查询协议允许 pipelining,这意味着发送一系列查询,而不等待之前的查询完成。这样可以减少完成一系列给定操作所需的网络往返次数。然而,用户必须仔细考虑步骤之一失败时所需的行为,因为后续查询将已经处于向服务器传输中。
处理这种问题的方法之一是将整个查询系列作为单一事务,即用 BEGIN … COMMIT 将其包装起来。然而,如果希望其中一些命令独立于其他命令提交,这并无帮助。
扩展查询协议提供了另一种管理这个问题的方法,即省略在有依赖关系的步骤之间发送同步消息。由于在出现错误后,后端将跳过命令消息,直到找到同步,所以如果较早的一个失败,允许管道中的后续命令自动跳过,而无需客户端使用 BEGIN 和 COMMIT 明确管理这一点。可以通过同步消息分隔管道中可以独立提交的段。
如果客户端没有发出明确的 BEGIN,则每个同步通常会导致隐式 COMMIT(如果前一个步骤成功)或隐式 ROLLBACK(如果它们失败)。然而,有少数 DDL 命令(如 CREATE DATABASE),它们无法在事务块中执行。如果管道中执行其中一个命令,它将失败,除非它是管道中的第一个命令。此外,它如果成功,它将强制立即提交以保留数据库一致性。因此,紧跟这些命令之一后的同步除用准备好查询响应之外没有其他作用。
当使用此方法时,管道的完成必须通过计数准备好查询消息并等待其达到已发送同步的数量来确定。计数命令完成响应不可靠,因为某些命令可能会跳过,因此不会生成完成消息。
55.2.5. Function Call #
函数调用子协议允许客户端请求数据库 pg_proc 系统目录中存在的任何函数的直接调用。客户端必须具有该函数的执行权限。
Note
函数调用子协议是一项遗留功能,最好在编写新代码时避免使用。通过设置执行 SELECT function($1, …) 的准备好的语句,能实现类似结果。然后,可以用 Bind/Execute 替换函数调用循环。
函数调用周期通过前端向后端发送函数调用消息开始。然后,后端根据函数调用的结果发送一个或多个响应消息,最后发送一个准备好查询响应消息。准备好查询通知前端可以安全地发送新的查询或函数调用。
后端可能的响应消息是:
-
ErrorResponse
-
An error has occurred.
-
-
FunctionCallResponse
-
函数调用已完成并返回了消息中给出的结果。(请注意,函数调用协议只能处理单个标量结果,而不是行类型或结果集。)
-
-
ReadyForQuery
-
函数调用的处理已完成。 无论处理成功终止还是出错,都会始终发送 ReadyForQuery。
-
-
NoticeResponse
-
已发出有关函数调用的警告消息。通知是除其他响应之外的,即后端将继续处理命令。
-
55.2.6. COPY Operations #
COPY 命令允许以高速将大量数据传输到或从服务器。 每个复制输入和复制输出操作将连接切换到一个不同的子协议,该子协议将持续到操作完成。
当后端执行 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 视为终止复制输出模式。
在 CopyData 消息之间插入 NoticeResponse 和 ParameterStatus 消息是可能的;前端必须处理这些情况,并应为其他异步消息类型做好准备(请参见 Section 55.2.7)。否则,任何除 CopyData 或 CopyDone 以外的消息类型都可以视为终止 copy-out 模式。
还有另一种与复制相关的模式称为 copy-both,它允许高速将大量数据从服务器传输到 and。当 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 模式下传输的子协议的更多信息,请参见 Section 55.4。
CopyInResponse、CopyOutResponse 和 CopyBothResponse 消息包括通知前端每行列数和用于每列的格式代码的字段。(在目前的实现中,给定的 COPY 操作中的所有列都将使用相同的格式,但消息设计并没有对此做出假设。)
55.2.7. Asynchronous Operations #
在后端不会由前端的命令流专门提示发送消息的几种情况下。前端必须随时准备好处理这些消息,即使不执行查询。至少,在开始读取查询响应之前,应检查这些情况。
由于外部活动可能会生成 NoticeResponse 消息;例如,如果数据库管理员命令“快速”关闭数据库,则后端将在关闭连接之前发送 NoticeResponse 表示此事实。因此,即使连接实际上处于空闲状态,前端也应始终做好接受和显示 NoticeResponse 消息的准备。
只要对后端认为前端应该了解的任何参数的活动值发生更改,就会生成 ParameterStatus 消息。最常见的情况是响应前端执行的 SET SQL 命令,并且此情况下实际上是同步的——但也有可能由于管理员更改了配置文件然后向服务器发送 SIGHUP 信号而发生参数状态更改。此外,如果 SET 命令回滚,则会生成一个适当的 ParameterStatus 消息来报告当前有效值。
目前有一组固定参数,ParameterStatus 将为此生成。 他们是:
(server_encoding、TimeZone 和 integer_datetimes 未在 8.0 之前的版本中报告;standard_conforming_strings 未在 8.1 以前的版本中报告;IntervalStyle 未在 8.4 以前的版本中报告;application_name 未在 9.0 之前的版本中报告;default_transaction_read_only 和 in_hot_standby 未在 14 之前的版本中报告;scram_iterations 未在 16 之前的版本中报告。)请注意,server_version、server_encoding 和 integer_datetimes 是启动后无法更改的伪参数。此设置可能会在未来更改,甚至变得可配置。因此,前端应简单地忽略它不理解或不关心的参数的 ParameterStatus。
如果前端发出 LISTEN 命令,则只要为同一个通道名称执行 NOTIFY 命令,后端就会发送 NotificationResponse 消息(不要与 NoticeResponse 混淆!)。
Note
目前,NotificationResponse 只能在事务外发送,因此它不会出现在命令-响应系列的中间,尽管它可能会在 ReadyForQuery 前出现。不过,设计假定此行为的前端逻辑是不明智的。良好的做法是能够在协议中的任何位置接受 NotificationResponse。
55.2.8. Canceling Requests in Progress #
在处理查询期间,前端可能会请求取消查询。出于实现效率的原因,取消请求不会直接发送到与后端建立的开放连接上:我们不希望后端在查询处理期间不断检查来自前端的新输入。取消请求相对较少见,因此我们让它们稍有麻烦,以避免在正常情况下受到惩罚。
要发出取消请求,前端会打开与服务器的新连接,并发送 CancelRequest 消息,而不是新连接中正常发送的 StartupMessage 消息。服务器将处理此请求,然后关闭连接。出于安全原因,取消请求消息没有直接的回复。
除非 CancelRequest 消息包含连接启动期间传递给前端的相同密钥数据(PID 和密钥),否则将忽略该消息。如果该请求与当前执行的后端匹配 PID 和密钥,则会终止当前查询的处理。(在现有实现中,这是通过向处理查询的后端进程发送特殊信号来完成的。)
取消信号可能有或可能没有影响——例如,如果取消信号在后端完成查询处理后再到达,那么其将没有任何影响。如果取消有效,它会导致当前命令因出错消息而提前终止。
这一切的结果是,出于安全和效率的考虑,前端没有直接的方法来验证取消请求是否成功。其必须继续等待后端对查询做出响应。发出取消只是提高了当前查询将很快完成的可能性,并提高了其将因出错消息而失败而不是成功完成的可能性。
由于取消请求是通过与服务器的新连接发送的,而不是通过常规的前端/后端通信链接发送的,因此可以由任何进程发出取消请求,而不仅仅是需要取消其查询的前端。这在构建多进程应用程序时可能会提供额外的灵活性。它还引入了一个安全风险,即未经授权的人员可能会尝试取消查询。通过要求在取消请求中提供动态生成的密钥来解决安全风险。
55.2.9. Termination #
正常的优雅终止程序是,前端发送一个 Terminate 消息并立即关闭连接。收到此消息后,后端关闭连接并终止。
在罕见的情况下(例如管理员命令的数据库关闭),后端可能会在没有任何前端请求的情况下断开连接。在这种情况下,后端将在关闭连接之前尝试发送一个提供断开连接原因的错误消息或通知消息。
其他终止场景是由各种故障案例引起的,例如一端的核心转储或另一端,通信链接的丢失,消息边界同步的丢失等。如果前端或后端看到连接意外关闭,它应该清理并终止。前端可以选择通过重新联系服务器来启动一个新后端,如果它不想自行终止。如果接收到无法识别的消息类型,也建议关闭连接,因为这可能表明消息边界同步丢失。
对于正常或异常终止,任何打开的事务都不会提交,而是回滚。然而应该注意的是,如果一个前端在处理非 SELECT 查询时断开连接,后端可能会在注意到断开连接之前完成查询。如果查询在任何事务块之外 (BEGIN … COMMIT 序列),那么在识别断开连接之前其结果可能会被提交。
55.2.10. SSL Session Encryption #
如果使用 SSL 支持构建 PostgreSQL,可以使用 SSL 对前端/后端通信进行加密。这在攻击者可能捕获会话流量的环境中提供了通信安全性。有关使用 SSL 加密 PostgreSQL 会话的更多信息,请参见 Section 19.9。
为了启动 SSL 加密连接,前端最初发送一个 SSLRequest 消息,而不是 StartupMessage。然后服务器使用单个字节进行响应,包含 S 或 N,分别表示它愿意或不愿意执行 SSL。如果前端对响应不满意,它可能会在此处关闭连接。要在 S 之后继续执行,请与服务器执行 SSL 启动握手(此处未描述,是 SSL 规范的一部分)。如果成功,请继续发送常规的 StartupMessage。在这种情况下,StartupMessage 和所有后续数据都将通过 SSL 加密。要在 N 之后继续执行,请发送常规的 StartupMessage 并继续执行而不加密。(或者,允许在 N 响应之后发出 GSSENCRequest 消息,尝试使用 GSSAPI 加密而不是 SSL。)
前端还应做好准备,处理服务器对 SSLRequest 发出的 ErrorMessage 响应。这只会出现在服务器早于 SSL 支持添加到 PostgreSQL 的情况下。(此类服务器现在非常古老,并且可能不再存在。)在这种情况下,必须关闭连接,但前端可能会选择打开一个全新连接,并在不请求 SSL 的情况下继续执行。
当可以执行 SSL 加密时,服务器将只发送单个 S 字节,然后等待前端启动 SSL 握手。如果此时能读取到额外的字节,这可能意味着中间人在尝试执行缓冲区填充攻击 ( CVE-2021-23222)。前端的编写方式应为要么在将套接字转交给其 SSL 库之前从套接字中读取恰好一个字节,要么在发现已读取额外的字节时将其视为协议违规。
初始 SSLRequest 也可以用于正在打开发送 CancelRequest 消息的连接中。
虽然协议本身没有提供服务器强制 SSL 加密的方法,但管理员可以将服务器配置为将未加密会话作为身份验证检查的副产品拒之门外。
55.2.11. GSSAPI Session Encryption #
如果 PostgreSQL 是使用 GSSAPI 支持构建的,则前端/后端的通信可以使用 GSSAPI 加密。在攻击者可能能够捕获会话流量的环境中,这提供了通信安全性。有关使用 GSSAPI 加密 PostgreSQL 会话的详细信息,请参见 Section 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 响应。这只会出现在服务器早于 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 加密的方法,但管理员可以将服务器配置为拒绝未加密的会话,作为身份验证检查的副产品。