10. 如何发起http请求

大家好,我是程序喵。


前面我们设计了上报的协议,并且确定使用http协议做埋点的网络上报。


HTTP请求是基于TCP协议的,它涉及以下几个主要步骤:


  1. 解析域名,获取对应的IP地址和端口号。
  2. 建立TCP连接,确保客户端和服务器之间的通信。
  3. 获取套接字通道FD,用于在通道中传输数据。
  4. 向通道FD中写入HTTP相关信息,包括请求方法、目标路径、HTTP版本以及请求头字段等。
  5. 将要上传的请求体写入通道FD中,向服务器发送数据。
  6. 从通道FD接收服务端返回的数据,获取服务器的响应。
  7. 关闭TCP链接,结束通信。


值得注意,HTTP协议本身只是一个应用层协议,定义了一些应用层规则。上述步骤中的许多细节实际上并不属于HTTP的一部分,我将它们整理到了HTTP通信的代码流程中,方便更好地理解整个过程。


在C++中,有很多关于HTTP请求的第三方库可供选择,包括Beast、cpp-httplib、libcurl、libhv等。在这些库中,Boost中的Beast可能是使用难度最高、集成使用最复杂的。尽管如此,我仍然在这个项目中选择Beast作为HTTP请求的库,主要是因为Beast的HTTP请求是基于Asio库的封装。


在使用Beast的过程中,我们将更加深入了解Asio库的设计理念和使用方法。值得一提的是,Asio将成为C++标准库的一部分,因此选择Beast可以让我们提前熟悉这套Asio设计,相信这可以为我们未来的职业生涯增加一些优势。


如何使用Beast发起http请求?


直接看代码:


TEST(BuriedHttpTest, DISABLED_HttpPost) {
try {
auto const host = "localhost";
auto const target = "/buried";
int version = 11;

// The io_context is required for all I/O
net::io_context ioc;

// These objects perform our I/O
tcp::resolver resolver(ioc);
beast::tcp_stream stream(ioc);

boost::asio::ip::tcp::resolver::query query(host, "5678");
// Look up the domain name
auto const results = resolver.resolve(query);

// Make the connection on the IP address we get from a lookup
stream.connect(results);

// Set up an HTTP POST request message
http::request<http::string_body> req{http::verb::post, target, version};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.set(http::field::content_type, "application/json");
req.body() = "{\\"hello\\":\\"world\\"}";
req.prepare_payload();

// Send the HTTP request to the remote host
http::write(stream, req);

// This buffer is used for reading and must be persisted
beast::flat_buffer buffer;

// Declare a container to hold the response
http::response<http::dynamic_body> res;

// Receive the HTTP response
http::read(stream, buffer, res);

std::string bdy = boost::beast::buffers_to_string(res.body().data());
std::cout << "bdy " << bdy << std::endl;
// Write the message to standard out
std::cout << "res " << res << std::endl;
std::cout << "res code " << res.result_int() << std::endl;

// Gracefully close the socket
beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);

// not_connected happens sometimes
// so don't bother reporting it.
//
if (ec && ec != beast::errc::not_connected) throw beast::system_error{ec};

// If we get here then the connection is closed gracefully
} catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}


这段代码是一个使用Boost的Beast库发起HTTP请求的示例。整体和我上面介绍的HTTP流程类似,我来详细解释一下每个步骤:


首先,这里指定了要请求的主机名(host)和目标路径(target)。接下来,创建了一个io_context对象,该对象对所有的I/O操作都是必需的,可以理解为是IO操作的运行时环境。我们还创建了一个resolver对象和一个tcp_stream对象,用于执行I/O操作。


接着,使用resolver对象来解析host,获取对应的IP地址和端口号。然后,使用tcp_stream对象与服务器建立TCP连接。


接着,定义了一个http::request对象,表示一个HTTP POST请求。我们设置了请求的方法为POST,目标路径为target,HTTP版本为11。然后,这里设置了一些HTTP头字段,包括host、user_agent和content_type。我们将请求的主体(body)设置为测试的body{"hello":"world"},并调用prepare_payload方法,用于构造整个请求体。


接着,我们使用http::write函数将请求发送到远程host。然后使用beast::flat_buffer对象作为缓冲区来接收服务器的response。这里定义了一个http::response对象来存储接收到的HTTP响应。


然后,我们使用http::read函数从流中读取服务器的响应。我们将响应的主体转换为字符串,并打印到标准输出。我们还打印了完整的响应和响应状态码。


最后,我们使用beast::error_code对象来处理错误,并通过调用shutdown方法优雅地关闭socket连接。


当然,如果发生了异常,这里也会捕获并打印相关的错误信息。


相关代码:https://github.com/chengxumiaodaren/BuriedPoint/blob/main/tests/test_http.cc


到这里,大家应该已经知道了如何使用Beast发起http请求。


但,这样肯定不太好用,难道每次发起网络请求都要写这么一大堆代码吗?我们需要基于埋点项目需求,封装一套好用的http接口。


class HttpReporter {
public:
explicit HttpReporter(std::shared_ptr<spdlog::logger> logger);

HttpReporter& Host(const std::string& host) {
host_ = host;
return *this;
}

HttpReporter& Topic(const std::string& topic) {
topic_ = topic;
return *this;
}

HttpReporter& Port(const std::string& port) {
port_ = port;
return *this;
}

HttpReporter& Body(const std::string& body) {
body_ = body;
return *this;
}

bool Report();

private:
std::string host_;
std::string topic_;
std::string port_;
std::string body_;

std::shared_ptr<spdlog::logger> logger_;
};


我再解释下:


首先我定义了一个名为HttpReporter的类,用于封装一个方便使用的HTTP接口。


在类的构造函数中,接收一个std::shared_ptr<spdlog::logger>类型的参数,用于记录日志,看过我前几节专栏的朋友应该会知道如何传入这个logger实例。


类中定义了一些公有成员函数,用于设置HTTP请求的各个参数。Host函数用于设置主机名,Topic函数用于设置具体路径,Port函数用于设置端口号,Body函数用于设置请求的主体。


这些函数都返回一个HttpReporter&类型的引用,允许我们使用链式调用来方便地设置参数。


最后,定义了一个Report函数,用于发起HTTP请求。该函数会使用之前设置的参数,使用Boost的Beast库发起HTTP请求,并返回一个bool类型的值,表示请求是否成功。


整个HttpReporter类的设计目的是为了封装和简化HTTP请求的过程,让它更加方便易用。


然后,我们再实现它:


#include "report/http_report.h"

#include "boost/asio/connect.hpp"
#include "boost/asio/io_context.hpp"
#include "boost/asio/ip/tcp.hpp"
#include "boost/beast/core.hpp"
#include "boost/beast/http.hpp"
#include "boost/beast/version.hpp"
#include "spdlog/spdlog.h"

namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp>

namespace buried {

static boost::asio::io_context ioc;

HttpReporter::HttpReporter(std::shared_ptr<spdlog::logger> logger)
: logger_(logger) {}

bool HttpReporter::Report() {
try {
int version = 11;

// These objects perform our I/O
tcp::resolver resolver(ioc);
beast::tcp_stream stream(ioc);

boost::asio::ip::tcp::resolver::query query(host_, port_);
// Look up the domain name
auto const results = resolver.resolve(query);

// Make the connection on the IP address we get from a lookup
stream.connect(results);

// Set up an HTTP POST request message
http::request<http::string_body> req{http::verb::post, topic_, version};
req.set(http::field::host, host_);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.set(http::field::content_type, "application/json");
req.body() = body_;
req.prepare_payload();

// Send the HTTP request to the remote host
http::write(stream, req);

// This buffer is used for reading and must be persisted
beast::flat_buffer buffer;

// Declare a container to hold the response
http::response<http::dynamic_body> res;

// Receive the HTTP response
http::read(stream, buffer, res);

// Gracefully close the socket
beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);

// not_connected happens sometimes
// so don't bother reporting it.
//
if (ec && ec != beast::errc::not_connected) throw beast::system_error{ec};

auto res_status = res.result();
if (res_status != http::status::ok) {
SPDLOG_LOGGER_ERROR(logger_,
"report error " + std::to_string(res.result_int()));
return false;
}

std::string res_body = boost::beast::buffers_to_string(res.body().data());
// Write the message to log
SPDLOG_LOGGER_TRACE(logger_, "report success" + res_body);
// If we get here then the connection is closed gracefully
} catch (std::exception const& e) {
SPDLOG_LOGGER_ERROR(logger_, "report error " + std::string(e.what()));
return false;
}
return true;
}


接口都已经实现好之后,我们就可以方便的发起http请求。


bool BuriedReportImpl::ReportData_(const std::string& data) {
HttpReporter reporter(logger_);
return reporter.Host(host)
.Topic(topic)
.Port(port)
.Body(data)
.Report();
}


这样简单一调用就OK了。


如何测试http的代码?


这里大家可以自己搭建一个server,也可以使用我在项目中使用Rust开发的简易server。


直接进入server目录(https://github.com/chengxumiaodaren/BuriedPoint/tree/main/server),执行cargo run命令即可。


无论你选择哪种方法,都需要先安装Rust开发环境。


到这里,本节内容已经介绍完毕。在下一节中,我将详细介绍项目中加解密功能的实现。下周再见!


本节相关代码见:


  1. https://github.com/chengxumiaodaren/BuriedPoint/blob/main/src/report/http_report.h
  2. https://github.com/chengxumiaodaren/BuriedPoint/blob/main/src/report/http_report.cc
  3. https://github.com/chengxumiaodaren/BuriedPoint/blob/main/tests/test_http.cc
  4. https://github.com/chengxumiaodaren/BuriedPoint/tree/main/server


0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
程序喵
下载 APP