How to Use Asio for Network Programming: A Complete Guide to C++ Async I/O

Asio provides a cross-platform C++ library for asynchronous network programming using an io_context event loop, I/O objects like tcp::socket, and completion handlers or C++20 coroutines to manage concurrent connections without blocking threads.

The chriskohlhoff/asio repository is a header-only C++ library that abstracts platform-specific networking APIs into a consistent asynchronous model. Whether you are building TCP servers, UDP clients, or protocol implementations, understanding how to use Asio for network programming enables you to write scalable, non-blocking I/O code that runs efficiently on Linux, Windows, and macOS.

Core Architecture of Asio

Asio's design revolves around three fundamental concepts that separate operation initiation from handler execution.

The io_context Event Loop

The io_context class defined in asio/io_context.hpp serves as the central nervous system of any Asio application. This object manages the operating system resources and dispatches completion handlers when asynchronous operations finish. All I/O objects must be associated with an io_context instance, and you must call io_context::run() to start the event loop.

I/O Objects

I/O objects such as ip::tcp::socket, ip::tcp::acceptor, and ip::udp::socket provide the interface for network communication. These classes, implemented in headers like asio/basic_socket.hpp and asio/basic_socket_acceptor.hpp, expose both synchronous (blocking) and asynchronous (non-blocking) member functions for operations like connect, read, and write.

Completion Handlers

Completion handlers are user-provided callable objects—functions, lambdas, or std::function instances—that Asio invokes when an asynchronous operation completes. With C++20, you can also use coroutine-compatible awaitables via co_await with asio::awaitable, allowing linear async code flow.

Key Components for Network Programming

Asio provides specialized facilities for common networking tasks through modular headers.

TCP and UDP Sockets

For stream-oriented communication, use ip::tcp::socket defined in asio/basic_stream_socket.hpp. For datagram communication, use ip::udp::socket from asio/basic_datagram_socket.hpp. Both support the same async operation model but differ in connection semantics.

Host Resolution

The ip::tcp::resolver class in asio/ip/basic_resolver.hpp performs asynchronous DNS resolution, converting hostnames to endpoint addresses without blocking the event loop.

Timers and Timeouts

Use asio::steady_timer (from asio/steady_timer.hpp) to implement timeouts, periodic tasks, or connection keepalives. These integrate seamlessly with the io_context event loop.

Thread Safety with Strands

The strand class in asio/strand.hpp serializes handler execution across multiple threads, preventing data races when handlers access shared state. This is essential for multi-threaded io_context usage.

Implementation Examples

Synchronous TCP Client

For simple scripts or blocking operations, use synchronous methods. The following example connects to localhost:12345 and performs a blocking echo exchange:

#include <asio.hpp>
#include <iostream>

int main() {
    asio::io_context ctx;
    asio::ip::tcp::socket sock(ctx);
    asio::ip::tcp::resolver resolver(ctx);
    auto endpoints = resolver.resolve("localhost", "12345");

    asio::connect(sock, endpoints);
    std::string msg = "Hello Asio!\n";
    asio::write(sock, asio::buffer(msg));

    char reply[1024];
    std::size_t n = sock.read_some(asio::buffer(reply));
    std::cout << "Server replied: " << std::string(reply, n);
}

This approach uses the umbrella header asio.hpp and blocks the calling thread until each operation completes.

Asynchronous TCP Server

For production servers, use asynchronous operations to handle thousands of concurrent connections on a single thread. The following example implements a callback-based echo server:

#include <asio.hpp>
#include <iostream>
#include <memory>

using asio::ip::tcp;

class Session : public std::enable_shared_from_this<Session> {
public:
    explicit Session(tcp::socket socket) : socket_(std::move(socket)) {}
    void start() { do_read(); }

private:
    void do_read() {
        auto self = shared_from_this();
        socket_.async_read_some(asio::buffer(data_),
            [this, self](std::error_code ec, std::size_t length) {
                if (!ec) do_write(length);
            });
    }
    void do_write(std::size_t length) {
        auto self = shared_from_this();
        asio::async_write(socket_, asio::buffer(data_, length),
            [this, self](std::error_code ec, std::size_t /*len*/) {
                if (!ec) do_read();
            });
    }
    tcp::socket socket_;
    std::array<char, 1024> data_;
};

int main() {
    asio::io_context ctx;
    tcp::acceptor acceptor(ctx, tcp::endpoint(tcp::v4(), 12345));

    std::function<void()> do_accept;
    do_accept = [&]() {
        acceptor.async_accept(
            [&](std::error_code ec, tcp::socket socket) {
                if (!ec) std::make_shared<Session>(std::move(socket))->start();
                do_accept();
            });
    };
    do_accept();
    ctx.run();
}

This example demonstrates the Reactor pattern implementation in Asio, where async_accept, async_read_some, and async_write initiate operations and the io_context invokes lambdas upon completion.

C++20 Coroutine-Based Client

Modern Asio supports C++20 coroutines for cleaner async code. The asio::awaitable template (defined in asio/awaitable.hpp) combined with co_spawn (from asio/co_spawn.hpp) enables linear programming flow:

#include <asio.hpp>
#include <asio/awaitable.hpp>
#include <asio/detached.hpp>
#include <iostream>

using asio::awaitable;
using asio::co_spawn;
using asio::detached;
namespace this_coro = asio::this_coro;

awaitable<void> chat(asio::io_context& ctx) {
    asio::ip::tcp::resolver resolver(co_await this_coro::executor);
    auto endpoints = co_await resolver.async_resolve("localhost", "12345",
                                                   asio::use_awaitable);
    asio::ip::tcp::socket socket(co_await this_coro::executor);
    co_await socket.async_connect(*endpoints.begin(), asio::use_awaitable);

    std::string request = "Hello coroutine!\n";
    co_await asio::async_write(socket, asio::buffer(request),
                               asio::use_awaitable);

    char reply[1024];
    std::size_t n = co_await socket.async_read_some(asio::buffer(reply),
                                                    asio::use_awaitable);
    std::cout << "Reply: " << std::string(reply, n) << '\n';
}

int main() {
    asio::io_context ctx;
    co_spawn(ctx, chat(ctx), detached);
    ctx.run();
}

Coroutines eliminate callback nesting while maintaining the performance benefits of asynchronous I/O.

Critical Source Files

Understanding these header locations helps when navigating the chriskohlhoff/asio source code:

Summary

  • Asio uses an io_context event loop to dispatch asynchronous I/O handlers without blocking threads.
  • I/O objects like ip::tcp::socket provide both synchronous and asynchronous APIs for network operations.
  • Completion handlers can be callbacks, lambdas, or C++20 coroutines using co_await and asio::awaitable.
  • Strands serialize handler execution to prevent data races in multi-threaded applications.
  • All components are header-only; link against system libraries like -lpthread on POSIX systems.

Frequently Asked Questions

What is the difference between synchronous and asynchronous operations in Asio?

Synchronous operations like asio::connect and socket::read_some block the calling thread until the operation completes or fails. Asynchronous operations like async_connect and async_read_some return immediately after initiation, scheduling the provided completion handler to run when the operation finishes. Asynchronous operations allow a single thread to manage thousands of concurrent connections via the io_context event loop.

How does io_context work in Asio network programming?

The io_context class manages the underlying operating system resources and runs the event loop. When you call io_context::run(), it blocks and dispatches completion handlers for completed asynchronous operations. You must call run(), run_one(), or poll() to execute handlers, otherwise asynchronous operations never complete. In asio/io_context.hpp, this class implements the Reactor pattern using epoll, kqueue, or IOCP depending on the platform.

When should I use C++20 coroutines with Asio?

Use C++20 coroutines when you want to write asynchronous network code that resembles synchronous programming flow. Coroutines via asio::awaitable and asio::use_awaitable eliminate callback nesting and make error handling more intuitive with try-catch blocks. They are ideal for complex protocols with multiple sequential operations, though they require C++20 compiler support and understanding of co_await semantics.

How do I handle thread safety in Asio applications?

Asio guarantees that completion handlers will not be called concurrently for the same I/O object, but handlers for different objects may run on different threads simultaneously. Use asio::strand (from asio/strand.hpp) to serialize handler execution across threads, ensuring that specific handlers never run concurrently. Alternatively, use io_context::strand or call io_context::run() from multiple threads to create a thread pool, using strands to protect shared data.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →