1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
//
// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/asio/awaitable.hpp>
#include <boost/pfr/config.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT) && BOOST_PFR_CORE_NAME_ENABLED
//[example_http_server_cpp20_server_cpp
//
// File: server.cpp
//
// This file contains all the boilerplate code to implement a HTTP
// server. Functions here end up invoking handle_request.
#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/this_coro.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/parser.hpp>
#include <boost/beast/http/read.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/http/write.hpp>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <string_view>
#include "error.hpp"
#include "handle_request.hpp"
#include "server.hpp"
namespace asio = boost::asio;
namespace http = boost::beast::http;
namespace mysql = boost::mysql;
namespace {
// Runs a single HTTP session until the client closes the connection.
// This coroutine will be spawned on a strand, to prevent data races.
asio::awaitable<void> run_http_session(asio::ip::tcp::socket sock, mysql::connection_pool& pool)
{
using namespace std::chrono_literals;
boost::system::error_code ec;
// A buffer to read incoming client requests
boost::beast::flat_buffer buff;
// A timer, to use with asio::cancel_after to implement timeouts.
// Re-using the same timer multiple times with cancel_after
// is more efficient than using raw cancel_after,
// since the timer doesn't need to be re-created for every operation.
asio::steady_timer timer(co_await asio::this_coro::executor);
// A HTTP session might involve more than one message if
// keep-alive semantics are used. Loop until the connection closes.
while (true)
{
// Construct a new parser for each message
http::request_parser<http::string_body> parser;
// Apply a reasonable limit to the allowed size
// of the body in bytes to prevent abuse.
parser.body_limit(10000);
// Read a request. redirect_error prevents exceptions from being thrown
// on error. We use cancel_after to set a timeout for the overall read operation.
co_await http::async_read(
sock,
buff,
parser.get(),
asio::cancel_after(timer, 60s, asio::redirect_error(ec))
);
if (ec)
{
if (ec == http::error::end_of_stream)
{
// This means they closed the connection
sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
}
else
{
// An unknown error happened
orders::log_error("Error reading HTTP request: ", ec);
}
co_return;
}
const auto& request = parser.get();
// Process the request to generate a response.
// This invokes the business logic, which will need to access MySQL data.
// Apply a timeout to the overall request handling process.
auto response = co_await asio::co_spawn(
// Use the same executor as this coroutine (it will be a strand)
co_await asio::this_coro::executor,
// The logic to invoke
[&] { return orders::handle_request(request, pool); },
// Completion token. Returns an object that can be co_await'ed
asio::cancel_after(timer, 30s)
);
// Adjust the response, setting fields common to all responses
bool keep_alive = response.keep_alive();
response.version(request.version());
response.keep_alive(keep_alive);
response.prepare_payload();
// Send the response
co_await http::async_write(sock, response, asio::cancel_after(timer, 60s, asio::redirect_error(ec)));
if (ec)
{
orders::log_error("Error writing HTTP response: ", ec);
co_return;
}
// This means we should close the connection, usually because
// the response indicated the "Connection: close" semantic.
if (!keep_alive)
{
sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
co_return;
}
}
}
} // namespace
asio::awaitable<void> orders::run_server(mysql::connection_pool& pool, unsigned short port)
{
// An object that allows us to accept incoming TCP connections
asio::ip::tcp::acceptor acc(co_await asio::this_coro::executor);
// The endpoint where the server will listen. Edit this if you want to
// change the address or port we bind to.
asio::ip::tcp::endpoint listening_endpoint(asio::ip::make_address("0.0.0.0"), port);
// Open the acceptor
acc.open(listening_endpoint.protocol());
// Allow address reuse
acc.set_option(asio::socket_base::reuse_address(true));
// Bind to the server address
acc.bind(listening_endpoint);
// Start listening for connections
acc.listen(asio::socket_base::max_listen_connections);
std::cout << "Server listening at " << acc.local_endpoint() << std::endl;
// Start the acceptor loop
while (true)
{
// Accept a new connection
asio::ip::tcp::socket sock = co_await acc.async_accept();
// Function implementing our session logic.
// Takes ownership of the socket.
// Having this as a named variable workarounds a gcc bug
// (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107288)
auto session_logic = [&pool, socket = std::move(sock)]() mutable {
return run_http_session(std::move(socket), pool);
};
// Launch a new session for this connection. Each session gets its
// own coroutine, so we can get back to listening for new connections.
asio::co_spawn(
// Every session gets its own strand. This prevents data races.
asio::make_strand(co_await asio::this_coro::executor),
// The actual coroutine
std::move(session_logic),
// Callback to run when the coroutine finishes
[](std::exception_ptr ptr) {
if (ptr)
{
// For extra safety, log the exception but don't propagate it.
// If we failed to anticipate an error condition that ends up raising an exception,
// terminate only the affected session, instead of crashing the server.
try
{
std::rethrow_exception(ptr);
}
catch (const std::exception& exc)
{
auto guard = lock_cerr();
std::cerr << "Uncaught error in a session: " << exc.what() << std::endl;
}
}
}
);
}
}
//]
#endif
|