Skip to content

Commit 30a4027

Browse files
committed
initial boost beast example
1 parent ba70603 commit 30a4027

9 files changed

Lines changed: 352 additions & 1 deletion

File tree

cmake/Conan.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ macro(run_conan)
2222
fmt/8.0.1
2323
spdlog/1.9.2
2424
sml/1.1.4
25+
nlohmann_json/3.10.0
26+
boost/1.76.0
2527
OPTIONS
2628
${CONAN_EXTRA_OPTIONS}
2729
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+
4+
namespace data {
5+
// a simple struct to model a person
6+
struct person
7+
{
8+
std::string name;
9+
std::string address;
10+
int age = {0};
11+
int id = {0};
12+
};
13+
14+
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 } }; }
15+
16+
inline void from_json(const nlohmann::json &j, person &p)
17+
{
18+
j.at("name").get_to(p.name);
19+
j.at("address").get_to(p.address);
20+
j.at("age").get_to(p.age);
21+
j.at("id").get_to(p.id);
22+
}
23+
24+
}// namespace data

src/boost.beast/helper.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#pragma once
2+
#include <fmt/format.h>
3+
4+
namespace beast = boost::beast;// from <boost/beast.hpp>
5+
namespace http = beast::http;// from <boost/beast/http.hpp>
6+
namespace net = boost::asio;// from <boost/asio.hpp>
7+
using tcp = boost::asio::ip::tcp;// from <boost/asio/ip/tcp.hpp>
8+
9+
10+
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 "helper.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(net::io_context &ioc, const tcp::endpoint &endpoint) : m_ioc(ioc), m_acceptor(net::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(net::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(net::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() { doAccept(); }
49+
50+
private:
51+
void doAccept()
52+
{
53+
// The new connection gets its own strand
54+
m_acceptor.async_accept(net::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+
doAccept();
71+
}
72+
73+
net::io_context &m_ioc;
74+
tcp::acceptor m_acceptor;
75+
std::vector<data::person> m_persons;
76+
};

src/boost.beast/main.cpp

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

src/boost.beast/request.h

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

src/boost.beast/session.h

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#pragma once
2+
#include "helper.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+
net::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+
// This is the C++11 equivalent of a generic lambda.
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+
114+
beast::tcp_stream m_stream;
115+
beast::flat_buffer m_buffer;
116+
http::request<http::string_body> m_req;
117+
std::shared_ptr<void> m_res;
118+
sendLambda m_lambda;
119+
};

0 commit comments

Comments
 (0)