pg8000

# 背景知识

首先要知道,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行.
 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值