本节描述消息流以及每种消息类型的语意(每种信息的准确形式在Section 51.7里)。根据连接的状态不同,存在几种不同的子协议: 启动、查询、函数调用、COPY
和终止。还有特殊的规定用于一步操作(包括通知响应和命令取消),这些可能在启动阶段过后的任何时间产生。
要开始一个会话,前端打开一个与服务器的连接并且发送一个启动消息。这个消息包括用户名以及用户希望连接的数据库名; 它还标识要使用的特定的协议版本(启动信息可以有选择地包括运行时参数的额外设置)。服务器然后就使用这些信息及服务器配置文件的内容 (比如 lt_hba.conf
)来判断这个连接是否可以接受以及需要什么样的额外的认证(如果需要)。
然后服务器就发送合适的认证请求信息,前端必须用合适的认证响应信息来响应(比如一个口令)。对于除了SSPI和SASL之外的所有认证方式都至少有一个请求和一个响应。在有些方法中前端不需要发出任何响应,并且因此就不会由任何认证请求发生。对于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()
函数返回的结果是一个十六进制串)。
这个响应只用于在支持SCM信任消息的平台上的本地Unix域连接。前端必须发出一条SCM信任消息然后发送一个数据字节(数据字节的内容并没有意义; 它只被用于确保服务器等待足够长的时间来接受信任信息)。如果信任是可以接受的, 那么服务器用AuthenticationOk响应,否则用ErrorResponse响应(该消息只在9.1之前的服务器中发出。它可能最终会从协议规范中被删除)。
前端现在必须发起一次SASL协商,使用该消息中列出的一种SASL机制。前端将用选中机制的名字发送一个SASLInitialResponse,并且SASL数据流的第一部分对应于此。如果需要进一步的消息,服务器将用AuthenticationSASLContinue回复。详见Section 51.3。
这个消息包含从SASL协商(AuthenticationSASL或者一个之前的AuthenticationSASLContinue)的前一步中得到的挑战数据。前端必须用一个SASLResponse消息响应。
已经用额外的与机制相关的数据为该客户端完成SASL认证。服务器接下来将发送AuthenticationOk来表示认证成功,或者发送ErrorResponse表示失败。只有在SASL机制指定要在完成时从服务器发送额外数据到客户端的情况下才会发送这个消息。
服务器不支持客户端请求的次协议版本,但是支持该协议更早的一个版本,这个消息会指出受支持的最高次版本。如果客户端在启动包中请求了不受支持的协议选项(例如以_pq_.
开始),则这个消息也会被发出。这个消息后面将会跟着一个ErrorResponse或者一个指示认证成功或失败的消息。
如果前端不支持服务器要求的认证方式,那么它应该马上关闭连接。
在收到AuthenticationOk包之后,前端必须等待来自服务器的进一步消息。在这个阶段会启动一个后端进程, 而前端只是一个感兴趣的旁观者。启动尝试仍有可能失败(ErrorResponse),服务器也有可能拒绝支持所请求的次协议版本(NegotiateProtocolVersion),但是通常情况下,后端将发送一些ParameterStatus消息、BackendKeyData以及最后的ReadyForQuery。
在这个阶段,后端将尝试应用任何在启动消息里给出的额外的运行时参数设置。如果成功,这些值将成为会话的缺省值。错误将导致ErrorResponse并退出。
这个阶段来自后端的可能消息是:
这个消息提供了密钥数据,前端如果想要在稍后发出取消请求,则必须保存这个数据。前端不应该响应这个信息,但是应该继续侦听等待ReadyForQuery消息。
这个消息告诉前端有关后端参数的当前(初始)设置,比如client_encoding或者DateStyle等。前端可以忽略这些信息,或者记录其设置用于将来使用; 参阅Section 51.2.7获取更多细节。前端不应该响应这些信息, 而是应该继续侦听ReadyForQuery消息。
启动成功,前端现在可以发出命令。
启动失败,在发送完这个消息之后连接被关闭。
发出了一个警告信息。前端应该显示这个信息,但是要继续监听ReadyForQuery或ErrorResponse。
后端在每个命令周期后都会发出一个相同的ReadyForQuery消息。 出于前端的编码需要,前端可以合理地认为ReadyForQuery是一个命令周期的开始,或者认为ReadyForQuery 是启动阶段和每个随后命令周期的结束, 具体是那种情况取决于前端的编码需要。
一个简单查询周期是由前端发送一条Query消息给后端进行初始化的。这条消息包含一个用文本字符串表达的 SQL 命令(或者一些命令)。 后端根据查询命令串的内容发送一条或者更多条响应消息给前端,并且最后是一条ReadyForQuery响应消息。ReadyForQuery通知前端它可以安全地发送新命令了 (实际上前端不必在发送其它命令之前等待ReadyForQuery,但是这样一来,前端必须能发现较早发出的命令失败而稍后发出的命令成功的情况)。
来自后端的可能的消息是:
一个SQL命令正常结束。
后端已经准备好从前端拷贝数据到一个表里面去,参见Section 51.2.6。
后端已经准备好从一个表里拷贝数据到前端,参见Section 51.2.6。
表示为了响应一个SELECT
、FETCH
等查询, 将要返回行。这条消息的内容描述了行的列布局。这条消息后面将跟着DataRow消息,每个DataRow消息包含一个要被返回的行。
SELECT
、FETCH
等查询返回的结果集中的一行。
识别了一个空的查询字符串。
出错了。
查询字符串的处理完成。发送一个独立的消息来标识这种情况是因为查询字符串可能包含多个SQL命令(CommandComplete只是标记一条SQL命令处理完毕,而不是整个查询字符串处理完毕)。不管是处理成功结束还是产生错误,ReadyForQuery总会被发送。
发送了一个与查询有关的警告信息。提示信息是附加在其他响应上的,也就是说,后端将继续处理该命令。
对SELECT查询(或其它返回行集的查询,比如EXPLAIN
或SHOW
)的响应通常包含 RowDescription、零个或者多个 DataRow 消息以及最后的 CommandComplete。 COPY
到前端或者从前端COPY
会调用Section 51.2.6里描述的特殊协议。所有其他查询类型通常只产生一个CommandComplete消息。
因为查询字符串可能包含若干个查询(用分号分隔),所以在后端完成查询字符串的处理之前可能有好几个这样的响应序列。如果整个字符串已经处理完,后端已经准备好接受新查询字符串的时候则发出 ReadyForQuery消息。
如果收到一个完全空(除了空白之外没有内容)的查询字符串,那么响应是一条EmptyQueryResponse后面跟着ReadyForQuery。
在出现错误的时候,发出一个ErrorResponse消息,后面跟着ReadyForQuery。查询字符串的所有进一步的处理都被ErrorResponse中止(即使里面还有查询)。请注意这些事情可能在处理一个查询产生的消息序列的中途发生。
在简单查询模式中,检索出来的值的格式总是文本,除非给出的命令是在一个使用BINARY
选项声明的游标上FETCH
。 在这种情况下,检索出来的值是二进制格式的。在 RowDescription消息里给出的格式代码将告诉我们用了那种格式。
前端在等待其他类型的消息时必须准备接收ErrorResponse和NoticeResponse消息。 参阅 Section 51.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
没有被提交。在语义分析及其后阶段检测到的错误(例如拼错的表名或者列名)不会有这种效果。
扩展查询协议把上面描述的简单协议分裂成若干个步骤。准备步骤的结果可以被多次复用以提高效率。另外,还可以获得额外的特性, 比如可以把数据值作为独立的参数提供而不是必须把它们直接插入一个查询字符串。
在扩展协议里,前端首先发送一个Parse消息,它包含一个文本查询字符串, 另外还有一些可选的有关参数占位符的数据类型的信息,以及一个最终的预备语句对象的名字(一个空字符串选择未命名的预备语句)。响应是一个ParseComplete或者ErrorResponse。 参数的数据类型可以用OID来指定;如果没有给出,那么分析器将试图用应付无类型文字符串常量的方法来推导其数据类型。
一个参数的数据类型可以通过设置为零, 或者让参数类型OID的数目比查询字符串里的参数符号($
n
)的数目少的方式不予指定。另外一个特例是参数的类型可以声明为void
(也就是说,伪类型void
的OID)。这是为了允许用于某些函数参数的参数符号实际上是OUT参数。通常情况下,没有什么环境会用到void
参数, 但是如果在函数的参数列表里出现了这么一个参数符号,那么它实际上会被忽略。比如,一个像这样的函数调用:foo($1,$2,$3,$4)
,如果$3
和$4
被指定为具有类型是void
,那么这个函数调用会匹配一个带有两个IN和两个OUT参数的函数。
在一个Parse消息里包含的查询字符串不能包含超过一个SQL语句;否则就会报告一个语法错误。这个限制在简单查询协议中并不存在,它只存在于扩展协议中,因为允许预备语句或者入口包含多个命令将导致协议过度地复杂。
如果成功创建了一个命名的预备语句对象,那么它将持续到当前会话结束, 除非被明确地删除。一个未命名的预备语句只持续到下一个声明未命名语句的Parse语句发出为止(请注意一个简单的查询消息也会销毁未命名语句)。命名预备语句必须被明确地关闭,然后才能用一个Parse消息重新定义,但是未命名语句并不要求这个动作。命名预备语句也可以在SQL命令级别创建和访问,方法是使用PREPARE
和EXECUTE
。
一旦一个预备语句存在,就可以使用Bind消息使之进入执行状态。Bind消息给出源预备语句的名字(空字符串表示未命名预备语句)、目标入口的名字(空字符串表示未命名的入口)及用于那些在预备语句中出现的所有参数占位符的值。提供的参数集必须匹配那些预备语句所需要的参数(如果你在Parse消息里声明任何void
参数,那么在Bind消息里给它们传递NULL值)。Bind还指定被查询返回的数据的格式;格式可以在总体上声明,也可以对每个列进行声明。响应是BindComplete或ErrorResponse。
输出的格式是文本还是二进制是由Bind里给出的格式代码决定的,而不管涉及的是什么SQL命令。在使用扩展查询协议的时候,游标声明里的BINARY
属性是无关的。
当Bind消息被处理时通常会进行查询规划。如果预备语句没有参数或者是被反复执行,服务器可能会保存创建好的计划并在后续对同一个预备语句的Bind消息中重用之。不过,当它发现可以创建一个效率比依赖指定参数值的计划不低很多的一般性计划时,它仍然会进行查询规划。但是这对于协议所关注的来说是透明的。
如果成功创建了一个命名入口对象,那么它将持续到当前事务的结尾,除非被明确地删除。一个未命名入口在事务的结尾删除,或者是在发出的下一个Bind语句声明了一个未命名入口为止(请注意一个简单查询消息也会删除这个未命名入口)。命名入口在可以用一个Bind消息重新定义之前必须明确地关闭,但是未命名入口不要求这个动作。命名入口也可以在SQL命令的级别创建和访问,方法是使用DECLARE CURSOR
和FETCH
。
一旦一个入口存在,那么就可以用一个Execute消息执行它。Execute消息指定入口的名字(空字符串表示未命名入口)和一个最大的结果行计数(零表示“取出所有行”)。 结果行计数只对包含返回行集的入口有意义;在其它情况下,该命令总是被执行到结束,而行计数会被忽略。Execute消息的可能响应和那些通过简单查询协议发出的查询一样,只不过执行不会导致后端发出ReadyForQuery或者RowDescription。
如果Execute在入口的执行完成之前终止(因为达到了一个非零的结果行计数),它将发送一个PortalSuspended消息;这个消息的出现告诉前端应该在同一个入口上发出另外一个Execute消息以完成操作。在入口的执行完成之前,不会发出表示源SQL命令结束的CommandComplete消息。因此执行阶段总是由下列消息之一出现标志着结束: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消息不产生任何特定的输出,但会强制后端发送任何还在它的输出缓冲区中待处理的数据。Flush必须在除Sync外的任何扩展查询命令后面发出——如果前端希望在发出更多的命令之前检查该命令的结果的话。如果没有Flush,后端返回的消息将组合成尽可能少的数据包,以减少网络负荷。
简单查询消息大概等于一系列使用未命名预备语句和无参数入口对象的Parse、Bind、入口Describe、Execute、Close、Sync。一个区别是它会在查询字符串中接受多个SQL语句,并连续地为每个语句自动执行绑定/描述/执行序列。另外一个区别是它不会返回ParseComplete、Bindcomplete、CloseComplete或者NoData消息。
使用扩展查询协议允许 流水线传输,这意味着可以发送一系列查询,而无需等待之前的查询完成。这减少了完成一系列操作所需的网络往返次数。但是,如果其中一个步骤失败,用户必须仔细考虑所需的行为,因为后续查询已经在向服务器发送。
应对此问题的一种方法是将整个查询系列作为单个事务处理,即将其包装在 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
语句的时候初始化的。后端发送一个CopyInResponse消息给前端。前端应该发送零条或者更多CopyData消息,形成一个输出数据的流(消息的边界和行的边界没有任何相关性要求,尽管通常那是合理的选择)。前端可以通过发送一个CopyDone消息来终止拷贝入操作(允许成功终止),也可以发出一个CopyFail消息(它将导致COPY
语句带着错误失败)。 然后后端就恢复回它在COPY
开始之前的命令处理模式,可能是简单查询协议,也可能是扩展查询协议。然后它会发送CommandComplete(如果成功)或者ErrorResponse(如果失败)。
如果在拷贝入模式下后端检测到了错误(包括接受接收到CopyFiail消息, 或者是任何除了CopyData或者CopyDone之外的前端消息),那么后端将发出一个ErrorResponse消息。如果COPY
命令是通过一个扩展查询消息发出的, 那么后端从现在开始将抛弃前端消息,直到一个Sync消息到达,然后它将发出ReadyForQuery并且返回到正常的处理中。如果COPY
命令是在一个简单查询消息里发出的,那么该消息剩余部分被丢弃然后发出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 51.2.7)准备好。否则任何除CopyData或CopyDone之外的消息类型都会被认为是要中止拷贝出模式。
还有另外一种被称为双向拷贝的与拷贝相关的模式,它允许“向”和“从”服务器高速传输大批量数据。当后端处于walsender模式中执行一个START_REPLICATION
语句时,它会启动双向拷贝模式。后端会发送一个CopyBothResponse消息给前端。然后前端和后端都会发送CopyData消息,然后直到最后发送一个CopyDone消息。在客户端发送一个CopyDone消息后,连接将从双向拷贝模式转换到拷贝出模式,并且客户端将不能发送更多CopyData消息。类似的,当服务器发送了一个CopyDone消息,连接进入到拷贝入模式,并且服务器将不能发送更多CopyData消息。在双方发送完一个CopyDone消息后,拷贝模式被中断,而后端将回到之前的命令处理模式。如果在双向拷贝模式中出现一个后端检测到的错误,后端将发出一个ErrorResponse消息,然后将发出ReadyForQuery并返回到普通处理。前端将把收到ErrorResponse作为在双向上中断拷贝的信号,在这种情况下不会有CopyDone被发出。关于在双向拷贝模式下传输的子协议请参见Section 51.4。
CopyInResponse、CopyOutResponse和CopyBothResponse消息包括域和格式代码,域告诉前端每行的列数,而格式代码则用于具体每个列(就目前的实现而言,一个给定COPY
操作中的所有列都将使用同样的格式,但是消息设计并不做这个假设)。
有几种情况下后端会发送一些并非由特定前端命令流传达的消息。在任何时候前端都必须准备处理这些消息,即使它是并未参与一个查询。在最低限度下,我们应该在开始读取查询响应之前检查这些情况。
NoticeResponse消息有可能是因为外部的活动而产生的;比如,如果数据库管理员进行一次“快速”数据库关闭,那么后端将在关闭连接之前发送一个NoticeResponse来表明这些。相应地,前端应该总是准备接受和显示NoticeResponse消息,即使连接事实上是空闲的。
如果任何时候有任何参数值的活跃值改变且后端认为前端应该知道这些,那么都会产生ParameterStatus消息。这种情况最常见发生的情形是对前端执行的一个SET
命令进行响应,并且这种情况实际上是同步的 — 但是也有可能是数据库管理员改变了配置文件然后项服务器发出SIGHUP信号导致了参数状态的变化。同样,如果一个SET
命令回滚,那么也会生成一个合适的ParameterStatus 消息以报告当前有效值。
目前,系统内有一套会生成ParameterStatus消息的写成硬代码的参数,它们是:
(server_encoding,TimeZone 和 integer_datetimes 在 8.0 版本之前没有报告。standard_conforming_strings 在版本 8.1 之前没有报告。) 请注意 server_version, server_encoding 和 integer_datetimes 是伪参数,启动后不能修改。 这些可能在将来改变,或者甚至是变成可以配置的。 因此,前端应该简单地忽略那些它不懂或者不关心的 ParameterStatus。
server_version
、
server_encoding
、
client_encoding
、
application_name
、
is_superuser
、
session_authorization
、
DateStyle
、
IntervalStyle
、
TimeZone
、
integer_datetimes
以及
standard_conforming_strings
(server_encoding
、TimeZone
以及integer_datetimes
在版本8.0之前不会被报告;
standard_conforming_strings
在版本8.1之前不会被报告;
IntervalStyle
在版本8.4之前不会被报告;
application_name
在版本9.0之前不会被报告)。
注意server_version
、server_encoding
以及integer_datetimes
是伪参数,它们不能在启动之后被改变。这种设置可能在未来改变,甚至变成可配置的。相应地,一个前端应该简单地忽略那些与它不懂或者不关心的参数相关的ParameterStatus。
如果前端发出一个LISTEN
命令, 那么无论何时在为同一个通道名NOTIFY
时,后端将发送一个NotificationResponse消息(不要和NoticeResponse搞混!)。
目前,NotificationResponse只能在一个事务外面发送,因此它将不会在一个命令响应序列中间出现,但是它可能正好在ReadyForQuery之前出现。不过,在前端逻辑中做上述假设是不明智的。好的做法是在协议的任何点上都可以接受NotificationResponse。
在一条查询正在处理的时候,前端可以请求取消该查询。这种取消请求不是直接通过打开的连接发送给后端的,这么做是因为实现的效率:我们不希望后端在处理查询的过程中不停地检查前端来的输入。 取消请求应该相对而言比较少见,所以我们把取消做得稍微笨拙一些,以便不影响正常状况的性能。
要发出一条取消请求,前端打开一个与服务器的新连接并且发送一条CancelRequest消息, 而不是通常在新连接中经常发送的StartupMessage消息。服务器将处理这个请求然后关闭连接。 出于安全原因,对取消请求消息不做直接的响应。
除非CancelRequest消息包含在连接启动过程中传递给前端的相同的关键数据(PID和密钥),否则它将被忽略。如果该请求匹配当前运行着的后端的PID和密钥, 则退出当前查询的处理(目前的实现里采用的方法是向正在处理该查询的后端进程发送一个特殊的信号)。
取消信号可能产生或者不产生效果 — 例如,如果它在后端完成查询的处理后到达,那么它就没有做用。如果取消起作用了,会导致当前命令伴随着一个错误消息提前退出。
这么做是对安全性和有效性通盘考虑的结果,前端没有直接的方法获知一个取消请求是否成功。它必须继续等待后端对查询响应。发出一个取消仅仅是增加了当前查询快些结束的可能性, 同时也增加了当前查询会伴随着一条错误消息失败而不是成功执行的可能性。
因为取消请求是通过新的联接发送给服务器而不是通过平常的前端/后端通讯链接,所以取消请求可能被任意进程发出的,而不仅仅是要取消查询的前端。 这样可能对创建多进程应用提供了更多的灵活性。同时这样也带来了安全风险,因为任何一个非授权用户都可能试图取消查询。这个安全风险通过要求在取消请求中提供一个动态生成的密钥来解决。
通常优雅的终止过程是前端发送一条Terminate消息并且立刻关闭连接。一旦收到消息,后端马上关闭连接并且终止。
在少数情况下(比如一个管理员命令数据库关闭),后端可能在没有任何前端请求的情况下断开连接。在这种情况下,后端将在它断开连接之前尝试发送一个错误或者通知消息给出断开的原因。
其它终止的情况发生在各种失败的场合,比如某一方的内核转储、失去通讯链路、丢失了消息边界同步等。不管是前端还是后端看到了一个意外的连接关闭,那么它应该清理现场并且终止。 如果前端不想终止自己,那么它有一个选项是重连服务器的方法启动一个新的后端。如果收到了一个无法识别的消息类型,那么我们也建议关闭连接,因为出现这种情况可能意味着是丢失了消息边界的同步。
不管是正常还是不正常的终止,任何打开的事务都会回滚而不是提交。不过,我们应该注意的是如果一个前端在一个非SELECT
查询正在处理的时候断开, 那么后端很可能在发现断开之前先完成查询的处理。如果查询处于任何事务块之外(BEGIN
... COMMIT
序列),那么其结果很可能在得知断开之前被提交。
如果编译LightDB的时候打开了SSL支持,那么前后端通讯就可以用SSL加密。 这样就提供了一种在攻击者可能捕获会话通讯数据包的环境下保证通讯安全的方法。有关使用SSL加密LightDB会话的更多信息, 请参阅Section 17.9。
要开始一次SSL加密连接,前端先是发送一个SSLRequest消息,而不是StartupMessage。然后服务器以一个包含S
或N
的字节响应,分别表示它愿意还是不愿意进行SSL。如果此时前端对响应不满意, 那么它可以关闭连接。要在S
之后继续,那么先进行与服务器的SSL启动握手(没有在这里描述,是SSL规范的一部分)。 如果这些成功了,那么继续发送普通的StartupMessage。这种情况下,StartupMessage和所有随后的数据都将由SSL加密。要在N
之后继续,则发送普通的StartupMessage并不适用加密继续处理。
前端应该也准备处理一个来自服务器的给SSLRequest的ErrorMessage响应。这种情况只在服务器早于LightDB的SSL支持的情况下才会出现(这种服务器现在非常古老,并且可能不再存在了)。在这种情况下,连接必需关闭,但是前端可以选择打开一个新的连接然后不使用SSL进行连接。
当可以执行 SSL 加密时,服务器预计只发送单个 S
字节,然后等待前端启动 SSL 握手。如果此时有其他字节可读,则很可能意味着中间人正在尝试执行缓冲区填充攻击(CVE-2021-23222)。前端应该编写代码,要么在将套接字交给 SSL 库之前从套接字中精确读取一个字节,要么在发现已读取额外字节时将其视为协议违规行为。
一个初始化的 SSLRequest 也可以用于打开来用于发送一条 CancelRequest 消息的联接中。
如果协议本身并未提供某种方法强制SSL加密,那么管理员可以把服务器配置为拒绝未加密的会话,这是认证检查的一个副产品。