MySQL C语言连接库引言网上关于MySQL C API的资料不少但很多都太简略要么直接贴一堆代码不解释要么版本太老连不上。自己折腾的时候踩了不少坑我顺手整理了一遍希望能帮到和我一样需要手写C连数据库的人。一.准备工作先理清一个概念MySQL数据库是服务端跑在某个机器上可能是本地也可能是远程。我们要写一个C程序去连接这个服务端执行SQL拿回结果。那C程序怎么知道怎么跟MySQL说话这就需要MySQL官方提供的客户端库——Connector/C。这个库做了几件事封装了MySQL的网络协议其实就是MySQL自己的一套通信格式提供了一套函数比如mysql_query、mysql_fetch_row这些帮你处理连接、认证、数据转换之类的底层细节所以用之前先去MySQL官网下载Connector/C选对操作系统和架构。Linux下一般是.tar.gz包Windows下是.zip。二.库长什么样解压之后目录结构大概是这样的mysql-connector-c-版本号-平台 ├── include/ │ ├── mysql.h # 最主要的头文件函数声明都在里面 │ ├── mysql_version.h │ ├── mysql_time.h │ └── ... # 一堆其他头文件 └── lib/ ├── libmysqlclient.a # 静态库 ├── libmysqlclient.so # 动态库的软链接 ├── libmysqlclient.so.18 # 另一个软链接 └── libmysqlclient.so.18.3.0 # 真正的动态库文件include里的mysql.h是我们写代码时要包含的lib里的库文件是链接时要用的。静态库.a会把代码直接编进你的可执行文件文件会变大但运行时不需要额外找库。动态库.so是运行时动态加载的可执行文件小但运行时系统必须能找到这个.so文件。三.测试程序证库写一个最简单的程序只调一个函数看看能不能编译、能不能跑。// test.c #include stdio.h #include mysql.h int main() { // 这个函数返回一个字符串是客户端库的版本号 const char *version mysql_get_client_info(); printf(MySQL client version: %s\n, version); return 0; }编译命令gcc -o test test.c -I./include -L./lib -lmysqlclient解释一下这几个参数-I./include告诉编译器去哪里找头文件mysql.h就在这里面-L./lib告诉链接器去哪里找库文件-lmysqlclient链接libmysqlclient这个库。前缀lib和后缀.so/.a可以省略编译器会自动补齐如果编译成功会产生一个test可执行文件。但这时候跑一下会看到这样的错误./test: error while loading shared libraries: libmysqlclient.so.18: cannot open shared object file: No such file or directory这就是动态库的问题。编译的时候我们用-L告诉了链接器库在哪但运行的时候操作系统不知道这个路径。它默认会去/usr/lib、/lib这些系统目录找找不到就报错。解决办法有三个方法一设置环境变量export LD_LIBRARY_PATH./lib:$LD_LIBRARY_PATH ./test这个环境变量告诉动态链接器去当前目录的lib子目录下找库。注意这个方法只对当前终端窗口有效关了就没了。方法二把库拷到系统目录sudo cp lib/libmysqlclient.so* /usr/lib/不推荐容易污染系统环境。方法三编译的时候加上-rpathgcc -o test test.c -I./include -L./lib -lmysqlclient -Wl,-rpath./lib-rpath会把库的搜索路径直接写进可执行文件里。这个比较干净建议用这个。四.正式连接数据库测试通过之后就可以真正连数据库了。4.1 初始化使用库之前必须先初始化得到一个MYSQL句柄。这个句柄后面几乎所有函数都要用到。MYSQL *mysql mysql_init(NULL); if (mysql NULL) { fprintf(stderr, mysql_init() failed\n); return -1; }参数传NULL表示让库自己分配内存。如果你已经有一个MYSQL结构体变量也可以传它的地址。4.2 设置字符集重要不设置字符集会出中文乱码。if (mysql_set_character_set(mysql, utf8) ! 0) { fprintf(stderr, set character set failed: %s\n, mysql_error(mysql)); }为什么必须做这一步因为MySQL服务端默认的字符集可能是latin1客户端库默认也是latin1。如果你的数据库里存了中文或者你要写入中文编码对不上就是乱码。设置成utf8基本能解决绝大多数情况。4.3 建立连接初始化完就要真正去连数据库服务器了。用到的是mysql_real_connect函数MYSQL *mysql_real_connect( MYSQL *mysql, // init返回的句柄 const char *host, // 主机名或IP填127.0.0.1或localhost const char *user, // 数据库用户名 const char *password, // 密码 const char *db, // 要连接的数据库名比如test_db unsigned int port, // 端口MySQL默认3306 const char *unix_socket,// Unix socket路径一般填NULL unsigned long clientflag // 客户端标志一般填0 );返回值成功返回第一个参数也就是mysql句柄失败返回NULL。示例演示MYSQL *conn mysql_real_connect( mysql, 127.0.0.1, // 本地MySQL root, // 用户名 123456, // 密码 mydb, // 数据库名 3306, // 端口 NULL, // socket 0 // 标志 ); if (conn NULL) { fprintf(stderr, Connection failed: %s\n, mysql_error(mysql));//mysql_error函数很重要 mysql_close(mysql); return -1; } printf(Connected successfully!\n);4.4 执行非查询SQL增删改连接成功后就可以执行SQL了。执行语句用mysql_queryint mysql_query(MYSQL *mysql, const char *query);返回值0表示成功非0表示出错。示例演示插入一条数据const char *sql INSERT INTO users (name, age) VALUES (张三, 25); if (mysql_query(conn, sql) ! 0) { fprintf(stderr, Insert failed: %s\n, mysql_error(conn)); } else { printf(Insert success, affected rows: %lld\n, mysql_affected_rows(conn)); }mysql_affected_rows返回受影响的记录数对INSERT就是插入的行数对UPDATE就是修改的行数对DELETE就是删除的行数。更新和删除类似就是换SQL语句。4.5 执行查询SQL查并获取结果查询比增删改多一个步骤你得把结果拿回来。先执行查询const char *sql SELECT id, name, age FROM users; if (mysql_query(conn, sql) ! 0) { fprintf(stderr, Query failed: %s\n, mysql_error(conn)); return -1; }查询成功后结果还在MySQL服务器那边没传到你的程序里。需要用mysql_store_result把它拉回来MYSQL_RES *result mysql_store_result(conn); if (result NULL) { fprintf(stderr, mysql_store_result failed: %s\n, mysql_error(conn)); return -1; }注意mysql_store_result会分配内存来存放结果集用完必须释放不然内存泄漏。拿到结果集之后可以看看有多少行、多少列函数演示int row_count mysql_num_rows(result); int field_count mysql_num_fields(result); printf(共 %d 行%d 列\n, row_count, field_count);想看列名表头的话MYSQL_FIELD *fields mysql_fetch_fields(result); for (int i 0; i field_count; i) { printf(%s\t, fields[i].name); } printf(\n); 最关键的取每一行的数据MYSQL_ROW row; while ((row mysql_fetch_row(result)) ! NULL) { for (int i 0; i field_count; i) { // row[i]可能是NULL数据库里存的是NULL值 // 直接printf(%s, row[i])会崩溃 if (row[i] NULL) { printf(NULL\t); } else { printf(%s\t, row[i]); } } printf(\n); }MYSQL_ROW其实就是char**每行是一个char*数组每个元素是字符串形式的数据。不管你数据库里存的是整数还是日期取出来都是字符串需要的话自己转换。最后释放结果集mysql_free_result(result);4.6 完整的查询示例把上面的串起来#include stdio.h #include mysql.h int main() { MYSQL *conn mysql_init(NULL); if (conn NULL) { fprintf(stderr, mysql_init failed\n); return 1; } if (mysql_set_character_set(conn, utf8) ! 0) { fprintf(stderr, set charset failed\n); mysql_close(conn); return 1; } if (mysql_real_connect(conn, 127.0.0.1, root, 123456, mydb, 3306, NULL, 0) NULL) { fprintf(stderr, connect failed: %s\n, mysql_error(conn)); mysql_close(conn); return 1; } if (mysql_query(conn, SELECT id, name, age FROM users) ! 0) { fprintf(stderr, query failed: %s\n, mysql_error(conn)); mysql_close(conn); return 1; } MYSQL_RES *result mysql_store_result(conn); if (result NULL) { fprintf(stderr, store result failed: %s\n, mysql_error(conn)); mysql_close(conn); return 1; } int fields mysql_num_fields(result); MYSQL_ROW row; while ((row mysql_fetch_row(result)) ! NULL) { for (int i 0; i fields; i) { printf(%s\t, row[i] ? row[i] : NULL); } printf(\n); } mysql_free_result(result); mysql_close(conn); return 0; }4.7 事务操作默认情况下每执行一条SQL就会自动提交。如果想把多条SQL作为一个事务需要先关闭自动提交// 关闭自动提交 if (mysql_autocommit(conn, 0) ! 0) { fprintf(stderr, set autocommit failed\n); } // 执行几条SQL mysql_query(conn, UPDATE account SET money money - 100 WHERE id 1); mysql_query(conn, UPDATE account SET money money 100 WHERE id 2); // 检查有没有出错没有就提交 if (/* 都成功了 */) { mysql_commit(conn); } else { mysql_rollback(conn); } // 恢复自动提交 mysql_autocommit(conn, 1);六、易错点找不到动态库编译用-L指定了路径但运行时系统不认识。解决办法用-rpath或者设LD_LIBRARY_PATH。中文乱码连上后立刻mysql_set_character_set改成utf8别等。结果集内存泄漏mysql_store_result返回的MYSQL_RES一定要mysql_free_result。这个很容易忘。NULL值崩溃mysql_fetch_row返回的字段可能是NULL直接printf(%s)会段错误要判断一下。mysql_error是调试神器几乎所有函数失败后都可以用mysql_error(conn)看具体原因。端口号是整数不是字符串3306不是3306。密码明文写在代码里生产环境别这么干从配置文件或环境变量读。七、建议这套API封装得比较底层好处是可控性强坏处是写起来啰嗦。每次查询都要query → store_result → fetch_row → free_result重复劳动很多。如果项目规模变大建议自己封装一层或者考虑用更高层的库。但作为理解MySQL通信机制的学习从头写一遍还是很有价值的。另外以上代码都没有做完整的错误处理实际项目中建议每个函数调用都检查返回值出错了及时清理资源退出。MYSQL连接池原理与简易数据网站数据流动是如何进行的引言假设你写了一个网站后台挂了个MySQL。每个用户请求来了你都要去连一下数据库查完关掉。访问量小还好但一旦用户多了比如几百个人同时点你的网站——那每次请求都新建连接、断开连接MySQL压力很大的。为什么因为TCP三次握手建连接、MySQL认证、再四次挥手断开这套流程走一遍几十毫秒。单次看着不多但一秒钟来几千次CPU直接拉满所以就有了连接池。一.连接池的定义连接池是一种预先创建并维护多个数据库连接的机制。程序启动时提前创建一批线程每个线程与MySQL建立好连接这些线程被统一管理形成一个池子。当有数据库操作任务到来时任务被封装成结构体包含SQL语句和回调函数推入任务队列池中的空闲线程循环等待并从队列中取出任务执行执行完成后根据是否设置了回调函数决定是否回调结果。连接池内部采用生产者-消费者模型通过任务队列实现线程间的任务调度。简单来说就是提前建好一堆MySQL连接放在一个池子里。谁要用就来拿用完还回去而不是真的关掉。我用图片大致画了一下大致流程是程序启动的时候一次性创建N个MySQL连接比如20个把这20个连接扔进一个队列或者列表里每个线程或者每个请求要从池子里“借”一个连接用完再“还”回去如果池子里的连接都被借走了后来的请求要么等要么报错同时我写的图片里还写了“delete... - taskpush - 连接池”是指执行完删除操作后封装成一个task再推回连接池还是说任务队列里放着要执行的SQL连接池负责取出来跑。这个看具体实现。二.一个简单的连接池要有什么我大致按照图片说一下图片里列了几个private里面的一个std::string sql要执行的SQL语句一个std::function cb回调函数class_task... 是想把每个数据库操作封装成任务任务队列存放待执行的任务线程池每个线程从队列里拿任务从连接池里拿连接执行SQL回调大家可以网上搜一搜“MySQL连接池 C”能找到不少实现。我是推荐去GitCode或者GitHub上搜索。因为自己从头写一个生产可用的连接池挺麻烦的看看别人的实现能省不少事。三.网站数据流动客户端和服务器之间的事用户打开注册页面填了一堆信息点提交。这时候浏览器和网站服务器先来一次TCP三次握手握完了才能发数据。请求一般是POST方式账号密码什么的都塞在请求体里。服务器收到请求开始处理。具体用什么语言就看网站怎么搭的了PHP、Java、Python都比较常见。不管什么语言干的活都差不多把HTTP请求里的参数抠出来比如username、password、email这些再判断一下这是个注册请求还是登录请求通常看URL路径就能知道。参数都拿齐了后端代码就开始拼SQL语句。注册的话大概是这样的INSERT INTO users (username, password, email) VALUES (xxx, yyy, zzz)SQL拼好之后就得发给数据库了。但这里有个问题数据库可能不止一台。小网站一台MySQL就能扛住但流量大了或者要做高可用就得搞数据库集群。比如一主多从主库负责写从库负责读。或者分库分表不同数据放在不同机器上。这时候服务器直连某一个数据库就不太灵活了。所以中间通常会加一层——叫数据库中间件。图片里提到“选择用效率高的语言C/C/其它语言写成”确实很多中间件底层是用C或C写的比如ProxySQL、MaxScale、Vitess这些。用这些语言主要是为了性能毕竟中间件要处理大量的网络转发和协议解析用Python这种解释型语言扛不住高并发。中间件做什么事呢第一负载均衡。比如你有一个主库两个从库读请求来了中间件决定发给哪个从库别让某一个压力太大。可以按轮询、按权重、或者按连接数来分配。第二读写分离。中间件能解析SQL语句看出你是SELECT还是INSERT/UPDATE/DELETE。SELECT走从库写操作走主库。应用程序不用自己操心连哪个库只管连中间件就行。第三故障转移。某个数据库挂了中间件能自动把请求转发到其他健康的节点应用程序基本没感知。所以说中间件有点像数据库侧的反向代理。Nginx是转发HTTP请求数据库中间件是转发MySQL协议请求。位置不同干的活类似。总结最后整个数据流动的路径大概是客户端 - 三次握手 - 发送HTTP请求 - Web服务器(Nginx/Apache) - 后端代码(PHP/Java/Python)提取参数、拼SQL - 数据库中间件 - 中间件做负载均衡、读写分离 - 实际MySQL数据库 - 结果原路返回中间件这一层小网站可以没有直接连数据库也行。但上了规模之后基本都会加一层不然数据库那边容易出问题。四.连接池的几个关键点最小连接数和最大连接数池子一开始创建多少个最多能扩展到多少个。设太小了高并发不够用设太大了MySQL那边连接数撑不住。连接存活检查连接存活检查MySQL配置wait_timeout空闲过久连接被MySQL服务端断开。Hikari不在取连接时ping空闲期后台自动校验失效连接并销毁老旧/C连接池一般取出前执行ping无效就废弃创建新连接。空闲回收池子里那些很久没人用的连接可以定时关掉一些释放资源。超时等待池子里没空闲连接了新来的请求最多等多久。等不到就报错返回别让请求一直卡着。补充图片里写的“一个用户逻辑是怎么走的注册登陆”其实就是上面这一串浏览器 - http服务器 - 后端代码提取参数 - 从连接池拿连接 - 执行SQL - 返回结果 - 连接放回池子。连接池这个组件不算复杂但用好了效果很明显。GitHub上搜“mysql connection pool c”或者“HikariCP mysql”能找到不少参考代码抄一个下来改改就能用。要是真准备自己写注意处理好线程安全和连接失效的问题就行别写出内存泄漏或者死锁。