...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
This example demonstrates how to use connection_pool
.
It implements an HTTP REST API server for a text notes application. The API
provides CRUD methods on note objects. Instead of opening a new MySQL connection
per HTTP request, the server uses a connection pool to reuse connections.
The example employs async functions with stackful coroutines.
This example contains multiple files, and requires linking to Boost.Context, Boost.Json and and Boost.Url. This example assumes you have gone through the setup.
// // File: main.cpp // #include <boost/mysql/any_address.hpp> #include <boost/mysql/connection_pool.hpp> #include <boost/mysql/pool_params.hpp> #include <boost/asio/detached.hpp> #include <boost/asio/signal_set.hpp> #include <boost/asio/thread_pool.hpp> #include <boost/system/error_code.hpp> #include <cstddef> #include <cstdlib> #include <iostream> #include <memory> #include <string> #include "server.hpp" // This example demonstrates how to use a connection_pool. // It implements a minimal REST API to manage notes. // A note is a simple object containing a user-defined title and content. // The REST API offers CRUD operations on such objects: // POST /notes Creates a new note. // GET /notes Retrieves all notes. // GET /notes/<id> Retrieves a single note. // PUT /notes/<id> Replaces a note, changing its title and content. // DELETE /notes/<id> Deletes a note. // // Notes are stored in MySQL. The note_repository class encapsulates // access to MySQL, offering friendly functions to manipulate notes. // server.cpp encapsulates all the boilerplate to launch an HTTP server, // match URLs to API endpoints, and invoke the relevant note_repository functions. // All communication happens asynchronously. We use stackful coroutines to simplify // development, using boost::asio::spawn and boost::asio::yield_context. // // Note: connection_pool is an experimental feature. using namespace notes; // The number of threads to use static constexpr std::size_t num_threads = 5; int main(int argc, char* argv[]) { // Check command line arguments. if (argc != 5) { std::cerr << "Usage: " << argv[0] << " <username> <password> <mysql-hostname> <port>\n"; return EXIT_FAILURE; } // Application config const char* mysql_username = argv[1]; const char* mysql_password = argv[2]; const char* mysql_hostname = argv[3]; auto port = static_cast<unsigned short>(std::stoi(argv[4])); // An event loop, where the application will run. // We will use the main thread to run the pool, too, so we use // one thread less than configured boost::asio::thread_pool th_pool(num_threads - 1); // Configuration for the connection pool boost::mysql::pool_params pool_prms{ // Connect using TCP, to the given hostname and using the default port boost::mysql::host_and_port{mysql_hostname}, // Authenticate using the given username mysql_username, // Password for the above username mysql_password, // Database to use when connecting "boost_mysql_examples", }; // Using thread_safe will make the pool thread-safe by internally // creating and using a strand. // This allows us to share the pool between sessions, which may run // concurrently, on different threads. pool_prms.thread_safe = true; // 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 shared_st->pool.async_run(boost::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(); // Stop the execution context. This will cause main to exit th_pool.stop(); }); // Attach the current thread to the thread pool. This will block // until stop() is called th_pool.attach(); // Wait until all threads have exited th_pool.join(); std::cout << "Server exiting" << std::endl; // (If we get here, it means we got a SIGINT or SIGTERM) return EXIT_SUCCESS; }
// // 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. // 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 notes { struct note_t { // The unique database ID of the object. std::int64_t id; // The note's title. std::string title; // The note's actual content. std::string content; }; BOOST_DESCRIBE_STRUCT(note_t, (), (id, title, content)) // // REST API requests. // // Used for creating and replacing notes struct note_request_body { // The title that the new note should have. std::string title; // The content that the new note should have. std::string content; }; BOOST_DESCRIBE_STRUCT(note_request_body, (), (title, content)) // // REST API responses. // // Used by endpoints returning several notes (like GET /notes). struct multi_notes_response { // The retrieved notes. std::vector<note_t> notes; }; BOOST_DESCRIBE_STRUCT(multi_notes_response, (), (notes)) // Used by endpoints returning a single note (like GET /notes/<id>) struct single_note_response { // The retrieved note. note_t note; }; BOOST_DESCRIBE_STRUCT(single_note_response, (), (note)) // Used by DELETE /notes/<id> struct delete_note_response { // true if the note was found and deleted, false if the note didn't exist. bool deleted; }; BOOST_DESCRIBE_STRUCT(delete_note_response, (), (deleted)) } // namespace notes
// // File: repository.hpp // #include <boost/mysql/connection_pool.hpp> #include <boost/mysql/string_view.hpp> #include <boost/asio/error.hpp> #include <boost/asio/spawn.hpp> #include <boost/optional/optional.hpp> #include <cstdint> #include "types.hpp" namespace notes { using boost::optional; using boost::mysql::string_view; // 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, // an exception of type boost::mysql::error_with_diagnostics is thrown. class note_repository { boost::mysql::connection_pool& pool_; public: // Constructor (this is a cheap-to-construct object) note_repository(boost::mysql::connection_pool& pool) : pool_(pool) {} // Retrieves all notes present in the database std::vector<note_t> get_notes(boost::asio::yield_context yield); // Retrieves a single note by ID. Returns an empty optional // 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); // Creates a new note in the database with the given components. // 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); // Replaces the note identified by note_id, setting its components to the // ones passed. Returns the updated note. If no note with ID matching // note_id can be found, an empty optional is returned. optional<note_t> replace_note( std::int64_t note_id, string_view title, string_view content, boost::asio::yield_context yield ); // Deletes the note identified by note_id. Returns true if // a matching note was deleted, false otherwise. bool delete_note(std::int64_t note_id, boost::asio::yield_context yield); }; } // namespace notes
// // File: repository.cpp // #include <boost/mysql/statement.hpp> #include <boost/mysql/static_results.hpp> #include <boost/mysql/string_view.hpp> #include <boost/mysql/with_diagnostics.hpp> #include <boost/mysql/with_params.hpp> #include <iterator> #include <tuple> #include <utility> #include "repository.hpp" #include "types.hpp" using namespace notes; namespace mysql = boost::mysql; using mysql::with_diagnostics; // 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 // ); 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, // which is a proxy to an any_connection object. Connections are returned to the // pool when the proxy object is destroyed. // with_diagnostics ensures that thrown exceptions include diagnostic information mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield)); // Execute the query to retrieve all notes. We use the static interface to // parse results directly into static_results. mysql::static_results<note_t> result; conn->async_execute("SELECT id, title, content FROM notes", result, with_diagnostics(yield)); // By default, connections are reset after they are returned to the pool // (by using any_connection::async_reset_connection). This will reset any // session state we changed while we were using the connection // (e.g. it will deallocate any statements we prepared). // We did nothing to mutate session state, so we can tell the pool to skip // this step, providing a minor performance gain. // We use pooled_connection::return_without_reset to do this. conn.return_without_reset(); // Move note_t objects into the result vector to save allocations return std::vector<note_t>( std::make_move_iterator(result.rows().begin()), std::make_move_iterator(result.rows().end()) ); // If an exception is thrown, pooled_connection's destructor will // return the connection automatically to the pool. } optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yield_context yield) { // 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 // pool when the proxy object is destroyed. mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield)); // When executed, with_params expands a query client-side before sending it to the server. // Placeholders are marked with {} mysql::static_results<note_t> result; conn->async_execute( mysql::with_params("SELECT id, title, content FROM notes WHERE id = {}", note_id), result, with_diagnostics(yield) ); // We did nothing to mutate session state, so we can skip reset conn.return_without_reset(); // An empty results object indicates that no note was found if (result.rows().empty()) return {}; else return std::move(result.rows()[0]); } note_t note_repository::create_note(string_view title, string_view content, boost::asio::yield_context yield) { // 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 // pool when the proxy object is destroyed. mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield)); // We will use statements in this function for the sake of example. // We don't need to deallocate the statement explicitly, // since the pool takes care of it after the connection is returned. // You can also use with_params instead of statements. mysql::statement stmt = conn->async_prepare_statement( "INSERT INTO notes (title, content) VALUES (?, ?)", with_diagnostics(yield) ); // Execute the statement. The statement won't produce any rows, // so we can use static_results<std::tuple<>> mysql::static_results<std::tuple<>> result; conn->async_execute(stmt.bind(title, content), result, with_diagnostics(yield)); // MySQL reports last_insert_id as a uint64_t regardless of the actual ID type. // Given our table definition, this cast is safe auto new_id = static_cast<std::int64_t>(result.last_insert_id()); return note_t{new_id, title, content}; // There's no need to return the connection explicitly to the pool, // pooled_connection's destructor takes care of it. } optional<note_t> note_repository::replace_note( std::int64_t note_id, string_view title, string_view content, boost::asio::yield_context yield ) { // 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 // pool when the proxy object is destroyed. mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield)); // Expand and execute the query. // It won't produce any rows, so we can use static_results<std::tuple<>> mysql::static_results<std::tuple<>> empty_result; conn->async_execute( mysql::with_params( "UPDATE notes SET title = {}, content = {} WHERE id = {}", title, content, note_id ), empty_result, with_diagnostics(yield) ); // We didn't mutate session state, so we can skip reset conn.return_without_reset(); // No affected rows means that the note doesn't exist if (empty_result.affected_rows() == 0u) return {}; return note_t{note_id, title, content}; } bool note_repository::delete_note(std::int64_t note_id, boost::asio::yield_context yield) { // 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 // pool when the proxy object is destroyed. mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield)); // Expand and execute the query. // It won't produce any rows, so we can use static_results<std::tuple<>> mysql::static_results<std::tuple<>> empty_result; conn->async_execute( mysql::with_params("DELETE FROM notes WHERE id = {}", note_id), empty_result, with_diagnostics(yield) ); // We didn't mutate session state, so we can skip reset conn.return_without_reset(); // No affected rows means that the note didn't exist return empty_result.affected_rows() != 0u; }
// // File: handle_request.hpp // #include <boost/asio/error.hpp> #include <boost/asio/spawn.hpp> #include <boost/beast/http/message.hpp> #include <boost/beast/http/string_body.hpp> #include "repository.hpp" namespace notes { // Handles an individual HTTP request, producing a response. boost::beast::http::response<boost::beast::http::string_body> handle_request( const boost::beast::http::request<boost::beast::http::string_body>& request, note_repository repo, boost::asio::yield_context yield ); } // namespace notes
// // 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 fuctions. 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 crated 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); }
// // File: server.hpp // #include <boost/mysql/connection_pool.hpp> #include <boost/asio/any_io_executor.hpp> #include <boost/asio/io_context.hpp> #include <boost/system/error_code.hpp> #include <memory> namespace notes { // State shared by all sessions created by our server. // For this application, we only need a connection_pool object. // Place here any other singleton objects your application may need. // We will use std::shared_ptr<shared_state> to ensure that objects // are kept alive until all sessions are terminated. struct shared_state { boost::mysql::connection_pool pool; shared_state(boost::mysql::connection_pool pool) : pool(std::move(pool)) {} }; // Launches a HTTP server that will listen on 0.0.0.0:port. // If the server fails to launch (e.g. because the port is aleady in use), // returns a non-zero error code. ex should identify the io_context or thread_pool // where the server should run. The server is run until the underlying execution // context is stopped. boost::system::error_code launch_server( boost::asio::any_io_executor ex, std::shared_ptr<shared_state> state, unsigned short port ); } // namespace notes
// // 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 acept 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(); }
// // 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