图床项目实现:注册登录 + 文件上传等功能的完善
用户上传文件时,客户端先计算文件 MD5。如果服务器上已存在相同 MD5 的文件(其他用户上传过),直接引用该文件记录,无需真正上传,节省带宽和时间。功能实现方式关键文件注册MySQL 预处理插入,检测用户名重复登录MySQL 查密码 + Redis 存 Token(UUID,24h过期)Token 验证Redis Get(key=token) -> 对比用户名文件上传nginx 临时目录 ->
·
一、项目整体架构
客户端 ( 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 | 否 | 电话 |
| 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
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)