使用Boost实现Http(s)客户端请求

近一年的时间里,从了解底层类SDK的使用,辅助客户完成对接,到理解原理,再到读改代码;让我提前了进入C++领域的节奏。当然不能满足于了解表面,修改简单需求,想着什么时候也能自己完整去实现这样一个底层的产品。

由于公司SDK产品本身是类似一个转发服务器机制,所以对c++的学习也直接从网络库入手,主要应用asio和beast,还有线程以及协程方式的应用。计划是从客户端实现起,到服务端实现,再结合起来实现转发器。最后回过头来读一遍产品代码,争取完全读透。

简单实现

从官网例子学起,用了几天敲了同步/异步/协程三种方式的Http/Https客户端示例。

同步和异步的方式很容易理解,就不照搬照抄了,可以去官网或者我的github上参考,有一些延申知识的注释个人觉得还是有用的:

https://github.com/Yyyyshen/CppLearningDemos

(IDE选用的是CLion,配合Cmake做的库链接和编译配置。)

单独拿出来比较有综合性的协程版Https示例来说吧。

头文件

1
2
3
4
5
6
7
8
9
10
11
12
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream.hpp>
//为代码简易使用如下定义,官网更加简略,但个人觉得刚开始学习,不要怕麻烦写全可以更好的理解库结构
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
namespace http = boost::beast::http; // from <boost/beast/http.hpp>

主要代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//方便测试,定义一些全局变量
auto const ssl_host = "127.0.0.1";//配合写的本地服务端使用
auto const ssl_port = "445";
auto const ssl_target = "/";
int version = 11;
void coHttpsRequest();

int main() {
//计时
auto start_time = boost::get_system_time();
std::cout << "Ready, GO!" << std::endl << std::endl;

coHttpsRequest();

long end_time = (boost::get_system_time() - start_time).total_milliseconds();
std::cout << "Completed in: " << end_time << "ms" << std::endl;
return 0;
}

//参数用std::string类型的目的应该是防止回调时const char*类型已经被回收变成野指针,之前遇到过类似问题
void do_ssl_session(
std::string const &host_,
std::string const &port_,
std::string const &target_,
int version_,
boost::asio::io_context &ioc,
ssl::context &ctx,
boost::asio::yield_context yield
) {
boost::beast::error_code ec;
//解析器
tcp::resolver resolver(ioc);
//tcp流包装为ssl流
boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
/**
* SNI(Server Name Indication)定义在RFC 4366,是一项用于改善SSL/TLS的技术,在SSLv3/TLSv1中被启用。
* 它允许客户端在发起SSL握手请求时(具体说来,是客户端发出SSL请求中的ClientHello阶段),就提交请求的Host信息,使得服务器能够切换到正确的域并返回相应的证书。
* 在 TLSv1.2(OpenSSL 0.9.8)版本开始支持。
* 少部分网站会需要做这一步。
*/
if (!SSL_set_tlsext_host_name(stream.native_handle(), host_.c_str())) {
ec.assign(static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category());
std::cerr << ec.message() << std::endl;
return;
}
//解析域名
auto const results = resolver.async_resolve(host_, port_, yield[ec]);
if (ec)
return fail(ec, "resolve");
//设置超时
boost::beast::get_lowest_layer(stream)
.expires_after(std::chrono::seconds(30));
//连接
boost::beast::get_lowest_layer(stream)
.async_connect(results, yield[ec]);
if (ec)
return fail(ec, "connect");
//设置超时并握手
boost::beast::get_lowest_layer(stream)
.expires_after(std::chrono::seconds(30));
stream.async_handshake(ssl::stream_base::client, yield[ec]);
if (ec)
return fail(ec, "handshake");
//设置http请求体并发出请求
http::request<http::empty_body> req{http::verb::get, target, version};
req.set(http::field::host, host_);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
boost::beast::get_lowest_layer(stream)
.expires_after(std::chrono::seconds(30));
http::async_write(stream, req, yield[ec]);
if (ec)
return fail(ec, "write");
//创建流接收响应体
http::response<http::dynamic_body> res;
boost::beast::flat_buffer buffer;
http::async_read(stream, buffer, res, yield[ec]);
if (ec)
return fail(ec, "read");
std::cout << res << std::endl;
//关闭流
boost::beast::get_lowest_layer(stream)
.expires_after(std::chrono::seconds(30));
stream.async_shutdown(yield[ec]);
if (ec == boost::asio::error::eof) {
ec = {};
}
if (ec)
return fail(ec, "shutdown");

}

void coHttpsRequest() {
//创建必要的两个上下文
boost::asio::io_context ioc;
ssl::context ctx(ssl::context::tlsv12_client);
//设置证书验证参数 客户端理论上不验证也可保证请求正确
//使用自己的证书文件测试还存在问题,需要了解证书工作方式之后再来研
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_peer);
//使用spawn开启一个协程
boost::asio::spawn(ioc, std::bind(
&do_ssl_session,
std::string(ssl_host),
std::string(ssl_port),
std::string(ssl_target),
version,
/**
std::ref 用于包装按引用传递的值。
std::cref 用于包装按const引用传递的值。
为什么需要std::ref和std::cref
bind()是一个函数模板,它的原理是根据已有的模板,生成一个函数。
但是由于bind()不知道生成的函数执行的时候,传递进来的参数是否还有效,在实现时,默认对参数进行了拷贝。
所以如果想传递引用保证效率,std::ref和std::cref就派上用场了。
*/
std::ref(ioc),
std::ref(ctx),
std::placeholders::_1
));
ioc.run();
}

个人理解

代码中每一步都做了详细注释,并且在一些方法使用上也去了解了使用其的原因并做了记录。

同步、异步和协程

之前只知道同步就是按代码流程一步步走,某一步卡住会阻塞进程直到整块代码完成;异步就是在每一步使用一个通知,在该步骤结束时会调用该方法进行后续操作;而对于协程一直只知道一个概念,并且感觉大家都在用。

经过这个例子的学习,感受就是,协程可以说是两种方式的一种结合,代码结构上就像同步实现方式一样,一行行执行,不会像异步一样需要定义大量的回调函数,代码逻辑更加清晰;而执行起来,又像是异步的,在每个步骤中使用yield,当执行到这一步时,在得到结果之前会挂起当前协程去执行其他协程,当得到回调时再回来继续进行后续逻辑。整体上又比开多个线程去这样处理开销会小很多,具体原因就需要了解CPU工作机制,这里不详谈(因为我也只是查询了一些文章简单了解- -!)。

这样看的话,如果想要高并发,使用协程确实真香。

接下来

客户端比较简单,遇到的问题很少。后面会整理服务端实现,还有一些cmake配置。

Powered by Hexo

Copyright © 2018 - 2022 Yshen's Blog All Rights Reserved.

UV : | PV :

Fork me on GitHub