Boost.Asio 한눈에 보기

  • 무엇? 네트워킹/타이머/직렬포트/파일(플랫폼 일부) 등 I/O를 비동기로 다루는 크로스플랫폼 라이브러리.
  • 핵심 모델: 이벤트 루프(io_context) + 실행자(Executor) + 비동기 연산(CompletionToken = 콜백/std::future/코루틴).
  • 백엔드: epoll/kqueue/IOCP 등 OS 고성능 I/O API 위에서 동작(사용자는 신경 안 써도 됨).
  • 두 가지 페이스: 동기 API(간단하지만 블로킹) vs 비동기 API(확장성과 성능).

기본 구성 요소

  • io_context: 모든 비동기 작업의 “심장”. io.run()이 이벤트 루프를 돌립니다.
  • 소켓/타이머/리졸버: ip::tcp::socket, steady_timer, ip::tcp::resolver
  • 버퍼: buffer(ptr, n), dynamic_buffer(string) 등.
  • 동기/비동기 API:
    • 동기: read, write, connect (예외 or error_code 반환)
    • 비동기: async_* (콜백/코루틴 등 CompletionToken으로 완료 통지)
  • 동시성 제어: strand (같은 strand에 바인딩된 핸들러는 절대 병렬 실행 안 됨)
  • 취소/타임아웃: 타이머 + cancel() 조합, 혹은 코루틴 + 선택적 취소.

0) 설치 & 빌드

  • 헤더 포함: #include <boost/asio.hpp>
  • 링크: 최신 Boost는 대개 헤더온리지만, 환경에 따라 -lboost_system 링크가 필요할 수 있음.
  • g++ 예시:
g++ -std=c++20 -O2 main.cpp -lboost_system -lpthread

1) “헬로, 비동기” — 타이머 예제 (콜백)

#include <boost/asio.hpp>
#include <chrono>
#include <iostream>
namespace asio = boost::asio;
using namespace std::chrono_literals;

int main() {
  asio::io_context io;
  asio::steady_timer timer(io, 1s);

  timer.async_wait([](const boost::system::error_code& ec) {
    if (!ec) std::cout << "1초 후!\n";
  });

  io.run();
}

C++20 코루틴 버전

#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <chrono>
#include <iostream>
namespace asio = boost::asio;
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::use_awaitable;
using namespace std::chrono_literals;

awaitable<void> demo_timer() {
  asio::steady_timer t(co_await asio::this_coro::executor, 1s);
  co_await t.async_wait(use_awaitable);
  std::cout << "코루틴: 1초 후!\n";
  co_return;
}

int main() {
  asio::io_context io;
  co_spawn(io, demo_timer(), detached);
  io.run();
}

2) TCP 에코 서버 (C++20 코루틴, 깔끔한 최신식)

#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/this_coro.hpp>
#include <array>
namespace asio = boost::asio;
using asio::ip::tcp;
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::use_awaitable;

awaitable<void> session(tcp::socket sock) {
  try {
    std::array<char, 1024> buf;
    for (;;) {
      std::size_t n = co_await sock.async_read_some(asio::buffer(buf), use_awaitable);
      co_await asio::async_write(sock, asio::buffer(buf.data(), n), use_awaitable);
    }
  } catch (...) {
    // 연결 종료/에러시 세션 종료
  }
  co_return;
}

awaitable<void> listener(unsigned short port) {
  auto ex = co_await asio::this_coro::executor;
  tcp::acceptor acc(ex, {tcp::v4(), port});
  for (;;) {
    tcp::socket sock = co_await acc.async_accept(use_awaitable);
    co_spawn(ex, session(std::move(sock)), detached);
  }
}

int main() {
  asio::io_context io;

  // Ctrl+C 안전 종료
  asio::signal_set signals(io, SIGINT, SIGTERM);
  signals.async_wait([&](auto, auto){ io.stop(); });

  co_spawn(io, listener(5555), detached);
  io.run();
}
  • 포트 5555로 접속하면 받은 바이트를 그대로 돌려줍니다.
  • 동시 접속도 코루틴으로 자연스럽게 스케일됩니다.

3) 스레드/동시성 모델

  • 단일 스레드: io.run()을 한 스레드에서만 돌리면, 모든 핸들러가 순차 실행 → 디버깅 쉬움.
  • 멀티스레드: std::thread 여러 개로 io.run() 병렬 호출 → 높은 처리량.
  • strand: 멀티스레드 환경에서 특정 시퀀스를 직렬화하고 싶다면 auto ex = asio::make_strand(io);로 실행자 생성 후 그 실행자에 바인딩하여 async_... 호출.
    예) tcp::socket sock(ex); 로 만들면, 그 소켓 핸들러들은 같은 strand에서 순서 보장.

4) 타임아웃/취소 패턴(정석)

비동기 read에 타임아웃 붙이는 대표 패턴:

  1. 타이머와 read를 동시에 시작
  2. 먼저 끝난 쪽이 다른 쪽을 cancel()
    (코루틴 예시 – 간단 버전)
