...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 assumes you have gone through the setup.
/** * 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. * This example requires linking to Boost::context, Boost::json and Boost::url. */ #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" 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 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(); }
// // 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