使用 boost::beast 发送 http 请求时可能提高性能

问题描述 投票:0回答:1

我正在构建一个低延迟项目,尽快发送 http 请求是一个关键组件。这是我当前 http 请求的构造

#include <thread>
#include <iostream>
#include <coroutine>
#include <optional>
#include <variant>
#include <vector>
#include <utility>
#include <string>
#include <chrono>

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/json.hpp>
#include <boost/beast.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/coroutine/all.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/core/flat_static_buffer.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/detached.hpp>

class http_client {
private:
    using response = boost::beast::http::response<boost::beast::http::string_body>;

    std::string http_hostname;
    std::string ip_address;
    boost::asio::ssl::context ssl_context;
    boost::asio::ip::tcp::resolver hostname_resolver;
    std::optional<boost::beast::ssl_stream<boost::beast::tcp_stream>> tcp_stream;
    boost::beast::flat_static_buffer<4 * 1024 * 1024> receive_flat_buffer;
public:
    http_client(const std::string& http_name, boost::asio::io_context& io_context) :
        http_hostname(http_name),
        ssl_context(boost::asio::ssl::context::tlsv12_client),
        hostname_resolver(io_context),
        tcp_stream(boost::beast::ssl_stream<boost::beast::tcp_stream>(io_context, ssl_context)),
        receive_flat_buffer()
    {

        ssl_context.set_verify_mode(boost::asio::ssl::verify_peer);
        ssl_context.set_options(
            boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 |
            boost::asio::ssl::context::no_sslv3 | boost::asio::ssl::context::single_dh_use);

        if (!SSL_set_tlsext_host_name(tcp_stream->native_handle(), http_hostname.c_str())) {
            boost::beast::error_code error_code{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
            throw boost::beast::system_error{error_code};
        }

        auto const resolved_endpoint = hostname_resolver.resolve(http_hostname, "443");
        ip_address = resolved_endpoint->endpoint().address().to_string();

        boost::beast::get_lowest_layer(tcp_stream.value()).connect(resolved_endpoint);
        boost::beast::get_lowest_layer(tcp_stream.value()).socket().set_option(boost::asio::socket_base::keep_alive(true));
        boost::beast::get_lowest_layer(tcp_stream.value()).socket().set_option(boost::asio::ip::tcp::no_delay(true));

        std::cout << "Connected to REST endpoint at IP address <" << ip_address << "> which was resolved from <" << resolved_endpoint->host_name() << std::endl;
        tcp_stream->handshake(boost::asio::ssl::stream_base::client);
    }


    void send_request(boost::asio::io_context& io_context, const std::string& target, const std::function<void(response)>& callback) {
        boost::asio::spawn(
            io_context, [target = std::move(target), callback = std::move(callback), this](boost::asio::yield_context yield_context) mutable
            {
                boost::beast::http::request<boost::beast::http::string_body> http_request{
                    boost::beast::http::verb::get,
                    target,
                    11};

                http_request.set(boost::beast::http::field::host, http_hostname);
                http_request.set(boost::beast::http::field::content_type, "application/json");
                http_request.set(boost::beast::http::field::connection, "Keep-Alive");
                http_request.set(boost::beast::http::field::keep_alive, "timeout=86400");
                http_request.keep_alive(true);
                http_request.prepare_payload();

                size_t bytes_transferred = boost::beast::http::async_write(tcp_stream.value(), http_request, yield_context);

                response http_response;
                boost::beast::http::async_read(tcp_stream.value(), receive_flat_buffer, http_response, yield_context);

                callback(http_response);
            }
        );
    }
};

int main() {
    boost::asio::io_context io_context{};

    std::string host_name{"fapi.binance.com"};
    http_client client(host_name, io_context);

    for (int i = 0; i < 100; i++) {
        auto const stime = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
        client.send_request(io_context, "/fapi/v1/time", [&](boost::beast::http::response<boost::beast::http::string_body> const& http_response) {});

        auto const etime = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
        std::cout << "time diff = " << etime - stime << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    io_context.run();

    return 0;
}

这是我正在使用的编译器标志

g++ -std=c++20 -O2 -flto -g beast_http_client.cpp -I/home/dev/vcpkg/installed/x64-linux/include -L/home/dev/vcpkg/installed/x64-linux/lib -lboost_system -lboost_coroutine -lboost_thread -lboost_json -lssl -lcrypto -lboost_context

我对此进行了计时,延迟平均约为 10-20 我们。我想知道是否可以做任何改进来将其降低到低个位数微。我知道 boost::beast 是一个相当重的库,但是是的,我想我会向专家学习有关明显优化的知识

硬件:我在具有 Intel Xeon 处理器 3GHz 的 AWS 虚拟机上运行此程序

optimization networking c++17 boost-asio boost-beast
1个回答
0
投票

很多问题。

就像我在评论中提到的那样,不清楚您要测量什么(并以某种方式称为“延迟”)。

但是继续阅读,就会发现代码在很多方面都被破坏了。

main
循环调度 100 个协程,每个协程之间等待 100 毫秒(完全没有原因),然后,仅在 main 末尾通过调用
io_context.run()
一次性执行所有协程。

这不仅会在服务器上造成类似于拒绝服务攻击的情况,而且还明显违反了一次只能执行一个写入操作的限制(这尤其适用于 SSL 流,也适用于 SSL 流)底层 POSIX 互联网域流套接字)。

事实上,如果您运行调试构建,毫无疑问会有像这样的断言为您中止程序: 最测试:

/home/sehe/custom/superboost/boost/beast/core/detail/stream_base.hpp:116: void boost::beast::detail::stream_base::pending_guard::assign(bool&): Assertion `! *b_' failed.

其他一些问题包括: 解决这些问题和一些可靠性问题:

  • 你经常使用const变量
    move
    。不是很有用
  • 你多余地传递了
    io_context
    引用而不是使用执行器
  • 您可能会晚设置套接字选项
  • 您无缘无故地使用
    optional<>
    。如果必须的话,至少使用
    stream_(std::in_place, ...)
    来构建
  • std::function 引入类型擦除(虚拟调度)和复制
  • 您可能想要计算开始-完成(往返)时间
  • 您在 GET 请求中使用
    string_body
    而不是
    empty_body

住在Coliru

#include <iomanip>
#include <iostream>

#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast.hpp>
#include <boost/beast/ssl.hpp>

namespace net   = boost::asio;
namespace ssl   = net::ssl;
namespace beast = boost::beast;
namespace http  = beast::http;
using beast::error_code;
using net::ip::tcp;
using namespace std::chrono_literals;

static constexpr auto now = std::chrono::steady_clock::now;
using duration            = std::chrono::steady_clock::duration;

class http_client {
  private:
    using Stream  = beast::ssl_stream<beast::tcp_stream>;

    std::string  host_;
    ssl::context ctx_{ssl::context::tlsv12_client};
    Stream       stream_;

    std::string                        ip_address;
    beast::flat_static_buffer<4 << 10> buf_;

  public:
    using response = http::response<http::string_body>;

    http_client(std::string host, net::io_context& ioc) : host_(std::move(host)), stream_(ioc, ctx_) {
        ctx_.set_verify_mode(ssl::verify_peer);
        using C = ssl::context;
        ctx_.set_options(C::default_workarounds | C::no_sslv2 | C::no_sslv3 | C::single_dh_use);

        if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str()))
            throw beast::system_error(::ERR_get_error(), net::error::get_ssl_category());

        auto eps   = tcp::resolver(ioc).resolve(host_, "443");
        ip_address = eps->endpoint().address().to_string();

        {
            auto& ll = beast::get_lowest_layer(stream_);
            auto& s  = ll.socket();
            s.open(tcp::v4());
            s.set_option(tcp::no_delay(true));
            s.set_option(tcp::socket::keep_alive(true));
            ll.connect(eps);
        }

        std::cout << "Connected to REST endpoint at IP address " << quoted(ip_address)
                  << " which was resolved from " << quoted(eps->host_name()) << std::endl;
        stream_.handshake(Stream::client);
    }

    // template <typename F>
    // requires std::invocable<F, error_code, response, duration>
    void send_request(std::string target, std::function<void(error_code, response, duration)> callback) {
        spawn(stream_.get_executor(),
              [start = now(), target = std::move(target), cb = std::move(callback),
               this](net::yield_context yield) mutable {
                  http::request<http::empty_body> http_request{http::verb::get, target, 11};

                  http_request.set(http::field::host, host_);
                  http_request.set(http::field::content_type, "application/json");
                  http_request.set(http::field::connection, "Keep-Alive");
                  http_request.set(http::field::keep_alive, "timeout=86400");
                  http_request.keep_alive(true);
                  http_request.prepare_payload();

                  /*size_t bytes_transferred =*/async_write(stream_, http_request, yield);

                  response http_response;
                  error_code ec;
                  async_read(stream_, buf_, http_response, yield[ec]);

                  std::move(cb)(ec, std::move(http_response), now() - start);
              });
    }
};

void send_loop(http_client& client, std::string const& target, unsigned n) {
    if (n == 0)
        return;

    client.send_request(target, [=, &client](error_code ec, http_client::response res, duration dur) {
        send_loop(client, target, n - 1); // only now it is safe to schedule a new write

        std::cout << "#" << n << " " << ec.message() << " in " << (dur / 1ms) << " ms";
        if (!ec)
            std::cout << " HTTP " << res.reason();
        std::cout << std::endl;
    });
}

int main() {
    net::io_context io_context;

    http_client client("fapi.binance.com", io_context);
    send_loop(client, "/fapi/v1/time", 3);

    io_context.run();
}

本地演示:

其他问题/改进

  • 您依赖永不失败的连接
  • 常见的 HTTP 服务器实现限制了管道请求的数量
  • 您正在使用默认类型的已擦除执行器(
    any_io_executor
    )
  • 你正在使用堆栈协程,这有点重

您可能应该考虑一个单独的 IO 线程和可能的多个连接。参见例如如何使此 HTTPS 连接在 Beast 中持久存在? 从这里获取想法。

© www.soinside.com 2019 - 2024. All rights reserved.