4.2. 协议

本节描述信息流.根据联接的状态不同有四种类型的流: 启动(startup),查询(query),函数调用(function call)和结束(termination). 还有用于通知响应和命令取消的特殊信息, 这些特殊信息可能在启动阶段过后的任何时间产生.

4.2.1. 启动

开始时,前端发送一个 StartupPacket (启动包). 服务器利用这个信息和pg_hba.conf 文件的内容决定这个前端必须使用哪种认证方式. 然后服务器用下面信息之一响应:

ErrorResponse

然后服务器马上关闭联接.

AuthenticationOk

认证交换完成.

AuthenticationKerberosV4

然后前端必须与服务器进行一次 Kerberos V4 认证对话(在这里没有描述,Kerberos 规范的一部分). 如果对话成功,服务器响应一个 AuthenticationOk (认证成功)信息, 否则它响应一个 ErrorResponse (错误响应).

AuthenticationKerberosV5

然后前端必须与服务器进行一次 Kerberos V5 认证对话(在这里没有描述,Kerberos 规范的一部分). 如果对话成功,服务器响应一个 AuthenticationOk (认证成功)信息, 否则它响应一个 ErrorResponse (错误响应).

AuthenticationUnencryptedPassword

然后前端必须发送一个 UnencryptedPasswordPacket (未加密口令)包. 如果这是正确的口令,服务器用一个 AuthenticationOk 包响应, 否则它响应一个 ErrorResponse 包.

AuthenticationEncryptedPassword

然后前端必须发送一个 EncryptedPasswordPacket (加密口令)包. 如果这是正确口令,服务器用一个AuthenticationOk 响应, 否则它用一个 ErrorResponse 响应.

如果前端不支持服务器要求的认证方式,那么它应该马上关闭联接.

在发送完 AuthenticationOk 包之后, 前端必须等待后端确认成功启动. 这时前端不应该发送任何信息.在这个阶段从后端来的信息可能是:

BackendKeyData

这个消息提供了密钥(secret-key)数据, 前端如果想要在稍后发出取消的请求, 则必须保存这个数据.前端不应该响应这个信息, 但是应该继续侦听等待 ReadyForQuery 消息.

ReadyForQuery

后端启动成功,前端现在可以发出查询或者函数调用消息.

ErrorResponse

后端启动失败.在发送完这个消息之后联接被关闭.

NoticeResponse

发出了一个警告信息.前端应该显示这个信息, 并且继续等待 ReadyForQuery 或 ErrorResponse.

后端在每个查询循环后都会发出一个相同的 ReadyForQuery 消息. 前端可以认为 ReadyForQuery 是一个查询循环的开始 (而 BackendKeyData 表明启动阶段的成功完成), 或者认为 ReadyForQuery 是启动阶段和每个随后查询循环的结束, 具体是那种情况取决于前端的编码需要.

4.2.2. 查询

一个查询循环是由前端发送一条 Query 消息给后端初始化的. 后端根据查询命令字串的内容发送一条或者更多条响应消息给前端, 并且最后是一条 ReadyForQuery 响应信息. ReadyForQuery 通知前端它可以安全地发送新查询或者函数调用给后端了.

从后端来的可能的消息是:

CompletedResponse

一个 SQL 命令正常结束.

CopyInResponse

后端已经准备好从前端拷贝数据到一个关系里面去. 然后前端应该发送一条 CopyDataRows 消息. 然后后端用一个带有标记 COPY 的 CompletedResponse 消息响应.

CopyOutResponse

后端已经准备好从一个表里拷贝数据到前端里面去. 然后它会发送一条 CopyDataRows 消息. 最后后端发送一个带有标记 COPY 的 CompletedResponse 消息.

CursorResponse

对一个 SELECTFETCHINSERTUPDATE, 或者 DELETE 查询响应的开始. 如果是 FETCH,那么再消息里包含正在从中 抓取数据的游标的名字.否则消息总是谈及"blank"游标.

RowDescription

表明正准备返回该行以响应一个 SELECTFETCH 查询.该消息内容描述了该行的布局. 它后面的每一个返回给前端的行都将跟着一个 AsciiRow 或者 BinaryRow 消息 (取决于是否声明了一个二进制游标).

EmptyQueryResponse

识别了一个空的查询字串.

ErrorResponse

出错了.

ReadyForQuery

查询字串的处理完成. 发送一个独立的消息来标识这个是因为查询字串可能包含多个 SQL 命令. (CompletedResponse 只是标记一条 SQL 命令处理完毕, 而不是整个字串.) ReadyForQuery 总会被发送,不管是处理成功结束还是产生错误.

NoticeResponse

发送了一个与查询有关的警告信息. 注意信息是附加在其他响应上的,也就是说, 后端将继续处理该命令.

SELECTFETCH 查询的响应信息通常包含 CursorResponse,RowDescription,零个或者多个 AsciiRow 或者 BinaryRow 消息,以及最后的 CompletedResponse.INSERTUPDATE,和 DELETE 查询 生成 CursorResponse 消息以及随后的 CompletedResponse. 从前端来回 COPY 调用前面提到的特殊的协议. 所有其他查询类型通常只生成一个 CompletedResponse 消息.

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

如果收到一个完全空(除了空白之外没有内容)的查询字串,那么响应是 一条 EmptyQueryResponse 后面跟着 ReadyForQuery.(需要如此特殊对待 的原因是历史原因.)

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

前端在等待其他类型的消息时必须准备接收 ErrorResponse 和 NoticeResponse 消息.

实际上,当前端没有等待任何消息时 NoticeResponse 消息也有可能到达, 就是说,后端正常空闲.(具体来说是后端可以被其父进程的命令终止运行. 在那种情况下,后端将在关闭联接之前发送一条 NoticeResponse 消息.) 我们建议前端在发出任何命令之前检查这样的异步通知消息.

同样,如果前端发出了任何LISTEN命令, 那么它必须在任何时候准备接收 NotificationResponse 消息;见下文.

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

4.2.3. 函数调用

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

从后端来的可能的响应信息是:

ErrorResponse

发生了一个错误.

FunctionResultResponse

函数调用被执行并且返回一个结果.

FunctionVoidResponse

函数调用被执行并且没有返回结果.

ReadyForQuery

函数调用处理完成.ReadyForQuery 将总是被发送, 不管是成功完成处理还是发生一个错误.

NoticeResponse

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

前端在等待其他类型的消息时必须准备接收 ErrorResponse 和 NoticeResponse 消息. 同样,如果前端发出了任何 LISTEN命令, 那么它必须准备在任何时候接收 NotificationResponse 消息;见下文.

4.2.4. 通知响应

如果前端发出了一条LISTEN命令, 那么每当为同样名称的通知执行一条NOTIFY命令, 后端都会发送一条NotificationResponse 消息 (不要与 NoticeResponse 弄混了!).

除了在另外一个后端的消息里以外,在协议里的任何地方(启动后)都允许通知响应. 因此,前端在等待任何消息的时候都必须准备识别 NotificationResponse 消息. 实际上,前端甚至在不能进行查询的时候都要能够处理NotificationResponse 消息.

NotificationResponse

为前面执行的LISTEN命令的名称被执行一条 NOTIFY命令. 通知可以在任何时候发送.

有一点值得我们指出来,就是在 listen 和 notify 里面的名称 不必与 SQL 数据库里的关系(表)的名称有任何关系. 通知名只是一个独立选出来的条件名.

4.2.5. 取消正在处理的请求

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

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

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

取消信号可能有也可能没有做用 -- 例如,如果它在后端完成查询的处理后到达, 那么它就没有做用.如果取消起作用了,其结果是当前命令带着一个错误信息提前退出.

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

因为取消请求是通过新的联接发送给服务器而不是通过平常的前端/后端通讯链接, 所以取消请求可能是任意进程执行的,而不仅仅是要取消查询的前端. 这样可能对创建多进程应用有某种灵活性的好处.但是同时这样也带来了安全风险, 因为这样任何一个非认证用户都可能试图取消查询. 这个安全风险通过要求在取消请求中提供一个动态生成的安全键字排除.

4.2.6. 终止

通常的和优雅的终止过程是前端发送一条 Terminate (终止)消息并且立刻关闭联接. 一旦收到消息,后端马上关闭联接并且退出.

一个欠优雅的的退出可能因为任何一端的软件失效(例如.内核倾倒)产生. 如果前端或后端看到一个意外的联接关闭,它应该清理并退出. 前端可以选择通过服务器重新建立一个新的联接 -- 如果它不想终止自己的处的话.

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

4.2.7. SSL 会话加密

最近的 PostgreSQL 版本允许前后端通讯使用 SSL 加密. 这样就提供了一种在攻击者可能捕获会话通讯数据包的环境下保证通讯安全的 方法.

要开始一次 SSL 加密联接,前端先是发送一个 SSLRequest 消息,而不是 StartupPacket.然后服务器以一个包含 YN 的字节响应,分别表示它愿意还是不愿意进行 SSL.如果前端对响应不满意, 那么它可疑关闭联接.要在 Y 之后继续,那么先进行与服务器的 SSL 启动握手(没有在这里描述,是 SSL 规范的一部分). 如果这些成功了,那么继续发送普通的 StartupPacket.这种情况下, StartupPacket 很所有随后的数据都将由 SSL 加密.要在 N 之后继续,则发送普通的 StartupPacket 然后不带加密进行处理.

前端应该也准备处理一个来自服务器的给 SSLRequest 的 ErrorMessage 响应.这种情况只有在服务器给 PostgreSQL 的 SSL 支持增加了附加的期望的情况下才会出现.在这种情况下, 联接必需关闭,但是前端可以选择打开一个新的联接然后不带 SSL 进行联接.

一个初始化的 SSLRequest 也可以用于打开来用于发送一条 CancelRequest 消息的联接中.

如果协议本身并未提供某种方法强制 SSL 加密,那么管理员可以把服务器 配置为拒绝未加密的会话,这是人证检查的一个副产品.