이번 포스트에서는 Boost.Asio를 활용해 구현한 비동기 TCP 에코 서버의 구조와 동작 원리를 Server와 Session 클래스의 역할을 중심으로 살펴보겠습니다.
이 서버는 두 개의 주요 클래스로 구성됩니다.
Server 클래스
StartAccept(): 새로운 연결을 기다리도록 비동기 수락 작업을 시작합니다.HandleAccept(): 클라이언트 연결이 수락되면 호출되어 세션을 시작하고, 이후 다시 연결 요청을 대기합니다.Start()와 Stop(): IO 컨텍스트의 이벤트 루프를 시작하거나 중지합니다.Session 클래스
boost::enable_shared_from_this를 상속하여 자신을 가리키는 공유 포인터를 콜백 함수에 안전하게 전달합니다.async_read_some)와 쓰기(async_write)를 사용하여 데이터를 처리합니다.Start(): 클라이언트로부터 데이터를 받기 위해 비동기 읽기를 시작합니다.HandleRead(): 데이터를 읽은 후 에코 처리를 위해 비동기 쓰기를 시작합니다.HandleWrite(): 에코 전송 후, 다시 클라이언트의 데이터를 기다리도록 읽기 작업을 재개합니다.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()를 호출하여 서버가 계속해서 새로운 연결 요청을 대기할 수 있도록 합니다.void Server::Start() {
io_context_.run();
}
void Server::Stop() {
io_context_.stop();
}
Start() 메서드는 io_context_.run()을 호출하여 IO 컨텍스트의 이벤트 루프를 시작합니다. 이를 통해 등록된 모든 비동기 작업(연결 수락, 읽기/쓰기 등)이 처리됩니다.Stop() 메서드를 호출하여 IO 컨텍스트를 중지할 수 있습니다.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;
}
서버 시작:
Server 객체가 생성되면, 지정된 포트에서 연결 수락 준비를 마치고 최초의 StartAccept()를 호출합니다.Start()를 호출하면 IO 컨텍스트의 이벤트 루프가 시작되어 비동기 작업들이 처리됩니다.연결 수락 및 세션 생성:
async_accept()가 연결을 수락하고 HandleAccept()가 호출됩니다.Session 객체의 Start() 메서드가 호출되어 클라이언트와의 통신이 시작됩니다.데이터 통신 및 에코 처리:
Session 객체는 async_read_some()을 통해 클라이언트의 데이터를 비동기로 읽고, 읽은 데이터를 그대로 클라이언트에게 전송합니다.async_read_some()을 재호출하여 지속적으로 데이터를 주고받습니다.자원 정리:
StartAccept()를 재귀적으로 호출하여 계속해서 연결을 처리합니다.소켓(Socket) 은 네트워크 통신의 끝점(endpoint)으로, 데이터를 주고받기 위한 기본 단위입니다.
TCP 소켓의 경우, 연결 지향적(connection-oriented) 프로토콜을 사용합니다. 이는 데이터를 전송하기 전에 3-way 핸드셰이크와 같은 과정을 통해 클라이언트와 서버 간에 연결을 설정한 후, 데이터를 신뢰성 있게 주고받는 방식을 의미합니다.
아래 코드는 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;
}
클라이언트 소켓(socket):
이 객체는 로컬에서 생성되어 아직 서버와 연결되지 않은 상태입니다. 내부에는 클라이언트의 IP 주소나 임시 포트 등 기본 정보가 담기며, 실제 통신을 시작하기 위한 준비 상태로 존재합니다.
endpoints:
tcp::resolver를 통해 생성된 endpoints 목록은 서버의 IP 주소와 포트 정보를 포함합니다. Boost.Asio의 connect 함수는 이 목록을 순회하며, 서버에 연결 요청을 보냅니다.
즉, boost::asio::connect(socket, endpoints); 구문은 클라이언트 소켓을 이용해 endpoints에 명시된 서버에 접속을 시도하는 과정입니다. 연결이 성공하면 해당 소켓을 통해 서버와 안정적인 통신 채널이 형성됩니다.
이처럼 Boost.Asio를 활용한 비동기 TCP 에코 서버는 Server와 Session 두 클래스로 역할을 분리하여,
비동기 방식 덕분에 서버는 블로킹 없이 여러 클라이언트와 동시에 통신할 수 있으며, 재귀적 연결 수락 패턴은 지속적인 서비스 제공을 가능하게 합니다.
클라이언트는 resolver를 통해 endPoint객체 생성해, 소켓과 함께 서버에 연결 요청을 보내고 연결이 완료되면 안정적인 통신채널이 형성됩니다.
이후 통신 단계에서는,
write 함수: 소켓을 통해 서버로 데이터를 전송할 때 사용하며, 전송할 데이터를 버퍼 형태로 전달합니다.
read 함수: 서버에서 전송한 데이터를 소켓으로 받아, 클라이언트의 버퍼로 전달합니다.
와 같은 구조로 진행됩니다.