项目链接
项目架构目前项目包含四个子目录,其中http定义了http消息的解析封装和响应逻辑,tcpserver负责底层的网络数据收发,jsoncpp负责json数据的解析封装,log是项目日志(这一部分暂时没写)。
使用方法导入头文件"cin.h",与gin使用方法基本一致
#include
#include
#include "json.h"
#include "cin.hpp"
using namespace std;
void handle1(context* c) {
c->STRING(Status_OK, "hi, have a good day");
}
void handle2(context* c) {
string resp = "I heard your name: " + c->req_.getQuery()["name"];
c->STRING(Status_OK, resp);
}
void handle3(context* c) {
string resp = "dynamic param: " + c->params_["param"];
c->STRING(Status_OK, resp);
}
void handle4(context* c) {
cout << "use group handle4" << endl;
c->next();
}
void handle5(context* c) {
Json::Value root;
root["name"] = "lemon";
root["sex"] = "man";
root["age"] = 23;
Json::Value hobby;
hobby["sport"] = "football";
hobby["else"] = "sing";
root["hobby"] = hobby;
c->JSON(Status_OK, root);
}
int main(int argc, const char * argv[]) {
Cweb c("127.0.0.1", 6666); //初始化
c.GET("/api/sayhi", handle1);
c.GET("/api/echo", handle2); //带参数 ?key=value
c.GET("/api/dynamic/:param", handle3); //动态路由
group* g1 = c.Group("/group"); //分组 *** 作
g1->USE(handle4); //中间件
g1->GET("/sayhi", handle5);
c.run(4);//运行
return 0;
}
接口测试
http消息结构如下图所示, 可以看到请求行/响应行、请求头/响应头、请求体/响应体之间通过"\r\n“分割,请求行中字段通过空格分割。路由和参数之间通过"?"分割,参数和参数之间通过“&"分割,如“http:// www.fishbay.cn: 80/mix/76.html?name=kelvin&password=123456”,于是便可以根据以上特征对http消息进行解析。
http消息的解析代码:
bool HttpServer::parse(ByteBuffer *buf, timer receiveTime) {
bool status = true;
bool finished = false;
while(!finished) {
switch (parseStatus_) {
case PARSE_STATU_REQUESTLINE: { //解析请求行
const char* crlf = buf->findCRLF();
if(crlf) {
if(parseRequestLine(buf->peek(), crlf)) {
req_.setReceiveTime(receiveTime);
parseStatus_ = PARSE_STATU_HEADER;
}else {
status = false;
finished = true;
}
buf->hasReadUtil(crlf + 2);
}else {
status = false;
finished = true;
}
break;
}
case PARSE_STATU_HEADER: { //解析请求头
const char* crlf = buf->findCRLF();
if(crlf) {
if(parseRequestHeader(buf->peek(), crlf)) {
}else {
parseStatus_ = PARSE_STATU_BODY;
}
buf->hasReadUtil(crlf + 2);
}
break;
}
case PARSE_STATU_BODY: { //解析请求体
parseRequestBody(buf);
parseStatus_ = PARSE_STATU_FINISHED;
finished = true;
}
default:
break;
}
}
return status;
}
bool HttpServer::parseRequestBody(ByteBuffer *buf) {
if((req_.getHeaders())["Content-Type"] == "application/json") {
if(*(buf->peek()) == '{') {
const char* t = buf->readJSON();
if(t != buf->peek()) {
req_.setContentJSON(buf->peek(), t);
buf->hasReadUtil(t + 1);
}
}
}
return true;
}
bool HttpServer::parseRequestHeader(const char *begin, const char *end) {
return req_.setHeader(begin, end);
}
bool HttpServer::parseRequestLine(const char* begin, const char* end) {
bool status = true;
const char* start = begin;
const char* space = std::find(start, end, ' ');
if(space != end) {
req_.setMethod(start, space);
start = space + 1;
space = std::find(start, end, ' ');
if(space != end) {
const char* question = std::find(start, space, '?');
if(question != space) {
req_.setPath(start, question);
req_.setQuery(question + 1, space);
}else {
req_.setPath(start, space);
}
}else {
status = false;
}
}else {
status = false;
}
return status;
}
路由的绑定
解析完http消息后便交付给上层定义的路由进行处理,接下来介绍是如何实现路由绑定的。从下面代码中可以看到,每一个Cweb中都维护了一个路由表router,一个分组表group,一个httpserver。通过addRouter方法便可实现路由的绑定。
class Cweb
{
private:
router* router_; //路由
vector<group*> groups_; //分组
unique_ptr<HttpServer> httpServer_;
public:
Cweb(string ip, short port) {
httpServer_.reset(new HttpServer(ip, port));
router_ = new router();
}
virtual ~Cweb() {
delete router_;
for(group* g : groups_) {
delete g;
}
}
void GET(string pattern, HandlerFunc handler) {
router_->addRouter("GET", pattern, handler);
}
void POST(string pattern, HandlerFunc handler) {
router_->addRouter("POST", pattern, handler);
}
group* Group(string prefix) {
group* gp = new group(prefix, router_);
groups_.push_back(gp);
return gp;
}
void run(int threadNum) {
httpServer_->setRequestCallback(std::bind(&Cweb::serveHTTP, this, std::placeholders::_1, std::placeholders::_2));
httpServer_->start(threadNum);
}
//
void serveHTTP(const TcpConnectionPtr& conn, const HttpRequset& req) {
context* c = new context(conn, req);
//中间件
for(group* g : groups_) {
if(req.getPath().find(g->prefix) == 0) {
for(HandlerFunc f : g->middlewares) {
c->handlers_.push_back(f);
}
}
}
router_->handle(c);
delete c;
}
};
其中路由表router是通过前缀树实现的,利用前缀树可以实现动态路由的匹配,比如这段代码绑定的路由如图所示。/api/dynamic/:param这条路径可以匹配/api/dynamic/a、/api/dynamic/b等路由,对应的参数param分别为a、b等。
Cweb c("127.0.0.1", 6666); //初始化
c.GET("/api/sayhi", handle1);
c.GET("/api/echo", handle2); //带参数 ?key=value
c.GET("/api/dynamic/:param", handle3); //动态路由
group* g1 = c.Group("/group"); //分组 *** 作
g1->USE(handle4); //中间件
g1->GET("/sayhi", handle5);
上面提到Cweb中还维护了分组表,分组可以为一组路由绑定相同的 *** 作,即中间件,比如有一组接口的访问需要鉴权,便可以为这组接口对应的路由绑定鉴权中间件,中间件代码规范如下,最后必须要调用context的next方法。
void handle4(context* c) {
cout << "use group handle4" << endl;
c->next();
}
context的next方法:
class context {
public:
int index_;
vector<HandlerFunc> handlers_;
HttpRequset req_;
shared_ptr<TcpConnection> conn_;
unordered_map<string, string> params_;
public:
context(const TcpConnectionPtr& conn, HttpRequset req)
:index_ (-1),
conn_(conn),
req_(req){}
void next() {
index_++;
if(index_ < handlers_.size()) {
(handlers_[index_])(this);//中间件中要调用next
}
}
void setParams(unordered_map<string, string> params) {
params_ = params;
}
void STRING(HttpStatusCode status, string resp);
void JSON(HttpStatusCode status, Json::Value root);
};
当访问分组路由时,首先为其添加分组中绑定的中间件 *** 作,最后绑定路由对应的接口 *** 作,执行 *** 作时首先依次执行中间件 *** 作,最后执行接口 *** 作,若中途某个中间件条件无法满足则退出不再往下执行,有效的保护了接口,也提高了程序的扩展能力。
void serveHTTP(const TcpConnectionPtr& conn, const HttpRequset& req) {
context* c = new context(conn, req);
//中间件
for(group* g : groups_) {
if(req.getPath().find(g->prefix) == 0) {
for(HandlerFunc f : g->middlewares) {
c->handlers_.push_back(f);
}
}
}
router_->handle(c);
delete c;
}
void router::handle(context *c) {
routerWithParams* rwp = getRouter(c->req_.getMethod(), c->req_.getPath());
if(rwp != nullptr) {
string key = c->req_.getMethod() + "-" + rwp->node->pattern;
c->setParams(rwp->params);
c->handlers_.push_back(handlers[key]);
}else {
}
c->next();
}
tcpserver目录中内容讲解
tcpserver目录中实现了网络数据的交互功能,其实现方法可以查看我的公众号“猫不吃芒果”中的《cpp网络基础能力建设》这篇文章,不同的是我去掉了acceptor这一层,把它做成了tcpserver里的一个acceptEvent,感觉这样更容易去理解。于是整个消息交互流程可以用下图表示。
因为业务经验比较少,所以代码有很多功能缺失和漏洞,期待你的建议。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)