Made the asio::yield_context HTTP server single-threaded and reworked it

The example is now much more legible
The example no longer crashes on termination
Renamed it to match the C++ standard it requires

close #414
This commit is contained in:
Anarthal (Rubén Pérez) 2025-02-14 21:13:01 +01:00 committed by GitHub
parent b4365f3254
commit 07200f17c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 698 additions and 827 deletions

View File

@ -158,15 +158,14 @@ END
[import ../../example/3_advanced/http_server_cpp20/handle_request.cpp] [import ../../example/3_advanced/http_server_cpp20/handle_request.cpp]
[import ../../example/3_advanced/http_server_cpp20/server.hpp] [import ../../example/3_advanced/http_server_cpp20/server.hpp]
[import ../../example/3_advanced/http_server_cpp20/server.cpp] [import ../../example/3_advanced/http_server_cpp20/server.cpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/main.cpp] [import ../../example/3_advanced/http_server_cpp14_coroutines/main.cpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/types.hpp] [import ../../example/3_advanced/http_server_cpp14_coroutines/types.hpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/repository.hpp] [import ../../example/3_advanced/http_server_cpp14_coroutines/repository.hpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/repository.cpp] [import ../../example/3_advanced/http_server_cpp14_coroutines/repository.cpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/handle_request.hpp] [import ../../example/3_advanced/http_server_cpp14_coroutines/handle_request.hpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/handle_request.cpp] [import ../../example/3_advanced/http_server_cpp14_coroutines/handle_request.cpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/server.hpp] [import ../../example/3_advanced/http_server_cpp14_coroutines/server.hpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/server.cpp] [import ../../example/3_advanced/http_server_cpp14_coroutines/server.cpp]
[import ../../example/3_advanced/http_server_cpp11_coroutines/log_error.hpp]
[import ../../test/integration/test/snippets/prepared_statements.cpp] [import ../../test/integration/test/snippets/prepared_statements.cpp]
[import ../../test/integration/test/snippets/sql_formatting_advanced.cpp] [import ../../test/integration/test/snippets/sql_formatting_advanced.cpp]
[import ../../test/integration/test/snippets/connection_establishment.cpp] [import ../../test/integration/test/snippets/connection_establishment.cpp]

View File

@ -360,27 +360,25 @@ This example assumes you have gone through the [link mysql.examples.setup setup]
[section:http_server_cpp11_coroutines A REST API server that uses asio::yield_context] [section:http_server_cpp14_coroutines A C++14 REST API server that uses asio::yield_context]
This example assumes you have gone through the [link mysql.examples.setup setup]. This example assumes you have gone through the [link mysql.examples.setup setup].
[example_http_server_cpp11_coroutines_main_cpp] [example_http_server_cpp14_coroutines_main_cpp]
[example_http_server_cpp11_coroutines_types_hpp] [example_http_server_cpp14_coroutines_types_hpp]
[example_http_server_cpp11_coroutines_repository_hpp] [example_http_server_cpp14_coroutines_repository_hpp]
[example_http_server_cpp11_coroutines_repository_cpp] [example_http_server_cpp14_coroutines_repository_cpp]
[example_http_server_cpp11_coroutines_handle_request_hpp] [example_http_server_cpp14_coroutines_handle_request_hpp]
[example_http_server_cpp11_coroutines_handle_request_cpp] [example_http_server_cpp14_coroutines_handle_request_cpp]
[example_http_server_cpp11_coroutines_server_hpp] [example_http_server_cpp14_coroutines_server_hpp]
[example_http_server_cpp11_coroutines_server_cpp] [example_http_server_cpp14_coroutines_server_cpp]
[example_http_server_cpp11_coroutines_log_error_hpp]
[endsect] [endsect]

View File

