Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aont/b4f8e3d3eb7634df1b192d7933608dd2 to your computer and use it in GitHub Desktop.

Select an option

Save aont/b4f8e3d3eb7634df1b192d7933608dd2 to your computer and use it in GitHub Desktop.

Simple Asio + C++20 coroutine echo server (with build notes)

This short article explains a tiny C++ program that uses standalone Asio and C++20 coroutines to run multiple I/O tasks concurrently: a TCP echo server plus a couple of periodic timers. It also explains the provided g++ command line and a few Windows-specific macros that appear at the top of main.cpp.


What the program does

The program is a compact example showing how to mix network I/O and timers using co_await / co_spawn:

  • listener(port) — an awaitable coroutine that accepts incoming TCP connections on the given port. For each accepted socket it spawns an echo_session coroutine.
  • echo_session(socket) — reads lines (up to '\n') from the socket using async_read_until, prints the received text to stdout, and writes the same data back (an echo).
  • ticker(id, interval) — a periodic task implemented with steady_timer that prints a tick message every interval seconds.
  • main() — creates an asio::io_context, spawns the listener and two ticker coroutines, hooks a signal_set to gracefully stop on SIGINT/SIGTERM, and runs the event loop.

The structure demonstrates how multiple asynchronous I/O operations — socket accept/read/write and timers — can coexist naturally using coroutine syntax, giving readable sequential style without blocking the thread.


The compile command explained

# Linux
g++ -std=c++20 -Ipath/to/asio/include main.cpp -pthread -o asio_coroutines_server
# Windows
g++ -std=c++20 -Ipath/to/asio/include main.cpp -pthread -D_WIN32_WINNT=0x0601 -lws2_32 -lmswsock -o asio_coroutines_server
  • -std=c++20 — use C++20 (required for co_await coroutine support).
  • -Iasio-1.36.0/include/ — add the Asio header directory (standalone Asio is header-only).
  • main.cpp — source file.
  • -pthread — enable pthreads (POSIX threads). On some toolchains this still helps for io_context and thread safety.
  • -D_WIN32_WINNT=0x0601 — define the minimum Windows target version (here 0x0601 → Windows 7). This controls which Windows API symbols are visible when including Windows headers.
  • -lws2_32 -lmswsock — link against Winsock libraries on Windows (needed for network functions).

Important: don’t accidentally pass -D_WIN32_WINNT without a value — that breaks Windows headers and causes cryptic compile errors (e.g., undefined FINDEX_INFO_LEVELS). Define it with a value (or define it in the source before including Windows headers).


Windows-specific macros in main.cpp

At the top of main.cpp you see:

#if defined(_WIN32)
  #ifndef WIN32_LEAN_AND_MEAN
  #define WIN32_LEAN_AND_MEAN
  #endif
  #ifndef NOMINMAX
  #define NOMINMAX
  #endif

  #ifndef _WIN32_WINNT
  #define _WIN32_WINNT 0x0601
  #endif
#endif

#define ASIO_STANDALONE
#include <asio.hpp>
  • WIN32_LEAN_AND_MEAN reduces the size of windows.h to avoid pulling in rarely-used APIs.
  • NOMINMAX prevents Windows headers from defining min/max macros that conflict with C++ standard functions.
  • _WIN32_WINNT sets the Windows platform level if it wasn’t already defined on the command line.
  • ASIO_STANDALONE tells the headers that you’re using standalone Asio (not Boost.Asio).

Defining those macros in source makes the build more robust against toolchain differences. If you also set _WIN32_WINNT on the compiler command line, command-line definitions take precedence.


How to run and test

  1. Make sure standalone Asio headers are available in asio-1.36.0/include/ (or update the -I path).
  2. Compile with the command above (adjust include path and possibly change/remove -pthread on Windows toolchains if not needed).
  3. Run the produced executable. It will print that it’s listening (port 12345 in the example).
  4. Use nc (netcat) or telnet from another terminal to connect:
nc localhost 12345
# type: Hello<Enter>
# you'll see the same line echoed back

Open multiple clients simultaneously to observe concurrent sessions. The console will also show ticker messages every 5s and 7s.


