一、项目整体架构

客户端 ( Apifox / 浏览器)
    |
    v
Nginx (上传临时目录 /root/tmp)
    |
    v
API Server (8081端口)
    |
    +---> MySQL (用户信息、文件信息)
    +---> Redis (Token 存储)
    +---> FastDFS (文件存储)

新增模块一览

模块 说明
注册功能 MySQL 连接池,插入 user_info 表
登录功能 MySQL 验证密码 + Redis 存 Token(UUID,24h过期)
文件上传 nginx 临时目录 -> 解析 multipart -> 上传 FastDFS -> 写 MySQL
MD5 秒传 查询 file_info.md5 -> 引用计数+1 -> 写 user_file_list
MySQL 连接池 mysql/ 目录,连接池管理
Redis 连接池 redis/ 目录,连接池管理
配置文件 tc_http_server.conf 统一配置所有参数
config 解析器 base/config_file_reader.cc

二、数据库表设计

user_info 用户信息表

存储用户注册信息:

CREATE TABLE user_info (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_name VARCHAR(128) NOT NULL UNIQUE,  -- 用户名(唯一)
    nick_name VARCHAR(128),                   -- 昵称
    password VARCHAR(256),                    -- 密码(建议加密存储)
    phone VARCHAR(32),                       -- 电话
    email VARCHAR(128),                       -- 邮箱
    create_time DATETIME                       -- 创建时间
);

file_info 文件信息表

存储所有上传文件的核心信息,用于 MD5 秒传判断:

CREATE TABLE file_info (
    md5 VARCHAR(256) PRIMARY KEY,             -- 文件 MD5(唯一标识)
    file_id VARCHAR(128),                    -- FastDFS 返回的 file_id
    url VARCHAR(512),                         -- 完整 URL
    size BIGINT,                              -- 文件大小(字节)
    type VARCHAR(8),                          -- 文件类型后缀(png, mp4, zip...)
    count INT DEFAULT 1                       -- 引用计数(有多少用户拥有此文件)
);

user_file_list 用户文件列表

存储每个用户拥有哪些文件:

CREATE TABLE user_file_list (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user VARCHAR(128) NOT NULL,               -- 文件所属用户
    md5 VARCHAR(256),                         -- 文件 MD5
    create_time DATETIME,                     -- 创建时间
    file_name VARCHAR(256),                   -- 文件名(用户看到的名字)
    shared_status INT DEFAULT 0,             -- 共享状态(0未共享,1已共享)
    pv INT DEFAULT 0 -- 下载量(每下载一次+1)
);

三、注册功能实现

请求与响应(Apifox 测试)

接口信息:

  • 请求方法:POST
  • 请求路径:/api/reg
  • Content-Type:application/json

请求体:

{
    "userName": "tes1t",
    "nickName": "lssssss",
    "firstPwd": "213213213123",
    "phone": "1333",
    "email": "13333333@qq.com"
}

字段说明:

字段 类型 必填 说明
userName string 用户名,唯一标识
nickName string 昵称
firstPwd string 密码
phone string 电话
email string 邮箱

响应:

{
    "code": 0
}

code 返回值含义:

code 含义
0 注册成功
1 注册失败(数据库错误)
2 用户名已存在

核心代码 — api_register.cc

