图床项目详解-1

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 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]; // 列
    }
}
目录
相关文章
|
7月前
|
存储 JavaScript
【开源图床】使用Typora+PicGo+Gitee搭建个人博客图床
【开源图床】使用Typora+PicGo+Gitee搭建个人博客图床
104 2
|
7月前
|
存储 JavaScript 网络架构
【开源图床】使用Typora+PicGo+Github+CDN搭建个人博客图床
【开源图床】使用Typora+PicGo+Github+CDN搭建个人博客图床
305 3
|
7月前
|
存储 定位技术 Windows
GitHub与PicGo搭建免费稳定图床并实现Typora内复制自动上传
GitHub与PicGo搭建免费稳定图床并实现Typora内复制自动上传
101 1
|
NoSQL 关系型数据库 MySQL
图床项目详解-3
图床项目详解
70 0
|
NoSQL 关系型数据库 MySQL
图床项目详解-2
图床项目详解
120 0
|
前端开发 算法 JavaScript
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享(下)
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享(下)
201 0
|
存储 Web App开发 JavaScript
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享(上)
从零开始开发图床工具:使用 Gitee 和 Electron 实现上传、管理和分享(上)
249 0
|
对象存储
利用Typora+码云来搭建你的个人图床
图床一般是指储存图片的服务器,有国内和国外之分。国外的图床由于有空间距离等因素决定访问速度很慢影响图片显示速度。就是专门用来存放图片,同时允许你把图片对外连接的网上空间,不少图床都是免费的。
138 0
|
前端开发 Java 程序员
搭建一个属于自己的图床
搭建一个属于自己的图床
搭建一个属于自己的图床
|
对象存储 开发者
如何管理属于自己的图床?
作为开发者的你,平时肯定会有很多自己的笔记,记录着许多工作问题、学习记录等等。近年来有很多支持在线编辑的平台,例如wolai、语雀等等,它们或多或少的都支持在线的markdown的编辑,也支持导入导出等丰富功能。但是对笔者来说,这些平台虽然功能繁多,但因个人习惯不同,笔者还是习惯了Typora的简约的风格,也便于持久化存放、不会因为网络等问题访问不了笔记等原因。在实际使用的过程中,会遇到在笔记中图片上传的问题,在发布到其他平台的时候由于都是本地图片,还需要上传一次,这个时候图床就派上用场了。
390 0
如何管理属于自己的图床?