用C++写一个类似gin的简易版Web框架(Cweb)

用C++写一个类似gin的简易版Web框架(Cweb),第1张

项目链接

项目链接

项目架构

目前项目包含四个子目录,其中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目录中的内容讲解 http.cpp中负责http消息的解析和封装

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,感觉这样更容易去理解。于是整个消息交互流程可以用下图表示。

小结

因为业务经验比较少,所以代码有很多功能缺失和漏洞,期待你的建议。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1353128.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-14
下一篇 2022-06-14

发表评论

登录后才能评论

评论列表(0条)

保存