Echo Server Tutorial

This tutorial builds a production-quality echo server using the tcp_server framework. We’ll explore worker pools, connection lifecycle, and the launcher pattern.

Code snippets assume:

#include <boost/corosio/tcp_server.hpp>
#include <boost/capy/task.hpp>
#include <boost/capy/buffers.hpp>
#include <boost/capy/write.hpp>

namespace corosio = boost::corosio;
namespace capy = boost::capy;

Overview

An echo server accepts TCP connections and sends back whatever data clients send. While simple, this pattern demonstrates core concepts:

  • Using tcp_server for connection management

  • Implementing workers with worker_base

  • Launching session coroutines with launcher

  • Reading and writing data with sockets

Architecture

The tcp_server framework uses a worker pool pattern:

  1. Derive from tcp_server and define your worker type

  2. Preallocate workers during construction

  3. The framework accepts connections and dispatches them to idle workers

  4. Workers run session coroutines and return to the pool when done

This avoids allocation during operation and limits resource usage.

Worker Implementation

Workers derive from tcp_server::worker_base and implement two methods:

class echo_worker : public corosio::tcp_server::worker_base
{
    corosio::io_context& ctx_;
    corosio::tcp_socket sock_;
    char buf_[4096];

public:
    explicit echo_worker(corosio::io_context& ctx)
        : ctx_(ctx)
        , sock_(ctx)
    {
    }

    corosio::tcp_socket& socket() override
    {
        return sock_;
    }

    void run(corosio::tcp_server::launcher launch) override
    {
        launch(ctx_.get_executor(), do_session());
    }

    capy::task<> do_session();
};

Each worker:

  • Stores a reference to the io_context for executor access

  • Owns its socket (returned via socket())

  • Owns any per-connection state (like the buffer)

  • Implements run() to launch the session coroutine

Session Coroutine

The session coroutine handles one connection:

capy::task<> echo_worker::do_session()
{
    for (;;)
    {
        auto [ec, n] = co_await sock_.read_some(
            capy::mutable_buffer(buf_, sizeof buf_));

        auto [wec, wn] = co_await capy::write(
            sock_, capy::const_buffer(buf_, n));

        if (wec || ec)
            break;
    }

    sock_.close();
}

Notice:

  • We reuse the worker’s buffer across reads

  • read_some() returns when any data arrives — it may deliver bytes alongside an error

  • We always write before checking the error (advance-then-check); writing zero bytes is a no-op

  • capy::write() writes all data (it’s a composed operation)

  • When the coroutine ends, the launcher returns the worker to the pool

Server Construction

A helper builds the worker pool, then the constructor hands it to set_workers():

inline auto
make_echo_workers(corosio::io_context& ctx, int n)
{
    std::vector<std::unique_ptr<corosio::tcp_server::worker_base>> v;
    v.reserve(n);
    for (int i = 0; i < n; ++i)
        v.push_back(std::make_unique<echo_worker>(ctx));
    return v;
}

class echo_server : public corosio::tcp_server
{
public:
    echo_server(corosio::io_context& ctx, int max_workers)
        : tcp_server(ctx, ctx.get_executor())
    {
        set_workers(make_echo_workers(ctx, max_workers));
    }
};

Workers are owned polymorphically through std::unique_ptr<worker_base>, so set_workers() accepts any range of worker types.

Main Function

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        std::cerr <<
            "Usage: echo_server <port> <max-workers>\n"
            "Example:\n"
            "    echo_server 8080 10\n";
        return EXIT_FAILURE;
    }

    // Parse port
    int port_int = std::atoi(argv[1]);
    if (port_int <= 0 || port_int > 65535)
    {
        std::cerr << "Invalid port: " << argv[1] << "\n";
        return EXIT_FAILURE;
    }
    auto port = static_cast<std::uint16_t>(port_int);

    // Parse max workers
    int max_workers = std::atoi(argv[2]);
    if (max_workers <= 0)
    {
        std::cerr << "Invalid max-workers: " << argv[2] << "\n";
        return EXIT_FAILURE;
    }

    // Create I/O context
    corosio::io_context ioc;

    // Create server with worker pool
    echo_server server(ioc, max_workers);

    // Bind to port
    auto ec = server.bind(corosio::endpoint(port));
    if (ec)
    {
        std::cerr << "Bind failed: " << ec.message() << "\n";
        return EXIT_FAILURE;
    }

    std::cout << "Echo server listening on port " << port
              << " with " << max_workers << " workers\n";

    // Start accepting connections
    server.start();

    // Run the event loop
    ioc.run();

    return EXIT_SUCCESS;
}

Key Design Decisions

Why tcp_server?

The tcp_server framework provides:

  • Automatic pool management: Workers cycle between idle and active states

  • Safe lifecycle: The launcher ensures workers return to the pool

  • Multiple ports: Bind to several endpoints sharing one worker pool

Why Worker Pooling?

  • Bounded memory: Fixed number of connections

  • No allocation: Sockets and buffers preallocated

  • Simple accounting: Framework tracks worker availability

Why Composed Write?

The capy::write() free function ensures all data is sent:

// write_some: may write partial data
auto [ec, n] = co_await sock.write_some(buf);  // n might be < buf.size()

// write: writes all data or fails
auto [ec, n] = co_await capy::write(sock, buf);  // n == buf.size() or error

For echo servers, we want complete message delivery.

Why Not Use Exceptions?

The session loop needs to handle EOF gracefully. Using structured bindings with advance-then-check, we always act on n before inspecting ec:

auto [ec, n] = co_await sock.read_some(buf);
auto [wec, wn] = co_await capy::write(
    sock, capy::const_buffer(buf.data(), n));
if (wec || ec)
    break;  // Normal termination path

With exceptions, EOF would require a try-catch:

try {
    auto [ec, n] = co_await sock.read_some(buf);
    if (ec) throw std::system_error(ec);
} catch (...) {
    // EOF is an exception here
}

Testing

Start the server:

$ ./echo_server 8080 10
Echo server listening on port 8080 with 10 workers

Connect with netcat:

$ nc localhost 8080
Hello
Hello
World
World

Next Steps