Network Programming 1.5일차

열수철·2025년 2월 8일

비동기 TCP 서버 구현

이번 포스트에서는 Boost.Asio를 활용해 구현한 비동기 TCP 에코 서버의 구조와 동작 원리를 ServerSession 클래스의 역할을 중심으로 살펴보겠습니다.

1. 전체 구조 개요

이 서버는 두 개의 주요 클래스로 구성됩니다.

  • Server 클래스

    • 역할: 클라이언트의 연결 요청을 비동기로 수락(accept)하고, 연결이 성립되면 새로운 세션(Session)을 생성하여 연결을 관리합니다.
    • 주요 메서드:
      • StartAccept(): 새로운 연결을 기다리도록 비동기 수락 작업을 시작합니다.
      • HandleAccept(): 클라이언트 연결이 수락되면 호출되어 세션을 시작하고, 이후 다시 연결 요청을 대기합니다.
      • Start()Stop(): IO 컨텍스트의 이벤트 루프를 시작하거나 중지합니다.
  • Session 클래스

    • 역할: 클라이언트와의 실제 통신을 담당합니다. 클라이언트로부터 데이터를 비동기로 읽고, 받은 데이터를 그대로 다시 보내는 에코(echo) 서버 기능을 수행합니다.
    • 특징:
      • boost::enable_shared_from_this를 상속하여 자신을 가리키는 공유 포인터를 콜백 함수에 안전하게 전달합니다.
      • 비동기 읽기(async_read_some)와 쓰기(async_write)를 사용하여 데이터를 처리합니다.
    • 주요 메서드:
      • Start(): 클라이언트로부터 데이터를 받기 위해 비동기 읽기를 시작합니다.
      • HandleRead(): 데이터를 읽은 후 에코 처리를 위해 비동기 쓰기를 시작합니다.
      • HandleWrite(): 에코 전송 후, 다시 클라이언트의 데이터를 기다리도록 읽기 작업을 재개합니다.

2. Server 클래스의 동작 원리

생성자와 초기화

Server::Server(boost::asio::io_context& io_context, short port)
    : io_context_(io_context), 
      acceptor_(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port))
{
    StartAccept();
}
  • IO 컨텍스트와 수신자 초기화:
    생성자에서는 외부에서 전달받은 io_context를 기반으로 acceptor_를 초기화합니다. 포트와 IPv4 엔드포인트를 지정하여 서버가 지정된 포트에서 클라이언트의 연결 요청을 받을 준비를 합니다.

  • 연결 대기 시작:
    StartAccept()를 호출하여 최초의 비동기 연결 수락 작업을 시작합니다.

비동기 연결 수락

void Server::StartAccept() {
    boost::shared_ptr<Session> new_session(new Session(io_context_));
    acceptor_.async_accept(new_session->socket(),
        boost::bind(&Server::HandleAccept, this, new_session,
            boost::asio::placeholders::error));
}
  • 새로운 세션 생성:
    클라이언트 연결 요청을 처리할 새로운 Session 객체를 boost::shared_ptr로 생성합니다. 이를 통해 세션의 생명 주기를 안전하게 관리할 수 있습니다.
  • 비동기 수락 호출:
    async_accept()를 통해 연결 요청을 비동기로 대기하며, 연결이 수락되면 HandleAccept() 콜백 함수가 호출됩니다.

연결 요청 처리

void Server::HandleAccept(boost::shared_ptr<Session> new_session, const boost::system::error_code& error) {
    if (!error) {
        new_session->Start();
    }
    else {
        std::cerr << "Error on accept: " << error.message() << std::endl;
    }

    StartAccept(); // 다음 연결 요청을 계속해서 대기
}
  • 오류 검사 및 세션 시작:
    연결 요청이 성공하면, 새로 생성된 세션의 Start() 메서드를 호출해 통신을 시작합니다. 오류가 발생한 경우 오류 메시지를 출력합니다.
  • 연결 반복 대기:
    마지막에 다시 StartAccept()를 호출하여 서버가 계속해서 새로운 연결 요청을 대기할 수 있도록 합니다.

IO 이벤트 루프

void Server::Start() {
    io_context_.run();
}