@ -1,375 +0,0 @@
//
// 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/mysql/static_results.hpp>
#include "log_error.hpp"
#ifdef BOOST_MYSQL_CXX14
//[example_http_server_cpp11_coroutines_handle_request_cpp
//
// File: handle_request.cpp
//
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
#include <boost/optional/optional.hpp>
#include <boost/url/parse.hpp>
#include <boost/variant2/variant.hpp>
#include <chrono>
#include <cstdint>
#include <exception>
#include <string>
#include "handle_request.hpp"
#include "repository.hpp"
#include "types.hpp"
// This file contains all the boilerplate code to dispatch HTTP
// requests to API endpoints. Functions here end up calling
// note_repository functions.
namespace asio = boost::asio;
namespace http = boost::beast::http;
using boost::mysql::error_code;
using boost::mysql::string_view;
using namespace notes;
namespace {
// Attempts to parse a numeric ID from a string.
// If you're using C++17, you can use std::from_chars, instead
static boost::optional<std::int64_t> parse_id(const std::string& from)
{
try
{
std::size_t consumed = 0;
int res = std::stoi(from, &consumed);
if (consumed != from.size())
return {};
else if (res < 0)
return {};
return res;
}
catch (const std::exception&)
{
return {};
}
}
// Encapsulates the logic required to match a HTTP request
// to an API endpoint, call the relevant note_repository function,
// and return an HTTP response.
class request_handler
{
// The HTTP request we're handling. Requests are small in size,
// so we use http::request<http::string_body>
const http::request<http::string_body>& request_;
// The repository to access MySQL
note_repository repo_;
// Creates an error response
http::response<http::string_body> error_response(http::status code, string_view msg) const
{
http::response<http::string_body> res;
// Set the status code
res.result(code);
// Set the keep alive option
res.keep_alive(request_.keep_alive());
// Set the body
res.body() = msg;
// Adjust the content-length field
res.prepare_payload();
// Done
return res;
}
// Used when the request's Content-Type header doesn't match what we expect
http::response<http::string_body> invalid_content_type() const
{
return error_response(http::status::bad_request, "Invalid content-type");
}
// Used when the request body didn't match the format we expect
http::response<http::string_body> invalid_body() const
{
return error_response(http::status::bad_request, "Invalid body");
}
// Used when the request's method didn't match the ones allowed by the endpoint
http::response<http::string_body> method_not_allowed() const
{
return error_response(http::status::method_not_allowed, "Method not allowed");
}
// Used when the request target couldn't be matched to any API endpoint
http::response<http::string_body> endpoint_not_found() const
{
return error_response(http::status::not_found, "The requested resource was not found");
}
// Used when the user requested a note (e.g. using GET /note/<id> or PUT /note/<id>)
// but the note doesn't exist
http::response<http::string_body> note_not_found() const
{
return error_response(http::status::not_found, "The requested note was not found");
}
// Creates a response with a serialized JSON body.
// T should be a type with Boost.Describe metadata containing the
// body data to be serialized
template <class T>
http::response<http::string_body> json_response(const T& body) const
{
http::response<http::string_body> res;
// A JSON response is always a 200
res.result(http::status::ok);
// Set the content-type header
res.set("Content-Type", "application/json");
// Set the keep-alive option
res.keep_alive(request_.keep_alive());
// Serialize the body data into a string and use it as the response body.
// We use Boost.JSON's automatic serialization feature, which uses Boost.Describe
// reflection data to generate a serialization function for us.
res.body() = boost::json::serialize(boost::json::value_from(body));
// Adjust the content-length header
res.prepare_payload();
// Done
return res;
}
// Returns true if the request's Content-Type is set to JSON
bool has_json_content_type() const
{
auto it = request_.find("Content-Type");
return it != request_.end() && it->value() == "application/json";
}
// Attempts to parse the request body as a JSON into an object of type T.
// T should be a type with Boost.Describe metadata.
// We use boost::system::result, which may contain a result or an error.
template <class T>
boost::system::result<T> parse_json_request() const
{
error_code ec;
// Attempt to parse the request into a json::value.
// This will fail if the provided body isn't valid JSON.
auto val = boost::json::parse(request_.body(), ec);
if (ec)
return ec;
// Attempt to parse the json::value into a T. This will
// fail if the provided JSON doesn't match T's shape.
return boost::json::try_value_to<T>(val);
}
http::response<http::string_body> handle_request_impl(boost::asio::yield_context yield)
{
// Parse the request target. We use Boost.Url to do this.
auto url = boost::urls::parse_origin_form(request_.target());
if (url.has_error())
return error_response(http::status::bad_request, "Invalid request target");
// We will be iterating over the target's segments to determine
// which endpoint we are being requested
auto segs = url->segments();
auto segit = segs.begin();
auto seg = *segit++;
// All endpoints start with /notes
if (seg != "notes")
return endpoint_not_found();
if (segit == segs.end())
{
if (request_.method() == http::verb::get)
{
// GET /notes: retrieves all the notes.
// The request doesn't have a body.
// The response has a JSON body with multi_notes_response format
auto res = repo_.get_notes(yield);
return json_response(multi_notes_response{std::move(res)});
}
else if (request_.method() == http::verb::post)
{
// POST /notes: creates a note.
// The request has a JSON body with note_request_body format.
// The response has a JSON body with single_note_response format.
// Parse the request body
if (!has_json_content_type())
return invalid_content_type();
auto args = parse_json_request<note_request_body>();
if (args.has_error())
return invalid_body();
// Actually create the note
auto res = repo_.create_note(args->title, args->content, yield);
// Return the newly created note as response
return json_response(single_note_response{std::move(res)});
}
else
{
return method_not_allowed();
}
}
else
{
// The URL has the form /notes/<note-id>. Parse the note ID.
auto note_id = parse_id(*segit++);
if (!note_id.has_value())
{
return error_response(
http::status::bad_request,
"Invalid note_id specified in request target"
);
}
// /notes/<note-id>/<something-else> is not a valid endpoint
if (segit != segs.end())
return endpoint_not_found();
if (request_.method() == http::verb::get)
{
// GET /notes/<note-id>: retrieves a single note.
// The request doesn't have a body.
// The response has a JSON body with single_note_response format
// Get the note
auto res = repo_.get_note(*note_id, yield);
// If we didn't find it, return a 404 error
if (!res.has_value())
return note_not_found();
// Return it as response
return json_response(single_note_response{std::move(*res)});
}
else if (request_.method() == http::verb::put)
{
// PUT /notes/<note-id>: replaces a note.
// The request has a JSON body with note_request_body format.
// The response has a JSON body with single_note_response format.
// Parse the JSON body
if (!has_json_content_type())
return invalid_content_type();
auto args = parse_json_request<note_request_body>();
if (args.has_error())
return invalid_body();
// Perform the update
auto res = repo_.replace_note(*note_id, args->title, args->content, yield);
// Check that it took effect. Otherwise, it's because the note wasn't there
if (!res.has_value())
return note_not_found();
// Return the updated note as response
return json_response(single_note_response{std::move(*res)});
}
else if (request_.method() == http::verb::delete_)
{
// DELETE /notes/<note-id>: deletes a note.
// The request doesn't have a body.
// The response has a JSON body with delete_note_response format.
// Attempt to delete the note
bool deleted = repo_.delete_note(*note_id, yield);
// Return whether the delete was successful in the response.
// We don't fail DELETEs for notes that don't exist.
return json_response(delete_note_response{deleted});
}
else
{
return method_not_allowed();
}
}
}
public:
// Constructor
request_handler(const http::request<http::string_body>& req, note_repository repo)
: request_(req), repo_(repo)
{
}
// Generates a response for the request passed to the constructor
http::response<http::string_body> handle_request(boost::asio::yield_context yield)
{
try
{
// Attempt to handle the request. We use cancel_after to set
// a timeout to the overall operation
return asio::spawn(
yield.get_executor(),
[this](asio::yield_context yield2) { return handle_request_impl(yield2); },
asio::cancel_after(std::chrono::seconds(30), yield)
);
}
catch (const boost::mysql::error_with_diagnostics& err)
{
// A Boost.MySQL error. This will happen if you don't have connectivity
// to your database, your schema is incorrect or your credentials are invalid.
// Log the error, including diagnostics, and return a generic 500
log_error(
"Uncaught exception: ",
err.what(),
"\nServer diagnostics: ",
err.get_diagnostics().server_message()
);
return error_response(http::status::internal_server_error, "Internal error");
}
catch (const std::exception& err)
{
// Another kind of error. This indicates a programming error or a severe
// server condition (e.g. out of memory). Same procedure as above.
log_error("Uncaught exception: ", err.what());
return error_response(http::status::internal_server_error, "Internal error");
}
}
};
} // namespace
// External interface
boost::beast::http::response<boost::beast::http::string_body> notes::handle_request(
const boost::beast::http::request<boost::beast::http::string_body>& request,
note_repository repo,
boost::asio::yield_context yield
)
{
return request_handler(request, repo).handle_request(yield);
}
//]
#endif

View File

@ -1,53 +0,0 @@
//
// 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)
//
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_LOG_ERROR_HPP
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_LOG_ERROR_HPP
//[example_http_server_cpp11_coroutines_log_error_hpp
//
// File: log_error.hpp
//
#include <iostream>
#include <mutex>
// Helper function to safely write diagnostics to std::cerr.
// Since we're in a multi-threaded environment, directly writing to std::cerr
// can lead to interleaved output, so we should synchronize calls with a mutex.
// This function is only called in rare cases (e.g. unhandled exceptions),
// so we can afford the synchronization overhead.
namespace notes {
// If you're in C++17+, you can write this using fold expressions
// instead of recursion.
inline void log_error_impl() {}
template <class Arg1, class... Tail>
void log_error_impl(const Arg1& arg, const Tail&... tail)
{
std::cerr << arg;
log_error_impl(tail...);
}
template <class... Args>
void log_error(const Args&... args)
{
static std::mutex mtx;
// Acquire the mutex, then write the passed arguments to std::cerr.
std::unique_lock<std::mutex> lock(mtx);
log_error_impl(args...);
std::cerr << std::endl;
}
} // namespace notes
//]
#endif

View File

