图床项目详解-1

本文涉及的产品
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生内存数据库 Tair,内存型 2GB
简介: 图床项目详解

一、图床项目介绍


实现一个能够上传、存储、分享图片的后端项目。


1)上传:上传文件,并且如果上传的文件在数据库中有记录,即md5匹配,则实现秒传效果。

2)分享(共享)文件:共享文件给其他已注册的用户。其他注册用户可以在 “共享文件–>文件列表” 中看到共享的文件,并且可转存到自己的文件列表或者下载。同样在自己的 “共享文件–>文件列表”中,可以查看共享文件的信息,也可以取消共享若取消共享,除非其他用户已经转存,否则就看不到。

3)分享图片:生成链接,其他未注册用户可以根据链接查看已分享的图片可在 “共享文件 -->我的共享图片” 中看到相关浏览信息也可以取消分享。



二、图床项目架构


文件上传逻辑:

客户端上传图片⟹ \Longrightarrow⟹nginx代理⟹ \Longrightarrow⟹通过nginx-upload-module,上传到某个临时目录⟹ \Longrightarrow⟹透传到后端服务程序tc-http-server⟹ \Longrightarrow⟹reactor网络模型监听到任务,解析http请求,然后将任务交由线程池处理⟹ \Longrightarrow⟹把文件信息存储到数据库,同时把文件上传到fastdfs

40821de8dd56490b80f56acc84830d99.png

主要的http api接口

576f62d33a4c43db5d416921229f9cad_afbf16569ddd47d3a83d132a2a4e16d1.png

reactor网络模型

483f638cd299ad974c5e0fd9e2c99246_ad0593e104af4855a6cf9cf04e7791fd.png


三、图床功能实现


3.1 注册功能


1f07035606b80a990603e21a6a00e5c1_7cc68ed9f46a42c4bbd2db36988ef0f9.png

// 回发信息给前端的格式
#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 登录功能

b57dde8d7bd4323e8afdb2841dc0b0b3_bec6b421914542958bc9fdfba5b6b04b.png


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:文件列表

bb605be7f76b99e6acf147ec53cf84e1_585e024b81b54eb9b2c54fbcab8b9279.png

当然,我们还有按排序

/api/myfiles&cmd=pvasc 文件列表( ( 按下载量升序) )

/api/myfiles&cmd= pvdesc 文件列表( ( 按下载量降序) )

d32917ec40bac13fa8cf72e7f767dc5d_9bac50af5e664d8eb8713bc4b977c3ad.png


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:文件数量

5c9b4a209cf83bc520a82060587ae107_ce5899ddf0404b5e823b9d75e64ff90c.png

需要注意获取文件数量,我们是先从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”: 文件类型

f50123d6c00a92a8cce965630b2cb9cc_4932e9eddf1d4545bcf0cfc69735db7a.png


/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]; // 列
    }
}
目录
相关文章
|
2月前
|
存储 JavaScript
【开源图床】使用Typora+PicGo+Gitee搭建个人博客图床
【开源图床】使用Typora+PicGo+Gitee搭建个人博客图床
50 2
|
2月前
|
存储 JavaScript 网络架构
【开源图床】使用Typora+PicGo+Github+CDN搭建个人博客图床
【开源图床】使用Typora+PicGo+Github+CDN搭建个人博客图床
145 3
|
12月前
|
存储 对象存储 CDN
Hexo从0到1搭建博客系列04:图床的最佳实践
Hexo从0到1搭建博客系列04:图床的最佳实践
291 0
|
10月前
|
域名解析 应用服务中间件 网络安全
|
14天前
|
Linux
Typore+PicGo+GitHub图床搭建
Typore+PicGo+GitHub图床搭建
11 1
|
9月前
|
开发者
picgo+GitHub搭建图床
picgo+GitHub搭建图床
91 0
|
2月前
|
存储 定位技术 Windows
GitHub与PicGo搭建免费稳定图床并实现Typora内复制自动上传
GitHub与PicGo搭建免费稳定图床并实现Typora内复制自动上传
|
8月前
|
NoSQL 关系型数据库 MySQL
图床项目详解-3
图床项目详解
36 0
|
8月前
|
NoSQL 关系型数据库 MySQL
图床项目详解-2
图床项目详解
68 0
|
前端开发 Java 程序员
搭建一个属于自己的图床
搭建一个属于自己的图床
搭建一个属于自己的图床