void Server::Stop() {
    io_context_.stop();
}
  • 이벤트 루프 실행:
    Start() 메서드는 io_context_.run()을 호출하여 IO 컨텍스트의 이벤트 루프를 시작합니다. 이를 통해 등록된 모든 비동기 작업(연결 수락, 읽기/쓰기 등)이 처리됩니다.
  • 서버 중지:
    필요 시 Stop() 메서드를 호출하여 IO 컨텍스트를 중지할 수 있습니다.

3. Session 클래스의 동작 원리

생성자와 소켓 관리

Session::Session(boost::asio::io_context& io_context)
    : socket_(io_context) {
}
  • 소켓 생성:
    생성자에서는 전달받은 io_context를 사용해 TCP 소켓을 초기화합니다.

통신 시작

void Session::Start() {
    socket_.async_read_some(boost::asio::buffer(data_, max_length),
        boost::bind(&Session::HandleRead, shared_from_this(),
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred));
}
  • 비동기 읽기 시작:
    Start() 메서드는 async_read_some()을 호출하여 클라이언트로부터 데이터를 읽기 시작합니다.
    • 읽은 데이터는 data_ 버퍼에 저장되며, 읽기가 완료되면 HandleRead() 콜백 함수가 호출됩니다.
    • shared_from_this()를 사용하여 현재 객체의 공유 포인터를 콜백에 전달, 객체가 콜백 실행 중 소멸되지 않도록 보장합니다.

데이터 수신 및 에코 처리

void Session::HandleRead(const boost::system::error_code& error, size_t bytes_transferred) {
    if (!error) {
        message_ = std::string(data_, bytes_transferred);
        std::cout << "Received: " << message_ << std::endl;

        // 에코 서버: 받은 메시지를 그대로 클라이언트에게 전송
        boost::asio::async_write(socket_, boost::asio::buffer(message_),
            boost::bind(&Session::HandleWrite, shared_from_this(),
                boost::asio::placeholders::error));
    }
    else {
        std::cerr << "Error on receive: " << error.message() << std::endl;
    }
}
  • 데이터 처리:
    데이터 수신에 성공하면, 수신된 데이터를 message_에 저장하고 콘솔에 출력합니다.
  • 에코 응답 전송:
    수신된 데이터를 그대로 클라이언트에게 전송하기 위해 async_write()를 호출하고, 전송 완료 후 HandleWrite()가 호출됩니다.

에코 전송 후 처리

void Session::HandleWrite(const boost::system::error_code& error) {
    if (!error) {
        // 전송 후, 클라이언트의 다음 메시지를 기다립니다.
        socket_.async_read_some(boost::asio::buffer(data_, max_length),
            boost::bind(&Session::HandleRead, shared_from_this(),
                boost::asio::placeholders::error,
                boost::asio::placeholders::bytes_transferred));
    }
    else {
        std::cerr << "Error on send: " << error.message() << std::endl;
    }
}
  • 연속적인 통신 처리:
    에코 전송이 성공하면, 다시 비동기 읽기를 시작하여 클라이언트의 추가 메시지를 기다립니다.
  • 오류 처리:
    전송 중 오류가 발생하면 오류 메시지를 출력합니다.

소멸자

Session::~Session() {
    socket_.close();
    std::cout << "Session closed." << std::endl;
}
  • 자원 정리:
    Session 객체가 소멸될 때 소켓을 닫고, 연결 종료 메시지를 출력하여 자원 누수를 방지합니다.

4. 전체 흐름 정리

  1. 서버 시작:

    • Server 객체가 생성되면, 지정된 포트에서 연결 수락 준비를 마치고 최초의 StartAccept()를 호출합니다.
    • Start()를 호출하면 IO 컨텍스트의 이벤트 루프가 시작되어 비동기 작업들이 처리됩니다.
  2. 연결 수락 및 세션 생성:

    • 클라이언트가 연결 요청을 보내면, async_accept()가 연결을 수락하고 HandleAccept()가 호출됩니다.
    • 성공적인 연결의 경우, 새로운 Session 객체의 Start() 메서드가 호출되어 클라이언트와의 통신이 시작됩니다.
  3. 데이터 통신 및 에코 처리:

    • Session 객체는 async_read_some()을 통해 클라이언트의 데이터를 비동기로 읽고, 읽은 데이터를 그대로 클라이언트에게 전송합니다.
    • 전송 후 async_read_some()을 재호출하여 지속적으로 데이터를 주고받습니다.
  4. 자원 정리:

    • 세션 종료 시 소멸자가 호출되어 소켓을 닫고 연결 종료를 알리며, 자원 누수를 방지합니다.
    • 서버는 새로운 연결 요청이 들어올 때마다 StartAccept()를 재귀적으로 호출하여 계속해서 연결을 처리합니다.

