diff --git a/cmake/Conan.cmake b/cmake/Conan.cmake index f103a6d..6d5b90c 100644 --- a/cmake/Conan.cmake +++ b/cmake/Conan.cmake @@ -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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b672b59..c55760f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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( diff --git a/src/boost.beast/CMakeLists.txt b/src/boost.beast/CMakeLists.txt new file mode 100644 index 0000000..1d35646 --- /dev/null +++ b/src/boost.beast/CMakeLists.txt @@ -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) diff --git a/src/boost.beast/data.h b/src/boost.beast/data.h new file mode 100644 index 0000000..380e298 --- /dev/null +++ b/src/boost.beast/data.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include + +namespace data { +// a simple struct to model a person +struct person +{ + std::string name; + 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 diff --git a/src/boost.beast/error_handling.h b/src/boost.beast/error_handling.h new file mode 100644 index 0000000..581a0fc --- /dev/null +++ b/src/boost.beast/error_handling.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +// from +namespace beast = boost::beast; +// from +namespace http = beast::http; +// from +namespace asio = boost::asio; +// from +using tcp = boost::asio::ip::tcp; + + +inline void fail(beast::error_code ec, char const *what) { fmt::format("FAILED {0}: {1}", what, ec.message()); } diff --git a/src/boost.beast/listener.h b/src/boost.beast/listener.h new file mode 100644 index 0000000..fa03bfb --- /dev/null +++ b/src/boost.beast/listener.h @@ -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 +{ + 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(std::move(socket), m_persons)->run(); + } + + // Accept another connection + acceptNextConnection(); + } + + asio::io_context &m_ioc; + tcp::acceptor m_acceptor; + std::vector m_persons; +}; diff --git a/src/boost.beast/main.cpp b/src/boost.beast/main.cpp new file mode 100644 index 0000000..49c764f --- /dev/null +++ b/src/boost.beast/main.cpp @@ -0,0 +1,30 @@ +#include +#include +#include +#include +#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(ioc, tcp::endpoint{ address, port })->run(); + + // Run the I/O service on the requested number of threads + std::vector v; + v.reserve(threads); + for (auto i = 0; i < threads; i++) + { + v.emplace_back([&ioc] { ioc.run(); }); + } + ioc.run(); + + return EXIT_SUCCESS; +} diff --git a/src/boost.beast/request.h b/src/boost.beast/request.h new file mode 100644 index 0000000..afbd86d --- /dev/null +++ b/src/boost.beast/request.h @@ -0,0 +1,75 @@ +#pragma once +#include "error_handling.h" + +template http::response inline createResponse(Request &&req, http::status status, const nlohmann::json &jsonResponse) +{ + http::response 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 inline void handleRequest(http::request> &&req, Send &&send, std::vector &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(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))); + } + } +} diff --git a/src/boost.beast/session.h b/src/boost.beast/session.h new file mode 100644 index 0000000..1130348 --- /dev/null +++ b/src/boost.beast/session.h @@ -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 +{ + public: + // Take ownership of the stream + explicit Session(tcp::socket &&socket, std::vector &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 &m_person; + + // The function object is used to send an HTTP message. + struct sendLambda + { + public: + explicit sendLambda(Session &self) : m_self(self) {} + + template void operator()(http::message &&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>(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 m_req; + std::shared_ptr m_res; + sendLambda m_lambda; +};