项目Github主页 Coke

在这个时间点开发本项目,有以下几点考虑

  1. 常用的编译器对C++ 20的支持已经逐步完善,本项目依赖于GCC >= 11Clang >= 15
  2. 常用的操作系统发行版支持了新编译器,例如CentOS Stream 8Ubuntu 22.04Fedora 38
  3. C++ Workflow使用回调函数的方式组织异步任务,一部分习惯写同步代码的用户可能会对此感到困扰,尤其是随着回调函数个数的增加,其理解难度也会越来越高

借助C++ 20提供的协程组件,Coke使C++ Workflow的任务以顺序的方式编写,大大简化了理解的复杂度。Coke项目的一项重要目标就是简洁、便捷,让代码写起来方便、读起来方便、跑起来也方便。

下面通过发起Http请求的示例来展示Coke的便捷性

#include <iostream>
#include <string>
#include <string_view>

#include "coke/coke.h"
#include "coke/http_client.h"
#include "coke/http_utils.h"

void show_response(const coke::HttpResponse &resp) {
    std::cout << resp.get_http_version() << ' '
              << resp.get_status_code() << ' '
              << resp.get_reason_phrase() << std::endl;

    // 使用 coke::HttpHeaderCursor 方便地遍历所有header
    for (const coke::HttpHeaderView &header : coke::HttpHeaderCursor(resp))
        std::cout << header.name << ": " << header.value << std::endl;

    std::cout << "\n";

    // 为了简洁起见,省略了Http body内容的输出

    if (resp.is_chunked()) {
        // 对于Chunk编码的Http body,可使用coke::HttpChunkCursor来遍历
        for (std::string_view chunk : coke::HttpChunkCursor(resp))
            std::cout << "chunk body length " << chunk.size() << std::endl;
    }
    else {
        // 对于非Chunk编码的Http body,可使用coke::http_body_view函数获取
        std::string_view body_view = coke::http_body_view(resp);
        std::cout << "\nbody length " << body_view.size() << std::endl;
    }
}

int main(int argc, char *argv[]) {
    coke::HttpClient cli;
    std::string url;

    if (argc > 1 && argv[1])
        url.assign(argv[1]);
    else
        url.assign("http://sogou.com/");

    // 使用coke::HttpClient发起GET请求,并同步等待结果
    coke::HttpResult result = coke::sync_wait(cli.request(url));

    if (result.state != coke::STATE_SUCCESS)
        std::cerr << coke::get_error_string(result.state, result.error) << std::endl;
    else
        show_response(result.resp);

    return 0;
}

在上述示例代码中,主函数通过关键的coke::sync_wait(cli.request(url));一句话,完成了Http GET请求的创建、发送以及同步等待结果,充分体现了Coke的便捷性。在show_response函数中,展示了coke::HttpHeaderCursorcoke::HttpChunkCursorcoke::http_body_view的使用方法,借助这些组件,遍历Http的消息内容也是非常方便的。

需要注意的是,这里提到的view(视图)都是coke::HttpResponse resp中内容的引用,会在resp发生修改或者销毁之后不可用,使用时需注意生命周期等问题。

coke::sync_wait常用于在主函数同步等待一组异步任务结束,在协程函数中需要用co_await来等待异步任务返回,而不可以使用同步的方式等待,以免导致阻塞。关于同步/异步等待的内容后续会单独分享。

在C++ Workflow中,创建Http任务时可通过指定redirect_max参数来指定重定向的次数,这会在一次任务中自动实现重定向,调用方直接得到最终结果,然而在实际应用中有些场景需要获得重定向的中间结果。例如:

  1. Http请求可能被劫持并重定向到恶意网站,需要分析重定向后的url来决定是否需要继续请求
  2. 重定向到其他域名时,未根据重定向后的Host重置Cookie,会导致信息泄露或新请求失败
  3. 发生环形重定向时无法感知

coke::HttpClient支持设置redirect_max参数,但实现一个更安全的重定向逻辑依然很简单

#include <iostream>
#include <string>
#include <string_view>
#include <set>
#include <cstdlib>

#include "coke/coke.h"
#include "coke/http_client.h"
#include "coke/http_utils.h"

bool need_redirect(const coke::HttpResponse &resp, std::string &redirect_url) {
    const char *status = resp.get_status_code();
    int code = status ? std::atoi(status) : 0;

    // 简单起见,此处仅实现一个极其朴素的检查重定向的方法,所以可能出现未能正确重定向的情况,
    // 严谨的方法请参考相关标准
    if (code == 301 || code == 302) {
        for (const coke::HttpHeaderView &header : coke::HttpHeaderCursor(resp)) {
            if (header.name == "Location" || header.name == "location") {
                // value也可能是个相对路径,此处忽略检查
                // header.value是个view,会随着resp的销毁变得不可用,所以需要redirect_url拷贝一份
                redirect_url = header.value;
                return true;
            }
        }
    }

    return false;
}

// `show_response`函数内容与上一个示例相同,为了节约篇幅,此处省略
// void show_response(const coke::HttpResponse &resp);

coke::Task<> http_get(std::string url, int max_redirect) {
    std::set<std::string> history;
    std::string redirect_url;
    coke::HttpClient cli;
    coke::HttpResult res;
    int i = 0;

    for (i = 0; i < max_redirect; i++) {
        // 记录重定向历史,检查是否有环形重定向
        if (history.find(url) != history.end()) {
            std::cerr << "Url redirect loop" << std::endl;
            co_return;
        }

        std::cout << "Visit " << url << std::endl;
        history.insert(url);

        // 使用coke::HttpClient发起GET请求,在协程中使用co_await等待,不可使用同步等待
        res = co_await cli.request(url);
        if (res.state != coke::STATE_SUCCESS) {
            std::cerr << "Request failed url:" << url << std::endl;
            co_return;
        }

        // 检查是否仍需重定向
        if (!need_redirect(res.resp, url))
            break;
    }

    if (i == max_redirect)
        std::cerr << "Too many redirect" << std::endl;
    else
        show_response(res.resp);
}

int main(int argc, char *argv[]) {
    std::string url;

    if (argc > 1 && argv[1])
        url.assign(argv[1]);
    else
        url.assign("http://sogou.com/");

    coke::sync_wait(http_get(url, 5));
    return 0;
}

本文通过两个示例展示了Coke的便捷性,为避免篇幅过长,coke::HttpClient的其他细节下次再分享。

本系列文章同步发布于个人网站知乎专栏,转载请注明来源,以便读者及时获取最新内容及勘误。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注