클라이언트와 연결

1. 소켓과 연결 지향적 통신

소켓(Socket) 은 네트워크 통신의 끝점(endpoint)으로, 데이터를 주고받기 위한 기본 단위입니다.
TCP 소켓의 경우, 연결 지향적(connection-oriented) 프로토콜을 사용합니다. 이는 데이터를 전송하기 전에 3-way 핸드셰이크와 같은 과정을 통해 클라이언트와 서버 간에 연결을 설정한 후, 데이터를 신뢰성 있게 주고받는 방식을 의미합니다.


2. Boost.Asio를 활용한 클라이언트 연결 예제

아래 코드는 Boost.Asio를 사용하여 간단한 TCP 클라이언트를 구현한 예제입니다. 이 코드는 클라이언트가 서버의 주소와 포트("127.0.0.1", "12345")를 resolve한 후, 소켓을 생성하여 연결을 시도합니다.

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind/bind.hpp>
#include <string>

using boost::asio::ip::tcp;

int main() {
    try {
        boost::asio::io_context io_context;

        // 서버 주소와 포트를 해석하여 endpoint 정보를 생성합니다.
        tcp::resolver resolver(io_context);
        tcp::resolver::results_type endpoints = resolver.resolve("127.0.0.1", "12345");

        // 아직 연결되지 않은 클라이언트 소켓 생성
        tcp::socket socket(io_context);
        
        // socket과 endpoints를 이용해 서버에 연결합니다.
        boost::asio::connect(socket, endpoints);

        // 메시지 송수신 루프
        while (true) {
            std::cout << "Enter message: ";
            std::string message;
            std::getline(std::cin, message);

            if (message == "quit") {
                break;
            }

            // 메시지에 줄바꿈 추가 후 전송
            boost::asio::write(socket, boost::asio::buffer(message + "\n"));

            // 서버로부터 응답 수신
            char reply[1024];
            size_t reply_length = socket.read_some(boost::asio::buffer(reply, 1024));
            std::cout << "Reply is: ";
            std::cout.write(reply, reply_length);
            std::cout << std::endl;
        }
    }
    catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

connect 함수와 소켓, endpoints의 역할

  • 클라이언트 소켓(socket):
    이 객체는 로컬에서 생성되어 아직 서버와 연결되지 않은 상태입니다. 내부에는 클라이언트의 IP 주소나 임시 포트 등 기본 정보가 담기며, 실제 통신을 시작하기 위한 준비 상태로 존재합니다.

  • endpoints:
    tcp::resolver를 통해 생성된 endpoints 목록은 서버의 IP 주소와 포트 정보를 포함합니다. Boost.Asio의 connect 함수는 이 목록을 순회하며, 서버에 연결 요청을 보냅니다.

즉, boost::asio::connect(socket, endpoints); 구문은 클라이언트 소켓을 이용해 endpoints에 명시된 서버에 접속을 시도하는 과정입니다. 연결이 성공하면 해당 소켓을 통해 서버와 안정적인 통신 채널이 형성됩니다.


5. 마무리

이처럼 Boost.Asio를 활용한 비동기 TCP 에코 서버는 ServerSession 두 클래스로 역할을 분리하여,

  • 서버는 클라이언트 연결 요청을 비동기적으로 수락하고,
  • 세션은 실제 통신을 담당하며 에코 기능을 수행합니다.

비동기 방식 덕분에 서버는 블로킹 없이 여러 클라이언트와 동시에 통신할 수 있으며, 재귀적 연결 수락 패턴은 지속적인 서비스 제공을 가능하게 합니다.

클라이언트는 resolver를 통해 endPoint객체 생성해, 소켓과 함께 서버에 연결 요청을 보내고 연결이 완료되면 안정적인 통신채널이 형성됩니다.

이후 통신 단계에서는,
write 함수: 소켓을 통해 서버로 데이터를 전송할 때 사용하며, 전송할 데이터를 버퍼 형태로 전달합니다.
read 함수: 서버에서 전송한 데이터를 소켓으로 받아, 클라이언트의 버퍼로 전달합니다.
와 같은 구조로 진행됩니다.


profile
그래픽스, 수학, 물리, 게임 만세

0개의 댓글