基于muduo库的图床云共享存储项目(二)
基于muduo库的图床云共享存储项目(二)
- 文件传输和接口设计
- HTTP服务构建
- HTTP API设计
- /api/reg注册接口
- 代码实现
- /api/login登录接口
- 代码实现
- 阶段性功能验证
在上一节当中,我们主要介绍了图床云共享存储项目的架构以及一些依赖的组件,接下来我们就需要来手把手实现对应的后端代码了。
文件传输和接口设计
HTTP服务构建
图床项目主要是http请求,所以首先构建http应用,在这儿我们是基于 muduo 构建http server,代码如下:
#include <iostream>
#include "muduo/net/TcpServer.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/net/EventLoop.h"
#include "muduo/base/Logging.h"using namespace muduo;
using namespace muduo::net;class HttpServer {public://构造函数 loop主线程的EventLoop, addr封装ip,port, name服务名字,num_event_loops多少个subReactorHttpServer(EventLoop *loop, const InetAddress &addr, const std::string &name, int num_event_loops): loop_(loop), server_(loop, addr, name){// 各类回调函数的设置server_.setConnectionCallback(std::bind(&HttpServer::onConnection, this, std::placeholders::_1));}void start() {server_.start();}private:// TcpServer回调的设置void onConnection(const TcpConnectionPtr &conn) {LOG_INFO << "onConnectio: " << conn.get();}// 业务相关的处理void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp time) {LOG_INFO << "onMessage: " << conn.get();}void onWriteComplete(const TcpConnectionPtr& conn) {LOG_INFO << "onWriteComplete " << conn.get();}private:TcpServer server_; // 用于每个连接的回调函数 接收新连接 收发数据EventLoop *loop_ = nullptr; // 主线程的EventLoop
};int main()
{std::cout << "hello tucuang!!! tc_http_src2\n";uint16_t http_bind_port = 8081; // 端口号const char *http_bind_ip = "0.0.0.0"; // ip地址int32_t num_event_loops = 4; // subRecator的数量EventLoop loop; // 主循环的loopInetAddress addr(http_bind_ip, http_bind_port);LOG_INFO << "port: " << http_bind_port;HttpServer server(&loop, addr, "HttpServer", num_event_loops);server.start();loop.loop();return 0;
}
基于api fox 来进行测试,我们可以看见,端口是可以正常来进行连接的:
接下来我们写一段测试代码,对 http 请求进行处理并且回发数据,看看是否是正常的:
#include <iostream>
#include "muduo/net/TcpServer.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/net/EventLoop.h"
#include "muduo/base/Logging.h"using namespace muduo;
using namespace muduo::net;class HttpServer {public://构造函数 loop主线程的EventLoop, addr封装ip,port, name服务名字,num_event_loops多少个subReactorHttpServer(EventLoop *loop, const InetAddress &addr, const std::string &name, int num_event_loops): loop_(loop), server_(loop, addr, name){// 各类回调函数的设置server_.setConnectionCallback(std::bind(&HttpServer::onConnection, this, std::placeholders::_1));server_.setMessageCallback(std::bind(&HttpServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));server_.setWriteCompleteCallback(std::bind(&HttpServer::onWriteComplete, this, std::placeholders::_1));// subReactor对应线程数量设置server_.setThreadNum(num_event_loops);}void start() {server_.start();}private:// TcpServer回调的设置void onConnection(const TcpConnectionPtr &conn) {LOG_INFO << "onConnectio: " << conn.get();}// 业务相关的处理void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp time) {LOG_INFO << "onMessage: " << conn.get();// 获取buf的数据const char* in_buf = buf->peek();LOG_INFO << "get msg: " << in_buf;char *resp_content = new char[256];string str_json = "{\"code\": 0}"; uint32_t len_json = str_json.size();//暂时先放这里#define HTTP_RESPONSE_REQ \"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"snprintf(resp_content, 256, HTTP_RESPONSE_REQ, len_json, str_json.c_str()); conn->send(resp_content);conn->shutdown();}void onWriteComplete(const TcpConnectionPtr& conn) {LOG_INFO << "onWriteComplete " << conn.get();}private:TcpServer server_; // 用于每个连接的回调函数 接收新连接 收发数据EventLoop *loop_ = nullptr; // 主线程的EventLoop
};int main()
{std::cout << "hello tucuang!!! tc_http_src2\n";uint16_t http_bind_port = 8081; // 端口号const char *http_bind_ip = "0.0.0.0"; // ip地址int32_t num_event_loops = 4; // subRecator的数量EventLoop loop; // 主循环的loopInetAddress addr(http_bind_ip, http_bind_port);LOG_INFO << "port: " << http_bind_port;HttpServer server(&loop, addr, "HttpServer", num_event_loops);server.start();loop.loop();return 0;
}
测试结果如下,可以看见正常回发数据:
接下来就是对 httpconnection 类进行封装,他需要跟我们 tcpconnection 实现互联,包括对应的收到数据,解析数据,回发数据,都是通过 httpconnection 类来进行实现的:
http_conn.h
#ifndef __HTTP_CONN_H__
#define __HTTP_CONN_H__
#include "http_parser_wrapper.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/net/Buffer.h"using namespace muduo;
using namespace muduo::net;
using namespace std;class CHttpConn : public std::enable_shared_from_this<CHttpConn>
{
public:// 构造函数CHttpConn(TcpConnectionPtr tcp_con);// 析构函数virtual ~CHttpConn();// 业务处理void OnRead(Buffer *buf);private:TcpConnectionPtr tcp_conn_; // 对应的tcp connectionuint32_t uuid_; // 生成唯一idCHttpParserWrapper http_parser; // http协议解析对象
};using CHttpConnPtr = std::shared_ptr<CHttpConn>;#endif
http_conn.c
#include "http_conn.h"
#include "muduo/base/Logging.h" CHttpConn::CHttpConn(TcpConnectionPtr tcp_conn): tcp_conn_(tcp_conn)
{// 构造对应的uuiduuid_ = std::any_cast<uint32_t>(tcp_conn_->getContext());LOG_INFO << "构造CHttpConn uuid: "<< uuid_ ;
}void CHttpConn::OnRead(Buffer *buf) {// 获取buf的数据const char* in_buf = buf->peek();// LOG_INFO << "get msg: " << in_buf;// 获取对应的数据长度int32_t length = buf->readableBytes();// 对url进行解析http_parser.ParseHttpContent(in_buf, length);string url = http_parser.GetUrlString();string content = http_parser.GetBodyContentString();LOG_INFO << "url: " << url << ", content: " << content;if (http_parser.IsReadAll()) {char *resp_content = new char[256];string str_json = "{\"code\": 0}"; uint32_t len_json = str_json.size();//暂时先放这里#define HTTP_RESPONSE_REQ \"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"snprintf(resp_content, 256, HTTP_RESPONSE_REQ, len_json, str_json.c_str()); tcp_conn_->send(resp_content);}
}CHttpConn::~CHttpConn() {LOG_INFO << "析构CHttpConn uuid: "<< uuid_ ;
}
注意:
- 每一个连接都会对应一个 http 请求,肯定会存在多个连接的场景,我们就需要建立对应的 http 连接的映射关系,因为后期我们是需要进行识别的,当前采用的就是生成一个唯一的 uuid 的方法,当建立一个连接以后,对应的 uuid 就++,然后去识别对应不同的 http 连接;
- OnRead函数其实就是收到数据以后进行解析,然后回发数据,我们只是现在将其封装在了 httpconnectio 类中进行处理,最终还是在主函数的 onMessage 中进行调用即可。
main.cc
#include <iostream>
#include "muduo/net/TcpServer.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/net/EventLoop.h"
#include "muduo/base/Logging.h"
#include "http_parser.h"
#include "http_parser_wrapper.h"
#include "http_conn.h"using namespace muduo;
using namespace muduo::net;// 用于保存对应的http请求的映射关系
std::map<uint32_t, CHttpConnPtr> s_http_map;class HttpServer {
public://构造函数 loop主线程的EventLoop, addr封装ip,port, name服务名字,num_event_loops多少个subReactorHttpServer(EventLoop *loop, const InetAddress &addr, const std::string &name, int num_event_loops): loop_(loop), server_(loop, addr, name){// 各类回调函数的设置server_.setConnectionCallback(std::bind(&HttpServer::onConnection, this, std::placeholders::_1));server_.setMessageCallback(std::bind(&HttpServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));server_.setWriteCompleteCallback(std::bind(&HttpServer::onWriteComplete, this, std::placeholders::_1));// subReactor对应线程数量设置server_.setThreadNum(num_event_loops);}void start() {server_.start();}private:// TcpServer回调的设置void onConnection(const TcpConnectionPtr &conn) {if (conn->connected()){uint32_t uuid = conn_uuid_generator_++;// 设置tcp的唯一标识conn->setContext(uuid);CHttpConnPtr http_conn = std::make_shared<CHttpConn>(conn);// 建立对应映射关系s_http_map.insert({uuid, http_conn});LOG_INFO << "onConnection new conn: " << conn.get();} else {// 删除对应映射关系uint32_t uuid = std::any_cast<uint32_t>(conn->getContext());s_http_map.erase(uuid);LOG_INFO << "onConnection dis conn: " << conn.get();}}// 业务相关的处理void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp time) {LOG_INFO << "onMessage: " << conn.get();uint32_t uuid = std::any_cast<uint32_t>(conn->getContext());CHttpConnPtr &http_conn = s_http_map[uuid];//处理 相关业务http_conn->OnRead(buf); // 直接在io线程处理}void onWriteComplete(const TcpConnectionPtr& conn) {LOG_INFO << "onWriteComplete " << conn.get();}private:TcpServer server_; // 用于每个连接的回调函数 接收新连接 收发数据EventLoop *loop_ = nullptr; // 主线程的EventLoopstd::atomic<uint32_t> conn_uuid_generator_ = 0; // 这里是用于表示唯一http请求,不会一直保持链接
};int main()
{std::cout << "hello tucuang!!! tc_http_src2\n";uint16_t http_bind_port = 8081; // 端口号const char *http_bind_ip = "0.0.0.0"; // ip地址int32_t num_event_loops = 4; // subRecator的数量EventLoop loop; // 主循环的loopInetAddress addr(http_bind_ip, http_bind_port);LOG_INFO << "port: " << http_bind_port;HttpServer server(&loop, addr, "HttpServer", num_event_loops);server.start();loop.loop();return 0;
}
- onMessage 就是相应的业务处理,收到数据,回发数据,onConnection 其实就是 http 请求发过来,就需要来建立连接,然后构建对应的 http 请求的映射关系,表示当前已经建立一个新的连接了,后续就是对业务进行处理了;
- onMessage 和 onConnection 都需要设置成对应的回调函数,在 HttpServer 构造函数初始化的过程就设置,然后一旦有 http 连接发过来就会立马调用回调函数建立连接,然后对对应的请求进行相应的处理工作。
HTTP API设计
HTTP API设计大多数是基于JSON格式,这是有原因的。JSON构建起来很简单,因为大多数语言都内置了JSON支持。
JSON/HTTP对于基础设施项目的API来说是一个很好的选择,我们项目也是采用 HTTP + JSON的方式通过客户端向服务器请求数据。
主要的接口:
- /api/reg 注册
- /api/login 登录
- /api/myfiles 用户文件列表
- /api/md5 文件秒传检测
- /api/upload 上传文件
- /api/sharefiles 共享文件
- /api/dealfile 分享/删除文件
- /api/dealsharefile 处理分享文件相关
- /api/sharepic 图片分享相关
本文主要是针对于注册以及登录的接口实现,其它接口在后续的文章当中也会进行实现。
/api/reg注册接口
注册是一个简单的HTTP接口,根据用户输入的注册信息,创建一个新的用户。
请求URL
URL | http://192.168.1.6:8081/api/reg |
---|---|
请求方式 | POST |
HTTP 版本 | 1.1 |
Content-Type | application/json |
请求参数
请求参数要区分必填字段和可选字段, 如果是必填字段,服务端没有检测该字段的时候 直接返回失败。
参数名 | 含义 | 规则说明 | 是否必须 | 缺省值 |
---|---|---|---|---|
邮箱 | 必须符合email规范 | 可选 | 无 | |
firstPwd | 密码 | md5加密后的值 | 必填 | 无 |
nickName | 用户昵称 | 不能超过32个字符 | 必填 | 无 |
phone | 手机号码 | 不能超过16个字符 | 可选 | 无 |
userName | 用户名称 | 不能超过32个字符 | 必填 | 无 |
应答参数
名称 | 含义 | 规则说明 |
---|---|---|
code | 结果值 | 0:成功 1:失败 2:用户存在 |
代码实现
api_register.h
#ifndef _API_REGISTER_H_
#define _API_REGISTER_H_
#include <iostream>// 用于用户登录信息的注册
int ApiRegisterUser(std::string &post_data, std::string &resp_json);#endif
api_register.cc
#include "api_register.h"
#include "muduo/base/Logging.h" // Logger日志头文件
#include <jsoncpp/json/json.h>// 封装对应结果的序列化接口
int encdoeRegisterJson(int code, std::string& str_json) {Json::Value root;root["code"] = code;Json::FastWriter writer;str_json = writer.write(root);return 0;
}// 对json进行反序列化
int decodeRegisterJson(const std::string &str_json, std::string &user_name,std::string &nick_name, std::string &pwd, std::string &phone,std::string &email)
{bool res;Json::Value root;Json::Reader jsonReader;res = jsonReader.parse(str_json, root); if (!res) {LOG_ERROR << "parse reg json failed ";return -1;}// 用户名if (root["userName"].isNull()) {LOG_ERROR << "userName null";return -1;}user_name = root["userName"].asString();// 昵称if (root["nickName"].isNull()) {LOG_ERROR << "nickName null";return -1;}nick_name = root["nickName"].asString();//密码if (root["firstPwd"].isNull()) {LOG_ERROR << "firstPwd null";return -1;}pwd = root["firstPwd"].asString();//电话 非必须if (root["phone"].isNull()) {LOG_WARN << "phone null";} else {phone = root["phone"].asString();}//邮箱 非必须if (root["email"].isNull()) {LOG_WARN << "email null";} else {email = root["email"].asString();}return 0;
}int registerUser(std::string &user_name, std::string &nick_name, std::string &pwd,std::string &phone, std::string &email) {int ret = 0;// 还没有处理,先直接返回0return ret;
}// 用户注册接口
int ApiRegisterUser(std::string &post_data, std::string &resp_json) {int ret = 0;// 关于用户注册信息的一些变量std::string user_name;std::string nick_name;std::string pwd;std::string phone;std::string email;LOG_INFO << "post_data: " << post_data << "\n";// 判断当前传输的数据是否为空if (post_data.empty()) {LOG_ERROR << "post_data is empty!!!";// 序列化,返回对应的结果给客户端// code = 1encdoeRegisterJson(1, resp_json);return -1;}// 进行数据的反序列化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;
}
api_register.cc 文件主要实现的就是对于用户注册的序列化以及反序列化:
- 对于用户的注册信息,我们需要实现的功能就是如果当前用户存在,就在数据库中进行查找,如果不存在,就进行注册;
- 对于客户端发送过来的数据,我们首先就要进行反序列化,转化为服务端的处理规则的数据,然后在进行对应的处理,最终为客户端返回对应的结果即可。
/api/login登录接口
登录,根据用户输入的登录信息,登录进入到后台系统。
请求URL
URL | http://192.168.1.6:8081/api/reg |
---|---|
请求方式 | POST |
HTTP 版本 | 1.1 |
Content-Type | application/json |
请求参数
参数名 | 含义 | 规则说明 | 是否必须 | 缺省值 |
---|---|---|---|---|
pwd | 密码 | md5加密后的值 | 必填 | 无 |
user | 用户名称 | 不能超过32个字符 | 必填 | 无 |
应答参数
名称 | 含义 | 规则说明 |
---|---|---|
code | 结果值 | 0: 成功 1: 失败 |
token | 令牌 | 每次登录后,生成的token不一样,后续其他接口请求时,需要带上token,用来校验请求的合法性 |
代码实现
api_login.h
#ifndef _API_LOGIN_H_
#define _API_LOGIN_H_
#include <iostream>
int ApiUserLogin(std::string &post_data, std::string &resp_json);#endif // ! _API_LOGIN_H_
api_login.cc
#include "api_register.h"
#include "muduo/base/Logging.h" // Logger日志头文件
#include <jsoncpp/json/json.h>// 封装登录结果的json
int encodeLoginJson(int code, std::string &token, std::string &str_json) {Json::Value root;root["code"] = code;if (code == 0) {root["token"] = token; // 正常返回的时候才写入token}Json::FastWriter writer;str_json = writer.write(root);return 0;
}// 解析登录信息
int decodeLoginJson(const std::string &str_json, std::string &user_name,std::string &pwd) {bool res;Json::Value root;Json::Reader jsonReader;res = jsonReader.parse(str_json, root);if (!res) {LOG_ERROR << "parse login json failed ";return -1;}// 用户名if (root["user"].isNull()) {LOG_ERROR << "user null";return -1;}user_name = root["user"].asString();//密码if (root["pwd"].isNull()) {LOG_ERROR << "pwd null";return -1;}pwd = root["pwd"].asString();return 0;
}// 验证账号密码是否匹配
int verifyUserPassword(std::string &user_name, std::string &pwd) {int ret = 0;// 这里暂时不做处理,因为这里还没有涉及数据库return ret;
}// 生成token信息
int setToken(std::string &user_name, std::string &token) {int ret = 0;token = "1234";//更新到redisreturn ret;
}// 登录接口
int ApiUserLogin(std::string &post_data, std::string &resp_json) {// 登录所用的用户名和密码std::string user_name;std::string pwd;// token跟后续的操作有关,这儿需要返回一个生成的唯一tokenstd::string token;if (post_data.empty()) {LOG_INFO << "post_data is empty!!!";encodeLoginJson(1, token, resp_json);return -1;}// 解析对应的json文件if (decodeLoginJson(post_data, user_name, pwd) < 0) {LOG_ERROR << "decodeRegisterJson failed";encodeLoginJson(1, token, resp_json);return -1;}// 验证账号和密码是否匹配if (verifyUserPassword(user_name, pwd) < 0) {LOG_ERROR << "verifyUserPassword failed";encodeLoginJson(1, token, resp_json);return -1;}// 生成token信息if (setToken(user_name, token) < 0) {LOG_ERROR << "setToken failed";encodeLoginJson(1, token, resp_json);return -1;}// 封装登录结果encodeLoginJson(0, token, resp_json);return 0;}
api_login.cc 文件主要实现的就是对于用户的序列化以及反序列化,跟 api_register.cc 文件的逻辑其实大差不差,只是各自的功能不一样:
- 因为当前还没有引入数据库操作,所以这儿检查密码与用户名的并没有实现,只是直接返回 code = 0 成功的这样一个操作;
- 每次登录后,会生成一个 token,生成的 token不一样,后续其他接口请求时,需要带上token,用来校验请求的合法性,token 是要存储在 Redis 当中的,这儿我们目前也没有进行实现,所以成功默认返回一个值即可。
当前功能已经实现,对应的数据就需要在 http server 当中进行处理,所以我们就需要添加对应的用户注册以及登录的处理接口:
http_conn.cc
#include "http_conn.h"
#include "muduo/base/Logging.h"
#include "api/api_login.h"
#include "api/api_register.h"#define HTTP_RESPONSE_JSON_MAX 4096
#define HTTP_RESPONSE_JSON \"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"#define HTTP_RESPONSE_HTML \"HTTP/1.1 200 OK\r\n" \"Connection:close\r\n" \"Content-Length:%d\r\n" \"Content-Type:text/html;charset=utf-8\r\n\r\n%s"#define HTTP_RESPONSE_BAD_REQ \"HTTP/1.1 400 Bad\r\n" \"Connection:close\r\n" \"Content-Length:%d\r\n" \"Content-Type:application/json;charset=utf-8\r\n\r\n%s"#define HTTP_RESPONSE_REQ \"HTTP/1.1 404 OK\r\n" \"Connection:close\r\n" \"Content-Length:%d\r\n" \"Content-Type:application/json;charset=utf-8\r\n\r\n%s"CHttpConn::CHttpConn(TcpConnectionPtr tcp_conn): tcp_conn_(tcp_conn)
{// 构造对应的uuiduuid_ = std::any_cast<uint32_t>(tcp_conn_->getContext());LOG_INFO << "构造CHttpConn uuid: "<< uuid_ ;
}void CHttpConn::OnRead(Buffer *buf) {// 获取buf的数据const char* in_buf = buf->peek();// LOG_INFO << "get msg: " << in_buf;// 获取对应的数据长度int32_t length = buf->readableBytes();// 对url进行解析http_parser.ParseHttpContent(in_buf, length);if (http_parser.IsReadAll()) {string url = http_parser.GetUrlString();string content = http_parser.GetBodyContentString();LOG_INFO << "url: " << url << ", content: " << content;if (strncmp(url.c_str(), "/api/reg", 8) == 0) {_HandleRegisterRequest(content);} else if (strncmp(url.c_str(), "/api/login", 10) == 0) {_HandleLoginRequest(content);} else {char *resp_content = new char[256];string str_json = "{\"code\": 0}"; uint32_t len_json = str_json.size();snprintf(resp_content, 256, HTTP_RESPONSE_REQ, len_json, str_json.c_str()); tcp_conn_->send(resp_content);}}
}// 账号注册处理
int CHttpConn::_HandleRegisterRequest(std::string &post_data) {string resp_json;// 调用注册的进行处理int ret = ApiRegisterUser(post_data, resp_json);// 封装http_bodychar *http_body = new char[HTTP_RESPONSE_JSON_MAX];uint32_t ulen = resp_json.length();snprintf(http_body, HTTP_RESPONSE_JSON_MAX, HTTP_RESPONSE_JSON, ulen,resp_json.c_str()); tcp_conn_->send(http_body);delete[] http_body;LOG_INFO << " uuid: "<< uuid_;return 0;
}int CHttpConn::_HandleLoginRequest(std::string &post_data)
{string str_json;// 调用登录的接口进行处理int ret = ApiUserLogin(post_data, str_json);// 封装返回内容char *szContent = new char[HTTP_RESPONSE_JSON_MAX];uint32_t ulen = str_json.length();snprintf(szContent, HTTP_RESPONSE_JSON_MAX, HTTP_RESPONSE_JSON, ulen, str_json.c_str()); tcp_conn_->send(szContent);delete [] szContent;LOG_INFO << " uuid: "<< uuid_; return 0;
}CHttpConn::~CHttpConn() {LOG_INFO << "析构CHttpConn uuid: "<< uuid_ ;
}
当我们读取到对应的 http 请求以后就会进行解析,通过对应的 url 进行判断,如果是注册就调用对应的注册处理函数,如果是登录就调用对应的登录处理函数,依次进行处理就好了。
阶段性功能验证
当前我们已经实现了注册以及登录的接口,我们先来进行验证一下:
可以看见,对应的功能是可以正常的实现的,本篇文章对于项目的介绍就到这儿,后续会继续进行更新。