1. 项目概述
这个图床项目是基于C++开发的Web服务,主要实现了用户注册登录和文件上传功能,并将数据持久化存储到MySQL数据库中。项目采用了典型的Web服务架构,包含HTTP请求处理、数据库操作、文件存储等核心模块。
作为开发者,我在实现过程中遇到了不少技术难点,比如:
- 如何高效处理HTTP请求和响应
- 数据库连接池的管理与优化
- 文件上传的完整流程处理
- 系统各模块间的协同工作
下面我将详细拆解每个功能模块的实现细节,分享我在开发过程中的经验和教训。
2. 数据库设计与实现
2.1 数据库表结构设计
项目中主要使用了三张核心表:
- user_info 用户信息表:
sql复制CREATE TABLE `user_info` (
`id` int NOT NULL AUTO_INCREMENT,
`user_name` varchar(255) NOT NULL,
`nick_name` varchar(255) DEFAULT NULL,
`password` varchar(255) NOT NULL,
`phone` varchar(20) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_name` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- file_info 文件信息表:
sql复制CREATE TABLE `file_info` (
`id` int NOT NULL AUTO_INCREMENT,
`md5` varchar(255) NOT NULL,
`file_id` varchar(255) NOT NULL,
`url` varchar(512) NOT NULL,
`size` bigint NOT NULL,
`type` varchar(20) NOT NULL,
`count` int NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `md5` (`md5`),
UNIQUE KEY `file_id` (`file_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- user_file_list 用户文件关联表:
sql复制CREATE TABLE `user_file_list` (
`id` int NOT NULL AUTO_INCREMENT,
`user` varchar(255) NOT NULL,
`md5` varchar(255) NOT NULL,
`create_time` datetime NOT NULL,
`file_name` varchar(255) NOT NULL,
`shared_status` tinyint NOT NULL DEFAULT '0',
`pv` int NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2 数据库连接池实现
项目中使用了单例模式的数据库连接池管理类CDBManager,主要特点包括:
- 连接池初始化:
cpp复制CDBManager* CDBManager::getInstance() {
if (!s_db_manager) {
s_db_manager = new CDBManager();
if (s_db_manager->Init()) {
delete s_db_manager;
s_db_manager = NULL;
}
}
return s_db_manager;
}
- 连接获取与释放:
cpp复制CDBConn* CDBManager::GetDBConn(const char* dbpool_name) {
map<string, CDBPool*>::iterator it = m_dbpool_map.find(dbpool_name);
if (it == m_dbpool_map.end()) {
return NULL;
} else {
return it->second->GetDBConn();
}
}
void CDBManager::RelDBConn(CDBConn* pConn) {
if (!pConn) {
return;
}
map<string, CDBPool*>::iterator it = m_dbpool_map.find(pConn->m_pool_name);
if (it != m_dbpool_map.end()) {
it->second->RelDBConn(pConn);
}
}
- RAII自动管理:
项目中使用了AUTO_REL_DBCONN宏自动管理连接生命周期:
cpp复制#define AUTO_REL_DBCONN(db_manager, db_conn) \
std::shared_ptr<CDBConn> auto_rel_##db_conn(db_conn, [db_manager](CDBConn* p) { \
if (p) db_manager->RelDBConn(p); \
})
提示:数据库连接池的配置参数从配置文件中读取,包括最大连接数、连接超时等,这在实际项目中非常重要,可以避免连接泄漏和性能问题。
3. 用户注册登录实现
3.1 用户注册流程
用户注册的核心逻辑在ApiRegisterUser函数中实现:
- 参数校验与JSON解析:
cpp复制int ApiRegisterUser(string &post_data, string &resp_json) {
// 检查post数据是否为空
if (post_data.empty()) {
LOG_ERROR << "decodeRegisterJson failed";
encdoeRegisterJson(1, resp_json);
return -1;
}
// 解析JSON数据
string user_name, nick_name, pwd, phone, email;
int ret = decodeRegisterJson(post_data, user_name, nick_name, pwd, phone, email);
if(ret < 0) {
encdoeRegisterJson(1, resp_json);
return -1;
}
// 注册用户
ret = registerUser(user_name, nick_name, pwd, phone, email);
encdoeRegisterJson(ret, resp_json);
return 0;
}
- 数据库操作:
cpp复制int registerUser(string &user_name, string &nick_name, string &pwd,
string &phone, string &email) {
// 获取数据库连接
CDBConn *db_conn = db_manager->GetDBConn("tuchuang_master");
AUTO_REL_DBCONN(db_manager, db_conn);
// 检查用户名是否已存在
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()) {
// 用户名已存在
delete result_set;
return 2;
}
// 插入新用户
time_t now = time(NULL);
char create_time[TIME_STRING_LEN];
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(?,?,?,?,?,?)";
CPrepareStatement *stmt = new CPrepareStatement();
if (stmt->Init(db_conn->GetMysql(), str_sql)) {
uint32_t index = 0;
stmt->SetParam(index++, user_name);
stmt->SetParam(index++, nick_name);
stmt->SetParam(index++, pwd);
stmt->SetParam(index++, phone);
stmt->SetParam(index++, email);
stmt->SetParam(index++, create_time);
bool bRet = stmt->ExecuteUpdate();
if (bRet) {
uint32_t user_id = db_conn->GetInsertId();
LOG_INFO << "insert user_id: " << user_id << ", user_name: " << user_name;
ret = 0;
} else {
LOG_ERROR << "insert user_info failed. " << str_sql;
ret = 1;
}
}
delete stmt;
return ret;
}
3.2 用户登录流程
用户登录的核心逻辑在ApiUserLogin函数中实现:
- 登录验证:
cpp复制int ApiUserLogin(std::string &post_data, std::string &resp_json) {
string user_name, pwd, token;
// 参数检查
if (post_data.empty()) {
encodeLoginJson(1, token, resp_json);
return -1;
}
// 解析JSON
if (decodeLoginJson(post_data, user_name, pwd) < 0) {
encodeLoginJson(1, token, resp_json);
return -1;
}
// 验证密码
if (verifyUserPassword(user_name, pwd) < 0) {
encodeLoginJson(1, token, resp_json);
return -1;
}
// 生成并设置token
if (setToken(user_name, token) < 0) {
encodeLoginJson(1, token, resp_json);
return -1;
}
// 返回登录结果
encodeLoginJson(0, token, resp_json);
return 0;
}
- 密码验证:
cpp复制int verifyUserPassword(string &user_name, string &pwd) {
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");
if (password == pwd) {
ret = 0; // 密码匹配
} else {
ret = -1; // 密码不匹配
}
} else {
ret = -1; // 用户不存在
}
delete result_set;
return ret;
}
- Token生成与存储:
cpp复制int setToken(string &user_name, string &token) {
CacheConn *cache_conn = cache_manager->GetCacheConn("token");
AUTO_REL_CACHECONN(cache_manager, cache_conn);
token = generateUUID(); // 生成UUID作为token
if (cache_conn) {
// 存储token,设置24小时有效期
cache_conn->SetEx(token, 86400, user_name);
return 0;
}
return -1;
}
注意事项:在实际项目中,密码应该进行加密存储,而不是明文保存。可以使用bcrypt等加密算法对密码进行哈希处理,提高安全性。
4. 文件上传实现
4.1 文件上传整体流程
文件上传的整体流程如下:
- 客户端通过HTTP POST上传文件
- Nginx接收文件并暂存到临时目录
- Nginx将文件信息转发给后端服务
- 后端服务处理文件上传:
- 解析上传的文件信息
- 将文件上传到FastDFS
- 将文件信息存储到数据库
- 返回文件访问URL给客户端
4.2 文件上传核心代码实现
4.2.1 文件信息解析
cpp复制// 解析boundary
char *begin = (char *)post_data.c_str();
char *p1 = strstr(begin, "\r\n");
if (p1 == NULL) {
LOG_ERROR << "wrong no boundary!";
return -1;
}
// 获取boundary
char boundary[TEMP_BUF_MAX_LEN] = {0};
strncpy(boundary, begin, p1 - begin);
boundary[p1 - begin] = '\0';
LOG_INFO << "boundary: " << boundary;
// 解析文件名
begin = p1 + 2;
p2 = strstr(begin, "name=\"file_name\"");
if (!p2) {
LOG_ERROR << "wrong no file_name!";
return -1;
}
p2 = strstr(begin, "\r\n");
p2 += 4;
begin = p2;
p2 = strstr(begin, "\r\n");
strncpy(file_name, begin, p2 - begin);
LOG_INFO << "file_name: " << file_name;
// 解析文件临时路径
begin = p2 + 2;
p2 = strstr(begin, "name=\"file_path\"");
if (!p2) {
LOG_ERROR << "wrong no file_path!";
return -1;
}
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;
4.2.2 文件上传到FastDFS
cpp复制int uploadFileToFastDfs(char *file_path, char *fileid) {
int fd[2];
if (pipe(fd) < 0) {
LOG_ERROR << "pipe error";
return -1;
}
pid_t pid = fork();
if (pid < 0) {
LOG_ERROR << "fork error";
return -1;
}
if (pid == 0) { // 子进程
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("fdfs_upload_file", "fdfs_upload_file",
s_dfs_path_client.c_str(), file_path, NULL);
LOG_ERROR << "execlp fdfs_upload_file error";
close(fd[1]);
exit(1);
} else { // 父进程
close(fd[1]);
read(fd[0], fileid, TEMP_BUF_MAX_LEN);
TrimSpace(fileid);
if (strlen(fileid) == 0) {
LOG_ERROR << "upload failed";
return -1;
}
wait(NULL);
close(fd[0]);
}
return 0;
}
4.2.3 获取文件完整URL
cpp复制int getFullUrlByFileid(char *fileid, char *fdfs_file_url) {
char fdfs_file_stat_buf[TEMP_BUF_MAX_LEN] = {0};
int fd[2];
if (pipe(fd) < 0) {
LOG_ERROR << "pipe error";
return -1;
}
pid_t pid = fork();
if (pid < 0) {
LOG_ERROR << "fork error";
return -1;
}
if (pid == 0) { // 子进程
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("fdfs_file_info", "fdfs_file_info",
s_dfs_path_client.c_str(), fileid, NULL);
LOG_ERROR << "execlp fdfs_file_info error";
close(fd[1]);
exit(1);
} else { // 父进程
close(fd[1]);
read(fd[0], fdfs_file_stat_buf, TEMP_BUF_MAX_LEN);
wait(NULL);
close(fd[0]);
// 拼接完整URL
strcat(fdfs_file_url, "http://");
strcat(fdfs_file_url, s_storage_web_server_ip.c_str());
strcat(fdfs_file_url, "/");
strcat(fdfs_file_url, fileid);
}
return 0;
}
4.2.4 文件信息存储到数据库
cpp复制int storeFileinfo(CDBConn *db_conn, CacheConn *cache_conn, char *user,
char *filename, char *md5, long size, char *fileid,
const char *fdfs_file_url) {
char suffix[SUFFIX_LEN];
char sql_cmd[SQL_MAX_LEN] = {0};
// 获取文件后缀
GetFileSuffix(filename, suffix);
// 插入文件信息
sprintf(sql_cmd,
"insert into file_info (md5, file_id, url, size, type, count) "
"values ('%s', '%s', '%s', '%ld', '%s', %d)",
md5, fileid, fdfs_file_url, size, suffix, 1);
if (!db_conn->ExecuteCreate(sql_cmd)) {
LOG_ERROR << sql_cmd << " 操作失败";
return -1;
}
// 插入用户文件关联
time_t now = time(NULL);
char create_time[TIME_STRING_LEN];
strftime(create_time, TIME_STRING_LEN - 1, "%Y-%m-%d %H:%M:%S", localtime(&now));
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, create_time, filename, 0, 0);
if (!db_conn->ExecuteCreate(sql_cmd)) {
LOG_ERROR << sql_cmd << " 操作失败";
return -1;
}
return 0;
}
提示:在实际项目中,文件上传需要考虑大文件上传、断点续传、文件秒传等功能。可以通过分块上传、文件MD5校验等方式优化上传体验。
5. 项目配置与部署
5.1 配置文件解析
项目使用配置文件tc_http_server.conf管理各项参数:
ini复制# HTTP服务配置
http_bind_ip=0.0.0.0
http_bind_port=8081
num_event_loops=1
num_threads=8
# 日志级别
log_level=2
# FastDFS配置
dfs_path_client=/etc/fdfs/client.conf
storage_web_server_ip=192.168.6.131
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=123456
tuchuang_master_maxconncnt=8
# Redis配置
CacheInstances=token,ranking_list
token_host=127.0.0.1
token_port=6379
token_db=0
token_maxconncnt=64
5.2 Nginx配置
Nginx作为反向代理和文件上传处理前端,相关配置如下:
nginx复制server {
listen 80;
server_name your.domain.com;
# 文件上传配置
client_max_body_size 100M;
location /api/upload {
upload_pass @backend;
upload_store /tmp/nginx_upload;
upload_store_access user:rw group:rw all:rw;
upload_set_form_field $upload_field_name.name "$upload_file_name";
upload_set_form_field $upload_field_name.path "$upload_tmp_path";
upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";
upload_pass_form_field "^submit$|^description$";
upload_cleanup 400 404 499 500-505;
}
location @backend {
proxy_pass http://127.0.0.1:8081;
}
# 静态文件访问
location /group1/M00 {
root /data/fastdfs/storage/data;
ngx_fastdfs_module;
}
}
5.3 系统部署
- 依赖安装:
bash复制# FastDFS客户端
sudo apt-get install libfdfsclient-dev
# MySQL客户端
sudo apt-get install libmysqlclient-dev
# Redis客户端
sudo apt-get install libhiredis-dev
# UUID库
sudo apt-get install uuid-dev
- 编译项目:
bash复制mkdir build && cd build
cmake ..
make
- 启动服务:
bash复制sudo ./tc_http_server -c ../conf/tc_http_server.conf
注意事项:由于文件上传需要操作临时目录,服务需要以root权限运行。在生产环境中,应该创建专用用户并配置适当的权限,而不是直接使用root。
6. 性能优化与问题排查
6.1 性能优化点
-
数据库连接池:
- 合理设置最大连接数,避免连接过多导致数据库压力大
- 使用读写分离,查询操作使用从库,减轻主库压力
-
Redis缓存:
- 使用Redis缓存用户token,减少数据库查询
- 设置合理的过期时间,避免缓存堆积
-
文件上传:
- 使用Nginx处理文件上传,减轻后端压力
- 临时文件及时清理,避免磁盘空间占用
-
日志优化:
- 生产环境适当提高日志级别,减少IO操作
- 使用异步日志,避免阻塞主线程
6.2 常见问题排查
-
文件上传失败:
- 检查FastDFS服务是否正常运行
- 检查临时目录权限是否正确
- 检查磁盘空间是否充足
-
数据库连接失败:
- 检查数据库服务是否正常运行
- 检查连接参数是否正确
- 检查最大连接数是否设置合理
-
Token验证失败:
- 检查Redis服务是否正常运行
- 检查Token过期时间设置
- 检查Token生成和验证逻辑
-
性能瓶颈:
- 使用性能分析工具定位热点代码
- 检查线程池配置是否合理
- 检查是否有不必要的同步操作
6.3 监控与维护
-
系统监控:
- 监控服务进程状态
- 监控系统资源使用情况(CPU、内存、磁盘、网络)
- 监控数据库连接池使用情况
-
日志分析:
- 定期分析错误日志,及时发现潜在问题
- 统计接口调用情况,了解系统负载
-
数据备份:
- 定期备份数据库
- 重要文件定期备份
在实际开发过程中,我发现最耗时的部分是文件上传和数据库操作。通过优化文件处理流程和使用连接池,系统性能得到了显著提升。另外,合理的错误处理和日志记录对于问题排查非常重要,建议在开发初期就建立完善的日志系统。