int registerUser(std::string &user_name, std::string &nick_name,
                 std::string &pwd, std::string &phone, std::string &email) {
    int ret = 0;
    uint32_t user_id = 0;

    //1. 从连接池获取 MySQL 连接(主库用于写入)
    CDBManager *db_manager = CDBManager::getInstance();
    CDBConn *db_conn = db_manager->GetDBConn("tuchuang_master");
    AUTO_REL_DBCONN(db_manager, db_conn);  // 函数退出时自动归还连接

    if (!db_conn) {
        LOG_ERROR << "GetDBConn(tuchuang_master) failed";
        return 1;
    }

    // 2. 查询用户名是否已存在
    string str_sql = FormatString(
        "select id from user_info where user_name='%s'", user_name.c_str());
    CResultSet *result_set = db_conn->ExecuteQuery(str_sql.c_str());

    if (result_set && result_set->Next()) {
        // 用户名已存在
        LOG_WARN << "id: " << result_set->GetInt("id")
                 << ", user_name: " << result_set->GetString("user_name")
                 << " already exists";
        delete result_set;
        ret = 2;  // 用户名已存在
    } else {
        // 3. 用户名不存在,插入新用户
        time_t now;
        char create_time[TIME_STRING_LEN];
        now = time(NULL);
        strftime(create_time, TIME_STRING_LEN - 1, "%Y-%m-%d %H:%M:%S",
                 localtime(&now));

        str_sql = "insert into user_info "
                  "(user_name, nick_name, password, phone, email, create_time) "
                  "values (?,?,?,?,?,?)";

        // 预处理语句方式插入,防止 SQL 注入
        CPrepareStatement *stmt = new CPrepareStatement();
        if (stmt->Init(db_conn->GetMysql(), str_sql)) {
            uint32_t index = 0;
            string c_time = create_time;
            stmt->SetParam(index++, user_name);   // 第1个参数:用户名
            stmt->SetParam(index++, nick_name);   // 第2个参数:昵称
            stmt->SetParam(index++, pwd);          // 第3个参数:密码
            stmt->SetParam(index++, phone);        // 第4个参数:电话
            stmt->SetParam(index++, email);        // 第5个参数:邮箱
            stmt->SetParam(index++, c_time);       // 第6个参数:创建时间

            bool bRet = stmt->ExecuteUpdate();  // 执行插入
            if (bRet) {
                ret = 0;  // 插入成功
                user_id = db_conn->GetInsertId();  // 获取自增 ID
                LOG_INFO << "insert user_id: " << user_id
                         << ", user_name: " << user_name;
            } else {
                LOG_ERROR << "insert user_info failed. " << str_sql;
                ret = 1;  // 插入失败
            }
        }
        delete stmt;  // 释放预处理对象
    }

    return ret;
}

JSON 解析 — decodeRegisterJson

int decodeRegisterJson(const std::string &str_json,
    string &user_name, string &nick_name, string &pwd,
    string &phone, string &email) {

    Json::Value root;
    Json::Reader jsonReader;
    bool res = jsonReader.parse(str_json, root);

    if (!res) {
        LOG_ERROR << "parse reg json failed ";
        return -1;
    }

    // 用户名(必填)
    if (root["userName"].isNull()) {
        LOG_ERROR << "userName null";
        return -1;
    }
    user_name = root["userName"].asString();

    // 昵称(必填)
    if (root["nickName"].isNull()) {
        LOG_ERROR << "nickName null";
        return -1;
    }
    nick_name = root["nickName"].asString();

    // 密码(必填)
    if (root["firstPwd"].isNull()) {
        LOG_ERROR << "firstPwd null";
        return -1;
    }
    pwd = root["firstPwd"].asString();

    // 电话(非必填)
    if (!root["phone"].isNull()) {
        phone = root["phone"].asString();
    }

    // 邮箱(非必填)
    if (!root["email"].isNull()) {
        email = root["email"].asString();
    }

    return 0;
}

四、登录功能与 Token 机制实现

请求与响应(Apifox 测试)

接口信息:

  • 请求方法:POST
  • 请求路径:/api/login
  • Content-Type:application/json

请求体:

{
    "user": "tes1t",
    "pwd": "213213213123"
}

字段说明:

字段 类型 必填 说明
user string 用户名
pwd string 密码

响应:

{
    "code": 0,
    "token": "550e8400-e29b-41d4-a716-446655440000"
}

code 返回值含义:

code 含义
0 登录成功,返回 token
1 登录失败(参数错误、密码错误等)

Token 机制设计

传统方式(不安全):用户名 -> Token(用户名作为 key)
    攻击者拿到 Token 后,直接知道对应用户名

本项目方式(安全):UUID Token -> 用户名(Token 作为 key)
    Token 是随机 UUID,攻击者无法从 Token 反推用户名

Redis 存储结构:

Key:   "550e8400-e29b-41d4-a716-446655440000"  (UUID Token)
Value: "tes1t"  (用户名)
TTL:   86400 秒(24小时自动过期)

核心代码 — api_login.cc

// 生成 UUID 作为 Token
std::string generateUUID() {
    uuid_t uuid;
    uuid_generate_time_safe(uuid);  // 生成时间安全的 UUID
    char uuidStr[40] = {0};
    uuid_unparse(uuid, uuidStr);    // UUID 转字符串
    return std::string(uuidStr);
}

