# 背景知识
首先要知道,pg8000是一个客户端,
它会与postgre服务器端的程序进行通信,
发送一些数据给服务器,并接收服务器的返回。
比如发送用户名、密码等用来认证的信息,或者是发送sql语句等等。
接收的是服务器返回的认证成功或者认证失败的信息,或者是sql语句的执行结果。
然后呢,通信本身是通过socket来进行的,
python里面对socket的操作与对普通文件的操作类似,就是读取和写入,也就是代码里面随处可见的self._write, self._read。
pg8000对socket进行写入,则这些数据会发送到服务器端 。
pg8000对socket进行读取,会读取到服务器端返回回来的数据。
但是由于文件本身是有缓冲的,写入之后并不会立刻发送到服务端,而是等着缓冲区满了才会发(由操作系统决定,具体算法不明),
所以在代码中一般self._write之后,会主动调用self._flush()刷新缓冲区,这样数据就会立刻发送到服务端了。
既然是通信, 就需要约定一套通信的协议.
官方文档:
https://www.postgresql.org/docs/current/protocol.html
All communication is through a stream of messages。
就像发短信一样,pg8000发一个或多个message给服务器端, 服务器端回复一个或多个message.
message的第一个字节标识消息类型,从core.py1000行左右可以看到有很多种类的message。
比如第一个字节为Z的 message: READY_FOR_QUERY = b"Z"
message在第一个字节之后,后面跟着的四个字节声明message其余部分的长度
(这个长度包括长度域自身,但是不包括消息类型的一个字节),
再后面就是message本身的内容(data部分)了。
1030行的这个方法就是在创建一个message:
def create_message(code, data=b''):
return code + i_pack(len(data) + 4) + data
可以很明显看到一个message是由上述的三部分组成的:
第一部分code就是消息类型, 第三部分data就是数据本身
len(data) + 4 就是数据本身的长度再加上4, 4表示的是长度域的长度.
i_pack()使它从数字变成了二进制数据, 做为message的第二个部分:长度域。
# 主流程
## 1. connect
首先调用的是__init__.py中的connect()方法,43行,
在代码中实例化了一个Connection对象。
既然是实例化,那么就会调用Connection类里面的 __init__方法 . core文件1093行。
主要工作是:
1) 创建socket并与服务端建立连接(如果已经存在socket连接,就不建立新的连接了,比如在代码里面写了多次connect, 那么也只会创建一个连接)
2) 与服务端进行start-up通信
https://www.postgresql.org/docs/current/protocol-flow.html#id-1.10.5.7.3
发送启动数据message给服务端,1425行附近 。
message中包含了协议版本:196608,
以及1112行定义的init_params(里面有数据库名和用户名,application_name不理解、replication不理解)
这个时候message的创建并没有调用create_message方法,是因为这个启动message格式比较特殊,它的第一部分不是code(消息类型),而是协议版本号。
服务端收到此message之后,会发回多条message.
因此代码中使用了一个while循环进行处理. 1431行
1432行从消息中读取5个字节,第一个字节是code,表示消息类型,后面4个字节表示消息长度,
可见服务端发送过来的message与客户端发送的message格式都是一样的。
这些message里面有些需要pg8000进行回复,有些不需要回复。
message的处理函数就定义在self.message_types中,它的key就是code(消息类型), value是具体的处理函数
在while循环里面加个print(),可以看到服务端一共发回来了十几条message:
b'R' 表示 AUTHENTICATION_REQUEST , 处理函数是 self.handle_AUTHENTICATION_REQUEST
这个message的data部分为 5(1613行 1623行),
表示需要对用户输入的密码进行md5加密,然后再传输到服务端 。
加密的过程暂时看不太懂, 在加密后,
1642行通过调用_send_message方法发送到了一个code为p的message给服务端,data部分就是加密后的密码.
b'R' 表示 AUTHENTICATION_REQUEST
这个message的data部分为0,表示服务端认为密码验证ok.
b'S' 表示 PARAMETER_STATUS 处理函数是self.handle_PARAMETER_STATUS
服务端发送过来的是服务端里面的一些参数配置,比如字符集啦,时区、服务端版本号之类的,
每个message只发送一个参数配置, 所以下面还有好多个code为S的message。
b'S'
b'S'
b'S'
b'S'
b'S'
b'S'
b'S'
b'S'
b'S'
b'S'
b'K' 表示 BACKEND_KEY_DATA 处理函数是
self.handle_BACKEND_KEY_DATA, 没做什么处理逻辑。
b'Z' 表示 READY_FOR_QUERY 处理函数是 self.handle_READY_FOR_QUERY
表示start-up流程的结束.
## 2. run sql语句
这里先要理解一个叫做 prepared statements的概念。
如果是以很naive的想法来看的话,客户端把sql语句传给服务端,服务端解析并优化sql语句,运行sql语句,返回结果给客户端,应该是这种流程,
但是很多情况下,我们的一条sql语句可能会反复执行,
或者每次执行的时候只有个别的值不同(比如select语句的where子句值不同, update的set子句值不同, insert的value值不同)。
如果每次服务端都需要经过sql解析优化的过程,其实是进行了重复的劳动(这些sql语句的解析优化过程是完全相同的),
而且解析并优化的这个过程其实挺耗时的。
为了对其进行优化,大神们就想出来了一种叫做prepared statements的优化技术:
客户端 将这类sql语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化.
比如:con.run("INSERT INTO book (title) VALUES (:title)", title='aaa')
:title这一部分就是一个占位符(placeholder)
服务端在解析并优化这个sql语句模板(以及模板参数)之后,会把它保存在内存中,下次再碰到相同的sql语句,就省去了解析优化的过程。
还需要理解事务(transaction)的概念:
postgre中的默认是开启事务的,如果需要执行一条sql语句,需要先执行 "begin transaction"语句,表示事务开始执行,
然后执行sql语句(可以是多条),
最后执行commit完成数据的写入,也可以执行rollback语句回滚前面sql语句的操作。
继续看代码,
方法run(self, sql, stream=None, **params)位于1555行附近,
sql参数就是sql语句(带placeholder的),stream意义不明, params就是模板参数。
比如:con.run("INSERT INTO book (title) VALUES (:title)", title='aaa')
params就是 title='aaa' 这部分,本质上是一个字典: {"title": 'aaa'}。
run()方法调用的是cursor的execute方法(833行),
cursor的execute方法先调用Connection的execute方法执行了 begin transaction,
然后才调用Connection的execute方法执行真正的sql语句。
那么再看1761行的Connection的execute方法:
def execute(self, cursor, operation, vals):
参数operation就是sql模板语句, vals是模板参数{'title': 'aaa'}
1765行 pid = getpid() 得到当前进程的id,也可能是当前线程的id(如果是多线程的话)
Connection 类里面有一个变量: self._caches[cursor.paramstyle][pid]
其实就是一个多维的字典,用来存放程序运行中的某些数据。
_caches的前两个维度 cursor.paramstyle是固定的值(named), pid是进程id,
看代码的时候可以忽略这2个维度,直接从后面的维度看起:‘
可以认为_caches的结构为: {'statement': {}, 'ps': {}}
里面就2个元素,一个用来保存statement相关的数据,另外一个保存ps(也就是prepared statement)相关的数据。
下面填充statement部分的数据:
'statement' 也是一个字典,其结构为:
key: sql语句
value: 一个tuple ,这个tuple是由 convert_paramstyle()方法返回的。
convert_paramstyle做了什么呢? 以下面这个sql语句为例:
con.run("INSERT INTO book (title) VALUES (:title)", title='aaa')
其实就是把sql语句转成了
INSERT INTO book (title) VALUES ($1)"
把每个placeholder换成了$1 $2 $3 这样...
为什么要这样转化呢? 因为是服务端的要求,服务端只能理解$1 $2 $3这样的占位符,
理解不了 :title 这种类型的占位符
那为什么不一开始就让用户把sql语句写成$1 $2 $3的格式呢? 可能是怕占位符太多的时候,用户会混乱吧。
然后convert_paramstyle返回了这个转化之后的sql语句。
还额外返回了个function: make_args() 后面会用到。
所以程序执行到这里的时候, _caches的结构是这样的:
{'statement':
{
'begin transaction':
('begin transaction',
<function convert_paramstyle.<locals>.make_args at 0x000001AF5A1C7B88>),
'INSERT INTO book (title) VALUES (:title)':
('INSERT INTO book (title) VALUES ($1)',
<function convert_paramstyle.<locals>.make_args at 0x000001AF5A1C7CA8>)
},
'ps':
{}
}
下面填充ps部分的数据, ps也是个字典:
首先 1785行make_args()把模板参数 {'title': 'aaa'} 做了一下转化,转成('aaa'), 去掉了key,保留了value.
然后 1786行make_params()继续做转化, 这次转化的目的是:
模板参数 ('aaa') 它其实是一种python的数据结构(比如dict, list, datetime等等,在这个例子中'aaa'就是str字符串),
如果直接发给服务端,服务端是无法理解的...
所以需要转化成服务端可以接受的数据格式。
在1352定义一个self.py_types,是一个字典,用来做数据格式的转化:
key为python中的数据结构,value分为三个部分,比如:
str: (705, FC_TEXT, text_out),
其中,key为str, 意义为python中的str类型
value为 (705, FC_TEXT, text_out) 由三部分组成:
705是postgre中的oid(数据类型的id?)
FC_TEXT表示经过处理后,会成为文本数据
(与之相对的是二进制数据,
在message中只能发送文本数据或者二进制数据,
然后postgre会把接收到的数据再转成oid类型的数据
)
text_out是转化函数(python数据格式 -> postgre数据格式)
其实1786行并没有真正把模板参数进行数据上的转化,它只是得到了每个模板参数对应的py_types数据:
在本例中为: ((705, 0, <function Connection.__init__.<locals>.text_out at 0x000002592B5091F8>),)
然后把原始的sql语句和上面的这个模板参数的py_types数据组合起来做为 ps的key.
那ps的value呢?
就是1806行定义的这个字典:
ps = {
'statement_name_bin': statement_name_bin,
'pid': pid,
'statement_num': statement_num,
'row_desc': [],
'param_funcs': tuple(x[2] for x in params)}
其中pid就是进程id, statement_num是一个序号,从1开始递增,第一条sql的序号是1, 第二条 sql的序号是2 ...
row_desc是一个数组,暂时为空,后面用来保存服务端返回的数据。
param_funcs 就是上面 py_types数据里面的最后的那个function. 比如text_out.
statement_name_bin的结构由四部分组成:
第一部分是字符串pg8000
第二部分是字符串statement
第三部分是pid 进程id
第四部分是是statement_num序号
举例: pg8000_statement_11432_2
1824行准备与服务端通信:
1) 1837行发送 PARSE message给服务端,目的是让服务端创建一个prepared statement。
本例中发送的数据为:b'pg8000_statement_13060_1\x00INSERT INTO book (title) VALUES ($1)\x00\x00\x01\x00\x00\x02\xc1'
由几部分组成吧: 第一部分是statement_name_bin, 后面是sql模板语句($1 $2那种),
再后面的2个字节是\x00\x01,16进制,值为1,表示只有一个模板参数,最后的4个字节:\x00\x00\x02\xc1也是16进制的,转成10进制就是705,也就是这个模板参数的oid
2) 1838行发送 DESCRIBE message给服务端, data以S开头,表示后面是一个 statement; S后面是statement_name_bin
感觉与PARSE message发送的数据有些重复了...
此message的目的是希望服务端返回数据表中每个字段的meta信息(如果是select语句的话)
3) 1839行发送 SYNC message 给服务端,表示一条sql语句(模板部分)发送完毕了。
4) 1849行 调用 handle_messages() 处理服务端的返回
仍然是根据返回值中的code从 self.message_types 中选择合适的处理函数。
在本例中,
服务端返回了4个message:
b'1' PARSE_COMPLETE 处理方法为 self.handle_PARSE_COMPLETE 其实啥也没做
b't' PARAMETER_DESCRIPTION ...也没处理
b'n' NO_DATA 无需 处理
b'Z' READY_FOR_QUERY 处理方法为 handle_READY_FOR_QUERY,
它会判断message中的data是否为IDLE, 为IDLE的话表示目前服务器端不处理于事务中
如果是一条select语句的话,服务端不会返回NO_DATA, 而是返回
b'T' ROW_DESCRIPTION
处理方法为handle_ROW_DESCRIPTION,在此方法中,从服务端的返回数据中解析出数据表每个字段的meta信息:如:
表中的id字段:
{'table_oid': 16802, 'column_attrnum': 1, 'type_oid': 23, 'type_size': 4, 'type_modifier': -1, 'format': 0, 'name': b'id'}
表中的title字段:
{'table_oid': 16802, 'column_attrnum': 2, 'type_oid': 25, 'type_size': -1, 'type_modifier': -1, 'format': 0, 'name': b'title'}
其中name就是字段名,type_oid是此字段在postgre中的数据格式。
这些表字段相关的meta信息将被填充到 ps的 row_desc数组中。
同时,还有一些信息会被填充到row_desc中:
那就是根据数据表每个字段的type_oid,在字典中self.pg_types中查询,查询得到这个字段是文本还是二进制,以及对应的转化函数(postgre数据格式 -> python数据格式)
然后填充ps的bind_1(statement_name_bin以及每个模板参数的python数据类型)和bind_2(每个返回字段的postgre数据类型)字典。
最后把ps填充到_caches里面。
所以程序执行到这个时候, _caches的结构是这样的:
{
'statement':
{
'begin transaction':
('begin transaction',
<function convert_paramstyle.<locals>.make_args at 0x000002A3F4B27B88>),
'INSERT INTO book (title) VALUES (:title)':
('INSERT INTO book (title) VALUES ($1)',
<function convert_paramstyle.<locals>.make_args at 0x000002A3F4B27CA8>),
'SELECT * FROM book':
('SELECT * FROM book',
<function convert_paramstyle.<locals>.make_args at 0x000002A3F4B27D38>)},
'ps':
{
('INSERT INTO book (title) VALUES (:title)', ((705, 0, <function Connection.__init__.<locals>.text_out at 0x000002A3F47B91F8>),)):
{
'statement_name_bin': b'pg8000_statement_12600_1\x00',
'pid': 12600,
'statement_num': 1,
'row_desc': [],
'param_funcs': (<function Connection.__init__.<locals>.text_out at 0x000002A3F47B91F8>,),
'input_funcs': (),
'bind_1': b'\x00pg8000_statement_12600_1\x00\x00\x01\x00\x00\x00\x01',
'bind_2': b'\x00\x00'
},
('SELECT * FROM book', ()):
{
'statement_name_bin': b'pg8000_statement_12600_2\x00',
'pid': 12600,
'statement_num': 2,
'row_desc': [
{
'table_oid': 16847,
'column_attrnum': 1,
'type_oid': 23,
'type_size': 4,
'type_modifier': -1,
'format': 0,
'name': b'id',
'pg8000_fc': 1,
'func': <function int4_recv at 0x000002A3F4ABECA8>
},
{
'table_oid': 16847,
'column_attrnum': 2,
'type_oid': 25,
'type_size': -1,
'type_modifier': -1,
'format': 0,
'name': b'title',
'pg8000_fc': 1,
'func': <function Connection.__init__.<locals>.text_recv at 0x000002A3F4ADE048>
}
],
'param_funcs': (),
'input_funcs': (<function int4_recv at 0x000002A3F4ABECA8>, <function Connection.__init__.<locals>.text_recv at 0x000002A3F4ADE048>),
'bind_1': b'\x00pg8000_statement_12600_2\x00\x00\x00\x00\x00',
'bind_2': b'\x00\x02\x00\x01\x00\x01'
}
}
}
为什么要填充这些数据到_caches里面呢?
主要还是为了实现prepared statements。
如果没有cache, 每次sql语句的执行都会经过上面的PARSE、DESCRIBE、SYNC的过程,
如果有了cache,碰到相同或者结构相似的sql语句,其ps的key也是相同的,那么在cache里面就可以直接找到已经填充好的ps数据,
可以省去上面的PARSE DESCRIBE和SYNC的通信。
5) 1907 行 开始调用param_funcs对模板参数进行转化,转化成postgre的数据格式
并与bind_1 bind_2拼接在一起
6) 1917行发送BIND message给服务端。 BIND表示要把模板参数绑定到某个prepared statement上面
在pg8000中,prepared statement的唯一标识就是statement_name_bin
7) 1918行发送execute message给服务端。
8) 1919行发送sync message给服务端。
9) 1921行调用handle_messages处理服务端的返回数据
以上面的insert语句为例, 服务端返回以下的几个message:
b'2' BIND_COMPLETE 未做任何的处理
b'C' COMMAND_COMPLETE 处理方法为 handle_COMMAND_COMPLETE,
从服务端的返回message中解析出row_count(插入了几行、删除了几行、返回了几行数据等等)
b'Z' READY_FOR_QUERY 处理方法为 handle_READY_FOR_QUERY,
以上面的select语句为例 ,服务端返回以下的几个message:
b'2' BIND_COMPLETE
b'D' DATA_ROW 处理函数为 handle_DATA_ROW()
从message中解析出返回的每个字段,并把postgre数据类型转成为python的数据类型.
每个字段的结构都由两个部分组成,第一部分是字段data的长度,第二部分是字段data本身。
解析出来的字段保存到 _cached_rows中。
b'D' DATA_ROW
b'C' COMMAND_COMPLETE
b'Z' READY_FOR_QUERY
注:_cached_rows是一个队列,或者认为是数组也可以。
## 3. fetchone / fetchall 取数据
这个时候 数据 已经存放在_cached_rows里面了。
只需要取出来就行。
fetchone调用的是next()方法,next()方法又会自动调用到 984行的 __next__方法,
它里面其实就一行代码: return self._cached_rows.popleft()
从_cached_rows中取了第一条数据出来并返回
fetchall与fetchone类似,使用for循环遍历fetchall的返回值的时候,会反复调用
__next__方法,从而从_cached_rows中取出一条又一条的数据,直到取完为止: 993行.