一、 MySQL C API 发展出来的背景
正如 websocketpp 是为了发挥 C++ 在网络通信上的性能优势一样,MySQL C API 的存在也是为了追求极致的控制力和速度。
最底层的基石: MySQL 数据库服务端本身就是用 C 和 C++ 编写的。官方提供的这个 C API(动态链接库为
libmysqlclient)是所有连接方式中最原生、最底层的一种。万物之源: 在实际开发中,很多高级语言操作 MySQL 的库(比如 Python 的
mysqlclient、PHP 的mysqli等),其底层源码其实全是对这个 C API 的二次封装。性能优势: 在 C++ 后端开发中,如果不使用臃肿的 ORM(对象关系映射)框架,直接调用原生 C API 来发送 SQL 语句和提取数据,是执行效率最高、内存开销最小的方案,非常适合高并发的网关和游戏服务器后台。
二、 MySQL 的底层通信协议与数据格式
跟 WebSocket 有着异曲同工之妙,MySQL 客户端与服务端同样是建立在可靠的 TCP 长连接之上,但它使用的是自己独有的 MySQL 客户端/服务器协议 (MySQL Wire Protocol)。这个协议的交互阶段大致分为两部分:
1.连接与鉴权阶段 (Connection Phase)
-
服务器主动"打招呼": 与 HTTP/WebSocket 由客户端发起升级请求不同,TCP 连接一建立,MySQL 服务器会先主动向客户端发送一个握手初始包,里面包含数据库版本号和一个随机挑战码(类似于 WebSocket 的 nonce)。
-
客户端回应: 客户端拿到挑战码后,结合用户密码进行加密哈希计算,连同用户名一起发给服务器。验证通过后,才正式进入指令阶段。
2.命令执行阶段 (Command Phase)
所有发送的数据被封装在报文 (Packet) 中。常见的报文格式如下:
-
命令包: 客户端向服务器发送的每一个动作都有一个操作码 (Opcode)。例如发送 SQL 语句使用的是
COM_QUERY(值为 0x03)。 -
OK 包: 当你执行
INSERT、UPDATE、DELETE等不需要返回具体数据的语句时,服务器会返回一个 OK 包,里面包含"受影响的行数"等状态信息。 -
结果集包 (Result Set): 当你执行
SELECT语句时,服务器返回的数据不是简单的字符串,而是经过严格分割的二进制块。它首先发包告诉你"有几列",然后发包描述"每一列叫什么名字",最后才连续发送包含"每一行具体数据"的报文。
三、 MySQL C API 常用核心接口介绍
使用 MySQL C API 就像流水线作业,核心分为:句柄操作、执行 SQL、解析结果。
1. 核心数据结构与句柄
MYSQL:连接句柄 。相当于我们之前用的websocketpp::connection_hdl。它代表着你和数据库之间的一条物理 TCP 连接通道,所有的状态都存在这里。
MYSQL_RES:结果集指针。当你执行了查询语句后,服务器返回的那个庞大的"结果集包",就会被存放在这个结构里。
MYSQL_ROW:单行数据类型 。它本质上是一个字符串数组(char**)。你可以通过数组下标row[0],row[1]轻松取出当前这一行的第一列、第二列的值。
2. 连接与生命周期管理接口
mysql_init(MYSQL *mysql):分配或初始化一个MYSQL连接对象。
mysql_real_connect(...):发起真正的 TCP 连接并完成上面提到的密码鉴权过程。你需要传入主机 IP、用户名、密码、数据库名和端口号(通常是 3306)。
mysql_close(MYSQL *mysql):彻底关闭连接,并释放该句柄占用的内部内存。
3. 指令执行接口
mysql_query(MYSQL *mysql, const char *stmt_str):最核心的"发号施令"函数。把你的 SQL 语句(比如"SELECT * FROM users")直接传给它。返回0代表服务端成功接收并执行;返回非0代表 SQL 语句有语法错误或执行失败。
4. 结果集解析接口 (专用于 SELECT)
当你执行完查询后,必须按顺序使用以下接口来"拆快递":
mysql_store_result(MYSQL *mysql):将刚刚查到的所有数据,一次性从服务器网络缓冲区全部拉取到本地内存中,并返回一个MYSQL_RES指针。
mysql_num_rows(MYSQL_RES *result):快速获取结果集中一共有多少行数据。
mysql_num_fields(MYSQL_RES *result):获取数据表一共有多少列(字段)。
mysql_fetch_row(MYSQL_RES *result):核心遍历动作 。每次调用这个函数,它就像一个迭代器,会指向结果集的下一行,并返回一个装满该行数据的MYSQL_ROW数组。当遍历到最后一行之后,它会返回NULL。
mysql_free_result(MYSQL_RES *result):极其重要! 拿到所需数据后,必须调用这个函数把庞大的结果集内存清理掉,否则每次查询都会造成内存泄漏。
四、代码展示
cpp
#include <iostream>
#include <string>
#include <mysql/mysql.h> // 引入 MySQL C API 的核心头文件
int main() {
// ==========================================
// 步骤一:初始化与建立连接
// ==========================================
MYSQL *conn = mysql_init(nullptr); // 初始化连接句柄
if (conn == nullptr) {
std::cerr << "MySQL 句柄初始化失败!" << std::endl;
return 1;
}
// 设置连接参数 (请替换为您自己的数据库账号密码)
const char* host = "127.0.0.1";
const char* user = "your_username"; // 替换为您的用户名
const char* password = "your_password"; // 替换为您的密码
// 发起真实的 TCP 连接
// 参数说明:句柄, IP, 用户名, 密码, 默认数据库(先填NULL), 端口(默认3306), Socket(通常NULL), 标志位(0)
if (mysql_real_connect(conn, host, user, password, nullptr, 3306, nullptr, 0) == nullptr) {
std::cerr << "连接数据库失败: " << mysql_error(conn) << std::endl;
mysql_close(conn);
return 1;
}
std::cout << "成功连接到 MySQL 服务器!\n" << std::endl;
// 设置通信字符集为 utf8mb4,防止中文存取时变成问号乱码
mysql_set_character_set(conn, "utf8mb4");
// ==========================================
// 步骤二:执行 SQL 指令 (建库、建表、插入)
// ==========================================
// 准备我们要执行的 SQL 语句
std::string sql_create_db = "CREATE DATABASE IF NOT EXISTS chat_system_db;";
std::string sql_use_db = "USE chat_system_db;";
std::string sql_create_table = R"(
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
sender VARCHAR(50) NOT NULL,
content TEXT NOT NULL
);
)";
std::string sql_insert = "INSERT INTO messages (sender, content) VALUES ('Alice', 'Hello C++!'), ('Bob', '这是一个测试。');";
// 执行建库并切换过去
mysql_query(conn, sql_create_db.c_str());
mysql_query(conn, sql_use_db.c_str());
// 执行建表操作
if (mysql_query(conn, sql_create_table.c_str()) != 0) {
std::cerr << "建表失败: " << mysql_error(conn) << std::endl;
} else {
std::cout << "数据表 `messages` 准备就绪!" << std::endl;
}
// 执行数据插入操作
if (mysql_query(conn, sql_insert.c_str()) != 0) {
std::cerr << "插入数据失败: " << mysql_error(conn) << std::endl;
} else {
// mysql_affected_rows 可以获取刚才的 INSERT 操作影响了多少行
std::cout << "成功插入数据,受影响的行数: " << mysql_affected_rows(conn) << std::endl;
}
// ==========================================
// 步骤三:查询数据并解析结果集 (SELECT)
// ==========================================
std::string sql_select = "SELECT id, sender, content FROM messages;";
if (mysql_query(conn, sql_select.c_str()) != 0) {
std::cerr << "查询失败: " << mysql_error(conn) << std::endl;
} else {
// 核心:把服务端返回的查询结果拉取到本地内存
MYSQL_RES *result = mysql_store_result(conn);
if (result == nullptr) {
std::cerr << "获取结果集失败: " << mysql_error(conn) << std::endl;
} else {
int num_fields = mysql_num_fields(result); // 获取有多少列 (id, sender, content 就是 3 列)
int num_rows = mysql_num_rows(result); // 获取有多少行数据
std::cout << "\n--- 查询成功,共找到 " << num_rows << " 条聊天记录 ---\n";
MYSQL_ROW row;
// 核心循环:每次调用 fetch_row 都会拿出一行数据,直到拿完返回 NULL 为止
while ((row = mysql_fetch_row(result))) {
// row[0] 是 id,row[1] 是 sender,row[2] 是 content
std::cout << "[ID: " << (row[0] ? row[0] : "NULL") << "] ";
std::cout << (row[1] ? row[1] : "NULL") << " 说: ";
std::cout << (row[2] ? row[2] : "NULL") << std::endl;
}
std::cout << "-------------------------------------------\n";
// ⚠️ 极其重要:使用完结果集后,必须手动释放内存,否则会导致严重的内存泄漏!
mysql_free_result(result);
}
}
// ==========================================
// 步骤四:断开连接,清理资源
// ==========================================
mysql_close(conn);
std::cout << "数据库连接已安全关闭。" << std::endl;
return 0;
}