// 验证用户名密码
int verifyUserPassword(std::string &user_name, std::string &pwd) {
    int ret = 0;

    CDBManager *db_manager = CDBManager::getInstance();
    // 从从库读取(读操作分流到从库)
    CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");
    AUTO_REL_DBCONN(db_manager, db_conn);

    // 根据用户名查询密码
    string strSql = FormatString(
        "select password from user_info where user_name='%s'", user_name.c_str());
    CResultSet *result_set = db_conn->ExecuteQuery(strSql.c_str());

    if (result_set && result_set->Next()) {
        string password = result_set->GetString("password");
        LOG_INFO << "mysql-pwd: " << password << ", user-pwd: " << pwd;
        if (password == pwd)
            ret = 0; // 密码一致,验证通过
        else
            ret = -1; // 密码不一致
    } else {
        ret = -1; // 用户不存在
    }

    delete result_set;
    return ret;
}

// 将 Token 存入 Redis,24小时过期
int setToken(std::string &user_name, std::string &token) {
    int ret = 0;

    CacheManager *cache_manager = CacheManager::getInstance();
    CacheConn *cache_conn = cache_manager->GetCacheConn("token");
    AUTO_REL_CACHECONN(cache_manager, cache_conn);

    if (cache_conn) {
        token = generateUUID();  // 生成唯一 UUID
        // key = token, value = 用户名,过期时间 86400秒(24小时)
        cache_conn->SetEx(token, 86400, user_name);
    } else {
        ret = -1;
    }

    return ret;
}

// 完整的登录处理
int ApiLoginUser(std::string &post_data, std::string &resp_json) {
    std::string user_name;
    std::string pwd;
    std::string token;

    // 1.判空
    if (post_data.empty()) {
        encodeLoginJson(1, token, resp_json);
        return -1;
    }

    // 2. 解析 JSON
    if (decodeLoginJson(post_data, user_name, pwd) < 0) {
        LOG_ERROR << "decodeRegisterJson failed";
        encodeLoginJson(1, token, resp_json);
        return -1;
    }

    // 3. 验证账号密码
    if (verifyUserPassword(user_name, pwd) < 0) {
        LOG_ERROR << "verifyUserPassword failed";
        encodeLoginJson(1, token, resp_json);
        return -1;
    }

    // 4. 生成 Token 并存入 Redis
    if (setToken(user_name, token) < 0) {
        LOG_ERROR << "setToken failed";
        encodeLoginJson(1, token, resp_json);
        return -1;
    }

    // 5. 返回成功响应
    encodeLoginJson(0, token, resp_json);
    return 0;
}

JSON 解析 — decodeLoginJson

int decodeLoginJson(const std::string &str_json,
                    std::string &user_name, std::string &pwd) {
    Json::Value root;
    Json::Reader jsonReader;
    bool res = jsonReader.parse(str_json, root);

    if (!res) {
        LOG_ERROR << "parse login json failed ";
        return -1;
    }

    // 用户名
    if (root["user"].isNull()) {
        LOG_ERROR << "user null";
        return -1;
    }
    user_name = root["user"].asString();

    // 密码
    if (root["pwd"].isNull()) {
        LOG_ERROR << "pwd null";
        return -1;
    }
    pwd = root["pwd"].asString();

    return 0;
}

五、文件上传流程(nginx + FastDFS)

整体流程

客户端 POST multipart/form-data
    |
    v
Nginx 接收文件,存入临时目录 /root/tmp/1/0035297749
    |
    v
Nginx 通过 upload_pass 通知 API: 127.0.0.1:8081/api/upload
    |
    v
API 解析 multipart 数据(手动字符串解析)
    |  字段:file_name, file_content_type, file_path,
    |       file_md5, file_size, user
    v
重命名临时文件(加后缀): 0035297749 -> 0035297749.jpg
    |
    v
fork + execvp 调用 fdfs_upload_file 上传到 FastDFS
    |
    v
FastDFS 返回 file_id(如 group1/M00/00/00/xxx.jpg)
    |
    v
删除本地临时文件
    |
    v
拼接完整 URL: http://192.168.181.138/group1/M00/00/00/xxx.jpg
    |
    v
写入 MySQL: file_info + user_file_list

请求与响应(Apifox 测试)