#include <boost/asio.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/this_coro.hpp>
#include <chrono>
namespace asio = boost::asio;
using namespace std::chrono_literals;
using asio::awaitable; using asio::use_awaitable;

awaitable<std::size_t> read_with_timeout(asio::ip::tcp::socket& s,
                                         asio::mutable_buffer buf,
                                         std::chrono::milliseconds timeout) {
  auto ex = co_await asio::this_coro::executor;
  asio::steady_timer timer(ex);
  boost::system::error_code ec_read, ec_timer;
  std::size_t n_read = 0;

  timer.expires_after(timeout);

  bool read_done = false, timer_done = false;

  s.async_read_some(buf, [&](auto ec, std::size_t n){
    ec_read = ec; n_read = n; read_done = true; timer.cancel();
  });
  timer.async_wait([&](auto ec){ ec_timer = ec; timer_done = true; s.cancel(); });

  // 이벤트 루프에 제어를 돌려주며 두 작업 중 하나가 먼저 끝나길 기다림
  while (!read_done && !timer_done)
    co_await asio::post(ex, use_awaitable);

  if (timer_done && ec_read == asio::error::operation_aborted)
    throw std::runtime_error("read timeout");
  if (ec_read) throw boost::system::system_error(ec_read);
  co_return n_read;
}

5) CompletionToken 패턴(Asio의 큰 장점)

하나의 async_*가 다양한 “완료 통지 방식”을 지원:

  • 콜백: async_read(..., [](error_code, size_t){})
  • 코루틴: co_await async_read(..., use_awaitable)
  • std::future: async_read(..., use_future)
    → 프로젝트 특성에 맞게 스타일을 고를 수 있고, 나중에 바꾸기도 쉽습니다.

6) 버퍼 & 스트림 퀵 팁

  • 고정 버퍼: asio::buffer(ptr, size)
  • 가변 버퍼(문자열 확대/축소): asio::dynamic_buffer(std::string&)
  • 라인/구분자 파싱: async_read_until(sock, dynamic_buffer(buf), "\r\n")

간단 HTTP GET(동기, 이해용) 예:

#include <boost/asio.hpp>
#include <iostream>
namespace asio = boost::asio;
using asio::ip::tcp;

int main(){
  try {
    asio::io_context io;
    tcp::resolver r(io);
    auto eps = r.resolve("example.com", "80");
    tcp::socket s(io);
    asio::connect(s, eps);

    std::string req =
      "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
    asio::write(s, asio::buffer(req));

    std::string buf;
    asio::streambuf sb; // or use dynamic_buffer(buf)
    boost::system::error_code ec;
    while (asio::read(s, sb, ec)) {}
    if (ec != asio::error::eof && ec) throw boost::system::system_error(ec);
    std::cout << &sb;
  } catch (std::exception& e) {
    std::cerr << "err: " << e.what() << "\n";
  }
}

7) 실전 베스트 프랙티스

  • 수명 관리: 비동기 세션 객체는 std::shared_ptr로 소유해 핸들러에서 안전하게 참조.
  • strand 적극 사용: 멀티스레드에서 소켓 1개당 1-strand로 레이스 컨디션 방지.
  • 타임아웃 기본 탑재: 네트워크는 언제든 멈춥니다. 타이머+cancel() 패턴을 습관화.
  • 소켓 옵션: tcp::no_delay(true)(Nagle off), reuse_address(true) 등 상황에 맞게.
  • 작은 핸들러: 핸들러 안에서 무거운 연산 금지(필요시 post()로 분리).
  • 에러코드 우선: 예외보다 error_code로 제어 흐름 명확화(핵심 루프에서 try/catch 남발 X).
  • 구성요소 분리: acceptor, session, router/codec 등 역할별 클래스로 분리하면 테스트/유지보수 쉬움.

8) 흔한 함정

  • io_context.run()을 호출 안 함(혹은 일찍 반환됨) → 콜백/코루틴이 절대 실행 안 됨.
    필요시 auto guard = asio::make_work_guard(io);로 이벤트 루프가 일찍 끝나지 않게 유지.
  • 같은 소켓에 동시에 두 개의 async_read 날리기 → 정의되지 않은 동작. 읽기/쓰기 각각 1개씩만 동시 진행.
  • strand 없이 멀티스레드로 같은 자원 접근 → 미묘한 레이스. make_strand(io) 사용!
  • 타임아웃 미적용 → “유령 커넥션”이 리소스 잡아먹음.

9) Boost.Asio vs standalone Asio

  • 네임스페이스: boost::asio vs asio
  • API는 거의 동일. Boost 생태계(seralization, beast 등)와 어울리면 Boost.Asio가 편합니다.

10) 작은 치트시트

  • 이벤트 루프 시작: io_context io; io.run();
  • 멀티스레드: N개 스레드에서 io.run() 병렬 호출
  • strand: auto ex = asio::make_strand(io);
  • 코루틴 토큰: use_awaitable
  • 스폰: co_spawn(io, task(), detached);
  • 타임아웃: steady_timer + cancel() 조합
  • 종료: signal_set(SIGINT,SIGTERM).async_wait(... io.stop());

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다