Skip to content

Latest commit

 

History

History
860 lines (645 loc) · 29.1 KB

File metadata and controls

860 lines (645 loc) · 29.1 KB

🔝 Retour au Sommaire

22.4.2 — Boost.Asio : Écosystème complet

Section 22.4 : Librairies réseau modernes


Introduction

Standalone Asio (section 22.4.1) offre une base réseau solide — sockets TCP/UDP, timers, résolution DNS, coroutines. Mais dès que vos besoins dépassent le transport brut, les questions arrivent : comment faire du HTTP ? Du WebSocket ? Du TLS/SSL ? Comment sérialiser mes données ? Comment gérer des pools de connexions ?

C'est là qu'intervient Boost.Asio et, plus largement, l'écosystème Boost. Boost.Asio est le même moteur que Standalone Asio (même auteur, même code, même architecture), mais intégré dans Boost avec accès à un écosystème de librairies complémentaires qui couvrent les besoins d'un serveur réseau complet.

La pièce maîtresse de cet écosystème est Boost.Beast — une librairie HTTP et WebSocket construite directement au-dessus de Boost.Asio. C'est elle qui transforme Asio d'une librairie de transport en une plateforme capable de servir des API REST et des connexions WebSocket temps réel.


Ce que Boost ajoute à Asio

Le noyau : même code, autre namespace

Le code réseau de Boost.Asio est identique à celui de Standalone Asio. La différence se résume essentiellement à :

// Standalone Asio
#include <asio.hpp>
asio::io_context ctx;  
asio::ip::tcp::socket socket(ctx);  

// Boost.Asio
#include <boost/asio.hpp>
boost::asio::io_context ctx;  
boost::asio::ip::tcp::socket socket(ctx);  

L'espace de noms passe de asio:: à boost::asio::, les headers passent de <asio/...> à <boost/asio/...>, et les types d'erreur utilisent boost::system::error_code au lieu de std::error_code (bien que les versions récentes supportent les deux).

Tout ce que vous avez appris en section 22.4.1 — io_context, coroutines, callbacks, timers, strands — s'applique identiquement avec Boost.Asio.

L'écosystème : les librairies complémentaires

La valeur ajoutée de Boost réside dans les librairies qui s'intègrent nativement avec Boost.Asio :

Librairie Rôle Pertinence réseau
Boost.Beast HTTP/1.1, WebSocket Serveurs et clients HTTP, API REST, temps réel
Boost.Asio SSL TLS/SSL via OpenSSL HTTPS, connexions sécurisées
Boost.JSON Parsing/sérialisation JSON Corps des requêtes/réponses HTTP
Boost.URL Parsing d'URLs Routage, extraction de paramètres
Boost.Log Logging structuré Observabilité serveur
Boost.Cobalt Coroutines simplifiées Alternative aux awaitables natifs d'Asio

Installation

Via Conan 2

# conanfile.txt
[requires]
boost/1.86.0

[generators]
CMakeDeps  
CMakeToolchain  
conan install . --output-folder=build --build=missing

Via vcpkg

vcpkg install boost-asio boost-beast boost-json boost-url

Installation système (Ubuntu)

sudo apt install libboost-all-dev libssl-dev

Le paquet libssl-dev est nécessaire pour le support TLS/SSL.

Configuration CMake

cmake_minimum_required(VERSION 3.20)  
project(boost_asio_demo LANGUAGES CXX)  

set(CMAKE_CXX_STANDARD 20)  
set(CMAKE_CXX_STANDARD_REQUIRED ON)  

find_package(Boost REQUIRED COMPONENTS system)  
find_package(OpenSSL REQUIRED)  
find_package(Threads REQUIRED)  

add_executable(http_server http_server.cpp)  
target_link_libraries(http_server PRIVATE  
    Boost::system
    OpenSSL::SSL
    OpenSSL::Crypto
    Threads::Threads
)

💡 Boost.Asio et Boost.Beast sont largement header-only. Le seul composant compilé nécessaire est Boost::system (pour boost::system::error_code). Avec Boost 1.85+, même cette dépendance devient optionnelle si vous utilisez les std::error_code natifs.


