Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmake/Conan.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ macro(run_conan)
fmt/8.0.1
spdlog/1.9.2
sml/1.1.4
nlohmann_json/3.10.0
boost/1.76.0
OPTIONS
${CONAN_EXTRA_OPTIONS}
gtest:build_gmock=True
Expand Down
10 changes: 9 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
OPTION(CPP_STARTER_USE_SML "Enable compilation of SML sample" OFF)
OPTION(CPP_STARTER_USE_BOOST_BEAST "Enable compilation of boost beast sample" OFF)

# Nana
# SML
IF(CPP_STARTER_USE_SML)
MESSAGE("Using SML")
ADD_SUBDIRECTORY(sml)
ENDIF()

# Boost Beast
IF(CPP_STARTER_USE_BOOST_BEAST)
MESSAGE("Using Boost Beast")
ADD_SUBDIRECTORY(boost.beast)
ENDIF()


# Generic test that uses conan libs
ADD_EXECUTABLE(intro main.cpp)
TARGET_LINK_LIBRARIES(
Expand Down
2 changes: 2 additions & 0 deletions src/boost.beast/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ADD_EXECUTABLE(test_boost_beast main.cpp)
TARGET_LINK_LIBRARIES(test_boost_beast PRIVATE CONAN_PKG::boost CONAN_PKG::nlohmann_json CONAN_PKG::fmt)
24 changes: 24 additions & 0 deletions src/boost.beast/data.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once
#include <nlohmann/json.hpp>
#include <string>

namespace data {
// a simple struct to model a person
struct person
{
std::string name;
Comment thread
arnemertz marked this conversation as resolved.
std::string address;
int age = {0};
int id = {0};
};

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 } }; }

inline void from_json(const nlohmann::json &j, person &p)
{
j.at("name").get_to(p.name);
j.at("address").get_to(p.address);
j.at("age").get_to(p.age);
j.at("id").get_to(p.id);
}
}// namespace data
19 changes: 19 additions & 0 deletions src/boost.beast/error_handling.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/strand.hpp>
#include <fmt/format.h>

// from <boost/beast.hpp>
namespace beast = boost::beast;
// from <boost/beast/http.hpp>
namespace http = beast::http;
// from <boost/asio.hpp>
namespace asio = boost::asio;
// from <boost/asio/ip/tcp.hpp>
using tcp = boost::asio::ip::tcp;


inline void fail(beast::error_code ec, char const *what) { fmt::format("FAILED {0}: {1}", what, ec.message()); }
76 changes: 76 additions & 0 deletions src/boost.beast/listener.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#pragma once
#include "error_handling.h"
#include "data.h"
#include "session.h"

// Accepts incoming connections and launches the sessions
class Listener : public std::enable_shared_from_this<Listener>
{
public:
Listener(asio::io_context &ioc, const tcp::endpoint &endpoint) : m_ioc(ioc), m_acceptor(asio::make_strand(ioc))
{
beast::error_code ec;

// Open the acceptor
m_acceptor.open(endpoint.protocol(), ec);
if (ec)
{
fail(ec, "open");
return;
}

// Allow address reuse
m_acceptor.set_option(asio::socket_base::reuse_address(true), ec);
if (ec)
{
fail(ec, "set_option");
return;
}

// Bind to the server address
m_acceptor.bind(endpoint, ec);
if (ec)
{
fail(ec, "bind");
return;
}

// Start listening for connections
m_acceptor.listen(asio::socket_base::max_listen_connections, ec);
if (ec)
{
fail(ec, "listen");
return;
}
}

// Start accepting incoming connections
void run() { acceptNextConnection(); }

private:
void acceptNextConnection()
{
// The new connection gets its own strand
m_acceptor.async_accept(asio::make_strand(m_ioc), beast::bind_front_handler(&Listener::onAccept, shared_from_this()));
}

void onAccept(beast::error_code ec, tcp::socket socket)
{
if (ec)
{
fail(ec, "accept");
}
else
{
// Create the session and run it
std::make_shared<Session>(std::move(socket), m_persons)->run();
}

// Accept another connection
acceptNextConnection();
}

asio::io_context &m_ioc;
tcp::acceptor m_acceptor;
std::vector<data::person> m_persons;
};
30 changes: 30 additions & 0 deletions src/boost.beast/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include <cstdlib>
#include <memory>
#include <thread>
#include <vector>
#include "listener.h"


int main()
{
const auto address = asio::ip::make_address("0.0.0.0");
const uint16_t port = 8080u;
const auto threads = 1;

// The io_context is required for all I/O
asio::io_context ioc{ threads };

// Create and launch a listening port
std::make_shared<Listener>(ioc, tcp::endpoint{ address, port })->run();

// Run the I/O service on the requested number of threads
std::vector<std::thread> v;
v.reserve(threads);
for (auto i = 0; i < threads; i++)
{
v.emplace_back([&ioc] { ioc.run(); });
}
ioc.run();

return EXIT_SUCCESS;
}
75 changes: 75 additions & 0 deletions src/boost.beast/request.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#pragma once
#include "error_handling.h"

