一、数据库简介
1.1 简介
我的电脑
├── 内存 (RAM) ← 程序运行时的临时数据,断电消失
│ └── L-Chat 进程
│ ├── 变量、数组、链表
│ └── 运行时的消息缓存
│
└── 磁盘 (硬盘/SSD) ← 永久存储,断电不丢
└── MySQL 服务进程 ← 独立的程序,专门管数据
├── 数据文件(/var/lib/mysql/)
├── 日志文件
└── 网络端口 3306
MySQL 是独立进程,通过网络通信
L-Chat 程序 MySQL 程序
| |
| TCP/本地socket |
|◄──────────────────►|
| "INSERT INTO..." |
| |
内存(临时) 磁盘(永久)
MySQL 是另一个独立的程序, L-Chat 通过 网络协议 跟它说话,不是直接操作内存。
MySQL 不是"内存管理方式",而是"另一个专业程序",通过网络把数据永久存到磁盘,还能高效查询。
1.2 主流数据库分类
| 类型 | 代表 | 特点 | 适用场景 |
|---|---|---|---|
| 关系型 (SQL) | MySQL, PostgreSQL, SQLite | 表结构,强约束,SQL语言 | 大多数业务系统 |
| 文档型 (NoSQL) | MongoDB | JSON格式,灵活结构 | 内容管理、日志 |
| 键值型 | Redis | 内存存储,极速读写 | 缓存、会话、实时排行榜 |
| 嵌入式 | SQLite | 无独立进程,直接文件 | 单机应用、手机APP |
二、创建MySQL
2.1 登录 MySQL 并建库建表
- 建库:lchat
- 两个用户:admin、guest
- 一个专用用户:lchat_user'@'localhost
2.2 封装一个带互斥锁(Mutex)的全局数据库实例
db.h
#ifndef _DB_H_
#define _DB_H_
#include <stdint.h>
// 初始化数据库连接
int db_init();
// 销毁数据库连接
void db_destroy();
// 验证用户密码,并返回数据库中的 uid
// 返回值:1 表示成功,0 表示密码错误,-1 表示用户不存在或查询失败
int db_verify_user(const char* username, const char* password, uint32_t* out_uid);
#endif
db.c
#include "db.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <mysql/mysql.h>
#include <logger.h>
static MYSQL *g_db_conn = NULL;
static pthread_mutex_t g_db_lock;
int db_init() {
g_db_conn = mysql_init(NULL);
if(g_db_conn == NULL) {
LOG_ERROR("MYSQL init failed");
return -1;
}
if(mysql_real_connect(g_db_conn,"localhost","lchat_user","lchat_pwd","lchat",0,NULL,0) == NULL) {
LOG_ERROR("MySQL connection failed: %s", mysql_error(g_db_conn));
return -1;
}
pthread_mutex_init(&g_db_lock, NULL);
LOG_INFO(" Conncted to MySQL database [lchat] successfully.");
return 0;
}
void db_destroy() {
if (g_db_conn)
{
mysql_close(g_db_conn);
}
pthread_mutex_destroy(&g_db_lock);
LOG_INFO("MySQL connection closed and resources released.");
}
int db_verify_user(const char* username, const char *password, uint32_t *out_uid) {
int status = -1;
MYSQL_STMT *stmt;
MYSQL_BIND bind_param[1];
MYSQL_BIND bind_result[2];
// ==========================================
// 1. 定义 SQL 骨架:使用 ? 作为占位符
// ==========================================
const char *query = "SELECT uid, password FROM users WHERE username = ?";// 先定义「模板」,? 是占位符,后续通过 mysql_stmt_bind_param() 填充实际值
pthread_mutex_lock(&g_db_lock);
// 初始化 stmt, 创建语句对象
stmt = mysql_stmt_init(g_db_conn);
if(!stmt) {
LOG_ERROR("mysql_stmt_init() failed");
pthread_mutex_unlock(&g_db_lock);
return -1;
}
// 预处理 SQL骨架,编译模板
if(mysql_stmt_prepare(stmt,query,strlen(query))){//此时数据库已经解析了 SQL 结构,? 的位置被标记为「参数占位符」
LOG_ERROR("mysql_stmt_prepare() failed: %s", mysql_stmt_error(stmt));
mysql_stmt_close(stmt);
pthread_mutex_unlock(&g_db_lock);
return -1;
}
// ==========================================
// 2. 绑定输入参数 (填充 ?)
// ==========================================
memset(bind_param,0,sizeof(bind_param));
bind_param[0].buffer_type = MYSQL_TYPE_STRING;// 参数类型:字符串
bind_param[0].buffer = (char *)username; // 数据指针
bind_param[0].buffer_length = strlen(username);// 数据长度
if(mysql_stmt_bind_param(stmt,bind_param)){// 绑定到第 1 个? 用户输入被当作「纯数据」,即使有 ' OR '1'='1 也不会改变 SQL 语义
LOG_ERROR("mysql_stmt_bind_param() failed: %s",mysql_stmt_error(stmt));
mysql_stmt_close(stmt);
pthread_mutex_unlock(&g_db_lock);
return -1;
}
// ==========================================
// 3. 执行预处理查询,// 查询结果仍在 MySQL 服务器端,不在C程序里!
// ==========================================
if(mysql_stmt_execute(stmt)){//执行时,数据库会把用户名当作字符串值比较,而不是解析成 SQL 代码
LOG_ERROR("mysql_stmt_execute() failed: %s", mysql_stmt_error(stmt));
mysql_stmt_close(stmt);
pthread_mutex_unlock(&g_db_lock);
return -1;
}
// ==========================================
// 4. 绑定输出结果 (告诉MySQL服务器:查询结果要放到我C程序的哪些变量里)
// ==========================================
uint32_t db_uid; // 存储查询到的uid
char db_password[64]; // 存储查询到的密码
unsigned long pwd_len; // 存储密码实际长度
my_bool is_null[2]; // 标记每列是否为NULL
my_bool error[2]; // 标记每列是否有错误
memset(bind_result,0,sizeof(bind_result));
// 告诉数据库:查询结果的第 1 列(uid)放到 &db_uid,第 2 列(password)放到 db_password
// 第1列:uid (整数类型)
bind_result[0].buffer_type = MYSQL_TYPE_LONG; // 32位整数
bind_result[0].buffer = &db_uid; // 存到db_uid变量
bind_result[0].is_null = &is_null[0]; // NULL标志位
bind_result[0].error = &error[0]; // 错误标志位
// 第2列:password (字符串类型)
bind_result[1].buffer_type = MYSQL_TYPE_STRING;
bind_result[1].buffer = db_password; // 存到字符数组
bind_result[1].buffer_length = sizeof(db_password); // 最多64字节
bind_result[1].length = &pwd_len; // 实际长度
bind_result[1].is_null = &is_null[1];
bind_result[1].error = &error[1];
if(mysql_stmt_bind_result(stmt,bind_result)){
LOG_ERROR("mysql_stmt_bind_result() failed: %s", mysql_stmt_error(stmt));
mysql_stmt_close(stmt);
pthread_mutex_unlock(&g_db_lock);
return -1;
}
// ==========================================
// 5. 抓取数据并进行业务比对
// ==========================================
int fetch_res = mysql_stmt_fetch(stmt); // 从数据库取出一行结果,填充到绑定的变量中
if(fetch_res == 0){ // 0 → 成功获取一行数据
// 在末尾加\0,确保字符串安全/强制截断
if(pwd_len < sizeof(db_password)){
db_password[pwd_len] = '\0';
} else {
db_password[sizeof(db_password)-1] = '\0';
}
if(strcmp(password,db_password) == 0){
*out_uid = db_uid;
status = 1; // 成功
} else {
status = 0; // 密码错误
}
}else if(fetch_res == MYSQL_NO_DATA){ // MYSQL_NO_DATA → 没有更多数据了(结果集为空或已取完)
status = -1; // 用户不存在
} else { // 其他值 → 出错(网络断开、类型转换失败等)
LOG_ERROR("mysql_stmt_fetch() failed: %s", mysql_stmt_error(stmt));
status = -1; // 查询失败
}
// ==========================================
// 6. 清理现场,释放锁
// ==========================================
mysql_stmt_close(stmt);
pthread_mutex_unlock(&g_db_lock);
return status;
}