首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

模块化的 HTTP 服务器

现在我们回看一下类HttpServer的定义,如右侧的框图所示, 我们一直在解析报文-->生成报文-->发送报文的过程中不断循环。根据报文的不同形式,以及请求资源的类型和权限,我们生成不同的报文。

对应的cpp文件中,我们可以看到很多 switch-case 和 if-else 语句来根据事件类型和报文类型调用相应的接口函数。 这对于后续的代码维护而言,将是一场噩梦。目前,我们的服务器只能处理 HEAD 和 GET 请求。而且异常处理也很简单,只有 400 和 404 的异常。 如果我们后续增加新的功能,处理更多种形式的报文时,除了要增加对应的接口函数之外,还需要对运行框架进行调整,增加更多的 case 和 if-else。 可以想见,右面的框图将随着分支的增加不断膨胀,而代码也将称为一座“X山”。这并不是我们想看到的,为了避免这种情况出现,我们需要一个模块化的可扩展的框架。

nginx是一个轻量的 web 服务器实现,由于其出色的性能和稳定性,被广泛的使用,是目前主流的 web 服务器之一。 在它的设计理念中,模块化是一个十分重要的概念,可以说除了少量的框架核心代码,其他都是模块。 整个服务器大体上由五类模块构成,核心模块、配置模块、事件模块、HTTP模块、mail模块。模块可以通过配置文件灵活配置。

借鉴 nginx 的设计,结合我们的代码现状,现阶段我们只对 Http 进行模块化。后续将在不断的重构代码的过程中,逐渐对整个系统模块化处理。 本文中我们将详细解释 HttpModule 的设计理念,并对 HttpServer 进行改写。

1. 状态机与流水线

最初我寻思着用状态机的形式处理请求报文,解析和检查报文的不同阶段和报文类型对应着不同的状态。但经过深入思考之后,发现现阶段我并不能充分的讨论清楚有多少种状态, 各个状态之间的转换关系是什么。参考 nginx 的代码设计,我感觉用流水线的方式来描述 HTTP 请求响应的过程更合理一些。把处理报文的每个环节想象成一个个的工位,报文则在各个工位之间流转, 完成所有的加工工序和质检工作之后,从流水线上下来,交付给请求者。

状态机是我们处理复杂逻辑的一种常用方法。其本质就是把 switch-case 或者 if-else 语句的各种分支描述成一个个的状态,分支的选择被抽象为状态之间的切换。 我们在特定的状态下,只需要关注该状态的行为逻辑以及状态切换。一个典型的状态机实现一般有四个部分:①状态列表,②状态行为映射表,③各个状态的行为函数,④驱动状态机运转的超级循环。 其代码逻辑大体如下,把各个状态用枚举类型一一列举出来,用一个 std::map 将状态与处理状态的函数入口绑定在一起,在超级循环中根据当前的状态 gCurState 查询相应的函数并调用。

        enum EStatus {
            EState_1,
            EState_2,
            ...
            EState_Error
        };
        std::map<EStatus, std::function<void()>> gHandlers = {
            { EState_1, HandleState_1 },
            { EState_2, HandleState_2 },
            ...
            { EState_Error, HandleState_Error }
        };
        while (1) {
            ...
            gHandlers[gCurState]();
            ...
        }

这种实现方式避免了大量的 switch-case 和 if-else 的语句,但是对于新增或者删除状态并不友好,需要修改枚举类型和映射表,所以最终并没有采用这种形式。本着功能最小化的原则, 我们把处理 HTTP 报文的过程抽象为一个个的模块,尽量做到每个模块只实现一种简单的功能,通过组合多个小模块完成复杂任务。

我们可以把一次 HTTP 请求响应的过程想象成取做体检。一次请求的发起过程,就好比取体检机构的前台领一张导检单一样,这张导检单就是一个请求报文。 我们拿着导检单在机构的各个小房间之间穿梭。这些小房间一般都只做一项检查,比如抽血、B超、CT等等。这一个个的小房间就好比是我们的功能模块,对请求报文做各种筛选,并填充响应报文。 从每个房间出来,都会有一个工作人员帮忙指示下一个体检项目在哪个小房间里。最后我们做完所有的体检项目之后,过几天体检机构就会出具一份体检报告给我们,就是响应报文。

上面描述的就是一个流水线的过程。针对这一过程,我们设计了基础的抽象接口 HttpModule 描述了流水线上各个工位的一些共有特性和通用接口。针对不同的功能,都会继承这个接口,并专门实现自己的功能逻辑。 比如说检查资源文件,有专门的 HttpModuleCheckURL 的模块;检查请求报文是否合法,有 HttpModuleInvalidRequest 等等。HttpModule 就是所谓的“小房间”,HttpModuleCheckURL、HttpModuleInvalidRequest 等实现了具体功能的模块就好比是“做B超的小房子”、“做CT的小房子”等。

