美文网首页
Pistache源码分析 —— ParserImpl类

Pistache源码分析 —— ParserImpl类

作者: 蟹蟹宁 | 来源:发表于2021-06-27 19:41 被阅读0次

    ParserImpl类用于解析/打包HTTP请求/响应:

    template <typename Message>
    class ParserImpl;
    
    template <>
    class ParserImpl<Http::Request> : public ParserBase {...};
    
    template <>
    class ParserImpl<Http::Response> : public ParserBase {...};
    

    ParserBase类

    ParserImpl是模板类,并继承自父类ParserBase,主要的工作都在父类中完成:

    class ParserBase
    {
    public:
        static constexpr size_t StepsCount = 3;
    
        explicit ParserBase(size_t maxDataSize);
    
        ParserBase(const ParserBase&) = delete;
        ParserBase& operator=(const ParserBase&) = delete;
        ParserBase(ParserBase&&)                 = default;
        ParserBase& operator=(ParserBase&&) = default;
    
        virtual ~ParserBase() = default;
    
        bool feed(const char* data, size_t len);
        virtual void reset();
        State parse();
    
        Step* step();
    
    protected:
        std::array<std::unique_ptr<Step>, StepsCount> allSteps;
        size_t currentStep = 0;
    
    private:
        ArrayStreamBuf<char> buffer;
        StreamCursor cursor;
    };
    

    成员变量

    public

    • static constexpr size_t StepsCount = 3;
      阶段个数,默认为3个阶段,表示HTTP请求的解析分三个阶段进行,即解析请求行、解析请求头和解析请求体。

    protected

    • std::array<std::unique_ptr<Step>, StepsCount> allSteps;
      为每一个阶段定义一个Step类,将在下文展开介绍Step类
    • size_t currentStep = 0;
      当前所处的解析阶段

    private

    • ArrayStreamBuf<char> buffer;
      用于存放从TCP流中读取的数据
    • StreamCursor cursor;
      流游标,表示当前处理到流的那个位置

    构造函数

    public:
    ParserBase::ParserBase(size_t maxDataSize)
        : buffer(maxDataSize)
        , cursor(&buffer)
    { }
    

    设置buffer的最大值(buffer的底层是用vetcor<char>实现的),并初始化游标。
    这里的最大值就是HTTP请求或响应的最大值,在Endpoint.Options.maxRequestSize_Endpoint.Options.maxResponseSize_中配置。

    成员函数

    • bool feed(const char* data, size_t len);
      buffer中填充数据,注意是在buffer现有数据的基础之上填充数据,如果填充的数据超过了设定的最大值,就会返回false,否则返回true。
      这与TCP流的特点有关,因为无论是接受还是发送数据,都是分段发送的(凭借我仅存的网络知识,隐约记得有IP分段发送,然后再组装的概念),除此之外,我们也可能分批次的读取网络数据(epoll的ET触发模式),因此这就需要多次的调用feed。每次调用feed后马上执行解析操作,但是问题在于,因为我们是分段解析的,这就有可能导致某些片段不完整,对于不完整的部分需要在下一次片段到达后进行重新的解析,因此需要保留之前的数据,所以每次feed是追加型的,而非覆盖型。
    • virtual void reset();
    void ParserBase::reset()
    {
        buffer.reset();
        cursor.reset();
        currentStep = 0;
    }
    

    当解析过程发生错误或者解析完成,都要调用reset函数,以重置buffercursorcurrentStep

    • Step* step();
    Step* ParserBase::step()
    {
        return allSteps[currentStep].get();
    }
    

    返回当前的Step,详见Step类。

    • State ParserBase::parse()
      真正执行解析操作的函数
    State ParserBase::parse()
            {
                State state;
                do
                {
                    Step* step = allSteps[currentStep].get();
                    state      = step->apply(cursor);
                    if (state == State::Next)
                    {
                        ++currentStep;
                    }
                } while (state == State::Next);
                // Should be either Again or Done
                return state;
            }
    

    返回值表示解析的状态:

    enum class State { Again,
        Next,
        Done };
    
    • Again 就是上面介绍feed的时候提到的,这意味着在解析某一个字段时数据不完整,需要在重新feed获取更多数据之后,重新解析
    • Next 表示当前阶段已经完全解析结束
    • Done 表示所有阶段都解析完成

    step->apply(cursor);是每个阶段自己的解析方法,分阶段解析显然是个不错的方案。

    ParserImpl<Http::Request> 类

    template <>
    class ParserImpl<Http::Request> : public ParserBase
    {
    public:
        explicit ParserImpl(size_t maxDataSize);
    
        void reset() override;
    
        std::chrono::steady_clock::time_point time() const
        {
            return time_;
        }
    
        Request request;
    
    private:
        std::chrono::steady_clock::time_point time_;
    };
    

    ParserImpl<Http::Request> 主要添加了两个字段Request requeststd::chrono::steady_clock::time_point time_

    time_用于记录对象创建时或执行reset时的时间点,服务端将定期检查peer是否有请求在传输,如果超过一定的时间(由Endpoint.Options.headerTimeout_Endpoint.Options.bodyTimeout_指定)没有数据传输,将会断开连接。

    request则是定义的Request对象,用于存放解析的数据,以传递给用户处理程序使用。

    构造函数

    Private::ParserImpl<Http::Request>::ParserImpl(size_t maxDataSize)
        : ParserBase(maxDataSize)
        , request()
        , time_(std::chrono::steady_clock::now())
    {
        allSteps[0] = std::make_unique<RequestLineStep>(&request);
        allSteps[1] = std::make_unique<HeadersStep>(&request);
        allSteps[2] = std::make_unique<BodyStep>(&request);
    }
    

    我们用request初始化了父类中的Step数组allSteps。Step类将实现分阶段的解析TCP数据流,并将结果存放到request中。

    Step类 RequestLineStep类

    struct Step
    {
        explicit Step(Message* request);
    
        virtual ~Step() = default;
    
        virtual StepId id() const                 = 0;
        virtual State apply(StreamCursor& cursor) = 0;
    
        static void raise(const char* msg, Code code = Code::Bad_Request);
    
    protected:
        Message* message;
    };
    
    class RequestLineStep : public Step
    {
    public:
        static constexpr StepId Id = Meta::Hash::fnv1a("RequestLine");
    
        explicit RequestLineStep(Request* request)
            : Step(request)
        { }
    
        StepId id() const override { return Id; }
        State apply(StreamCursor& cursor) override;
    };
    

    我们以的RequestLineStep为例,探讨一下解析的过程,因此我们主要关注State apply(StreamCursor& cursor) override;函数
    Request类的分析中我们知道,RequestLine主要包括三部分:

    • 请求方法,如 GET
    • 请求的URI,如 /ping?k=v
    • 协议版本,一般是http1.1

    这些字段分别定义在Request.method_Request.resource_&&Request.query_Message.version_中,详见Request类
    代码注释:

    State RequestLineStep::apply(StreamCursor& cursor)
    {
        // 保存当前cursor的状态,如果最终的解析状态是Next,即当前step解析完成,则忽略
        // 如果当前最终的解析状态是Again,即表示数据不完整,那么将直接返回,在析构函数中恢复cursor
        // 这也就意味着,如果解析状态是Again,那么整个step将重新解析,这是比较简单的实现方式,但是效率略低
        StreamCursor::Revert revert(cursor);
        auto* request = static_cast<Request*>(message);
        // Token用于记录一个字段,一个字段就是一个字符串
        StreamCursor::Token methodToken(cursor);
        // cursor向前走,直到遇到一个空格,因为在HTTP请求行里面是用空格来分隔的
        // 如果未遇到空格,说明数据不完整,将会返回Again状态,此时会调用Revert的析构函数来恢复cursor
        if (!match_until(' ', cursor))
            return State::Again;
        // methodToken.text() 返回获取到的字符串,并与httpMethods进行匹配
        // 如果匹配成功,则给request->method_赋值,如果匹配失败,那么将抛出异常
        // 异常的处理函数会直接返回400,然后调用ParserImpl.reset进行重置
        auto it = httpMethods.find(methodToken.text());
        if (it != httpMethods.end())
        {
            request->method_ = it->second;
        }
        else
        {
            raise("Unknown HTTP request method");
        }
        // 空格后应该是URI,不能再是空格
        int n;
        if (cursor.eof())
            return State::Again;
        else if ((n = cursor.current()) != ' ')
            raise("Malformed HTTP request after Method, expected SP");
        // 若没有待解析数据,则数据不完整,不足以完整解析此step,返回Again
        if (!cursor.advance(1))
            return State::Again;
        // 同样的这里是在解析resource_,因为URI分两部分,?前的路径,和?后的查询参数,当然查询参数可有可无
        StreamCursor::Token resToken(cursor);
        while ((n = cursor.current()) != '?' && n != ' ')
            if (!cursor.advance(1))
                return State::Again;
        // 给request->resource_赋值
        request->resource_ = resToken.text();
        // 如果存在查询参数,那么就开始解析然后给request->query_赋值,就不展开注释了
        // Query parameters of the Uri
        if (n == '?')
        {
             ...
        }
        // SP
        if (!cursor.advance(1))
            return State::Again;
    
        // 下面是解析协议版本,方法和前面一样
        // HTTP-Version
        StreamCursor::Token versionToken(cursor);
        while (!cursor.eol())
            if (!cursor.advance(1))
                return State::Again;
        const char* ver   = versionToken.rawText();
        const size_t size = versionToken.size();
        if (strncmp(ver, "HTTP/1.0", size) == 0)
        {
            request->version_ = Version::Http10;
        }
        else if (strncmp(ver, "HTTP/1.1", size) == 0)
        {
            request->version_ = Version::Http11;
        }
        else
        {
            raise("Encountered invalid HTTP version");
        }
        // 最后的最后,是\n和\r,标志着此阶段完成解析
        if (!cursor.advance(2))
            return State::Again;
        // 到此,RequestLine的解析完成,调用revert.ignore,避免在析构函数中执行对游标的恢复
        revert.ignore();
        // 最后返回Next状态
        return State::Next;
    }
    

    结合``State ParserBase::parse()```,对解析的过程能有不错的理解深度。

    相关文章

      网友评论

          本文标题:Pistache源码分析 —— ParserImpl类

          本文链接:https://www.haomeiwen.com/subject/eqgxultx.html