File: server.cpp

package info (click to toggle)
boost1.88 1.88.0-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 576,932 kB
  • sloc: cpp: 4,149,234; xml: 136,789; ansic: 35,092; python: 33,910; asm: 5,698; sh: 4,604; ada: 1,681; makefile: 1,633; pascal: 1,139; perl: 1,124; sql: 640; yacc: 478; ruby: 271; java: 77; lisp: 24; csh: 6
file content (220 lines) | stat: -rw-r--r-- 7,616 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//
// 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/asio/awaitable.hpp>
#include <boost/pfr/config.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT) && BOOST_PFR_CORE_NAME_ENABLED

//[example_http_server_cpp20_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/mysql/connection_pool.hpp>
#include <boost/mysql/error_code.hpp>

#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/this_coro.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/verb.hpp>
#include <boost/beast/http/write.hpp>

#include <cstdlib>
#include <exception>
#include <iostream>
#include <string_view>

#include "error.hpp"
#include "handle_request.hpp"
#include "server.hpp"

namespace asio = boost::asio;
namespace http = boost::beast::http;
namespace mysql = boost::mysql;

namespace {

// Runs a single HTTP session until the client closes the connection.
// This coroutine will be spawned on a strand, to prevent data races.
asio::awaitable<void> run_http_session(asio::ip::tcp::socket sock, mysql::connection_pool& pool)
{
    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(co_await asio::this_coro::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. redirect_error prevents exceptions from being thrown
        // on error. We use cancel_after to set a timeout for the overall read operation.
        co_await http::async_read(
            sock,
            buff,
            parser.get(),
            asio::cancel_after(timer, 60s, asio::redirect_error(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
                orders::log_error("Error reading HTTP request: ", ec);
            }
            co_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 = co_await asio::co_spawn(
            // Use the same executor as this coroutine (it will be a strand)
            co_await asio::this_coro::executor,

            // The logic to invoke
            [&] { return orders::handle_request(request, pool); },

            // Completion token. Returns an object that can be co_await'ed
            asio::cancel_after(timer, 30s)
        );

        // 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
        co_await http::async_write(sock, response, asio::cancel_after(timer, 60s, asio::redirect_error(ec)));
        if (ec)
        {
            orders::log_error("Error writing HTTP response: ", ec);
            co_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);
            co_return;
        }
    }
}

}  // namespace

asio::awaitable<void> orders::run_server(mysql::connection_pool& pool, unsigned short port)
{
    // An object that allows us to accept incoming TCP connections
    asio::ip::tcp::acceptor acc(co_await asio::this_coro::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 = co_await acc.async_accept();

        // Function implementing our session logic.
        // Takes ownership of the socket.
        // Having this as a named variable workarounds a gcc bug
        // (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107288)
        auto session_logic = [&pool, socket = std::move(sock)]() mutable {
            return run_http_session(std::move(socket), pool);
        };

        // Launch a new session for this connection. Each session gets its
        // own coroutine, so we can get back to listening for new connections.
        asio::co_spawn(
            // Every session gets its own strand. This prevents data races.
            asio::make_strand(co_await asio::this_coro::executor),

            // The actual coroutine
            std::move(session_logic),

            // 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)
                    {
                        auto guard = lock_cerr();
                        std::cerr << "Uncaught error in a session: " << exc.what() << std::endl;
                    }
                }
            }
        );
    }
}

//]

#endif