@ -1,206 +0,0 @@
//
// 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/mysql/static_results.hpp>
#include "log_error.hpp"
#ifdef BOOST_MYSQL_CXX14
//[example_http_server_cpp11_coroutines_server_cpp
//
// File: server.cpp
//
#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/strand.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/write.hpp>
#include <cstddef>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>
#include <string>
#include "handle_request.hpp"
#include "repository.hpp"
#include "server.hpp"
#include "types.hpp"
// This file contains all the boilerplate code to implement a HTTP
// server. Functions here end up invoking handle_request.
namespace asio = boost::asio;
namespace http = boost::beast::http;
using boost::mysql::error_code;
using namespace notes;
namespace {
static void run_http_session(
boost::asio::ip::tcp::socket sock,
std::shared_ptr<shared_state> st,
boost::asio::yield_context yield
)
{
error_code ec;
// A buffer to read incoming client requests
boost::beast::flat_buffer buff;
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
http::async_read(sock, buff, parser.get(), yield[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
log_error("Error reading HTTP request: ", ec);
}
return;
}
// Process the request to generate a response.
// This invokes the business logic, which will need to access MySQL data
auto response = handle_request(parser.get(), note_repository(st->pool), yield);
// Determine if we should close the connection
bool keep_alive = response.keep_alive();
// Send the response
http::async_write(sock, response, yield[ec]);
if (ec)
return log_error("Error writing HTTP response: ", ec);
// 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);
return;
}
}
}
// Implements the server's accept loop. The server will
// listen for connections until stopped.
static void do_accept(
asio::any_io_executor executor, // The original executor (without strands)
std::shared_ptr<asio::ip::tcp::acceptor> acceptor,
std::shared_ptr<shared_state> st
)
{
acceptor->async_accept([executor, st, acceptor](error_code ec, asio::ip::tcp::socket sock) {
// If there was an error accepting the connection, exit our loop
if (ec)
return log_error("Error while accepting connection", ec);
// Launch a new session for this connection. Each session gets its
// own stackful coroutine, so we can get back to listening for new connections.
boost::asio::spawn(
// Every session gets its own strand. This prevents data races.
asio::make_strand(executor),
// The actual coroutine
[st, socket = std::move(sock)](boost::asio::yield_context yield) mutable {
run_http_session(std::move(socket), std::move(st), yield);
},
// All errors in the session are handled via error codes or by catching
// exceptions explicitly. An unhandled exception here means an error.
// Rethrowing it will propagate the exception, making io_context::run()
// to throw and terminate the program.
[](std::exception_ptr ex) {
if (ex)
std::rethrow_exception(ex);
}
);
// Accept a new connection
do_accept(executor, acceptor, st);
});
}
} // namespace
error_code notes::launch_server(
boost::asio::any_io_executor ex,
std::shared_ptr<shared_state> st,
unsigned short port
)
{
error_code ec;
// An object that allows us to accept incoming TCP connections.
// Since we're in a multi-threaded environment, we create a strand for the acceptor,
// so all accept handlers are run serialized
auto acceptor = std::make_shared<asio::ip::tcp::acceptor>(asio::make_strand(ex));
// The endpoint where the server will listen. Edit this if you want to
// change the address or port we bind to.
boost::asio::ip::tcp::endpoint listening_endpoint(boost::asio::ip::make_address("0.0.0.0"), port);
// Open the acceptor
acceptor->open(listening_endpoint.protocol(), ec);
if (ec)
return ec;
// Allow address reuse
acceptor->set_option(asio::socket_base::reuse_address(true), ec);
if (ec)
return ec;
// Bind to the server address
acceptor->bind(listening_endpoint, ec);
if (ec)
return ec;
// Start listening for connections
acceptor->listen(asio::socket_base::max_listen_connections, ec);
if (ec)
return ec;
std::cout << "Server listening at " << acceptor->local_endpoint() << std::endl;
// Launch the acceptor loop
do_accept(std::move(ex), std::move(acceptor), std::move(st));
// Done
return error_code();
}
//]
#endif

View File

