使用Boost实现Http(s)服务端

从当初刚开始接触网站,使用IIS或者tomcat搭建服务器,从来没有想过自己有一天会去尝试自己实现一个简易服务端,隔了这么多年,现在也终于触及到这一步了,还是蛮有意思的。算是向底层又迈进一步,不过也仅仅是开始。

还是一样,看基础实现思路,参考官方示例,记录自己的想法。

整个练习代码项目链接放在上一篇客户端实现了,官方为: https://boost.org

基本实现

先说个额外的话题,用多了JetBrains家的IDE,快捷键习惯有点改不掉,开始写C++后用了一阵VS,效率有点低,就去找了CLion用。然后写着写着,到服务端这里,可以与客户端联调测试了,发现默认设置不能同时运行多个main函数;查了些资料,解决方案还是很简单的,稍微配置下cmake脚本即可,这里提一下。

Cmake编译脚本

只要添加多个add_executable及target_link_libraries块,分别链接两个端代码需要的类和库,重新load配置后,即可发现在界面右上角运行那里会出现多个target,分别选择去运行就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cmake_minimum_required(VERSION 3.16)
project(CppLearningDemos)

set(CMAKE_CXX_STANDARD 14)

#配置boost库
#添加头文件搜索路径
include_directories(/your/dir/boost_1_73_0/prefix/include)
include_directories(/usr/local/Cellar/openssl@1.1/1.1.1d/include)
#添加库文件搜索路径
link_directories(/your/dir/boost_1_73_0/prefix/lib)
link_directories(/usr/local/Cellar/openssl@1.1/1.1.1d/lib)

add_executable(CppLearningDemos main.cpp src/SomeTools.cpp src/SomeTools.h src/TestClazzSize.cpp src/TestClazzSize.h demos.cpp src/TestFileTemplate.cpp src/TestFileTemplate.h src/SimpleHttpClient.cpp src/SimpleHttpClient.h src/SimpleHttpClient2.cpp src/SimpleHttpClient2.h src/ROOT_CERITICATES.h src/SimpleHttpsClient.cpp src/SimpleHttpsClient.h)
#使用openssl 需要link如下两个库, 使用非head-only的库需要手动link
target_link_libraries(CppLearningDemos ssl crypto boost_coroutine)

#配置多个main函数同时运行
#servers
add_executable(servers servers.cpp src/SimpleHttpServer.cpp src/SimpleHttpServer.h src/RequestHandler.h src/SimpleHttpsServer.cpp src/SimpleHttpsServer.h)
target_link_libraries(servers ssl crypto boost_coroutine)

主要代码

还是以协程方式的Https来做记录,综合性比较强。

处理请求回响应体部分:

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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>
namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>

//确定mime type,使用string_view类型更轻量,比std::string效率更高
inline beast::string_view mime_type(beast::string_view path) {
using beast::iequals;
auto const ext = [&path] {
auto const pos = path.rfind(".");
return pos == beast::string_view::npos ? beast::string_view{} : path.substr(pos);
}();
if (iequals(ext, ".htm")) return "text/html";
if (iequals(ext, ".html")) return "text/html";
if (iequals(ext, ".php")) return "text/html";
if (iequals(ext, ".css")) return "text/css";
if (iequals(ext, ".txt")) return "text/plain";
if (iequals(ext, ".js")) return "application/javascript";
if (iequals(ext, ".json")) return "application/json";
if (iequals(ext, ".xml")) return "application/xml";
if (iequals(ext, ".swf")) return "application/x-shockwave-flash";
if (iequals(ext, ".flv")) return "video/x-flv";
if (iequals(ext, ".png")) return "image/png";
if (iequals(ext, ".jpe")) return "image/jpeg";
if (iequals(ext, ".jpeg")) return "image/jpeg";
if (iequals(ext, ".jpg")) return "image/jpeg";
if (iequals(ext, ".gif")) return "image/gif";
if (iequals(ext, ".bmp")) return "image/bmp";
if (iequals(ext, ".ico")) return "image/vnd.microsoft.icon";
if (iequals(ext, ".tiff")) return "image/tiff";
if (iequals(ext, ".tif")) return "image/tiff";
if (iequals(ext, ".svg")) return "image/svg+xml";
if (iequals(ext, ".svgz")) return "image/svg+xml";
return "application/text";
}

