项目链接
项目架构目前项目包含四个子目录,其中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消息结构如下图所示, 可以看到请求行/响应行、请求头/响应头、请求体/响应体之间通过"rn“分割,请求行中字段通过空格分割。路由和参数之间通过"?"分割,参数和参数之间通过“&"分割,如“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 groups_; //分组
unique_ptr 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 handlers_;
HttpRequset req_;
shared_ptr conn_;
unordered_map 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 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,感觉这样更容易去理解。于是整个消息交互流程可以用下图表示。
因为业务经验比较少,所以代码有很多功能缺失和漏洞,期待你的建议。



