本节描述消息流以及各种消息类型的语义(每种消息的精确格式见Section 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() 函数返回的是十六进制字符串。)
对 MD5 加密密码的支持已弃用,并将在未来版本 PostgreSQL 中移除。关于迁移到其他密码类型,请参阅 Section 20.5。
前端现在必须发起一次 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。详见Section 53.3。
该消息包含 SASL 协商前一步(AuthenticationSASL 或前一个 AuthenticationSASLContinue)的挑战数据。前端必须用一条 SASLResponse 消息响应。
SASL 认证已完成,并附带了面向客户端的额外机制相关数据。服务器接下来会发送 AuthenticationOk 表示认证成功,或发送 ErrorResponse 表示认证失败。只有当 SASL 机制规定在完成时要从服务器向客户端发送附加数据时,才会发送该消息。
如果服务器不支持客户端请求的协议次版本,但支持更早的协议版本,就会发送该消息并指明其支持的最高次版本。如果客户端在启动包中请求了不受支持的协议选项(即以 _pq_. 开头),也会发送该消息。
在该消息之后,认证会继续使用服务器指明的协议版本。如果客户端不支持该较旧版本,就应立即关闭连接。如果服务器没有发送该消息,则表示它支持客户端请求的协议版本以及所有协议选项。
如果前端不支持服务器要求的认证方式,那么它应该马上关闭连接。
在收到 AuthenticationOk 消息之后,前端必须继续等待来自服务器的后续消息。在这个阶段,一个后端进程正在启动,而前端只是旁观者。启动尝试仍可能失败(ErrorResponse),服务器也可能拒绝支持所请求的协议次版本(NegotiateProtocolVersion);但通常情况下,后端会发送一些 ParameterStatus 消息、BackendKeyData,以及最后的 ReadyForQuery。
在这个阶段,后端会尝试应用启动消息中给出的任何额外运行时参数设置。如果成功,这些值就会成为会话的默认值。发生错误则会导致 ErrorResponse 并退出。
这个阶段来自后端的可能消息是:
该消息提供密钥数据。如果前端希望稍后发送取消请求,就必须保存这些数据。前端不应响应该消息,而应继续等待 ReadyForQuery 消息。
PostgreSQL 服务器总会发送该消息,但已知一些不支持查询取消的第三方后端实现不会发送。
该消息告知前端后端参数的当前(初始)设置,例如client_encoding或DateStyle。前端可以忽略这些信息,也可以记录下来供后续使用;详见Section 53.2.7。前端不应响应该消息,而应继续等待 ReadyForQuery 消息。
启动成功,前端现在可以发出命令。
启动失败,在发送完这个消息之后连接被关闭。
已发出一条警告消息。前端应显示该消息,但仍应继续等待 ReadyForQuery 或 ErrorResponse。
ReadyForQuery 消息与后端在每个命令周期结束后发出的消息是同一个。前端可根据自身编码需要,将 ReadyForQuery 视为一个命令周期的开始,或视为启动阶段以及后续每个命令周期的结束。
一个简单查询周期由前端向后端发送一条 Query 消息来启动。该消息包含一条或多条以文本字符串表示的 SQL 命令。后端随后会根据查询命令串的内容向前端发送一条或多条响应消息,最后再发送一条 ReadyForQuery 响应消息。ReadyForQuery 通知前端它现在可以安全地发送新的命令。(实际上,前端并不一定非要等到收到 ReadyForQuery 才发送下一条命令,但这样一来,前端就必须自己处理“先前命令失败而后续已发出的命令成功”这种情况。)
后端可能返回的响应消息有:
一条 SQL 命令正常完成。
后端已准备好把数据从前端复制到表中;参见Section 53.2.6。
后端已准备好把数据从表中复制到前端;参见Section 53.2.6。
表示即将返回行作为对SELECT、FETCH等查询的响应。 此消息的内容描述了行的列布局。这将跟随每个返回给前端的行的DataRow消息。
由SELECT、FETCH等查询返回的一组行中的一个。
识别出了一条空查询字符串。
发生了一个错误。
查询字符串的处理已完成。发送一个单独的消息来指示这一点,因为查询字符串可能包含多个SQL命令。 (CommandComplete标记了一个SQL命令的处理结束,而不是整个字符串的结束。) 无论处理是成功还是出现错误,都将始终发送ReadyForQuery。
与查询相关的警告消息已发出。 通知是其他响应的补充,即后端将继续处理命令。
对 SELECT 查询(或其他返回行集的查询,如 EXPLAIN 或 SHOW)的响应,通常包含 RowDescription、零条或多条 DataRow 消息,以及最后的 CommandComplete。向前端 COPY 或从前端 COPY 会调用Section 53.2.6中描述的特殊协议。所有其他类型的查询通常只产生一条 CommandComplete 消息。
由于查询字符串可能包含若干条查询(以分号分隔),因此在后端完成整个查询字符串的处理之前,可能会出现多个这样的响应序列。只有在整个字符串处理完毕且后端已准备好接受新的查询字符串时,才会发出 ReadyForQuery 消息。
如果收到的是一条完全为空的查询字符串(除了空白字符之外没有任何内容),响应就是一条 EmptyQueryResponse,后面跟着 ReadyForQuery。
一旦发生错误,就会发出一条 ErrorResponse 消息,后面跟着 ReadyForQuery。ErrorResponse 会中止该查询字符串中后续所有处理(即使其中还包含其他查询)。请注意,这种情况可能发生在处理单条查询所生成的消息序列中途。
在简单查询模式中,取回值的格式总是文本,除非给出的命令是在使用 BINARY 选项声明的游标上执行FETCH。在这种情况下,取回的值将采用二进制格式。RowDescription 消息中给出的格式代码会告诉我们使用的是哪种格式。
当前端正在等待其他类型的消息时,也必须准备好接收 ErrorResponse 和 NoticeResponse 消息。参见Section 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,但不会回滚第一个。
这种行为通过在一个隐式事务块中的一个多语句Query消息中运行那些语句来实现,除非它们运行在某个显式事务块中。隐式事务块与常规事务块之间的区别在于隐式块会在Query消息结束时自动被关闭,或者是在没有错误的情况下由一个隐式提交关闭,或者是在有错误时由一个隐式的回滚关闭。这类似于一个语句自己执行(当不在事务块中时)时发生的隐式提交或回滚。
如果会话已经在一个事务块中,作为前面某个消息中BEGIN的结果,那么Query消息会简单地继续那个事务块,不管该消息包含一个语句还是多个语句。不过,如果该Query消息包含一个关闭现有事务块的COMMIT或者ROLLBACK,那么任何接下来的语句都会在一个隐式事务块中被执行。反过来,如果在多语句Query消息中出现一个BEGIN,那么它会开始一个常规事务块,这个常规事务块将只能被一个显式的COMMIT或者ROLLBACK终止,不管这两种命令是出现在这个Query消息还是后面的一个Query消息中。如果BEGIN跟在一些作为隐式事务块执行的语句后面,那些语句不会被立刻提交。实际上,它们会被包括到新的常规事务块中。
出现在一个隐式事务块中的COMMIT或者ROLLBACK会被正常执行并且关闭该隐式块。不过,由于没有先前的BEGIN配对的COMMIT或者ROLLBACK表示一种错误,所以将会发出一个警告。如果后面还有更多语句,将会为它们开始一个新的隐式事务块。
在隐式事务块中不允许保存点,因为它们会与发生错误时自动关闭块的行为发生冲突。
记住,不管任何事务控制命令存不存在,Query消息的执行会在第一个错误时停止。因此,对于下面的在一个Query消息中的示例
BEGIN; SELECT 1/0; ROLLBACK;
会话中将留下一个失败的常规事务块,因为在出现除零错误后不会到达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 语句;否则会报告语法错误。这个限制在简单查询协议中并不存在,但在扩展协议中必须如此,因为若允许预备语句或 portal 包含多条命令,会使协议变得过于复杂。
如果成功创建了一个有名的预备语句对象,它会一直持续到当前会话结束,除非被显式销毁。未命名预备语句只会持续到下一条把未命名语句作为目标的 Parse 消息发出为止。(注意,简单 Query 消息也会销毁未命名语句。)有名预备语句在被另一条 Parse 消息重新定义之前必须显式关闭,但未命名语句则无此要求。有名预备语句也可以在 SQL 命令级别通过PREPARE和EXECUTE来创建和访问。
一旦预备语句存在,就可以用 Bind 消息把它准备为可执行状态。Bind 消息给出源预备语句的名称(空字符串表示未命名预备语句)、目标 portal 的名称(空字符串表示未命名 portal),以及预备语句中所有参数占位符应使用的值。所提供的参数集必须与预备语句所需参数相匹配。(如果你在 Parse 消息中声明了任何 void 参数,那么在 Bind 消息中应为它们传递 NULL 值。)Bind 还会指定查询返回数据所使用的格式;格式既可以统一指定,也可以按列指定。响应要么是 BindComplete,要么是 ErrorResponse。
输出采用文本还是二进制格式,由 Bind 中给出的格式代码决定,而与涉及的 SQL 命令无关。在使用扩展查询协议时,游标声明中的 BINARY 属性并不起作用。
通常会在处理 Bind 消息时进行查询规划。如果预备语句没有参数,或者会被重复执行,服务器可能会保存生成的计划,并在后续针对同一预备语句的 Bind 消息中重用它。不过,只有当它发现可以创建一个效率并不比依赖具体参数值的计划差很多的通用计划时,才会这样做。就协议而言,这一切都是透明的。
如果成功创建了一个有名 portal 对象,它会一直持续到当前事务结束,除非被显式销毁。未命名 portal 会在事务结束时销毁,或者在下一条把未命名 portal 作为目标的 Bind 消息发出时立即销毁。(注意,简单 Query 消息也会销毁未命名 portal。)有名 portal 在被另一条 Bind 消息重新定义之前必须显式关闭,而未命名 portal 则无此要求。有名 portal 也可以在 SQL 命令级别通过DECLARE CURSOR和FETCH来创建和访问。
一旦 portal 存在,就可以使用 Execute 消息执行它。Execute 消息指定 portal 的名称(空字符串表示未命名 portal)以及一个最大的结果行计数(零表示“取回全部行”)。结果行计数只对包含返回行集命令的 portal 有意义;在其他情况下,命令总会执行到完成,而行计数会被忽略。Execute 的可能响应与通过简单查询协议发出的查询相同,只是执行不会导致后端发送 ReadyForQuery 或 RowDescription。
如果 Execute 在 portal 执行完成之前终止(因为达到了非零的结果行计数),它会发送一条 PortalSuspended 消息;该消息表明前端应当针对同一个 portal 再发出一条 Execute 消息,以完成此次操作。在 portal 执行完成之前,不会发送表示源 SQL 命令结束的 CommandComplete 消息。因此,一个 Execute 阶段总会以下列消息之一结束:CommandComplete、EmptyQueryResponse(如果 portal 是从空查询字符串创建的)、ErrorResponse 或 PortalSuspended。
每一组扩展查询消息完成后,前端都应发送一条 Sync 消息。这条无参数消息会让后端关闭当前事务,如果当前事务并不处在 BEGIN/COMMIT 事务块内(这里的“关闭”指的是:无错误则提交,有错误则回滚)。随后会发送一条 ReadyForQuery 响应。Sync 的目的是为错误恢复提供一个重新同步点。如果在处理任何扩展查询消息时检测到错误,后端会发出 ErrorResponse,然后持续读取并丢弃消息,直到遇到 Sync,再发送 ReadyForQuery 并回到正常的消息处理流程。(但要注意,如果是在处理 Sync 期间检测到错误,则不会跳过任何消息,这样就能保证每条 Sync 恰好对应一条 ReadyForQuery。)
Sync 不会关闭由BEGIN打开的事务块。之所以能够检测出这种情况,是因为 ReadyForQuery 消息中包含事务状态信息。
除了这些基本的、必须的操作之外,在扩展查询协议里还有几种可选的操作可以使用。
Describe 消息(portal 变体)指定一个现有 portal 的名称(或者用空字符串表示未命名 portal)。响应要么是一条 RowDescription 消息,用于描述执行该 portal 时将返回的行;要么是一条 NoData 消息,如果该 portal 不包含会返回行的查询;要么是 ErrorResponse,如果不存在这样的 portal。
Describe 消息(语句变体)指定一个现有预备语句的名称(或者用空字符串表示未命名预备语句)。响应是一条 ParameterDescription 消息,用于描述该语句所需的参数,随后是一条 RowDescription 消息,用于描述该语句最终执行时将返回的行(如果该语句不返回行,则为 NoData 消息)。如果不存在这样的预备语句,则返回 ErrorResponse。请注意,由于还没有发出 Bind,后端尚不知道返回列将使用什么格式;因此在这种情况下,RowDescription 消息中的格式代码字段将为零。
在大多数场景下,前端都应在发出 Execute 之前先发送某一种 Describe 变体,以确保它知道应如何解释接收到的结果。
Close 消息会关闭一个现有的预备语句或 portal,并释放相关资源。针对不存在的语句或 portal 发出 Close 并不算错误。响应通常是 CloseComplete,但如果在释放资源时遇到困难,也可能返回 ErrorResponse。请注意,关闭预备语句会隐式关闭所有基于该语句构造出的打开 portal。
Flush 消息本身不会产生任何特定输出,但会强制后端发送其输出缓冲区中所有尚待发送的数据。如果前端希望在发出更多命令之前检查某条扩展查询命令的结果,那么除了 Sync 之外,任何扩展查询命令之后都必须发送 Flush。如果没有 Flush,后端返回的消息会尽量合并成最少的数据包,以降低网络开销。
简单 Query 消息大致等价于一串 Parse、Bind、portal Describe、Execute、Close、Sync 操作,它们使用未命名预备语句和未命名 portal 对象,并且不带参数。不同之处在于,简单 Query 会接受包含多条 SQL 语句的查询字符串,并依次自动为每条语句执行绑定、描述和执行序列;另一个区别是它不会返回 ParseComplete、BindComplete、CloseComplete 或 NoData 消息。
使用扩展查询协议允许流水线处理,这意味着发送一系列查询而无需等待先前的查询完成。 这减少了完成一系列操作所需的网络往返次数。然而,用户必须仔细考虑所需的行为,如果其中一步失败, 因为后续查询已经在传输到服务器的过程中。
处理这个问题的一种方法是将整个查询系列作为一个事务处理,即将其包装在BEGIN... COMMIT中。然而,如果希望其中一些命令独立于其他命令提交,这并没有帮助。
扩展查询协议提供了另一种管理这个问题的方式,即在依赖的步骤之间省略发送同步消息。 由于在错误后,后端会跳过命令消息直到找到同步消息,这允许在管道中的后续命令在前面的命令失败时自动跳过,而无需客户端明确地使用BEGIN和COMMIT来管理。 管道中可以通过同步消息分隔独立可提交的段。
如果客户端没有发出显式 BEGIN,则会启动一个隐式事务块。每个 Sync 通常会在前序步骤成功时导致隐式 COMMIT,失败时导致隐式 ROLLBACK。服务器只有在第一个命令结束且尚未收到 Sync 时,才能识别这种隐式事务块。有一些 DDL 命令(例如 CREATE DATABASE)不能在事务块内执行;若这类命令在管道中执行,除非它是某个 Sync 之后的第一条命令,否则会失败。此外,它成功后会强制立即提交以保持数据库一致性。因此,紧随这些命令之后的 Sync 除了返回 ReadyForQuery 外不会产生额外效果。
当使用这种方法时,必须通过计算ReadyForQuery消息的数量并等待达到发送的Syncs数量来确定管道的完成。 计算命令完成响应是不可靠的,因为其中一些命令可能会被跳过,因此不会产生完成消息。
函数调用子协议允许客户端请求一个对存在于数据库pg_proc系统表中的任意函数的直接调用。客户端必须在该函数上有执行的权限。
函数调用子协议是一个遗留的特性,在新代码里可能最好避免用它。类似的结果可以通过设置一个执行SELECT function($1, ...)的预备语句得到。这样函数调用流程就可以用 Bind/Execute 代替。
一个函数调用流程是由前端向后端发送一条FunctionCall消息初始化的。然后后端根据函数调用的结果发送一条或者更多响应消息,并且最后是一条ReadyForQuery响应消息。ReadyForQuery通知前端它可以安全地发送一个新的查询或者函数调用了。
来自后端的可能的响应消息是:
COPY命令允许在服务器和客户端之间进行高速大批量数据传输。拷贝入和拷贝出操作每个都把连接切换到一个独立的子协议中,并且持续到操作结束。
拷贝入模式(向服务器传输数据)在后端执行COPY FROM STDIN SQL 语句时启动。后端会向前端发送一条 CopyInResponse 消息。随后前端应发送零条或多条 CopyData 消息,构成一条输入数据流。(消息边界与行边界之间没有任何对应关系要求,尽管让它们对齐通常是合理的选择。)前端可以通过发送 CopyDone 消息来结束拷贝入模式(表示成功结束),也可以发送 CopyFail 消息(这会使COPY语句以错误失败)。然后后端会恢复到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语句的时候初始化的。后端发出一个CopyOutResponse消息给前端,后面跟着零或者多个CopyData消息(总是每行一个),然后跟着CopyDone。然后后端回退到它在COPY开始之前的命令处理模式,然后发送CommandComplete。前端不能退出传输(除非是关闭连接或者发出一个Cancel请求),但是它可以抛弃不需要的CopyData和CopyDone消息。
在拷贝出模式中,如果后端检测到错误,那么它将发出一个ErrorResponse消息并且回到正常的处理。前端应该把收到ErrorResponse当作终止拷贝出模式的标志。
在CopyData消息中间可能会散布有NoticeResponse和ParameterStatus消息。前端必须处理这些情况,并且应该也为异步消息类型(参见Section 53.2.7)准备好。否则任何除CopyData或CopyDone之外的消息类型都会被认为是要中止拷贝出模式。
还有另一种与拷贝相关的模式,称为双向拷贝(copy-both),它允许数据以高速批量方式在客户端与服务器之间双向传输。当处于 walsender 模式的后端执行START_REPLICATION语句时,会启动双向拷贝模式。后端会向前端发送一条 CopyBothResponse 消息。此后,前端和后端都可以发送 CopyData 消息,直到任一方发送 CopyDone 消息。客户端发送 CopyDone 后,连接会从双向拷贝模式切换到拷贝出模式,客户端也不得再发送 CopyData。类似地,当服务器发送 CopyDone 后,连接会进入拷贝入模式,服务器也不得再发送 CopyData。当双方都发送完 CopyDone 后,拷贝模式结束,后端恢复到原先的命令处理模式。如果双向拷贝模式期间发生后端检测到的错误,后端会发出 ErrorResponse,丢弃前端消息直到收到 Sync,然后发出 ReadyForQuery 并返回正常处理。前端应将收到 ErrorResponse 视为双向拷贝终止的信号;在这种情况下不应再发送 CopyDone。关于在双向拷贝模式上传输的子协议,见Section 53.4。
CopyInResponse、CopyOutResponse和CopyBothResponse消息包括域和格式代码,域告诉前端每行的列数,而格式代码则用于具体每个列(就目前的实现而言,一个给定COPY操作中的所有列都将使用同样的格式,但是消息设计并不做这个假设)。
在若干情况下,后端会发送并非由前端命令流直接触发的消息。前端必须随时准备处理这些消息,即使当前并未处于查询过程中。至少,在开始读取查询响应之前应检查这些情况。
NoticeResponse 消息可能因外部活动而产生;例如,如果数据库管理员发起一次“快速”数据库关闭,后端会在关闭连接之前发送一条 NoticeResponse 说明这一事实。因此,前端应始终准备好接收并显示 NoticeResponse 消息,即使连接表面上处于空闲状态。
只要后端认为前端应当知晓的某个参数的当前有效值发生变化,就会生成 ParameterStatus 消息。最常见的情况是响应前端执行的SET命令,这种情况实际上是同步的;但也可能是管理员修改了配置文件,然后向服务器发送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消息包含在连接启动过程中传递给前端的相同的关键数据(PID和密钥),否则它将被忽略。如果该请求匹配当前运行着的后端的PID和密钥, 则退出当前查询的处理(目前的实现里采用的方法是向正在处理该查询的后端进程发送一个特殊的信号)。
取消信号可能有效,也可能无效;例如,如果它在后端已经处理完查询之后才到达,就不会起作用。如果取消生效,当前命令就会以一条错误消息提前终止。
这么做是对安全性和有效性通盘考虑的结果,前端没有直接的方法获知一个取消请求是否成功。它必须继续等待后端对查询响应。发出一个取消仅仅是增加了当前查询快些结束的可能性, 同时也增加了当前查询会伴随着一条错误消息失败而不是成功执行的可能性。
由于取消请求是通过一条新的连接发送给服务器,而不是通过常规的前端/后端通信链路发送,因此发出取消请求的可以是任意进程,而不一定非要是要取消查询的那个前端。这为构建多进程应用提供了额外的灵活性,同时也带来了安全风险,因为未授权用户可能会尝试取消查询。通过要求在取消请求中提供动态生成的密钥,可以缓解这一安全风险。
通常优雅的终止过程是前端发送一条Terminate消息并且立刻关闭连接。一旦收到消息,后端马上关闭连接并且终止。
在少数情况下(比如一个管理员命令数据库关闭),后端可能在没有任何前端请求的情况下断开连接。在这种情况下,后端将在它断开连接之前尝试发送一个错误或者通知消息给出断开的原因。
其他终止场景来自各种故障,例如任一端发生内核转储、通信链路中断、消息边界同步丢失等。如果前端或后端看到连接意外关闭,就应清理并终止。若前端不想自行终止,也可以重新联系服务器以启动一个新的后端。如果收到无法识别的消息类型,同样建议关闭连接,因为这通常意味着消息边界同步已经丢失。
不管是正常还是不正常的终止,任何打开的事务都会回滚而不是提交。不过,我们应该注意的是如果一个前端在一个非SELECT查询正在处理的时候断开, 那么后端很可能在发现断开之前先完成查询的处理。如果查询处于任何事务块之外(BEGIN ... COMMIT序列),那么其结果很可能在得知断开之前被提交。
如果编译PostgreSQL时启用了SSL支持,那么前端/后端通信就可以使用SSL加密。这为攻击者可能截获会话流量的环境提供了通信安全性。有关使用SSL加密PostgreSQL会话的更多信息,请参阅Section 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 库,要么在发现他们已经读取到额外的字节时将其视为协议违规。
同样,服务器也期望客户端在收到服务器对 SSLRequest 的单字节响应之后,再开始SSL协商。如果客户端不等服务器响应到达就立即开始SSL协商,可以减少一次网络往返时延;但代价是无法处理服务器对SSL请求返回否定响应的情况。在这种情况下,服务器通常会直接断开连接,而不会继续回退到 GSSAPI、未加密连接或协议错误流程。
初始 SSLRequest 也可以用于那些正在建立、目的是发送 CancelRequest 消息的连接。
还支持第二种发起SSL加密的方式:客户端可以在不发送任何 SSLRequest 包的情况下,直接开始SSL协商。SSL连接建立后,服务器会在加密通道上继续等待普通的启动请求包并继续协商。在这种情况下,任何其他的加密请求都会被拒绝。这种方法不适合通用工具,因为它既不能协商出最佳的连接加密方式,也不能处理未加密连接;但在客户端和服务器都由同一方控制的环境中,它很有用,因为它可以减少一次往返时延,并允许使用依赖标准SSL连接的网络工具。使用这种风格的SSL连接时,客户端必须使用由 RFC 7301 定义的 ALPN 扩展,以防范协议混淆攻击。PostgreSQL 协议对应的 ALPN 标识是 postgresql,见 IANA TLS ALPN Protocol IDs 注册表。
虽然协议本身并不提供让服务器强制启用SSL加密的方法,但管理员可以把服务器配置为拒绝未加密会话,把它作为认证检查的一个副作用。
如果PostgreSQL构建时启用了GSSAPI支持,就可以使用GSSAPI对前端/后端通信进行加密。这为攻击者可能截获会话流量的环境提供了通信安全性。有关使用GSSAPI加密PostgreSQL会话的详细信息,请参阅Section 18.10。
要启动一个GSSAPI加密连接,前端最初发送一个GSSENCRequest消息,而不是一个StartupMessage。 服务器随后会响应一个包含G或N的单个字节,分别表示愿意或不愿意执行GSSAPI加密。 如果前端对响应不满意,可能会在此时关闭连接。 要在G之后继续,使用GSSAPI C绑定,如在RFC 2744 中讨论的,或等效的,通过在循环中调用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加密的方法,但管理员可以把服务器配置为拒绝未加密会话,把它作为认证检查的一个副作用。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。