[카테고리:] 미분류

  • 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());