接口信息:

  • 请求方法:POST
  • 请求路径:/api/upload
  • Content-Type:multipart/form-data

请求表单字段:

字段 类型 说明
file_name string 文件名(如 demo.jpg)
file_content_type string 文件类型(如 image/jpeg)
file_path string 文件在服务器的临时路径
file_md5 string 文件的 MD5 值
file_size string 文件大小(字节)
user string 操作用户名

响应:

{
    "code": 0
}

核心代码 — fork + execvp 上传 FastDFS

int uploadFileToFastDfs(char *file_path, char *fileid) {
    int ret = 0;
    if (s_dfs_path_client.empty()) {
        LOG_ERROR << "s_dfs_path_client is empty";
        return -1;
    }

    int fd[2];
    // 创建无名管道:fd[0] 为读端,fd[1] 为写端
    if (pipe(fd) < 0) {
        LOG_ERROR << "pipe error";
        ret = -1;
        goto END;
    }

    pid_t pid = fork();  // 创建子进程
    if (pid < 0) {
        LOG_ERROR << "fork error";
        ret = -1;
        goto END;
    }

    if (pid == 0) {
        // 子进程:关闭读端,将标准输出重定向到管道写端
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);
        // 执行 fdfs_upload_file 命令
        execlp("fdfs_upload_file", "fdfs_upload_file",
               s_dfs_path_client.c_str(), file_path, NULL);
        // execlp 成功则后面代码不会执行(子进程被新程序替换)
        LOG_ERROR << "execlp fdfs_upload_file error";
        close(fd[1]);
        exit(-1);
    } else {
        // 父进程:关闭写端,从管道读取 FastDFS 返回的 file_id
        close(fd[1]);
        read(fd[0], fileid, TEMP_BUF_MAX_LEN);
        TrimSpace(fileid);  // 去掉首尾空白字符

        if (strlen(fileid) == 0) {
            LOG_ERROR << "upload failed";
            ret = -1;
            goto END;
        }

        LOG_INFO << "fileid: " << fileid;
        wait(NULL);  // 等待子进程结束,回收其资源
        close(fd[0]);
    }

END:
    return ret;
}

父子进程管道通信示意:

子进程                          父进程
  |                              |
  | close(fd[0])                 | close(fd[1])
  | dup2(fd[1], STDOUT_FILENO) |
  | execlp(fdfs_upload_file)     |
  | |
  | stdout 内容 ----------------->| read(fd[0], fileid)
  | (fdfs 返回的 file_id)         |

核心代码 — 解析 multipart/form-data

