Skip to content

Commit e506863

Browse files
authored
Merge pull request #2 from StephanKa/feature/add-boost-beast-example
boost beast example
2 parents a23b1ce + f32d58b commit e506863

9 files changed

Lines changed: 355 additions & 1 deletion

File tree

cmake/Conan.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ macro(run_conan)
1616
fmt/8.0.1
1717
spdlog/1.9.2
1818
sml/1.1.4
19+
nlohmann_json/3.10.0
20+
boost/1.76.0
1921
OPTIONS
2022
${CONAN_EXTRA_OPTIONS}
2123
gtest:build_gmock=True

src/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
OPTION(CPP_STARTER_USE_SML "Enable compilation of SML sample" OFF)
2+
OPTION(CPP_STARTER_USE_BOOST_BEAST "Enable compilation of boost beast sample" OFF)
23

3-
# Nana
4+
# SML
45
IF(CPP_STARTER_USE_SML)
56
MESSAGE("Using SML")
67
ADD_SUBDIRECTORY(sml)
78
ENDIF()
89

10+
# Boost Beast
11+
IF(CPP_STARTER_USE_BOOST_BEAST)
12+
MESSAGE("Using Boost Beast")
13+
ADD_SUBDIRECTORY(boost.beast)
14+
ENDIF()
15+
16+
917
# Generic test that uses conan libs
1018
ADD_EXECUTABLE(intro main.cpp)
1119
TARGET_LINK_LIBRARIES(

src/boost.beast/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ADD_EXECUTABLE(test_boost_beast main.cpp)
2+
TARGET_LINK_LIBRARIES(test_boost_beast PRIVATE CONAN_PKG::boost CONAN_PKG::nlohmann_json CONAN_PKG::fmt)

src/boost.beast/data.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
#include <nlohmann/json.hpp>
3+
#include <string>
4+
5+
namespace data {
6+
// a simple struct to model a person
7+
struct person
8+
{
9+
std::string name;
10+
std::string address;
11+
int age = {0};
12+
int id = {0};
13+
};
14+
15+
inline void to_json(nlohmann::json &j, const person &p) { j = nlohmann::json{ { "name", p.name }, { "address", p.address }, { "age", p.age }, { "id", p.id } }; }
16+
17+
inline void from_json(const nlohmann::json &j, person &p)
18+
{
19+
j.at("name").get_to(p.name);
20+
j.at("address").get_to(p.address);
21+
j.at("age").get_to(p.age);
22+
j.at("id").get_to(p.id);
23+
}
24+
}// namespace data

src/boost.beast/error_handling.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
#include <boost/beast/core.hpp>
3+
#include <boost/beast/http.hpp>
4+
#include <boost/beast/version.hpp>
5+
#include <boost/asio/dispatch.hpp>
6+
#include <boost/asio/strand.hpp>
7+
#include <fmt/format.h>
8+
9+
// from <boost/beast.hpp>
10+
namespace beast = boost::beast;
11+
// from <boost/beast/http.hpp>
12+
namespace http = beast::http;
13+
// from <boost/asio.hpp>
14+
namespace asio = boost::asio;
15+
// from <boost/asio/ip/tcp.hpp>
16+
using tcp = boost::asio::ip::tcp;
17+
18+
19+
inline void fail(beast::error_code ec, char const *what) { fmt::format("FAILED {0}: {1}", what, ec.message()); }

src/boost.beast/listener.h

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#pragma once
2+
#include "error_handling.h"
3+
#include "data.h"
4+
#include "session.h"
5+
6+
// Accepts incoming connections and launches the sessions
7+
class Listener : public std::enable_shared_from_this<Listener>
8+
{
9+
public:
10+
Listener(asio::io_context &ioc, const tcp::endpoint &endpoint) : m_ioc(ioc), m_acceptor(asio::make_strand(ioc))
11+
{
12+
beast::error_code ec;
13+
14+
// Open the acceptor
15+
m_acceptor.open(endpoint.protocol(), ec);
16+
if (ec)
17+
{
18+
fail(ec, "open");
19+
return;
20+
}
21+
22+
// Allow address reuse
23+
m_acceptor.set_option(asio::socket_base::reuse_address(true), ec);
24+
if (ec)
25+
{
26+
fail(ec, "set_option");
27+
return;
28+
}
29+
30+
// Bind to the server address
31+
m_acceptor.bind(endpoint, ec);
32+
if (ec)
33+
{
34+
fail(ec, "bind");
35+
return;
36+
}
37+
38+
// Start listening for connections
39+
m_acceptor.listen(asio::socket_base::max_listen_connections, ec);
40+
if (ec)
41+
{
42+
fail(ec, "listen");
43+
return;
44+
}
45+
}
46+
47+
// Start accepting incoming connections
48+
void run() { acceptNextConnection(); }
49+
50+
private:
51+
void acceptNextConnection()
52+
{
53+
// The new connection gets its own strand
54+
m_acceptor.async_accept(asio::make_strand(m_ioc), beast::bind_front_handler(&Listener::onAccept, shared_from_this()));
55+
}
56+
57+
void onAccept(beast::error_code ec, tcp::socket socket)
58+
{
59+
if (ec)
60+
{
61+
fail(ec, "accept");
62+
}
63+
else
64+
{
65+
// Create the session and run it
66+
std::make_shared<Session>(std::move(socket), m_persons)->run();
67+
}
68+
69+
// Accept another connection
70+
acceptNextConnection();
71+
}
72+
73+
asio::io_context &m_ioc;
74+
tcp::acceptor m_acceptor;
75+
std::vector<data::person> m_persons;
76+
};

src/boost.beast/main.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#include <cstdlib>
2+
#include <memory>
3+
#include <thread>
4+
#include <vector>
5+
#include "listener.h"
6+
7+
8+
int main()
9+
{
10+
const auto address = asio::ip::make_address("0.0.0.0");
11+
const uint16_t port = 8080u;
12+
const auto threads = 1;
13+
14+
// The io_context is required for all I/O
15+
asio::io_context ioc{ threads };
16+
17+
// Create and launch a listening port
18+
std::make_shared<Listener>(ioc, tcp::endpoint{ address, port })->run();
19+
20+
// Run the I/O service on the requested number of threads
21+
std::vector<std::thread> v;
22+
v.reserve(threads);
23+
for (auto i = 0; i < threads; i++)
24+
{
25+
v.emplace_back([&ioc] { ioc.run(); });
26+
}
27+
ioc.run();
28+
29+
return EXIT_SUCCESS;
30+
}

src/boost.beast/request.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#pragma once
2+
#include "error_handling.h"
3+
4+
template<typename Request> http::response<http::string_body> inline createResponse(Request &&req, http::status status, const nlohmann::json &jsonResponse)
5+
{
6+
http::response<http::string_body> res{ status, req.version() };
7+
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
8+
res.set(http::field::content_type, "application/json");
9+
res.keep_alive(req.keep_alive());
10+
res.body() = jsonResponse.dump();
11+
res.prepare_payload();
12+
return res;
13+
}
14+
15+
16+
// This function produces an HTTP response for the given
17+
// request. The type of the response object depends on the
18+
// contents of the request, so the interface requires the
19+
// caller to pass a generic lambda for receiving the response.
20+
template<class Body, class Allocator, class Send> inline void handleRequest(http::request<Body, http::basic_fields<Allocator>> &&req, Send &&send, std::vector<data::person> &data)
21+
{
22+
// Returns a bad request response
23+
const auto badRequest = [&req](beast::string_view why) {
24+
const auto j = nlohmann::json::parse(std::string(why));
25+
return createResponse(req, http::status::bad_request, j);
26+
};
27+
28+
// Make sure we can handle the method
29+
switch (req.method())
30+
{
31+
case http::verb::get: {
32+
// Respond to GET request
33+
nlohmann::json j = data;
34+
return send(std::move(createResponse(req, http::status::ok, j)));
35+
}
36+
case http::verb::put: {
37+
try
38+
{
39+
const auto j = nlohmann::json::parse(req.body());
40+
const data::person d = j;
41+
if (d.id > data.size() - 1 || d.id < 0)
42+
{
43+
const nlohmann::json j = R"({"error": "id is larger than data list or negative"})";
44+
return send(std::move(createResponse(req, http::status::internal_server_error, j)));
45+
}
46+
auto &temp = data.at(d.id - 1);
47+
temp.name = d.name;
48+
temp.address = d.address;
49+
temp.age = d.age;
50+
return send(std::move(createResponse(req, http::status::ok, j)));
51+
}
52+
catch (nlohmann::json::exception &e)
53+
{
54+
return send(std::move(badRequest(e.what())));
55+
}
56+
}
57+
case http::verb::post: {
58+
try
59+
{
60+
const auto j = nlohmann::json::parse(req.body());
61+
data.push_back(j);
62+
data.back().id = static_cast<int>(data.size());
63+
return send(std::move(createResponse(req, http::status::ok, j)));
64+
}
65+
catch (nlohmann::json::exception &e)
66+
{
67+
return send(std::move(badRequest(e.what())));
68+
}
69+
}
70+
default: {
71+
const nlohmann::json j = R"({"error": "http method not supported"})";
72+
return send(std::move(createResponse(req, http::status::internal_server_error, j)));
73+
}
74+
}
75+
}

src/boost.beast/session.h

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#pragma once
2+
#include "error_handling.h"
3+
#include "request.h"
4+
5+
// Handles an HTTP server connection
6+
class Session : public std::enable_shared_from_this<Session>
7+
{
8+
public:
9+
// Take ownership of the stream
10+
explicit Session(tcp::socket &&socket, std::vector<data::person> &person) : m_person(person), m_stream(std::move(socket)), m_lambda(*this) {}
11+
12+
// Start the asynchronous operation
13+
void run()
14+
{
15+
// We need to be executing within a strand to perform async operations
16+
// on the I/O objects in this session. Although not strictly necessary
17+
// for single-threaded contexts, this example code is written to be
18+
// thread-safe by default.
19+
asio::dispatch(m_stream.get_executor(), beast::bind_front_handler(&Session::doRead, shared_from_this()));
20+
}
21+
22+
void doRead()
23+
{
24+
// Make the request empty before reading,
25+
// otherwise the operation behavior is undefined.
26+
m_req = {};
27+
28+
// Set the timeout.
29+
m_stream.expires_after(std::chrono::seconds(30));
30+
31+
// Read a request
32+
http::async_read(m_stream, m_buffer, m_req, beast::bind_front_handler(&Session::onRead, shared_from_this()));
33+
}
34+
35+
void onRead(beast::error_code ec, std::size_t bytes_transferred)
36+
{
37+
boost::ignore_unused(bytes_transferred);
38+
39+
// This means they closed the connection
40+
if (ec == http::error::end_of_stream)
41+
{
42+
return doClose();
43+
}
44+
45+
if (ec)
46+
{
47+
return fail(ec, "read");
48+
}
49+
50+
// Send the response
51+
handleRequest(std::move(m_req), m_lambda, m_person);
52+
}
53+
54+
void onWrite(bool close, beast::error_code ec, std::size_t bytes_transferred)
55+
{
56+
boost::ignore_unused(bytes_transferred);
57+
58+
if (ec)
59+
{
60+
return fail(ec, "write");
61+
}
62+
63+
if (close)
64+
{
65+
// This means we should close the connection, usually because
66+
// the response indicated the "Connection: close" semantic.
67+
return doClose();
68+
}
69+
70+
// We're done with the response so delete it
71+
m_res = nullptr;
72+
73+
// Read another request
74+
doRead();
75+
}
76+
77+
void doClose()
78+
{
79+
// Send a TCP shutdown
80+
beast::error_code ec;
81+
m_stream.socket().shutdown(tcp::socket::shutdown_send, ec);
82+
83+
// At this point the connection is closed gracefully
84+
}
85+
86+
private:
87+
std::vector<data::person> &m_person;
88+
89+
// The function object is used to send an HTTP message.
90+
struct sendLambda
91+
{
92+
public:
93+
explicit sendLambda(Session &self) : m_self(self) {}
94+
95+
template<bool isRequest, class Body, class Fields> void operator()(http::message<isRequest, Body, Fields> &&msg) const
96+
{
97+
// The lifetime of the message has to extend
98+
// for the duration of the async operation so
99+
// we use a shared_ptr to manage it.
100+
auto sp = std::make_shared<http::message<isRequest, Body, Fields>>(std::move(msg));
101+
102+
// Store a type-erased version of the shared
103+
// pointer in the class to keep it alive.
104+
m_self.m_res = sp;
105+
106+
// Write the response
107+
http::async_write(m_self.m_stream, *sp, beast::bind_front_handler(&Session::onWrite, m_self.shared_from_this(), sp->need_eof()));
108+
}
109+
110+
private:
111+
Session &m_self;
112+
};
113+
beast::tcp_stream m_stream;
114+
beast::flat_buffer m_buffer;
115+
http::request<http::string_body> m_req;
116+
std::shared_ptr<void> m_res;
117+
sendLambda m_lambda;
118+
};

0 commit comments

Comments
 (0)