4月是颓废的一月,先是身体不适,呆坐了好几天,再是发现自己花了近20天写的库竟然已经有人实现了,连名字都一样,郁闷了好久,只能无奈改个名字(ddbc->c2db),然后改变项目的计划和侧重。
在开发c2db的时候,我们计划至少支持mysql、postgresql、sql server这几个dbms,自信哪里来?说白了,无非就是给那么dbms提供的C驱动套一层马甲罢了,纯粹是体力活,细心点就成。
那么为何不自己写这些驱动?理由很简单,性能和稳定性是在短期内无法苛求的。不过下一步c2db或许就要这么做了,因为我们发现似乎IO复用比连接池更好,而这要求我们自己编写驱动。当然,我最近也发现了一个叫mysql-native的库,就是用D实现的。
虽然近乎白忙乎了20天,但好歹有点积累,它就是libmysqlclient。坦白说,使用libmysqlclient并不难,不过肯定是没有用jdbc来得轻松的,本文就是我的一些笔记。我的建议是,各位可以通过本文入门,然后仔细阅读官方manual。
0x00 错误处理
我个人的习惯是从错误处理函数开始介绍,毕竟这也算是C的特色之一了。libmysqlclient的错误处理函数是仿照errno这种机制实现的,因此使用起来也大同小异。
unsigned mysql_errno(MYSQL*);
const char* mysql_error(MYSQL*);
这里有一个规律:
- 对于整型的返回值,0一般表示成功
- 对于指针的返回值,NULL一般表示失败
使用libmysqlclient的时候,对于错误处理相对理性的策略是“出错则终止”,因为一般情况下我们是没能力解决这些错误的。
0x01 初始化和关闭
下面这对函数用于初始化和关闭MYSQL实例,没什么好解释的,看名字就能懂,注意异常处理即可。
MYSQL* mysql_init(MYSQL*);
void mysql_close(MYSQL*);
我们可以先创建MYSQL实例,然后让mysql_init初始化,也可以将NULL作为参数,让mysql_init直接返回一个实例。
一般情况下,需要自己传入结构体实例指针的函数,可能就意味着是线程安全的,但是按照我的理解,这里和线程安全无关,纯粹是习惯问题。
0x02 客户端信息
以下两对函数用于获取客户端的版本号,分别是以unsigned long和const char*的形式。这两个函数都不会出错。
unsigned long mysql_get_client_version();
const char* mysql_get_client_info();
version的计算方式为:
- major_version 10000 + release_level 100 + sub_version
0x03 事务相关
典型的commit和rollback,附带autocommit。这里是有可能出错的,尽管官方manual里没有指明会具体可能会出现什么问题,但是应该和同步有关,而这些问题似乎也不是客户端程序能够解决的。
my_bool mysql_autocommit(MYSQL*, my_bool);
my_bool mysql_commit(MYSQL*);
my_bool mysql_rollback(MYSQL*);
my_bool就是unsigned char。这几个函数的具体行为和completion_type值有关,当它为RELEASE(2)时,那么服务器就会在函数完成后释放连接,不过默认为0。
0x04 连接与查询
如下是基本的连接与查询操作,没有贴出全部函数,不过这两个函数完全够用了,需要注意错误处理。
MYSQL* mysql_real_connect(MYSQL* mysql, const char* host,
const char* user, const char* password,
const char* db, unsigned port,
const char* unix_socket, unsigned long client_flag);
int mysql_real_query(MYSQL* mysql, const char* stmt_str, unsigned long length);
对于client_flag需要注意一下,可以为其设置CLIENT_MULTI_STATEMENTS,用以支持同时查询多条语句,语句之间通过分号分割。
0x05 ResultSet相关函数
如下为函数原型:
MYSQL_RES* mysql_store_result(MYSQL*);
void mysql_free_result(MYSQL_RES*);
my_bool mysql_more_results(MYSQL*);
int mysql_next_result(MYSQL*);
函数功能(省略mysql_):
- store_result:获取MYSQL_RES实例
- free_result:释放MYSQL_RES
- more_results:确定是否还有resultset
- next_result:获取下一个resultset
针对store_result的返回值,如果不为NULL,则表示成功,但是这里有个坑,就是返回NULL的情况。如果返回NULL,则可能表示出错,或者根本没有resultset,比如update操作就没有resultset。在下一节我会给出解决方案。
next_result返回3种值,其中大于0则表示出错,0表示还有更多的resultset,-1表示没有更多的resultset。不过我建议使用more_results来判断是否还有更多的resultset,next_result仅仅用来获取下一个resultset。
0x06 Field相关函数
首先是MYSQL_FIELD结构体,这里就不完全贴出代码了,只给出常用的属性:
- name: 字段名(别名)
- org_name: 原始字段名
- type: 字段类型
接下来是函数声明,基本给出了所有field相关的函数。
unsigned mysql_field_count(MYSQL* mysql);
MYSQL_FIELD* mysql_fetch_field(MYSQL_RES* result);
MYSQL_FIELD* mysql_fetch_fields(MYSQL_RES* result);
MYSQL_FIELD* mysql_fetch_field_direct(MYSQL_RES* result, unsigned fieldnr);
unsigned mysql_num_fields(MYSQL_RES* result);
MYSQL_FIELD_OFFSET mysql_field_tell(MYSQL_RES* result);
MYSQL_FIELD_OFFSET mysql_field_seek(MYSQL_RES* result, MYSQL_FIELD_OFFSET offset);
MYSQL_FIELD_OFFSET的类型为unsigned。
函数功能(省略mysql_):
- fetch_fields: 返回的field数组
- fetch_field:返回的是下一个field
- fetch_field_direct:根据指定的下标返回field
- num_fields:返回field的数量
- field_tell:返回当前field的下标
- field_seek:设置当前field的下标
大家应该不难从上述功能描述中发现,这里有好几种操作field的解决方案。但是C语言主要的消耗在于函数调用,思考一下,我们真的需要那么多的函数?我个人建议只使用mysql_num_fields和mysql_fetch_fields这两个函数。
另外这里还有一个field_count是干什么的呢?坦白说,我们根本不需要通过这个函数来获取field的数量,因为num_fields就能完成相关任务了,field_count的用处在于确定查询的语句是否返回resultset,这不是正好填了0x05中的那个坑了吗?
0x07 Row相关函数
函数原型:
unsigned long mysql_affected_rows(MYSQL*);
my_ulonglong mysql_num_rows(MYSQL_RES*);
MYSQL_ROW mysql_fetch_row(MYSQL_RES*);
unsigned long* mysql_fetch_lengths(MYSQL_RES*);
MYSQL_ROW_OFFSET mysql_row_tell(MYSQL_RES*);
MYSQL_ROW_OFFSET mysql_row_seek(MYSQL_RES*, MYSQL_ROW_OFFSET);
MYSQL_ROW的真实类型为const char*,也就是字符串数组。MYSQL_ROW_OFFSET的真实类型则为MYSQL_ROWS,那么MYSQL_ROWS是个怎样的结构体呢?
struct mysql_rows {
mysql_rows* next;
mysql_row data;
unsigned long length;
}
从这个结构体来看,我们应该能够猜到MYSQL_ROW是按照链表的形式存储的,当然仅仅是猜测,不过应该八九不离十。
函数功能:
- affected_rows: 获取语句影响的行数,适用于那些没有resultset的操作,比如update,delete等
- num_rows:返回row的数量
- fetch_row:改变游标位置,获取下一个MYSQL_ROW
- fetch_lengths:获取row中每个column的长度
- row_tell:设置游标的位置
- row_seek:获取当前的游标位置
这里需要特别指明的是fetch_lengths函数,由于column中可能存储的是二进制,因此并不一定是以'0'结尾,所以我们需要length。和field一样,我个人不建议使用row_tell和row_seek。
0x08 操作示例
我们通过一个简单的示例来说明如何使用libmysqlclient。
示例中,首先创建一个名为test的表,然后插入2行数据,将其输出,最后删除test表。
#include <mysql/mysql.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef const char *cstring;
MYSQL* Connect(cstring addr, unsigned port, cstring user, cstring passwd, cstring db) {
MYSQL* mysql = mysql_init(NULL);
if(mysql == NULL) {
fprintf(stderr, "%s\n", mysql_error(mysql));
exit(-1);
}
MYSQL* conn = mysql_real_connect(mysql, addr,
user, passwd, db, port, NULL, CLIENT_MULTI_STATEMENTS);
if(conn == NULL) {
fprintf(stderr, "%s\n", mysql_error(mysql));
exit(-1);
}
return conn;
}
void Close(MYSQL* conn) {
mysql_close(conn);
}
void ExecUpdate(MYSQL* conn, cstring sql) {
int ret = mysql_real_query(conn, sql, strlen(sql));
if(ret != 0) {
fprintf(stderr, "%s\n", mysql_error(conn));
exit(-1);
}
}
void QueryAndPrint(MYSQL* conn, cstring sql) {
int ret = mysql_real_query(conn, sql, strlen(sql));
if(ret != 0) {
fprintf(stderr, "%s\n", mysql_error(conn));
exit(-1);
}
MYSQL_RES* res = mysql_store_result(conn);
if(res == NULL) {
fprintf(stderr, "%s\n", mysql_error(conn));
exit(-1);
}
int i;
int count = mysql_num_rows(res);
for(i = 0; i < count; ++i) {
MYSQL_ROW row = mysql_fetch_row(res);
if(row == NULL) {
fprintf(stderr, "%s\n", mysql_error(conn));
exit(-1);
}
unsigned len = mysql_num_fields(res);
int j;
for(j = 0; j < len; ++j) {
printf(" | %s", row[j]);
}
printf(" |\n");
}
mysql_free_result(res);
}
int main() {
MYSQL* conn = Connect("localhost", 3306, "root", "", "test");
ExecUpdate(conn, "create table test(id int, name varchar(20), primary key(id));");
ExecUpdate(conn, "insert into test(id, name) values(1, 'name1'), (2, 'name2');");
QueryAndPrint(conn, "select * from test");
ExecUpdate(conn, "drop table test;");
Close(conn);
return 0;
}
按照如下方式编译:
gcc test.c -lmysqlclient -o test
这里有个奇葩的问题,各位可以将示例中的Connect函数改成connect,然后应该就能得到一个段错误了,我暂时没有查明究竟是什么导致了这个问题,很可能是libmysqlclient.so也定义了一个connect函数,然后链接的时候混淆了,这也就导致了最终的段错误。
调试之后,我发现了问题所在,首先上两张图,第一张是反汇编之后的截图,第二张是callq命令之后的栈帧截图。
那么,如何调试呢?首先我们需要大概确定问题所在,这里很明显,根据猜测,问题是出在mysql_real_connect内部的,那么我们只需要在call mysql_real_connect的下一条指令处设置断点即可获得该时刻的栈帧,然后通过栈帧判断问题。
connect并不是一个递归函数,但是栈帧上却出现了两个connect,那么说明有代码调用了connect,而C是不支持函数重载的,因此connect的确被错误调用了。但是最关键的函数由于头文件中没有声明,我们无法从调试器中获取函数名。
但是没关系,还有错误信息,足够我们判断了。贴上从调试器中获取的错误信息:
connect(addr = 0x3 <error: Cannot access memory at address 0x3>, port = 4294959568, user = 0x6e <error: Cannot access memory at address 0x6e>, passwd = 0x0, db = 0x7fffffffe1ea "")
请注意数据的地址,这极有可能是内核空间的数据,如果事实如此,那这里的connect就是socket中的connect函数。为了验证猜想,我们可以声明一个空的名为connect的函数,然后运行一下程序。此时会提示mysql.sock连接错误,我们的猜想成立了。
严格来说,这既不是编译器的问题,也不能算是libmysqlclient的问题,因为它根本没有必要在头文件中声明connect函数,一切都只不过是内部使用罢了。但是作为开发者,我们就要很小心了,这绝对是一个坑啊,而且是没有人会为其负责。
0x09 总结
上述就是libmysqlclient的主要内容了,使用起来也还是挺方便的。不过了解libmysqlclient的读者可能会说你压根就没讲preparedstatement。是的,这部分内容会在c2db实现PreparedStatement的时候加上,敬请期待。