int ApiUpload(string &post_data, string &str_json) {
    char suffix[SUFFIX_LEN] = {0};
    char fileid[TEMP_BUF_MAX_LEN] = {0};
    char fdfs_file_url[FILE_URL_LEN] = {0};
    int ret = 0;
    char boundary[TEMP_BUF_MAX_LEN] = {0};
    char file_name[128] = {0};
    char file_content_type[128] = {0};
    char file_path[128] = {0};
    char new_file_path[128] = {0};
    char file_md5[128] = {0};
    char file_size[32] = {0};
    long long_file_size = 0;
    char user[32] = {0};

    char *begin = (char *)post_data.c_str();
    char *p1, *p2;

    // ========== 1. 解析 boundary 分隔符 ==========
    // Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjWE3qXXORSg2hZiB
    p1 = strstr(begin, "\r\n");
    if (p1 == NULL) {
        LOG_ERROR << "wrong no boundary!";
        ret = -1;
        goto END;
    }
    strncpy(boundary, begin, p1 - begin);
    boundary[p1 - begin] = '\0';
    LOG_INFO << "boundary: " << boundary;

    // ========== 2. 解析 file_name ==========
    begin = p1 + 2;
    p2 = strstr(begin, "name=\"file_name\"");
    if (!p2) {
        LOG_ERROR << "wrong no file_name!";
        ret = -1;
        goto END;
    }
    p2 = strstr(p2, "\r\n");
    p2 += 4;  // 跳过 \r\n
    begin = p2;
    p2 = strstr(begin, "\r\n");
    strncpy(file_name, begin, p2 - begin);
    LOG_INFO << "file_name: " << file_name;

    // ========== 3. 解析 file_content_type ==========
    begin = p2 + 2;
    p2 = strstr(begin, "name=\"file_content_type\"");
    p2 = strstr(p2, "\r\n");
    p2 += 4;
    begin = p2;
    p2 = strstr(begin, "\r\n");
    strncpy(file_content_type, begin, p2 - begin);
    LOG_INFO << "file_content_type: " << file_content_type;

    // ========== 4. 解析 file_path ==========
    begin = p2 + 2;
    p2 = strstr(begin, "name=\"file_path\"");
    p2 = strstr(p2, "\r\n");
    p2 += 4;
    begin = p2;
    p2 = strstr(begin, "\r\n");
    strncpy(file_path, begin, p2 - begin);
    LOG_INFO << "file_path: " << file_path;

    // ========== 5. 解析 file_md5 ==========
    begin = p2 + 2;
    p2 = strstr(begin, "name=\"file_md5\"");
    p2 = strstr(p2, "\r\n");
    p2 += 4;
    begin = p2;
    p2 = strstr(begin, "\r\n");
    strncpy(file_md5, begin, p2 - begin);
    LOG_INFO << "file_md5: " << file_md5;

    // ========== 6. 解析 file_size ==========
    begin = p2 + 2;
    p2 = strstr(begin, "name=\"file_size\"");
    p2 = strstr(p2, "\r\n");
    p2 += 4;
    begin = p2;
    p2 = strstr(begin, "\r\n");
    strncpy(file_size, begin, p2 - begin);
    long_file_size = strtol(file_size, NULL, 10);  // 字符串转 long
    LOG_INFO << "file_size: " << long_file_size;

    // ========== 7. 解析 user ==========
    begin = p2 + 2;
    p2 = strstr(begin, "name=\"user\"");
    p2 = strstr(p2, "\r\n");
    p2 += 4;
    begin = p2;
    p2 = strstr(begin, "\r\n");
    strncpy(user, begin, p2 - begin);
    LOG_INFO << "user: " << user;

    // ========== 8. 重命名临时文件(加后缀)==========
    GetFileSuffix(file_name, suffix);        // 从文件名获取后缀
    strcat(new_file_path, file_path);       // /root/tmp/1/0035297749
    strcat(new_file_path, ".");             // /root/tmp/1/0035297749.
    strcat(new_file_path, suffix);          // /root/tmp/1/0035297749.jpg
    ret = rename(file_path, new_file_path);  // 重命名
    if (ret < 0) {
        LOG_ERROR << "rename failed";
        ret = -1;
        goto END;
    }

    // ========== 9. 上传到 FastDFS ==========
    if (uploadFileToFastDfs(new_file_path, fileid) < 0) {
        LOG_ERROR << "uploadFileToFastDfs failed";
        unlink(new_file_path);  // 删除本地文件
        ret = -1;
        goto END;
    }

    // ========== 10. 删除本地临时文件 ==========
    unlink(new_file_path);

    // ========== 11. 拼接完整 URL ==========
    getFullUrlByFileid(fileid, fdfs_file_url);

    // ========== 12. 写入数据库 ==========
    storeFileinfo(db_conn, NULL, user, file_name, file_md5,
                  long_file_size, fileid, fdfs_file_url);

    ret = 0;
    value["code"] = 0;
    str_json = value.toStyledString();
    return 0;

END:
    value["code"] = 1;
    str_json = value.toStyledString();
    return -1;
}

六、MD5秒传原理实现

什么是秒传?

用户上传文件时,客户端先计算文件 MD5。如果服务器上已存在相同 MD5 的文件(其他用户上传过),直接引用该文件记录,无需真正上传,节省带宽和时间。

请求与响应(Apifox 测试)

接口信息:

  • 请求方法:POST
  • 请求路径:/api/md5
  • Content-Type:application/json

请求体:

{
    "user": "tes1t",
    "token": "550e8400-e29b-41d4-a716-446655440000",
    "md5": "d41d8cd98f00b204e9800998ecf8427e",
    "filename": "demo.jpg"
}

字段说明:

字段 类型 必填 说明
user string 用户名
token string 登录返回的 Token
md5 string 文件的 MD5 值
filename string 文件名

响应:

{
    "code": 0
}

code 返回值含义:

code 含义
0 秒传成功(文件已存在,直接引用)
1 秒传失败(文件不存在,需正常上传)
4 Token 验证失败
5 此用户已拥有此文件

秒传流程详解

客户端计算文件 MD5
    |
    v
