目录
一:CHttpConn 类
二:注册
三:登录
四:获取文件
C/C++精品项目之图床共享云存储(1):基础组件-CSDN博客
C/C++精品项目之图床共享云存储(2):MySql连接池_mysql 连接池c++ ping-CSDN博客
C/C++精品项目之图床共享云存储(3):网络缓冲区类和main-CSDN博客
一:CHttpConn 类
我们上一期讲了一点点这个 CHttpConn 类,这一期继续讲解这个类。首先我们回顾一下,我们客户端与服务端创建好连接之后,分配了一个socket,然后我们在连接上之后,触发回调函数,创建了一个CHttpConn对象,这个对象和这个socket进行绑定,然后这个对象进行初始化,再一次传入新的回调函数,当我们触发不同的读写事件,那么进入不同的读写回调函数。
void httpconn_callback(void *callback_data, uint8_t msg, uint32_t handle,uint32_t uParam, void *pParam) {NOTUSED_ARG(uParam);NOTUSED_ARG(pParam);// convert void* to uint32_t, oopsuint32_t conn_handle = *((uint32_t *)(&callback_data)); //获取连接标识 ,也就是获得跟socket绑定的http的handleCHttpConn *pConn = FindHttpConnByHandle(conn_handle); //根据连接标识找到对应的http对象if (!pConn) { //如果没有则返回// LogWarn("no find conn_handle: {}", conn_handle);return;}pConn->AddRef(); //添加引用计数switch (msg) {case NETLIB_MSG_READ: //可读事件pConn->OnRead();break;case NETLIB_MSG_WRITE: //可写事件pConn->OnWrite();break;case NETLIB_MSG_CLOSE:pConn->OnClose(); //关闭事件break;default:LogError("!!!httpconn_callback error msg:{}", msg); //fix me,可以考虑如果这里触发,直接关闭该连接// pConn->Close();break;}pConn->ReleaseRef(); //释放引用计数,如果数据发送完毕,对应的http 连接在这个位置为析构对象
}
现在我们查看读事件中的代码:我们触发读事件,无非注册,登录,发送请求。我们在这个函数中,肯定是先将发送来的数据读出来,这个时候就用到我们网络缓冲区类了,读完数据后,要对数据进行解析。我们分别得到url和body的数据,我们根据传来的url进行区分不同的请求。而body是具体的一些参数。
//这里的onread函数:就是我们客户端向服务端发送数据,首先就是接收数据到读缓冲区,然后我们将数据拿出来,然后就是进行http解析,通过解析到不同的url \
我们能跳转到不同的处理函数中去。
void CHttpConn::OnRead() // CHttpConn业务层面的OnRead
{LogInfo("conn_handle_ = {}, socket_handle_ = {}", conn_handle_, socket_handle_);// 1. 把能读取的数据都读取出来for (;;) {uint32_t free_buf_len = in_buf_.GetAllocSize() - in_buf_.GetWriteOffset();if (free_buf_len < READ_BUF_SIZE + 1) //这里多预留一个字节的目的是加上结束符时不会越界,因为我们可能会发送多条消息 in_buf_.Extend(READ_BUF_SIZE + 1); //为了区分这些消息,我们每次接受完都要加上一个结束符,这样我们就可以通过结束符来区分不同的消息//读取socket数据int ret = netlib_recv(socket_handle_,in_buf_.GetBuffer() + in_buf_.GetWriteOffset(), //写道buf的写后面READ_BUF_SIZE);if (ret <= 0)break; //没有数据可以读取了in_buf_.IncWriteOffset(ret); // 更新下一个接收数据的位置}// 2.通过http模块解析http请求的数据char *in_buf = (char *)in_buf_.GetBuffer();uint32_t buf_len = in_buf_.GetWriteOffset();in_buf[buf_len] = '\0'; // 末尾加上结束符 方便分析结束位置和打印,这里之所以不越界是因为有预留in_buf_.Extend(READ_BUF_SIZE + 1)// 如果buf_len 过长可能是受到攻击,则断开连接 ,目前我们接受的所有数据长度不得大于2Kif (buf_len > 2048) {LogError("get too much data: {}", in_buf);Close();return;}LogInfo("buf_len: {}, in_buf: {}", buf_len, in_buf); //将请求的数据都打印出来,方便调试分析http请求// 解析http数据http_parser_.ParseHttpContent(in_buf, buf_len); // 1. 从socket接口读取数据;2.然后把数据放到buffer in_buf; 3.http解析if (http_parser_.IsReadAll()) {string url = http_parser_.GetUrl();string content = http_parser_.GetBodyContent();LogInfo("url: {}", url); // for debug// 根据url处理不同的业务 if (strncmp(url.c_str(), "/api/reg", 8) == 0) { // 注册 url 路由。 根据根据url快速找到对应的处理函数, 能不能使用map,hash_HandleRegisterRequest(url, content);} else if (strncmp(url.c_str(), "/api/login", 10) == 0) { // 登录_HandleLoginRequest(url, content);} else if (strncmp(url.c_str(), "/api/myfiles", 10) == 0) { //获取我的文件数量_HandleMyfilesRequest(url, content);} else {LogError("url unknown, url= {}", url);Close();}}
}
二:注册
对于注册来说,无非就是从数据库上查询和插入嘛。首先我们通过解析body中的所有参数,也就是我们注册所填的参数:姓名,昵称,密码,等等。我们通过一个http解析的函数将这些数据解析出来,然后进入到注册的具体环节。
int ApiRegisterUser(string &post_data, string &resp_json) {int ret = 0;string user_name;string nick_name;string pwd;string phone;string email;LogInfo("post_data: {}", post_data);// 判断数据是否为空if (post_data.empty()) {LogError("decodeRegisterJson failed");// 封装注册结果encodeRegisterJson(1, resp_json);return -1;}// 解析jsonif (decodeRegisterJson(post_data, user_name, nick_name, pwd, phone, email) < 0) {LogError("decodeRegisterJson failed");// 封装注册结果encodeRegisterJson(1, resp_json);return -1;}// 注册账号ret = registerUser(user_name, nick_name, pwd, phone, email);// 封装注册结果ret = encodeRegisterJson(ret, resp_json);return 0;
}
注册的具体环节也就是和数据库打交道,我们首先通过我们的MySql连接池获得一个空闲的连接,然后给数据库发送查询命令,看看是否存在了已经。已存在返回具体的值,不存在则进行插入环节。我们准备好具体的SQL命令,通过添加其中所需要的参数,然后执行。这样就注册成功了。
int registerUser(string &user_name, string &nick_name, string &pwd,string &phone, string &email) {int ret = 0;uint32_t user_id;CDBManager *db_manager = CDBManager::getInstance(); //通过数据库的manger,我们可以拿到相关数据库的连接池。CDBConn *db_conn = db_manager->GetDBConn("tuchuang_master");AUTO_REL_DBCONN(db_manager, db_conn); //栈上构建一个对象 退出的时候自动把连接规划连接池// 先查看用户是否存在string str_sql = FormatString("select * from user_info where user_name='%s'", user_name.c_str());LogInfo("执行: {}", str_sql);CResultSet *result_set = db_conn->ExecuteQuery(str_sql.c_str());if (result_set && result_set->Next()) { // 检测是否存在用户记录// 存在在返回LogWarn("id: {}, user_name: {} 已经存在", result_set->GetInt("id"), result_set->GetString("user_name"));delete result_set;ret = 2;} else { // 如果不存在则注册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);// mysql操作 如果不熟悉可以参考:https://www.yuque.com/linuxer/linux_senior/rcz4xl?singleDoc# 《mysql api c客户端》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); //用户id是自增id} else {LogError("insert user_info failed. {}", str_sql);ret = 1;}}delete stmt;}return ret;
}
注册成功我们也需要返回一些数据给客户端,首先就是通信的头部,然后就是将代表注册成功的信息填入我们要发送的数据中,然后发送。
// 账号注册处理
void CHttpConn::_HandleRegisterRequest(string &url, string &post_data) {string resp_json;int ret = ApiRegisterUser(post_data, resp_json);char *http_body = new char[HTTP_RESPONSE_HTML_MAX];uint32_t ulen = resp_json.length();snprintf(http_body, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, ulen,resp_json.c_str()); ret = Send((void *)http_body, strlen(http_body));delete[] http_body;LogInfo("Send remain = {}", ret);
}
三:登录
其实登录和注册也差不多,就是从数据库中查询是否有这个用户,先解析姓名和密码,然后通过一个函数验证这个姓名和密码是否存在于数据库中。
int ApiUserLogin(string &post_data, string &resp_json) {string user_name;string pwd;string token;// 判断数据是否为空if (post_data.empty()) {return -1;}// 解析jsonif (decodeLoginJson(post_data, user_name, pwd) < 0) {LogError("decodeRegisterJson failed");encodeLoginJson(1, token, resp_json);return -1;}// 验证账号和密码是否匹配if (verifyUserPassword(user_name, pwd) < 0) {LogError("verifyUserPassword failed");encodeLoginJson(1, token, resp_json);return -1;}// 生成token并存储在redisif (setToken(user_name, token) < 0) {LogError("setToken failed");encodeLoginJson(1, token, resp_json);return -1;} else {encodeLoginJson(0, token, resp_json);return 0;}
}
这个函数就是进行检查的,先连接数据库,执行相应的命令,返回具体的结果。
int verifyUserPassword(string &user_name, 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;strSql = FormatString("select password from user_info where user_name='%s'", user_name.c_str());CResultSet *result_set = db_conn->ExecuteQuery(strSql.c_str());uint32_t nowtime = time(NULL);if (result_set && result_set->Next()) {// 存在在返回string password = result_set->GetString("password");LogInfo("mysql-pwd: {}, user-pwd: {}", password, pwd);if (result_set->GetString("password") == pwd)ret = 0;elseret = -1;} else { // 如果不存在则注册ret = -1;}delete result_set;return ret;
}
当我们验证登陆成功之后,我们开始创建一个token,token是什么呢?
Token 的中文有人翻译成 “令牌”,意思就是,你拿着这个令牌,才能过一些关卡。
Token 是一个用户自定义的任意字符串。在成功提交了开发者自定义的这个字符串之后,Token 的值会保存到服务器后台。只有服务器和客户端前端知道这个字符串,于是 Token 就成了这两者之间的密钥,它可以让服务器确认请求是来自客户端还是恶意的第三方。这里所说的 Token,本质上就是 http session。使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
1) 客户端使用用户名跟密码请求登录
2) 服务端收到请求,去验证用户名与密码
3) 验证成功后,服务端生成一个 Token,这个 Token 可以存储在内存、磁盘、或者数据库里,再把这个 Token 发送给客户端
4) 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage5) 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
6) 服务端收到请求,然后去验证客户端请求里面带着的 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;
}
在最后我们不是已经生成好token了吗,我们要将这个token返回给客户端,让客户端前端和服务器之间有令牌。
void CHttpConn::_HandleLoginRequest(string &url, string &post_data)
{string resp_json;int ret = ApiUserLogin( post_data, resp_json);char *http_body = new char[HTTP_RESPONSE_HTML_MAX];uint32_t ulen = resp_json.length();snprintf(http_body, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, ulen,resp_json.c_str()); ret = Send((void *)http_body, strlen(http_body));delete[] http_body;LogInfo("Send remain = {}", ret);
}
四:获取文件
获取文件列表,这个操作是在登陆成功之后做的事情,那么让我们看看其中token的作用,以及我们怎么获取文件列表的。首先我们看到的就是一些变量,下面就是一个解析cmd的函数,这里的cmd是什么意思呢?比如我们要访问 127.0.0.1:80/api/myfiles&cmd=normal ,我们写入这个url,那么就代表我们要对文件进行排序操作。而count这个变量是在具体的json里面的。如果为0,那就是返回全部。
接下来除了一开始的解析 json 参数外,最重要的也就是验证token,因为这里的操作已经是在登录以后得了。
//获取文件列表,首先就是要解析json,看看是要获取文件个数还是文件信息,无论是什么,首先就是要获取token,这个是为了确保使我们当前的连接 \
如果是数量,那么我们就直接通过传入来的用户名进行查找,当然查找之前要验证token,所以我们要连接mysql和redis,当token通过就开始查找数量
//如果是对文件进行排序的话,和它差不多,首先获取具体命令,验证token,连接mysql,拿取数据。
int ApiMyfiles(string &url, string &post_data, string &resp_json) {// 解析url有没有命令// count 获取用户文件个数// display 获取用户文件信息,展示到前端char cmd[20];string user_name;string token; //这个tocken是在服务之间进行传递的,用来验证用户的合法性,它存在于json中。int ret = 0;int start = 0; //文件起点int count = 0; //文件个数//解析命令 解析url获取自定义参数,这里的自定义参数是指按什么顺序返回,比如从大到小QueryParseKeyValue(url.c_str(), "cmd", cmd, NULL);LogInfo("url: {}, cmd: {} ",url, cmd);if (strcmp(cmd, "count") == 0) {// 解析jsonif (decodeCountJson(post_data, user_name, token) < 0) {encodeCountJson(1, 0, resp_json);LogError("decodeCountJson failed");return -1;}//验证登陆token,成功返回0,失败-1ret = VerifyToken(user_name, token); // util_cgi.hif (ret == 0) {// 获取文件数量if (handleUserFilesCount(user_name, count) < 0) { //获取用户文件个数LogError("handleUserFilesCount failed");encodeCountJson(1, 0, resp_json);} else {LogInfo("handleUserFilesCount ok, count: {}", count);encodeCountJson(0, count, resp_json);}} else {LogError("VerifyToken failed");encodeCountJson(1, 0, resp_json);}return 0;} else {if ((strcmp(cmd, "normal") != 0) && (strcmp(cmd, "pvasc") != 0) &&(strcmp(cmd, "pvdesc") != 0)) {LogError("unknow cmd: {}", cmd);encodeCountJson(1, 0, resp_json);}//获取用户文件信息 127.0.0.1:80/api/myfiles&cmd=normal//按下载量升序 127.0.0.1:80/api/myfiles?cmd=pvasc//按下载量降序127.0.0.1:80/api/myfiles?cmd=pvdescret = decodeFileslistJson(post_data, user_name, token, start,count); //通过json包获取信息LogInfo("user_name: {}, token:{}, start: {}, count: {}", user_name,token, start, count);if (ret == 0) {//验证登陆token,成功返回0,失败-1ret = VerifyToken(user_name, token); // util_cgi.hif (ret == 0) {string str_cmd = cmd;if (getUserFileList(str_cmd, user_name, start, count,resp_json) < 0) { //获取用户文件列表LogError("getUserFileList failed");encodeCountJson(1, 0, resp_json);}} else {LogError("VerifyToken failed");encodeCountJson(1, 0, resp_json);}} else {LogError("decodeFileslistJson failed");encodeCountJson(1, 0, resp_json);}}return 0;
}
我们这里的验证token是需要连接redis数据库,为什么用户数据和文件数据存放在mysql,而token需要放在redis,因为redis是内存数据库,速度很快,我们每一次的内部操作都需要验证token,所以存放在redis中。我们通过传入来的用户信息,来获得具体的token。
int VerifyToken(string &user_name, string &token) {int ret = 0;CacheManager *cache_manager = CacheManager::getInstance();// increase message countCacheConn *cache_conn = cache_manager->GetCacheConn("token");AUTO_REL_CACHECONN(cache_manager, cache_conn);if (cache_conn) {string tmp_token = cache_conn->Get(user_name);if (tmp_token == token) {ret = 0;} else {ret = -1;}} else {ret = -1;}return ret;
}
接下来肯定就是具体的查找逻辑了,当然在这操作之前,肯定要获取一个空闲的mysql连接了。
int DBGetUserFilesCountByUsername(CDBConn *db_conn, string user_name,int &count) {count = 0;int ret = 0;// 先查看用户是否存在string str_sql;str_sql = FormatString("select count(*) from user_file_list where user='%s'", user_name.c_str());LogInfo("执行: {}", str_sql);CResultSet *result_set = db_conn->ExecuteQuery(str_sql.c_str());if (result_set && result_set->Next()) {// 存在在返回count = result_set->GetInt("count(*)");LogInfo("count: {}", count);ret = 0;delete result_set;} else if (!result_set) { // 操作失败LogError("{} 操作失败", str_sql);LogError("{} 操作失败", str_sql);ret = -1;} else {// 没有记录则初始化记录数量为0ret = 0;LogInfo("没有记录: count: {}", count);}return ret;
}
而这里就是查找全部的文件信息了,一样是线连接mysql数据库,然后设置sql命令,返回结果之后,我们将结果返回给客户端。
int getUserFileList(string &cmd, string &user_name, int &start, int &count,string &str_json) {LogInfo("getUserFileList into");int ret = 0;int total = 0;string str_sql;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);ret = getUserFilesCount(db_conn, cache_conn, user_name,total); // 总共的文件数量if (ret < 0) {LogError("getUserFilesCount failed");return -1;} else {if (total == 0) {Json::Value root;root["code"] = 0;root["count"] = 0;root["total"] = 0;Json::FastWriter writer;str_json = writer.write(root);LogWarn("getUserFilesCount = 0");return 0;}}//多表指定行范围查询if (cmd == "normal") //获取用户文件信息{str_sql = FormatString("select user_file_list.*, file_info.url, file_info.size, file_info.type from file_info, user_file_list where user = '%s' \and file_info.md5 = user_file_list.md5 limit %d, %d",user_name.c_str(), start, count);} else {LogError("unknown cmd: {}", cmd);return -1;}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;}
}
本篇主要讲解,注册,登录,token,获取文件的一些具体操作,其中比较重要的就是token,而其他的业务操作全都是和数据库打交道,将查询的结果返回到客户端这里。0voice · GitHub