一、图床项目介绍
实现一个能够上传、存储、分享图片的后端项目。
1)上传:上传文件,并且如果上传的文件在数据库中有记录,即md5匹配,则实现秒传效果。
2)分享(共享)文件:共享文件给其他已注册的用户。其他注册用户可以在 “共享文件–>文件列表” 中看到共享的文件,并且可转存到自己的文件列表或者下载。同样在自己的 “共享文件–>文件列表”中,可以查看共享文件的信息,也可以取消共享若取消共享,除非其他用户已经转存,否则就看不到。
3)分享图片:生成链接,其他未注册用户可以根据链接查看已分享的图片可在 “共享文件 -->我的共享图片” 中看到相关浏览信息也可以取消分享。
二、图床项目架构
文件上传逻辑:
客户端上传图片⟹ \Longrightarrow⟹nginx代理⟹ \Longrightarrow⟹通过nginx-upload-module,上传到某个临时目录⟹ \Longrightarrow⟹透传到后端服务程序tc-http-server⟹ \Longrightarrow⟹reactor网络模型监听到任务,解析http请求,然后将任务交由线程池处理⟹ \Longrightarrow⟹把文件信息存储到数据库,同时把文件上传到fastdfs
主要的http api接口
reactor网络模型
三、图床功能实现
3.1 注册功能
// 回发信息给前端的格式 #define HTTP_RESPONSE_HTML \ "HTTP/1.1 200 OK\r\n" \ "Connection:close\r\n" \ "Content-Length:%d\r\n" \ "Content-Type:application/json;charset=utf-8\r\n\r\n%s" // 注册函数 int ApiRegisterUser(uint32_t conn_uuid, string &url, string &post_data) { string str_json; UNUSED(url); int ret = 0; string user_name; string nick_name; string pwd; string phone; string email; LogInfo("uuid: {}, url: {}, post_data: {}", conn_uuid, url, post_data); // 1、判断数据是否为空 if (post_data.empty()) { LogError("decodeRegisterJson failed"); encodeRegisterJson(1, str_json); ret = -1; goto END; } // 2、解析json if (decodeRegisterJson(post_data, user_name, nick_name, pwd, phone, email) < 0) { LogError("decodeRegisterJson failed"); encodeRegisterJson(1, str_json); ret = -1; goto END; } // 3、注册账号 ret = registerUser(user_name, nick_name, pwd, phone, email); ret = encodeRegisterJson(ret, str_json); // 这里是裸数据 // 发送到回发队列里 END: // 3、把状态结果按回复消息的格式打包 char *str_content = new char[HTTP_RESPONSE_HTML_MAX]; uint32_t ulen = str_json.length(); snprintf(str_content, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, ulen, str_json.c_str()); str_json = str_content; // 4、添加到回发队列 CHttpConn::AddResponseData(conn_uuid, str_json); delete str_content; return ret; }
重点看一下registerUser(user_name, nick_name, pwd, phone, email);的处理过程:先查看用户是否存在,存在就返回,不存在需要就把用户信息添加到数据库,完成注册。
int registerUser(string &user_name, string &nick_name, string &pwd, string &phone, string &email) { int ret = 0; uint32_t user_id; // 1、获取数据库连接池 CDBManager *db_manager = CDBManager::getInstance(); CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave"); AUTO_REL_DBCONN(db_manager, db_conn); // 2、先查看用户是否存在 string str_sql; str_sql = formatString2("select * from user_info where user_name='%s'", user_name.c_str()); CResultSet *result_set = db_conn->ExecuteQuery(str_sql.c_str()); // 执行sql语句 if (result_set && result_set->Next()) { // 2.1 存在用户记录,返回 LogWarn("id: {}, user_name: {} 已经存在", result_set->GetInt("id"), result_set->GetString("user_name")); delete result_set; ret = 2; } else { // 2.2 如果不存在,注册 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(?,?,?,?,?,?)"; LogInfo("执行: {}", str_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); stmt->SetParam(index++, nick_name); stmt->SetParam(index++, pwd); stmt->SetParam(index++, phone); stmt->SetParam(index++, email); stmt->SetParam(index++, c_time); bool bRet = stmt->ExecuteUpdate(); if (bRet) { ret = 0; user_id = db_conn->GetInsertId(); LogInfo("insert user_id: {}", user_id); } else { LogError("insert user_info failed. {}", str_sql); ret = 1; } } delete stmt; } return ret; }
3.2 登录功能
int ApiUserLogin(u_int32_t conn_uuid, std::string &url, std::string &post_data) { UNUSED(url); string user_name; string pwd; string token; string str_json; // 1、判断数据是否为空 if (post_data.empty()) { encodeLoginJson(1, token, str_json); goto END; } // 2、解析json if (decodeLoginJson(post_data, user_name, pwd) < 0) { LogError("decodeRegisterJson failed"); encodeLoginJson(1, token, str_json); goto END; } // 3、验证账号和密码是否匹配 if (verifyUserPassword(user_name, pwd) < 0) { LogError("verifyUserPassword failed"); encodeLoginJson(1, token, str_json); goto END; } // 4、生成token,并存储到redis中 if (setToken(user_name, token) < 0) { LogError("setToken failed"); encodeLoginJson(1, token, str_json); goto END; } // 5、加载 我的文件数量 我的分享图片数量 if (loadMyfilesCountAndSharepictureCount(user_name) < 0) { LogError("loadMyfilesCountAndSharepictureCount failed"); encodeLoginJson(1, token, str_json); goto END; } encodeLoginJson(0, token, str_json); END: char *str_content = new char[HTTP_RESPONSE_HTML_MAX]; uint32_t ulen = str_json.length(); snprintf(str_content, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, ulen, str_json.c_str()); str_json = str_content; CHttpConn::AddResponseData(conn_uuid, str_json); delete str_content; return 0; }
关注一下三个过程:
verifyUserPassword(user_name, pwd),验证账号密码是否匹配。
setToken(user_name, token),生成token,并存储到redis中。所谓token相当于令牌,前面账号密码验证过后,说明你是有户口的人,放你进来。但是你在访问其他功能的时候,需要有个通关令牌,一个只有服务器和客户端前端知道这个字符串,来再次验证你的身份而不用每次都通过账号密码。于是 Token 就成了这两者之间的密钥,它可以让服务器确认请求是来自客户端还是恶意的第三方。
int setToken(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); token = RandomString(32); // 随机32个字母 if (cache_conn) { //用户名:token, 86400有效时间为24小时 cache_conn->SetEx(user_name, 86400, token); // redis做超时 } else { ret = -1; } return ret; }
loadMyfilesCountAndSharepictureCount(user_name) :加载 我的文件数量 和 我的分享图片数量
int loadMyfilesCountAndSharepictureCount(string &user_name) { int64_t redis_file_count = 0; int mysq_file_count = 0; // 1. 获取mysql 连接池 CDBManager *db_manager = CDBManager::getInstance(); CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave"); AUTO_REL_DBCONN(db_manager, db_conn); // 2. 获取redis 连接池 CacheManager *cache_manager = CacheManager::getInstance(); CacheConn *cache_conn = cache_manager->GetCacheConn("token"); AUTO_REL_CACHECONN(cache_manager, cache_conn); // 3. 从mysql加载 用户文件个数 if (DBGetUserFilesCountByUsername(db_conn, user_name, mysq_file_count) < 0) { LogError("DBGetUserFilesCountByUsername failed"); return -1; } // 4. 存储到redis redis_file_count = (int64_t)mysq_file_count; if (CacheSetCount(cache_conn, FILE_USER_COUNT + user_name, redis_file_count) < 0) // 失败了那下次继续从mysql加载 { LogError("DBGetUserFilesCountByUsername failed"); return -1; } LogInfo("FILE_USER_COUNT: {}", redis_file_count); // 5. 从mysql加载 我的分享图片数量 if (DBGetSharePictureCountByUsername(db_conn, user_name, mysq_file_count) < 0) { LogError("DBGetUserFilesCountByUsername failed"); return -1; } // 6. 存储到redis redis_file_count = (int64_t)mysq_file_count; if (CacheSetCount(cache_conn, SHARE_PIC_COUNT + user_name, redis_file_count) < 0) // 失败了那下次继续从mysql加载 { LogError("DBGetUserFilesCountByUsername failed"); return -1; } LogInfo("SHARE_PIC_COUNT: {}", redis_file_count); return 0; }
3.3 用户文件列表
查看我的文件时候,显示的是图片信息。从浏览器的抓包直观看到,我们请求的两个命令
myfiles?cmd=count:文件数量
myfiles?cmd=normal:文件列表
当然,我们还有按排序
/api/myfiles&cmd=pvasc 文件列表( ( 按下载量升序) )
/api/myfiles&cmd= pvdesc 文件列表( ( 按下载量降序) )
int ApiMyfiles(string &url, string &post_data, string &str_json) { // 解析url有没有命令 // count 获取用户文件个数 // display 获取用户文件信息,展示到前端 char cmd[20]; string user_name; string token; int ret = 0; int start = 0; //文件起点 int count = 0; //文件个数 //1、解析命令 解析url获取自定义参数 QueryParseKeyValue(url.c_str(), "cmd", cmd, NULL); LogInfo("url: {}, cmd: {} ",url, cmd); if (strcmp(cmd, "count") == 0) { // 2. cmd == 'count' 获取文件数量 // 2.1 解析json if (decodeCountJson(post_data, user_name, token) < 0) { encodeCountJson(1, 0, str_json); LogError("decodeCountJson failed"); return -1; } //2.2 验证登陆token,成功返回0,失败-1 ret = VerifyToken(user_name, token); // util_cgi.h if (ret == 0) { // 2.3 获取文件数量 if (handleUserFilesCount(user_name, count) < 0) { //获取用户文件个数 LogError("handleUserFilesCount failed"); encodeCountJson(1, 0, str_json); } else { LogInfo("handleUserFilesCount ok, count: {}", count); encodeCountJson(0, count, str_json); } } else { LogError("VerifyToken failed"); encodeCountJson(1, 0, str_json); } return 0; } else { // 3. cmd == 'normal' 或者 ‘pvdesc’ 或者 ‘pvasc’ 获取文件列表 if ((strcmp(cmd, "normal") != 0) && (strcmp(cmd, "pvasc") != 0) && (strcmp(cmd, "pvdesc") != 0)) { LogError("unknow cmd: {}", cmd); encodeCountJson(1, 0, str_json); } // 3.1 通过json包获取信息 ret = decodeFileslistJson(post_data, user_name, token, start,count); LogInfo("user_name: {}, token:{}, start: {}, count:", user_name,token, start, count); if (ret == 0) { // 3.2 验证登陆token,成功返回0,失败-1 ret = VerifyToken(user_name, token); // util_cgi.h if (ret == 0) { string str_cmd = cmd; // 3.3 获取用户文件列表 if (getUserFileList(str_cmd, user_name, start, count,str_json) < 0) { LogError("getUserFileList failed"); encodeCountJson(1, 0, str_json); } } else { LogError("VerifyToken failed"); encodeCountJson(1, 0, str_json); } } else { LogError("decodeFileslistJson failed"); encodeCountJson(1, 0, str_json); } } return 0; }
1、myfiles?cmd=count:文件数量
需要注意获取文件数量,我们是先从redis获取,如果redis没有,再从MySQL获取。如果MySQL有,从MySQL获取,并把数据写入redis。如果MySQL也没有,就报错。
int handleUserFilesCount(string &user_name, int &count) { 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); int ret = getUserFilesCount(db_conn, cache_conn, user_name, count); return ret; } int getUserFilesCount(CDBConn *db_conn, CacheConn *cache_conn, string &user_name, int &count) { int ret = 0; int64_t file_count = 0; // 先查看用户是否存在 string str_sql; // 1. 先从redis里面获取 if (CacheGetCount(cache_conn, FILE_USER_COUNT + user_name, file_count) < 0) { LogWarn("CacheGetCount failed"); // 有可能是因为没有key,不要急于判断为错误 file_count = 0; ret = -1; } // 2. redis没有,从mysql获取。若MySQL获取到,再写入redis if (file_count == 0) { // 2.1 从mysql加载 count = 0; if (DBGetUserFilesCountByUsername(db_conn, user_name, count) < 0) { // 如果MySQL也没有就报错 LogError("DBGetUserFilesCountByUsername failed"); return -1; } // 2.2 将获取的数据写入redis file_count = (int64_t)count; if (CacheSetCount(cache_conn, FILE_USER_COUNT + user_name, file_count) < 0) { LogError("CacheSetCount failed"); return -1; } } count = file_count; return ret; }
2、myfiles?cmd=normal:文件列表
这是我们后端程序返回的结果,前端根据这些字段解析展现
{ "code": 0, "count": 3, "files": [ { "create_time": "2023-08-29 06:45:34", "file_name": "黄山景区高清地图.jpg", "md5": "825a70d2c0132eca6afe84694c984120", "pv": 1, "share_status": 1, "size": 875885, "type": "jpg", "url": "http://192.168.3.128:80/group1/M00/00/00/wKgDgGTtlA6AYQyyAA1dbSSfUFk261.jpg", "user": "handsome1" } ], "total": 1 }
“code”: 0 正常,1 失败
“count”: 返回的当前文件数量,比如 2
“total”: 个人文件总共的数量
“user”: 用户名称,
“md5”: md5 值,
“create_time”: 创建时间,
“file_name”: 文件名,
“share_status”: 共享状态, 0 为没有共享, 1 为共享
“pv”: 文件下载量,下载一次加 1
“url”: URL,
“size”: 文件大小,
“type”: 文件类型
/api/myfiles&cmd=pvasc 文件列表( ( 按下载量升序) ) ------ 升序:order by pv asc
/api/myfiles&cmd= pvdesc 文件列表( ( 按下载量降序) ) ----- 降序:order by pv desc
这两个和normal一样,只是sql语句中的查询方式不一样。
getUserFileList()函数的大概就是,获取连接池,然后编写sql执行语句,然后交由连接池db_conn->ExecuteQuery(str_sql.c_str())执行,最后根据结果解析。这就不再赘述,都差不多。主要看看解析成json打包的过程
LogInfo("执行: {}", str_sql); CResultSet *result_set = db_conn->ExecuteQuery(str_sql.c_str()); if (result_set) { // 遍历所有的内容 // 获取大小 int file_index = 0; Json::Value root, files; root["code"] = 0; while (result_set->Next()) { Json::Value file; file["user"] = result_set->GetString("user"); file["md5"] = result_set->GetString("md5"); file["create_time"] = result_set->GetString("create_time"); file["file_name"] = result_set->GetString("file_name"); file["share_status"] = result_set->GetInt("shared_status"); file["pv"] = result_set->GetInt("pv"); file["url"] = result_set->GetString("url"); file["size"] = result_set->GetInt("size"); file["type"] = result_set->GetString("type"); files[file_index] = file; file_index++; } root["files"] = files; root["count"] = file_index; root["total"] = total; Json::FastWriter writer; str_json = writer.write(root); delete result_set; return 0; } else { LogError("{} 操作失败", str_sql); return -1; }
对于result_set->GetString("user")。在数据库连接池设计中,我们获取到一行数据,我们将<列名和列数>插入到map中。后续我们可以根据要获取的字段名,得到列数,再到结果集查找具体数据。
举个例子,map里面有<user,1>、<md5,2>。通过_GetIndex(user)可知道user字段的数据在结果集的第一列,然后通过row_[1],获取结果集row_的第一个,也就是user对应的数据。
char *CResultSet::GetString(const char *key) { int idx = _GetIndex(key); if (idx == -1) { return NULL; } else { return row_[idx]; // 列 } }