Boost.Beast : HTTP et WebSocket

Beast est la raison principale de choisir Boost.Asio plutôt que Standalone Asio. C'est une librairie bas niveau mais complète pour HTTP/1.1 et WebSocket, conçue pour fonctionner au-dessus de n'importe quel stream Asio (TCP, SSL, ou custom).

Philosophie de Beast

Beast n'est pas un framework web à la Express.js ou Flask. C'est une librairie de protocole : elle sait parser et générer des messages HTTP, effectuer le handshake WebSocket, et gérer les frames — mais elle ne gère pas le routage, les middlewares, ou les templates. C'est à vous (ou à un framework de plus haut niveau) de structurer l'application.

Ce positionnement bas niveau est un choix délibéré : Beast est conçu pour être la brique de construction d'outils HTTP/WebSocket performants et spécifiques, pas un framework généraliste.

Types fondamentaux de Beast

#include <boost/beast.hpp>

namespace beast = boost::beast;  
namespace http = beast::http;  
namespace websocket = beast::websocket;  

Les types HTTP principaux :

// Requête HTTP (méthode, cible, version, headers, body)
http::request<http::string_body> req;  
req.method(http::verb::get);  
req.target("/api/status");  
req.version(11);  // HTTP/1.1  
req.set(http::field::host, "example.com");  
req.set(http::field::user_agent, "Beast/1.0");  

// Réponse HTTP
http::response<http::string_body> res;  
res.result(http::status::ok);  
res.version(11);  
res.set(http::field::content_type, "application/json");  
res.body() = R"({"status": "ok"})";  
res.prepare_payload();  // Calcule Content-Length automatiquement  

Le template http::request<Body> et http::response<Body> est paramétré par le type de corps. Beast fournit plusieurs implémentations :

Body type Usage
string_body Corps stocké comme std::string — le plus courant
dynamic_body Corps stocké dans un beast::flat_buffer dynamique
file_body Corps lu/écrit directement depuis/vers un fichier — zéro copie
empty_body Pas de corps (HEAD, réponses 204)

Serveur HTTP avec Beast et coroutines

Voici un serveur HTTP complet qui répond à des requêtes GET sur plusieurs routes :

// http_server.cpp
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/beast.hpp>
#include <print>
#include <string>

namespace asio = boost::asio;  
namespace beast = boost::beast;  
namespace http = beast::http;  
using tcp = asio::ip::tcp;  
using asio::awaitable;  
using asio::use_awaitable;  

// --- Routage simple ---
http::response<http::string_body>  
handle_request(const http::request<http::string_body>& req) {  
    http::response<http::string_body> res;
    res.version(req.version());
    res.set(http::field::server, "Beast-Server/1.0");

    if (req.method() != http::verb::get) {
        res.result(http::status::method_not_allowed);
        res.set(http::field::content_type, "text/plain");
        res.body() = "Method not allowed";
        res.prepare_payload();
        return res;
    }

    auto target = req.target();

    if (target == "/") {
        res.result(http::status::ok);
        res.set(http::field::content_type, "text/plain");
        res.body() = "Hello from Boost.Beast!";
    }
    else if (target == "/api/status") {
        res.result(http::status::ok);
        res.set(http::field::content_type, "application/json");
        res.body() = R"({"status":"ok","connections":42})";
    }
    else if (target == "/api/health") {
        res.result(http::status::ok);
        res.set(http::field::content_type, "application/json");
        res.body() = R"({"healthy":true})";
    }
    else {
        res.result(http::status::not_found);
        res.set(http::field::content_type, "text/plain");
        res.body() = "Not found: " + std::string(target);
    }

    res.prepare_payload();
    return res;
}