//处理path,主要是不同系统下的地址格式不同
inline std::string path_cat(
beast::string_view base,
beast::string_view path
) {
if (base.empty())
return std::string(path);
std::string result(base);
#ifdef BOOST_MSVC
char constexpr path_separator = '\\';
if (result.back() == path_separator)
result.resize(result.size() - 1);
result.append(path.data(), path.size());
for (auto &c : result)
if (c == '/')
c = path_separator;
#else
char constexpr path_separator = '/';
if (result.back() == path_separator)
result.resize(result.size() - 1);
result.append(path.data(), path.size());
#endif
return result;
}

//处理请求,产生响应体,参数右值引用避免赋值提高效率
template<class Body, class Allocator, class Send>
inline void handle_request(
beast::string_view doc_root,
/**
* 移动语义是C++11新增的重要功能,其重点是对右值的操作。
* 右值可以看作程序运行中的临时结果,可以避免复制提高效率。
*/
http::request<Body, http::basic_fields<Allocator>> &&req,
Send &&send
) {
//创建几种错误回复
auto const bad_request =
[&req](beast::string_view why) {
http::response<http::string_body> res{http::status::bad_request, req.version()};
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.set(http::field::content_type, "text/html");
res.keep_alive(req.keep_alive());
res.body() = std::string(why);
res.prepare_payload();
return res;
};
auto const not_found =
[&req](beast::string_view target) {
http::response<http::string_body> res{http::status::not_found, req.version()};
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.set(http::field::content_type, "text/html");
res.keep_alive(req.keep_alive());
res.body() = "The resource '" + std::string(target) + "' was not found.";
res.prepare_payload();
return res;
};
auto const server_error =
[&req](beast::string_view what) {
http::response<http::string_body> res{http::status::internal_server_error, req.version()};
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.set(http::field::content_type, "text/html");
res.keep_alive(req.keep_alive());
res.body() = "An error occurred: '" + std::string(what) + "'";
res.prepare_payload();
return res;
};
//不能处理的method返回bad_request
/**
* HTTP请求方法并不是只有GET和POST,只是最常用的。
* 据RFC2616标准(现行的HTTP/1.1)得知,通常有以下8种方法:OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE和CONNECT。
* 官方定义
* HEAD方法跟GET方法相同,只不过服务器响应时不会返回消息体。一个HEAD请求的响应中,HTTP头中包含的元信息应该和一个GET请求的响应消息相同。
* 这种方法可以用来获取请求中隐含的元信息,而不用传输实体本身。也经常用来测试超链接的有效性、可用性和最近的修改。
* 一个HEAD请求的响应可被缓存,也就是说,响应中的信息可能用来更新之前缓存的实体。
* 如果当前实体跟缓存实体的阈值不同(可通过Content-Length、Content-MD5、ETag或Last-Modified的变化来表明),那么这个缓存就被视为过期了。
简而言之
HEAD请求常常被忽略,但是能提供很多有用的信息,特别是在有限的速度和带宽下。主要有以下特点:
1、只请求资源的首部;
2、检查超链接的有效性;
3、检查网页是否被修改;
4、多用于自动搜索机器人获取网页的标志信息,获取rss种子信息,或者传递安全认证信息等
*/
if (req.method() != http::verb::get &&
req.method() != http::verb::head)
return send(bad_request("Unknown HTTP-method"));
//path验证失败的也返回bad_request
if (req.target().empty() ||
req.target()[0] != '/' ||
req.target().find("..") != beast::string_view::npos)
return send(bad_request("Illegal request-target"));

//处理path
std::string path = path_cat(doc_root, req.target());
if (req.target().back() == '/')
path.append("index.html");
//处理请求
beast::error_code ec;
http::file_body::value_type body;
body.open(path.c_str(), beast::file_mode::scan, ec);
//404
if (ec == beast::errc::no_such_file_or_directory)
return send(not_found(req.target()));
//未知错误
if (ec)
return send(server_error(ec.message()));

//返回正常请求
auto const size = body.size();
if (req.method() == http::verb::head) {
http::response<http::empty_body> res(http::status::ok, req.version());
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.set(http::field::content_type, mime_type(path));
res.content_length(size);
res.keep_alive(req.keep_alive());
return send(std::move(res));
}
/**
* std::move相关
* 在c++中,一个值要么是右值,要么是左值,左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。
* 比如:
* 常见的右值:“abc",123等都是右值。
* 右值引用,用以引用一个右值,可以延长右值的生命期。
* 为什么用右值引用
* C++引入右值引用之后,可以通过右值引用,充分使用临时变量,或者即将不使用的变量即右值的资源,减少不必要的拷贝,提高效率
*
* 从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
* 此方法以非常简单的方式将左值引用转换为右值引用
* 使用场景:1 定义的类使用了资源并定义了移动构造函数和移动赋值运算符,2 该变量即将不再使用
*/
http::response<http::file_body> res{
std::piecewise_construct,
std::make_tuple(std::move(body)),
std::make_tuple(http::status::ok, req.version())
};
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.set(http::field::content_type, mime_type(path));
res.content_length(size);
res.keep_alive(req.keep_alive());
return send(std::move(res));
/**
* map 的特殊情况
* map 类型的 emplace 处理比较特殊,因为和其他的容器不同,map 的 emplace 函数把它接收到的所有的参数都转发给 pair的构造函数。
* 对于一个 pair 来说,它既需要构造它的 key 又需要构造它的 value。
* 如果我们按照普通的 的语法使用变参模板,我们无法区分哪些参数用来构造 key, 哪些用来构造 value。 比如下面的代码:
* map<string, complex<double>> scp;
* scp.emplace("hello", 1, 2); // 无法区分哪个参数用来构造 key 哪些用来构造 value
* 所以我们需要一种方式既可以接受异构变长参数,又可以区分 key 和 value,解决 方式是使用 C++11 中提供的 tuple。
* pair<string, complex<double>> scp(make_tuple("hello"), make_tuple(1, 2));
* 然后这种方式是有问题的,因为这里有歧义,第一个 tuple 会被当成是 key,第二 个tuple会被当成 value。
* 最终的结果是类型不匹配而导致对象创建失败,为了解决 这个问题,C++11 设计了 piecewise_construct_t 这个类型用于解决这种歧义
* 它是一个空类,存在的唯一目的就是解决这种歧义,全局变量 std::piecewise_construct 就是该类型的一个变量。
*/
}