请求 /api/md5 {user, token, md5, filename}
    |
    v
验证 Token(Redis 中查找 token -> 用户名,与 user字段对比)
    |
    v
查询 file_info 表 md5 字段
    |
    +--> 存在:user_file_list 中此用户是否已有此文件?
    |       +--> 有:返回 code=5(此用户已拥有此文件)
    |       +--> 无:file_info.count + 1,插入 user_file_list,返回 code=0
    |
    +--> 不存在:返回 code=1(秒传失败,需正常上传)

核心代码 — handleDealMd5

void handleDealMd5(const char *user, const char *md5,
                   const char *filename, string &str_json) {
    Md5State md5_state = Md5Failed;
    int ret = 0;
    int file_ref_count = 0;
    char sql_cmd[SQL_MAX_LEN] = {0};

    CDBManager *db_manager = CDBManager::getInstance();
    CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");
    AUTO_REL_DBCONN(db_manager, db_conn);

    CacheManager *cache_manager = CacheManager::getInstance();
    CacheConn *cache_conn = cache_manager->GetCacheConn("token");
    AUTO_REL_CACHECONN(cache_manager, cache_conn);

    // ========== 1. 查询 file_info 中 md5 是否存在,获取引用计数 ==========
    sprintf(sql_cmd, "select count from file_info where md5 = '%s'", md5);
    LOG_INFO << "执行: " << sql_cmd;
    ret = GetResultOneCount(db_conn, sql_cmd, file_ref_count);
    LOG_INFO << "ret: " << ret << ", file_ref_count: " << file_ref_count;

    if (ret == 0) {
        // ========== 2. md5 存在,检查此用户是否已有此文件 ==========
        sprintf(sql_cmd,
                "select * from user_file_list where user='%s' and md5='%s' "
                "and file_name='%s'",
                user, md5, filename);
        ret = CheckwhetherHaveRecord(db_conn, sql_cmd);
        if (ret == 1) {
            // 此用户已经拥有此文件,不能重复上传
            md5_state = Md5FileExit;  // code = 5
            goto END;
        }

        // ========== 3. md5 存在但此用户没有,更新引用计数 ==========
        sprintf(sql_cmd, "update file_info set count = %d where md5 = '%s'",
                file_ref_count + 1, md5);
        if (!db_conn->ExecutePassQuery(sql_cmd)) {
            md5_state = Md5Failed;
            goto END;
        }

        // ========== 4. 插入 user_file_list 记录 ==========
        struct timeval tv;
        struct tm *ptm;
        char time_str[128];
        gettimeofday(&tv, NULL);
        ptm = localtime(&tv.tv_sec);
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", ptm);

        sprintf(sql_cmd,
                "insert into user_file_list(user, md5, create_time, file_name, "
                "shared_status, pv) values ('%s', '%s', '%s', '%s', %d, %d)",
                user, md5, time_str, filename, 0, 0);
        if (!db_conn->ExecuteCreate(sql_cmd)) {
            // 插入失败,回滚引用计数
            sprintf(sql_cmd, "update file_info set count = %d where md5 = '%s'",
                    file_ref_count, md5);
            db_conn->ExecutePassQuery(sql_cmd);
            md5_state = Md5Failed;
            goto END;
        }

        // ========== 5. 更新 Redis 用户文件计数 ==========
        CacheIncrCount(cache_conn, FILE_USER_COUNT + string(user));

        md5_state = Md5Ok;  // code = 0,秒传成功
    } else {
        // md5 不存在,秒传失败
        md5_state = Md5Failed;  // code = 1
        goto END;
    }

END:
    /*
    秒传返回码:
        code = 0: 秒传成功
        code = 1: 秒传失败
        code = 5: 此用户已拥有此文件
    */
    encodeMd5Json((int)md5_state, str_json);
}

七、Token 验证实现(api_common.cc)

所有需要登录才能访问的 API(如文件上传、MD5 秒传)都需要先验证 Token:

