L-Chat(5)——M5.1数据库

0 阅读5分钟

一、数据库简介

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)MongoDBJSON格式,灵活结构内容管理、日志
键值型Redis内存存储,极速读写缓存、会话、实时排行榜
嵌入式SQLite无独立进程,直接文件单机应用、手机APP

二、创建MySQL

2.1 登录 MySQL 并建库建表

image.png

  • 建库: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;
}