PostgreSQL协议解析

在访问pg数据库时,需要客户端按照规定的协议访问。所有的数据库都有一套自己的协议,所以可以开发各种语言对应的链接库,也可以根据协议自己开发一套。

消息总体格式

官方描述

pg的消息格式是文字描述的,所以需要仔细阅读官方文档。

 协议在启动和正常操作过程中有不同的阶段。在启动阶段里,前端打开一个到服务器的连接并且认证自身以满足服务器(这可能涉及到一条或多条消息,取决于使用的认证方法)。 如果一切正常,服务器就发送状态信息给前端,并最后进入正常操作。除了最初的启动请求消息之外,协议的这个部分是服务器驱动的。

在正常操作中,前端发送查询和其它命令到后端,然后后端返回查询结果和其它响应。在少数几种情况(比如NOTIFY)中,后端会发送未被请求的消息,但这个会话中的绝大多部分都是由前端请求驱动的。

会话的终止通常是由前端来选择的,但是也可以在某些情况下由后端强制执行。不管在那种情况下,如果后端关闭连接,那么它将在退出之前回滚所有打开的(未完成的)事务。

在正常操作中,SQL命令可以通过两个子协议中的任何一个执行。 在“简单查询”协议中,前端只是发送一个文本查询串, 然后后端马上分析并执行它。在“扩展查询”协议中, 查询的处理被分割为多个步骤:分析、参数值绑定和执行。这样就可以提供灵活性和性能的改进,但代价是额外的复杂性。

正常操作还有用于类似COPY这样的额外的子协议。 

 所有通讯都是通过一个消息流进行的。消息的第一个字节标识消息类型, 然后后面跟着的四个字节声明消息剩下部分的长度(这个长度包括长度域自身,但是不包括消息类型字节)。 剩下的消息内容由消息类型决定。由于历史原因,客户端发送的最初的消息(启动消息)不包含消息类型字节。

为了避免失去与消息流的同步,服务器和客户端通常都是把整个消息读取到一个缓冲区里(使用字节计数), 然后才试图处理其内容。这样在处理内容的过程时如果发现错误,就比较容易恢复。 在非常极端的情况下(比如说没有足够的内存缓冲消息),接收端可以使用字节计数来判断它在继续读取消息之前需要跳过多少输入。

反之,服务器和客户端都需要注意决不能发送一条不完整的消息。保证这一点的方法通常是在发送整条信息之前先在一个缓冲区里整理整条消息。 如果在发送或者接受一条消息的中间发生了通讯错误,那么唯一合理的响应是放弃连接,因为恢复消息边界同步的希望很小。 

格式示意图

|00|01|02|03|04|...|
|c |    len    |...|

pg开始的ssl和starup消息因为兼容问题,没有前面的c,直接是len,后续的包括认证消息(密码等)、查询语句,都是上面的格式。

这里len的长度包括len和后续数据,不包括前面的c标志位。

注意事项

  • pg消息使用的是网络字节序(大端)。

通信流程

  • 询问是否ssl加密
  • startup启动阶段
  • 认证
  • 查询阶段
  • 结束

客户端请求是否ssl加密

前32 int是8,表示长度;后32是80877103,表示请求码

 SSLRequest (F)
    Int32(8)
        以字节计的消息内容的长度,包括长度本身。

    Int32(80877103)
        SSL请求码。选取的值在高16位里包含1234,在低16位里包含5679 (为了避免混淆,这个编码必须和任何协议版本号不同)。 

SSLResponse (B)
    S-加密
    N-不加密

客户端请求询问服务器是否加密,服务器如果回复N,表示不加密,如果回复S,表示加密,走加密流程,交换密钥,后续通信通过SSL加密完成。ssl加密后,在网络上抓包获取的数据就无法解析了,因为ssl理论上是不可被破解的。

非对称加密

这里顺便说一下,为什么ssl加密后无法破解。ssl是非对称加密,除了非对称加密,还有对应的对称加密。主要区别就是原文用密钥加密后,是否可以用同一个密钥解密。加密解密是同一个密钥的是对称加密,不是的就是非对称加密。

ssl就是为了网络上传输的安全,在双方进行交互前,会交换各自的公钥,发给对方的数据用对方的公钥加密,加密过的内容用相同的公钥是无法解密的,必须要用对应的私钥。私钥都是在各自系统上的,并不会传播,所以就算获取到加密内容和公钥,同样是无法知道原数据是什么。

