mirror of
https://github.com/boostorg/mysql.git
synced 2025-05-12 14:11:41 +00:00
Made the asio::yield_context HTTP server single-threaded and reworked it
The example is now much more legible The example no longer crashes on termination Renamed it to match the C++ standard it requires close #414
This commit is contained in:
parent
b4365f3254
commit
07200f17c2
@ -158,15 +158,14 @@ END
|
||||
[import ../../example/3_advanced/http_server_cpp20/handle_request.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp20/server.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp20/server.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/main.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/types.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/repository.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/repository.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/handle_request.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/handle_request.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/server.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/server.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/log_error.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp14_coroutines/main.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp14_coroutines/types.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp14_coroutines/repository.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp14_coroutines/repository.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp14_coroutines/handle_request.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp14_coroutines/handle_request.cpp]
|
||||
[import ../../example/3_advanced/http_server_cpp14_coroutines/server.hpp]
|
||||
[import ../../example/3_advanced/http_server_cpp14_coroutines/server.cpp]
|
||||
[import ../../test/integration/test/snippets/prepared_statements.cpp]
|
||||
[import ../../test/integration/test/snippets/sql_formatting_advanced.cpp]
|
||||
[import ../../test/integration/test/snippets/connection_establishment.cpp]
|
||||
|
@ -360,27 +360,25 @@ This example assumes you have gone through the [link mysql.examples.setup setup]
|
||||
|
||||
|
||||
|
||||
[section:http_server_cpp11_coroutines A REST API server that uses asio::yield_context]
|
||||
[section:http_server_cpp14_coroutines A C++14 REST API server that uses asio::yield_context]
|
||||
|
||||
This example assumes you have gone through the [link mysql.examples.setup setup].
|
||||
|
||||
[example_http_server_cpp11_coroutines_main_cpp]
|
||||
[example_http_server_cpp14_coroutines_main_cpp]
|
||||
|
||||
[example_http_server_cpp11_coroutines_types_hpp]
|
||||
[example_http_server_cpp14_coroutines_types_hpp]
|
||||
|
||||
[example_http_server_cpp11_coroutines_repository_hpp]
|
||||
[example_http_server_cpp14_coroutines_repository_hpp]
|
||||
|
||||
[example_http_server_cpp11_coroutines_repository_cpp]
|
||||
[example_http_server_cpp14_coroutines_repository_cpp]
|
||||
|
||||
[example_http_server_cpp11_coroutines_handle_request_hpp]
|
||||
[example_http_server_cpp14_coroutines_handle_request_hpp]
|
||||
|
||||
[example_http_server_cpp11_coroutines_handle_request_cpp]
|
||||
[example_http_server_cpp14_coroutines_handle_request_cpp]
|
||||
|
||||
[example_http_server_cpp11_coroutines_server_hpp]
|
||||
[example_http_server_cpp14_coroutines_server_hpp]
|
||||
|
||||
[example_http_server_cpp11_coroutines_server_cpp]
|
||||
|
||||
[example_http_server_cpp11_coroutines_log_error_hpp]
|
||||
[example_http_server_cpp14_coroutines_server_cpp]
|
||||
|
||||
[endsect]
|
||||
|
||||
|
@ -1,375 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/mysql/static_results.hpp>
|
||||
|
||||
#include "log_error.hpp"
|
||||
|
||||
#ifdef BOOST_MYSQL_CXX14
|
||||
|
||||
//[example_http_server_cpp11_coroutines_handle_request_cpp
|
||||
//
|
||||
// File: handle_request.cpp
|
||||
//
|
||||
|
||||
#include <boost/mysql/error_code.hpp>
|
||||
#include <boost/mysql/error_with_diagnostics.hpp>
|
||||
|
||||
#include <boost/asio/cancel_after.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <boost/json/value_from.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <boost/url/parse.hpp>
|
||||
#include <boost/variant2/variant.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
#include "handle_request.hpp"
|
||||
#include "repository.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
// This file contains all the boilerplate code to dispatch HTTP
|
||||
// requests to API endpoints. Functions here end up calling
|
||||
// note_repository functions.
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace http = boost::beast::http;
|
||||
using boost::mysql::error_code;
|
||||
using boost::mysql::string_view;
|
||||
using namespace notes;
|
||||
|
||||
namespace {
|
||||
|
||||
// Attempts to parse a numeric ID from a string.
|
||||
// If you're using C++17, you can use std::from_chars, instead
|
||||
static boost::optional<std::int64_t> parse_id(const std::string& from)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::size_t consumed = 0;
|
||||
int res = std::stoi(from, &consumed);
|
||||
if (consumed != from.size())
|
||||
return {};
|
||||
else if (res < 0)
|
||||
return {};
|
||||
return res;
|
||||
}
|
||||
catch (const std::exception&)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Encapsulates the logic required to match a HTTP request
|
||||
// to an API endpoint, call the relevant note_repository function,
|
||||
// and return an HTTP response.
|
||||
class request_handler
|
||||
{
|
||||
// The HTTP request we're handling. Requests are small in size,
|
||||
// so we use http::request<http::string_body>
|
||||
const http::request<http::string_body>& request_;
|
||||
|
||||
// The repository to access MySQL
|
||||
note_repository repo_;
|
||||
|
||||
// Creates an error response
|
||||
http::response<http::string_body> error_response(http::status code, string_view msg) const
|
||||
{
|
||||
http::response<http::string_body> res;
|
||||
|
||||
// Set the status code
|
||||
res.result(code);
|
||||
|
||||
// Set the keep alive option
|
||||
res.keep_alive(request_.keep_alive());
|
||||
|
||||
// Set the body
|
||||
res.body() = msg;
|
||||
|
||||
// Adjust the content-length field
|
||||
res.prepare_payload();
|
||||
|
||||
// Done
|
||||
return res;
|
||||
}
|
||||
|
||||
// Used when the request's Content-Type header doesn't match what we expect
|
||||
http::response<http::string_body> invalid_content_type() const
|
||||
{
|
||||
return error_response(http::status::bad_request, "Invalid content-type");
|
||||
}
|
||||
|
||||
// Used when the request body didn't match the format we expect
|
||||
http::response<http::string_body> invalid_body() const
|
||||
{
|
||||
return error_response(http::status::bad_request, "Invalid body");
|
||||
}
|
||||
|
||||
// Used when the request's method didn't match the ones allowed by the endpoint
|
||||
http::response<http::string_body> method_not_allowed() const
|
||||
{
|
||||
return error_response(http::status::method_not_allowed, "Method not allowed");
|
||||
}
|
||||
|
||||
// Used when the request target couldn't be matched to any API endpoint
|
||||
http::response<http::string_body> endpoint_not_found() const
|
||||
{
|
||||
return error_response(http::status::not_found, "The requested resource was not found");
|
||||
}
|
||||
|
||||
// Used when the user requested a note (e.g. using GET /note/<id> or PUT /note/<id>)
|
||||
// but the note doesn't exist
|
||||
http::response<http::string_body> note_not_found() const
|
||||
{
|
||||
return error_response(http::status::not_found, "The requested note was not found");
|
||||
}
|
||||
|
||||
// Creates a response with a serialized JSON body.
|
||||
// T should be a type with Boost.Describe metadata containing the
|
||||
// body data to be serialized
|
||||
template <class T>
|
||||
http::response<http::string_body> json_response(const T& body) const
|
||||
{
|
||||
http::response<http::string_body> res;
|
||||
|
||||
// A JSON response is always a 200
|
||||
res.result(http::status::ok);
|
||||
|
||||
// Set the content-type header
|
||||
res.set("Content-Type", "application/json");
|
||||
|
||||
// Set the keep-alive option
|
||||
res.keep_alive(request_.keep_alive());
|
||||
|
||||
// Serialize the body data into a string and use it as the response body.
|
||||
// We use Boost.JSON's automatic serialization feature, which uses Boost.Describe
|
||||
// reflection data to generate a serialization function for us.
|
||||
res.body() = boost::json::serialize(boost::json::value_from(body));
|
||||
|
||||
// Adjust the content-length header
|
||||
res.prepare_payload();
|
||||
|
||||
// Done
|
||||
return res;
|
||||
}
|
||||
|
||||
// Returns true if the request's Content-Type is set to JSON
|
||||
bool has_json_content_type() const
|
||||
{
|
||||
auto it = request_.find("Content-Type");
|
||||
return it != request_.end() && it->value() == "application/json";
|
||||
}
|
||||
|
||||
// Attempts to parse the request body as a JSON into an object of type T.
|
||||
// T should be a type with Boost.Describe metadata.
|
||||
// We use boost::system::result, which may contain a result or an error.
|
||||
template <class T>
|
||||
boost::system::result<T> parse_json_request() const
|
||||
{
|
||||
error_code ec;
|
||||
|
||||
// Attempt to parse the request into a json::value.
|
||||
// This will fail if the provided body isn't valid JSON.
|
||||
auto val = boost::json::parse(request_.body(), ec);
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Attempt to parse the json::value into a T. This will
|
||||
// fail if the provided JSON doesn't match T's shape.
|
||||
return boost::json::try_value_to<T>(val);
|
||||
}
|
||||
|
||||
http::response<http::string_body> handle_request_impl(boost::asio::yield_context yield)
|
||||
{
|
||||
// Parse the request target. We use Boost.Url to do this.
|
||||
auto url = boost::urls::parse_origin_form(request_.target());
|
||||
if (url.has_error())
|
||||
return error_response(http::status::bad_request, "Invalid request target");
|
||||
|
||||
// We will be iterating over the target's segments to determine
|
||||
// which endpoint we are being requested
|
||||
auto segs = url->segments();
|
||||
auto segit = segs.begin();
|
||||
auto seg = *segit++;
|
||||
|
||||
// All endpoints start with /notes
|
||||
if (seg != "notes")
|
||||
return endpoint_not_found();
|
||||
|
||||
if (segit == segs.end())
|
||||
{
|
||||
if (request_.method() == http::verb::get)
|
||||
{
|
||||
// GET /notes: retrieves all the notes.
|
||||
// The request doesn't have a body.
|
||||
// The response has a JSON body with multi_notes_response format
|
||||
auto res = repo_.get_notes(yield);
|
||||
return json_response(multi_notes_response{std::move(res)});
|
||||
}
|
||||
else if (request_.method() == http::verb::post)
|
||||
{
|
||||
// POST /notes: creates a note.
|
||||
// The request has a JSON body with note_request_body format.
|
||||
// The response has a JSON body with single_note_response format.
|
||||
|
||||
// Parse the request body
|
||||
if (!has_json_content_type())
|
||||
return invalid_content_type();
|
||||
auto args = parse_json_request<note_request_body>();
|
||||
if (args.has_error())
|
||||
return invalid_body();
|
||||
|
||||
// Actually create the note
|
||||
auto res = repo_.create_note(args->title, args->content, yield);
|
||||
|
||||
// Return the newly created note as response
|
||||
return json_response(single_note_response{std::move(res)});
|
||||
}
|
||||
else
|
||||
{
|
||||
return method_not_allowed();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The URL has the form /notes/<note-id>. Parse the note ID.
|
||||
auto note_id = parse_id(*segit++);
|
||||
if (!note_id.has_value())
|
||||
{
|
||||
return error_response(
|
||||
http::status::bad_request,
|
||||
"Invalid note_id specified in request target"
|
||||
);
|
||||
}
|
||||
|
||||
// /notes/<note-id>/<something-else> is not a valid endpoint
|
||||
if (segit != segs.end())
|
||||
return endpoint_not_found();
|
||||
|
||||
if (request_.method() == http::verb::get)
|
||||
{
|
||||
// GET /notes/<note-id>: retrieves a single note.
|
||||
// The request doesn't have a body.
|
||||
// The response has a JSON body with single_note_response format
|
||||
|
||||
// Get the note
|
||||
auto res = repo_.get_note(*note_id, yield);
|
||||
|
||||
// If we didn't find it, return a 404 error
|
||||
if (!res.has_value())
|
||||
return note_not_found();
|
||||
|
||||
// Return it as response
|
||||
return json_response(single_note_response{std::move(*res)});
|
||||
}
|
||||
else if (request_.method() == http::verb::put)
|
||||
{
|
||||
// PUT /notes/<note-id>: replaces a note.
|
||||
// The request has a JSON body with note_request_body format.
|
||||
// The response has a JSON body with single_note_response format.
|
||||
|
||||
// Parse the JSON body
|
||||
if (!has_json_content_type())
|
||||
return invalid_content_type();
|
||||
auto args = parse_json_request<note_request_body>();
|
||||
if (args.has_error())
|
||||
return invalid_body();
|
||||
|
||||
// Perform the update
|
||||
auto res = repo_.replace_note(*note_id, args->title, args->content, yield);
|
||||
|
||||
// Check that it took effect. Otherwise, it's because the note wasn't there
|
||||
if (!res.has_value())
|
||||
return note_not_found();
|
||||
|
||||
// Return the updated note as response
|
||||
return json_response(single_note_response{std::move(*res)});
|
||||
}
|
||||
else if (request_.method() == http::verb::delete_)
|
||||
{
|
||||
// DELETE /notes/<note-id>: deletes a note.
|
||||
// The request doesn't have a body.
|
||||
// The response has a JSON body with delete_note_response format.
|
||||
|
||||
// Attempt to delete the note
|
||||
bool deleted = repo_.delete_note(*note_id, yield);
|
||||
|
||||
// Return whether the delete was successful in the response.
|
||||
// We don't fail DELETEs for notes that don't exist.
|
||||
return json_response(delete_note_response{deleted});
|
||||
}
|
||||
else
|
||||
{
|
||||
return method_not_allowed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
request_handler(const http::request<http::string_body>& req, note_repository repo)
|
||||
: request_(req), repo_(repo)
|
||||
{
|
||||
}
|
||||
|
||||
// Generates a response for the request passed to the constructor
|
||||
http::response<http::string_body> handle_request(boost::asio::yield_context yield)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Attempt to handle the request. We use cancel_after to set
|
||||
// a timeout to the overall operation
|
||||
return asio::spawn(
|
||||
yield.get_executor(),
|
||||
[this](asio::yield_context yield2) { return handle_request_impl(yield2); },
|
||||
asio::cancel_after(std::chrono::seconds(30), yield)
|
||||
);
|
||||
}
|
||||
catch (const boost::mysql::error_with_diagnostics& err)
|
||||
{
|
||||
// A Boost.MySQL error. This will happen if you don't have connectivity
|
||||
// to your database, your schema is incorrect or your credentials are invalid.
|
||||
// Log the error, including diagnostics, and return a generic 500
|
||||
log_error(
|
||||
"Uncaught exception: ",
|
||||
err.what(),
|
||||
"\nServer diagnostics: ",
|
||||
err.get_diagnostics().server_message()
|
||||
);
|
||||
return error_response(http::status::internal_server_error, "Internal error");
|
||||
}
|
||||
catch (const std::exception& err)
|
||||
{
|
||||
// Another kind of error. This indicates a programming error or a severe
|
||||
// server condition (e.g. out of memory). Same procedure as above.
|
||||
log_error("Uncaught exception: ", err.what());
|
||||
return error_response(http::status::internal_server_error, "Internal error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// External interface
|
||||
boost::beast::http::response<boost::beast::http::string_body> notes::handle_request(
|
||||
const boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
note_repository repo,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
return request_handler(request, repo).handle_request(yield);
|
||||
}
|
||||
|
||||
//]
|
||||
|
||||
#endif
|
@ -1,53 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_LOG_ERROR_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_LOG_ERROR_HPP
|
||||
|
||||
//[example_http_server_cpp11_coroutines_log_error_hpp
|
||||
//
|
||||
// File: log_error.hpp
|
||||
//
|
||||
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
|
||||
// Helper function to safely write diagnostics to std::cerr.
|
||||
// Since we're in a multi-threaded environment, directly writing to std::cerr
|
||||
// can lead to interleaved output, so we should synchronize calls with a mutex.
|
||||
// This function is only called in rare cases (e.g. unhandled exceptions),
|
||||
// so we can afford the synchronization overhead.
|
||||
|
||||
namespace notes {
|
||||
|
||||
// If you're in C++17+, you can write this using fold expressions
|
||||
// instead of recursion.
|
||||
inline void log_error_impl() {}
|
||||
|
||||
template <class Arg1, class... Tail>
|
||||
void log_error_impl(const Arg1& arg, const Tail&... tail)
|
||||
{
|
||||
std::cerr << arg;
|
||||
log_error_impl(tail...);
|
||||
}
|
||||
|
||||
template <class... Args>
|
||||
void log_error(const Args&... args)
|
||||
{
|
||||
static std::mutex mtx;
|
||||
|
||||
// Acquire the mutex, then write the passed arguments to std::cerr.
|
||||
std::unique_lock<std::mutex> lock(mtx);
|
||||
log_error_impl(args...);
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
|
||||
} // namespace notes
|
||||
|
||||
//]
|
||||
|
||||
#endif
|
@ -1,206 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/mysql/static_results.hpp>
|
||||
|
||||
#include "log_error.hpp"
|
||||
|
||||
#ifdef BOOST_MYSQL_CXX14
|
||||
|
||||
//[example_http_server_cpp11_coroutines_server_cpp
|
||||
//
|
||||
// File: server.cpp
|
||||
//
|
||||
|
||||
#include <boost/mysql/connection_pool.hpp>
|
||||
#include <boost/mysql/error_code.hpp>
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/parser.hpp>
|
||||
#include <boost/beast/http/read.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/write.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "handle_request.hpp"
|
||||
#include "repository.hpp"
|
||||
#include "server.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
// This file contains all the boilerplate code to implement a HTTP
|
||||
// server. Functions here end up invoking handle_request.
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace http = boost::beast::http;
|
||||
using boost::mysql::error_code;
|
||||
using namespace notes;
|
||||
|
||||
namespace {
|
||||
|
||||
static void run_http_session(
|
||||
boost::asio::ip::tcp::socket sock,
|
||||
std::shared_ptr<shared_state> st,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
error_code ec;
|
||||
|
||||
// A buffer to read incoming client requests
|
||||
boost::beast::flat_buffer buff;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Construct a new parser for each message
|
||||
http::request_parser<http::string_body> parser;
|
||||
|
||||
// Apply a reasonable limit to the allowed size
|
||||
// of the body in bytes to prevent abuse.
|
||||
parser.body_limit(10000);
|
||||
|
||||
// Read a request
|
||||
http::async_read(sock, buff, parser.get(), yield[ec]);
|
||||
|
||||
if (ec)
|
||||
{
|
||||
if (ec == http::error::end_of_stream)
|
||||
{
|
||||
// This means they closed the connection
|
||||
sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
|
||||
}
|
||||
else
|
||||
{
|
||||
// An unknown error happened
|
||||
log_error("Error reading HTTP request: ", ec);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the request to generate a response.
|
||||
// This invokes the business logic, which will need to access MySQL data
|
||||
auto response = handle_request(parser.get(), note_repository(st->pool), yield);
|
||||
|
||||
// Determine if we should close the connection
|
||||
bool keep_alive = response.keep_alive();
|
||||
|
||||
// Send the response
|
||||
http::async_write(sock, response, yield[ec]);
|
||||
if (ec)
|
||||
return log_error("Error writing HTTP response: ", ec);
|
||||
|
||||
// This means we should close the connection, usually because
|
||||
// the response indicated the "Connection: close" semantic.
|
||||
if (!keep_alive)
|
||||
{
|
||||
sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implements the server's accept loop. The server will
|
||||
// listen for connections until stopped.
|
||||
static void do_accept(
|
||||
asio::any_io_executor executor, // The original executor (without strands)
|
||||
std::shared_ptr<asio::ip::tcp::acceptor> acceptor,
|
||||
std::shared_ptr<shared_state> st
|
||||
)
|
||||
{
|
||||
acceptor->async_accept([executor, st, acceptor](error_code ec, asio::ip::tcp::socket sock) {
|
||||
// If there was an error accepting the connection, exit our loop
|
||||
if (ec)
|
||||
return log_error("Error while accepting connection", ec);
|
||||
|
||||
// Launch a new session for this connection. Each session gets its
|
||||
// own stackful coroutine, so we can get back to listening for new connections.
|
||||
boost::asio::spawn(
|
||||
// Every session gets its own strand. This prevents data races.
|
||||
asio::make_strand(executor),
|
||||
|
||||
// The actual coroutine
|
||||
[st, socket = std::move(sock)](boost::asio::yield_context yield) mutable {
|
||||
run_http_session(std::move(socket), std::move(st), yield);
|
||||
},
|
||||
|
||||
// All errors in the session are handled via error codes or by catching
|
||||
// exceptions explicitly. An unhandled exception here means an error.
|
||||
// Rethrowing it will propagate the exception, making io_context::run()
|
||||
// to throw and terminate the program.
|
||||
[](std::exception_ptr ex) {
|
||||
if (ex)
|
||||
std::rethrow_exception(ex);
|
||||
}
|
||||
);
|
||||
|
||||
// Accept a new connection
|
||||
do_accept(executor, acceptor, st);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
error_code notes::launch_server(
|
||||
boost::asio::any_io_executor ex,
|
||||
std::shared_ptr<shared_state> st,
|
||||
unsigned short port
|
||||
)
|
||||
{
|
||||
error_code ec;
|
||||
|
||||
// An object that allows us to accept incoming TCP connections.
|
||||
// Since we're in a multi-threaded environment, we create a strand for the acceptor,
|
||||
// so all accept handlers are run serialized
|
||||
auto acceptor = std::make_shared<asio::ip::tcp::acceptor>(asio::make_strand(ex));
|
||||
|
||||
// The endpoint where the server will listen. Edit this if you want to
|
||||
// change the address or port we bind to.
|
||||
boost::asio::ip::tcp::endpoint listening_endpoint(boost::asio::ip::make_address("0.0.0.0"), port);
|
||||
|
||||
// Open the acceptor
|
||||
acceptor->open(listening_endpoint.protocol(), ec);
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Allow address reuse
|
||||
acceptor->set_option(asio::socket_base::reuse_address(true), ec);
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Bind to the server address
|
||||
acceptor->bind(listening_endpoint, ec);
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Start listening for connections
|
||||
acceptor->listen(asio::socket_base::max_listen_connections, ec);
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
std::cout << "Server listening at " << acceptor->local_endpoint() << std::endl;
|
||||
|
||||
// Launch the acceptor loop
|
||||
do_accept(std::move(ex), std::move(acceptor), std::move(st));
|
||||
|
||||
// Done
|
||||
return error_code();
|
||||
}
|
||||
|
||||
//]
|
||||
|
||||
#endif
|
@ -0,0 +1,341 @@
|
||||
//
|
||||
// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/mysql/static_results.hpp>
|
||||
#ifdef BOOST_MYSQL_CXX14
|
||||
|
||||
//[example_http_server_cpp14_coroutines_handle_request_cpp
|
||||
//
|
||||
// File: handle_request.cpp
|
||||
//
|
||||
// This file contains all the boilerplate code to dispatch HTTP
|
||||
// requests to API endpoints. Functions here end up calling
|
||||
// note_repository functions.
|
||||
|
||||
#include <boost/mysql/error_code.hpp>
|
||||
#include <boost/mysql/error_with_diagnostics.hpp>
|
||||
#include <boost/mysql/string_view.hpp>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/charconv/from_chars.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <boost/json/value_from.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <boost/url/parse.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "handle_request.hpp"
|
||||
#include "repository.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace mysql = boost::mysql;
|
||||
namespace http = boost::beast::http;
|
||||
using namespace notes;
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper function that logs errors thrown by db_repository
|
||||
// when an unexpected database error happens
|
||||
void log_mysql_error(boost::system::error_code ec, const mysql::diagnostics& diag)
|
||||
{
|
||||
// Inserting the error code only prints the number and category. Add the message, too.
|
||||
std::cerr << "MySQL error: " << ec << " " << ec.message();
|
||||
|
||||
// client_message() contains client-side generated messages that don't
|
||||
// contain user-input. This is usually embedded in exceptions.
|
||||
// When working with error codes, we need to log it explicitly
|
||||
if (!diag.client_message().empty())
|
||||
{
|
||||
std::cerr << ": " << diag.client_message();
|
||||
}
|
||||
|
||||
// server_message() contains server-side messages, and thus may
|
||||
// contain user-supplied input. Printing it is safe.
|
||||
if (!diag.server_message().empty())
|
||||
{
|
||||
std::cerr << ": " << diag.server_message();
|
||||
}
|
||||
|
||||
// Done
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
|
||||
// Attempts to parse a numeric ID from a string.
|
||||
// If you're using C++17, you can use std::from_chars, instead
|
||||
boost::optional<std::int64_t> parse_id(const std::string& from)
|
||||
{
|
||||
std::int64_t id{};
|
||||
auto res = boost::charconv::from_chars(from.data(), from.data() + from.size(), id);
|
||||
if (res.ec != std::errc{} || res.ptr != from.data() + from.size())
|
||||
return {};
|
||||
return id;
|
||||
}
|
||||
|
||||
// Helpers to create error responses with a single line of code
|
||||
http::response<http::string_body> error_response(http::status code, const char* msg)
|
||||
{
|
||||
http::response<http::string_body> res;
|
||||
res.result(code);
|
||||
res.body() = msg;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Like error_response, but always uses a 400 status code
|
||||
http::response<http::string_body> bad_request(const char* body)
|
||||
{
|
||||
return error_response(http::status::bad_request, body);
|
||||
}
|
||||
|
||||
// Like error_response, but always uses a 500 status code and
|
||||
// never provides extra information that might help potential attackers.
|
||||
http::response<http::string_body> internal_server_error()
|
||||
{
|
||||
return error_response(http::status::internal_server_error, "Internal server error");
|
||||
}
|
||||
|
||||
// Creates a response with a serialized JSON body.
|
||||
// T should be a type with Boost.Describe metadata containing the
|
||||
// body data to be serialized
|
||||
template <class T>
|
||||
http::response<http::string_body> json_response(const T& body)
|
||||
{
|
||||
http::response<http::string_body> res;
|
||||
|
||||
// Set the content-type header
|
||||
res.set("Content-Type", "application/json");
|
||||
|
||||
// Serialize the body data into a string and use it as the response body.
|
||||
// We use Boost.JSON's automatic serialization feature, which uses Boost.Describe
|
||||
// reflection data to generate a serialization function for us.
|
||||
res.body() = boost::json::serialize(boost::json::value_from(body));
|
||||
|
||||
// Done
|
||||
return res;
|
||||
}
|
||||
|
||||
// Returns true if the request's Content-Type is set to JSON
|
||||
bool has_json_content_type(const http::request<http::string_body>& req)
|
||||
{
|
||||
auto it = req.find("Content-Type");
|
||||
return it != req.end() && it->value() == "application/json";
|
||||
}
|
||||
|
||||
// Attempts to parse a string as a JSON into an object of type T.
|
||||
// T should be a type with Boost.Describe metadata.
|
||||
// We use boost::system::result, which may contain a result or an error.
|
||||
template <class T>
|
||||
boost::system::result<T> parse_json(boost::mysql::string_view json_string)
|
||||
{
|
||||
// Attempt to parse the request into a json::value.
|
||||
// This will fail if the provided body isn't valid JSON.
|
||||
boost::system::error_code ec;
|
||||
auto val = boost::json::parse(json_string, ec);
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Attempt to parse the json::value into a T. This will
|
||||
// fail if the provided JSON doesn't match T's shape.
|
||||
return boost::json::try_value_to<T>(val);
|
||||
}
|
||||
|
||||
// Contains data associated to an HTTP request.
|
||||
// To be passed to individual handler functions
|
||||
struct request_data
|
||||
{
|
||||
// The incoming request
|
||||
const http::request<http::string_body>& request;
|
||||
|
||||
// The URL the request is targeting
|
||||
boost::urls::url_view target;
|
||||
|
||||
// Connection pool
|
||||
mysql::connection_pool& pool;
|
||||
|
||||
note_repository repo() const { return note_repository(pool); }
|
||||
};
|
||||
|
||||
//
|
||||
// Endpoint handlers. We have a function per method.
|
||||
// All of our endpoints have /notes as the URL path.
|
||||
//
|
||||
|
||||
// GET /notes: retrieves all the notes.
|
||||
// The request doesn't have a body.
|
||||
// The response has a JSON body with multi_notes_response format
|
||||
//
|
||||
// GET /notes?id=<note-id>: retrieves a single note.
|
||||
// The request doesn't have a body.
|
||||
// The response has a JSON body with single_note_response format
|
||||
//
|
||||
// Both endpoints share path and method, so they share handler function
|
||||
http::response<http::string_body> handle_get(const request_data& input, asio::yield_context yield)
|
||||
{
|
||||
// Parse the query parameter
|
||||
auto params_it = input.target.params().find("id");
|
||||
|
||||
// Did the client specify an ID?
|
||||
if (params_it == input.target.params().end())
|
||||
{
|
||||
auto res = input.repo().get_notes(yield);
|
||||
return json_response(multi_notes_response{std::move(res)});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse id
|
||||
auto id = parse_id((*params_it).value);
|
||||
if (!id.has_value())
|
||||
return bad_request("URL parameter 'id' should be a valid integer");
|
||||
|
||||
// Get the note
|
||||
auto res = input.repo().get_note(*id, yield);
|
||||
|
||||
// If we didn't find it, return a 404 error
|
||||
if (!res.has_value())
|
||||
return error_response(http::status::not_found, "The requested note was not found");
|
||||
|
||||
// Return it as response
|
||||
return json_response(single_note_response{std::move(*res)});
|
||||
}
|
||||
}
|
||||
|
||||
// POST /notes: creates a note.
|
||||
// The request has a JSON body with note_request_body format.
|
||||
// The response has a JSON body with single_note_response format.
|
||||
http::response<http::string_body> handle_post(const request_data& input, asio::yield_context yield)
|
||||
{
|
||||
// Parse the request body
|
||||
if (!has_json_content_type(input.request))
|
||||
return bad_request("Invalid Content-Type: expected 'application/json'");
|
||||
auto args = parse_json<note_request_body>(input.request.body());
|
||||
if (args.has_error())
|
||||
return bad_request("Invalid JSON");
|
||||
|
||||
// Actually create the note
|
||||
auto res = input.repo().create_note(args->title, args->content, yield);
|
||||
|
||||
// Return the newly created note as response
|
||||
return json_response(single_note_response{std::move(res)});
|
||||
}
|
||||
|
||||
// PUT /notes?id=<note-id>: replaces a note.
|
||||
// The request has a JSON body with note_request_body format.
|
||||
// The response has a JSON body with single_note_response format.
|
||||
http::response<http::string_body> handle_put(const request_data& input, asio::yield_context yield)
|
||||
{
|
||||
// Parse the query parameter
|
||||
auto params_it = input.target.params().find("id");
|
||||
if (params_it == input.target.params().end())
|
||||
return bad_request("Mandatory URL parameter 'id' not found");
|
||||
auto id = parse_id((*params_it).value);
|
||||
if (!id.has_value())
|
||||
return bad_request("URL parameter 'id' should be a valid integer");
|
||||
|
||||
// Parse the request body
|
||||
if (!has_json_content_type(input.request))
|
||||
return bad_request("Invalid Content-Type: expected 'application/json'");
|
||||
auto args = parse_json<note_request_body>(input.request.body());
|
||||
if (args.has_error())
|
||||
return bad_request("Invalid JSON");
|
||||
|
||||
// Perform the update
|
||||
auto res = input.repo().replace_note(*id, args->title, args->content, yield);
|
||||
|
||||
// Check that it took effect. Otherwise, it's because the note wasn't there
|
||||
if (!res.has_value())
|
||||
return bad_request("The requested note was not found");
|
||||
|
||||
// Return the updated note as response
|
||||
return json_response(single_note_response{std::move(*res)});
|
||||
}
|
||||
|
||||
// DELETE /notes/<note-id>: deletes a note.
|
||||
// The request doesn't have a body.
|
||||
// The response has a JSON body with delete_note_response format.
|
||||
http::response<http::string_body> handle_delete(const request_data& input, asio::yield_context yield)
|
||||
{
|
||||
// Parse the query parameter
|
||||
auto params_it = input.target.params().find("id");
|
||||
if (params_it == input.target.params().end())
|
||||
return bad_request("Mandatory URL parameter 'id' not found");
|
||||
auto id = parse_id((*params_it).value);
|
||||
if (!id.has_value())
|
||||
return bad_request("URL parameter 'id' should be a valid integer");
|
||||
|
||||
// Attempt to delete the note
|
||||
bool deleted = input.repo().delete_note(*id, yield);
|
||||
|
||||
// Return whether the delete was successful in the response.
|
||||
// We don't fail DELETEs for notes that don't exist.
|
||||
return json_response(delete_note_response{deleted});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// External interface
|
||||
http::response<http::string_body> notes::handle_request(
|
||||
mysql::connection_pool& pool,
|
||||
const http::request<http::string_body>& request,
|
||||
asio::yield_context yield
|
||||
)
|
||||
{
|
||||
// Parse the request target
|
||||
auto target = boost::urls::parse_origin_form(request.target());
|
||||
if (!target.has_value())
|
||||
return bad_request("Invalid request target");
|
||||
|
||||
// All our endpoints have /notes as path, with different verbs and parameters.
|
||||
// Verify that the path matches
|
||||
if (target->path() != "/notes")
|
||||
return error_response(http::status::not_found, "Endpoint not found");
|
||||
|
||||
// Compose the request_data object
|
||||
request_data input{request, *target, pool};
|
||||
|
||||
// Invoke the relevant handler, depending on the method
|
||||
try
|
||||
{
|
||||
switch (input.request.method())
|
||||
{
|
||||
case http::verb::get: return handle_get(input, yield);
|
||||
case http::verb::post: return handle_post(input, yield);
|
||||
case http::verb::put: return handle_put(input, yield);
|
||||
case http::verb::delete_: return handle_delete(input, yield);
|
||||
default: return error_response(http::status::method_not_allowed, "Method not allowed for /notes");
|
||||
}
|
||||
}
|
||||
catch (const mysql::error_with_diagnostics& err)
|
||||
{
|
||||
// A Boost.MySQL error. This will happen if you don't have connectivity
|
||||
// to your database, your schema is incorrect or your credentials are invalid.
|
||||
// Log the error, including diagnostics
|
||||
log_mysql_error(err.code(), err.get_diagnostics());
|
||||
|
||||
// Never disclose error info to a potential attacker
|
||||
return internal_server_error();
|
||||
}
|
||||
catch (const std::exception& err)
|
||||
{
|
||||
// Another kind of error. This indicates a programming error or a severe
|
||||
// server condition (e.g. out of memory). Same procedure as above.
|
||||
std::cerr << "Uncaught exception: " << err.what() << std::endl;
|
||||
return internal_server_error();
|
||||
}
|
||||
}
|
||||
|
||||
//]
|
||||
|
||||
#endif
|
@ -5,27 +5,28 @@
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_HANDLE_REQUEST_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_HANDLE_REQUEST_HPP
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_HANDLE_REQUEST_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_HANDLE_REQUEST_HPP
|
||||
|
||||
//[example_http_server_cpp11_coroutines_handle_request_hpp
|
||||
//[example_http_server_cpp14_coroutines_handle_request_hpp
|
||||
//
|
||||
// File: handle_request.hpp
|
||||
//
|
||||
|
||||
#include <boost/asio/error.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>
|
||||
|
||||
#include "repository.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,
|
||||
note_repository repo,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
@ -6,33 +6,31 @@
|
||||
//
|
||||
|
||||
#include <boost/mysql/static_results.hpp>
|
||||
|
||||
#include "log_error.hpp"
|
||||
|
||||
#ifdef BOOST_MYSQL_CXX14
|
||||
|
||||
//[example_http_server_cpp11_coroutines_main_cpp
|
||||
//[example_http_server_cpp14_coroutines_main_cpp
|
||||
|
||||
/**
|
||||
* Implements a HTTP REST API using Boost.MySQL and Boost.Beast.
|
||||
* The server is asynchronous and uses asio::yield_context as its completion
|
||||
* style. It only requires C++11 to work.
|
||||
* style. It only requires C++14 to work.
|
||||
*
|
||||
* It implements a minimal REST API to manage notes.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* development, using asio::spawn and asio::yield_context.
|
||||
* This example requires linking to Boost::context, Boost::json and Boost::url.
|
||||
*/
|
||||
|
||||
@ -41,11 +39,12 @@
|
||||
#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 <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
@ -53,11 +52,10 @@
|
||||
|
||||
#include "server.hpp"
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace mysql = boost::mysql;
|
||||
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.
|
||||
@ -74,14 +72,12 @@ int main(int argc, char* argv[])
|
||||
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);
|
||||
asio::io_context ctx;
|
||||
|
||||
// Configuration for the connection pool
|
||||
boost::mysql::pool_params pool_prms{
|
||||
mysql::pool_params params{
|
||||
// Connect using TCP, to the given hostname and using the default port
|
||||
boost::mysql::host_and_port{mysql_hostname},
|
||||
mysql::host_and_port{mysql_hostname},
|
||||
|
||||
// Authenticate using the given username
|
||||
mysql_username,
|
||||
@ -93,47 +89,40 @@ int main(int argc, char* argv[])
|
||||
"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};
|
||||
// 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
|
||||
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();
|
||||
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
|
||||
th_pool.stop();
|
||||
ctx.stop();
|
||||
});
|
||||
|
||||
// Attach the current thread to the thread pool. This will block
|
||||
// until stop() is called
|
||||
th_pool.attach();
|
||||
// Launch the server. This will run until the context is stopped
|
||||
asio::spawn(
|
||||
// Spawn the coroutine in the io_context
|
||||
ctx,
|
||||
|
||||
// Wait until all threads have exited
|
||||
th_pool.join();
|
||||
// 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;
|
||||
|
@ -6,15 +6,21 @@
|
||||
//
|
||||
|
||||
#include <boost/mysql/static_results.hpp>
|
||||
|
||||
#ifdef BOOST_MYSQL_CXX14
|
||||
|
||||
//[example_http_server_cpp11_coroutines_repository_cpp
|
||||
//[example_http_server_cpp14_coroutines_repository_cpp
|
||||
//
|
||||
// File: repository.cpp
|
||||
//
|
||||
// SQL code to create the notes table is located under $REPO_ROOT/example/db_setup.sql
|
||||
// The table looks like this:
|
||||
//
|
||||
// CREATE TABLE notes(
|
||||
// id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
// title TEXT NOT NULL,
|
||||
// content TEXT NOT NULL
|
||||
// );
|
||||
|
||||
#include <boost/mysql/statement.hpp>
|
||||
#include <boost/mysql/static_results.hpp>
|
||||
#include <boost/mysql/string_view.hpp>
|
||||
#include <boost/mysql/with_diagnostics.hpp>
|
||||
@ -27,20 +33,12 @@
|
||||
#include "repository.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
using namespace notes;
|
||||
namespace asio = boost::asio;
|
||||
namespace mysql = boost::mysql;
|
||||
using namespace notes;
|
||||
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)
|
||||
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
|
||||
@ -72,7 +70,7 @@ std::vector<note_t> note_repository::get_notes(boost::asio::yield_context yield)
|
||||
// return the connection automatically to the pool.
|
||||
}
|
||||
|
||||
optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yield_context yield)
|
||||
boost::optional<note_t> note_repository::get_note(std::int64_t note_id, asio::yield_context yield)
|
||||
{
|
||||
// Get a fresh connection from the pool. This returns a pooled_connection object,
|
||||
// which is a proxy to an any_connection object. Connections are returned to the
|
||||
@ -98,7 +96,11 @@ optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yi
|
||||
return std::move(result.rows()[0]);
|
||||
}
|
||||
|
||||
note_t note_repository::create_note(string_view title, string_view content, boost::asio::yield_context yield)
|
||||
note_t note_repository::create_note(
|
||||
mysql::string_view title,
|
||||
mysql::string_view content,
|
||||
asio::yield_context yield
|
||||
)
|
||||
{
|
||||
// Get a fresh connection from the pool. This returns a pooled_connection object,
|
||||
// which is a proxy to an any_connection object. Connections are returned to the
|
||||
@ -129,11 +131,11 @@ note_t note_repository::create_note(string_view title, string_view content, boos
|
||||
// pooled_connection's destructor takes care of it.
|
||||
}
|
||||
|
||||
optional<note_t> note_repository::replace_note(
|
||||
boost::optional<note_t> note_repository::replace_note(
|
||||
std::int64_t note_id,
|
||||
string_view title,
|
||||
string_view content,
|
||||
boost::asio::yield_context yield
|
||||
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,
|
||||
@ -165,7 +167,7 @@ optional<note_t> note_repository::replace_note(
|
||||
return note_t{note_id, title, content};
|
||||
}
|
||||
|
||||
bool note_repository::delete_note(std::int64_t note_id, boost::asio::yield_context yield)
|
||||
bool note_repository::delete_note(std::int64_t note_id, asio::yield_context yield)
|
||||
{
|
||||
// Get a fresh connection from the pool. This returns a pooled_connection object,
|
||||
// which is a proxy to an any_connection object. Connections are returned to the
|
@ -5,10 +5,10 @@
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_REPOSITORY_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_REPOSITORY_HPP
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_REPOSITORY_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_REPOSITORY_HPP
|
||||
|
||||
//[example_http_server_cpp11_coroutines_repository_hpp
|
||||
//[example_http_server_cpp14_coroutines_repository_hpp
|
||||
//
|
||||
// File: repository.hpp
|
||||
//
|
||||
@ -16,7 +16,6 @@
|
||||
#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>
|
||||
|
||||
@ -26,41 +25,40 @@
|
||||
|
||||
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).
|
||||
// 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 boost::mysql::error_with_diagnostics is thrown.
|
||||
// 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) : pool_(pool) {}
|
||||
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.
|
||||
optional<note_t> get_note(std::int64_t note_id, boost::asio::yield_context yield);
|
||||
boost::optional<note_t> get_note(std::int64_t note_id, boost::asio::yield_context yield);
|
||||
|
||||
// Creates a new note in the database with the given components.
|
||||
// Returns the newly created note, including the newly allocated ID.
|
||||
note_t create_note(string_view title, string_view content, boost::asio::yield_context yield);
|
||||
note_t create_note(
|
||||
boost::mysql::string_view title,
|
||||
boost::mysql::string_view content,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
// Replaces the note identified by note_id, setting its components to the
|
||||
// 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(
|
||||
boost::optional<note_t> replace_note(
|
||||
std::int64_t note_id,
|
||||
string_view title,
|
||||
string_view content,
|
||||
boost::mysql::string_view title,
|
||||
boost::mysql::string_view content,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
196
example/3_advanced/http_server_cpp14_coroutines/server.cpp
Normal file
196
example/3_advanced/http_server_cpp14_coroutines/server.cpp
Normal file
@ -0,0 +1,196 @@
|
||||
//
|
||||
// Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/mysql/static_results.hpp>
|
||||
#ifdef BOOST_MYSQL_CXX14
|
||||
|
||||
//[example_http_server_cpp14_coroutines_server_cpp
|
||||
//
|
||||
// File: server.cpp
|
||||
//
|
||||
// This file contains all the boilerplate code to implement a HTTP
|
||||
// server. Functions here end up invoking handle_request.
|
||||
|
||||
#include <boost/asio/cancel_after.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/parser.hpp>
|
||||
#include <boost/beast/http/read.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/write.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
#include "handle_request.hpp"
|
||||
#include "server.hpp"
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace http = boost::beast::http;
|
||||
using namespace notes;
|
||||
|
||||
namespace {
|
||||
|
||||
// Runs a single HTTP session until the client closes the connection
|
||||
void run_http_session(std::shared_ptr<shared_state> st, asio::ip::tcp::socket sock, asio::yield_context yield)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
boost::system::error_code ec;
|
||||
|
||||
// A buffer to read incoming client requests
|
||||
boost::beast::flat_buffer buff;
|
||||
|
||||
// A timer, to use with asio::cancel_after to implement timeouts.
|
||||
// Re-using the same timer multiple times with cancel_after
|
||||
// is more efficient than using raw cancel_after,
|
||||
// since the timer doesn't need to be re-created for every operation.
|
||||
asio::steady_timer timer(yield.get_executor());
|
||||
|
||||
// A HTTP session might involve more than one message if
|
||||
// keep-alive semantics are used. Loop until the connection closes.
|
||||
while (true)
|
||||
{
|
||||
// Construct a new parser for each message
|
||||
http::request_parser<http::string_body> parser;
|
||||
|
||||
// Apply a reasonable limit to the allowed size
|
||||
// of the body in bytes to prevent abuse.
|
||||
parser.body_limit(10000);
|
||||
|
||||
// Read a request. yield[ec] prevents exceptions from being thrown
|
||||
// on error. We use cancel_after to set a timeout for the overall read operation.
|
||||
http::async_read(sock, buff, parser.get(), asio::cancel_after(60s, yield[ec]));
|
||||
|
||||
if (ec)
|
||||
{
|
||||
if (ec == http::error::end_of_stream)
|
||||
{
|
||||
// This means they closed the connection
|
||||
sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
|
||||
}
|
||||
else
|
||||
{
|
||||
// An unknown error happened
|
||||
std::cout << "Error reading HTTP request: " << ec.message() << std::endl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& request = parser.get();
|
||||
|
||||
// Process the request to generate a response.
|
||||
// This invokes the business logic, which will need to access MySQL data.
|
||||
// Apply a timeout to the overall request handling process.
|
||||
auto response = asio::spawn(
|
||||
// Use the same executor as this coroutine
|
||||
yield.get_executor(),
|
||||
|
||||
// The logic to invoke
|
||||
[&](asio::yield_context yield2) { return handle_request(st->pool, request, yield2); },
|
||||
|
||||
// Completion token. Passing yield blocks the current coroutine
|
||||
// until handle_request completes.
|
||||
asio::cancel_after(timer, 30s, yield)
|
||||
);
|
||||
|
||||
// Adjust the response, setting fields common to all responses
|
||||
bool keep_alive = response.keep_alive();
|
||||
response.version(request.version());
|
||||
response.keep_alive(keep_alive);
|
||||
response.prepare_payload();
|
||||
|
||||
// Send the response
|
||||
http::async_write(sock, response, asio::cancel_after(60s, yield[ec]));
|
||||
if (ec)
|
||||
{
|
||||
std::cout << "Error writing HTTP response: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// This means we should close the connection, usually because
|
||||
// the response indicated the "Connection: close" semantic.
|
||||
if (!keep_alive)
|
||||
{
|
||||
sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void notes::run_server(std::shared_ptr<shared_state> st, unsigned short port, asio::yield_context yield)
|
||||
{
|
||||
// An object that allows us to accept incoming TCP connections
|
||||
asio::ip::tcp::acceptor acc(yield.get_executor());
|
||||
|
||||
// The endpoint where the server will listen. Edit this if you want to
|
||||
// change the address or port we bind to.
|
||||
asio::ip::tcp::endpoint listening_endpoint(asio::ip::make_address("0.0.0.0"), port);
|
||||
|
||||
// Open the acceptor
|
||||
acc.open(listening_endpoint.protocol());
|
||||
|
||||
// Allow address reuse
|
||||
acc.set_option(asio::socket_base::reuse_address(true));
|
||||
|
||||
// Bind to the server address
|
||||
acc.bind(listening_endpoint);
|
||||
|
||||
// Start listening for connections
|
||||
acc.listen(asio::socket_base::max_listen_connections);
|
||||
|
||||
std::cout << "Server listening at " << acc.local_endpoint() << std::endl;
|
||||
|
||||
// Start the acceptor loop
|
||||
while (true)
|
||||
{
|
||||
// Accept a new connection
|
||||
asio::ip::tcp::socket sock = acc.async_accept(yield);
|
||||
|
||||
// Launch a new session for this connection. Each session gets its
|
||||
// own coroutine, so we can get back to listening for new connections.
|
||||
asio::spawn(
|
||||
yield.get_executor(),
|
||||
|
||||
// Function implementing our session logic.
|
||||
// Takes ownership of the socket.
|
||||
[st, sock = std::move(sock)](asio::yield_context yield2) mutable {
|
||||
return run_http_session(std::move(st), std::move(sock), yield2);
|
||||
},
|
||||
|
||||
// Callback to run when the coroutine finishes
|
||||
[](std::exception_ptr ptr) {
|
||||
if (ptr)
|
||||
{
|
||||
// For extra safety, log the exception but don't propagate it.
|
||||
// If we failed to anticipate an error condition that ends up raising an exception,
|
||||
// terminate only the affected session, instead of crashing the server.
|
||||
try
|
||||
{
|
||||
std::rethrow_exception(ptr);
|
||||
}
|
||||
catch (const std::exception& exc)
|
||||
{
|
||||
std::cerr << "Uncaught error in a session: " << exc.what() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//]
|
||||
|
||||
#endif
|
@ -5,19 +5,17 @@
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_SERVER_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_SERVER_HPP
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_SERVER_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_SERVER_HPP
|
||||
|
||||
//[example_http_server_cpp11_coroutines_server_hpp
|
||||
//[example_http_server_cpp14_coroutines_server_hpp
|
||||
//
|
||||
// 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 <boost/asio/spawn.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
@ -32,19 +30,14 @@ struct shared_state
|
||||
{
|
||||
boost::mysql::connection_pool pool;
|
||||
|
||||
shared_state(boost::mysql::connection_pool pool) : pool(std::move(pool)) {}
|
||||
shared_state(boost::mysql::connection_pool pool) noexcept : pool(std::move(pool)) {}
|
||||
};
|
||||
|
||||
// Launches a HTTP server that will listen on 0.0.0.0:port.
|
||||
// Runs a HTTP server that will listen on 0.0.0.0:port.
|
||||
// If the server fails to launch (e.g. because the port is already in use),
|
||||
// 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
|
||||
// throws an exception. The server runs 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
|
||||
);
|
||||
void run_server(std::shared_ptr<shared_state> st, unsigned short port, boost::asio::yield_context yield);
|
||||
|
||||
} // namespace notes
|
||||
|
@ -5,27 +5,25 @@
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_TYPES_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_COROUTINES_TYPES_HPP
|
||||
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_TYPES_HPP
|
||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_TYPES_HPP
|
||||
|
||||
//[example_http_server_cpp11_coroutines_types_hpp
|
||||
//[example_http_server_cpp14_coroutines_types_hpp
|
||||
//
|
||||
// File: types.hpp
|
||||
//
|
||||
|
||||
#include <boost/core/span.hpp>
|
||||
#include <boost/describe/class.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Contains type definitions used in the REST API and database code.
|
||||
// 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
|
@ -22,7 +22,6 @@
|
||||
#include <boost/mysql/error_with_diagnostics.hpp>
|
||||
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/asio/cancel_after.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
|
@ -173,16 +173,8 @@ asio::awaitable<void> orders::run_server(mysql::connection_pool& pool, unsigned
|
||||
// Start the acceptor loop
|
||||
while (true)
|
||||
{
|
||||
// Accept a new connection. asio::as_tuple prevents async_accept
|
||||
// from throwing exceptions on failure.
|
||||
auto [ec, sock] = co_await acc.async_accept(asio::as_tuple);
|
||||
|
||||
// If there was an error accepting the connection, exit our loop
|
||||
if (ec)
|
||||
{
|
||||
log_error("Error while accepting connection", ec);
|
||||
co_return;
|
||||
}
|
||||
// Accept a new connection
|
||||
asio::ip::tcp::socket sock = co_await acc.async_accept();
|
||||
|
||||
// Function implementing our session logic.
|
||||
// Takes ownership of the socket.
|
||||
|
@ -12,6 +12,11 @@
|
||||
//
|
||||
// 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>
|
||||
|
||||
@ -21,12 +26,6 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// Contains type definitions used in the REST API and database code.
|
||||
// We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection
|
||||
// capabilities to our types. This allows using Boost.MySQL
|
||||
// static interface (i.e. static_results<T>) to parse query results,
|
||||
// and Boost.JSON automatic serialization/deserialization.
|
||||
|
||||
namespace orders {
|
||||
|
||||
// A product object, as defined in the database and in the GET /products endpoint
|
||||
|
@ -99,12 +99,12 @@ endif()
|
||||
|
||||
# Advanced
|
||||
add_example(
|
||||
http_server_cpp11_coroutines
|
||||
http_server_cpp14_coroutines
|
||||
SOURCES
|
||||
3_advanced/http_server_cpp11_coroutines/repository.cpp
|
||||
3_advanced/http_server_cpp11_coroutines/handle_request.cpp
|
||||
3_advanced/http_server_cpp11_coroutines/server.cpp
|
||||
3_advanced/http_server_cpp11_coroutines/main.cpp
|
||||
3_advanced/http_server_cpp14_coroutines/repository.cpp
|
||||
3_advanced/http_server_cpp14_coroutines/handle_request.cpp
|
||||
3_advanced/http_server_cpp14_coroutines/server.cpp
|
||||
3_advanced/http_server_cpp14_coroutines/main.cpp
|
||||
LIBS
|
||||
Boost::context
|
||||
Boost::json
|
||||
|
@ -105,11 +105,11 @@ run_example unix_socket : 2_simple/unix_socket.cpp
|
||||
;
|
||||
|
||||
# Advanced
|
||||
run_example http_server_cpp11_coroutines :
|
||||
3_advanced/http_server_cpp11_coroutines/main.cpp
|
||||
3_advanced/http_server_cpp11_coroutines/repository.cpp
|
||||
3_advanced/http_server_cpp11_coroutines/handle_request.cpp
|
||||
3_advanced/http_server_cpp11_coroutines/server.cpp
|
||||
run_example http_server_cpp14_coroutines :
|
||||
3_advanced/http_server_cpp14_coroutines/main.cpp
|
||||
3_advanced/http_server_cpp14_coroutines/repository.cpp
|
||||
3_advanced/http_server_cpp14_coroutines/handle_request.cpp
|
||||
3_advanced/http_server_cpp14_coroutines/server.cpp
|
||||
/boost/mysql/test//boost_context_lib
|
||||
/boost/mysql/test//boost_json_lib
|
||||
/boost/url//boost_url
|
||||
|
@ -27,14 +27,14 @@ def _random_string() -> str:
|
||||
|
||||
|
||||
def _call_endpoints(port: int):
|
||||
base_url = 'http://127.0.0.1:{}'.format(port)
|
||||
url = 'http://127.0.0.1:{}/notes'.format(port)
|
||||
|
||||
# Create a note
|
||||
note_unique = _random_string()
|
||||
title = 'My note {}'.format(note_unique)
|
||||
content = 'This is a note about {}'.format(note_unique)
|
||||
res = requests.post(
|
||||
'{}/notes'.format(base_url),
|
||||
url,
|
||||
json={'title': title, 'content': content}
|
||||
)
|
||||
_check_response(res)
|
||||
@ -44,7 +44,7 @@ def _call_endpoints(port: int):
|
||||
assert note['note']['content'] == content
|
||||
|
||||
# Retrieve all notes
|
||||
res = requests.get('{}/notes'.format(base_url))
|
||||
res = requests.get(url)
|
||||
_check_response(res)
|
||||
all_notes = res.json()
|
||||
assert len([n for n in all_notes['notes'] if n['id'] == note_id]) == 1
|
||||
@ -54,7 +54,8 @@ def _call_endpoints(port: int):
|
||||
title = 'Edited {}'.format(note_unique)
|
||||
content = 'This is a note an edit on {}'.format(note_unique)
|
||||
res = requests.put(
|
||||
'{}/notes/{}'.format(base_url, note_id),
|
||||
url,
|
||||
params={'id': note_id},
|
||||
json={'title': title, 'content': content}
|
||||
)
|
||||
_check_response(res)
|
||||
@ -64,7 +65,7 @@ def _call_endpoints(port: int):
|
||||
assert note['note']['content'] == content
|
||||
|
||||
# Retrieve the note
|
||||
res = requests.get('{}/notes/{}'.format(base_url, note_id))
|
||||
res = requests.get(url, params={'id': note_id})
|
||||
_check_response(res)
|
||||
note = res.json()
|
||||
assert int(note['note']['id']) == note_id
|
||||
@ -72,12 +73,12 @@ def _call_endpoints(port: int):
|
||||
assert note['note']['content'] == content
|
||||
|
||||
# Delete the note
|
||||
res = requests.delete('{}/notes/{}'.format(base_url, note_id))
|
||||
res = requests.delete(url, params={'id': note_id})
|
||||
_check_response(res)
|
||||
assert res.json()['deleted'] == True
|
||||
|
||||
# The note is not there
|
||||
res = requests.get('{}/notes/{}'.format(base_url, note_id))
|
||||
res = requests.get(url, params={'id': note_id})
|
||||
assert res.status_code == 404
|
||||
|
||||
|
||||
|
@ -142,17 +142,16 @@ ADVANCED_EXAMPLES = [
|
||||
'3_advanced/http_server_cpp20/server.cpp',
|
||||
], 'A REST API server that uses C++20 coroutines'),
|
||||
|
||||
MultiExample('http_server_cpp11_coroutines', [
|
||||
'3_advanced/http_server_cpp11_coroutines/main.cpp',
|
||||
'3_advanced/http_server_cpp11_coroutines/types.hpp',
|
||||
'3_advanced/http_server_cpp11_coroutines/repository.hpp',
|
||||
'3_advanced/http_server_cpp11_coroutines/repository.cpp',
|
||||
'3_advanced/http_server_cpp11_coroutines/handle_request.hpp',
|
||||
'3_advanced/http_server_cpp11_coroutines/handle_request.cpp',
|
||||
'3_advanced/http_server_cpp11_coroutines/server.hpp',
|
||||
'3_advanced/http_server_cpp11_coroutines/server.cpp',
|
||||
'3_advanced/http_server_cpp11_coroutines/log_error.hpp',
|
||||
], 'A REST API server that uses asio::yield_context'),
|
||||
MultiExample('http_server_cpp14_coroutines', [
|
||||
'3_advanced/http_server_cpp14_coroutines/main.cpp',
|
||||
'3_advanced/http_server_cpp14_coroutines/types.hpp',
|
||||
'3_advanced/http_server_cpp14_coroutines/repository.hpp',
|
||||
'3_advanced/http_server_cpp14_coroutines/repository.cpp',
|
||||
'3_advanced/http_server_cpp14_coroutines/handle_request.hpp',
|
||||
'3_advanced/http_server_cpp14_coroutines/handle_request.cpp',
|
||||
'3_advanced/http_server_cpp14_coroutines/server.hpp',
|
||||
'3_advanced/http_server_cpp14_coroutines/server.cpp',
|
||||
], 'A C++14 REST API server that uses asio::yield_context'),
|
||||
]
|
||||
|
||||
ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES
|
||||
|
Loading…
x
Reference in New Issue
Block a user