C++ https server based on boost asio and beast
程序员文章站
2022-06-01 09:20:12
...
目前在做的项目需要一个C++版本的https server,只能求助于boost库。幸运的是确实存在。并且提供了协程版本,本着学习的精神拿来改造一下,就获得了如下成果。
AsyncHttpServerV2.hpp
//
// Created by chuanqin on 7/5/21.
//
#ifndef CBRS_UT_TOOL_ASYNCHTTPSERVERV2_HPP_
#define CBRS_UT_TOOL_ASYNCHTTPSERVERV2_HPP_
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/config.hpp>
#include "common/Logger.hpp"
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
namespace cbrs
{
namespace ut
{
namespace tool
{
class AsyncHttpServerV2 : public std::enable_shared_from_this<AsyncHttpServerV2>
{
public:
AsyncHttpServerV2(unsigned short port);
~AsyncHttpServerV2() {}
void start();
void stop();
private:
void load_server_certificate();
void do_listen(
boost::asio::yield_context yield);
void do_session(
tcp::socket &socket,
boost::asio::yield_context yield);
void fail(boost::system::error_code ec, char const *what);
template <class Body, class Allocator, class Send>
void handle_request(
http::request<Body, http::basic_fields<Allocator>> &&req,
Send &&send);
std::string path_cat(boost::beast::string_view base, boost::beast::string_view path);
boost::beast::string_view mime_type(boost::beast::string_view path);
net::ip::address address;
unsigned short port;
std::shared_ptr<std::string> doc_root;
unsigned short threads;
net::io_context ioc;
ssl::context ctx;
std::string tls12CipherSuite;
logging::Logger logger_;
std::string certChain;
std::string privateKey;
std::vector<std::thread> threadList;
};
template <class Stream>
struct send_lambda
{
Stream &stream_;
bool &close_;
boost::system::error_code &ec_;
boost::asio::yield_context yield_;
explicit send_lambda(
Stream &stream,
bool &close,
boost::system::error_code &ec,
boost::asio::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
{
// Determine if we should close the connection after
close_ = msg.need_eof();
// We need the serializer here because the serializer requires
// a non-const file_body, and the message oriented version of
// http::write only works with const messages.
http::serializer<isRequest, Body, Fields> sr{msg};
http::async_write(stream_, sr, yield_[ec_]);
}
};
} // namespace tool
} // namespace ut
} // namespace cbrs
#endif // CBRS_UT_TOOL_ASYNCHTTPSERVERV2_HPP_
AsyncHttpServerV2.cpp
#include "AsyncHttpServerV2.hpp"
#include "CertificateInfo.hpp"
#include "common/Logger.hpp"
#include <boost/assert.hpp>
namespace boost
{
void assertion_failed_msg(
char const *expr, char const *msg, char const *function, char const *file, long line)
{
}
} // namespace boost
namespace cbrs
{
namespace ut
{
namespace tool
{
AsyncHttpServerV2::AsyncHttpServerV2(unsigned short port)
: address(net::ip::make_address("127.0.0.1")), port(port), doc_root(std::make_shared<std::string>(".")), threads(1), ioc{threads}, ctx{ssl::context::tlsv12_server}, tls12CipherSuite("AES128-GCM-SHA256:AES256-GCM-SHA384"), logger_("tool::AsyncHttpServerV2")
{
certChain = "please add the certs chain here";
privateKey = "add the private key here";
logger_ << info << "AsyncHttpServerV2 construct";
load_server_certificate();
threadList.reserve(threads);
logger_ << info << "ssl::context loaded";
}
void AsyncHttpServerV2::load_server_certificate()
{
ctx.set_options(
boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use);
SSL_CTX_set_cipher_list(ctx.native_handle(), tls12CipherSuite.c_str());
boost::system::error_code ecCert;
ctx.use_certificate_chain(boost::asio::buffer(certChain.data(), certChain.size()), ecCert);
if (ecCert)
return fail(ecCert, "cert chain");
boost::system::error_code ecKey;
ctx.use_private_key(
boost::asio::buffer(privateKey.data(), privateKey.size()),
boost::asio::ssl::context::pem,
ecKey);
if (ecKey)
return fail(ecKey, "private key");
ctx.set_verify_mode(ssl::verify_none);
}
void AsyncHttpServerV2::start()
{
boost::asio::spawn(
ioc,
std::bind(
&AsyncHttpServerV2::do_listen,
shared_from_this(),
std::placeholders::_1));
// Run the I/O service on the requested number of threads
for (auto i = threads; i > 0; --i)
threadList.emplace_back([this]
{ ioc.run(); });
logger_ << info << "ready to provide the service";
}
void AsyncHttpServerV2::stop()
{
logger_ << info << "stopping the https server";
ioc.stop();
for (auto &item : threadList)
{
item.join();
}
logger_ << info << "stopped the https server";
}
void AsyncHttpServerV2::do_listen(
boost::asio::yield_context yield)
{
boost::system::error_code ec;
// Open the acceptor
tcp::acceptor acceptor(ioc);
auto endpoint = tcp::endpoint{address, port};
acceptor.open(endpoint.protocol(), ec);
if (ec)
return fail(ec, "open");
// Allow address reuse
acceptor.set_option(boost::asio::socket_base::reuse_address(true), ec);
if (ec)
return fail(ec, "set_option");
// Bind to the server address
acceptor.bind(endpoint, ec);
if (ec)
return fail(ec, "bind");
// Start listening for connections
acceptor.listen(boost::asio::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
{
logger_ << info << "accept success";
boost::asio::spawn(
acceptor.get_executor().context(),
std::bind(
&AsyncHttpServerV2::do_session,
shared_from_this(),
std::move(socket),
std::placeholders::_1));
}
}
}
void AsyncHttpServerV2::do_session(
tcp::socket &socket,
boost::asio::yield_context yield)
{
bool close = false;
boost::system::error_code ec;
// Construct the stream around the socket
ssl::stream<tcp::socket &> stream{socket, ctx};
// Perform the SSL handshake
stream.async_handshake(ssl::stream_base::server, yield[ec]);
if (ec)
return fail(ec, "handshake");
logger_ << info << "handshake success";
// This buffer is required to persist across reads
boost::beast::flat_buffer buffer;
// This lambda is used to send messages
send_lambda<ssl::stream<tcp::socket &>> lambda{stream, close, ec, yield};
for (;;)
{
// Read a request
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");
// Send the response
handle_request(std::move(req), lambda);
if (ec)
return fail(ec, "write");
if (close)
{
// This means we should close the connection, usually because
// the response indicated the "Connection: close" semantic.
break;
}
}
// Perform the SSL shutdown
stream.async_shutdown(yield[ec]);
if (ec)
return fail(ec, "shutdown");
// At shared_from_this() point the connection is closed gracefully
}
void AsyncHttpServerV2::fail(boost::system::error_code ec, char const *what)
{
std::cerr << what << ": " << ec.message() << "\n";
}
template <class Body, class Allocator, class Send>
void AsyncHttpServerV2::handle_request(
http::request<Body, http::basic_fields<Allocator>> &&req,
Send &&send)
{
// Returns a bad request response
auto const bad_request = [&req](boost::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() = why.to_string();
res.prepare_payload();
return res;
};
// Returns a not found response
auto const not_found = [&req](boost::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 '" + target.to_string() + "' was not found.";
res.prepare_payload();
return res;
};
// Returns a server error response
auto const server_error = [&req](boost::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: '" + what.to_string() + "'";
res.prepare_payload();
return res;
};
// Make sure we can handle the method
if (req.method() != http::verb::get && req.method() != http::verb::head)
return send(bad_request("Unknown HTTP-method"));
// Request path must be absolute and not contain "..".
if (req.target().empty() || req.target()[0] != '/' || req.target().find("..") != boost::beast::string_view::npos)
return send(bad_request("Illegal request-target"));
// Build the path to the requested file
std::string path = path_cat(*doc_root, req.target());
if (req.target().back() == '/')
path.append("index.html");
// Attempt to open the file
boost::beast::error_code ec;
http::file_body::value_type body;
body.open(path.c_str(), boost::beast::file_mode::scan, ec);
// Handle the case where the file doesn't exist
if (ec == boost::system::errc::no_such_file_or_directory)
return send(not_found(req.target()));
// Handle an unknown error
if (ec)
return send(server_error(ec.message()));
// Cache the size since we need it after the move
auto const size = body.size();
// Respond to HEAD request
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));
}
// Respond to GET request
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));
}
std::string AsyncHttpServerV2::path_cat(
boost::beast::string_view base, boost::beast::string_view path)
{
if (base.empty())
return path.to_string();
std::string result = base.to_string();
char constexpr path_separator = '/';
if (result.back() == path_separator)
result.resize(result.size() - 1);
result.append(path.data(), path.size());
return result;
}
boost::beast::string_view AsyncHttpServerV2::mime_type(boost::beast::string_view path)
{
using boost::beast::iequals;
auto const ext = [&path]
{
auto const pos = path.rfind(".");
if (pos == boost::beast::string_view::npos)
return boost::beast::string_view{};
return 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";
}
} // namespace tool
} // namespace ut
} // namespace cbrs
testAsync.cxx
#include "AsyncHttpServerV2.hpp"
using AsyncHttpServerV2 = cbrs::ut::tool::AsyncHttpServerV2;
int main()
{
std::shared_ptr<AsyncHttpServerV2> server = std::make_shared<AsyncHttpServerV2>();
server->start();
sleep(100);
server->stop();
return 0;
}
最后来个简单的
CMakeLists.txt
project(asyncserver)
cmake_minimum_required(VERSION 3.17)
set(Boost_COMPONENTS date_time regex coroutine2)
set(BOOST_ROOT "/usr")
find_package(Boost)
find_package(PkgConfig)
find_package(OpenSSL REQUIRED)
add_executable(asyncserver
AsyncHttpServerV2.cpp
testAsync.cxx)
target_link_directories(asyncserver PRIVATE "/usr/lib/x86_64-linux-gnu/")
target_link_libraries(asyncserver PRIVATE boost_system boost_coroutine boost_thread pthread boost_context ssl crypto)
等我有空加点注释。