// --- Session HTTP (une par connexion) ---
awaitable<void> handle_session(tcp::socket socket) {
    beast::flat_buffer buffer;  // Buffer de lecture persistant entre requêtes

    try {
        while (true) {
            // Lire une requête HTTP complète
            http::request<http::string_body> req;
            co_await http::async_read(socket, buffer, req, use_awaitable);

            // Vérifier si le client demande la fermeture
            bool keep_alive = req.keep_alive();

            // Traiter la requête
            auto res = handle_request(req);
            res.keep_alive(keep_alive);

            // Envoyer la réponse
            co_await http::async_write(socket, res, use_awaitable);

            if (!keep_alive) {
                break;  // Le client a demandé Connection: close
            }
        }
    } catch (const boost::system::system_error& e) {
        if (e.code() != beast::errc::not_connected &&
            e.code() != asio::error::eof) {
            std::println(stderr, "Session error: {}", e.what());
        }
    }

    // Fermeture propre TCP
    beast::error_code ec;
    socket.shutdown(tcp::socket::shutdown_send, ec);
}

// --- Listener ---
awaitable<void> listener(tcp::acceptor acceptor) {
    while (true) {
        auto [ec, socket] = co_await acceptor.async_accept(
            asio::as_tuple(use_awaitable));

        if (ec) {
            if (ec == asio::error::operation_aborted) break;
            std::println(stderr, "Accept error: {}", ec.message());
            continue;
        }

        co_spawn(acceptor.get_executor(),
                 handle_session(std::move(socket)),
                 asio::detached);
    }
}

int main() {
    try {
        asio::io_context ctx;

        asio::signal_set signals(ctx, SIGINT, SIGTERM);
        signals.async_wait([&ctx](auto, auto) { ctx.stop(); });

        tcp::acceptor acceptor(ctx);
        acceptor.open(tcp::v6());
        acceptor.set_option(tcp::acceptor::reuse_address(true));
        acceptor.set_option(asio::ip::v6_only(false));
        acceptor.bind(tcp::endpoint(tcp::v6(), 8080));
        acceptor.listen();

        std::println("Serveur HTTP sur http://localhost:8080/");

        co_spawn(ctx, listener(std::move(acceptor)), asio::detached);
        ctx.run();

    } catch (const std::exception& e) {
        std::println(stderr, "Fatal: {}", e.what());
        return 1;
    }
}

Test avec curl

$ curl http://localhost:8080/
Hello from Boost.Beast!

$ curl http://localhost:8080/api/status
{"status":"ok","connections":42}

$ curl http://localhost:8080/api/health
{"healthy":true}

$ curl http://localhost:8080/nonexistent
Not found: /nonexistent

$ curl -X POST http://localhost:8080/
Method not allowed

Analyse du code

http::async_read — Lit une requête HTTP complète (headers + body) depuis le socket. Beast gère en interne le parsing HTTP, la chunked transfer-encoding, et la reconstitution du message — tout ce que vous auriez dû implémenter manuellement avec des sockets bruts.

beast::flat_buffer — Buffer de lecture persistant entre les requêtes sur la même connexion. Beast peut avoir besoin de lire plus de données que nécessaire pour une requête (pipelining HTTP) — le surplus reste dans le buffer pour la requête suivante.

keep_alive — HTTP/1.1 utilise des connexions persistantes par défaut. Le serveur doit respecter le souhait du client (Connection: close ou Connection: keep-alive) pour décider de fermer ou réutiliser la connexion.

prepare_payload() — Calcule et positionne le header Content-Length automatiquement en fonction de la taille du body. Sans cet appel, le client ne saurait pas combien d'octets lire.


Client HTTP avec Beast

Beast est aussi une librairie client HTTP performante :

// http_client.cpp
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/beast.hpp>
#include <print>

namespace asio = boost::asio;  
namespace beast = boost::beast;  
namespace http = beast::http;  
using tcp = asio::ip::tcp;  
using asio::awaitable;  
using asio::use_awaitable;  

