TCP Server
The tcp_server class provides a framework for building TCP servers with
connection pooling. It manages acceptors, worker pools, and connection
lifecycle automatically.
|
Code snippets assume:
|
Overview
tcp_server is a base class designed for inheritance. You derive from it,
define your worker type, and implement the connection handling logic. The
framework handles:
-
Listening on multiple ports
-
Accepting connections
-
Worker pool management
-
Coroutine lifecycle
class echo_worker : public corosio::tcp_server::worker_base
{
corosio::io_context& ctx_;
corosio::tcp_socket sock_;
std::string buf;
public:
explicit echo_worker(corosio::io_context& ctx)
: ctx_(ctx)
, sock_(ctx)
{
buf.reserve(4096);
}
corosio::tcp_socket& socket() override { return sock_; }
void run(launcher launch) override
{
launch(ctx_.get_executor(), do_echo());
}
capy::task<void> do_echo();
};
// Build the worker pool as a range of pointer-like objects.
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& ioc, int max_workers)
: tcp_server(ioc, ioc.get_executor())
{
set_workers(make_echo_workers(ioc, max_workers));
}
};
The Worker Pattern
Workers are preallocated objects that handle connections. Each worker contains a socket and any state needed for a session.
worker_base
The worker_base class is the foundation. It declares a constructor and a
virtual destructor, holds private intrusive-list bookkeeping used by the
server’s idle and active pools, and exposes two pure virtuals you must
override:
class worker_base
{
// Private list/bookkeeping members managed by tcp_server.
public:
worker_base();
virtual ~worker_base();
virtual void run(launcher launch) = 0;
virtual corosio::tcp_socket& socket() = 0;
};
Your worker inherits from worker_base, owns its socket, and implements the
required methods:
class my_worker : public corosio::tcp_server::worker_base
{
corosio::io_context& ctx_;
corosio::tcp_socket sock_;
std::string request_buf;
std::string response_buf;
public:
explicit my_worker(corosio::io_context& ctx)
: ctx_(ctx)
, sock_(ctx)
{}
corosio::tcp_socket& socket() override { return sock_; }
void run(launcher launch) override
{
launch(ctx_.get_executor(), handle_connection());
}
capy::task<void> handle_connection()
{
// Handle the connection using sock_
// Worker is automatically returned to pool when coroutine ends
}
};
Providing Workers
The worker pool is installed with set_workers(). It accepts any forward
range of pointer-like objects convertible to worker_base*, such as a
std::vector<std::unique_ptr<worker_base>>. The server takes ownership of the
range and reuses each worker across connections:
template<class Range>
void set_workers(Range&& workers);
A small helper that builds the range keeps construction tidy:
auto make_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<my_worker>(ctx));
return v;
}
class my_server : public corosio::tcp_server
{
public:
my_server(corosio::io_context& ioc, int max_workers)
: tcp_server(ioc, ioc.get_executor())
{
set_workers(make_workers(ioc, max_workers));
}
};
Because the range holds unique_ptr<worker_base>, workers are stored
polymorphically, allowing different worker types in the same pool if needed.
The Launcher
When a connection is accepted, tcp_server calls your worker’s run()
method with a launcher object. The launcher manages the coroutine lifecycle:
void run(launcher launch) override
{
// Create and launch the session coroutine
launch(executor, my_coroutine());
}
The launcher:
-
Starts your coroutine on the specified executor
-
Tracks the worker as in-use
-
Returns the worker to the pool when the coroutine completes
You must call the launcher exactly once. Failure to call it returns the
worker immediately. Calling it multiple times throws std::logic_error.
Binding and Starting
bind()
Bind to a local endpoint:
auto ec = server.bind(corosio::endpoint(8080));
if (ec)
std::cerr << "Bind failed: " << ec.message() << "\n";
You can bind to multiple ports:
server.bind(corosio::endpoint(80));
server.bind(corosio::endpoint(443));
start()
Begin accepting connections:
server.start();
Workers must have been provided via set_workers() before calling start(),
and at least one endpoint must be bound.
After start(), the server:
-
Listens on all bound ports
-
Accepts incoming connections
-
Assigns connections to available workers
-
Calls each worker’s
run()method
The accept loop runs until tcp_server::stop() is called, which requests the
workers' stop token. Note that stop() does not close the acceptors, so a
suspended accept completes once more before the loop exits.
Complete Example
#include <boost/corosio/tcp_server.hpp>
#include <boost/corosio/io_context.hpp>
#include <boost/capy/task.hpp>
#include <boost/capy/buffers.hpp>
#include <boost/capy/write.hpp>
#include <iostream>
#include <memory>
#include <vector>
namespace corosio = boost::corosio;
namespace capy = boost::capy;
class echo_worker : public corosio::tcp_server::worker_base
{
corosio::io_context& ctx_;
corosio::tcp_socket sock_;
std::string buf;
public:
explicit echo_worker(corosio::io_context& ctx)
: ctx_(ctx)
, sock_(ctx)
{
buf.reserve(4096);
}
corosio::tcp_socket& socket() override { return sock_; }
void run(launcher launch) override
{
launch(ctx_.get_executor(), do_session());
}
capy::task<void> do_session()
{
for (;;)
{
buf.resize(4096);
auto [ec, n] = co_await sock_.read_some(
capy::mutable_buffer(buf.data(), buf.size()));
if (ec || n == 0)
break;
buf.resize(n);
auto [wec, wn] = co_await capy::write(
sock_, capy::const_buffer(buf.data(), buf.size()));
if (wec)
break;
}
sock_.close();
}
};
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));
}
};
int main()
{
corosio::io_context ioc;
echo_server server(ioc, 100);
auto ec = server.bind(corosio::endpoint(8080));
if (ec)
{
std::cerr << "Bind failed: " << ec.message() << "\n";
return 1;
}
std::cout << "Echo server listening on port 8080\n";
server.start();
ioc.run();
}
Design Considerations
Why a Worker Pool?
A worker pool provides:
-
Bounded resources: Fixed maximum connections
-
No per-connection allocation: Sockets and buffers preallocated
-
Simple lifecycle: Workers cycle between idle and active states
Worker Reuse
When a session coroutine completes, its worker automatically returns to the idle pool. The next accepted connection receives this worker. Ensure your worker’s state is properly reset between connections:
capy::task<void> do_session()
{
// Reset state at session start
request_.clear();
response_.clear();
// ... handle connection ...
// Socket closed, worker returns to pool
}
Multiple Ports
tcp_server can listen on multiple ports simultaneously. All ports share
the same worker pool:
server.bind(corosio::endpoint(80)); // HTTP
server.bind(corosio::endpoint(443)); // HTTPS
server.start();
Connection Rejection
When all workers are busy, the server cannot accept new connections until a worker becomes available. The TCP listen backlog holds pending connections during this time.
For high-traffic scenarios, size your worker pool appropriately or implement connection limits at a higher layer.
Thread Safety
The tcp_server class is not thread-safe. All operations on the server
must occur from coroutines running on its io_context. Workers may not be
accessed concurrently.
For multi-threaded operation, create one server per thread, or use external synchronization.
Next Steps
-
Sockets — Socket operations
-
Concurrent Programming — Coroutine patterns
-
Echo Server Tutorial — Simpler approach