//错误报告
inline void fail(beast::error_code ec, const char *what) {
// ssl::error::stream_truncated, also known as an SSL "short read",
// indicates the peer closed the connection without performing the
// required closing handshake (for example, Google does this to
// improve performance). Generally this can be a security issue,
// but if your communication protocol is self-terminated (as
// it is with both HTTP and WebSocket) then you may simply
// ignore the lack of close_notify.
//
// https://github.com/boostorg/beast/issues/38
//
// https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
//
// When a short read would cut off the end of an HTTP message,
// Beast returns the error beast::http::error::partial_message.
// Therefore, if we see a short read here, it has occurred
// after the message has been completed, so it is safe to ignore it.
// ssl一个特有错误,上面是官方解释,如果出现,可以忽略
if (ec == net::ssl::error::stream_truncated)
return;
std::cerr << what << ": " << ec.message() << std::endl;
}

一个很巧妙的结构体,重写()操作符,即可当作递交参数的函数使用,后面处理会话时传给handle_request方法使用:

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
struct coro_send_ssl_lambda {
beast::ssl_stream<beast::tcp_stream> &stream_;
bool &close_;
beast::error_code &ec_;
net::yield_context yield_;

coro_send_ssl_lambda(
beast::ssl_stream<beast::tcp_stream> &stream,
bool &close,
beast::error_code &ec,
net::yield_context yield)
: stream_(stream),
close_(close),
ec_(ec),
yield_(yield) {
}

template<bool isRequest, class Body, class Fields>
void
operator()(http::message<isRequest, Body, Fields> &&msg) const {
// 需要关闭时关闭
close_ = msg.need_eof();

//回复响应
http::serializer<isRequest, Body, Fields> sr{msg};
http::async_write(stream_, sr, yield_[ec_]);
}
};

处理会话部分:

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
void coro_do_ssl_session(
beast::ssl_stream<beast::tcp_stream> &stream,
std::shared_ptr<std::string const> const &doc_root,
net::yield_context yield
) {
bool close = false;
beast::error_code ec;
//设置超时
beast::get_lowest_layer(stream)
.expires_after(std::chrono::seconds(30));
//与普通http区别就是读请求前先握手
stream.async_handshake(ssl::stream_base::server, yield[ec]);
if (ec)
return fail(ec, "handshake");
//用于接收请求
beast::flat_buffer buffer;
//初始化发包结构体
coro_send_ssl_lambda lambda(stream, close, ec, yield);

for (;;) {
//设置超时
beast::get_lowest_layer(stream)
.expires_after(std::chrono::seconds(30));
//读请求
http::request<http::string_body> req;
http::async_read(stream, buffer, req, yield[ec]);
if (ec == http::error::end_of_stream)
//这个错误一般是客户端突然关闭了请求
break;
if (ec)
return fail(ec, "read");
//处理请求,使用结构体递交相关参数
handle_request(*doc_root, std::move(req), lambda);
if (ec)
return fail(ec, "write");
if (close) {
//跳出循环,踢掉客户端连接
break;
}
}
beast::get_lowest_layer(stream)
.expires_after(std::chrono::seconds(30));
stream.async_shutdown(yield[ec]);
if (ec)
return fail(ec, "shutdown");
}