下面,我们先来看一下抽象接口 HttpModule 的定义。再介绍如何将若干功能模块组织起来构成一个复杂的 HttpServer。

2. 抽象接口 HttpModule

在体检机构里,我们并不知道也不关心那一个个的小房间有多少平米,装修的时候用的什么牌子的地板。我们就是知道,坐在里面有一位医生,他会对我们身体的某一部分进行检查, 最后告诉我们这个部位是不是健康的。至于医生检查的手法和原理,由于专业性太强,通常我们也不会花时间刨根问底的。类似的,我们也对 HTTP 模块进行抽象。

如下面的代码所示,我们定义了抽象类 HttpModule,在其中的第13行中,我们声明了一个抽象函数 Process()。以后所有的 HTTP 模块都应当继承 HttpModule 这个抽象接口,并实现自己的 Process 过程。 这个函数就是那位坐在小房间里的医生,至于处理请求报文的具体实现过程,我们并不那么关心,但是我们相信它会负责任地处理好每一份报文。

除了该函数之外,各个模块都应当实现自己的构造函数。这里我们虽然提供了一个构造函数,它也只是完成了基础的工作。 因为作为搬砖搞装修的我们并不知道做个B超需要从哪里购买什么设备需要花多少钱,这个应当谁用谁去购买。当然有些项目就不需要特殊的设备,比如外科,只要是个房间就行。此时就显式的调用一下父类的构造函数, 建个房间就可以。

        class HttpModule {
            public:
                HttpModule(bool immediately = false)
                    : mImmediately(immediately)
                {}

                bool Handle(HttpHandlerWeakPtr const & handler);
                void SetSuccessModule(HttpModulePtr const & module) { mSuccessModule = module; }
                void SetFailureModule(HttpModulePtr const & module) { mFailureModule = module; }

            protected:
                //! @brief 实际的处理过程
                virtual bool Process(HttpHandlerWeakPtr const & handler) = 0;

            protected:
                //! Process 成功后的处理模块索引
                HttpModulePtr mSuccessModule;
                //! Process 失败后的处理模块索引
                HttpModulePtr mFailureModule;

                bool mImmediately;
        };

体检的时候,我们拿着导检单进入小房间。就好比这里我们以 handler 为参数调用 Handle 接口一样。 这里的类 HttpHandler 是从前文中的 HttpSession 变更过来的, 因为有些人认为一个 Session 是浏览器跟服务器之间进行了身份认证之后的会话过程,这中间可能有很多次请求,也可能重新建立了很多次 TCP 连接。我们这里的 HttpHandler 则强调是一次 HTTP 的请求和响应过程。 如下面代码的第4-9行所示,我们拿着导检单进去之后,医生就接到了一个新的检查任务。字段 mImmediately 则是用来标记是否立即完成任务的布尔型变量。

        bool HttpModule::Handle(HttpHandlerWeakPtr const & handler) {
            HttpHandlerPtr ptr = std::static_pointer_cast<HttpHandler>(handler.lock());

            TaskPtr task = std::make_shared<Task>(std::bind(&HttpModule::Process, this, handler));
            if (mSuccessModule)
                task->SetSuccessFunc(std::bind(&HttpModule::Handle, mSuccessModule.get(), HttpHandlerWeakPtr(ptr)));
            if (mFailureModule)
                task->SetFailureFunc(std::bind(&HttpModule::Handle, mFailureModule.get(), HttpHandlerWeakPtr(ptr)));
            ptr->mCurrTask = task;

            if (mImmediately)
                task->Finish();
            return true;
        }

任务 Task, 我们在介绍任务队列 以及发送大文件时,都已经提到。一次任务的执行有成功和失败两类, 各模块可以根据自身的实际情况组织后续的工作。在这里,我们用两个 HttpModule 类型的指针 mSuccessModule 和 mFailureModule 分别指引任务成功或者失败运行之后的模块。 在实际使用的时候,可以通过 HttpModule 的接口函数 SetSuccessModule 和 SetFailureModule,注册相应的后继模块。

        //! @brief HTTP 处理非法请求报文的模块
        class HttpModuleInvalidRequest final : public HttpModule {
            public:
                HttpModuleInvalidRequest() : HttpModule(true) {}
            private:
                virtual bool Process(HttpHandlerWeakPtr const & handler) override;
        };