//验证登录 token,成功返回 0,失败返回 -1
int VerifyToken(string &user_name, string &token) {
    int ret = 0;

    CacheManager *cache_manager = CacheManager::getInstance();
    CacheConn *cache_conn = cache_manager->GetCacheConn("token");
    AUTO_REL_CACHECONN(cache_manager, cache_conn);

    if (cache_conn) {
        // 从 Redis 中按 token 查找对应的用户名
        string temp_user_name = cache_conn->Get(token);
        if (temp_user_name == user_name) {
            ret = 0;  // Token 验证通过
        } else {
            ret = -1; // Token 与用户名不匹配
        }
    } else {
        ret = -1;  // 无法连接 Redis
    }

    return ret;
}

八、配置文件 tc_http_server.conf

所有配置项统一管理,通过 base/config_file_reader.cc 读取:

# ==================== HTTP 服务 ====================
http_bind_ip=0.0.0.0
http_bind_port=8081
num_event_loops=4

# ==================== FastDFS ====================
dfs_path_client=/etc/fdfs/client.conf
storage_web_server_ip=192.168.181.138
storage_web_server_port=80

# ==================== MySQL 主从配置 ====================
# 实例列表(逗号分隔)
DBInstances=tuchuang_master,tuchuang_slave

# 主库(用于写入)
tuchuang_master_host=localhost
tuchuang_master_port=3306
tuchuang_master_dbname=0voice_tuchuang
tuchuang_master_username=root
tuchuang_master_password=root123
tuchuang_master_maxconncnt=8

# 从库(用于读取)
tuchuang_slave_host=localhost
tuchuang_slave_port=3306
tuchuang_slave_dbname=0voice_tuchuang
tuchuang_slave_username=root
tuchuang_slave_password=root123
tuchuang_slave_maxconncnt=64

# ==================== Redis Token 连接池 ====================
CacheInstances=token,ranking_list

token_host=127.0.0.1
token_port=6379
token_db=0
token_maxconncnt=64

九、CMakeLists.txt

INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR})
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/base)
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/api)
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/mysql)
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/redis)

# 第三方依赖头文件
INCLUDE_DIRECTORIES(/usr/include/mysql)
INCLUDE_DIRECTORIES(/usr/include/jsoncpp)
INCLUDE_DIRECTORIES(/usr/include/fastdfs)

# 收集各目录源文件
AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR}/base BASE_LIST)
AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR}/api API_LIST)
AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR}/mysql MYSQL_LIST)
AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR}/redis REDIS_LIST)

# 链接第三方库
TARGET_LINK_LIBRARIES(test_main muduo_net jsoncpp mysqlclient uuid)

十、项目文件结构

test2/
|-- base/
|   |-- config_file_reader.cc # 配置文件读取
|   |-- config_file_reader.h
|   |-- util.h
|-- api/
|   |-- api_common.cc # 公共函数(Token 验证、工具函数)
|   |-- api_common.h
|   |-- api_login.cc             # 登录接口
|   |-- api_login.h
|   |-- api_md5.cc               # MD5 秒传接口
|   |-- api_md5.h
|   |-- api_register.cc          # 注册接口
|   |-- api_register.h
|   |-- api_upload.cc # 文件上传接口
|   |-- api_upload.h
|-- mysql/
|   |-- db_pool.cc               # MySQL 连接池
|   |-- db_pool.h
|-- redis/
|   |-- cache_pool.cc # Redis 连接池
|   |-- cache_pool.h
|   |-- hiredis.h                # hiredis 头文件
|-- http_conn.cc                 # HTTP 连接处理
|-- http_conn.h
|-- http_parser.cc               # HTTP 解析
|-- http_parser_wrapper.cc
|-- main.cc                      # 程序入口
|-- CMakeLists.txt
|-- tc_http_server.conf # 配置文件

十一、总结

功能 实现方式 关键文件
注册 MySQL 预处理插入,检测用户名重复 api_register.cc
登录 MySQL 查密码 + Redis 存 Token(UUID,24h过期) api_login.cc
Token 验证 Redis Get(key=token) -> 对比用户名 api_common.cc
文件上传 nginx 临时目录 -> fork/execvp 上传 FastDFS -> 写 MySQL api_upload.cc
MD5 秒传 查询 file_info.md5 -> 引用计数+1 -> 写 user_file_list api_md5.cc
MySQL 连接池 双主从连接池,自动归还 mysql/db_pool.cc
Redis 连接池 Token 连接池,支持字符串、计数器、ZSet redis/cache_pool.cc
配置管理 统一配置文件,统一读取入口 base/config_file_reader.cc

根据零声教育教学写作https://github .com/0voice

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