C++에서 비동기 네트워크 프로그래밍을 수행할 때, 성능과 확장성을 고려한 설계가 중요합니다. 특히, WebSocket은 클라이언트와 서버 간의 실시간 데이터 교환을 가능하게 해 주는 강력한 프로토콜로, 게임 서버 개발에서 널리 사용됩니다.
본 기사에서는 C++의 Asio 라이브러리를 활용하여 WebSocket 서버를 구현하는 방법을 설명합니다. Asio는 네트워크 및 멀티스레딩 프로그래밍을 지원하는 강력한 라이브러리로, 비동기 입출력을 효율적으로 처리할 수 있습니다.
구체적으로 다음과 같은 내용을 다룰 예정입니다.
- Asio 및 WebSocket 프로토콜의 개요
- TCP 소켓 서버를 구현하는 방법
- WebSocket 핸드셰이크 처리
- WebSocket 데이터 송수신 및 실시간 통신 구현
- 멀티스레드 환경에서의 Asio 서버
- 성능 최적화 및 문제 해결
이를 통해 C++에서 WebSocket 기반의 게임 서버를 구축하고 최적화하는 방법을 이해할 수 있습니다.
Asio란 무엇인가?
Asio는 C++에서 비동기 입출력(Asynchronous I/O) 처리를 효율적으로 지원하는 라이브러리입니다. 특히 네트워크 프로그래밍에서 성능과 확장성을 고려한 애플리케이션을 개발하는 데 유용합니다.
Asio의 주요 특징
- 비동기 이벤트 기반 처리
io_context
와 같은 객체를 활용하여 비동기 작업을 효과적으로 관리할 수 있습니다.
- 플랫폼 독립적
- Windows, Linux, macOS 등 다양한 운영 체제에서 사용할 수 있습니다.
- 멀티스레드 지원
- 멀티스레드 환경에서 비동기 작업을 분산 처리하여 성능을 높일 수 있습니다.
- 표준 C++과 호환
- C++11 이후의 표준 기능과 잘 호환되며, 부스트(Boost) 라이브러리의 일부로 제공되기도 합니다.
Asio 기본 사용 예제
다음은 Asio를 이용한 간단한 타이머 예제입니다.
#include <iostream>
#include <asio.hpp>
int main() {
asio::io_context io;
asio::steady_timer timer(io, std::chrono::seconds(3));
timer.wait(); // 3초 동안 대기
std::cout << "타이머 종료!" << std::endl;
return 0;
}
위 코드는 asio::steady_timer
를 사용하여 3초 동안 대기한 후 메시지를 출력합니다.
Asio의 활용 분야
- 네트워크 프로그래밍 (TCP, UDP 소켓 통신)
- WebSocket 서버 및 클라이언트 구현
- 멀티스레드 기반의 고성능 서버 개발
- 파일 및 디스크 I/O 최적화
이처럼 Asio는 C++에서 비동기 네트워크 프로그래밍을 위한 핵심 라이브러리로, WebSocket 서버를 구축하는 데 필수적인 요소입니다.
WebSocket 프로토콜 개요
WebSocket은 풀 듀플렉스(Full-Duplex) 통신을 지원하는 프로토콜로, 클라이언트와 서버 간의 실시간 데이터 전송을 가능하게 합니다. 기존의 HTTP 기반 요청-응답 방식과 달리, WebSocket은 한 번 연결을 설정하면 지속적으로 양방향 통신이 가능하다는 장점이 있습니다.
WebSocket과 HTTP의 차이
특징 | HTTP | WebSocket |
---|---|---|
연결 방식 | 요청-응답 기반 (Stateless) | 지속적인 연결 (Stateful) |
데이터 흐름 | 클라이언트 요청 후 서버 응답 | 양방향(Full-Duplex) |
사용 사례 | 일반적인 웹 요청 및 API 호출 | 채팅, 게임, 실시간 알림 등 |
WebSocket 연결 과정
- HTTP 핸드셰이크(Handshake)
- WebSocket 연결은 기존의 HTTP 요청을 통해 시작됩니다.
- 클라이언트는
Upgrade
헤더를 포함한 요청을 보내고, 서버가 이를 승인하면 연결이 WebSocket으로 전환됩니다. 예제: WebSocket 핸드셰이크 요청
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
서버 응답 (WebSocket 연결 승인)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- 양방향 데이터 전송
- 연결이 성립되면, 클라이언트와 서버는 추가 요청 없이 데이터를 송수신할 수 있음
- 데이터를 프레임 단위로 송수신하며, 텍스트와 바이너리 데이터 전송 가능
- 연결 종료
Close
프레임을 전송하여 연결 종료
WebSocket의 장점
- 낮은 네트워크 비용: 요청-응답 방식이 아닌 지속 연결이므로, 반복적인 HTTP 요청보다 효율적
- 낮은 지연시간(Latency): 실시간으로 데이터를 주고받을 수 있어 빠른 응답 속도를 제공
- 서버 푸시 기능: 서버에서 클라이언트로 데이터를 직접 전송 가능
WebSocket 활용 사례
- 온라인 게임 서버
- 실시간 채팅 서비스
- 금융 거래 시스템 (주식 시세 업데이트)
- IoT 기기와의 실시간 데이터 교환
이처럼 WebSocket은 실시간성이 중요한 애플리케이션에서 필수적으로 활용됩니다. 이후 Asio를 사용하여 WebSocket 서버를 직접 구현하는 방법을 다룹니다.
Asio로 TCP 소켓 서버 구현하기
WebSocket은 기본적으로 TCP 위에서 동작하는 프로토콜이므로, WebSocket 서버를 구현하려면 먼저 TCP 소켓 서버를 구축해야 합니다. C++의 Asio 라이브러리를 사용하면 비동기 방식으로 TCP 서버를 효율적으로 구현할 수 있습니다.
TCP 소켓 서버 개념
TCP 서버는 클라이언트와의 연결을 수락(Accept)하고, 데이터를 송수신할 수 있도록 처리하는 구조로 되어 있습니다. 일반적인 TCP 서버의 동작 과정은 다음과 같습니다.
- 서버 소켓을 열고 특정 포트에서 수신 대기
- 클라이언트의 연결 요청(Accept) 처리
- 클라이언트와 데이터 송수신(Read/Write)
- 연결 종료 및 리소스 정리
Asio를 이용한 TCP 서버 구현
다음은 C++ Asio를 사용하여 간단한 TCP 서버를 구현하는 코드 예제입니다.
#include <iostream>
#include <asio.hpp>
using asio::ip::tcp;
void handle_client(tcp::socket& socket) {
try {
char data[1024];
asio::error_code error;
// 클라이언트로부터 메시지 수신
size_t length = socket.read_some(asio::buffer(data), error);
if (!error) {
std::cout << "클라이언트 메시지: " << std::string(data, length) << std::endl;
// 클라이언트에게 응답 전송
std::string response = "Hello from server!";
asio::write(socket, asio::buffer(response), error);
}
} catch (std::exception& e) {
std::cerr << "클라이언트 처리 중 오류 발생: " << e.what() << std::endl;
}
}
int main() {
try {
asio::io_context io_context;
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080));
std::cout << "TCP 서버가 포트 8080에서 실행 중..." << std::endl;
while (true) {
tcp::socket socket(io_context);
acceptor.accept(socket); // 클라이언트 연결 수락
handle_client(socket); // 클라이언트 요청 처리
}
} catch (std::exception& e) {
std::cerr << "서버 실행 중 오류 발생: " << e.what() << std::endl;
}
return 0;
}
코드 설명
asio::io_context
: 비동기 작업을 처리하는 이벤트 루프tcp::acceptor
: 클라이언트의 연결 요청을 수락하는 역할tcp::socket
: 클라이언트와 데이터 송수신을 담당read_some()
: 클라이언트가 보낸 데이터를 읽음write()
: 클라이언트에게 응답 메시지를 전송
실행 방법
- 위 코드를 컴파일 후 실행하면, 서버가 포트 8080에서 실행됩니다.
- 클라이언트가 연결을 요청하면, 메시지를 수신하고 응답을 보냅니다.
- 클라이언트는 서버로부터
"Hello from server!"
응답을 받습니다.
TCP 서버를 WebSocket과 연결하는 이유
- WebSocket은 TCP 위에서 동작하므로 기본적인 TCP 서버 구조를 이해하는 것이 중요합니다.
- 이후 단계에서 WebSocket 핸드셰이크를 추가하여 TCP 서버를 WebSocket 서버로 확장할 것입니다.
다음 단계에서는 TCP 서버 위에서 WebSocket 핸드셰이크를 구현하는 방법을 설명합니다.
WebSocket 핸드셰이크 처리
WebSocket 연결은 기존의 HTTP 요청을 기반으로 시작됩니다. 클라이언트가 WebSocket을 사용하기 위해서는 먼저 핸드셰이크(Handshake) 요청을 보내야 하며, 서버가 이를 승인하면 WebSocket 연결이 확립됩니다.
1. WebSocket 핸드셰이크 개념
- 클라이언트는 일반적인 HTTP 요청을 보내되, Upgrade 헤더를 포함하여 WebSocket 연결을 요청함.
- 서버는 클라이언트가 보낸 요청을 확인한 후, WebSocket 연결을 승인하는 101 Switching Protocols 응답을 보냄.
- 이후, 클라이언트와 서버 간에 WebSocket 프레임을 이용한 통신이 가능해짐.
2. 클라이언트의 WebSocket 요청 예제
클라이언트가 WebSocket 연결을 요청하는 HTTP 메시지 예제는 다음과 같습니다.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
3. 서버의 WebSocket 응답 예제
서버는 위 요청을 확인한 후, 다음과 같은 응답을 반환하여 WebSocket 연결을 승인합니다.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept
값은 Sec-WebSocket-Key
값을 SHA-1 해시 후 Base64 인코딩하여 생성됩니다.
WebSocket 핸드셰이크 구현 (Asio 기반)
C++ Asio를 사용하여 WebSocket 핸드셰이크를 처리하는 방법을 설명합니다.
1. WebSocket 핸드셰이크 요청을 처리하는 서버 구현
#include <iostream>
#include <asio.hpp>
#include <openssl/sha.h>
#include <openssl/pem.h>
#include <sstream>
#include <iomanip>
#include <vector>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
using asio::ip::tcp;
// Sec-WebSocket-Key를 기반으로 Sec-WebSocket-Accept 값 생성
std::string generate_websocket_accept_key(const std::string& key) {
const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
std::string concat = key + magic;
unsigned char hash[SHA_DIGEST_LENGTH];
SHA1(reinterpret_cast<const unsigned char*>(concat.c_str()), concat.size(), hash);
std::string accept_key = boost::beast::detail::base64_encode(hash, SHA_DIGEST_LENGTH);
return accept_key;
}
void handle_client(tcp::socket& socket) {
try {
asio::streambuf buffer;
asio::read_until(socket, buffer, "\r\n\r\n");
std::istream request_stream(&buffer);
std::string request_line;
std::getline(request_stream, request_line);
std::string header;
std::string websocket_key;
while (std::getline(request_stream, header) && header != "\r") {
if (header.find("Sec-WebSocket-Key: ") != std::string::npos) {
websocket_key = header.substr(19);
websocket_key.erase(websocket_key.find_last_not_of(" \r\n") + 1);
}
}
if (!websocket_key.empty()) {
std::string accept_key = generate_websocket_accept_key(websocket_key);
std::ostringstream response;
response << "HTTP/1.1 101 Switching Protocols\r\n"
<< "Upgrade: websocket\r\n"
<< "Connection: Upgrade\r\n"
<< "Sec-WebSocket-Accept: " << accept_key << "\r\n\r\n";
asio::write(socket, asio::buffer(response.str()));
std::cout << "WebSocket 핸드셰이크 완료\n";
}
} catch (std::exception& e) {
std::cerr << "클라이언트 처리 중 오류 발생: " << e.what() << std::endl;
}
}
int main() {
try {
asio::io_context io_context;
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080));
std::cout << "WebSocket 서버가 포트 8080에서 실행 중..." << std::endl;
while (true) {
tcp::socket socket(io_context);
acceptor.accept(socket);
handle_client(socket);
}
} catch (std::exception& e) {
std::cerr << "서버 실행 중 오류 발생: " << e.what() << std::endl;
}
return 0;
}
4. 코드 설명
generate_websocket_accept_key()
- 클라이언트가 보낸
Sec-WebSocket-Key
값을 받아 SHA-1 해시 후 Base64 인코딩하여Sec-WebSocket-Accept
값을 생성합니다.
handle_client()
- 클라이언트의 요청을 읽고
Sec-WebSocket-Key
값을 추출합니다. Sec-WebSocket-Accept
값을 계산하여 WebSocket 프로토콜 전환을 승인합니다.
main()
- 서버는 8080 포트에서 클라이언트 연결을 대기하고,
handle_client()
를 호출하여 핸드셰이크를 처리합니다.
5. 실행 방법
- 위 코드를 컴파일하고 실행하면, WebSocket 서버가 8080 포트에서 대기합니다.
- 브라우저 또는 WebSocket 클라이언트(예:
wscat
)를 사용하여 서버에 연결합니다.
wscat을 이용한 테스트 (Node.js 패키지)
npm install -g wscat
wscat -c ws://localhost:8080
연결이 성공하면 Connected (press CTRL+C to quit)
메시지가 나타납니다.
6. 결론
- WebSocket은 TCP 위에서 동작하며, HTTP 핸드셰이크 과정을 통해 연결이 설정됩니다.
- Asio를 활용하면 WebSocket 핸드셰이크를 구현할 수 있으며, 이후 프레임 단위로 데이터 송수신을 처리하는 단계로 확장할 수 있습니다.
- 다음 단계에서는 WebSocket 데이터 송수신 구현 방법을 설명합니다.
WebSocket 데이터 송수신 처리
WebSocket 연결이 성공적으로 수립되면, 클라이언트와 서버는 텍스트 또는 바이너리 데이터를 송수신할 수 있는 상태가 됩니다. WebSocket은 프레임 기반의 데이터 전송 방식을 사용하며, 이는 일반적인 TCP 스트림과 차이가 있습니다. 이번 섹션에서는 C++ Asio를 사용하여 WebSocket 데이터를 송수신하는 방법을 설명합니다.
1. WebSocket 데이터 프레임 구조
WebSocket은 프레임(Frame) 단위로 데이터를 송수신합니다. 각 프레임에는 다음과 같은 구조가 포함됩니다.
필드명 | 크기 | 설명 |
---|---|---|
FIN | 1 bit | 메시지의 마지막 프레임 여부 |
Opcode | 4 bits | 메시지 유형 (텍스트, 바이너리, 핑 등) |
Mask | 1 bit | 클라이언트에서 보낸 데이터는 항상 마스킹됨 |
Payload Len | 7 bits (최대 127) | 데이터 길이 |
Masking Key | 4 bytes (선택적) | 클라이언트에서 보낼 때 사용됨 |
Payload Data | 가변적 | 실제 데이터 |
서버는 클라이언트가 보낸 마스킹된 데이터(Masked Data)를 해제(Decode) 해야 하며, 클라이언트로 데이터를 보낼 때는 마스킹을 적용하지 않고 전송해야 합니다.
2. WebSocket 메시지 처리 로직
- 클라이언트 메시지 수신
- 클라이언트가 서버로 메시지를 보내면, WebSocket 프레임을 읽어 마스킹 해제 후 데이터를 추출합니다.
- 서버 메시지 응답
- 서버는 수신한 데이터를 처리한 후, WebSocket 프레임 형식으로 응답을 전송합니다.
3. WebSocket 데이터 송수신 구현 (Asio 기반)
아래 코드는 Asio와 Boost.Beast를 활용하여 WebSocket 메시지를 송수신하는 서버를 구현한 예제입니다.
서버 코드 (C++ WebSocket 서버)
#include <iostream>
#include <asio.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
using namespace boost::beast;
using namespace boost::asio;
using namespace boost::beast::websocket;
using tcp = ip::tcp;
void handle_client(tcp::socket socket) {
try {
websocket::stream<tcp::socket> ws(std::move(socket));
// WebSocket 핸드셰이크 (클라이언트 요청 처리)
ws.accept();
std::cout << "클라이언트 WebSocket 연결 완료\n";
for (;;) {
flat_buffer buffer;
ws.read(buffer); // 클라이언트 메시지 읽기
std::string received_msg = buffers_to_string(buffer.data());
std::cout << "클라이언트 메시지: " << received_msg << std::endl;
// 응답 메시지 작성
std::string response = "서버 응답: " + received_msg;
ws.text(true); // 텍스트 모드 설정
ws.write(buffer_copy(buffer, asio::buffer(response))); // 응답 전송
}
} catch (std::exception& e) {
std::cerr << "WebSocket 연결 종료: " << e.what() << std::endl;
}
}
int main() {
try {
asio::io_context io_context;
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080));
std::cout << "WebSocket 서버 실행 중 (포트 8080)...\n";
while (true) {
tcp::socket socket(io_context);
acceptor.accept(socket);
std::thread(handle_client, std::move(socket)).detach(); // 클라이언트 처리 스레드 실행
}
} catch (std::exception& e) {
std::cerr << "서버 오류 발생: " << e.what() << std::endl;
}
return 0;
}
4. 코드 설명
- 핸드셰이크 처리 (
ws.accept()
)
- 클라이언트가 연결을 요청하면 WebSocket 프로토콜로 전환합니다.
- 메시지 읽기 (
ws.read(buffer)
)
flat_buffer
를 사용하여 클라이언트로부터 WebSocket 메시지를 읽습니다.
- 텍스트 데이터 변환 (
buffers_to_string(buffer.data())
)
- 읽어온 데이터를 문자열로 변환하여 출력합니다.
- 응답 메시지 전송 (
ws.write()
)
- 수신한 데이터를 기반으로 서버 응답 메시지를 전송합니다.
- 멀티스레딩 처리 (
std::thread(handle_client, ...)
)
- 다수의 클라이언트가 동시에 연결할 수 있도록 스레드를 생성하여 각각의 연결을 처리합니다.
5. 실행 및 테스트 방법
서버 실행
g++ -std=c++17 websocket_server.cpp -o server -lboost_system -lpthread -lssl -lcrypto
./server
클라이언트 테스트 (wscat
사용)
Node.js의 wscat
을 이용하여 서버와 통신할 수 있습니다.
npm install -g wscat
wscat -c ws://localhost:8080
메시지 전송 테스트 예시
> Hello Server
< 서버 응답: Hello Server
6. WebSocket 데이터 송수신 요약
- WebSocket 연결이 수립된 후, 텍스트 또는 바이너리 데이터를 송수신 가능
- 클라이언트와 서버는 프레임 기반 메시지 전송을 수행
- Asio와 Boost.Beast를 사용하여 비동기 WebSocket 서버 구현 가능
7. 다음 단계
현재까지 WebSocket 서버를 구축하고 데이터를 송수신하는 방법을 설명했습니다.
다음 단계에서는 멀티스레드 환경에서 WebSocket 서버를 최적화하는 방법을 다룹니다.
멀티스레드 환경에서의 Asio WebSocket 서버
WebSocket 서버는 다수의 클라이언트와 동시 연결을 유지하면서 데이터를 실시간으로 송수신해야 합니다. Asio는 기본적으로 단일 스레드 환경에서도 비동기 처리를 지원하지만, 멀티스레드를 활용하면 서버 성능을 극대화할 수 있습니다.
본 섹션에서는 C++ Asio에서 멀티스레드 WebSocket 서버를 구현하는 방법을 설명합니다.
1. 멀티스레드 WebSocket 서버의 필요성
기본적인 WebSocket 서버는 단일 스레드에서 클라이언트 요청을 순차적으로 처리합니다. 하지만, 다수의 클라이언트가 동시에 연결될 경우 처리 지연이 발생할 수 있습니다. 이를 해결하기 위해 멀티스레드 방식으로 WebSocket 서버를 구현하면 다음과 같은 장점이 있습니다.
✅ 다수의 클라이언트 동시 처리: 클라이언트 요청을 여러 개의 스레드에서 병렬로 처리
✅ 서버 성능 향상: CPU 코어를 활용하여 네트워크 부하 분산
✅ 응답 속도 개선: 한 클라이언트의 대기 시간이 다른 클라이언트에 영향을 주지 않음
2. Asio에서 멀티스레드 처리 방식
Asio에서 멀티스레드 WebSocket 서버를 구현하는 방법은 크게 두 가지로 나뉩니다.
- 스레드 풀(Thread Pool) 방식:
io_context
를 여러 개의 스레드에서 공유하여 동시 처리 - 클라이언트별 개별 스레드 방식: 각 클라이언트 요청을 별도의 스레드에서 처리
이 중에서 스레드 풀 방식이 성능적으로 더 유리하므로, 해당 방식을 활용하여 WebSocket 서버를 구현합니다.
3. 멀티스레드 WebSocket 서버 구현 (Asio + Boost.Beast)
아래 코드는 멀티스레드 환경에서 WebSocket 서버를 실행하는 예제입니다.
멀티스레드 WebSocket 서버 코드 (C++)
#include <iostream>
#include <thread>
#include <vector>
#include <asio.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
using namespace boost::beast;
using namespace boost::asio;
using namespace boost::beast::websocket;
using tcp = ip::tcp;
// 클라이언트 핸들러 (WebSocket 연결 및 메시지 처리)
void handle_client(tcp::socket socket) {
try {
websocket::stream<tcp::socket> ws(std::move(socket));
ws.accept(); // WebSocket 핸드셰이크
std::cout << "클라이언트 연결 완료\n";
for (;;) {
flat_buffer buffer;
ws.read(buffer); // 클라이언트 메시지 수신
std::string received_msg = buffers_to_string(buffer.data());
std::cout << "클라이언트 메시지: " << received_msg << std::endl;
// 응답 메시지 전송
std::string response = "서버 응답: " + received_msg;
ws.text(true);
ws.write(buffer_copy(buffer, asio::buffer(response)));
}
} catch (std::exception& e) {
std::cerr << "WebSocket 연결 종료: " << e.what() << std::endl;
}
}
// 멀티스레드 WebSocket 서버 실행
int main() {
try {
asio::io_context io_context;
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080));
std::cout << "멀티스레드 WebSocket 서버 실행 중 (포트 8080)...\n";
// 스레드 풀 생성
const int num_threads = std::thread::hardware_concurrency();
std::vector<std::thread> thread_pool;
for (int i = 0; i < num_threads; ++i) {
thread_pool.emplace_back([&io_context]() {
io_context.run();
});
}
while (true) {
tcp::socket socket(io_context);
acceptor.accept(socket);
std::thread(handle_client, std::move(socket)).detach(); // 개별 클라이언트 처리 스레드 실행
}
// 모든 스레드 실행
for (auto& thread : thread_pool) {
thread.join();
}
} catch (std::exception& e) {
std::cerr << "서버 오류 발생: " << e.what() << std::endl;
}
return 0;
}
4. 코드 설명
✅ 스레드 풀을 활용한 WebSocket 서버 실행
const int num_threads = std::thread::hardware_concurrency();
std::vector<std::thread> thread_pool;
for (int i = 0; i < num_threads; ++i) {
thread_pool.emplace_back([&io_context]() {
io_context.run();
});
}
std::thread::hardware_concurrency()
를 사용하여 CPU 코어 개수만큼 스레드를 생성- 각 스레드는
io_context.run()
을 호출하여 이벤트 루프를 실행
✅ 클라이언트별 개별 스레드 처리
while (true) {
tcp::socket socket(io_context);
acceptor.accept(socket);
std::thread(handle_client, std::move(socket)).detach();
}
- 새로운 클라이언트가 연결될 때마다 개별 스레드를 생성하여 처리
std::thread::detach()
를 사용하여 스레드가 백그라운드에서 실행되도록 설정
✅ WebSocket 메시지 송수신
ws.read(buffer); // 클라이언트 메시지 수신
std::string received_msg = buffers_to_string(buffer.data());
ws.text(true);
ws.write(buffer_copy(buffer, asio::buffer(response))); // 응답 전송
ws.read(buffer)
를 통해 WebSocket 메시지를 읽고buffers_to_string()
으로 변환ws.write()
를 사용하여 클라이언트에게 응답 전송
5. 실행 및 테스트 방법
서버 실행
g++ -std=c++17 websocket_server.cpp -o server -lboost_system -lpthread -lssl -lcrypto
./server
클라이언트 테스트 (wscat
사용)
npm install -g wscat
wscat -c ws://localhost:8080
메시지 전송 테스트 예시
> Hello WebSocket Server!
< 서버 응답: Hello WebSocket Server!
6. 멀티스레드 WebSocket 서버 요약
✅ 스레드 풀(Thread Pool) 방식을 적용하여 성능 최적화
✅ 다중 클라이언트 연결을 효율적으로 처리
✅ CPU 리소스를 최적 활용하여 동시 요청을 빠르게 처리
이제 멀티스레드 환경에서 WebSocket 서버가 안정적으로 실행됩니다.
7. 다음 단계
다음 섹션에서는 게임 서버에서 WebSocket을 활용하는 방법과 실시간 데이터 전송 기법을 다룹니다.
게임 서버에서의 WebSocket 활용
멀티플레이 온라인 게임에서는 낮은 지연시간(Latency)과 실시간 데이터 전송이 필수적입니다. WebSocket은 양방향 통신(Full-Duplex)을 지원하여 실시간 데이터를 효율적으로 교환할 수 있어, 많은 온라인 게임에서 활용됩니다.
이번 섹션에서는 WebSocket을 활용한 게임 서버의 주요 설계 개념과 실시간 데이터 전송 기법을 설명합니다.
1. 게임 서버에서 WebSocket이 필요한 이유
기존 HTTP 방식과 WebSocket을 비교하면, 게임 서버에서 WebSocket이 적합한 이유를 이해할 수 있습니다.
특징 | HTTP 기반 게임 서버 | WebSocket 기반 게임 서버 |
---|---|---|
연결 방식 | 요청-응답(Stateless) | 지속적 연결(Stateful) |
데이터 흐름 | 클라이언트 요청 후 서버 응답 | 양방향 실시간 전송 |
속도 | 비교적 느림 (네트워크 부하↑) | 낮은 지연시간 (빠름) |
사용 사례 | 순차적 게임, 웹 API | 실시간 멀티플레이 게임 |
✔ HTTP는 요청-응답 기반으로, 매번 새로운 연결을 생성해야 하므로 실시간 게임에서는 속도가 느려질 수 있음
✔ WebSocket은 한 번 연결이 수립되면 양방향으로 즉시 데이터 송수신 가능, 즉 실시간 게임에 적합
2. WebSocket 기반 게임 서버의 주요 기능
WebSocket을 활용한 게임 서버는 다음과 같은 기능을 포함합니다.
✅ 플레이어 연결 관리
- 다수의 클라이언트가 접속 및 연결 해제 가능
- 연결된 플레이어의 상태를 유지 (세션 관리)
✅ 실시간 메시지 송수신
- 클라이언트 ↔ 서버 간 빠른 데이터 교환
- 위치 정보, 공격 액션 등 실시간 업데이트
✅ 멀티플레이 동기화
- 게임 내 모든 플레이어의 상태(위치, 점수, 아이템 등) 유지
- 물리 엔진과 함께 프레임 동기화 가능
3. WebSocket을 활용한 게임 서버 구현 (Asio + Boost.Beast)
아래 코드는 멀티플레이 게임 서버에서 WebSocket을 활용한 데이터 송수신 예제입니다.
C++ WebSocket 게임 서버 코드
#include <iostream>
#include <unordered_map>
#include <thread>
#include <asio.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
using namespace boost::beast;
using namespace boost::asio;
using namespace boost::beast::websocket;
using tcp = ip::tcp;
// 클라이언트 세션 관리
std::unordered_map<std::string, std::shared_ptr<websocket::stream<tcp::socket>>> clients;
std::mutex clients_mutex;
// 클라이언트 메시지 브로드캐스트
void broadcast_message(const std::string& message) {
std::lock_guard<std::mutex> lock(clients_mutex);
for (auto& [id, client] : clients) {
if (client) {
client->text(true);
client->write(asio::buffer(message));
}
}
}
// 클라이언트 핸들러 (플레이어 개별 연결)
void handle_client(tcp::socket socket) {
try {
auto ws = std::make_shared<websocket::stream<tcp::socket>>(std::move(socket));
ws->accept();
// 클라이언트 ID 생성
std::string client_id = std::to_string(reinterpret_cast<uintptr_t>(ws.get()));
{
std::lock_guard<std::mutex> lock(clients_mutex);
clients[client_id] = ws;
}
std::cout << "플레이어 " << client_id << " 접속 완료\n";
broadcast_message("플레이어 " + client_id + "이(가) 접속했습니다.");
for (;;) {
flat_buffer buffer;
ws->read(buffer);
std::string received_msg = buffers_to_string(buffer.data());
std::cout << "[클라이언트 " << client_id << "]: " << received_msg << std::endl;
// 받은 메시지를 모든 플레이어에게 전송
broadcast_message("[플레이어 " + client_id + "]: " + received_msg);
}
} catch (std::exception& e) {
std::cerr << "클라이언트 연결 종료: " << e.what() << std::endl;
}
}
// 멀티플레이 WebSocket 서버 실행
int main() {
try {
asio::io_context io_context;
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080));
std::cout << "멀티플레이 WebSocket 게임 서버 실행 중 (포트 8080)...\n";
while (true) {
tcp::socket socket(io_context);
acceptor.accept(socket);
std::thread(handle_client, std::move(socket)).detach(); // 개별 플레이어 스레드 실행
}
} catch (std::exception& e) {
std::cerr << "서버 오류 발생: " << e.what() << std::endl;
}
return 0;
}
4. 코드 설명
✅ 플레이어 세션 관리 (클라이언트 ID 부여)
std::unordered_map<std::string, std::shared_ptr<websocket::stream<tcp::socket>>> clients;
std::mutex clients_mutex;
- 플레이어의 WebSocket 세션을 관리하기 위해
unordered_map
사용 std::mutex
를 사용하여 동시 접근 방지
✅ 브로드캐스트 기능 (모든 플레이어에게 메시지 전송)
void broadcast_message(const std::string& message) {
std::lock_guard<std::mutex> lock(clients_mutex);
for (auto& [id, client] : clients) {
if (client) {
client->text(true);
client->write(asio::buffer(message));
}
}
}
- 한 플레이어가 보낸 메시지를 모든 접속된 플레이어에게 전달
std::lock_guard
를 사용하여 멀티스레드 환경에서 안정적인 데이터 접근 보장
✅ 플레이어별 개별 스레드 처리
while (true) {
tcp::socket socket(io_context);
acceptor.accept(socket);
std::thread(handle_client, std::move(socket)).detach();
}
- 새로운 클라이언트가 접속하면
handle_client()
를 실행하는 새로운 스레드를 생성하여 개별 처리
5. 실행 및 테스트 방법
서버 실행
g++ -std=c++17 websocket_game_server.cpp -o server -lboost_system -lpthread -lssl -lcrypto
./server
클라이언트 접속 (WebSocket 클라이언트 테스트)
npm install -g wscat
wscat -c ws://localhost:8080
테스트 시뮬레이션
- 첫 번째 클라이언트 접속:
Connected (press CTRL+C to quit)
서버 출력:
플레이어 140737488355328 접속 완료
- 두 번째 클라이언트 접속 및 메시지 전송:
> Hello from Player 2
모든 클라이언트에게 메시지 브로드캐스트:
[플레이어 140737488355329]: Hello from Player 2
6. WebSocket 게임 서버 요약
✅ WebSocket을 활용한 실시간 게임 서버 구현 가능
✅ 다수의 클라이언트가 동시 접속 및 메시지 송수신 가능
✅ 브로드캐스트를 활용한 멀티플레이 게임 동기화 가능
7. 다음 단계
다음 섹션에서는 WebSocket 서버의 성능 최적화 및 트러블슈팅 방법을 설명합니다.
성능 최적화 및 트러블슈팅
멀티플레이 WebSocket 게임 서버는 다수의 클라이언트와의 실시간 데이터 교환을 처리해야 하므로, 성능 최적화가 필수적입니다. 또한, 네트워크 지연, 데이터 손실, 서버 과부하 등의 문제를 해결하기 위한 트러블슈팅 기법도 중요합니다.
이번 섹션에서는 C++ Asio 기반 WebSocket 서버의 성능 최적화 기법과 트러블슈팅 방법을 다룹니다.
1. WebSocket 성능 최적화 기법
WebSocket 서버의 성능을 높이기 위해 적용할 수 있는 주요 기법은 다음과 같습니다.
✅ 1.1 멀티스레드 최적화 (io_context
풀 사용)
Asio의 io_context
는 비동기 이벤트를 관리하는 핵심 객체입니다. 기본적으로 단일 io_context
는 하나의 스레드에서 실행되지만, 여러 개의 스레드를 할당하면 병렬 처리가 가능해집니다.
해결 방법: io_context
를 여러 개의 스레드에서 실행하여 병렬 처리 강화
적용 코드 (멀티스레드 io_context
실행)
asio::io_context io_context;
std::vector<std::thread> thread_pool;
for (int i = 0; i < std::thread::hardware_concurrency(); ++i) {
thread_pool.emplace_back([&io_context]() {
io_context.run();
});
}
std::thread::hardware_concurrency()
를 사용하여 CPU 코어 수만큼 스레드를 생성io_context.run()
을 호출하여 여러 개의 네트워크 작업을 동시에 처리
✅ 1.2 데이터 전송 최적화 (압축 및 바이너리 전송)
웹 게임 서버에서 데이터 전송량을 줄이는 것이 성능에 큰 영향을 줍니다.
텍스트 기반 JSON 대신, CBOR(Concise Binary Object Representation) 또는 Protobuf 같은 바이너리 포맷을 사용하면 네트워크 대역폭을 절약할 수 있습니다.
해결 방법: JSON 대신 CBOR 또는 Protobuf 사용
적용 코드 (CBOR 변환 후 WebSocket 전송)
#include <nlohmann/json.hpp>
#include <vector>
std::vector<uint8_t> serialize_to_cbor(const nlohmann::json& data) {
return nlohmann::json::to_cbor(data);
}
nlohmann::json::to_cbor()
를 사용하여 CBOR 포맷으로 변환- CBOR는 JSON보다 약 30~50% 작은 크기의 바이너리 데이터로 변환 가능
- 클라이언트에서
CBOR
을 해석하여 게임 데이터 파싱
✅ 1.3 WebSocket Ping/Pong 패킷 활용 (연결 유지)
클라이언트 연결이 비정상적으로 종료될 경우, 서버는 이를 감지하여 불필요한 리소스를 정리해야 합니다.
WebSocket은 ping
과 pong
패킷을 지원하므로 유휴 클라이언트를 감지하고 자동으로 연결을 종료할 수 있습니다.
해결 방법: WebSocket Ping/Pong 메시지 사용
적용 코드 (Ping/Pong 핸들링)
ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
ws.control_callback([](websocket::frame_type kind, boost::beast::string_view payload) {
if (kind == websocket::frame_type::ping) {
std::cout << "Ping received. Responding with Pong." << std::endl;
}
});
ws.set_option()
을 사용하여 WebSocket 타임아웃을 설정ws.control_callback()
을 등록하여ping
을 수신하면pong
으로 응답
✅ 1.4 Zero-Copy 데이터 전송 (asio::buffer
)
Asio에서는 데이터를 복사 없이 전송(Zero-Copy Transmission) 하는 것이 성능을 향상시키는 중요한 기법입니다.
데이터를 std::string
으로 변환하지 않고 메모리 버퍼를 직접 사용하여 전송할 수 있습니다.
해결 방법: asio::buffer()
를 사용하여 메모리 복사 최소화
적용 코드 (Zero-Copy 데이터 전송)
void send_message(websocket::stream<tcp::socket>& ws, const std::vector<uint8_t>& data) {
ws.binary(true);
ws.write(asio::buffer(data));
}
ws.binary(true);
를 설정하여 바이너리 데이터 전송asio::buffer(data);
를 사용하여 데이터 복사 없이 바로 전송
2. WebSocket 서버 트러블슈팅
🚨 2.1 클라이언트가 일정 시간 후 연결이 끊기는 문제
원인
- 서버가 클라이언트의 연결 유지를 위한 Ping/Pong 메시지를 보내지 않음
- 방화벽 또는 프록시가 비활성 상태의 연결을 자동으로 종료
해결 방법
set_option()
을 사용하여 서버 타임아웃을 조정하고,ping
을 주기적으로 전송
적용 코드
ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
🚨 2.2 클라이언트에서 메시지를 보냈는데 서버가 응답하지 않는 문제
원인
- 비동기 작업 중 오류 발생
- 클라이언트가 보낸 메시지가 WebSocket 형식이 아님 (예: HTTP 요청)
해결 방법
try-catch
블록을 사용하여 예외를 잡고 디버깅
적용 코드
try {
ws.read(buffer);
} catch (std::exception& e) {
std::cerr << "WebSocket 오류 발생: " << e.what() << std::endl;
}
🚨 2.3 클라이언트가 갑자기 접속 종료될 때 서버가 크래시 되는 문제
원인
- 클라이언트가 비정상적으로 종료되었을 때, 서버가 해당 세션을 처리하는 도중 접근 오류 발생
해결 방법
try-catch
블록을 사용하여 예외를 처리하고, 비정상적인 세션을 정리
적용 코드
try {
ws.read(buffer);
} catch (boost::beast::system_error const& se) {
if (se.code() == websocket::error::closed) {
std::cout << "클라이언트가 연결을 종료했습니다." << std::endl;
} else {
std::cerr << "WebSocket 오류: " << se.what() << std::endl;
}
}
3. 성능 최적화 및 트러블슈팅 요약
최적화 기법 | 설명 |
---|---|
io_context 멀티스레딩 | 여러 개의 스레드에서 io_context 실행 |
데이터 압축 | CBOR/Protobuf 사용하여 전송량 절감 |
Ping/Pong 패킷 | 클라이언트 연결 상태 모니터링 |
Zero-Copy 전송 | asio::buffer() 활용하여 성능 향상 |
트러블슈팅 문제 | 해결 방법 |
---|---|
일정 시간 후 연결 종료 | ping/pong 메시지 활용 |
클라이언트 메시지 인식 오류 | try-catch 로 WebSocket 오류 처리 |
클라이언트 비정상 종료로 서버 크래시 | boost::beast::system_error 체크 |
4. 다음 단계
WebSocket 서버의 최적화 및 문제 해결 방법을 배웠습니다.
다음 섹션에서는 최종적으로 WebSocket 게임 서버의 핵심 개념을 요약합니다.
요약
이번 기사에서는 C++ Asio를 활용한 WebSocket 게임 서버 구현에 대해 다루었습니다. WebSocket은 양방향 통신(Full-Duplex)을 지원하여 실시간 데이터 교환이 필수적인 온라인 게임에서 효과적으로 사용됩니다.
주요 내용 요약
✅ Asio 및 WebSocket 개요
- Asio는 비동기 네트워크 프로그래밍을 지원하는 C++ 라이브러리
- WebSocket은 지속적인 연결을 유지하며, 실시간 통신이 가능
✅ TCP 소켓 서버 및 WebSocket 핸드셰이크
- TCP 소켓 서버를 Asio로 구현한 후, WebSocket 핸드셰이크를 처리하여 WebSocket 연결 수립
✅ WebSocket 데이터 송수신 처리
ws.read(buffer)
로 클라이언트 메시지를 읽고,ws.write(asio::buffer(response))
로 응답
✅ 멀티스레드 환경에서 성능 최적화
io_context
를 멀티스레드로 실행하여 다수의 클라이언트 처리 성능 향상- 개별 클라이언트 요청을 스레드로 분리하여 병렬 처리
✅ 게임 서버에서 WebSocket 활용
- WebSocket을 사용하여 플레이어 간 실시간 메시지 브로드캐스트 구현
- CBOR 또는 Protobuf을 사용하여 데이터 전송 최적화
✅ 성능 최적화 및 트러블슈팅
ping/pong
패킷을 활용하여 클라이언트 연결 유지- Zero-Copy 데이터 전송 (
asio::buffer()
)을 사용하여 메모리 복사 최소화 - 예외 처리를 강화하여 클라이언트 연결 오류로 인한 서버 크래시 방지
결론 및 확장 가능성
- WebSocket을 활용하면 낮은 지연시간(Latency)과 실시간 통신이 필요한 게임 서버를 구축할 수 있음
- 추가적으로 부하 분산(Load Balancing), 데이터 암호화(SSL/TLS WebSocket) 등을 적용하면 더욱 확장성 높은 게임 서버를 개발할 수 있음
이번 내용을 통해 C++ Asio 기반으로 WebSocket 서버를 구현하고, 게임 서버 개발에 적용하는 방법을 익혔습니다. 🚀