Acceptors
The tcp_acceptor class listens for incoming TCP connections and accepts them
into socket objects. It’s the foundation for building TCP servers.
|
Code snippets assume:
|
Overview
A tcp_acceptor binds to a local endpoint and waits for clients to connect:
// Convenience constructor: open + SO_REUSEADDR + bind + listen on port 8080
corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080));
corosio::tcp_socket peer(ioc);
auto [ec] = co_await acc.accept(peer);
if (!ec)
{
// peer is now a connected socket
}
Construction
Acceptors are constructed from an execution context or executor:
// From io_context
corosio::tcp_acceptor acc1(ioc);
// From executor
auto ex = ioc.get_executor();
corosio::tcp_acceptor acc2(ex);
A default-constructed acceptor doesn’t own system resources until it is opened, bound, and set to listen.
Listening
Setting up an acceptor involves three operations: opening a socket, binding it to a local endpoint, and marking it as passive (listening). You can do all three in one expression with the convenience constructor, or perform them separately for fine-grained control.
Convenience Constructor
The simplest way to get a listening acceptor is the convenience constructor,
which opens, sets SO_REUSEADDR, binds, and listens in a single step:
// open + SO_REUSEADDR + bind + listen; address family deduced from the endpoint
corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080));
This throws std::system_error on open, set_option(), bind, or listen
failure — exactly the codes the piecewise path (open, set option, bind,
listen) reports. Unlike the standalone open(), the convenience
constructor configures address reuse before binding, so the listening
port can be reused immediately after a restart. The option is
platform-specific: SO_REUSEADDR on POSIX, and SO_EXCLUSIVEADDRUSE on
Windows, where SO_REUSEADDR would instead let other sockets bind over
the port. Either way the observable contract is the same — a second
listener on an occupied endpoint throws errc::address_in_use.
bind() and listen()
For explicit error handling, construct the acceptor, then bind and listen as
separate steps. Both return a std::error_code and are marked
to prevent accidentally ignoring errors:
corosio::tcp_acceptor acc(ioc);
if (auto ec = acc.open()) // create an IPv4 TCP socket
return ec;
if (auto ec = acc.bind(corosio::endpoint(8080)))
{
std::cerr << "Bind failed: " << ec.message() << "\n";
return ec;
}
if (auto ec = acc.listen())
{
std::cerr << "Listen failed: " << ec.message() << "\n";
return ec;
}
The listen() method accepts an optional backlog parameter:
[[nodiscard]] std::error_code listen(int backlog = 128);
The backlog parameter specifies the maximum queue length for pending
connections. When the queue is full, the kernel may drop or refuse new
connection attempts. The default of 128 works for most applications.
Accepting Connections
accept()
The accept() operation waits for and accepts an incoming connection:
corosio::tcp_socket peer(ioc);
auto [ec] = co_await acc.accept(peer);
On success, peer is initialized with the new connection. Any existing
connection on peer is closed first.
The operation is asynchronous—your coroutine suspends until a connection arrives or an error occurs.
There is also a returning overload that constructs the peer socket for you, associated with the acceptor’s execution context:
auto [ec, peer] = co_await acc.accept();
Prefer the returning overload for the common case: it is simpler and guarantees
the acceptor and socket use the same io_context. Use the
accept(tcp_socket&) form when you pre-allocate or recycle sockets and want to
manage their lifetime yourself.
Errors
Common accept errors:
| Error | Meaning |
|---|---|
|
Cancelled via |
Resource errors |
System limit reached (file descriptors, memory) |
Calling accept() on a closed acceptor completes with
errc::bad_file_descriptor — the same code every corosio operation
reports for a closed object.
Cancellation
Move Semantics
Acceptors are move-only:
corosio::tcp_acceptor acc1(ioc);
corosio::tcp_acceptor acc2 = std::move(acc1); // OK
corosio::tcp_acceptor acc3 = acc2; // Error: deleted copy constructor
Move assignment closes any existing tcp_acceptor:
acc1 = std::move(acc2); // Closes acc1's socket if open, then moves acc2
| After a move, the destination uses the source’s execution context. |
Thread Safety
| Operation | Thread Safety |
|---|---|
Distinct acceptors |
Safe from different threads |
Same tcp_acceptor |
NOT safe for concurrent operations |
Don’t start multiple accept() operations concurrently on the same tcp_acceptor.
Example: Accept Loop
A typical server accept loop:
capy::task<void> accept_loop(
corosio::io_context& ioc,
corosio::tcp_acceptor& acc)
{
for (;;)
{
corosio::tcp_socket peer(ioc);
auto [ec] = co_await acc.accept(peer);
if (ec)
{
if (ec == capy::cond::canceled)
break; // Shutdown requested
std::cerr << "Accept error: " << ec.message() << "\n";
continue; // Try again
}
// Spawn a coroutine to handle this connection
capy::run_async(ioc.get_executor())(
handle_connection(std::move(peer)));
}
}
Key points:
-
Create a fresh socket for each accept
-
Move the socket into the handler coroutine
-
Continue accepting after non-fatal errors
-
Check for cancellation to support graceful shutdown
Example: Graceful Shutdown
Coordinate shutdown with signal handling:
capy::task<void> run_server(corosio::io_context& ioc)
{
corosio::tcp_acceptor acc(ioc);
if (auto ec = acc.open())
co_return;
if (auto ec = acc.bind(corosio::endpoint(8080)))
{
std::cerr << "Bind failed: " << ec.message() << "\n";
co_return;
}
if (auto ec = acc.listen())
{
std::cerr << "Listen failed: " << ec.message() << "\n";
co_return;
}
corosio::signal_set signals(ioc, SIGINT, SIGTERM);
// Spawn accept loop
capy::run_async(ioc.get_executor())(accept_loop(ioc, acc));
// Wait for shutdown signal
auto [ec, signum] = co_await signals.wait();
if (!ec)
{
std::cout << "Received signal " << signum << ", shutting down\n";
acc.cancel(); // Stop accepting
// Existing connections continue until complete
}
}
Relationship to tcp_server
For production servers, consider using tcp_server which provides:
-
Worker pool management
-
Connection limiting
-
Multi-port support
-
Automatic coroutine lifecycle
The tcp_acceptor class is the lower-level primitive that tcp_server builds
upon.
Next Steps
-
Sockets — Using accepted connections
-
TCP Server — Higher-level server framework
-
Echo Server Tutorial — Complete example