数年前为了实现高效率的RPC框架,研究了PostgreSQL数据库的通信协议,此次发布出来。使用数据库的通信协议作为RPC协议拥有很多好处,比如无需编写客户端,且数据库的客户端还可以利用上数据库连接池带来的若干好处。对此协议的研究最终写成了MagicRPC框架, https://github.com/gashero/magicrpc 。
译者:
gashero
日期:
2009-08-14
版本:
PostgreSQL-8.3
地址:
Documentation: 8.3: Frontend/Backend Protocol
目录
- 1 概览
- 1.1 报文概览
- 1.2 扩展查询概览
- 1.3 格式与格式代码
- 2 报文流
- 2.1 初始报文
- 2.2 简单查询
- 2.3 扩展查询
- 2.4 函数调用
- 2.5 COPY操作
- 2.6 异步操作
- 2.7 取消请求
- 2.8 终止
- 2.9 SSL会话加密
- 3 报文数据类型
- 4 报文格式
- 5 错误与提示信息字段
- 6 从Protocol2.0的改变
- 7 调试经验
- 7.1 开头的SSL确认
- 7.2 验证过程
- 7.3 密码确认
- 7.4 错误码
- 7.5 结果集的定义
PostgreSQL使用基于消息的协议在前端与后端之间通信。协议支持TCP/IP和Unix套接字。端口号5432已经注册到IANA作为著名协议,实际中可以使用任何端口号。
本文档描述3.0版的协议,基于PostgreSQL7.4和以后版本的实现。早起版本协议的描述请参考以前的文档。同一服务器可以支持多种版本的协议。初始化请求报文可以告知服务器所用的协议版本,而服务器可以跟随可用的协议。
高级的功能依赖于高版本的协议,例如libpq在连接时传递正确的环境变量。
为了有效的服务多个客户端,服务器启动一个新的”后端(backend)”进程给一个客户端。在当前实现一个新的客户端进程在连接时立即创建。这对协议是透明的,因此,对协议的目的,术语后端和服务器是可以互换(interchangeable)的;另外前端和客户端也是可以互换的。
其他参考文献:
- PgPP.pm: http://cpansearch.perl.org/src/ARC/DBD-PgPP-0.08/lib/DBD/PgPP.pm 找到了生成md5密码的方式
1 概览
http://www.postgresql.org/docs/8.3/static/protocol-overview.html
协议分为初始(startup)阶段和正常操作阶段。在初始阶段,前端打开连接、认证。这可能是一个报文,也可能是多个报文,依赖于认证方法。如果一切正常,服务器会发送状态信息到客户端,然后进入正常操作阶段。除了初始阶段请求报文外,这部分的协议由服务器驱动。
在正常操作阶段,前端发送请求和其他命令到后端,后端发送返回结果和其他响应。有些情况(例如 NOTIFY),后端会发送未请求(unsolicite)的报文,但是对大多数情况,会话由前端请求驱动。
会话的终止一般由前端选择,但是也可以由后端强制。在任何情况下,当后端关闭连接,会回滚(rollback)所有未完成的事务。
在正常操作阶段,SQL命令会通过两个子协议执行。在”简单查询”协议,前端只需要发送基于文本的查询字符串,然后立即解析和执行。在”扩展查询”协议,处理和请求分为多个步骤:解析、绑定参数值、执行。这是为了灵活性和性能而提供的。
正常操作还有些操作有附加子协议,例如 COPY 。
1.1 报文概览
所有通信都是报文流。报文的第一个字节标识了报文类型,随后的4字节指定了余下的报文长度(这个长度包含自身,但是不包含报文类型字节)。余下的内容基于报文类型来检测。出于历史原因,客户端发送的第一个报文(初始报文)没有消息类型字节。
为了避免在报文流中丢失同步,服务器和客户端都会在处理报文内容之前读入整个报文到缓冲区(依靠字节数)。这允许在处理发生错误时简单的恢复。在极端情况(extreme situation)下,例如没有足够的内存缓冲整个报文。接收方使用字节数决定跳过多少以读到报文。
相反的,服务器和客户端都必须小心的确保不能发送不完整的报文。可以通过把整个报文缓冲起来,直到完整后才发送。如果通信中途收发报文失败,明智的做法是放弃这个连接,哪怕还有一丝报文边界重新同步的希望。
1.2 扩展查询概览
对扩展查询协议,执行SQL命令分为多个步骤。步骤间的状态保留由两种类型的对象描述:prepared statement和portal。一个prepared statement描述了文本查询字符串的解析、语意分析、(可选的)查询计划的结果。一个prepared statement还没有准备好执行,因为缺少执行所必须的值。一个portal描述了准备好执行或已经部分执行的语句,所有缺少的参数都已经填入了。对于”SELECT”语句,一个portal等同于一个打开的游标,但是我们使用不同的术语因为游标无法处理非SELECT语句。
整个执行周期包含一个解析步骤,会从文本查询字符串创建prepared statement;一个绑定步骤,创建portal并传入prepared statement和参数值;一个执行步骤运行portal的查询。如果查询返回多行(SELECT、SHOW等等),执行步骤会被告知获取有限数量的行,以便多个执行查询步骤可以被需要完成操作。
后端可以保持多个prepared statement和portal(注意只存在于同一会话中,且不会在会话间共享)。已经存在的prepared statement和portal按照名字引用。另外,未命名的prepared statement和portal也是存在的。尽管其行为基本等同于命名对象,他们之上的优化面向只执行一次就丢弃,而命名对象的优化则是多次执行。
1.3 格式与格式代码
详细(particular)的数据类型可能以多种格式发送。在PostgreSQL 7.4,只支持”text”和”binary”格式,但是协议为扩展(provision)提供了可能。期望(desire)的格式依赖于格式码。客户端可以为每次发送制定格式码,也可以为每一列指定。文本的格式码为0,binary格式码为1,其他格式码保留作未来使用。
文本描述的值就是I/O转换时可以接受的值。在传输描述中,没有附带null符号,如果前端需要用C字符串处理,就要自己加上。(文本格式不允许潜入null)。
二进制描述的整数使用网络字节序。对其他数据类型参考文档。记住二进制描述的复杂数据类型可能在不同服务器版本之间是不同的;文本格式是更容易移植的方案。
2 报文流
Documentation: 8.3: Message Flow
这一节描述了报文流和报文类型的语意(semantics)。每个报文的详细信息参见后面章节。依赖于连接类型有多种不同的子协议:初始、查询、函数调用、 COPY 、终止。同时也有异步支持,包括通知响应和命令取消,在初始报文之后随时可用。
2.1 初始报文
为了开始会话,前端打开到服务器的连接,然后发送初始报文。这个报文包含用户名、数据库名、协议版本。同时,初始报文也可以包含运行时配置信息。服务器随后使用这些信息和自身配置文件(例如 pg_hba.conf )来检测连接是否可以临时性的接受,和使用何种后续的认证方式。
服务器随后发出对应的认证请求报文,而前端必须回复对应的认证响应报文(例如密码)。对所有除了GSSAPI和SSPI的认证方法,都是一个请求和一个响应。在一些方法中,前端无需响应,所以也没有认证请求发生。对于GSSAPI和SSPI,认证需要完成多次通信数据包的交换。
认证过程结束于服务器拒绝连接尝试(ErrorResponse),或者发送AuthenticationOk。
这个阶段服务器可能发送的报文包括:
- ErrorResponse :连接尝试被拒绝。服务里立即关闭连接。
- AuthenticationOk :认证成功
- AuthenticationKerberosV5 :前端必须加入服务器的Kerberos V5认证对话。如果成功,服务器响应AuthenticationOk,否则是ErrorResponse
- AuthenticationCleartextPassword :前端发送PasswordMessage,包含密码的clear-text(明文)形式。如果密码正确,服务器响应AuthenticationOk,否则是ErrorResponse。
- AuthenticationCryptPassword :前端必须发送PasswordMessage,包含通过crypt(3)加密的密码,AuthenticationCryptPassport报文中提供2字符的salt。
- AuthenticationMD5Password :前端必须发送PasswordMessage,包含MD5加密过的密码,AuthenticationMD5Password报文提供4字符的salt。
- AuthenticationSCMCredential :响应只能是local Unix-domain连接到平台支持的SCM凭据报文。前端必须生成SCM凭据报文,然后发送单一数据字节。数据字节的内容是无趣的,只用于确保服务器等待足够长事件来接受凭据报文。
- AuthenticationGSS :前端发起GSSAPI流。前端发送PasswordMessage,包含GSSAPI数据流的第一部分,后续的报文由服务器的AuthenticationGSSContinue要求。
- AuthenticationSSPI :前端发起(initite)SSPI流。前端发送PasswordMessage包含SSPI数据流,后续报文由服务器的AuthenticationGSSContinue要求。
- AuthenticationGSSContinue :这个报文包含GSSAPI和SSPI流的后续响应数据。如果GSSAPI或SSPI数据需要更多数据来完成认证,前端必须发送另一个PasswordMessage。如果GSSAPI或SSPI认证完成了,服务器会发送AuthenticationOK来表示认证成功,ErrorResponse表示失败。
如果前端不支持服务器请求的认证方法,会立即关闭连接。
在收到AuthenticationOk之后,前端必须等待服务器的另外一个报文。在这个阶段后端开始处理,而前端只能做旁观者。这时仍然有可能初始化失败(ErrorResponse),但是在大多数情况下是后端发送一些ParameterStatus报文、BackendKeyData和最后的ReadyForQuery。
在这个阶段后端会尝试应用初始报文中给出的运行时参数配置。如果成功,这些值会作为会话的默认值,否则ErrorResponse后退出。
这个阶段后端可能发送的报文:
- BackendKeyData :这个报文提供了以后前端用来取消请求的密码键数据。前端无需理会此报文,但是必须等待ReadyForQuery报文。
- ParameterStatus :这个报文告诉(informs)前端当前配置参数,例如 client_encoding 或者 DataStyle 。前端可以忽略此报文,或者记录下来供以后使用。前端无需响应此报文,但是应该继续等待ReadyForQuery报文。
- ReadyForQuery :初始化结束,前端可以发送命令了。
- ErrorResponse :初始化失败,发送此报文后连接关闭。
- NoticeResponse :一个警告信息。前端应该显示该消息然后继续等待ReadyForQuery或ErrorResponse。
在后端执行完命令后,也会发送ReadyForQuery报文。依赖于前端的代码,应该认为ReadyForQuery作为一个命令周期的开始,或者将ReadyForQuery作为初始化和每个子命令的结束。
2.2 简单查询
一个简单查询由前端发起,发送一个查询报文到后端。报文包括SQL命令文本。后端随后发送一个或多个相应报文,最后是一个ReadyForQuery响应报文。ReadyForQuery告知(inform)前端可以安全的发送新的命令了。(前端并不是必须等待ReadyForQuery,但是前端必须在命令失败时获取失败信息)。
后端可能的相应报文:
- CommandComplete :一个SQL命令正常结束
- CopyInResponse :后端准备好从前端拷贝数据到表格
- CopyOutResponse :后端准备好从表格拷贝数据到前端
- RowDescription :表明由SELECT、FETCH等返回的行。报文内容描述了列布局。这个报文跟随DataRow报文。
- DataRow :由SELECT、FETCH返回的行集合
- EmptyQueryResponse :检测到空的查询字符串
- ErrorResponse :发生了错误
- ReadyForQuery :查询过程结束了。一个分隔报文告知前端,因为查询字符串可能包含多个SQL命令。(CommandComplete标记一个SQL命令处理结束,而不是整个字符串)ReadyForQuery总是会发送,无论执行成功还是失败。
- NoticeResponse :表示查询有相关的警告信息。提示会在其他报文,如后端会继续执行命令。
SELECT查询的响应(或其他可以返回结果集的查询,如EXPLAIN、SHOW)一般包含RowDescription,0个或多个DataRow报文,随后是CommandComplete。COPY类操作调用特殊的协议。所有其他查询类型一般只产生一个CommandComplete报文。
因为一个查询字符串可能包含多个查询(由分号分开),所有可能有多个响应序列。ReadyForQuery用以标志整个查询字符串都处理完成,并可以接受新的查询字符串了。
如果一个空的查询字符串,响应就是EmptyQueryResponse,随后就是ReadyForQuery。
在发生错误时,ErrorResponse作响应,随后是ReadyForQuery。查询字符串中所有后续处理都会由ErrorResponse中断。注意这有可能在一系列报文中的一个无效查询锁产生。
在简单查询模式,取回值的格式总是文本,除非给定命令是FETCH,且已经有BINARY选项。在那种情况下,取回值是二进制格式。格式代码在RowDescription报文中。
前端必须准备好在准备接受其他报文时接受ErrorResponse和NoticeResponse报文。
建议实现前端时以状态机的风格,以便接受任意报文类型。
2.3 扩展查询
@waiting …
2.4 函数调用
函数调用子协议允许客户端直接请求已经存在的数据库pg_proc系统中的功能。客户端必须有函数的execute权限。
Note
函数调用子协议是一个遗留(legacy)功能,可能在未来删除。类似的功能可以通过设置一个prepared statement做”SELECT function($1,…)”。函数调用过程可以被Bind/Execute替换。
@waiting…
2.5 COPY操作
COPY命令允许与服务器的高速批量数据传输。拷贝入和拷贝出操作将协议转换到一种子协议中,直到操作完成。
@waiting …
2.6 异步操作
@waiting …
2.7 取消请求
@waiting …
2.8 终止
正常的终止过程是前端发送终止消息,并立即关闭连接。后端收到该消息以后也立即关闭连接。
特殊情况(如管理员强制关闭服务器),后端会在没有前端请求时关闭连接。在这种情况下后端会尝试发送错误或通知消息,在关闭连接之前给出关闭理由。
另一种关闭的情况是来自某些失败,例如核心错误,丢失通信连接丢失消息同步等等。前端或后端检测到没有预期的连接关闭,就应该清理和终止。前端有选项可以启动一个新的后端。收到未知类型消息时也可以关闭连接,通常发生在丢失消息同步时。
无论正常还是异常终止,任何打开的事务都要回滚,而不是提交。应该注意当前端执行非SELECT查询时发生了断开,后端会在通知断开前完成查询。如果查询不再任何事务块(BEGIN..COMMIT序列)中,其结果会在断开前提交。
2.9 SSL会话加密
如果构建PostgreSQL时有SSL支持,那么前后端通信会使用SSL加密。使得有攻击者的环境也没啥问题。对于加密的更多信息参考17.8节。
要初始化SSL加密的连接,前端要发送SSLRequest报文,而不是StartupMessage。服务器响应一个字节S或N,表示可以使用和不能使用SSL。如果前端收到的不是预期的,应该立即关闭连接。在S之后,可以执行SSL起始握手(这里不描述,是SSL规范的一部分)。如果成功,就继续发送普通的StartupMessage。这时StartupMessage和所有数据都是SSL加密的了。在N之后,发送普通StartupMessage而无加密。
前端也应该准备好接收ErrorMessage,发生于还没有SSL支持的PostgreSQL。这时连接会关闭,但是前端可以选择打开一个新的连接而不不使用SSL请求。
一个初始化SSLRequest也可以在已经打开的连接中发送CancelRequest报文。
协议本身不提供强制使用SSL加密,管理员可以配置服务器拒绝非加密会话,作为认证检查的备份。
3 报文数据类型
Documentation: 8.3: Message Data Types
这一节描述报文中的基本数据类型。
- Intn(i)
一个网络字节序的n bit整数,如果i指定了,则表示实际出现的数字,否则这个值为变量。例如 Int16 、 Int32(42) 。 - Intn[k]
一个k个元素的n bit整数的数组,网络字节序。数组长度k是由报文的前一字段决定的。 - String(s)
一个null结尾的C风格字符串。没有限制长度,如果s指定了,就是实际显示的值,否则就是变量。例如 String 、 String(“user”) 。 注意 :没有对字符串长度的预定义限制。前端的好编码策略是使用可扩展缓冲区,所以任何内存可以接受的东西都可以处理。如果不可行,读取整个字符串,然后丢弃缓冲区装不下的东西。 - Byten(c)
严格的n字节。如果字段宽度n不是常量,就是依据前一个字段。如果c指定了就是实际值。例如 Byte2 、 Byte1(‘n’) 。
4 报文格式
http://www.postgresql.org/docs/8.3/static/protocol-message-formats.html
本节描述每个消息报文的具体格式。报文上标注了发送方:前端(F)、后端(B)、双方(F&B)。注意,尽管每个报文在开始处都包含一个字节数,消息格式的结束可以不在字节数引用中。其目标是合法性检测。(CopyData报文是个例外,因为他的格式部分与数据流;任何非法CopyData数据都无法被中断)。
AuthenticationOK (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32(8) :内容长度,包含自身
- Int32(0) :制定认证成功了
AuthenticationKerberosV5 (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32(8) :内容长度,包含自身
- Int32(2) :指定Kerberos V5认证需求
AuthenticationCleartextPassword (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32(8) :内容长度,包含自身
- Int32(3) :指定需要clear-text密码
AuthenticationCryptPassword (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32(10) :内容长度,包含自身
- Int32(4) :指定需要crypt()加密过的密码
- Byte2 :加密时所需salt
AuthenticationMD5Password (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32(12) :内容长度,包含自身
- Int32(5) :指定需要MD5加密的密码
- Byte4 :加密时用到的salt
AuthenticationSCMCredential (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32(8) :内容长度,包含自身
- Int32(6) :标志使用SCM认证
AuthenticationGSS (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32(8) :内容长度,包含自身
- Int32(7) :标志使用GSS API认证
AuthenticationSSPI (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32(8) :内容长度,包含自身
- Int32(9) :标志使用SSPI认证
AuthenticationGSSContinue (B)
- Byte1(‘R’) :标志报文为认证请求
- Int32 :内容长度,包含自身
- Int32(8) :指定报文包含GSSAPI或SSPI数据
- Byten :GSSAPI或SSPI认证数据
BackendKeyData (B)
- Byte1(‘K’) :标志报文为取消关键数据,前端必须保存这些值,以便用于之后的CancelRequest报文
- Int32(12) :内容长度,包含自身
- Int32 :后端processID
- Int32 :后端的密钥
Bind (F)
- Byte1(‘B’) :标志报文为Bind命令
- Int32 :内容长度,包含自身
- String :目标portal的名字(空字符串表示未命名)
- String :源prepared statement的名字(空字符串表示未命名)
- Int16 :随后跟着的参数格式码(就是下面的C),0表示没参数或使用文本格式;1表示二进制格式;或者等于参数个数
- Int16[C] :参数格式码,每个都必须取值0(文本)或1(二进制)
- Int16 :跟随的参数值(可以是0)。必须匹配查询时参数个数
随后是每个参数的字段对:
- Int32 :参数值的长度(字节,不包含自身),可以是0,-1表示NULL,在NULL之后没有跟随的字节
- Byten :参数值,由之前指定的格式,n是上面的长度
在最后一个参数后的字段:
- Int16 :返回列的格式码(下面的R),可以是0表示无结果或文本格式,1表示二进制格式,或者等于结果列的数量
- Int16[R] :结果列格式码,每个都必须取值0(文本)或1(二进制)
BindComplete (B)
- Byte1(‘2’) :标志报文为绑定完成
- Int32(4) :内容长度,包含自身
CancelRequest (F)
- Int32(16) :内容长度,包含自身
- Int32(80877102) :取消请求的代码,值取自1234*0xffff+5678,为了避免混乱,这个代码必须不能与协议版本号相同
- Int32 :目标后端进程ID
- Int32 :目标后端密钥
Close (F)
- Byte1(‘C’) :标志关闭命令报文
- Int32 :内容长度,包含自身
- Byte1 :’S’关闭prepared statement;’P’关闭portal
- String :要关闭的prepared statement或portal的名字,空字符串选择未命名的
CloseComplete (B)
- Byte1(‘3’) :标志关闭完成报文
- Int32(4) :内容长度,包含自身
CommandComplete (B)
- Byte1(‘C’) :标志命令完成响应
- Int32 :内容长度,包含自身
- String :命令标签(tag)。通常是一个单词表示刚完成的SQL命令:
- 对INSERT命令,标签是 INSERT oid rows 参数rows是插入行数,oid是当插入1行时的对象ID,否则oid为0
- 对DELETE命令,标签是 DELETE rows ,rows是删除的行数
- 对UPDATE命令,标签是 UPDATE rows ,rows是更新的行数
- 对MOVE命令,标签是 MOVE rows ,rows是游标移动行数
- 对FETCH命令,标签是 FETCH rows ,rows是获取的行数
- 对COPY命令,标签是 COPY rows ,rows是拷贝的行数(注意行数仅在PostgreSQL 8.2以后有显示)
- CREATE TABLE
CopyData (F&B)
- Byte1(‘d’) :标志COPY数据报文
- Int32 :内容长度,包含自身
- Byten :COPY数据流。从后端发来的总是表示单行数据,但是前端发来的可能被无意义的分割
CopyDone (F&B)
- Byte1(‘c’) :标志COPY完成报文
- Int32(4) :内容长度,包含自身
CopyFail (F)
- Byte1(‘f’) :标志COPY失败报文
- Int32 :内容长度,包含自身
- String :错误信息描述失败原因
CopyInResponse (B)
- Byte1(‘G’) :标志开始拷贝入响应,前端必须立即发送拷贝进入数据(如果没有准备好,就发送CopyFail报文)
- Int32 :内容长度,包含自身
- Int8 :0表示全部COPY格式是文本(行以新行分隔,列以分隔符分隔),1表示所有拷贝数据格式是二进制(类似于DataRow格式)
- Int16 :拷贝进来的列数量,其数字是下面的N
- Int16[N] :每个列的格式码,每个必须是0(文本)或1(二进制)。如果所有拷贝格式是文本则必须全部是0
CopyOutResponse (B)
- Byte1(‘H’) :标志开始拷贝出响应,这个报文必须跟随着拷贝出的数据
- Int32 :内容长度,包含自身
- Int8 :0表示文本格式(行以新行分隔,列以分隔符分隔),1表示二进制格式(类似于DataRow格式),查看COPY命令了解更多
- Int16 :列的数量(下面的N)
- Int16[N] :每个列的格式码,必须取值0或1,分别表示文本和二进制
DataRow (B)
- Byte1(‘D’) :标志数据行报文
- Int32 :内容长度,包含自身
- Int16 :随后列的数值(可能是0)
接下来的是每一列的字段对:
- Int32 :列值的长度,单位是字节(不包含自身),可以为0,特殊情况-1表示NULL值,且NULL时没有跟随的值字节串
- Byten :列的值,有如格式码描述的,n是如上长度
Describe (F)
- Byte1(‘D’) :标志描述命令报文
- Int32 :内容长度,包含自身
- Byte1 :’S’表示prepared statement;’P’表示portal
- String :prepared statement或portal的名字,空字符串选择未命名的
EmptyQueryResponse (B)
- Byte1(‘I’) :标志查询结果为空的响应,作为CommandComplete的替代(substitute)
- Int32(4) :内容长度,包含自身
ErrorResponse (B)
- Byte1(‘E’) :标志错误信息报文
- Int32 :内容长度,包含自身
消息体由一个或多个字段组成,以null结尾,字段可以任何顺序出现,对每个字段应该有:
- Byte1 :代码用以标志字段类型;如果0则表示消息结束符,后面没字符串了。描述定义字段类型键45.5节。更多字段类型可能在未来添加进来,前端应该无声的忽略不识别的类型
- String :字段值
Execute (F)
- Byte1(‘E’) :标志为执行命令
- Int32 :内容长度,包含自身
- String :portal的名字,空字符串选择未命名portal
- Int32 :最大允许返回行数量,0表示无限制
Flush (F)
- Byte1(‘H’) :标志清空命令
- Int32(4) :内容长度,包含自身
FunctionCall (F)
- Byte1(‘F’) :标志是函数调用报文
- Int32 :内容长度,包含自身
- Int32 :指定函数的对象ID
- Int16 :参数格式码(下面的C),0表示无参数或文本,1表示二进制,也可以是实际参数个数
- Int16[C] :参数格式码,必须取值0或1,表示文本或二进制
- Int16 :指定提供给函数的参数个数
下面是每个参数的字段对:
- Int32 :参数值长度(字节,不包含自身),可以是0,-1表示NULL,NULL没有跟随的字节
- Byten :参数值,按照上面给定格式码,长度为上面字段n
在这些参数后,跟随的字段:
- 函数结果的格式码,必须取值0或1
FunctionCallResponse (B)
- Byte1(‘V’) :标志报文是函数调用结果
- Int32 :内容长度,包含自身
- Int32 :函数结果值长度(字节,不包含自身),可以是0,-1表示NULL,NULL后无跟随字节
- Byten :函数结果值,按照上面给定的格式码和长度
NoData (B)
- Byte1(‘n’) :标志无数据
- Int32(4) :内容长度,包含自身
NoticeResponse (B)
- Byte1(‘N’) :标志通知报文
- Int32 :内容长度,包含自身
消息体由一个或多个字段构成,跟随0表示结束符。字段可以用任何顺序显示,每个字段为:
- Byte1 :代码表示字段类型;如果0则为消息结束符。字段类型定义见45.5节。未来可能添加更多字段类型,前端应该无声的忽略不识别的字段
- String :字段值
NotificationResponse (B)
- Byte1(‘A’) :标志通知响应报文
- Int32 :内容长度,包含自身
- Int32 :通知的后端进程ID
- String :抛出通知的条件(condition)名字
- String :通知附加信息(当前尚未实现,所以该字段总是空字符串)
ParameterDescription (B)
- Byte1(‘t’) :标志参数描述报文
- Int32 :内容长度,包含自身
- Int16 :语句使用的参数数量(可以是0)
下面对每个参数:
- Int32 :指定参数数据类型的对象ID
ParameterStatus (B)
- Byte1(‘S’) :标志运行时参数状态报告
- Int32 :内容长度,包含自身
- String :运行时参数名字
- String :参数值
Parse (F)
- Byte1(‘P’) :标志Parse命令报文
- Int32 :内容长度,包含自身
- String :目标prepared statement的名字(空字符串选择未命名的)
- String :要解析的查询字符串
- Int16 :参数数据类型数字(可以是0)。注意这里不是参数在查询字符串中的占位符,只有前端想要预备的类型
然后对每个参数:
- Int32 :指定参数数据类型的对象ID。防止0等同于让类型不指定
ParseComplete (B)
- Byte1(‘1’) :标志解析完成报文
- Int32(4) :内容长度,包含自身
PasswordMessage (F)
- Byte1(‘p’) :标志密码响应,同时用于GSSAPI和SSPI的响应(这是个设计错误,因为包含数据不是以null结束的,可以是任意二进制数据)
- Int32 :内容产度,包含自身
- String :密码(按照请求不同,可能是加密的)
PortalSuspended (B)
- Byte1(‘s’) :标志portal挂起报文,注意只在Execute报文的行数限制到达时会显示
- Int32(4) :内容长度,包含自身
Query (F)
- Byte1(‘Q’) :定义这是个简单查询
- Int32 :内容长度,包含自身
- String :查询字符串
ReadyForQuery (B)
- Byte1(‘Z’) :标志报文为后端准备好接收新的查询
- Int32(5) :内容长度,包含自身
- Byte1 :当前后端事务状态码。”I”表示空闲(没有在事务中)、”T”表示在事务中;”E”表示在失败的事务中(事务块结束前查询都回被驳回)
RowDescription (B)
- Byte1(‘T’) :标志报文为行描述
- Int32 :内容长度,包含自身
- Int16 :指定字段数量(可以为0)
然后就是各个字段了
- String :字段名,注意有结尾的NULL
- Int32 :如果字段是特定表的列,则是表格的对象ID,否则为0
- Int16 :如果字段是特定表的列,则是列的属性名,否则为0
- Int32 :字段数据类型对象ID,详见下面的实践笔记
- Int16 :数据类型大小(查看pg_type.typlen),注意负数表示变长类型,-1为varlena,-2为NULL结尾C字符串,其他为固定长度
- Int32 :类型修饰符(查看pg_attribute.atttypmod),修饰符是类型相关的
- Int16 :字段的格式码,当前0为文本,1为二进制。如果是从Describe的语句变量返回的RowDescription,则格式码未知,总是返回0
SSLRequest (F)
- Int32(8) :内容长度,包含自身
- Int32(80877103) :SSL请求码,值来自1234*0xffff+5679。为了避免混乱,这个代码必须不能与协议版本号相同
StartupMessage (F)
- Int32 :内容长度,包含自身
- Int32(196608) :协议版本号,最重要的是16bit的主版本号3。随后是次版本号0。196608=0x00030000。
协议版本号之后就是多个键值对,最后一个键值对结尾要有null字符,键值对参数可以用任何顺序出现, user 是必须的,其他都可选。
- String :参数名,当前可以被识别的键名包括:user(数据库用户名)、database(要连接的数据库,默认是用户名)、options(后端命令行选项)。
- String :参数值
Sync (F)
- Byte1(‘S’) :标示同步命令
- Int32(4) :内容长度,包含自身
Terminate (F)
- Byte1(‘X’) :标志终止命令
- Int32(4) :内容长度,包含自身
5 错误与提示信息字段
Error and Notice Message Fields
本节描述ErrorResponse和NoticeResponse报文中的字段。每个字段类型有一个单一字节记号(token)。注意每个给定字段至少出现在一个报文中。
S(Severity) :严重程度,字段内容是ERROR、FATAL、PANIC(在错误信息中)、WARNING、NOTICE、DEBUG、INFO、LOG(在notice信息中)或本地翻译。
C(Code) :错误的SQLSTATE代码,非本地化的
M(Message) :人类可读的错误信息,一般很短且只有1行
D(Detail) :可选的第二错误信息,包含问题的详细描述,可能多行
H(Hint) :可选的问题建议,建议不同于detail的是包含建议,可能多行
P(Position) :字段值的ASCII码整数,引用错误查询语句的光标位置,第一个字符是1,以字符引用,而不是字节
p(internal position) :定义类似于P字段,只不过用于引用内部生成命令的错误位置,当这个字段显示则q字段也总是显示
q(internal query) :内部生成的查询命令,例如PL/pgSQL函数
W(Where) :一段索引正文,包括调用栈和当前处理语言函数的内部查询,每行一个
F(File) :发生错误的源码文件名
L(Line) :发生错误的源码行号
R(Routine) :发生错误的源码常规
客户端选择如何显示这些信息,一般至少应该对较长的行来折行,以及分段。
6 从Protocol2.0的改变
Summary of Changes since Protocol 2.0
@waiting…
7 调试经验
7.1 开头的SSL确认
实际安装了PostgreSQL8.3的客户端和服务器以后,使用代理服务器测试发现默认使用的是SSL。所以进入了postgres.conf修改了 ssl=false ,然后重启(restart)服务器。才可以以明文方式通信。
实际的协议中,第一个通信报文并不是startup包,而应该算是询问是否使用SSL的包。前端发来的包是 \x00\x00\x00\x08\x04\xd2\x16/ ,其中去除前4字节长度,剩余部分应该是用于询问是否支持SSL通信,如果支持优先使用SSL。
后端对此询问只需要回复一个字节,”S”表示支持SSL,我没有继续深究。如果回复”N”则不支持SSL。然后就是正常的Startup流程。
7.2 验证过程
正式收到版本号为 \x00\x03\x00\x00 的Startup包以后,后端发送验证需求。然后前段回复PasswordMessage。对于ClearText类密码的格式就是一个字符串,当然末尾有null。
验证通过后后端应该发送AuthenticationOK报文。不过这还没结束,后端还需要发一系列的报文才能完成整个验证和初始化,而这期间前端无需发送任何报文。
按照顺序,尚未解析的各个报文如下:
- S\x00\x00\x00\x19client_encoding\x00UTF8\x00
- S\x00\x00\x00\x17DataStyle\x00ISO, YMD\x00
- S\x00\x00\x00\x19integer_datatimes\x00on\x00
- S\x00\x00\x00\x14is_superuser\x00on\x00
- S\x00\x00\x00\x19server_encoding\x00UTF8\x00
- S\x00\x00\x00\x1aserver_version\x008.3.11\x00
- S\x00\x00\x00#session_authorization\x00postgres\x00
- S\x00\x00\x00$standard_conforming_strings\x00off\x00
- S\x00\x00\x00\x11TimeZone\x00PRC\x00
- K\x00\x00\x00\x0c\x00\x00&\xefY3>\xc1
- Z\x00\x00\x00\x05I
整体数据是:
‘R\x00\x00\x00\x08\x00\x00\x00\x00’
‘S\x00\x00\x00\x19client_encoding\x00UTF8\x00’
‘S\x00\x00\x00\x17DateStyle\x00ISO, YMD\x00’
‘S\x00\x00\x00\x19integer_datetimes\x00on\x00’
‘S\x00\x00\x00\x14is_superuser\x00on\x00’
‘S\x00\x00\x00\x19server_encoding\x00UTF8\x00’
‘S\x00\x00\x00\x1aserver_version\x008.3.11\x00’
‘S\x00\x00\x00#session_authorization\x00postgres\x00’
‘S\x00\x00\x00$standard_conforming_strings\x00off\x00’
‘S\x00\x00\x00\x11TimeZone\x00PRC\x00’
‘K\x00\x00\x00\x0c\x00\x00&\xefY3>\xc1’
‘Z\x00\x00\x00\x05I’
先不管那么多,先一起发过去,客户端已经算是登录成功了。可以发送请求了。
7.3 密码确认
客户端连接服务器,服务器如果要求密码,则客户端会不发送任何数据包立刻断开,等待用户输入完密码以后重新连接。至少psql命令就是如此。
实际的MD5密码生成方法为:
result=’md5’+md5sum(md5sum(password+username)+saltstr)
7.4 错误码
一个没有找到表的基本错误信息 ‘S\xe9\x94\x99\xe8\xaf\xaf\x00C42P01\x00M\xe5\x85\xb3\xe7\xb3\xbb “testtest” \xe4\xb8\x8d\xe5\xad\x98\xe5\x9c\xa8\x00Fnamespace.c\x00L273\x00RRangeVarGetRelid\x00\x00’ 。其中分解开就是每一小段以一个字母开头,随后是NULL结尾字符串。最后再加一个NULL。这些开头的字母如上面讲解的。
其中C(CODE)字段存储的是SQLSTATE,这是个5字节数组。这5个字节中可以包含数字和大写字母,前2个字符代表基本错误类,后3个字符表示错误子类。成功的SQLSTATE是”00000″。这些都是SQL标准定义的。建议以后都使用SQLSTATE而不是各个数据库自己的错误码。
一些SQLSTATE示例: http://www.postgresql.org/docs/8.3/static/errcodes-appendix.html
SQLSTATE与错误码: Documentation: 8.3: Error Handling
几个常用的:
- 00000 :成功
- 01000 :警告
- 02000 :无数据
- 03000 :SQL语句不完整
- 08000 :连接异常
- 0A000 :不支持的功能
- 22000 :数据异常
- 42000 :语法错误
- 54000 :程序范围限制
- 54001 :语句太复杂
- 54011 :太多的列
- 54023 :太多参数
- 58030 :I/O错误
- P0000 :PLPGSQL ERROR
- P0001 :抛出异常
- P0002 :没找到数据
- P0003 :太多行
- XX000 :内部错误
- XX001 :数据被误用
- XX002 :索引被误用
7.5 结果集的定义
使用RowDescription包。
一个结果集定义 \x00\x01name\x00\x00\x00@\x03\x00\x02\x00\x00\x04\x13\xff\xff\x00\x00\x00D\x00\x00 。其中字段定义如下:
- 字段数16bit:1个
- 字段名: name\x00
- 特定表的表格对象ID: \x00\x00@\x03
- 特定表的列属性对象ID: \x00\x02
- 字段数据类型对象ID: \x00\x00\x04\x13
- 数据类型长度: \xff\xff
- 类型修饰符: \x00\x00\x00D
- 字段格式码: \x00\x00
实际这是一个VARCHAR类型的字段。即类型代码1043对应varchar类型。具体的类型映射表可以进入PGSQL自己查询,使用 SELECT typname,oid FROM pg_type; ,我使用8.3.11,其中有269个返回结果。列出几个常用的:
- bool=16
- bytea=17
- char=18
- name=19
- int8=20
- int4=23
- text=25
- xml=142
- float4=700
- float8=701
- abstime=702
- reltime=703
- inet=869
- varchar=1043
- date=1082
- time=1083
- timestamp=1114
- numeric=1700
- uuid=2950
- record=2249
- cstring=2275
- trigger=2279
- parameters=11393