@ -0,0 +1,341 @@
//
// 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/mysql/static_results.hpp>
#ifdef BOOST_MYSQL_CXX14
//[example_http_server_cpp14_coroutines_handle_request_cpp
//
// File: handle_request.cpp
//
// This file contains all the boilerplate code to dispatch HTTP
// requests to API endpoints. Functions here end up calling
// note_repository functions.
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/charconv/from_chars.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
#include <boost/optional/optional.hpp>
#include <boost/url/parse.hpp>
#include <cstdint>
#include <exception>
#include <iostream>
#include <string>
#include "handle_request.hpp"
#include "repository.hpp"
#include "types.hpp"
namespace asio = boost::asio;
namespace mysql = boost::mysql;
namespace http = boost::beast::http;
using namespace notes;
namespace {
// Helper function that logs errors thrown by db_repository
// when an unexpected database error happens
void log_mysql_error(boost::system::error_code ec, const mysql::diagnostics& diag)
{
// Inserting the error code only prints the number and category. Add the message, too.
std::cerr << "MySQL error: " << ec << " " << ec.message();
// client_message() contains client-side generated messages that don't
// contain user-input. This is usually embedded in exceptions.
// When working with error codes, we need to log it explicitly
if (!diag.client_message().empty())
{
std::cerr << ": " << diag.client_message();
}
// server_message() contains server-side messages, and thus may
// contain user-supplied input. Printing it is safe.
if (!diag.server_message().empty())
{
std::cerr << ": " << diag.server_message();
}
// Done
std::cerr << std::endl;
}
// Attempts to parse a numeric ID from a string.
// If you're using C++17, you can use std::from_chars, instead
boost::optional<std::int64_t> parse_id(const std::string& from)
{
std::int64_t id{};
auto res = boost::charconv::from_chars(from.data(), from.data() + from.size(), id);
if (res.ec != std::errc{} || res.ptr != from.data() + from.size())
return {};
return id;
}
// Helpers to create error responses with a single line of code
http::response<http::string_body> error_response(http::status code, const char* msg)
{
http::response<http::string_body> res;
res.result(code);
res.body() = msg;
return res;
}
// Like error_response, but always uses a 400 status code
http::response<http::string_body> bad_request(const char* body)
{
return error_response(http::status::bad_request, body);
}
// Like error_response, but always uses a 500 status code and
// never provides extra information that might help potential attackers.
http::response<http::string_body> internal_server_error()
{
return error_response(http::status::internal_server_error, "Internal server error");
}
// Creates a response with a serialized JSON body.
// T should be a type with Boost.Describe metadata containing the
// body data to be serialized
template <class T>
http::response<http::string_body> json_response(const T& body)
{
http::response<http::string_body> res;
// Set the content-type header
res.set("Content-Type", "application/json");
// Serialize the body data into a string and use it as the response body.
// We use Boost.JSON's automatic serialization feature, which uses Boost.Describe
// reflection data to generate a serialization function for us.
res.body() = boost::json::serialize(boost::json::value_from(body));
// Done
return res;
}
// Returns true if the request's Content-Type is set to JSON
bool has_json_content_type(const http::request<http::string_body>& req)
{
auto it = req.find("Content-Type");
return it != req.end() && it->value() == "application/json";
}
// Attempts to parse a string as a JSON into an object of type T.
// T should be a type with Boost.Describe metadata.
// We use boost::system::result, which may contain a result or an error.
template <class T>
boost::system::result<T> parse_json(boost::mysql::string_view json_string)
{
// Attempt to parse the request into a json::value.
// This will fail if the provided body isn't valid JSON.
boost::system::error_code ec;
auto val = boost::json::parse(json_string, ec);
if (ec)
return ec;
// Attempt to parse the json::value into a T. This will
// fail if the provided JSON doesn't match T's shape.
return boost::json::try_value_to<T>(val);
}
// Contains data associated to an HTTP request.
// To be passed to individual handler functions
struct request_data
{
// The incoming request
const http::request<http::string_body>& request;
// The URL the request is targeting
boost::urls::url_view target;
// Connection pool
mysql::connection_pool& pool;
note_repository repo() const { return note_repository(pool); }
};
//
// Endpoint handlers. We have a function per method.
// All of our endpoints have /notes as the URL path.
//
// GET /notes: retrieves all the notes.
// The request doesn't have a body.
// The response has a JSON body with multi_notes_response format
//
// GET /notes?id=<note-id>: retrieves a single note.
// The request doesn't have a body.
// The response has a JSON body with single_note_response format
//
// Both endpoints share path and method, so they share handler function
http::response<http::string_body> handle_get(const request_data& input, asio::yield_context yield)
{
// Parse the query parameter
auto params_it = input.target.params().find("id");
// Did the client specify an ID?
if (params_it == input.target.params().end())
{
auto res = input.repo().get_notes(yield);
return json_response(multi_notes_response{std::move(res)});
}
else
{
// Parse id
auto id = parse_id((*params_it).value);
if (!id.has_value())
return bad_request("URL parameter 'id' should be a valid integer");
// Get the note
auto res = input.repo().get_note(*id, yield);
// If we didn't find it, return a 404 error
if (!res.has_value())
return error_response(http::status::not_found, "The requested note was not found");
// Return it as response
return json_response(single_note_response{std::move(*res)});
}
}
// POST /notes: creates a note.
// The request has a JSON body with note_request_body format.
// The response has a JSON body with single_note_response format.
http::response<http::string_body> handle_post(const request_data& input, asio::yield_context yield)
{
// Parse the request body
if (!has_json_content_type(input.request))
return bad_request("Invalid Content-Type: expected 'application/json'");
auto args = parse_json<note_request_body>(input.request.body());
if (args.has_error())
return bad_request("Invalid JSON");
// Actually create the note
auto res = input.repo().create_note(args->title, args->content, yield);
// Return the newly created note as response
return json_response(single_note_response{std::move(res)});
}
// PUT /notes?id=<note-id>: replaces a note.
// The request has a JSON body with note_request_body format.
// The response has a JSON body with single_note_response format.
http::response<http::string_body> handle_put(const request_data& input, asio::yield_context yield)
{
// Parse the query parameter
auto params_it = input.target.params().find("id");
if (params_it == input.target.params().end())
return bad_request("Mandatory URL parameter 'id' not found");
auto id = parse_id((*params_it).value);
if (!id.has_value())
return bad_request("URL parameter 'id' should be a valid integer");
// Parse the request body
if (!has_json_content_type(input.request))
return bad_request("Invalid Content-Type: expected 'application/json'");
auto args = parse_json<note_request_body>(input.request.body());
if (args.has_error())
return bad_request("Invalid JSON");
// Perform the update
auto res = input.repo().replace_note(*id, args->title, args->content, yield);
// Check that it took effect. Otherwise, it's because the note wasn't there
if (!res.has_value())
return bad_request("The requested note was not found");
// Return the updated note as response
return json_response(single_note_response{std::move(*res)});
}
// DELETE /notes/<note-id>: deletes a note.
// The request doesn't have a body.
// The response has a JSON body with delete_note_response format.
http::response<http::string_body> handle_delete(const request_data& input, asio::yield_context yield)
{
// Parse the query parameter
auto params_it = input.target.params().find("id");
if (params_it == input.target.params().end())
return bad_request("Mandatory URL parameter 'id' not found");
auto id = parse_id((*params_it).value);
if (!id.has_value())
return bad_request("URL parameter 'id' should be a valid integer");
// Attempt to delete the note
bool deleted = input.repo().delete_note(*id, yield);
// Return whether the delete was successful in the response.
// We don't fail DELETEs for notes that don't exist.
return json_response(delete_note_response{deleted});
}
} // namespace
// External interface
http::response<http::string_body> notes::handle_request(
mysql::connection_pool& pool,
const http::request<http::string_body>& request,
asio::yield_context yield
)
{
// Parse the request target
auto target = boost::urls::parse_origin_form(request.target());
if (!target.has_value())
return bad_request("Invalid request target");
// All our endpoints have /notes as path, with different verbs and parameters.
// Verify that the path matches
if (target->path() != "/notes")
return error_response(http::status::not_found, "Endpoint not found");
// Compose the request_data object
request_data input{request, *target, pool};
// Invoke the relevant handler, depending on the method
try
{
switch (input.request.method())
{
case http::verb::get: return handle_get(input, yield);
case http::verb::post: return handle_post(input, yield);
case http::verb::put: return handle_put(input, yield);
case http::verb::delete_: return handle_delete(input, yield);
default: return error_response(http::status::method_not_allowed, "Method not allowed for /notes");
}
}
catch (const mysql::error_with_diagnostics& err)
{
// A Boost.MySQL error. This will happen if you don't have connectivity
// to your database, your schema is incorrect or your credentials are invalid.
// Log the error, including diagnostics
log_mysql_error(err.code(), err.get_diagnostics());
// Never disclose error info to a potential attacker
return internal_server_error();
}
catch (const std::exception& err)
{
// Another kind of error. This indicates a programming error or a severe
// server condition (e.g. out of memory). Same procedure as above.
std::cerr << "Uncaught exception: " << err.what() << std::endl;
return internal_server_error();
}
}
//]
#endif

View File