监听端口部分:

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
void coro_do_ssl_listen(
net::io_context &ioc,
ssl::context &ctx,
tcp::endpoint endpoint,
std::shared_ptr<std::string const> const &doc_root,
net::yield_context yield
) {
beast::error_code ec;
//打开监听器
tcp::acceptor acceptor(ioc);
acceptor.open(endpoint.protocol(), ec);//目前一般是ipv4协议
if (ec)
return fail(ec, "open");
//设置允许重用
acceptor.set_option(net::socket_base::reuse_address(true), ec);
if (ec)
return fail(ec, "set_option");
//绑定地址及端口
acceptor.bind(endpoint, ec);
if (ec)
return fail(ec, "bind");
//开始监听
acceptor.listen(net::socket_base::max_listen_connections, ec);
if (ec)
return fail(ec, "listen");
for (;;) {
tcp::socket socket(ioc);
acceptor.async_accept(socket, yield[ec]);
if (ec)
fail(ec, "accept");
else
//开启协程处理会话
boost::asio::spawn(
acceptor.get_executor(),
std::bind(
&coro_do_ssl_session,
beast::ssl_stream<beast::tcp_stream>(std::move(socket), ctx),
doc_root,
std::placeholders::_1));
}
}

最后是主要的启动流程,main中调用即可:

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
void coroHttpsServer() {
auto const address_ = net::ip::make_address(address);
auto const port_ = static_cast<unsigned short>(ssl_port);
auto const root_ = std::make_shared<std::string>(root);
//创建io及ssl上下文
net::io_context ioc{thread_num};
ssl::context ctx{ssl::context::sslv23};
//设置使用的证书
ctx.use_certificate_chain_file("crt.crt");
ctx.use_private_key_file("key.key", ssl::context::file_format::pem);
//开启协程监听端口
boost::asio::spawn(
ioc,
std::bind(
&coro_do_ssl_listen,
std::ref(ioc),
std::ref(ctx),
tcp::endpoint(address_, port_),
root_,
std::placeholders::_1));
//设置开启线程数
std::vector<std::thread> vector;
vector.reserve(thread_num - 1);
for (auto i = thread_num - 1; i > 0; --i)
/**
* 使用emplace_back()取代push_back()
* push_back()函数向容器中加入一个临时对象(右值元素)时,
* 首先会调用构造函数生成这个对象,然后调用拷贝构造函数将这个对象放入容器中,最后释放临时对象。
* 但是emplace_back()函数向容器中中加入临时对象, 临时对象原地构造,不用拷贝,没有赋值或移动的操作。
*/
vector.emplace_back([&ioc] {
ioc.run();
});
ioc.run();
}

一些理解

这其中结构体的巧妙使用对我这个刚开始用c++的人来说,确实是一个小惊艳,原来重写操作符可以做到这么灵活。服务端的复杂度比客户端增加了一些,主要是处理各种异常情况和各类文件格式,还包括用模板函数来兼容多类型的请求和响应。

存在疑问

在使用上下文时多线程要开启多个,监听端口是协程开启,对应的处理会话也是协程开启,那么在高并发中,主要应该去处理哪个环节呢。并且感觉在代码逻辑中,如果并发数超过了设置的数量,后续的请求应该像同步一样处在等待状态才对。那么就突然有点不理解之前我们产品中出现的问题,当并发数超过了本地设置的数量,会卡死,但不崩溃,并且只有在安卓端会出现,ios就能正常工作,到时候还是要去想办法动态调试看怎么回事,也许是系统限制?

后续

客户端和服务端示例都看得差不多了,准备尝试写一个简单的转发器,理了下思路,应该不会太难。后面再完整的撸一遍产品代码,加深理解。

Powered by Hexo

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

UV : | PV :

Fork me on GitHub