Boost
C++ Libraries
...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
This version of Boost is under active development. You are currently in the master branch. The current version is 1.91.0.
This example assumes you have gone through the setup.
/** * Implements a HTTP REST API using Boost.MySQL and Boost.Beast. * The server is asynchronous and uses asio::yield_context as its completion * style. It only requires C++14 to work. * * 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=<id> Retrieves a single note. * PUT /notes?id=<id> Replaces a note, changing its title and content. * DELETE /notes?id=<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 asio::spawn and 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/io_context.hpp> #include <boost/asio/signal_set.hpp> #include <boost/asio/spawn.hpp> #include <boost/asio/thread_pool.hpp> #include <boost/system/error_code.hpp> #include <cstdlib> #include <iostream> #include <memory> #include <string> #include "server.hpp" namespace asio = boost::asio; namespace mysql = boost::mysql; using namespace notes; 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. asio::io_context ctx; // Configuration for the connection pool mysql::pool_params params{ // Connect using TCP, to the given hostname and using the default port 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", }; // Create the connection pool. // shared_state contains all singleton objects that our application may need. // Coroutines created by asio::spawn might survive until the io_context is destroyed // (even after io_context::stop() has been called). This is not the case for callbacks // 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))); // Launch the MySQL pool st->pool.async_run(asio::detached); // 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 ctx.stop(); }); // Launch the server. This will run until the context is stopped asio::spawn( // Spawn the coroutine in the io_context ctx, // The coroutine to run [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; // (If we get here, it means we got a SIGINT or SIGTERM) return EXIT_SUCCESS; }
// // 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 <cstdint> #include <string> #include <vector> 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/spawn.hpp> #include <boost/optional/optional.hpp> #include <cstdint> #include "types.hpp" namespace notes { // Encapsulates database logic. // All operations are async, and use stackful coroutines (asio::yield_context). // If the database can't be contacted, or unexpected database errors are found, // an exception of type 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) noexcept : 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. 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. // Returns the newly created note, including the newly allocated ID. 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 // ones passed. Returns the updated note. If no note with ID matching // note_id can be found, an empty optional is returned. boost::optional<note_t> replace_note( std::int64_t note_id, boost::mysql::string_view title, boost::mysql::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 // // 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/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" namespace asio = boost::asio; namespace mysql = boost::mysql; using namespace notes; using mysql::with_diagnostics; std::vector<note_t> note_repository::get_notes(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. } 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, // 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( 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, // 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. } boost::optional<note_t> note_repository::replace_note( std::int64_t note_id, 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, // 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, 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/mysql/connection_pool.hpp> #include <boost/asio/spawn.hpp> #include <boost/beast/http/message.hpp> #include <boost/beast/http/string_body.hpp> namespace notes { // 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::mysql::connection_pool& pool, const boost::beast::http::request<boost::beast::http::string_body>& request, boost::asio::yield_context yield ); } // namespace notes
// // 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(); } }
// // File: server.hpp // #include <boost/mysql/connection_pool.hpp> #include <boost/asio/spawn.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) noexcept : pool(std::move(pool)) {} }; // 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), // throws an exception. The server runs until the underlying execution // context is stopped. void run_server(std::shared_ptr<shared_state> st, unsigned short port, boost::asio::yield_context yield); } // namespace notes
// // 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; } } } ); } }