本节概述了 mysqlnd
插件架构。
MySQL 原生驱动概述
在开发 mysqlnd
插件之前,了解 mysqlnd
本身的组织方式很有用。 Mysqlnd
由以下模块组成
模块统计 | mysqlnd_statistics.c |
---|---|
连接 | mysqlnd.c |
结果集 | mysqlnd_result.c |
结果集元数据 | mysqlnd_result_meta.c |
语句 | mysqlnd_ps.c |
网络 | mysqlnd_net.c |
线协议 | mysqlnd_wireprotocol.c |
C 面向对象范式
在代码级别,mysqlnd
使用 C 模式来实现面向对象。
在 C 中,你使用 struct
来表示对象。结构体的成员表示对象的属性。指向函数的结构体成员表示方法。
与 C++ 或 Java 等其他语言不同,C 面向对象范式中没有关于继承的固定规则。但是,有一些约定需要遵守,这些约定将在后面讨论。
PHP 生命周期
在考虑 PHP 生命周期时,有两个基本循环
PHP 引擎启动和关闭循环
请求循环
当 PHP 引擎启动时,它将调用每个已注册扩展的模块初始化 (MINIT) 函数。这允许每个模块设置变量并分配将在整个 PHP 引擎进程生命周期中存在的资源。当 PHP 引擎关闭时,它将调用每个扩展的模块关闭 (MSHUTDOWN) 函数。
在 PHP 引擎的生命周期内,它将接收多个请求。每个请求构成另一个生命周期。在每个请求中,PHP 引擎将调用每个扩展的请求初始化函数。扩展可以执行处理请求所需的任何变量设置和资源分配。当请求循环结束时,引擎将调用每个扩展的请求关闭 (RSHUTDOWN) 函数,以便扩展可以执行所需的任何清理工作。
插件的工作原理
一个 mysqlnd
插件通过拦截使用 mysqlnd
的扩展对 mysqlnd
的调用来工作。这是通过获取 mysqlnd
函数表、备份它,并将其替换为自定义函数表来实现的,自定义函数表在需要时调用插件的函数。
以下代码展示了如何替换 mysqlnd
函数表
/* a place to store original function table */ struct st_mysqlnd_conn_methods org_methods; void minit_register_hooks(TSRMLS_D) { /* active function table */ struct st_mysqlnd_conn_methods * current_methods = mysqlnd_conn_get_methods(); /* backup original function table */ memcpy(&org_methods, current_methods, sizeof(struct st_mysqlnd_conn_methods); /* install new methods */ current_methods->query = MYSQLND_METHOD(my_conn_class, query); }
连接函数表操作必须在模块初始化 (MINIT) 期间完成。函数表是一个全局共享资源。在多线程环境中,使用 TSRM 构建,在请求处理期间操作全局共享资源几乎肯定会导致冲突。
注意:
操作
mysqlnd
函数表时不要使用任何固定大小的逻辑:可以在函数表末尾添加新方法。函数表在将来可能随时发生变化。
调用父方法
如果备份了原始函数表条目,仍然可以调用原始函数表条目 - 父方法。
在某些情况下,例如 Connection::stmt_init()
,在派生方法中的任何其他活动之前,调用父方法至关重要。
MYSQLND_METHOD(my_conn_class, query)(MYSQLND *conn, const char *query, unsigned int query_len TSRMLS_DC) { php_printf("my_conn_class::query(query = %s)\n", query); query = "SELECT 'query rewritten' FROM DUAL"; query_len = strlen(query); return org_methods.query(conn, query, query_len); /* return with call to parent */ }
扩展属性
一个 mysqlnd
对象由一个 C 结构体表示。无法在运行时向 C 结构体添加成员。 mysqlnd
对象的用户不能简单地向对象添加属性。
可以使用 mysqlnd_plugin_get_plugin_<object>_data()
系列的适当函数向 mysqlnd
对象添加任意数据(属性)。在分配对象时,mysqlnd
会在对象的末尾预留空间来存放指向任意数据的 void *
指针。 mysqlnd
为每个插件预留了一个 void *
指针的空间。
下表显示了如何计算特定插件的指针位置
内存地址 | 内容 |
---|---|
0 | mysqlnd 对象 C 结构体的开头 |
n | mysqlnd 对象 C 结构体的结尾 |
n + (m x sizeof(void*)) | 指向第 m 个插件的对象数据的 void* |
如果你计划对任何 mysqlnd
对象构造函数进行子类化,这是允许的,你必须牢记这一点!
以下代码展示了扩展属性
/* any data we want to associate */ typedef struct my_conn_properties { unsigned long query_counter; } MY_CONN_PROPERTIES; /* plugin id */ unsigned int my_plugin_id; void minit_register_hooks(TSRMLS_D) { /* obtain unique plugin ID */ my_plugin_id = mysqlnd_plugin_register(); /* snip - see Extending Connection: methods */ } static MY_CONN_PROPERTIES** get_conn_properties(const MYSQLND *conn TSRMLS_DC) { MY_CONN_PROPERTIES** props; props = (MY_CONN_PROPERTIES**)mysqlnd_plugin_get_plugin_connection_data( conn, my_plugin_id); if (!props || !(*props)) { *props = mnd_pecalloc(1, sizeof(MY_CONN_PROPERTIES), conn->persistent); (*props)->query_counter = 0; } return props; }
插件开发者负责插件数据内存的管理。
建议使用 mysqlnd
内存分配器来处理插件数据。这些函数使用以下约定命名:mnd_*loc()
。 mysqlnd
分配器有一些有用的特性,例如能够在非调试构建中使用调试分配器。
何时进行子类化? | 每个实例都有自己的私有函数表吗? | 如何进行子类化? | |
---|---|---|---|
连接 (MYSQLND) | MINIT | 否 | mysqlnd_conn_get_methods() |
结果集 (MYSQLND_RES) | MINIT 或更晚 | 是 | mysqlnd_result_get_methods() 或对象方法函数表操作 |
结果集元数据 (MYSQLND_RES_METADATA) | MINIT | 否 | mysqlnd_result_metadata_get_methods() |
语句 (MYSQLND_STMT) | MINIT | 否 | mysqlnd_stmt_get_methods() |
网络 (MYSQLND_NET) | MINIT 或更晚 | 是 | mysqlnd_net_get_methods() 或对象方法函数表操作 |
线协议 (MYSQLND_PROTOCOL) | MINIT 或更晚 | 是 | mysqlnd_protocol_get_methods() 或对象方法函数表操作 |
如果根据上表不允许,则绝不能在 MINIT 之后的时间点操作函数表。
某些类包含指向方法函数表的指针。同一类的所有实例将共享相同的函数表。为了避免混乱,尤其是在线程环境中,此类函数表只能在 MINIT 期间操作。
其他类使用全局共享函数表的副本。类函数表副本与对象一起创建。每个对象都使用自己的函数表。这为你提供了两种选择:你可以在 MINIT 期间操作对象的默认函数表,并且还可以进一步细化对象的方法,而不会影响同一类的其他实例。
共享函数表方法的优点是性能。无需为每个对象都复制函数表。
类型 | 分配、构造、重置 | 可以修改吗? | 调用者 |
---|---|---|---|
连接 (MYSQLND) | mysqlnd_init() | 否 | mysqlnd_connect() |
结果集 (MYSQLND_RES) | 分配
在以下情况下重置并重新初始化
|
是,但要调用父类! |
|
结果集元数据 (MYSQLND_RES_METADATA) | Connection::result_meta_init() | 是,但要调用父类! | Result::read_result_metadata() |
语句 (MYSQLND_STMT) | Connection::stmt_init() | 是,但要调用父类! | Connection::stmt_init() |
网络 (MYSQLND_NET) | mysqlnd_net_init() | 否 | Connection::init() |
线协议 (MYSQLND_PROTOCOL) | mysqlnd_protocol_init() | 否 | Connection::init() |
强烈建议你不要完全替换构造函数。构造函数执行内存分配。内存分配对于 mysqlnd
插件 API 和 mysqlnd
的对象逻辑至关重要。如果你不关心警告并坚持要挂钩构造函数,你至少应该在构造函数中做任何事情之前调用父构造函数。
无论所有警告,对构造函数进行子类化可能很有用。构造函数是修改具有非共享对象表的对象的函数表的理想位置,例如结果集、网络、线协议。
类型 | 派生方法必须调用父类吗? | 析构函数 |
---|---|---|
连接 | 是,在方法执行之后 | free_contents(),end_psession() |
结果集 | 是,在方法执行之后 | free_result() |
结果集元数据 | 是,在方法执行之后 | free() |
语句 | 是,在方法执行之后 | dtor(),free_stmt_content() |
网络 | 是,在方法执行之后 | free() |
线协议 | 是,在方法执行之后 | free() |
析构函数是释放属性的合适位置,mysqlnd_plugin_get_plugin_<object>_data()
。
列出的析构函数可能不等于实际 mysqlnd
方法释放对象本身。但是,它们是你挂钩并释放插件数据的最佳位置。与构造函数一样,你也可以完全替换方法,但这不建议这样做。如果上表中列出了多个方法,你需要挂钩所有列出的方法,并在 mysqlnd
首次调用哪个方法中释放你的插件数据。
插件的推荐方法是简单地挂钩方法,释放你的内存,并在紧随其后立即调用父类实现。