Notes & extensions

  • This sample is single-threaded (io_context{1}) and uses co_spawn(..., asio::detached) to run coroutines without awaiting their completion. You can scale to multiple threads by running io_context.run() on multiple threads or using a thread pool.
  • For TLS, replace tcp::socket with asio::ssl::stream<tcp::socket> and initialize an SSL context.
  • Be careful with Windows toolchains: ensure you use a consistent compiler (MSYS2/Mingw-w64 target) and avoid mixing incompatible headers.
  • If you get weird Windows header errors, double-check you didn’t accidentally pass -D_WIN32_WINNT without a value.

Summary

This tiny program is a clear, idiomatic demonstration of mixing sockets and timers with C++20 coroutines and standalone Asio. It shows how co_await and co_spawn let asynchronous code be written in straightforward sequential style, while remaining non-blocking and efficient. Use it as a starting point for building coroutine-based network services, timed tasks, or more advanced asynchronous workflows.

// main.cpp
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef _WIN32_WINNT
// Windows 7 相当。必要なら 0x0A00 (Windows 10) 等に変更
#define _WIN32_WINNT 0x0601
#endif
#endif
#define ASIO_STANDALONE
#include <asio.hpp>
#include <iostream>
#include <string>
#include <chrono>
#include <csignal>
using asio::ip::tcp;
// --- エコーセッション:クライアントごとにコルーチンで動く ---
asio::awaitable<void> echo_session(tcp::socket socket) {
try {
asio::streambuf buf;
for (;;) {
// 改行まで読み込む(co_await で非同期待ち)
std::size_t n = co_await asio::async_read_until(socket, buf, '\n', asio::use_awaitable);
// streambuf から文字列を取り出す
std::string line(asio::buffers_begin(buf.data()), asio::buffers_begin(buf.data()) + n);
buf.consume(n); // 既読分を消費
std::cout << "[session] recv: " << line; // 改行つきで表示
// 受け取ったデータをそのまま送り返す
co_await asio::async_write(socket, asio::buffer(line), asio::use_awaitable);
}
} catch (const std::exception& e) {
std::cerr << "[session] connection closed or error: " << e.what() << "\n";
}
}
// --- リスナー:接続を受け付けて各接続をコルーチンで spawn する ---
asio::awaitable<void> listener(unsigned short port) {
auto executor = co_await asio::this_coro::executor;
tcp::acceptor acceptor(executor, tcp::endpoint(tcp::v4(), port));
std::cout << "Listening on port " << port << "...\n";
for (;;) {
tcp::socket socket = co_await acceptor.async_accept(asio::use_awaitable);
std::cout << "[listener] accepted new connection\n";
// 接続ごとに別コルーチンで処理(デタッチ)
asio::co_spawn(executor, echo_session(std::move(socket)), asio::detached);
}
}
// --- タイカー:定期的に動作する別種の IO(例:定期処理) ---
asio::awaitable<void> ticker(int id, std::chrono::seconds interval) {
auto executor = co_await asio::this_coro::executor;
asio::steady_timer timer(executor);
for (;;) {
timer.expires_after(interval);
co_await timer.async_wait(asio::use_awaitable);
std::cout << "[ticker " << id << "] tick (every " << interval.count() << "s)\n";
}
}
int main() {
try {
asio::io_context io_context{1}; // シンプルに single-threaded
auto ex = io_context.get_executor();
// コルーチンを起動
asio::co_spawn(ex, listener(12345), asio::detached);
asio::co_spawn(ex, ticker(1, std::chrono::seconds(5)), asio::detached);
asio::co_spawn(ex, ticker(2, std::chrono::seconds(7)), asio::detached);
// Ctrl+C (SIGINT) で graceful shutdown
asio::signal_set signals(io_context, SIGINT, SIGTERM);
signals.async_wait([&](auto, auto){
std::cout << "Signal received: stopping io_context...\n";
io_context.stop();
});
io_context.run();
std::cout << "io_context finished.\n";
} catch (const std::exception& e) {
std::cerr << "Fatal: " << e.what() << "\n";
return 1;
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment