9.3 9.4 9.5 9.6 10 11 12 13 14 15 16 17 Current(18)
PostgreSQL中文社区 问题报告 纠错本页面

54.2. 消息流 #

54.2.1. 启动
54.2.2. 简单查询
54.2.3. 扩展查询
54.2.4. 流水线处理
54.2.5. 函数调用
54.2.6. COPY操作
54.2.7. 异步操作
54.2.8. 取消正在处理的请求
54.2.9. 终止
54.2.10. SSL 会话加密
54.2.11. GSSAPI 会话加密

本节描述消息流以及每种消息类型的语义(每种信息的准确形式在第 54.7 节里)。根据连接的状态不同,存在几种不同的子协议: 启动、查询、函数调用、COPY和终止。还有特殊的规定用于异步操作(包括通知响应和命令取消),这些可能在启动阶段过后的任何时间产生。

54.2.1. 启动 #

要开始一个会话,前端打开一个与服务器的连接并发送一个启动消息。这个消息包括用户名以及用户希望连接的数据库名;它还标识要使用的特定协议版本(启动消息可以有选择地包括运行时参数的额外设置)。服务器然后使用这些信息及服务器配置文件的内容(比如 pg_hba.conf)来判断这个连接是否可以接受,以及需要什么样的额外认证(如果需要)。

然后服务器发送合适的认证请求消息,前端必须用合适的认证响应消息来响应(比如一个密码)。对于除了 GSSAPI、SSPI 和 SASL 之外的所有认证方法,最多有一个请求和一个响应。在某些方法中,前端不需要发出任何响应,因此不会发生任何认证请求。对于 GSSAPI、SSPI 和 SASL,可能需要多个数据包的交换才能完成认证。

认证周期要么以服务器拒绝连接(ErrorResponse)结束,要么以 AuthenticationOk 结束。

在此阶段,服务器可能发送的消息有:

ErrorResponse

连接尝试已被拒绝。 服务器随后立即关闭连接。

AuthenticationOk

身份验证交换已成功完成。

AuthenticationKerberosV5

前端现在必须与服务器进行Kerberos V5 身份验证对话(此处未描述,属于Kerberos规范的一部分)。 如果成功,服务器将响应一个AuthenticationOk, 否则将响应一个ErrorResponse。这不再受支持。

AuthenticationCleartextPassword

前端现在必须发送一个包含明文形式密码的PasswordMessage。如果这是正确的密码, 服务器将以AuthenticationOk响应,否则将以ErrorResponse响应。

AuthenticationMD5Password

前端现在必须发送一个包含密码的PasswordMessage(带有用户名), 该密码首先通过MD5加密,然后再次使用AuthenticationMD5Password消息中 指定的4字节随机盐加密。如果这是正确的密码,服务器将响应一个 AuthenticationOk,否则将响应一个ErrorResponse。实际的PasswordMessage 可以在SQL中计算为concat('md5', md5(concat(md5(concat(password, username)), random-salt)))。(请记住,md5() 函数返回其结果为一个十六进制字符串。)

警告

对MD5加密密码的支持已被弃用,并将在未来的PostgreSQL版本中移除。有关迁移到其他密码类型的详细信息,请参见第 20.5 节

AuthenticationGSS

前端现在必须启动一个GSSAPI协商。前端将发送一个GSSResponse消息, 其中包含GSSAPI数据流的第一部分作为响应。如果需要进一步的消息, 服务器将以AuthenticationGSSContinue进行响应。

AuthenticationSSPI

前端现在必须启动一个SSPI协商。前端将发送一个包含SSPI数据流第一部分的 GSSResponse作为响应。如果需要进一步的消息,服务器将以 AuthenticationGSSContinue进行响应。

认证GSS继续

此消息包含来自上一步GSSAPI或SSPI协商(AuthenticationGSS、 AuthenticationSSPI或之前的AuthenticationGSSContinue)的响应数据。 如果此消息中的GSSAPI或SSPI数据表明需要更多数据来完成身份验证, 前端必须将这些数据作为另一个GSSResponse消息发送。如果此消息完成了 GSSAPI或SSPI身份验证,服务器将接下来发送AuthenticationOk以指示 身份验证成功,或发送ErrorResponse以指示失败。

认证SASL

前端现在必须启动SASL协商,使用消息中列出的SASL机制之一。前端将发送 一个SASLInitialResponse,其中包含所选机制的名称,以及响应此消息的 SASL数据流的第一部分。如果需要进一步的消息,服务器将以 AuthenticationSASLContinue进行响应。详情请参见 第 54.3 节

认证SASL继续

此消息包含来自SASL协商(AuthenticationSASL或之前的 AuthenticationSASLContinue)上一步的挑战数据。前端必须以 SASLResponse消息进行响应。

认证SASL最终

SASL认证已完成,并附带了特定机制的额外数据供客户端使用。服务器接下来将发送 AuthenticationOk以指示认证成功,或者发送ErrorResponse以指示失败。仅当 SASL机制指定在完成时需要从服务器向客户端发送额外数据时,才会发送此消息。

NegotiateProtocolVersion

服务器不支持客户端请求的次要协议版本,但支持早期版本的协议; 此消息指示最高支持的次要版本。如果客户端在启动数据包中请求了不支持的协议 选项(即以 _pq_. 开头),也会发送此消息。

在此消息之后,身份验证将继续使用服务器指示的版本。 如果客户端不支持旧版本,它应立即关闭连接。如果服务器 不发送此消息,则表示它支持客户端请求的协议版本及所有协议选项。

如果前端不支持服务器要求的认证方式,那么它应该立即关闭连接。

在收到AuthenticationOk包之后,前端必须等待来自服务器的进一步消息。在这个阶段会启动一个后端进程,而前端只是一个感兴趣的旁观者。启动尝试仍有可能失败(ErrorResponse),服务器也有可能拒绝支持所请求的次协议版本(NegotiateProtocolVersion),但是通常情况下,后端将发送一些ParameterStatus消息、BackendKeyData以及最后的ReadyForQuery。

在这个阶段,后端将尝试应用任何在启动消息里给出的额外的运行时参数设置。如果成功,这些值将成为会话的默认值。错误将导致ErrorResponse并退出。

后端在此阶段可能发送的消息包括:

BackendKeyData

此消息提供了前端必须保存的密钥数据,以便能够在稍后发出取消请求。 前端不应对该消息作出响应,而应继续监听 ReadyForQuery 消息。

PostgreSQL 服务器将始终发送此消息,但一些不支持查询 取消的第三方后端协议实现已知不会发送此消息。

ParameterStatus

此消息通知前端当前(初始)后端参数的设置,例如 client_encodingDateStyle。 前端可以忽略此消息,或记录设置以供将来使用;有关更多详细信息,请参见 第 54.2.7 节。前端不应对该消息作出响应,而应继续监听 ReadyForQuery 消息。

ReadyForQuery

启动已完成。前端现在可以发出命令。

ErrorResponse

启动失败。发送此消息后连接将关闭。

NoticeResponse

已发出警告消息。前端应显示该消息,但继续监听 ReadyForQuery 或 ErrorResponse。

后端在每个命令周期后都会发出一个相同的ReadyForQuery消息。出于前端的编码需要,前端可以合理地认为ReadyForQuery是一个命令周期的开始,或者认为ReadyForQuery是启动阶段和每个随后命令周期的结束,具体是那种情况取决于前端的编码需要。

54.2.2. 简单查询 #

一个简单查询周期是由前端发送一条Query消息给后端进行初始化的。这条消息包含一个用文本字符串表达的 SQL 命令(或者一些命令)。 后端根据查询命令串的内容发送一条或者更多条响应消息给前端,并且最后是一条ReadyForQuery响应消息。ReadyForQuery通知前端它可以安全地发送新命令了 (实际上前端不必在发送其他命令之前等待ReadyForQuery,但是这样一来,前端必须能发现较早发出的命令失败而稍后发出的命令成功的情况)。

后端可能返回的响应消息有:

CommandComplete

一个SQL命令正常完成。

CopyInResponse

后端准备好从前端复制数据到表;参见第 54.2.6 节

CopyOutResponse

后端准备好从表复制数据到前端;参见第 54.2.6 节

RowDescription

表示即将返回行作为对SELECTFETCH等查询的响应。 此消息的内容描述了行的列布局。这将跟随每个返回给前端的行的DataRow消息。

DataRow

SELECTFETCH等查询返回的一组行中的一个。

EmptyQueryResponse

识别出一个空查询字符串。

ErrorResponse

发生了一个错误。

ReadyForQuery

查询字符串的处理已完成。发送一个单独的消息来指示这一点,因为查询字符串可能包含多个SQL命令。 (CommandComplete标记了一个SQL命令的处理结束,而不是整个字符串的结束。) 无论处理是成功还是出现错误,都将始终发送ReadyForQuery。

NoticeResponse

与查询相关的警告消息已发出。 通知是其他响应的补充,即后端将继续处理命令。

SELECT查询(或其他返回行集的查询,如EXPLAINSHOW)的响应通常包含RowDescription、零个或多个DataRow消息,以及最后的CommandComplete。 COPY到前端或从前端COPY会调用第 54.2.6 节中描述的特殊协议。 所有其他查询类型通常只产生一个CommandComplete消息。

因为查询字符串可能包含若干个查询(用分号分隔),所以在后端完成查询字符串的处理之前可能有好几个这样的响应序列。如果整个字符串已经处理完,后端已经准备好接受新查询字符串的时候则发出ReadyForQuery消息。

如果收到一个完全空(除了空白之外没有内容)的查询字符串,那么响应是一条EmptyQueryResponse后面跟着ReadyForQuery。

在出现错误的时候,发出一个ErrorResponse消息,后面跟着ReadyForQuery。查询字符串的所有进一步的处理都被ErrorResponse中止(即使里面还有查询)。请注意这些事情可能在处理一个查询产生的消息序列的中途发生。

在简单查询模式中,检索出来的值的格式总是文本,除非给出的命令是在一个使用BINARY选项声明的游标上FETCH。在这种情况下,检索出来的值是二进制格式的。在RowDescription消息里给出的格式代码将告诉我们用了哪种格式。

前端在等待其他类型的消息时必须准备接收ErrorResponse和NoticeResponse消息。 参阅 第 54.2.7 节来了解后端因为外部事件可能生成的消息。

我们建议的方法是把前端代码写成状态机的风格,它可以在任何时刻接受任何有意义的消息类型,而不是假设消息的序列总是准确。

54.2.2.1. 一个简单查询中的多条语句 #

当一个简单查询消息中包含多于一条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);
SELECT 1/0;

那么这些语句都不会被运行,导致可见的差别,即第一个INSERT没有被提交。在语义分析及其后阶段检测到的错误(例如拼错的表名或者列名)不会有这种效果。

最后,请注意,Query消息中的所有语句将观察到相同的statement_timestamp()值,因为该时间戳仅在接收到Query消息时更新。这将导致它们都观察到相同的transaction_timestamp()值,除非查询字符串结束了先前启动的事务并开始了一个新事务。

54.2.3. 扩展查询 #

扩展查询协议把上面描述的简单查询协议分解成多个步骤。准备步骤的结果可以被多次复用以提高效率。另外,还可以获得额外的特性,比如可以把数据值作为独立的参数提供,而不是必须把它们直接插入一个查询字符串。

在扩展协议中,前端首先发送一个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命令级别创建和访问,方法是使用PREPAREEXECUTE

一旦一个预备语句存在,就可以使用Bind消息使之进入执行状态。Bind消息给出源预备语句的名字(空字符串表示未命名预备语句)、目标游标的名字(空字符串表示未命名的游标)及用于那些在预备语句中出现的所有参数占位符的值。提供的参数集必须匹配那些预备语句所需要的参数(如果你在Parse消息里声明任何void参数,那么在Bind消息里给它们传递NULL值)。Bind还指定被查询返回的数据的格式;格式可以在总体上声明,也可以对每个列进行声明。响应是BindComplete或ErrorResponse。

注意

输出的格式是文本还是二进制是由Bind里给出的格式代码决定的,而不管涉及的是什么SQL命令。在使用扩展查询协议的时候,游标声明里的BINARY属性是无关的。

当Bind消息被处理时通常会进行查询规划。如果预备语句没有参数或者是被反复执行,服务器可能会保存创建好的计划并在后续对同一个预备语句的Bind消息中重用之。不过,当它发现可以创建一个效率比依赖指定参数值的计划不低很多的一般性计划时,它仍然会进行查询规划。但是这对于协议所关注的来说是透明的。

如果成功创建了一个命名游标对象,那么它将持续到当前事务的结尾,除非被明确地删除。一个未命名游标在事务的结尾删除,或者是在发出的下一个Bind语句声明了一个未命名游标为止(请注意一个简单查询消息也会删除这个未命名游标)。命名游标在可以用一个Bind消息重新定义之前必须明确地关闭,但是未命名游标不要求这个动作。命名游标也可以在SQL命令的级别创建和访问,方法是使用DECLARE CURSORFETCH

一旦一个游标存在,那么就可以用一个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消息。

54.2.4. 流水线处理 #

使用扩展查询协议允许流水线处理,这意味着发送一系列查询而无需等待先前的查询完成。 这减少了完成一系列操作所需的网络往返次数。然而,用户必须仔细考虑所需的行为,如果其中一步失败, 因为后续查询已经在传输到服务器的过程中。

处理这个问题的一种方法是将整个查询系列作为一个事务处理,即将其包装在BEGIN... COMMIT中。然而,如果希望其中一些命令独立于其他命令提交,这并没有帮助。

扩展查询协议提供了另一种管理这个问题的方式,即在依赖的步骤之间省略发送同步消息。 由于在错误后,后端会跳过命令消息直到找到同步消息,这允许在管道中的后续命令在前面的命令失败时自动跳过,而无需客户端明确地使用BEGINCOMMIT来管理。 管道中可以通过同步消息分隔独立可提交的段。

如果客户端没有发出显式的 BEGIN, 则会启动一个隐式事务块,每个 Sync 通常会导致一个隐式 COMMIT(如果前面的步骤成功)或一个隐式 ROLLBACK(如果失败)。 服务器仅在第一个命令在没有同步的情况下结束时才能检测到此隐式事务块。 有一些 DDL 命令(例如 CREATE DATABASE)不能在 事务块内执行。如果在管道中执行其中一个命令,除非它是 Sync 后的第一个命令, 否则将失败。此外,成功后将强制立即提交以保持数据库一致性。 因此,紧接在这些命令之后的 Sync 除了响应 ReadyForQuery 外没有其他效果。

当使用这种方法时,必须通过计算ReadyForQuery消息的数量并等待达到发送的Sync的数量来确定管道的完成。 计算命令完成响应是不可靠的,因为其中一些命令可能会被跳过,因此不会产生完成消息。

54.2.5. 函数调用 #

函数调用子协议允许客户端请求一个对存在于数据库pg_proc系统目录中的任意函数的直接调用。客户端必须在该函数上有执行权限。

注意

函数调用子协议是一个遗留特性,在新代码中可能最好避免使用。类似的结果可以通过设置一个执行SELECT function($1, ...)的预备语句来实现。这样函数调用周期就可以用 Bind/Execute 代替。

一个函数调用周期是由前端向后端发送一条FunctionCall消息初始化的。然后后端根据函数调用的结果发送一条或更多响应消息,并且最后是一条ReadyForQuery响应消息。ReadyForQuery通知前端它可以安全地发送一个新的查询或函数调用。

来自后端的可能响应消息是:

ErrorResponse

发生了一个错误。

FunctionCallResponse

函数调用完成并且在消息中返回一个结果(请注意函数调用协议只能处理单个标量结果,不能处理行类型或结果集)。

ReadyForQuery

函数调用处理完成。ReadyForQuery将始终被发送,无论处理是成功完成还是发生错误。

NoticeResponse

发出了一条有关该函数调用的警告信息。通知是附加在其他响应上的,也就是说,后端将继续处理该命令。

54.2.6. COPY操作 #

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。前端不能退出传输(除非是关闭连接或者发出一个Cancel请求),但是它可以抛弃不需要的CopyData和CopyDone消息。

在拷贝出模式中,如果后端检测到错误,那么它将发出一个ErrorResponse消息并回到正常的处理。前端应该把收到ErrorResponse当作终止拷贝出模式的标志。

在CopyData消息中间可能会散布有NoticeResponse和ParameterStatus消息。前端必须处理这些情况,并且应该也为其他异步消息类型(参见第 54.2.7 节)准备好。否则任何除CopyData或CopyDone之外的消息类型都会被认为是要中止拷贝出模式。

还有另外一种被称为双向拷贝的与拷贝相关的模式,它允许“向”“从”服务器高速传输大批量数据。当后端处于walsender模式中执行一个START_REPLICATION语句时,它会启动双向拷贝模式。后端会发送一个CopyBothResponse消息给前端。然后前端和后端都会发送CopyData消息,直到最后发送一个CopyDone消息。在客户端发送一个CopyDone消息后,连接将从双向拷贝模式转换到拷贝出模式,并且客户端将不能发送更多CopyData消息。类似的,当服务器发送了一个CopyDone消息,连接进入到拷贝入模式,并且服务器将不能发送更多CopyData消息。在双方发送完一个CopyDone消息后,拷贝模式被终止,而后端将回到之前的命令处理模式。如果在双向拷贝模式中出现一个后端检测到的错误,后端将发出一个ErrorResponse消息,抛弃前端消息直到接收到Sync消息,然后发出ReadyForQuery并返回到正常处理。前端将把收到ErrorResponse作为在双向上中断拷贝的信号,在这种情况下不会有CopyDone被发出。关于在双向拷贝模式下传输的子协议请参见第 54.4 节

CopyInResponse、CopyOutResponse和CopyBothResponse消息包括字段,告知前端每行的列数和每列使用的格式代码。(就目前的实现而言,一个给定COPY操作中的所有列都将使用同样的格式,但是消息设计并不做这个假设。)

54.2.7. 异步操作 #

有几种情况下后端会发送一些并非由特定前端命令流传达的消息。在任何时候前端都必须准备处理这些消息,即使它并未参与一个查询。在最低限度下,我们应该在开始读取查询响应之前检查这些情况。

NoticeResponse消息有可能是因为外部的活动而产生的;比如,如果数据库管理员进行一次快速数据库关闭,那么后端将在关闭连接之前发送一个NoticeResponse来表明这一事实。相应地,前端应该总是准备接受和显示NoticeResponse消息,即使连接事实上是空闲的。

如果任何时候有任何参数值的活跃值改变且后端认为前端应该知道这些,那么都会产生ParameterStatus消息。这种情况最常见发生的情形是对前端执行的一个SET命令进行响应,并且这种情况实际上是同步的 — 但是也有可能是数据库管理员改变了配置文件然后向服务器发出SIGHUP信号导致了参数状态的变化。同样,如果一个SET命令回滚,那么也会生成一个合适的ParameterStatus消息以报告当前有效值。

目前有一组硬编码的参数将生成ParameterStatus。 它们是:

application_namescram_iterations
client_encodingsearch_path
DateStyleserver_encoding
default_transaction_read_onlyserver_version
in_hot_standbysession_authorization
integer_datetimesstandard_conforming_strings
IntervalStyleTimeZone
is_superuser 

(default_transaction_read_onlyin_hot_standby 在14之前的版本中未报告; scram_iterations 在16之前的版本中未报告; search_path 在18之前的版本中未报告。) 请注意 server_versionserver_encodinginteger_datetimes 是在启动后无法更改的伪参数。 这组参数在未来可能会更改,甚至变得可配置。 因此,前端应简单地忽略它不理解或不关心的ParameterStatus。

如果前端发出一个LISTEN命令,那么无论何时在为同一个通道名执行NOTIFY命令时,后端将发送一个NotificationResponse消息(不要和NoticeResponse搞混!)。

注意

目前,NotificationResponse只能在一个事务外面发送,因此它将不会在一个命令响应序列中间出现,但是它可能正好在ReadyForQuery之前出现。不过,在前端逻辑中做上述假设是不明智的。好的做法是在协议的任何点上都可以接受NotificationResponse。

54.2.8. 取消正在处理的请求 #

在一条查询正在处理的时候,前端可以请求取消该查询。这种取消请求不是直接通过打开的连接发送给后端的,这么做是因为实现的效率:我们不希望后端在处理查询的过程中不停地检查前端来的输入。取消请求应该相对而言比较少见,所以我们把取消做得稍微笨拙一些,以便不影响正常情况下的性能。

要发出一条取消请求,前端打开一个与服务器的新连接并发送一条CancelRequest消息,而不是通常在新连接中发送的StartupMessage消息。服务器将处理这个请求然后关闭连接。出于安全原因,对取消请求消息不做直接的响应。

除非CancelRequest消息包含在连接启动过程中传递给前端的相同关键数据(PID和密钥),否则它将被忽略。如果该请求匹配当前运行着的后端的PID和密钥,则退出当前查询的处理(目前的实现里采用的方法是向正在处理该查询的后端进程发送一个特殊的信号)。

取消信号可能产生或者不产生效果 — 例如,如果它在后端完成查询的处理后到达,那么它就没有效果。如果取消起作用,会导致当前命令伴随着一个错误消息提前终止。

这么做是出于安全性和有效性的考虑,前端没有直接的方法获知一个取消请求是否成功。它必须继续等待后端对查询的响应。发出一个取消仅仅是增加了当前查询快些结束的可能性,同时也增加了当前查询会伴随着一条错误消息失败而不是成功执行的可能性。

因为取消请求是通过新的连接发送给服务器而不是通过平常的前端/后端通信链接,所以取消请求可能被任意进程发出,而不仅仅是要取消查询的前端。这可能为创建多进程应用提供更多的灵活性。同时这也带来了安全风险,因为任何非授权用户都可能试图取消查询。这个安全风险通过要求在取消请求中提供一个动态生成的密钥来解决。

54.2.9. 终止 #

通常优雅的终止过程是前端发送一条Terminate消息并立即关闭连接。一旦收到消息,后端马上关闭连接并终止。

在少数情况下(比如一个管理员命令数据库关闭),后端可能在没有任何前端请求的情况下断开连接。在这种情况下,后端将在它断开连接之前尝试发送一个错误或通知消息,给出断开的原因。

其他终止的情况发生在各种失败的场合,比如某一方的内核转储、失去通讯链路、丢失了消息边界同步等。不管是前端还是后端看到了一个意外的连接关闭,那么它应该清理现场并且终止。如果前端不想终止自己,那么它有一个选项是重连服务器的方法启动一个新的后端。如果收到了一个无法识别的消息类型,那么我们也建议关闭连接,因为出现这种情况可能意味着是丢失了消息边界的同步。

不管是正常还是不正常的终止,任何打开的事务都会回滚而不是提交。不过,我们应该注意的是如果一个前端在一个非SELECT查询正在处理的时候断开,那么后端很可能在发现断开之前先完成查询的处理。如果查询处于任何事务块之外(BEGIN ... COMMIT序列),那么其结果很可能在得知断开之前被提交。

54.2.10. SSL 会话加密 #

如果编译PostgreSQL的时候打开了SSL支持,那么前后端通讯就可以用SSL加密。这样就提供了一种在攻击者可能捕获会话通讯数据包的环境下保证通讯安全的方法。有关使用SSL加密PostgreSQL会话的更多信息,请参阅第 18.9 节

要开始一次SSL加密连接,前端先是发送一个SSLRequest消息,而不是StartupMessage。然后服务器以一个包含SN的字节响应,分别表示它愿意还是不愿意进行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加密的第二种替代方法已可用。服务器将识别那些 立即开始SSL协商且没有任何先前SSLRequest数据包的连接。 一旦建立了SSL连接,服务器将期待一个正常的启动请求包, 并在加密通道上继续协商。在这种情况下,任何其他加密请求都会被拒绝。 这种方法不适合通用工具,因为它无法协商最佳的连接加密,也无法处理 未加密连接。然而,它适用于服务器和客户端共同受控的环境。在这种情 况下,它避免了一次往返延迟,并允许使用依赖标准SSL 连接的网络工具。使用这种方式的SSL连接时,客户端需 要使用由RFC 7301 定义的ALPN扩展,以防止协议混淆攻击。 PostgreSQL协议为“postgresql”,已在 IANA TLS ALPN Protocol IDs注册表中注册。

虽然协议本身并未提供强制SSL加密的方法,但管理员可以 将服务器配置为拒绝未加密的会话,这是认证检查的一个副产品。

54.2.11. GSSAPI 会话加密 #

如果PostgreSQL是使用GSSAPI支持构建的,则可以使用GSSAPI加密前端/后端之间的通信。 这为攻击者可能捕获会话流量的环境中提供了通信安全性。有关使用GSSAPI加密PostgreSQL会话的详细信息,请参阅 第 18.10 节

要启动一个GSSAPI加密连接,前端最初发送一个GSSENCRequest消息,而不是一个StartupMessage。 服务器随后会响应一个包含GN的单个字节,分别表示愿意或不愿意执行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 加密的方法,但管理员可以将服务器配置为拒绝未加密的会话,作为身份验证检查的副产品。