awaitable<void> fetch(asio::io_context& ctx,
                       std::string host, std::string port,
                       std::string target) {
    // Résolution + connexion
    tcp::resolver resolver(ctx);
    auto endpoints = co_await resolver.async_resolve(host, port, use_awaitable);

    tcp::socket socket(ctx);
    co_await asio::async_connect(socket, endpoints, use_awaitable);

    // Construire la requête
    http::request<http::empty_body> req{http::verb::get, target, 11};
    req.set(http::field::host, host);
    req.set(http::field::user_agent, "Beast-Client/1.0");

    // Envoyer
    co_await http::async_write(socket, req, use_awaitable);

    // Lire la réponse
    beast::flat_buffer buffer;
    http::response<http::string_body> res;
    co_await http::async_read(socket, buffer, res, use_awaitable);

    // Afficher (conversion en std::string_view car Beast
    // utilise boost::core::string_view, non formatable directement)
    std::println("HTTP {} {}", static_cast<int>(res.result()),
                 std::string_view(res.reason()));
    for (auto const& field : res) {
        std::println("  {}: {}", std::string_view(field.name_string()),
                     std::string_view(field.value()));
    }
    std::println("\n{}", res.body());

    // Fermeture propre
    beast::error_code ec;
    socket.shutdown(tcp::socket::shutdown_both, ec);
}

int main() {
    asio::io_context ctx;
    co_spawn(ctx,
             fetch(ctx, "example.com", "80", "/"),
             [](std::exception_ptr ep) {
                 if (ep) {
                     try { std::rethrow_exception(ep); }
                     catch (const std::exception& e) {
                         std::println(stderr, "Error: {}", e.what());
                     }
                 }
             });
    ctx.run();
}

HTTPS / TLS avec Boost.Asio SSL

La communication sécurisée est incontournable en production. Boost.Asio intègre un wrapper autour d'OpenSSL qui s'utilise comme un stream Asio classique :

#include <boost/asio/ssl.hpp>

namespace ssl = asio::ssl;

Client HTTPS

awaitable<void> fetch_https(asio::io_context& ctx,
                             std::string host, std::string target) {
    // Contexte SSL
    ssl::context ssl_ctx(ssl::context::tlsv13_client);
    ssl_ctx.set_default_verify_paths();  // Certificats CA du système

    // Résolution
    tcp::resolver resolver(ctx);
    auto endpoints = co_await resolver.async_resolve(host, "443", use_awaitable);

    // Stream SSL wrappant un socket TCP
    ssl::stream<tcp::socket> stream(ctx, ssl_ctx);

    // SNI (Server Name Indication) — nécessaire pour la plupart des serveurs
    SSL_set_tlsext_host_name(stream.native_handle(), host.c_str());

    // Connexion TCP
    co_await asio::async_connect(
        stream.next_layer(), endpoints, use_awaitable);

    // Handshake TLS
    co_await stream.async_handshake(ssl::stream_base::client, use_awaitable);

    // À partir d'ici, le stream chiffré s'utilise comme un socket normal
    http::request<http::empty_body> req{http::verb::get, target, 11};
    req.set(http::field::host, host);
    req.set(http::field::user_agent, "Beast-Client/1.0");

    co_await http::async_write(stream, req, use_awaitable);

    beast::flat_buffer buffer;
    http::response<http::string_body> res;
    co_await http::async_read(stream, buffer, res, use_awaitable);

    std::println("HTTPS {} — {} octets",
                 static_cast<int>(res.result()), res.body().size());

    // Shutdown TLS propre
    beast::error_code ec;
    co_await stream.async_shutdown(asio::redirect_error(use_awaitable, ec));
}

Le concept de stream composable

Le design clé de Beast est que toutes ses fonctions (http::async_read, http::async_write, websocket::stream) acceptent n'importe quel type de stream compatible avec le concept AsyncReadStream / AsyncWriteStream d'Asio :

Beast HTTP/WebSocket
        │
        │ fonctionne sur n'importe quel stream :
        │
        ├── tcp::socket              (TCP brut)
        ├── ssl::stream<tcp::socket> (TLS/SSL)
        ├── votre_stream_custom      (compression, logging, ...)
        │
        └── Le même code Beast fonctionne sur tous

C'est cette composabilité qui rend Beast puissant : vous pouvez ajouter TLS sans modifier votre code HTTP, simplement en changeant le type du stream.

Serveur HTTPS

Pour un serveur HTTPS, vous fournissez un certificat et une clé privée :

ssl::context ssl_ctx(ssl::context::tlsv13);  
ssl_ctx.use_certificate_chain_file("server.pem");  
ssl_ctx.use_private_key_file("server.key", ssl::context::pem);  