上面是一个用于检查并处理非法请求报文的模块 HttpModuleInvalidRequest。它继承了抽象接口 HttpModule,并重写了接口函数 Process。如下面的代码所示, 我们在 Process 中检查请求报文的 Method 字段,若为非法报文就会生成一个 400 的响应头。后续是直接发送,还是再填充一些报文则完全由 mFailureModule 来决定。

        bool HttpModuleInvalidRequest::Process(HttpHandlerWeakPtr const & handler) {
            HttpHandlerPtr h = handler.lock();
            if (nullptr == h)
                return false;

            HttpRequestPtr req = h->GetRequest();
            if (HttpRequest::eINVALID == req->GetMethod()) {
                DLOG(INFO) << "非法请求";

                HttpResponsePtr res = h->GetResponse();
                res->SetStatusCode(HttpResponse::e400_BadRequest);
                res->LockHead(0);

                h->WakeUp();
                return false;
            }

            h->WakeUp();
            return true;
        }

示例代码中,我们还定义了很多 HTTP 模块。下面就让我们一起看一下如何把这些模块组织起来, 构建一个 HTTP 服务器。

3. 乐高形态的 HttpServer

首先,我们要对 HttpServer 的构造函数进行修改。如下面的代码片段所示,我们在函数的一开始仍然保留了对 TCP 服务器的初始化过程,并注册了三个回调函数 OnNewConnection、 OnCloseConnection、OnMessage 分别用于响应新建连接、断开连接、接收消息三个事件。

        HttpServer::HttpServer(EventLoopPtr const & loop, int port, int max_conn, std::string const & ws)
            : TcpAppServer(loop, port, max_conn, ws)
        {
            mServer->SetTimeOut(10, 0, 5);
            mServer->SetNewConnCallBk(std::bind(&HttpServer::OnNewConnection, this, _1));
            mServer->SetCloseConnCallBk(std::bind(&HttpServer::OnCloseConnection, this, _1));
            mServer->SetMessageCallBk(std::bind(&HttpServer::OnMessage, this, _1, _2, _3));

在构造函数中,我们实例化了一系列的 HttpModule,分别用于检查请求是否合法、检查URL资源、进行基本的身份认证、响应Get请求、发送响应报文。

            HttpModulePtr invalidRequestModule = std::make_shared<HttpModuleInvalidRequest>();
            HttpModulePtr checkURLModule = std::make_shared<HttpModuleCheckURL>(mWorkSpace);
            HttpModulePtr identifyModule = std::make_shared<HttpModuleBasicIdentify>();
            HttpModulePtr getModule = std::make_shared<HttpModuleGet>();
            HttpModulePtr responseModule = std::make_shared<HttpModuleResponse>();

接着通过 SetFailureModule 和 SetSuccessModule 将各个模块串联起来。构成的工作流程就是依次检查请求、检查URL、验证身份、响应GET请求。 中间任何一个环节出错,都会直接通过模块 responseModule 把生成的报文发送出去。

            invalidRequestModule->SetSuccessModule(checkURLModule);
            invalidRequestModule->SetFailureModule(responseModule);
            checkURLModule->SetSuccessModule(identifyModule);
            checkURLModule->SetFailureModule(responseModule);
            identifyModule->SetSuccessModule(getModule);
            identifyModule->SetFailureModule(responseModule);
            getModule->SetFailureModule(responseModule);

最后,我们用成员变量 mFirstModule 记录下处理报文的第一个模块。

            mFirstModule = invalidRequestModule;
        }

此后在回调函数 OnMessage 中,每当有新的消息到来并成功解析出一条请求报文时,都将通过 mFirstModule 的 Handle 接口开始新的报文处理的过程。 新报文将在各个模块之间流转,直到最后发送出响应报文为止。

        void HttpServer::OnMessage(ConnectionPtr const & con, uint8_t const * buf, ssize_t n) {
            // 省略解析请求报文的代码
            ......
                if (HttpRequest::eResponsing == req->GetState() || HttpRequest::eError == req->GetState())
                    mFirstModule->Handle(ptr);
            ......
        }

4. 完

在本文中,我们借鉴 nginx 的设计,结合代码现状,对 HTTP 服务器进行了模块化处理。首先定义了一个基础的抽象类型 HttpModule, 每个具体的 HTTP 模块都将根据实际的功能实现其中的抽象函数接口 Process。然后通过 SetSuccessModule 和 SetFailureModule 指引报文在不同的模块之间流转,并最终生成响应报文。

本文最后组合的 HTTP 服务器只能处理 GET 请求,而且是通过硬编码的形式构建的。后续我们将丰富它,让它能够处理其它类型的请求,并通过配置文件的形式来构建服务器。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1