Learning Boost
Table of Contents
Asynchronous TCP Read and Write with Boost.Asio
Building a networked application with Boost.Asio and Boost.Beast involves several core components that work together to handle asynchronous I/O, networking, and HTTP message processing. Understanding the role of each component makes it much easier to reason about control flow, concurrency, and performance.
boost::asio::io_context
boost::asio::io_context is the core execution engine behind Boost.Asio's asynchronous model. It acts as the event loop of the application, coordinating all asynchronous operations such as network I/O, timers, and signal handling. Rather than performing work immediately, asynchronous operations register their completion handlers with the io_context, which are invoked once the corresponding event is ready.
Calling io_context.run() starts the event-processing loop. This call blocks the current thread and continuously pulls ready handlers from the internal queue, executing them one by one. As long as there is pending work, such as active sockets or scheduled timers, the event loop remains alive and keeps the application running.
All asynchronous components, including TCP servers, sockets, and timers, must be associated with the same io_context. This shared context ensures that events are properly ordered and executed within the same execution environment, making it easier to reason about concurrency and program flow.
1boost::asio::io_context io_context;
2
3// Construct asynchronous components using the same io_context
4TCPServer server(io_context, tcp::endpoint(tcp::v4(), 12345), order_book);
5
6// Start the event loop (blocks until all work is complete)
7io_context.run();boost::asio::ip::tcp::acceptor
The boost::asio::ip::tcp::acceptor is responsible for listening for incoming TCP connections on a specific network endpoint, which consists of an IP address and a port number. Its main role is to wait for new client connection requests and accept them by creating a TCP socket for each client.
Instead of blocking while waiting for a client to connect, the acceptor works asynchronously. When a connection request arrives, it notifies the application through a completion handler. This allows the server to handle many clients at the same time while remaining responsive and efficient.
To function correctly, a tcp::acceptor must be associated with an asio::io_context and a tcp::endpoint. The I/O context provides the event loop that drives all asynchronous operations, while the endpoint defines the local address and port on which the server listens for incoming connections.
In practice, the acceptor registers itself with the I/O context so that it can be notified when a new connection is ready to be accepted. This tight integration ensures that all network events are processed within the same execution environment.
1TCPServer::TCPServer(
2 boost::asio::io_context& ioc,
3 tcp::endpoint endpoint,
4 OrderBook& book
5): ioc_(ioc), acceptor_(ioc, endpoint), book_(book)
6{
7 std::cout << "[DEBUG] TCP server created" << std::endl;
8}boost::asio::ip::tcp::socket
A boost::asio::ip::tcp::socket represents an active TCP connection between the server and a client. After a connection is accepted by the acceptor, the socket becomes the main interface used to communicate with that client. All data sent to or received from the client flows through this socket.
Each socket is typically associated with a single client session and is managed independently. The socket provides functions such as read_some,write_some,async_read, and async_write to move data between the server and the client without blocking the main thread.
A tcp::socket must be created with an io_context, which connects the socket to the operating system's networking stack. The io_context is responsible for monitoring the socket and invoking the appropriate completion handlers when I/O operations complete.
1void TCPServer::do_accept()
2{
3 acceptor_.async_accept(
4 [this](boost::system::error_code ec, tcp::socket socket)
5 {
6 if (!ec)
7 {
8 std::cout
9 << "[DEBUG] New client accepted from: "
10 << socket.remote_endpoint().address().to_string()
11 << ":"
12 << socket.remote_endpoint().port()
13 << std::endl;
14
15 auto session = std::make_shared<Session>(
16 std::move(socket),
17 book_
18 );
19
20 session->start();
21 }
22
23 // Continue accepting the next incoming connection
24 do_accept();
25 }
26 );
27}Creating our Session
A session represents a single client connection and is responsible for managing all communication with that client. When a new TCP connection is accepted, the server creates a new session object and transfers ownership of the connected socket to it.
The session constructor takes a tcp::socket by value and immediately moves it into a member variable. This makes the session the sole owner of the socket and ensures that the socket remains valid for the entire lifetime of the client connection. The OrderBook is passed by reference so that all sessions can operate on the same shared order book.
Calling start() marks the beginning of the session's lifecycle. This function kicks off the first asynchronous read operation, allowing the server to begin receiving messages from the client without blocking. From this point onward, all communication with the client is handled through asynchronous callbacks.
1TCPServer::Session::Session(tcp::socket socket, OrderBook& book)
2 : socket_(std::move(socket)),
3 book_(book)
4{
5 std::cout << "[DEBUG] Session created" << std::endl;
6}
7
8// ------------------------------------------------------------
9// Session entry point
10// ------------------------------------------------------------
11void TCPServer::Session::start()
12{
13 do_read();
14
15 std::cout
16 << "[DEBUG] Session starting with client "
17 << socket_.remote_endpoint().address().to_string()
18 << ":"
19 << socket_.remote_endpoint().port()
20 << std::endl;
21}Reading from our session
Once a session starts, it immediately begins listening for incoming data from the connected client. Reading is done asynchronously so that the server can continue handling other clients while waiting for network input.
The do_read() function initiates an asynchronous read operation using async_read_until. This call waits until a newline character is received, allowing the server to process input one complete line at a time. This is a common pattern for text-based protocols.
The session captures shared_from_this() in the completion handler to ensure that the session object remains alive until the asynchronous read finishes. Without this, the session could be destroyed while an I/O operation is still in progress.
When a full line is received, on_read() is invoked. If an error occurs, such as the client disconnecting, the session handles cleanup and stops processing. If the read succeeds, the incoming data is extracted from the buffer, processed, and the session immediately schedules another read. This creates a continuous read loop for the lifetime of the connection.
1void TCPServer::Session::do_read()
2{
3 // Capturing self ensures the Session stays alive
4 // until the handler completes
5 async_read_until(
6 socket_,
7 buffer_,
8 "\n",
9 [this, self = shared_from_this()](boost::system::error_code ec,
10 std::size_t bytes_transferred)
11 {
12 on_read(ec, bytes_transferred);
13 });
14}
15
16// ------------------------------------------------------------
17// Handle a full incoming line
18// ------------------------------------------------------------
19void TCPServer::Session::on_read(boost::system::error_code ec,
20 std::size_t bytes_transferred)
21{
22 if (ec)
23 {
24 if (ec == boost::asio::error::eof)
25 {
26 std::cout << "[DEBUG] Client disconnected" << std::endl;
27
28 if (socket_.is_open())
29 {
30 socket_.close();
31 }
32 }
33 else
34 {
35 std::cerr << "[ERROR] on_read: "
36 << ec.message()
37 << std::endl;
38 }
39 return;
40 }
41
42 std::cout << "[DEBUG] Received "
43 << bytes_transferred
44 << " bytes from client"
45 << std::endl;
46
47 std::istream is(&buffer_);
48 std::string line;
49 std::getline(is, line);
50
51 std::cout << "[DEBUG] Processing line: "
52 << line
53 << std::endl;
54
55 process_line(line);
56
57 // Continue reading the next message
58 do_read();
59}Writing from our session
Sending data back to the client is also performed asynchronously to avoid blocking the server while network I/O is in progress. Each session is responsible for writing responses on its own socket once a request has been processed.
The write_response() function initiates an asynchronous write using boost::asio::async_write. This call ensures that the entire response buffer is sent before the completion handler is invoked, which is important for message-oriented protocols.
The response string is wrapped in a std::shared_ptr so that the data remains valid for the duration of the asynchronous write. Since the write operation may complete after write_response() returns, the buffer must outlive the function scope.
As with reads, the session captures shared_from_this() in the completion handler to keep the session object alive until the write finishes. If an error occurs, such as the client disconnecting, the socket is closed gracefully. On success, the server logs the number of bytes sent.
1void TCPServer::Session::write_response(const std::string& resp)
2{
3 // Keep the response buffer alive until the async write completes
4 auto out = std::make_shared<std::string>(resp);
5
6 boost::asio::async_write(
7 socket_,
8 boost::asio::buffer(*out),
9 [this, self = shared_from_this(), out]
10 (boost::system::error_code ec, std::size_t bytes_written)
11 {
12 if (ec)
13 {
14 std::cerr << "[ERROR] Write failed: "
15 << ec.message()
16 << std::endl;
17
18 boost::system::error_code ignored;
19 socket_.close(ignored);
20 }
21 else
22 {
23 std::cout << "[DEBUG] Sent "
24 << bytes_written
25 << " bytes to client"
26 << std::endl;
27 }
28 });
29}boost::asio::buffer
boost::asio::buffer is a helper function that creates a lightweight view over a block of contiguous memory. Instead of owning data, it simply wraps existing memory as a pointer-and-size pair that Boost.Asio can use for I/O operations.
This design allows Boost.Asio to work efficiently with many common data types such as C-style arrays, std::string,std::vector, and raw memory buffers, without copying data. The buffer object acts as a safe abstraction that removes the need for manual pointer arithmetic and size tracking.
Because boost::asio::buffer does not own the underlying memory, the programmer must ensure that the data remains valid for the duration of the asynchronous operation.
1std::string message = "Hello, client!\n";
2
3// Create a buffer that references existing memory
4auto buf = boost::asio::buffer(message);
5
6// Use the buffer in an asynchronous write
7boost::asio::async_write(
8 socket,
9 buf,
10 [](boost::system::error_code ec, std::size_t bytes_written) {
11 if (!ec) {
12 std::cout << "Sent " << bytes_written << " bytes" << std::endl;
13 }
14 }
15);