// Le listener accepte des sockets TCP, les wrappe dans ssl::stream
awaitable<void> handle_https_session(
    ssl::stream<tcp::socket> stream) {

    // Handshake TLS côté serveur
    co_await stream.async_handshake(
        ssl::stream_base::server, use_awaitable);

    // Utiliser stream exactement comme un socket TCP
    beast::flat_buffer buffer;
    http::request<http::string_body> req;
    co_await http::async_read(stream, buffer, req, use_awaitable);

    // ... traiter la requête ...
}

💡 En développement, générez un certificat auto-signé avec : openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.pem -days 365 -nodes. En production, utilisez Let's Encrypt ou un certificat de votre CA d'entreprise.


WebSocket avec Beast

Beast gère le protocole WebSocket nativement, incluant le handshake d'upgrade HTTP → WebSocket et le framing des messages :

Serveur WebSocket

#include <boost/beast/websocket.hpp>

namespace ws = beast::websocket;

awaitable<void> handle_websocket(tcp::socket socket) {
    // Upgrade HTTP → WebSocket
    ws::stream<tcp::socket> wss(std::move(socket));
    co_await wss.async_accept(use_awaitable);

    std::println("WebSocket connecté");

    try {
        beast::flat_buffer buffer;

        while (true) {
            // Lire un message WebSocket complet
            co_await wss.async_read(buffer, use_awaitable);

            // Echo : renvoyer le message
            wss.text(wss.got_text());  // Préserver le type (texte ou binaire)
            co_await wss.async_write(buffer.data(), use_awaitable);

            buffer.consume(buffer.size());  // Vider le buffer
        }
    } catch (const boost::system::system_error& e) {
        if (e.code() != ws::error::closed) {
            std::println(stderr, "WebSocket error: {}", e.what());
        }
    }

    std::println("WebSocket déconnecté");
}

Client WebSocket

awaitable<void> websocket_client(asio::io_context& ctx,
                                  std::string host, std::string port) {
    tcp::resolver resolver(ctx);
    auto endpoints = co_await resolver.async_resolve(host, port, use_awaitable);

    ws::stream<tcp::socket> wss(ctx);
    co_await asio::async_connect(wss.next_layer(), endpoints, use_awaitable);

    // Handshake WebSocket
    co_await wss.async_handshake(host, "/ws", use_awaitable);

    // Envoyer un message
    co_await wss.async_write(
        asio::buffer(std::string("Hello WebSocket!")), use_awaitable);

    // Recevoir la réponse
    beast::flat_buffer buffer;
    co_await wss.async_read(buffer, use_awaitable);
    std::println("Reçu: {}", beast::buffers_to_string(buffer.data()));

    // Fermeture propre
    co_await wss.async_close(ws::close_code::normal, use_awaitable);
}

WebSocket sécurisé (WSS)

Comme pour HTTP, il suffit de changer le type du stream sous-jacent :

// WebSocket non sécurisé
ws::stream<tcp::socket> wss(ctx);

// WebSocket sécurisé (WSS)
ws::stream<ssl::stream<tcp::socket>> wss(ctx, ssl_ctx);

// Le handshake TLS se fait d'abord sur la couche inférieure
co_await wss.next_layer().async_handshake(
    ssl::stream_base::client, use_awaitable);

// Puis le handshake WebSocket
co_await wss.async_handshake(host, "/ws", use_awaitable);

Boost.JSON : sérialisation intégrée

Pour les API REST, il est courant de manipuler du JSON dans les corps de requête/réponse. Boost.JSON s'intègre naturellement :

#include <boost/json.hpp>

namespace json = boost::json;

http::response<http::string_body>  
handle_api(const http::request<http::string_body>& req) {  
    http::response<http::string_body> res;
    res.version(req.version());
    res.set(http::field::content_type, "application/json");

    if (req.method() == http::verb::get && req.target() == "/api/users") {
        // Construire du JSON
        json::object users_response;
        json::array users;
        users.push_back({{"id", 1}, {"name", "Alice"}});
        users.push_back({{"id", 2}, {"name", "Bob"}});
        users_response["users"] = std::move(users);
        users_response["total"] = 2;

        res.result(http::status::ok);
        res.body() = json::serialize(users_response);
    }
    else if (req.method() == http::verb::post && req.target() == "/api/users") {
        // Parser le body JSON de la requête
        try {
            auto body = json::parse(req.body());
            auto& obj = body.as_object();
            auto name = obj.at("name").as_string();

            json::object created;
            created["id"] = 3;
            created["name"] = name;
            created["created"] = true;

            res.result(http::status::created);
            res.body() = json::serialize(created);
        } catch (const std::exception& e) {
            res.result(http::status::bad_request);
            json::object err;
            err["error"] = e.what();
            res.body() = json::serialize(err);
        }
    }

    res.prepare_payload();
    return res;
}

💡 Boost.JSON est une alternative à nlohmann/json (chapitre 24) avec un focus différent : Boost.JSON privilégie la performance (moins d'allocations, parsing incrémental), tandis que nlohmann/json privilégie l'ergonomie (conversion automatique depuis/vers les types C++). Les deux sont de bons choix ; Boost.JSON s'intègre simplement mieux si vous utilisez déjà Boost.


Différences d'API entre Standalone Asio et Boost.Asio

Pour faciliter la migration ou le travail sur des projets utilisant l'une ou l'autre variante :

Correspondance des types

// Standalone Asio                    Boost.Asio
// ────────────────                   ──────────
asio::io_context                      boost::asio::io_context  
asio::ip::tcp                         boost::asio::ip::tcp  
asio::steady_timer                    boost::asio::steady_timer  
asio::awaitable<T>                    boost::asio::awaitable<T>  
asio::use_awaitable                   boost::asio::use_awaitable  
asio::co_spawn                        boost::asio::co_spawn  
asio::detached                        boost::asio::detached  

std::error_code                       boost::system::error_code  
std::system_error                     boost::system::system_error  

Correspondance des headers

// Standalone                         Boost
// ──────────                         ─────
#include <asio.hpp>                   #include <boost/asio.hpp>
#include <asio/co_spawn.hpp>          #include <boost/asio/co_spawn.hpp>
#include <asio/ssl.hpp>               #include <boost/asio/ssl.hpp>
#include <asio/use_awaitable.hpp>     #include <boost/asio/use_awaitable.hpp>
                                      #include <boost/beast.hpp>      // Beast (Boost only)
                                      #include <boost/json.hpp>       // JSON (Boost only)

Macro de portabilité

Si vous voulez écrire du code compatible avec les deux, un header d'abstraction minimal suffit :

// asio_compat.hpp
#pragma once

#ifdef USE_BOOST_ASIO
    #include <boost/asio.hpp>
    #include <boost/asio/co_spawn.hpp>
    #include <boost/asio/use_awaitable.hpp>
    namespace net = boost::asio;
    using error_code = boost::system::error_code;
#else
    #include <asio.hpp>
    #include <asio/co_spawn.hpp>
    #include <asio/use_awaitable.hpp>
    namespace net = asio;
    using error_code = std::error_code;
#endif

using net::ip::tcp;  
using net::ip::udp;  
using net::awaitable;  
using net::use_awaitable;  

Ce pattern d'alias de namespace (namespace net = ...) est largement utilisé dans l'écosystème C++ pour isoler le choix entre standalone et Boost.


Architecture d'un serveur HTTP Beast en production

Pour un serveur HTTP réaliste, la structure s'organise typiquement ainsi :

main.cpp
  │
  ├── io_context + signal handling + thread pool
  │
  └── listener (acceptor)
        │
        └── pour chaque connexion :
              │
              ├── detect_ssl()          // Détecter HTTP vs HTTPS
              │     ├── plain_session   // HTTP
              │     └── ssl_session     // HTTPS
              │
              └── session (coroutine) :
                    │
                    ├── async_read (requête)
                    ├── router(req) → handler
                    │     ├── GET  /api/...  → api_handler
                    │     ├── POST /api/...  → api_handler
                    │     ├── GET  /static/... → file_handler
                    │     └── *             → not_found
                    ├── async_write (réponse)
                    └── keep_alive ? → boucle

Servir des fichiers statiques

Beast fournit http::file_body pour servir des fichiers directement du disque vers le socket, sans charger le fichier entier en mémoire :

http::response<http::file_body>  
serve_file(const std::string& doc_root, std::string_view target) {  
    // Construire le chemin du fichier
    std::string path = doc_root + std::string(target);
    if (target.back() == '/') path += "index.html";

    // Ouvrir le fichier
    beast::error_code ec;
    http::file_body::value_type body;
    body.open(path.c_str(), beast::file_mode::scan, ec);

    if (ec == beast::errc::no_such_file_or_directory) {
        // 404
        http::response<http::string_body> res{http::status::not_found, 11};
        res.set(http::field::content_type, "text/plain");
        res.body() = "File not found";
        res.prepare_payload();
        return {};  // Simplifié — en pratique, utilisez un variant
    }

    auto const size = body.size();

    http::response<http::file_body> res{
        std::piecewise_construct,
        std::make_tuple(std::move(body)),
        std::make_tuple(http::status::ok, 11)
    };

    res.set(http::field::content_type, "text/html");  // Déduire du MIME type
    res.content_length(size);

    return res;
}

Boost.Cobalt : coroutines simplifiées

Boost 1.84 a introduit Boost.Cobalt, une librairie de coroutines qui simplifie certains patterns récurrents avec les awaitables natifs d'Asio :

#include <boost/cobalt.hpp>

namespace cobalt = boost::cobalt;

// Cobalt fournit son propre main coroutine
cobalt::main co_main(int argc, char* argv[]) {
    // Pas besoin de créer un io_context explicitement
    tcp::acceptor acceptor({co_await cobalt::this_coro::executor,
                            {tcp::v6(), 8080}});

    while (true) {
        auto socket = co_await acceptor.async_accept(cobalt::use_op);
        cobalt::spawn(handle_client(std::move(socket)), cobalt::detached);
    }

    co_return 0;
}

Cobalt réduit le boilerplate (pas de io_context explicite, pas de co_spawn dans main), mais c'est une addition récente encore en maturation. Pour les projets qui démarrent en 2026, surveiller son évolution est recommandé, mais les awaitables Asio natifs restent le choix éprouvé.


Quand choisir Boost.Asio

Choisissez Boost.Asio (plutôt que Standalone Asio) quand :

  • Vous avez besoin de HTTP ou WebSocket — Beast n'existe que dans Boost.
  • Vous avez besoin de TLS/SSL — Bien que Standalone Asio supporte SSL via un wrapper séparé, l'intégration Boost est plus mature et mieux documentée.
  • Votre projet utilise déjà Boost — Ajouter Boost.Asio est gratuit si Boost est déjà une dépendance.
  • Vous avez besoin de Boost.JSON, Boost.URL, ou d'autres librairies Boost complémentaires.

Ce point est développé plus en détail dans la section suivante (22.4.3).


Résumé

Boost.Asio étend Standalone Asio avec un écosystème de librairies qui couvrent les besoins réels d'un serveur réseau moderne :

  • Boost.Beast — HTTP/1.1 et WebSocket intégrés, avec un design de streams composables qui permet d'ajouter TLS sans modifier le code applicatif.
  • Boost.Asio SSL — Wrapper OpenSSL natif pour HTTPS et WSS.
  • Boost.JSON — Sérialisation JSON performante, intégrée naturellement avec les corps HTTP de Beast.
  • Le même noyau Asio — Coroutines, callbacks, timers, résolution DNS : tout ce que vous avez appris en section 22.4.1 s'applique identiquement.
  • Surcoût — La dépendance Boost est le seul prix. Pour les projets qui n'ont besoin que de TCP/UDP brut, Standalone Asio reste plus léger.

Pour les serveurs HTTP et les applications web en C++, la combinaison Boost.Asio + Beast + JSON est en 2026 la plateforme la plus mature et la plus complète de l'écosystème.


Prochaine étape → Section 22.4.3 : Quand choisir Standalone Asio vs Boost.Asio — critères de décision et matrice de choix.

⏭️ Quand choisir Asio standalone vs Boost.Asio