@ -5,27 +5,28 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
// //
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_HANDLE_REQUEST_HPP #ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_HANDLE_REQUEST_HPP
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_HANDLE_REQUEST_HPP #define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_HANDLE_REQUEST_HPP
//[example_http_server_cpp11_coroutines_handle_request_hpp //[example_http_server_cpp14_coroutines_handle_request_hpp
// //
// File: handle_request.hpp // File: handle_request.hpp
// //
#include <boost/asio/error.hpp> #include <boost/mysql/connection_pool.hpp>
#include <boost/asio/spawn.hpp> #include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp> #include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>
#include "repository.hpp"
namespace notes { namespace notes {
// Handles an individual HTTP request, producing a response. // Handles an individual HTTP request, producing a response.
// The caller of this function should use response::version,
// response::keep_alive and response::prepare_payload to adjust the response.
boost::beast::http::response<boost::beast::http::string_body> handle_request( boost::beast::http::response<boost::beast::http::string_body> handle_request(
boost::mysql::connection_pool& pool,
const boost::beast::http::request<boost::beast::http::string_body>& request, const boost::beast::http::request<boost::beast::http::string_body>& request,
note_repository repo,
boost::asio::yield_context yield boost::asio::yield_context yield
); );

View File

@ -6,33 +6,31 @@
// //
#include <boost/mysql/static_results.hpp> #include <boost/mysql/static_results.hpp>
#include "log_error.hpp"
#ifdef BOOST_MYSQL_CXX14 #ifdef BOOST_MYSQL_CXX14
//[example_http_server_cpp11_coroutines_main_cpp //[example_http_server_cpp14_coroutines_main_cpp
/** /**
* Implements a HTTP REST API using Boost.MySQL and Boost.Beast. * Implements a HTTP REST API using Boost.MySQL and Boost.Beast.
* The server is asynchronous and uses asio::yield_context as its completion * The server is asynchronous and uses asio::yield_context as its completion
* style. It only requires C++11 to work. * style. It only requires C++14 to work.
* *
* It implements a minimal REST API to manage notes. * It implements a minimal REST API to manage notes.
* A note is a simple object containing a user-defined title and content. * A note is a simple object containing a user-defined title and content.
* The REST API offers CRUD operations on such objects: * The REST API offers CRUD operations on such objects:
* POST /notes Creates a new note. * POST /notes Creates a new note.
* GET /notes Retrieves all notes. * GET /notes Retrieves all notes.
* GET /notes/<id> Retrieves a single note. * GET /notes?id=<id> Retrieves a single note.
* PUT /notes/<id> Replaces a note, changing its title and content. * PUT /notes?id=<id> Replaces a note, changing its title and content.
* DELETE /notes/<id> Deletes a note. * DELETE /notes?id=<id> Deletes a note.
* *
* Notes are stored in MySQL. The note_repository class encapsulates * Notes are stored in MySQL. The note_repository class encapsulates
* access to MySQL, offering friendly functions to manipulate notes. * access to MySQL, offering friendly functions to manipulate notes.
* server.cpp encapsulates all the boilerplate to launch an HTTP server, * server.cpp encapsulates all the boilerplate to launch an HTTP server,
* match URLs to API endpoints, and invoke the relevant note_repository functions. * match URLs to API endpoints, and invoke the relevant note_repository functions.
*
* All communication happens asynchronously. We use stackful coroutines to simplify * All communication happens asynchronously. We use stackful coroutines to simplify
* development, using boost::asio::spawn and boost::asio::yield_context. * development, using asio::spawn and asio::yield_context.
* This example requires linking to Boost::context, Boost::json and Boost::url. * This example requires linking to Boost::context, Boost::json and Boost::url.
*/ */
@ -41,11 +39,12 @@
#include <boost/mysql/pool_params.hpp> #include <boost/mysql/pool_params.hpp>
#include <boost/asio/detached.hpp> #include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/signal_set.hpp> #include <boost/asio/signal_set.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/thread_pool.hpp> #include <boost/asio/thread_pool.hpp>
#include <boost/system/error_code.hpp> #include <boost/system/error_code.hpp>
#include <cstddef>
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
@ -53,11 +52,10 @@
#include "server.hpp" #include "server.hpp"
namespace asio = boost::asio;
namespace mysql = boost::mysql;
using namespace notes; using namespace notes;
// The number of threads to use
static constexpr std::size_t num_threads = 5;
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
// Check command line arguments. // Check command line arguments.
@ -74,14 +72,12 @@ int main(int argc, char* argv[])
auto port = static_cast<unsigned short>(std::stoi(argv[4])); auto port = static_cast<unsigned short>(std::stoi(argv[4]));
// An event loop, where the application will run. // An event loop, where the application will run.
// We will use the main thread to run the pool, too, so we use asio::io_context ctx;
// one thread less than configured
boost::asio::thread_pool th_pool(num_threads - 1);
// Configuration for the connection pool // Configuration for the connection pool
boost::mysql::pool_params pool_prms{ mysql::pool_params params{
// Connect using TCP, to the given hostname and using the default port // Connect using TCP, to the given hostname and using the default port
boost::mysql::host_and_port{mysql_hostname}, mysql::host_and_port{mysql_hostname},
// Authenticate using the given username // Authenticate using the given username
mysql_username, mysql_username,
@ -93,47 +89,40 @@ int main(int argc, char* argv[])
"boost_mysql_examples", "boost_mysql_examples",
}; };
// Using thread_safe will make the pool thread-safe by internally // Create the connection pool.
// creating and using a strand. // shared_state contains all singleton objects that our application may need.
// This allows us to share the pool between sessions, which may run // Coroutines created by asio::spawn might survive until the io_context is destroyed
// concurrently, on different threads. // (even after io_context::stop() has been called). This is not the case for callbacks
pool_prms.thread_safe = true; // and C++20 coroutines. Using a shared_ptr here ensures that the pool survives long enough.
auto st = std::make_shared<shared_state>(mysql::connection_pool(ctx, std::move(params)));
// Create the connection pool
auto shared_st = std::make_shared<shared_state>(
boost::mysql::connection_pool(th_pool, std::move(pool_prms))
);
// A signal_set allows us to intercept SIGINT and SIGTERM and
// exit gracefully
boost::asio::signal_set signals{th_pool.get_executor(), SIGINT, SIGTERM};
// Launch the MySQL pool // Launch the MySQL pool
shared_st->pool.async_run(boost::asio::detached); st->pool.async_run(asio::detached);
// Start listening for HTTP connections. This will run until the context is stopped
auto ec = launch_server(th_pool.get_executor(), shared_st, port);
if (ec)
{
log_error("Error launching server: ", ec);
exit(EXIT_FAILURE);
}
// Capture SIGINT and SIGTERM to perform a clean shutdown
signals.async_wait([shared_st, &th_pool](boost::system::error_code, int) {
// Cancel the pool. This will cause async_run to complete.
shared_st->pool.cancel();
// A signal_set allows us to intercept SIGINT and SIGTERM and exit gracefully
asio::signal_set signals{ctx.get_executor(), SIGINT, SIGTERM};
signals.async_wait([st, &ctx](boost::system::error_code, int) {
// Stop the execution context. This will cause main to exit // Stop the execution context. This will cause main to exit
th_pool.stop(); ctx.stop();
}); });
// Attach the current thread to the thread pool. This will block // Launch the server. This will run until the context is stopped
// until stop() is called asio::spawn(
th_pool.attach(); // Spawn the coroutine in the io_context
ctx,
// Wait until all threads have exited // The coroutine to run
th_pool.join(); [st, port](asio::yield_context yield) { run_server(st, port, yield); },
// If an exception is thrown in the coroutine, propagate it
[](std::exception_ptr exc) {
if (exc)
std::rethrow_exception(exc);
}
);
// Run the server until stopped
ctx.run();
std::cout << "Server exiting" << std::endl; std::cout << "Server exiting" << std::endl;

View File

@ -6,15 +6,21 @@
// //
#include <boost/mysql/static_results.hpp> #include <boost/mysql/static_results.hpp>
#ifdef BOOST_MYSQL_CXX14 #ifdef BOOST_MYSQL_CXX14
//[example_http_server_cpp11_coroutines_repository_cpp //[example_http_server_cpp14_coroutines_repository_cpp
// //
// File: repository.cpp // File: repository.cpp
// //
// SQL code to create the notes table is located under $REPO_ROOT/example/db_setup.sql
// The table looks like this:
//
// CREATE TABLE notes(
// id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
// title TEXT NOT NULL,
// content TEXT NOT NULL
// );
#include <boost/mysql/statement.hpp>
#include <boost/mysql/static_results.hpp> #include <boost/mysql/static_results.hpp>
#include <boost/mysql/string_view.hpp> #include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_diagnostics.hpp> #include <boost/mysql/with_diagnostics.hpp>
@ -27,20 +33,12 @@
#include "repository.hpp" #include "repository.hpp"
#include "types.hpp" #include "types.hpp"
using namespace notes; namespace asio = boost::asio;
namespace mysql = boost::mysql; namespace mysql = boost::mysql;
using namespace notes;
using mysql::with_diagnostics; using mysql::with_diagnostics;
// SQL code to create the notes table is located under $REPO_ROOT/example/db_setup.sql std::vector<note_t> note_repository::get_notes(asio::yield_context yield)
// The table looks like this:
//
// CREATE TABLE notes(
// id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
// title TEXT NOT NULL,
// content TEXT NOT NULL
// );
std::vector<note_t> note_repository::get_notes(boost::asio::yield_context yield)
{ {
// Get a fresh connection from the pool. This returns a pooled_connection object, // Get a fresh connection from the pool. This returns a pooled_connection object,
// which is a proxy to an any_connection object. Connections are returned to the // which is a proxy to an any_connection object. Connections are returned to the
@ -72,7 +70,7 @@ std::vector<note_t> note_repository::get_notes(boost::asio::yield_context yield)
// return the connection automatically to the pool. // return the connection automatically to the pool.
} }
optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yield_context yield) boost::optional<note_t> note_repository::get_note(std::int64_t note_id, asio::yield_context yield)
{ {
// Get a fresh connection from the pool. This returns a pooled_connection object, // Get a fresh connection from the pool. This returns a pooled_connection object,
// which is a proxy to an any_connection object. Connections are returned to the // which is a proxy to an any_connection object. Connections are returned to the
@ -98,7 +96,11 @@ optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yi
return std::move(result.rows()[0]); return std::move(result.rows()[0]);
} }
note_t note_repository::create_note(string_view title, string_view content, boost::asio::yield_context yield) note_t note_repository::create_note(
mysql::string_view title,
mysql::string_view content,
asio::yield_context yield
)
{ {
// Get a fresh connection from the pool. This returns a pooled_connection object, // Get a fresh connection from the pool. This returns a pooled_connection object,
// which is a proxy to an any_connection object. Connections are returned to the // which is a proxy to an any_connection object. Connections are returned to the
@ -129,11 +131,11 @@ note_t note_repository::create_note(string_view title, string_view content, boos
// pooled_connection's destructor takes care of it. // pooled_connection's destructor takes care of it.
} }
optional<note_t> note_repository::replace_note( boost::optional<note_t> note_repository::replace_note(
std::int64_t note_id, std::int64_t note_id,
string_view title, mysql::string_view title,
string_view content, mysql::string_view content,
boost::asio::yield_context yield asio::yield_context yield
) )
{ {
// Get a fresh connection from the pool. This returns a pooled_connection object, // Get a fresh connection from the pool. This returns a pooled_connection object,
@ -165,7 +167,7 @@ optional<note_t> note_repository::replace_note(
return note_t{note_id, title, content}; return note_t{note_id, title, content};
} }
bool note_repository::delete_note(std::int64_t note_id, boost::asio::yield_context yield) bool note_repository::delete_note(std::int64_t note_id, asio::yield_context yield)
{ {
// Get a fresh connection from the pool. This returns a pooled_connection object, // Get a fresh connection from the pool. This returns a pooled_connection object,
// which is a proxy to an any_connection object. Connections are returned to the // which is a proxy to an any_connection object. Connections are returned to the

View File

@ -5,10 +5,10 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
// //
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_REPOSITORY_HPP #ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_REPOSITORY_HPP
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_REPOSITORY_HPP #define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_REPOSITORY_HPP
//[example_http_server_cpp11_coroutines_repository_hpp //[example_http_server_cpp14_coroutines_repository_hpp
// //
// File: repository.hpp // File: repository.hpp
// //
@ -16,7 +16,6 @@
#include <boost/mysql/connection_pool.hpp> #include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/string_view.hpp> #include <boost/mysql/string_view.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp> #include <boost/asio/spawn.hpp>
#include <boost/optional/optional.hpp> #include <boost/optional/optional.hpp>
@ -26,41 +25,40 @@
namespace notes { namespace notes {
using boost::optional; // Encapsulates database logic.
using boost::mysql::string_view; // All operations are async, and use stackful coroutines (asio::yield_context).
// A lightweight wrapper around a connection_pool that allows
// creating, updating, retrieving and deleting notes in MySQL.
// This class encapsulates the database logic.
// All operations are async, and use stackful coroutines (boost::asio::yield_context).
// If the database can't be contacted, or unexpected database errors are found, // If the database can't be contacted, or unexpected database errors are found,
// an exception of type boost::mysql::error_with_diagnostics is thrown. // an exception of type mysql::error_with_diagnostics is thrown.
class note_repository class note_repository
{ {
boost::mysql::connection_pool& pool_; boost::mysql::connection_pool& pool_;
public: public:
// Constructor (this is a cheap-to-construct object) // Constructor (this is a cheap-to-construct object)
note_repository(boost::mysql::connection_pool& pool) : pool_(pool) {} note_repository(boost::mysql::connection_pool& pool) noexcept : pool_(pool) {}
// Retrieves all notes present in the database // Retrieves all notes present in the database
std::vector<note_t> get_notes(boost::asio::yield_context yield); std::vector<note_t> get_notes(boost::asio::yield_context yield);
// Retrieves a single note by ID. Returns an empty optional // Retrieves a single note by ID. Returns an empty optional
// if no note with the given ID is present in the database. // if no note with the given ID is present in the database.
optional<note_t> get_note(std::int64_t note_id, boost::asio::yield_context yield); boost::optional<note_t> get_note(std::int64_t note_id, boost::asio::yield_context yield);
// Creates a new note in the database with the given components. // Creates a new note in the database with the given components.
// Returns the newly created note, including the newly allocated ID. // Returns the newly created note, including the newly allocated ID.
note_t create_note(string_view title, string_view content, boost::asio::yield_context yield); note_t create_note(
boost::mysql::string_view title,
boost::mysql::string_view content,
boost::asio::yield_context yield
);
// Replaces the note identified by note_id, setting its components to the // Replaces the note identified by note_id, setting its components to the
// ones passed. Returns the updated note. If no note with ID matching // ones passed. Returns the updated note. If no note with ID matching
// note_id can be found, an empty optional is returned. // note_id can be found, an empty optional is returned.
optional<note_t> replace_note( boost::optional<note_t> replace_note(
std::int64_t note_id, std::int64_t note_id,
string_view title, boost::mysql::string_view title,
string_view content, boost::mysql::string_view content,
boost::asio::yield_context yield boost::asio::yield_context yield
); );

View File

@ -0,0 +1,196 @@
//
// 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/mysql/static_results.hpp>
#ifdef BOOST_MYSQL_CXX14
//[example_http_server_cpp14_coroutines_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/asio/cancel_after.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.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/write.hpp>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>
#include "handle_request.hpp"
#include "server.hpp"
namespace asio = boost::asio;
namespace http = boost::beast::http;
using namespace notes;
namespace {
// Runs a single HTTP session until the client closes the connection
void run_http_session(std::shared_ptr<shared_state> st, asio::ip::tcp::socket sock, asio::yield_context yield)
{
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(yield.get_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. yield[ec] prevents exceptions from being thrown
// on error. We use cancel_after to set a timeout for the overall read operation.
http::async_read(sock, buff, parser.get(), asio::cancel_after(60s, yield[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
std::cout << "Error reading HTTP request: " << ec.message() << std::endl;
}
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 = asio::spawn(
// Use the same executor as this coroutine
yield.get_executor(),
// The logic to invoke
[&](asio::yield_context yield2) { return handle_request(st->pool, request, yield2); },
// Completion token. Passing yield blocks the current coroutine
// until handle_request completes.
asio::cancel_after(timer, 30s, yield)
);
// 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
http::async_write(sock, response, asio::cancel_after(60s, yield[ec]));
if (ec)
{
std::cout << "Error writing HTTP response: " << ec.message() << std::endl;
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);
return;
}
}
}
} // namespace
void notes::run_server(std::shared_ptr<shared_state> st, unsigned short port, asio::yield_context yield)
{
// An object that allows us to accept incoming TCP connections
asio::ip::tcp::acceptor acc(yield.get_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 = acc.async_accept(yield);
// Launch a new session for this connection. Each session gets its
// own coroutine, so we can get back to listening for new connections.
asio::spawn(
yield.get_executor(),
// Function implementing our session logic.
// Takes ownership of the socket.
[st, sock = std::move(sock)](asio::yield_context yield2) mutable {
return run_http_session(std::move(st), std::move(sock), yield2);
},
// 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)
{
std::cerr << "Uncaught error in a session: " << exc.what() << std::endl;
}
}
}
);
}
}
//]
#endif

View File

@ -5,19 +5,17 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
// //
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_SERVER_HPP #ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_SERVER_HPP
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_SERVER_HPP #define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_SERVER_HPP
//[example_http_server_cpp11_coroutines_server_hpp //[example_http_server_cpp14_coroutines_server_hpp
// //
// File: server.hpp // File: server.hpp
// //
#include <boost/mysql/connection_pool.hpp> #include <boost/mysql/connection_pool.hpp>
#include <boost/asio/any_io_executor.hpp> #include <boost/asio/spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/system/error_code.hpp>
#include <memory> #include <memory>
@ -32,19 +30,14 @@ struct shared_state
{ {
boost::mysql::connection_pool pool; boost::mysql::connection_pool pool;
shared_state(boost::mysql::connection_pool pool) : pool(std::move(pool)) {} shared_state(boost::mysql::connection_pool pool) noexcept : pool(std::move(pool)) {}
}; };
// Launches a HTTP server that will listen on 0.0.0.0:port. // Runs a HTTP server that will listen on 0.0.0.0:port.
// If the server fails to launch (e.g. because the port is already in use), // If the server fails to launch (e.g. because the port is already in use),
// returns a non-zero error code. ex should identify the io_context or thread_pool // throws an exception. The server runs until the underlying execution
// where the server should run. The server is run until the underlying execution
// context is stopped. // context is stopped.
boost::system::error_code launch_server( void run_server(std::shared_ptr<shared_state> st, unsigned short port, boost::asio::yield_context yield);
boost::asio::any_io_executor ex,
std::shared_ptr<shared_state> state,
unsigned short port
);
} // namespace notes } // namespace notes

View File

@ -5,27 +5,25 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
// //
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_TYPES_HPP #ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_TYPES_HPP
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_TYPES_HPP #define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_TYPES_HPP
//[example_http_server_cpp11_coroutines_types_hpp //[example_http_server_cpp14_coroutines_types_hpp
// //
// File: types.hpp // File: types.hpp
// //
#include <boost/core/span.hpp>
#include <boost/describe/class.hpp>
#include <boost/optional/optional.hpp>
#include <string>
#include <vector>
// Contains type definitions used in the REST API and database code. // Contains type definitions used in the REST API and database code.
// We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection // We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection
// capabilities to our types. This allows using Boost.MySQL // capabilities to our types. This allows using Boost.MySQL
// static interface (i.e. static_results<T>) to parse query results, // static interface (i.e. static_results<T>) to parse query results,
// and Boost.JSON automatic serialization/deserialization. // and Boost.JSON automatic serialization/deserialization.
#include <boost/describe/class.hpp>
#include <cstdint>
#include <string>
#include <vector>
namespace notes { namespace notes {
struct note_t struct note_t

View File

@ -22,7 +22,6 @@
#include <boost/mysql/error_with_diagnostics.hpp> #include <boost/mysql/error_with_diagnostics.hpp>
#include <boost/asio/awaitable.hpp> #include <boost/asio/awaitable.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/beast/http/message.hpp> #include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp> #include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>

View File

@ -173,16 +173,8 @@ asio::awaitable<void> orders::run_server(mysql::connection_pool& pool, unsigned
// Start the acceptor loop // Start the acceptor loop
while (true) while (true)
{ {
// Accept a new connection. asio::as_tuple prevents async_accept // Accept a new connection
// from throwing exceptions on failure. asio::ip::tcp::socket sock = co_await acc.async_accept();
auto [ec, sock] = co_await acc.async_accept(asio::as_tuple);
// If there was an error accepting the connection, exit our loop
if (ec)
{
log_error("Error while accepting connection", ec);
co_return;
}
// Function implementing our session logic. // Function implementing our session logic.
// Takes ownership of the socket. // Takes ownership of the socket.

View File

@ -12,6 +12,11 @@
// //
// File: types.hpp // File: types.hpp
// //
// Contains type definitions used in the REST API and database code.
// We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection
// capabilities to our types. This allows using Boost.MySQL
// static interface (i.e. static_results<T>) to parse query results,
// and Boost.JSON automatic serialization/deserialization.
#include <boost/describe/class.hpp> #include <boost/describe/class.hpp>
@ -21,12 +26,6 @@
#include <string_view> #include <string_view>
#include <vector> #include <vector>
// Contains type definitions used in the REST API and database code.
// We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection
// capabilities to our types. This allows using Boost.MySQL
// static interface (i.e. static_results<T>) to parse query results,
// and Boost.JSON automatic serialization/deserialization.
namespace orders { namespace orders {
// A product object, as defined in the database and in the GET /products endpoint // A product object, as defined in the database and in the GET /products endpoint

View File

@ -99,12 +99,12 @@ endif()
# Advanced # Advanced
add_example( add_example(
http_server_cpp11_coroutines http_server_cpp14_coroutines
SOURCES SOURCES
3_advanced/http_server_cpp11_coroutines/repository.cpp 3_advanced/http_server_cpp14_coroutines/repository.cpp
3_advanced/http_server_cpp11_coroutines/handle_request.cpp 3_advanced/http_server_cpp14_coroutines/handle_request.cpp
3_advanced/http_server_cpp11_coroutines/server.cpp 3_advanced/http_server_cpp14_coroutines/server.cpp
3_advanced/http_server_cpp11_coroutines/main.cpp 3_advanced/http_server_cpp14_coroutines/main.cpp
LIBS LIBS
Boost::context Boost::context
Boost::json Boost::json

View File

@ -105,11 +105,11 @@ run_example unix_socket : 2_simple/unix_socket.cpp
; ;
# Advanced # Advanced
run_example http_server_cpp11_coroutines : run_example http_server_cpp14_coroutines :
3_advanced/http_server_cpp11_coroutines/main.cpp 3_advanced/http_server_cpp14_coroutines/main.cpp
3_advanced/http_server_cpp11_coroutines/repository.cpp 3_advanced/http_server_cpp14_coroutines/repository.cpp
3_advanced/http_server_cpp11_coroutines/handle_request.cpp 3_advanced/http_server_cpp14_coroutines/handle_request.cpp
3_advanced/http_server_cpp11_coroutines/server.cpp 3_advanced/http_server_cpp14_coroutines/server.cpp
/boost/mysql/test//boost_context_lib /boost/mysql/test//boost_context_lib
/boost/mysql/test//boost_json_lib /boost/mysql/test//boost_json_lib
/boost/url//boost_url /boost/url//boost_url

View File

@ -27,14 +27,14 @@ def _random_string() -> str:
def _call_endpoints(port: int): def _call_endpoints(port: int):
base_url = 'http://127.0.0.1:{}'.format(port) url = 'http://127.0.0.1:{}/notes'.format(port)
# Create a note # Create a note
note_unique = _random_string() note_unique = _random_string()
title = 'My note {}'.format(note_unique) title = 'My note {}'.format(note_unique)
content = 'This is a note about {}'.format(note_unique) content = 'This is a note about {}'.format(note_unique)
res = requests.post( res = requests.post(
'{}/notes'.format(base_url), url,
json={'title': title, 'content': content} json={'title': title, 'content': content}
) )
_check_response(res) _check_response(res)
@ -44,7 +44,7 @@ def _call_endpoints(port: int):
assert note['note']['content'] == content assert note['note']['content'] == content
# Retrieve all notes # Retrieve all notes
res = requests.get('{}/notes'.format(base_url)) res = requests.get(url)
_check_response(res) _check_response(res)
all_notes = res.json() all_notes = res.json()
assert len([n for n in all_notes['notes'] if n['id'] == note_id]) == 1 assert len([n for n in all_notes['notes'] if n['id'] == note_id]) == 1
@ -54,7 +54,8 @@ def _call_endpoints(port: int):
title = 'Edited {}'.format(note_unique) title = 'Edited {}'.format(note_unique)
content = 'This is a note an edit on {}'.format(note_unique) content = 'This is a note an edit on {}'.format(note_unique)
res = requests.put( res = requests.put(
'{}/notes/{}'.format(base_url, note_id), url,
params={'id': note_id},
json={'title': title, 'content': content} json={'title': title, 'content': content}
) )
_check_response(res) _check_response(res)
@ -64,7 +65,7 @@ def _call_endpoints(port: int):
assert note['note']['content'] == content assert note['note']['content'] == content
# Retrieve the note # Retrieve the note
res = requests.get('{}/notes/{}'.format(base_url, note_id)) res = requests.get(url, params={'id': note_id})
_check_response(res) _check_response(res)
note = res.json() note = res.json()
assert int(note['note']['id']) == note_id assert int(note['note']['id']) == note_id
@ -72,12 +73,12 @@ def _call_endpoints(port: int):
assert note['note']['content'] == content assert note['note']['content'] == content
# Delete the note # Delete the note
res = requests.delete('{}/notes/{}'.format(base_url, note_id)) res = requests.delete(url, params={'id': note_id})
_check_response(res) _check_response(res)
assert res.json()['deleted'] == True assert res.json()['deleted'] == True
# The note is not there # The note is not there
res = requests.get('{}/notes/{}'.format(base_url, note_id)) res = requests.get(url, params={'id': note_id})
assert res.status_code == 404 assert res.status_code == 404

View File

@ -142,17 +142,16 @@ ADVANCED_EXAMPLES = [
'3_advanced/http_server_cpp20/server.cpp', '3_advanced/http_server_cpp20/server.cpp',
], 'A REST API server that uses C++20 coroutines'), ], 'A REST API server that uses C++20 coroutines'),
MultiExample('http_server_cpp11_coroutines', [ MultiExample('http_server_cpp14_coroutines', [
'3_advanced/http_server_cpp11_coroutines/main.cpp', '3_advanced/http_server_cpp14_coroutines/main.cpp',
'3_advanced/http_server_cpp11_coroutines/types.hpp', '3_advanced/http_server_cpp14_coroutines/types.hpp',
'3_advanced/http_server_cpp11_coroutines/repository.hpp', '3_advanced/http_server_cpp14_coroutines/repository.hpp',
'3_advanced/http_server_cpp11_coroutines/repository.cpp', '3_advanced/http_server_cpp14_coroutines/repository.cpp',
'3_advanced/http_server_cpp11_coroutines/handle_request.hpp', '3_advanced/http_server_cpp14_coroutines/handle_request.hpp',
'3_advanced/http_server_cpp11_coroutines/handle_request.cpp', '3_advanced/http_server_cpp14_coroutines/handle_request.cpp',
'3_advanced/http_server_cpp11_coroutines/server.hpp', '3_advanced/http_server_cpp14_coroutines/server.hpp',
'3_advanced/http_server_cpp11_coroutines/server.cpp', '3_advanced/http_server_cpp14_coroutines/server.cpp',
'3_advanced/http_server_cpp11_coroutines/log_error.hpp', ], 'A C++14 REST API server that uses asio::yield_context'),
], 'A REST API server that uses asio::yield_context'),
] ]
ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES