libmysqlclient 使用浅析

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*);

这里有一个规律:

  1. 对于整型的返回值,0一般表示成功
  2. 对于指针的返回值,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的时候加上,敬请期待。

说两句: