Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于workflow 做postrgre v3 通信协议,需要注意那些事项? #766

Open
green750924 opened this issue Feb 24, 2022 · 4 comments

Comments

@green750924
Copy link

我单位数据库都是基于都是postgresql,应用workflow高性能就需要从协议层入手。但我们对于workflow 框架协议部分底层不能很熟悉,所以想得到你们的指定!

@Barenboim
Copy link
Contributor

Barenboim commented Feb 24, 2022

非常的可以啊。
基本上对照mysql的实现。 src/protocol目录下,参照MySQLMessage实现PostgresMessage,包含各种握手消息,登陆消息。
然后需要实现一个工厂函数用于创建Postgres任务,这个你可以参照src/factory/MySQLTaskImpl.cc(看nossl分支的,简单一些)。你需要实现ComplexClientTask<PostgresRequest, PostgresResponse>类,这里面实现各种登陆逻辑,这部分挺复杂的但是有MySQL 的参考。主要就是要根据不同的task seq(连接上的第几次交互)发送不同的消息。有可能还要使用到链接上下文。
最后你可能要实现事务,这个其实就是让不同的事务不要共享同一个connection。参照WFMySQLConnection。但主要的代码还是在MySQLImpl.cc里。
如果真的实现了基于workflow的Postgres client,欢迎开源。

@Barenboim
Copy link
Contributor

你可以发一下协议描述我看看。

@green750924
Copy link
Author

PostgreSQL 通信协议

简介: 我们在使用数据库服务时,通常需要使用客户端连接数据库服务端,以 PostgreSQL 为例,常用的客户端有自带的 psql,JAVA 应用的数据库驱动 JDBC,可视化工具 PgAdmin 等,这些客户端都需要遵守 PostgreSQL 的通信协议才能与之 "交流"。所谓协议,可以理解为一套信息交互规则或者规范,最为我们熟知的莫过于 TCP/IP 协议和 HTTP 协议。 ![image.p

我们在使用数据库服务时,通常需要使用客户端连接数据库服务端,以 PostgreSQL 为例,常用的客户端有自带的 psql,JAVA 应用的数据库驱动 JDBC,可视化工具 PgAdmin 等,这些客户端都需要遵守 PostgreSQL 的通信协议才能与之 "交流"。所谓协议,可以理解为一套信息交互规则或者规范,最为我们熟知的莫过于 TCP/IP 协议和 HTTP 协议。

image.png

PostgreSQL 在 TCP/IP 协议之上实现了一套基于消息的通信协议,同时,为避免客户端和服务端在同一台机器时的网络通信代价,也支持在 Unix 域套接字上使用该协议。PostgreSQL 至今共实现了三个版本的通信协议,现在普遍使用的是从 7.4 版本开始使用的 3.0 版本,其他版本的协议依然支持。一个 PostgreSQL 数据库实例同时支持所有版本的协议,具体使用那个版本取决于客户端的选择,无论选择哪个版本,客户端和服务端需要匹配,否则可能无法正常 "交流"。本文介绍 PostgreSQL 3.0 版本的通信协议。

PostgreSQL 是多进程架构,守护进程 Postmaster 为每个连接分配一个后台进程(backend),后台进程的分配是在协议处理之前进行的,每个后台进程自行负责协议的处理。在 PostgreSQL 源码或者文档中,通常认为 'backend' 和 'server' 是等价的,表示服务端;同样,'frontend' 和 'client' 是等价的,表示客户端。

协议基础

PostgreSQL 通信协议包括两个阶段: startup 阶段和常规 normal 阶段。 startup 阶段,客户端尝试创建连接并发送授权信息,如果一切正常,服务端会反馈状态信息,连接成功创建,随后进入 normal 阶段。 normal 阶段,客户端发送请求至服务端,服务端执行命令并将结果返回给客户端。客户端请求结束后,可以主动发送消息断开连接。

normal 阶段,客户端可以通过两种 "子协议" 来发送请求,分别是 simpel queryextened query。使用 simple query 时,客户端发送字符串文本请求,后端收到后立即处理并返回结果;使用 extened query 时,发送请求的过程被分为若干步骤,通常包括 Parse,Bind 和 Execute。

本节介绍通信协议的基础,包括消息格式和基本的消息流, normal 阶段的两种 "子协议" 在下一节详细介绍。

消息

消息格式

客户端和服务端所有通信都通过消息流进行。消息的第一个字节标识消息类型,随后四个字节标识消息内容的长度(该长度包括这四个字节本身),具体的消息内容由消息类型决定。

image.png

需要注意的是,客户端创建连接时,发送的第一条消息,即启动(startup)消息格式有所不同。它没有最开始的消息类型字段,以消息长度开始,随后紧跟协议版本号,然后是键值对形式的连接信息,如用户名、数据库以及其他 GUC 参数和值。

image.png

startup 消息的处理流程可以参考 [ProcessStartupPacket](https://github.com/postgres/postgres/blob/d90bd24391fdde2b73906e16052821c9e3c1ce82/src/backend/postmaster/postmaster.c#L1921)。

消息类型

PostgreSQL 目前支持如下客户端消息类型:

case 'Q':            /* simple query */
case 'P':            /* parse */
case 'B':            /* bind */
case 'E':            /* execute */
case 'F':            /* fastpath function call */
case 'C':            /* close */
case 'D':            /* describe */
case 'H':            /* flush */
case 'S':            /* sync */
case 'X':
case EOF:
case 'd':            /* copy data */
case 'c':            /* copy done */
case 'f':            /* copy fail */

服务端收到如上消息的处理流程可以参考 [PostgresMain](https://github.com/postgres/postgres/blob/bf68b79e50e3359accc85c94fa23cc03abb9350a/src/backend/tcop/postgres.c#L4277)。服务端发送给客户端的消息有如下类型(不完全):

case 'C':        /* command complete */
case 'E':        /* error return */
case 'Z':        /* backend is ready for new query */
case 'I':        /* empty query */
case '1':        /* Parse Complete */
case '2':        /* Bind Complete */
case '3':        /* Close Complete */
case 'S':        /* parameter status */
case 'K':        /* secret key data from the backend */
case 'T':        /* Row Description */
case 'n':        /* No Data */
case 't':        /* Parameter Description */
case 'D':        /* Data Row */
case 'G':        /* Start Copy In */
case 'H':        /* Start Copy Out */
case 'W':        /* Start Copy Both */
case 'd':        /* Copy Data */
case 'c':        /* Copy Done */
case 'R':        /* Authentication Request */

客户端处理如上服务端消息的流程可以参考 PostgreSQL libqp 的实现 [pqParseInput3](https://github.com/postgres/postgres/blob/c9d29775195922136c09cc980bb1b7091bf3d859/src/interfaces/libpq/fe-protocol3.c?spm=a2c6h.12873639.0.0.265b2eeerGaO34#L63)。

消息流

Startup

startup 阶段是客户端和服务端创建连接的阶段,消息流如下:

image.png

客户端首先发送 startup 消息至服务端,服务端判断是否需要授权信息,如若需要,则发送 AuthenticationRequest ,客户端随后发送密码至服务端,权限验证之后,服务端给客户端发送一些参数信息,即 ParameterStatus ,包括 server_versionclient_encodingDateStyle 等。最后,服务端发送一个 ReadyForQuery 消息,告知客户端一切就绪,可以发送请求了。至此,连接创建成功。

取消请求

startup 阶段,服务端还会给客户端发送一个 BackendKeyData 消息,该消息中包含服务端的进程 ID 和一个取消码(MyCancelKey)。如果客户端想取消当前正在执行的请求,则可以发送一个 CancelRequset 消息,该消息中包括 startup 阶段服务端提供的进程 ID 和取消码。

取消请求并不是通过当前正在处理请求的连接发送的,而是会创建一个新的连接,创建该连接发送的消息与之前创建连接的消息不同,不再发送 startup 消息,而是发送一个 CancelReqeust 消息,该消息同样没有消息类型字段。

image.png

取消请求不保证一定成功,可能服务端接收到取消请求时,当前的查询请求已经结束。取消请求只能在一定程度上加速当前查询结束,如果当前请求被取消,客户端会收到一条错误消息。

发送请求

连接创建之后,通信协议进入 normal 阶段,该阶段的大体流程是:客户端发送查询请求,服务端接收请求、处理请求并将结果返回给客户端。上文提到,该阶段有两种 "子协议",本节分别介绍这两种 "子协议" 的消息流。

Simple Query

客户端通过 Query 消息发送一个文本命令给服务端,服务端处理请求,回复查询结果。查询结果通常包括两部分内容:结构和数据。结构通过 RowDescription 消息传递,包括列名、类型 OID 和长度等;数据通过 DataRow 消息传递,每个 DataRow 消息中包含一行数据。

image.png

每个命令的结果发送完成之后,服务端会发送一条 CommandComplete 消息,表示当前命令执行完成。客户端的一条查询请求可能包含多条 SQL 命令,每个 SQL 命令执行完都会回复一条 CommandComplete 消息,查询请求执行结束后会回复一条 ReadyForQuery 消息,告知客户端可以发送新的请求。消息流如下:

image.png

注意,一个请求中的多条 SQL 命令会被当做一个事务来执行,如果有命令执行失败,整个事务都会回滚。用户可以在请求中显式添加 BEGINCOMMIT ,将一个请求划分为多个事务,避免事务全部回滚。显式添加事务控制语句的方式无法避免请求有语法错误的情况,如果请求有语法错误,整个请求都不会被执行。

ReadyForQuery 消息会反馈当前事务的执行状态,客户端可以根据事务状态做相应的处理,目前有如下三种事务状态:

'I';            /* idle --- not in transaction */
'T';            /* in transaction */
'E';            /* in failed transaction */

Extended Query

Extended Query 协议将以上 Simple Query 的处理流程分为若干步骤,每一步都由单独的服务端消息进行确认。该协议可以使用服务端的 perpared-statement 功能,即先发送一条参数化 SQL,服务端收到 SQL(Statement)之后对其进行解析、重写并保存,这里保存的 Statement 也就是所谓 Prepared-statement,可以被复用;执行 SQL 时,直接获取事先保存的 Prepared-statement 生成计划并执行,避免对同类型 SQL 重复解析和重写。

如下例, SELECT * FROM users u, logs l WHERE u.usrid=$1 AND u.usrid=l.usrid AND l.date = $2; 是一条参数化 SQL,执行 PREPARE 时,服务端对该 SQL 进行解析和重写;执行 EXECUTE 时,为 Prepared Statement 生成计划并执行。第二次执行 EXECUTE 时无需再对 SQL 进行解析和重写,直接生成计划并执行即可。PostgreSQL Prepared Statement 的具体细节可以参考[3],PostgreSQL JDBC 的相关介绍可以参考[4]。

PREPARE usrrptplan (int) AS
    SELECT * FROM users u, logs l WHERE u.usrid=$1 AND u.usrid=l.usrid
    AND l.date = $2;
EXECUTE usrrptplan(1, current_date);
EXECUTE usrrptplan(2, current_date);

可见,Extended Query 协议通过使用服务端的 Prepared Statement,提升同类 SQL 多次执行的效率。但与 Simple Query 相比,其不允许在一个请求中包含多条 SQL 命令,否则会报语法错误。

Extended Query 协议通常包括 5 个步骤,分别是 Parse,Bind,Describe,Execute 和 Sync。以下分别介绍各个阶段的处理流程。

Parse

客户端首先向服务端发送一个 Parse 消息,该消息包括参数化 SQL,参数占位符以及每个参数的类型,还可以指定 Statement 的名字,若不指定名字,即为一个 "未命名" 的 Statement,该 Statement 会在生成下一个 "未命名" Statement 时予以销毁,若指定名字,则必须在下次发送 Parse 消息前将其显式销毁。

image.png

PostgreSQL 服务端收到该消息后,调用 exec_parse_message 函数进行处理,进行语法分析、语义分析和重写,同时会创建一个 Plan Cache 的结构,用于缓存后续的执行计划。

Bind

客户端发送 Bind 消息,该消息携带具体的参数值、参数格式和返回列的格式,如下:

image.png

PostgreSQL 收到该消息后,调用 exec_bind_message 函数进行处理。为之前保存的 Prepared Statement 创建执行计划并将其保存在 Plan Cache 中,创建一个 Portal 用于后续执行。关于 Plan Cache 的具体实现和复用逻辑在此不细述,以后单独撰文介绍。

在 PostgreSQL 内核中,Portal 是对查询执行状态的一种抽象,该结构贯穿执行器运行的始终。

Describe

客户端可以发送 Describe 消息获取 Statment 或 Portal 的元信息,即返回结果的列名,类型等信息,这些信息由 RowDescription 消息携带。如果请求获取 Statement 的元信息,还会返回具体的参数信息,由 ParameterDescription 消息携带。

image.png

Execute

客户端发送 Execute 消息告知服务端执行请求,服务端收到消息后,执行 Bind 阶段创建的 Portal,执行结果通过 DataRow 消息返回给客户端,执行完成后发送 CommandComplete

image.png

Execute 消息中可以指定返回的行数,若行数为 0,表示返回所有行。

Sync

使用 Extended Query 协议时,一个请求总是以 Sync 消息结束,服务端接收到 Sync 消息后,关闭隐式开启的事务并回复 ReadyForQuery 消息。

Extended Query 完整的消息流如下:

image.png

Copy 子协议

为高效地导入/导出数据,PostgreSQL 支持 COPY 命令, COPY 操作会将当前连接切换至一种截然不同的子协议。

Copy 子协议对应三种模式:

  • copy-in 导入数据,对应命令 COPY FROM STDIN
  • copy-out 导出数据,对应命令 COPY TO STDOUT
  • copy-both 用于 walsender,在主备间批量传输数据

copy-in 为例,服务端收到 COPY 命令后,进入 COPY 模式,并回复 CopyInResponse。随后客户端通过 CopyData 消息传输数据,CopyComplete 消息标识数据传输完成,服务端收到该消息后,发送 CommandCompleteReadyForQuery 消息,消息流如下:

image.png

总结

本文简要介绍了 PostgreSQL 的通信协议,包括消息格式、消息类型和常见通信过程的消息流。一般通信过程分为两个阶段: startup 阶段创建连接, normal 阶段发送请求并返回结果。 normal 阶段又包括两种子协议, Simple Query 一次性发送查询请求; Extended Query 分阶段发送请求,利用服务端的 prepared statement 特性,提升反复执行同类请求的效率。

PostgreSQL 通信协议中,除本文介绍的 COPY 子协议,还有一些其他的子协议,如主备流复制子协议,限于篇幅,本文并未给出详尽的描述,感兴趣的同学可以参考相关文档[5]。

最后,本文严重参考了 2014 年 PG 大会这篇[6]分享,推荐大家阅读。

参考文献

  1. https://www.net.t-labs.tu-berlin.de/teaching/computer_networking/01.02.htm
  2. [https://www.postgresql.org/docs/current/protocol.html](https://www.postgresql.org/docs/current/protocol.html?spm=a2c6h.12873639.0.0.265b2eeerGaO34)
  3. https://www.postgresql.org/docs/12/sql-prepare.html
  4. https://jdbc.postgresql.org/documentation/head/server-prepare.html
  5. https://www.postgresql.org/docs/current/protocol-replication.html
  6. https://www.pgcon.org/2014/schedule/attachments/330_postgres-for-the-wire.pdf

本节描述每个消息报文的具体格式。报文上标注了发送方:前端(F)、后端(B)、双方(F&B)。注意,尽管每个报文在开始处都包含一个字节数,消息格式的结束可以不在字节数引用中。其目标是合法性检测。(CopyData报文是个例外,因为他的格式部分与数据流;任何非法CopyData数据都无法被中断)。

AuthenticationOK (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32(8) :内容长度,包含自身
  3. Int32(0) :制定认证成功了

AuthenticationKerberosV5 (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32(8) :内容长度,包含自身
  3. Int32(2) :指定Kerberos V5认证需求

AuthenticationCleartextPassword (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32(8) :内容长度,包含自身
  3. Int32(3) :指定需要clear-text密码

AuthenticationCryptPassword (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32(10) :内容长度,包含自身
  3. Int32(4) :指定需要crypt()加密过的密码
  4. Byte2 :加密时所需salt

AuthenticationMD5Password (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32(12) :内容长度,包含自身
  3. Int32(5) :指定需要MD5加密的密码
  4. Byte4 :加密时用到的salt

AuthenticationSCMCredential (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32(8) :内容长度,包含自身
  3. Int32(6) :标志使用SCM认证

AuthenticationGSS (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32(8) :内容长度,包含自身
  3. Int32(7) :标志使用GSS API认证

AuthenticationSSPI (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32(8) :内容长度,包含自身
  3. Int32(9) :标志使用SSPI认证

AuthenticationGSSContinue (B)

  1. Byte1('R') :标志报文为认证请求
  2. Int32 :内容长度,包含自身
  3. Int32(8) :指定报文包含GSSAPI或SSPI数据
  4. Byten :GSSAPI或SSPI认证数据

BackendKeyData (B)

  1. Byte1('K') :标志报文为取消关键数据,前端必须保存这些值,以便用于之后的CancelRequest报文
  2. Int32(12) :内容长度,包含自身
  3. Int32 :后端processID
  4. Int32 :后端的密钥

Bind (F)

  1. Byte1('B') :标志报文为Bind命令
  2. Int32 :内容长度,包含自身
  3. String :目标portal的名字(空字符串表示未命名)
  4. String :源prepared statement的名字(空字符串表示未命名)
  5. Int16 :随后跟着的参数格式码(就是下面的C),0表示没参数或使用文本格式;1表示二进制格式;或者等于参数个数
  6. Int16[C] :参数格式码,每个都必须取值0(文本)或1(二进制)
  7. Int16 :跟随的参数值(可以是0)。必须匹配查询时参数个数

随后是每个参数的字段对:

  1. Int32 :参数值的长度(字节,不包含自身),可以是0,-1表示NULL,在NULL之后没有跟随的字节
  2. Byten :参数值,由之前指定的格式,n是上面的长度

在最后一个参数后的字段:

  1. Int16 :返回列的格式码(下面的R),可以是0表示无结果或文本格式,1表示二进制格式,或者等于结果列的数量
  2. Int16[R] :结果列格式码,每个都必须取值0(文本)或1(二进制)

BindComplete (B)

  1. Byte1('2') :标志报文为绑定完成
  2. Int32(4) :内容长度,包含自身

CancelRequest (F)

  1. Int32(16) :内容长度,包含自身
  2. Int32(80877102) :取消请求的代码,值取自1234*0xffff+5678,为了避免混乱,这个代码必须不能与协议版本号相同
  3. Int32 :目标后端进程ID
  4. Int32 :目标后端密钥

Close (F)

  1. Byte1('C') :标志关闭命令报文
  2. Int32 :内容长度,包含自身
  3. Byte1 :'S'关闭prepared statement;'P'关闭portal
  4. String :要关闭的prepared statement或portal的名字,空字符串选择未命名的

CloseComplete (B)

  1. Byte1('3') :标志关闭完成报文
  2. Int32(4) :内容长度,包含自身

CommandComplete (B)

  1. Byte1('C') :标志命令完成响应

  2. Int32 :内容长度,包含自身

  3. String :命令标签(tag)。通常是一个单词表示刚完成的SQL命令:

    1. 对INSERT命令,标签是 INSERT oid rows 参数rows是插入行数,oid是当插入1行时的对象ID,否则oid为0
    2. 对DELETE命令,标签是 DELETE rows ,rows是删除的行数
    3. 对UPDATE命令,标签是 UPDATE rows ,rows是更新的行数
    4. 对MOVE命令,标签是 MOVE rows ,rows是游标移动行数
    5. 对FETCH命令,标签是 FETCH rows ,rows是获取的行数
    6. 对COPY命令,标签是 COPY rows ,rows是拷贝的行数(注意行数仅在PostgreSQL 8.2以后有显示)
    7. CREATE TABLE

CopyData (F&B)

  1. Byte1('d') :标志COPY数据报文
  2. Int32 :内容长度,包含自身
  3. Byten :COPY数据流。从后端发来的总是表示单行数据,但是前端发来的可能被无意义的分割

CopyDone (F&B)

  1. Byte1('c') :标志COPY完成报文
  2. Int32(4) :内容长度,包含自身

CopyFail (F)

  1. Byte1('f') :标志COPY失败报文
  2. Int32 :内容长度,包含自身
  3. String :错误信息描述失败原因

CopyInResponse (B)

  1. Byte1('G') :标志开始拷贝入响应,前端必须立即发送拷贝进入数据(如果没有准备好,就发送CopyFail报文)
  2. Int32 :内容长度,包含自身
  3. Int8 :0表示全部COPY格式是文本(行以新行分隔,列以分隔符分隔),1表示所有拷贝数据格式是二进制(类似于DataRow格式)
  4. Int16 :拷贝进来的列数量,其数字是下面的N
  5. Int16[N] :每个列的格式码,每个必须是0(文本)或1(二进制)。如果所有拷贝格式是文本则必须全部是0

CopyOutResponse (B)

  1. Byte1('H') :标志开始拷贝出响应,这个报文必须跟随着拷贝出的数据
  2. Int32 :内容长度,包含自身
  3. Int8 :0表示文本格式(行以新行分隔,列以分隔符分隔),1表示二进制格式(类似于DataRow格式),查看COPY命令了解更多
  4. Int16 :列的数量(下面的N)
  5. Int16[N] :每个列的格式码,必须取值0或1,分别表示文本和二进制

DataRow (B)

  1. Byte1('D') :标志数据行报文
  2. Int32 :内容长度,包含自身
  3. Int16 :随后列的数值(可能是0)

接下来的是每一列的字段对:

  1. Int32 :列值的长度,单位是字节(不包含自身),可以为0,特殊情况-1表示NULL值,且NULL时没有跟随的值字节串
  2. Byten :列的值,有如格式码描述的,n是如上长度

Describe (F)

  1. Byte1('D') :标志描述命令报文
  2. Int32 :内容长度,包含自身
  3. Byte1 :'S'表示prepared statement;'P'表示portal
  4. String :prepared statement或portal的名字,空字符串选择未命名的

EmptyQueryResponse (B)

  1. Byte1('I') :标志查询结果为空的响应,作为CommandComplete的替代(substitute)
  2. Int32(4) :内容长度,包含自身

ErrorResponse (B)

  1. Byte1('E') :标志错误信息报文
  2. Int32 :内容长度,包含自身

消息体由一个或多个字段组成,以null结尾,字段可以任何顺序出现,对每个字段应该有:

  1. Byte1 :代码用以标志字段类型;如果0则表示消息结束符,后面没字符串了。描述定义字段类型键45.5节。更多字段类型可能在未来添加进来,前端应该无声的忽略不识别的类型
  2. String :字段值

Execute (F)

  1. Byte1('E') :标志为执行命令
  2. Int32 :内容长度,包含自身
  3. String :portal的名字,空字符串选择未命名portal
  4. Int32 :最大允许返回行数量,0表示无限制

Flush (F)

  1. Byte1('H') :标志清空命令
  2. Int32(4) :内容长度,包含自身

FunctionCall (F)

  1. Byte1('F') :标志是函数调用报文
  2. Int32 :内容长度,包含自身
  3. Int32 :指定函数的对象ID
  4. Int16 :参数格式码(下面的C),0表示无参数或文本,1表示二进制,也可以是实际参数个数
  5. Int16[C] :参数格式码,必须取值0或1,表示文本或二进制
  6. Int16 :指定提供给函数的参数个数

下面是每个参数的字段对:

  1. Int32 :参数值长度(字节,不包含自身),可以是0,-1表示NULL,NULL没有跟随的字节
  2. Byten :参数值,按照上面给定格式码,长度为上面字段n

在这些参数后,跟随的字段:

  1. 函数结果的格式码,必须取值0或1

FunctionCallResponse (B)

  1. Byte1('V') :标志报文是函数调用结果
  2. Int32 :内容长度,包含自身
  3. Int32 :函数结果值长度(字节,不包含自身),可以是0,-1表示NULL,NULL后无跟随字节
  4. Byten :函数结果值,按照上面给定的格式码和长度

NoData (B)

  1. Byte1('n') :标志无数据
  2. Int32(4) :内容长度,包含自身

NoticeResponse (B)

  1. Byte1('N') :标志通知报文
  2. Int32 :内容长度,包含自身

消息体由一个或多个字段构成,跟随0表示结束符。字段可以用任何顺序显示,每个字段为:

  1. Byte1 :代码表示字段类型;如果0则为消息结束符。字段类型定义见45.5节。未来可能添加更多字段类型,前端应该无声的忽略不识别的字段
  2. String :字段值

NotificationResponse (B)

  1. Byte1('A') :标志通知响应报文
  2. Int32 :内容长度,包含自身
  3. Int32 :通知的后端进程ID
  4. String :抛出通知的条件(condition)名字
  5. String :通知附加信息(当前尚未实现,所以该字段总是空字符串)

ParameterDescription (B)

  1. Byte1('t') :标志参数描述报文
  2. Int32 :内容长度,包含自身
  3. Int16 :语句使用的参数数量(可以是0)

下面对每个参数:

  1. Int32 :指定参数数据类型的对象ID

ParameterStatus (B)

  1. Byte1('S') :标志运行时参数状态报告
  2. Int32 :内容长度,包含自身
  3. String :运行时参数名字
  4. String :参数值

Parse (F)

  1. Byte1('P') :标志Parse命令报文
  2. Int32 :内容长度,包含自身
  3. String :目标prepared statement的名字(空字符串选择未命名的)
  4. String :要解析的查询字符串
  5. Int16 :参数数据类型数字(可以是0)。注意这里不是参数在查询字符串中的占位符,只有前端想要预备的类型

然后对每个参数:

  1. Int32 :指定参数数据类型的对象ID。防止0等同于让类型不指定

ParseComplete (B)

  1. Byte1('1') :标志解析完成报文
  2. Int32(4) :内容长度,包含自身

PasswordMessage (F)

  1. Byte1('p') :标志密码响应,同时用于GSSAPI和SSPI的响应(这是个设计错误,因为包含数据不是以null结束的,可以是任意二进制数据)
  2. Int32 :内容产度,包含自身
  3. String :密码(按照请求不同,可能是加密的)

PortalSuspended (B)

  1. Byte1('s') :标志portal挂起报文,注意只在Execute报文的行数限制到达时会显示
  2. Int32(4) :内容长度,包含自身

Query (F)

  1. Byte1('Q') :定义这是个简单查询
  2. Int32 :内容长度,包含自身
  3. String :查询字符串

ReadyForQuery (B)

  1. Byte1('Z') :标志报文为后端准备好接收新的查询
  2. Int32(5) :内容长度,包含自身
  3. Byte1 :当前后端事务状态码。"I"表示空闲(没有在事务中)、"T"表示在事务中;"E"表示在失败的事务中(事务块结束前查询都回被驳回)

RowDescription (B)

  1. Byte1('T') :标志报文为行描述
  2. Int32 :内容长度,包含自身
  3. Int16 :指定字段数量(可以为0)

然后就是各个字段了

  1. String :字段名,注意有结尾的NULL
  2. Int32 :如果字段是特定表的列,则是表格的对象ID,否则为0
  3. Int16 :如果字段是特定表的列,则是列的属性名,否则为0
  4. Int32 :字段数据类型对象ID,详见下面的实践笔记
  5. Int16 :数据类型大小(查看pg_type.typlen),注意负数表示变长类型,-1为varlena,-2为NULL结尾C字符串,其他为固定长度
  6. Int32 :类型修饰符(查看pg_attribute.atttypmod),修饰符是类型相关的
  7. Int16 :字段的格式码,当前0为文本,1为二进制。如果是从Describe的语句变量返回的RowDescription,则格式码未知,总是返回0

SSLRequest (F)

  1. Int32(8) :内容长度,包含自身
  2. Int32(80877103) :SSL请求码,值来自1234*0xffff+5679。为了避免混乱,这个代码必须不能与协议版本号相同

StartupMessage (F)

  1. Int32 :内容长度,包含自身
  2. Int32(196608) :协议版本号,最重要的是16bit的主版本号3。随后是次版本号0。196608=0x00030000。

协议版本号之后就是多个键值对,最后一个键值对结尾要有null字符,键值对参数可以用任何顺序出现, user 是必须的,其他都可选。

  1. String :参数名,当前可以被识别的键名包括:user(数据库用户名)、database(要连接的数据库,默认是用户名)、options(后端命令行选项)。
  2. String :参数值

Sync (F)

  1. Byte1('S') :标示同步命令
  2. Int32(4) :内容长度,包含自身

Terminate (F)

  1. Byte1('X') :标志终止命令
  2. Int32(4) :内容长度,包含自身

5 错误与提示信息字段

[Error and Notice Message Fields](https://link.zhihu.com/?target=http%3A//www.postgresql.org/docs/8.3/static/protocol-error-fields.html)

本节描述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) :发生错误的源码常规

客户端选择如何显示这些信息,一般至少应该对较长的行来折行,以及分段。

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](https://link.zhihu.com/?target=http%3A//www.postgresql.org/docs/8.3/static/errcodes-appendix.html)

SQLSTATE与错误码: [Documentation: 8.3: Error Handling](https://link.zhihu.com/?target=http%3A//www.postgresql.org/docs/8.3/static/ecpg-errors.html)

几个常用的:

  1. 00000 :成功
  2. 01000 :警告
  3. 02000 :无数据
  4. 03000 :SQL语句不完整
  5. 08000 :连接异常
  6. 0A000 :不支持的功能
  7. 22000 :数据异常
  8. 42000 :语法错误
  9. 54000 :程序范围限制
  10. 54001 :语句太复杂
  11. 54011 :太多的列
  12. 54023 :太多参数
  13. 58030 :I/O错误
  14. P0000 :PLPGSQL ERROR
  15. P0001 :抛出异常
  16. P0002 :没找到数据
  17. P0003 :太多行
  18. XX000 :内部错误
  19. XX001 :数据被误用
  20. XX002 :索引被误用

7.5 结果集的定义

使用RowDescription包。

一个结果集定义 \x00\x01name\x00\x00\x00@\x03\x00\x02\x00\x00\x04\x13\xff\xff\x00\x00\x00D\x00\x00 。其中字段定义如下:

  1. 字段数16bit:1个
  2. 字段名: name\x00
  3. 特定表的表格对象ID: \x00\x00@\x03
  4. 特定表的列属性对象ID: \x00\x02
  5. 字段数据类型对象ID: \x00\x00\x04\x13
  6. 数据类型长度: \xff\xff
  7. 类型修饰符: \x00\x00\x00D
  8. 字段格式码: \x00\x00

实际这是一个VARCHAR类型的字段。即类型代码1043对应varchar类型。具体的类型映射表可以进入PGSQL自己查询,使用 SELECT typname,oid FROM pg_type; ,我使用8.3.11,其中有269个返回结果。列出几个常用的:

  1. bool=16
  2. bytea=17
  3. char=18
  4. name=19
  5. int8=20
  6. int4=23
  7. text=25
  8. xml=142
  9. float4=700
  10. float8=701
  11. abstime=702
  12. reltime=703
  13. inet=869
  14. varchar=1043
  15. date=1082
  16. time=1083
  17. timestamp=1114
  18. numeric=1700
  19. uuid=2950
  20. record=2249
  21. cstring=2275
  22. trigger=2279
  23. parameters=11393

@Barenboim
Copy link
Contributor

Barenboim commented Feb 25, 2022

大概看了一下,Postgre的消息比mysql实在友好太多了。整个登录阶段可以一次交互搞定。
Client发送第一个包之后,server回复的所有数据(auth ok或auth request以及后面的parameter status)都可以算作对client启动包的回复。其中,如果server发出auth request,client需要发送一个密码信息,这个通过CommMessageIn的feedback接口同步发就可以了,不用增加一次交互。
你从我们的tutorial-10看起,先看看怎么实现一个简单自定义协议,然后看看redis和mysql的实现大概就可以做了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants