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/handle_request.cpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp20/server.hpp]
|
[import ../../example/3_advanced/http_server_cpp20/server.hpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp20/server.cpp]
|
[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_cpp14_coroutines/main.cpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/types.hpp]
|
[import ../../example/3_advanced/http_server_cpp14_coroutines/types.hpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/repository.hpp]
|
[import ../../example/3_advanced/http_server_cpp14_coroutines/repository.hpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/repository.cpp]
|
[import ../../example/3_advanced/http_server_cpp14_coroutines/repository.cpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/handle_request.hpp]
|
[import ../../example/3_advanced/http_server_cpp14_coroutines/handle_request.hpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/handle_request.cpp]
|
[import ../../example/3_advanced/http_server_cpp14_coroutines/handle_request.cpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/server.hpp]
|
[import ../../example/3_advanced/http_server_cpp14_coroutines/server.hpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/server.cpp]
|
[import ../../example/3_advanced/http_server_cpp14_coroutines/server.cpp]
|
||||||
[import ../../example/3_advanced/http_server_cpp11_coroutines/log_error.hpp]
|
|
||||||
[import ../../test/integration/test/snippets/prepared_statements.cpp]
|
[import ../../test/integration/test/snippets/prepared_statements.cpp]
|
||||||
[import ../../test/integration/test/snippets/sql_formatting_advanced.cpp]
|
[import ../../test/integration/test/snippets/sql_formatting_advanced.cpp]
|
||||||
[import ../../test/integration/test/snippets/connection_establishment.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].
|
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_cpp14_coroutines_server_cpp]
|
||||||
|
|
||||||
[example_http_server_cpp11_coroutines_log_error_hpp]
|
|
||||||
|
|
||||||
[endsect]
|
[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)
|
// 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
|
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_HANDLE_REQUEST_HPP
|
||||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_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
|
// File: handle_request.hpp
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <boost/asio/error.hpp>
|
#include <boost/mysql/connection_pool.hpp>
|
||||||
|
|
||||||
#include <boost/asio/spawn.hpp>
|
#include <boost/asio/spawn.hpp>
|
||||||
#include <boost/beast/http/message.hpp>
|
#include <boost/beast/http/message.hpp>
|
||||||
#include <boost/beast/http/string_body.hpp>
|
#include <boost/beast/http/string_body.hpp>
|
||||||
|
|
||||||
#include "repository.hpp"
|
|
||||||
|
|
||||||
namespace notes {
|
namespace notes {
|
||||||
|
|
||||||
// Handles an individual HTTP request, producing a response.
|
// 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::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,
|
const boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||||
note_repository repo,
|
|
||||||
boost::asio::yield_context yield
|
boost::asio::yield_context yield
|
||||||
);
|
);
|
||||||
|
|
@ -6,33 +6,31 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <boost/mysql/static_results.hpp>
|
#include <boost/mysql/static_results.hpp>
|
||||||
|
|
||||||
#include "log_error.hpp"
|
|
||||||
|
|
||||||
#ifdef BOOST_MYSQL_CXX14
|
#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.
|
* Implements a HTTP REST API using Boost.MySQL and Boost.Beast.
|
||||||
* The server is asynchronous and uses asio::yield_context as its completion
|
* 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.
|
* It implements a minimal REST API to manage notes.
|
||||||
* A note is a simple object containing a user-defined title and content.
|
* A note is a simple object containing a user-defined title and content.
|
||||||
* The REST API offers CRUD operations on such objects:
|
* The REST API offers CRUD operations on such objects:
|
||||||
* POST /notes Creates a new note.
|
* POST /notes Creates a new note.
|
||||||
* GET /notes Retrieves all notes.
|
* GET /notes Retrieves all notes.
|
||||||
* GET /notes/<id> Retrieves a single note.
|
* GET /notes?id=<id> Retrieves a single note.
|
||||||
* PUT /notes/<id> Replaces a note, changing its title and content.
|
* PUT /notes?id=<id> Replaces a note, changing its title and content.
|
||||||
* DELETE /notes/<id> Deletes a note.
|
* DELETE /notes?id=<id> Deletes a note.
|
||||||
*
|
*
|
||||||
* Notes are stored in MySQL. The note_repository class encapsulates
|
* 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,
|
* 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
|
* 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.
|
* This example requires linking to Boost::context, Boost::json and Boost::url.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -41,11 +39,12 @@
|
|||||||
#include <boost/mysql/pool_params.hpp>
|
#include <boost/mysql/pool_params.hpp>
|
||||||
|
|
||||||
#include <boost/asio/detached.hpp>
|
#include <boost/asio/detached.hpp>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
#include <boost/asio/signal_set.hpp>
|
#include <boost/asio/signal_set.hpp>
|
||||||
|
#include <boost/asio/spawn.hpp>
|
||||||
#include <boost/asio/thread_pool.hpp>
|
#include <boost/asio/thread_pool.hpp>
|
||||||
#include <boost/system/error_code.hpp>
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@ -53,11 +52,10 @@
|
|||||||
|
|
||||||
#include "server.hpp"
|
#include "server.hpp"
|
||||||
|
|
||||||
|
namespace asio = boost::asio;
|
||||||
|
namespace mysql = boost::mysql;
|
||||||
using namespace notes;
|
using namespace notes;
|
||||||
|
|
||||||
// The number of threads to use
|
|
||||||
static constexpr std::size_t num_threads = 5;
|
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
// Check command line arguments.
|
// Check command line arguments.
|
||||||
@ -74,14 +72,12 @@ int main(int argc, char* argv[])
|
|||||||
auto port = static_cast<unsigned short>(std::stoi(argv[4]));
|
auto port = static_cast<unsigned short>(std::stoi(argv[4]));
|
||||||
|
|
||||||
// An event loop, where the application will run.
|
// An event loop, where the application will run.
|
||||||
// We will use the main thread to run the pool, too, so we use
|
asio::io_context ctx;
|
||||||
// one thread less than configured
|
|
||||||
boost::asio::thread_pool th_pool(num_threads - 1);
|
|
||||||
|
|
||||||
// Configuration for the connection pool
|
// 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
|
// 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
|
// Authenticate using the given username
|
||||||
mysql_username,
|
mysql_username,
|
||||||
@ -93,47 +89,40 @@ int main(int argc, char* argv[])
|
|||||||
"boost_mysql_examples",
|
"boost_mysql_examples",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Using thread_safe will make the pool thread-safe by internally
|
// Create the connection pool.
|
||||||
// creating and using a strand.
|
// shared_state contains all singleton objects that our application may need.
|
||||||
// This allows us to share the pool between sessions, which may run
|
// Coroutines created by asio::spawn might survive until the io_context is destroyed
|
||||||
// concurrently, on different threads.
|
// (even after io_context::stop() has been called). This is not the case for callbacks
|
||||||
pool_prms.thread_safe = true;
|
// 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)));
|
||||||
// Create the connection pool
|
|
||||||
auto shared_st = std::make_shared<shared_state>(
|
|
||||||
boost::mysql::connection_pool(th_pool, std::move(pool_prms))
|
|
||||||
);
|
|
||||||
|
|
||||||
// A signal_set allows us to intercept SIGINT and SIGTERM and
|
|
||||||
// exit gracefully
|
|
||||||
boost::asio::signal_set signals{th_pool.get_executor(), SIGINT, SIGTERM};
|
|
||||||
|
|
||||||
// Launch the MySQL pool
|
// Launch the MySQL pool
|
||||||
shared_st->pool.async_run(boost::asio::detached);
|
st->pool.async_run(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();
|
|
||||||
|
|
||||||
|
// 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
|
// 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
|
// Launch the server. This will run until the context is stopped
|
||||||
// until stop() is called
|
asio::spawn(
|
||||||
th_pool.attach();
|
// Spawn the coroutine in the io_context
|
||||||
|
ctx,
|
||||||
|
|
||||||
// Wait until all threads have exited
|
// The coroutine to run
|
||||||
th_pool.join();
|
[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;
|
std::cout << "Server exiting" << std::endl;
|
||||||
|
|
@ -6,15 +6,21 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <boost/mysql/static_results.hpp>
|
#include <boost/mysql/static_results.hpp>
|
||||||
|
|
||||||
#ifdef BOOST_MYSQL_CXX14
|
#ifdef BOOST_MYSQL_CXX14
|
||||||
|
|
||||||
//[example_http_server_cpp11_coroutines_repository_cpp
|
//[example_http_server_cpp14_coroutines_repository_cpp
|
||||||
//
|
//
|
||||||
// File: 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/static_results.hpp>
|
||||||
#include <boost/mysql/string_view.hpp>
|
#include <boost/mysql/string_view.hpp>
|
||||||
#include <boost/mysql/with_diagnostics.hpp>
|
#include <boost/mysql/with_diagnostics.hpp>
|
||||||
@ -27,20 +33,12 @@
|
|||||||
#include "repository.hpp"
|
#include "repository.hpp"
|
||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
|
|
||||||
using namespace notes;
|
namespace asio = boost::asio;
|
||||||
namespace mysql = boost::mysql;
|
namespace mysql = boost::mysql;
|
||||||
|
using namespace notes;
|
||||||
using mysql::with_diagnostics;
|
using mysql::with_diagnostics;
|
||||||
|
|
||||||
// SQL code to create the notes table is located under $REPO_ROOT/example/db_setup.sql
|
std::vector<note_t> note_repository::get_notes(asio::yield_context yield)
|
||||||
// The table looks like this:
|
|
||||||
//
|
|
||||||
// CREATE TABLE notes(
|
|
||||||
// id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
// title TEXT NOT NULL,
|
|
||||||
// content TEXT NOT NULL
|
|
||||||
// );
|
|
||||||
|
|
||||||
std::vector<note_t> note_repository::get_notes(boost::asio::yield_context yield)
|
|
||||||
{
|
{
|
||||||
// Get a fresh connection from the pool. This returns a pooled_connection object,
|
// 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
|
// 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.
|
// 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,
|
// 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
|
// 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]);
|
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,
|
// 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
|
// 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.
|
// 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,
|
std::int64_t note_id,
|
||||||
string_view title,
|
mysql::string_view title,
|
||||||
string_view content,
|
mysql::string_view content,
|
||||||
boost::asio::yield_context yield
|
asio::yield_context yield
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Get a fresh connection from the pool. This returns a pooled_connection object,
|
// 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};
|
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,
|
// 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
|
// 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)
|
// 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
|
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_REPOSITORY_HPP
|
||||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_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
|
// File: repository.hpp
|
||||||
//
|
//
|
||||||
@ -16,7 +16,6 @@
|
|||||||
#include <boost/mysql/connection_pool.hpp>
|
#include <boost/mysql/connection_pool.hpp>
|
||||||
#include <boost/mysql/string_view.hpp>
|
#include <boost/mysql/string_view.hpp>
|
||||||
|
|
||||||
#include <boost/asio/error.hpp>
|
|
||||||
#include <boost/asio/spawn.hpp>
|
#include <boost/asio/spawn.hpp>
|
||||||
#include <boost/optional/optional.hpp>
|
#include <boost/optional/optional.hpp>
|
||||||
|
|
||||||
@ -26,41 +25,40 @@
|
|||||||
|
|
||||||
namespace notes {
|
namespace notes {
|
||||||
|
|
||||||
using boost::optional;
|
// Encapsulates database logic.
|
||||||
using boost::mysql::string_view;
|
// All operations are async, and use stackful coroutines (asio::yield_context).
|
||||||
|
|
||||||
// A lightweight wrapper around a connection_pool that allows
|
|
||||||
// creating, updating, retrieving and deleting notes in MySQL.
|
|
||||||
// This class encapsulates the database logic.
|
|
||||||
// All operations are async, and use stackful coroutines (boost::asio::yield_context).
|
|
||||||
// If the database can't be contacted, or unexpected database errors are found,
|
// 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
|
class note_repository
|
||||||
{
|
{
|
||||||
boost::mysql::connection_pool& pool_;
|
boost::mysql::connection_pool& pool_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Constructor (this is a cheap-to-construct object)
|
// 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
|
// Retrieves all notes present in the database
|
||||||
std::vector<note_t> get_notes(boost::asio::yield_context yield);
|
std::vector<note_t> get_notes(boost::asio::yield_context yield);
|
||||||
|
|
||||||
// Retrieves a single note by ID. Returns an empty optional
|
// Retrieves a single note by ID. Returns an empty optional
|
||||||
// if no note with the given ID is present in the database.
|
// 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.
|
// Creates a new note in the database with the given components.
|
||||||
// Returns the newly created note, including the newly allocated ID.
|
// 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
|
// Replaces the note identified by note_id, setting its components to the
|
||||||
// ones passed. Returns the updated note. If no note with ID matching
|
// ones passed. Returns the updated note. If no note with ID matching
|
||||||
// note_id can be found, an empty optional is returned.
|
// 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,
|
std::int64_t note_id,
|
||||||
string_view title,
|
boost::mysql::string_view title,
|
||||||
string_view content,
|
boost::mysql::string_view content,
|
||||||
boost::asio::yield_context yield
|
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)
|
// 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
|
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_SERVER_HPP
|
||||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_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
|
// File: server.hpp
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <boost/mysql/connection_pool.hpp>
|
#include <boost/mysql/connection_pool.hpp>
|
||||||
|
|
||||||
#include <boost/asio/any_io_executor.hpp>
|
#include <boost/asio/spawn.hpp>
|
||||||
#include <boost/asio/io_context.hpp>
|
|
||||||
#include <boost/system/error_code.hpp>
|
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@ -32,19 +30,14 @@ struct shared_state
|
|||||||
{
|
{
|
||||||
boost::mysql::connection_pool pool;
|
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),
|
// 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
|
// throws an exception. The server runs until the underlying execution
|
||||||
// where the server should run. The server is run until the underlying execution
|
|
||||||
// context is stopped.
|
// context is stopped.
|
||||||
boost::system::error_code launch_server(
|
void run_server(std::shared_ptr<shared_state> st, unsigned short port, boost::asio::yield_context yield);
|
||||||
boost::asio::any_io_executor ex,
|
|
||||||
std::shared_ptr<shared_state> state,
|
|
||||||
unsigned short port
|
|
||||||
);
|
|
||||||
|
|
||||||
} // namespace notes
|
} // namespace notes
|
||||||
|
|
@ -5,27 +5,25 @@
|
|||||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
// 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
|
#ifndef BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP14_COROUTINES_TYPES_HPP
|
||||||
#define BOOST_MYSQL_EXAMPLE_3_ADVANCED_HTTP_SERVER_CPP11_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
|
// 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.
|
// Contains type definitions used in the REST API and database code.
|
||||||
// We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection
|
// We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection
|
||||||
// capabilities to our types. This allows using Boost.MySQL
|
// capabilities to our types. This allows using Boost.MySQL
|
||||||
// static interface (i.e. static_results<T>) to parse query results,
|
// static interface (i.e. static_results<T>) to parse query results,
|
||||||
// and Boost.JSON automatic serialization/deserialization.
|
// and Boost.JSON automatic serialization/deserialization.
|
||||||
|
|
||||||
|
#include <boost/describe/class.hpp>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace notes {
|
namespace notes {
|
||||||
|
|
||||||
struct note_t
|
struct note_t
|
@ -22,7 +22,6 @@
|
|||||||
#include <boost/mysql/error_with_diagnostics.hpp>
|
#include <boost/mysql/error_with_diagnostics.hpp>
|
||||||
|
|
||||||
#include <boost/asio/awaitable.hpp>
|
#include <boost/asio/awaitable.hpp>
|
||||||
#include <boost/asio/cancel_after.hpp>
|
|
||||||
#include <boost/beast/http/message.hpp>
|
#include <boost/beast/http/message.hpp>
|
||||||
#include <boost/beast/http/status.hpp>
|
#include <boost/beast/http/status.hpp>
|
||||||
#include <boost/beast/http/string_body.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
|
// Start the acceptor loop
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Accept a new connection. asio::as_tuple prevents async_accept
|
// Accept a new connection
|
||||||
// from throwing exceptions on failure.
|
asio::ip::tcp::socket sock = co_await acc.async_accept();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function implementing our session logic.
|
// Function implementing our session logic.
|
||||||
// Takes ownership of the socket.
|
// Takes ownership of the socket.
|
||||||
|
@ -12,6 +12,11 @@
|
|||||||
//
|
//
|
||||||
// File: types.hpp
|
// 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 <boost/describe/class.hpp>
|
||||||
|
|
||||||
@ -21,12 +26,6 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#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 {
|
namespace orders {
|
||||||
|
|
||||||
// A product object, as defined in the database and in the GET /products endpoint
|
// A product object, as defined in the database and in the GET /products endpoint
|
||||||
|
@ -99,12 +99,12 @@ endif()
|
|||||||
|
|
||||||
# Advanced
|
# Advanced
|
||||||
add_example(
|
add_example(
|
||||||
http_server_cpp11_coroutines
|
http_server_cpp14_coroutines
|
||||||
SOURCES
|
SOURCES
|
||||||
3_advanced/http_server_cpp11_coroutines/repository.cpp
|
3_advanced/http_server_cpp14_coroutines/repository.cpp
|
||||||
3_advanced/http_server_cpp11_coroutines/handle_request.cpp
|
3_advanced/http_server_cpp14_coroutines/handle_request.cpp
|
||||||
3_advanced/http_server_cpp11_coroutines/server.cpp
|
3_advanced/http_server_cpp14_coroutines/server.cpp
|
||||||
3_advanced/http_server_cpp11_coroutines/main.cpp
|
3_advanced/http_server_cpp14_coroutines/main.cpp
|
||||||
LIBS
|
LIBS
|
||||||
Boost::context
|
Boost::context
|
||||||
Boost::json
|
Boost::json
|
||||||
|
@ -105,11 +105,11 @@ run_example unix_socket : 2_simple/unix_socket.cpp
|
|||||||
;
|
;
|
||||||
|
|
||||||
# Advanced
|
# Advanced
|
||||||
run_example http_server_cpp11_coroutines :
|
run_example http_server_cpp14_coroutines :
|
||||||
3_advanced/http_server_cpp11_coroutines/main.cpp
|
3_advanced/http_server_cpp14_coroutines/main.cpp
|
||||||
3_advanced/http_server_cpp11_coroutines/repository.cpp
|
3_advanced/http_server_cpp14_coroutines/repository.cpp
|
||||||
3_advanced/http_server_cpp11_coroutines/handle_request.cpp
|
3_advanced/http_server_cpp14_coroutines/handle_request.cpp
|
||||||
3_advanced/http_server_cpp11_coroutines/server.cpp
|
3_advanced/http_server_cpp14_coroutines/server.cpp
|
||||||
/boost/mysql/test//boost_context_lib
|
/boost/mysql/test//boost_context_lib
|
||||||
/boost/mysql/test//boost_json_lib
|
/boost/mysql/test//boost_json_lib
|
||||||
/boost/url//boost_url
|
/boost/url//boost_url
|
||||||
|
@ -27,14 +27,14 @@ def _random_string() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _call_endpoints(port: int):
|
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
|
# Create a note
|
||||||
note_unique = _random_string()
|
note_unique = _random_string()
|
||||||
title = 'My note {}'.format(note_unique)
|
title = 'My note {}'.format(note_unique)
|
||||||
content = 'This is a note about {}'.format(note_unique)
|
content = 'This is a note about {}'.format(note_unique)
|
||||||
res = requests.post(
|
res = requests.post(
|
||||||
'{}/notes'.format(base_url),
|
url,
|
||||||
json={'title': title, 'content': content}
|
json={'title': title, 'content': content}
|
||||||
)
|
)
|
||||||
_check_response(res)
|
_check_response(res)
|
||||||
@ -44,7 +44,7 @@ def _call_endpoints(port: int):
|
|||||||
assert note['note']['content'] == content
|
assert note['note']['content'] == content
|
||||||
|
|
||||||
# Retrieve all notes
|
# Retrieve all notes
|
||||||
res = requests.get('{}/notes'.format(base_url))
|
res = requests.get(url)
|
||||||
_check_response(res)
|
_check_response(res)
|
||||||
all_notes = res.json()
|
all_notes = res.json()
|
||||||
assert len([n for n in all_notes['notes'] if n['id'] == note_id]) == 1
|
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)
|
title = 'Edited {}'.format(note_unique)
|
||||||
content = 'This is a note an edit on {}'.format(note_unique)
|
content = 'This is a note an edit on {}'.format(note_unique)
|
||||||
res = requests.put(
|
res = requests.put(
|
||||||
'{}/notes/{}'.format(base_url, note_id),
|
url,
|
||||||
|
params={'id': note_id},
|
||||||
json={'title': title, 'content': content}
|
json={'title': title, 'content': content}
|
||||||
)
|
)
|
||||||
_check_response(res)
|
_check_response(res)
|
||||||
@ -64,7 +65,7 @@ def _call_endpoints(port: int):
|
|||||||
assert note['note']['content'] == content
|
assert note['note']['content'] == content
|
||||||
|
|
||||||
# Retrieve the note
|
# Retrieve the note
|
||||||
res = requests.get('{}/notes/{}'.format(base_url, note_id))
|
res = requests.get(url, params={'id': note_id})
|
||||||
_check_response(res)
|
_check_response(res)
|
||||||
note = res.json()
|
note = res.json()
|
||||||
assert int(note['note']['id']) == note_id
|
assert int(note['note']['id']) == note_id
|
||||||
@ -72,12 +73,12 @@ def _call_endpoints(port: int):
|
|||||||
assert note['note']['content'] == content
|
assert note['note']['content'] == content
|
||||||
|
|
||||||
# Delete the note
|
# Delete the note
|
||||||
res = requests.delete('{}/notes/{}'.format(base_url, note_id))
|
res = requests.delete(url, params={'id': note_id})
|
||||||
_check_response(res)
|
_check_response(res)
|
||||||
assert res.json()['deleted'] == True
|
assert res.json()['deleted'] == True
|
||||||
|
|
||||||
# The note is not there
|
# 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
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,17 +142,16 @@ ADVANCED_EXAMPLES = [
|
|||||||
'3_advanced/http_server_cpp20/server.cpp',
|
'3_advanced/http_server_cpp20/server.cpp',
|
||||||
], 'A REST API server that uses C++20 coroutines'),
|
], 'A REST API server that uses C++20 coroutines'),
|
||||||
|
|
||||||
MultiExample('http_server_cpp11_coroutines', [
|
MultiExample('http_server_cpp14_coroutines', [
|
||||||
'3_advanced/http_server_cpp11_coroutines/main.cpp',
|
'3_advanced/http_server_cpp14_coroutines/main.cpp',
|
||||||
'3_advanced/http_server_cpp11_coroutines/types.hpp',
|
'3_advanced/http_server_cpp14_coroutines/types.hpp',
|
||||||
'3_advanced/http_server_cpp11_coroutines/repository.hpp',
|
'3_advanced/http_server_cpp14_coroutines/repository.hpp',
|
||||||
'3_advanced/http_server_cpp11_coroutines/repository.cpp',
|
'3_advanced/http_server_cpp14_coroutines/repository.cpp',
|
||||||
'3_advanced/http_server_cpp11_coroutines/handle_request.hpp',
|
'3_advanced/http_server_cpp14_coroutines/handle_request.hpp',
|
||||||
'3_advanced/http_server_cpp11_coroutines/handle_request.cpp',
|
'3_advanced/http_server_cpp14_coroutines/handle_request.cpp',
|
||||||
'3_advanced/http_server_cpp11_coroutines/server.hpp',
|
'3_advanced/http_server_cpp14_coroutines/server.hpp',
|
||||||
'3_advanced/http_server_cpp11_coroutines/server.cpp',
|
'3_advanced/http_server_cpp14_coroutines/server.cpp',
|
||||||
'3_advanced/http_server_cpp11_coroutines/log_error.hpp',
|
], 'A C++14 REST API server that uses asio::yield_context'),
|
||||||
], 'A REST API server that uses asio::yield_context'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES
|
ALL_EXAMPLES = TUTORIALS + SIMPLE_EXAMPLES + ADVANCED_EXAMPLES
|
||||||
|
Loading…
x
Reference in New Issue
Block a user