除了是否是对称加密外,加密算法还有一种分类就是是否可逆,md5就是一种不可逆的加密算法,加密得到的内容无法还原出原始内容,ssl就是一种可逆的算法,加密的密文是可以解密出来的。

startup启动阶段

确定好加密问题后,接下来就是启动阶段。启动阶段双方会协商通信协议、数据库版本等信息,还包括权限认证。认证方式有很多种,常见的就是用户名密码认证。服务端认证通过后会把服务端的一些信息(版本号等)发送到客户端。传输结束后,会发送一个结束消息,告诉客户端可以进行访问操作数据库了。

请求使用的用户名和数据库

startup消息中,还有一些其他字段,最重要的就是用户名和数据库名。
startup指定登录的用户和数据库后,服务端会返回是否需要认证,这里举例了一种用户名密码认证的消息,还有一些其他认证的消息格式,可以参考官方文档。

StartupMessage (F)
    Int32
        以字节计的消息内容长度,包括长度自身。 

    Int32(196608)
        协议版本号。高16位是主版本号(对这里描述的协议而言是 3)。低16位是次版本号(对于这里描述的协议而言是 0)。 

    协议版本号后面跟着一个或多个参数名和值字符串的对。每一个key和value后都要家0作为分割,在最后一个名字/数值对后面还要有个0作为终止符。 参数可以以任意顺序出现。user是必须的,其它都是可选的。每个参数是这样指定的:

    String
        参数名。目前可以识别的名字是:
        user
            用于连接的数据库用户名。必须;无缺省。

        database
            要连接的数据库。缺省是用户名。 

        options
            给后端的命令行参数(这个特性已经废弃,更好的方法是设置单独的运行时参数)。 这个字符串中的空格会被当做参数的分隔符,除非用一个反斜线(\) 对它转义。写\\可表示一个而字面意义上的反斜线。 

        replication
            用于连入流复制模式,其中可以发出复制命令的一个小型集合而不是SQL语句。值可以是true、false或者database,默认值是false。详情请参考第 53.4 节。 

        除了上述参数之外,还可以列出其他参数。以_pq_.开头的参数名被保留给协议扩展之用,而其他的参数名被当做在后端开始时要设置的运行时参数。这类设置将在后端启动期间被应用(如果有命令行参数,则在解析完命令行参数之后)并且将作为会话的默认值。 

    String
        参数值。 

AuthenticationMD5Password (B)
    Byte1('R')
        标识这条消息是一个认证请求。 

    Int32(12)
        以字节计的消息内容的长度,包括长度本身。 

    Int32(5)
        指定要求一个MD5加密的口令。 

    Byte4
        加密口令的时候使用的盐粒。 

发送认证信息

认证成功后,服务端返回的消息标识与上面请求认证的口令是同一个,根据长度后面的int32来标明是认证的哪种消息。

认证成功后,还会发很多ParameterStatus消息,告诉你服务器数据库的一些配置信息,比如编码格式,时区等。

最后都会跟一条ReadyForQuery消息,标识准备好了,可以进行接下来的查询了。

PasswordMessage (F)
    Byte1('p')
        标识该消息是一个口令响应。注意这也被用于GSSAPI、SSPI以及SASL响应消息。确切的消息类型可以从上下文推出。 

    Int32
        以字节计的消息内容的长度,包括长度本身。 

    String
        口令(如果要求了,就是加密后的)。


AuthenticationOk (B)
    Byte1('R')
        标识该消息是一条认证请求。 

    Int32(8)
        以字节计的消息内容长度,包括这个长度本身。 

    Int32(0)
        指定该认证是成功的。 

ParameterStatus (B)
    Byte1('S')
        标识这条消息是一个运行时参数的状态报告。 

    Int32
        以字节计的消息内容的长度,包括长度自身。 

    String
        被报告的运行时参数的名字。 

    String
        参数的当前值。 

ReadyForQuery (B)
    Byte1('Z')
        标识消息的类型。在后端为新的查询周期准备好的时候,总会发送 ReadyForQuery。 

    Int32(5)
        以字节计的消息内容的长度,包括长度本身。 

    Byte1
        当前后端事务状态指示器。可能的值是空闲状况下的'I'(不在事务块里);在事务块里是'T'; 或者在一个失败的事务块里是'E'(在事务块结束之前,任何查询都将被拒绝)。 

常规查询

Query (F)
    Byte1('Q')
        标识该消息是一个简单查询。 

    Int32
        以字节计的消息内容的长度,包括长度自身。 

    String
        查询字符串自身。sql语句等都是以文本的形式保存在这里。

select语句返回消息

一个介绍返回字段类型和名称的消息
RowDescription (B)
    Byte1('T')
        标识该消息是一个行描述。 

    Int32
        以字节计的消息内容的长度,包括长度自身。 

    Int16
        指定在一个行里面的域的数目(可以为零)。 

    然后对于每个字段,有下面的东西:
    String
        字段名字。0结尾

    Int32
        如果域可以被标识为一个指定表的列,这里就是表的对象ID;否则就是零。 

    Int16
        如果该域可以被标识为一个指定表的列,这里就是该列的属性号;否则就是零。 

    Int32
        域数据类型的对象ID。 

    Int16
        数据类型尺寸(参阅pg_type.typlen)。请注意负值表示变宽类型。 

    Int32
        类型修饰词(参阅pg_attribute.atttypmod)。 修饰词的含义是类型相关的。 

    Int16
        用于该域的格式码。目前会是零(文本)或者一(二进制)。 在Describe语句的变体返回的RowDescription里,格式码还是未知的,因此总是零。 


多条返回数据
DataRow (B)
    Byte1('D')
        标识这个消息是一个数据行。 

    Int32
        以字节计的消息内容的长度,包括长度自身。 

    Int16
        后面跟着的列值的个数(可能是零)。 

    然后,为每个列都会出现下面的域对:
    Int32
        列值的长度,以字节计(这个长度不包括它自己)。可以为零。一个特殊的情况是,-1表示一个NULL的域值。 如果是NULL的情况则后面不会跟着值字节。 

    Byten
        一个列的数值,以相关的格式代码指示的格式展现。n是上文的长度。 


一条传输结束的消息
CommandComplete (B)
    Byte1('C')
        标识此消息是一个命令结束响应。 

    Int32
        以字节计的消息内容的长度,包括长度本身。 

    String
        命令标记。它通常是一个单字,标识被完成的SQL命令。
        对于INSERT命令,该标记是INSERT oid rows, 其中rows是已被插入的行数。oid是在rows为 1并且目标表有OID时已插入行的对象ID;否则oid就是 0。
        对于DELETE命令,该标记是DELETE rows, 其中rows是已被删除的行数。
        对于UPDATE命令,该标记是UPDATE rows,其中rows是已被更新的行数。
        对于SELECT或CREATE TABLE AS命令,该标记是SELECT rows,其中rows是被检索的行数。
        对于MOVE命令,该标记是MOVE rows,其中rows是游标位置被移动的行数。
        对于FETCH命令,该标记是FETCH rows,其中rows是已从游标中检索出来的行数。
        对于COPY命令,该标记是COPY rows,其中rows是已拷贝的行数(注意,行计数只在PostgreSQL 8.2及其后的版本中出现)。 


一条可以继续查询的消息
ReadyForQuery (B)
    Byte1('Z')
        标识消息的类型。在后端为新的查询周期准备好的时候,总会发送 ReadyForQuery。 

    Int32(5)
        以字节计的消息内容的长度,包括长度本身。 

    Byte1
        当前后端事务状态指示器。可能的值是空闲状况下的'I'(不在事务块里);在事务块里是'T'; 或者在一个失败的事务块里是'E'(在事务块结束之前,任何查询都将被拒绝)。 

如果数据太多,Data row会比较多,有可能一条消息无法传输完成,那么就会分多个数据包发送。

终止

Terminate (F)
    Byte1('X')
        标识本消息是一个终止。 

    Int32(4)
        以字节计的消息内容的长度,包括长度自身。这里经过查看wireshark,发现并不匹配。按定义应该长度是5,但是查看下来是4,需要确认是否包含标识符。

http://www.postgres.cn/docs/11/protocol.html
https://www.postgresql.org/docs/11/protocol.html
http://www.postgres.cn/docs/11/protocol-overview.html#PROTOCOL-FORMAT-CODES
http://www.postgres.cn/docs/11/protocol-flow.html

posted @ 2023-06-30 09:37  秋来叶黄  阅读(1004)  评论(0编辑  收藏  举报