template<typename Request> http::response<http::string_body> inline createResponse(Request &&req, http::status status, const nlohmann::json &jsonResponse)
{
http::response<http::string_body> res{ status, req.version() };
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.set(http::field::content_type, "application/json");
res.keep_alive(req.keep_alive());
res.body() = jsonResponse.dump();
res.prepare_payload();
return res;
}


// This function produces an HTTP response for the given
// request. The type of the response object depends on the
// contents of the request, so the interface requires the
// caller to pass a generic lambda for receiving the response.
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)
{
// Returns a bad request response
const auto badRequest = [&req](beast::string_view why) {
const auto j = nlohmann::json::parse(std::string(why));
return createResponse(req, http::status::bad_request, j);
};

// Make sure we can handle the method
switch (req.method())
{
case http::verb::get: {
// Respond to GET request
nlohmann::json j = data;
return send(std::move(createResponse(req, http::status::ok, j)));
}
case http::verb::put: {
try
{
const auto j = nlohmann::json::parse(req.body());
const data::person d = j;
if (d.id > data.size() - 1 || d.id < 0)
{
const nlohmann::json j = R"({"error": "id is larger than data list or negative"})";
return send(std::move(createResponse(req, http::status::internal_server_error, j)));
}
auto &temp = data.at(d.id - 1);
temp.name = d.name;
temp.address = d.address;
temp.age = d.age;
return send(std::move(createResponse(req, http::status::ok, j)));
}
catch (nlohmann::json::exception &e)
{
return send(std::move(badRequest(e.what())));
}
}
case http::verb::post: {
try
{
const auto j = nlohmann::json::parse(req.body());
data.push_back(j);
data.back().id = static_cast<int>(data.size());
return send(std::move(createResponse(req, http::status::ok, j)));
}
catch (nlohmann::json::exception &e)
{
return send(std::move(badRequest(e.what())));
}
}
default: {
const nlohmann::json j = R"({"error": "http method not supported"})";
return send(std::move(createResponse(req, http::status::internal_server_error, j)));
}
}
}
118 changes: 118 additions & 0 deletions src/boost.beast/session.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#pragma once
#include "error_handling.h"
#include "request.h"

// Handles an HTTP server connection
class Session : public std::enable_shared_from_this<Session>
{
public:
// Take ownership of the stream
explicit Session(tcp::socket &&socket, std::vector<data::person> &person) : m_person(person), m_stream(std::move(socket)), m_lambda(*this) {}

// Start the asynchronous operation
void run()
{
// We need to be executing within a strand to perform async operations
// on the I/O objects in this session. Although not strictly necessary
// for single-threaded contexts, this example code is written to be
// thread-safe by default.
asio::dispatch(m_stream.get_executor(), beast::bind_front_handler(&Session::doRead, shared_from_this()));
}

void doRead()
{
// Make the request empty before reading,
// otherwise the operation behavior is undefined.
m_req = {};

// Set the timeout.
m_stream.expires_after(std::chrono::seconds(30));

// Read a request
http::async_read(m_stream, m_buffer, m_req, beast::bind_front_handler(&Session::onRead, shared_from_this()));
}

void onRead(beast::error_code ec, std::size_t bytes_transferred)
{
boost::ignore_unused(bytes_transferred);

// This means they closed the connection
if (ec == http::error::end_of_stream)
{
return doClose();
}

if (ec)
{
return fail(ec, "read");
}

// Send the response
handleRequest(std::move(m_req), m_lambda, m_person);
}

void onWrite(bool close, beast::error_code ec, std::size_t bytes_transferred)
{
boost::ignore_unused(bytes_transferred);

if (ec)
{
return fail(ec, "write");
}

if (close)
{
// This means we should close the connection, usually because
// the response indicated the "Connection: close" semantic.
return doClose();
}

// We're done with the response so delete it
m_res = nullptr;

// Read another request
doRead();
}

void doClose()
{
// Send a TCP shutdown
beast::error_code ec;
m_stream.socket().shutdown(tcp::socket::shutdown_send, ec);

// At this point the connection is closed gracefully
}

private:
std::vector<data::person> &m_person;

// The function object is used to send an HTTP message.
struct sendLambda
{
public:
explicit sendLambda(Session &self) : m_self(self) {}

template<bool isRequest, class Body, class Fields> void operator()(http::message<isRequest, Body, Fields> &&msg) const
{
// The lifetime of the message has to extend
// for the duration of the async operation so
// we use a shared_ptr to manage it.
auto sp = std::make_shared<http::message<isRequest, Body, Fields>>(std::move(msg));

// Store a type-erased version of the shared
// pointer in the class to keep it alive.
m_self.m_res = sp;

// Write the response
http::async_write(m_self.m_stream, *sp, beast::bind_front_handler(&Session::onWrite, m_self.shared_from_this(), sp->need_eof()));
}

private:
Session &m_self;
};
beast::tcp_stream m_stream;
beast::flat_buffer m_buffer;
http::request<http::string_body> m_req;
std::shared_ptr<void> m_res;
sendLambda m_lambda;
};