美文网首页
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