![]() |
Home | Libraries | People | FAQ | More |
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; } } } ); } }