本节概述了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
首次调用的任何方法中释放插件数据。
对于插件,推荐的方法是简单地挂接方法,释放内存,然后立即调用父实现。