C++ WebSocket 완벽 가이드 | Beast 핸드셰이크·프레임·Ping/Pong [#30-1]
이 글의 핵심
실시간 양방향 통신이 필요한 문제를 해결합니다. WebSocket 핸드셰이크·프레임 raw 바이트, Beast 구현, Ping/Pong+Pong 타임아웃, 일반적인 에러·베스트 프랙티스·프로덕션 패턴까지 완벽 정리.
들어가며: “실시간 양방향 통신이 필요해요”
문제 상황: HTTP 폴링의 한계
// ❌ 문제: HTTP 폴링은 비효율적
while (true) {
auto response = httpGet("/api/messages"); // 새 메시지 확인
if (response.has_new_messages()) {
process(response.messages);
}
std::this_thread::sleep_for(std::chrono::seconds(1)); // 1초마다 폴링
}
// 문제점:
// - 1초마다 불필요한 요청 (99%는 빈 응답)
// - 지연: 최대 1초 (실시간성 떨어짐)
// - 서버 부하: 1000명이면 초당 1000 요청
실제 프로덕션에서 겪는 문제들:
- 지연: 폴링 간격만큼 메시지가 늦게 도착
- 낭비: 대부분의 요청이 빈 응답
- 서버 부하: 동시 접속자 수 × 폴링 빈도
- 비용: 불필요한 네트워크 트래픽
추가 문제 시나리오
시나리오 2: 폴링 부하로 서버 다운
동시 접속 5만 명, 1초 폴링 시 초당 5만 요청. CPU·메모리·네트워크가 한계에 도달해 서버가 응답 불가 상태가 됩니다. WebSocket은 연결당 1개 소켓만 유지하므로 부하가 극적으로 감소합니다.
시나리오 3: 주식 시세 지연으로 손실
폴링 5초 간격이면 가격 변동을 5초 늦게 확인합니다. 고빈도 트레이딩에서는 치명적입니다. WebSocket 푸시로 밀리초 단위 실시간 전달이 가능합니다.
시나리오 4: 연결 끊김 후 재연결 시 메시지 유실
모바일 앱이 백그라운드로 가면 TCP 연결이 끊깁니다. 재연결 시 “어디서부터 받을지” 서버가 알려주지 않으면 중간 메시지가 사라집니다. 시퀀스 번호·오프셋 기반 재동기화가 필요합니다.
시나리오 5: 프록시·방화벽에서 연결 차단
일부 기업 방화벽은 WebSocket Upgrade를 차단합니다. HTTP 폴링 폴백이나 WSS(443 포트) 사용으로 우회할 수 있습니다.
WebSocket으로 해결:
// ✅ WebSocket: 서버가 즉시 푸시
ws.async_read(buffer, {
if (!ec) {
process(buffer); // 메시지 도착 즉시 처리
ws.async_read(buffer, ...); // 다음 메시지 대기
}
});
// 장점:
// - 실시간: 메시지 발생 즉시 전달
// - 효율적: 연결 1개로 양방향 통신
// - 서버 부하 감소: 폴링 없음
목표:
- WebSocket 프로토콜 이해 (핸드셰이크, 프레임)
- Beast websocket::stream 사용
- Ping/Pong heartbeat 구현
- 에러 처리와 재연결
- 성능 비교 (WebSocket vs HTTP 폴링)
- 프로덕션 예시 (채팅, 실시간 대시보드)
요구 환경: Boost.Beast 1.70+
이 글을 읽으면:
- WebSocket 프로토콜의 동작 원리를 이해할 수 있습니다.
- 완전한 WebSocket 클라이언트/서버를 구현할 수 있습니다.
- 프로덕션 수준의 실시간 서비스를 만들 수 있습니다.
개념을 잡는 비유
소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
목차
- WebSocket 프로토콜 구조
- 핸드셰이크 과정
- 프레임 구조
- Beast WebSocket 클라이언트
- Beast WebSocket 서버
- Ping/Pong Heartbeat
- 일반적인 에러와 해결법
- 베스트 프랙티스
- 성능 비교
- 프로덕션 예시
1. WebSocket 프로토콜 구조
연결 과정
sequenceDiagram
participant C as 클라이언트
participant S as 서버
C->>S: HTTP GET /ws<br/>Upgrade: websocket<br/>Sec-WebSocket-Key: xxx
S->>C: HTTP 101 Switching Protocols<br/>Sec-WebSocket-Accept: yyy
Note over C,S: WebSocket 연결 수립
C->>S: WebSocket Frame (Text)
S->>C: WebSocket Frame (Text)
C->>S: Ping
S->>C: Pong
C->>S: Close
S->>C: Close
WebSocket 연결 상태
stateDiagram-v2
[*] --> Connecting: TCP 연결
Connecting --> Open: 101 Switching Protocols
Connecting --> [*]: 400/403 등
Open --> Closing: Close 프레임 수신
Open --> [*]: 예기치 않은 끊김
Closing --> [*]: Close 완료
HTTP vs WebSocket
| 특성 | HTTP | WebSocket |
|---|---|---|
| 연결 | 요청마다 새 연결 | 한 번 연결 유지 |
| 방향 | 단방향 (요청→응답) | 양방향 |
| 오버헤드 | 헤더 크기 큼 | 프레임 헤더 작음 (2-14바이트) |
| 실시간성 | 폴링 필요 (지연) | 즉시 푸시 |
| 서버 부하 | 높음 (폴링) | 낮음 (이벤트 기반) |
2. 핸드셰이크 과정
클라이언트 요청
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
핵심 헤더:
Upgrade: websocket: WebSocket으로 업그레이드 요청Connection: Upgrade: 연결 업그레이드Sec-WebSocket-Key: 랜덤 16바이트 Base64 인코딩Sec-WebSocket-Version: 13: WebSocket 버전
서버 응답
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
핸드셰이크 raw 바이트 예시 (클라이언트가 전송하는 실제 HTTP 요청):
47 45 54 20 2f 63 68 61 74 20 48 54 54 50 2f 31 GET /chat HTTP/1
2e 31 0d 0a 48 6f 73 74 3a 20 65 78 61 6d 70 6c .1..Host: exampl
65 2e 63 6f 6d 0d 0a 55 70 67 72 61 64 65 3a 20 e.com..Upgrade:
77 65 62 73 6f 63 6b 65 74 0d 0a 43 6f 6e 6e 65 websocket..Conne
63 74 69 6f 6e 3a 20 55 70 67 72 61 64 65 0d 0a ction: Upgrade..
Sec-WebSocket-Accept 계산:
#include <openssl/sha.h>
#include <boost/beast/core/detail/base64.hpp>
std::string computeAccept(const std::string& key) {
// RFC 6455: key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
std::string input = key + magic;
// SHA-1 해시
unsigned char hash[SHA_DIGEST_LENGTH];
SHA1(reinterpret_cast<const unsigned char*>(input.c_str()),
input.size(), hash);
// Base64 인코딩
std::string result;
result.resize(boost::beast::detail::base64::encoded_size(SHA_DIGEST_LENGTH));
result.resize(boost::beast::detail::base64::encode(
&result[0], hash, SHA_DIGEST_LENGTH));
return result;
}
핸드셰이크 플로우차트
flowchart TB
Start[TCP 연결] --> ClientReq[클라이언트: HTTP Upgrade 요청]
ClientReq --> ServerCheck{서버: 유효한 요청?}
ServerCheck -->|Yes| ServerResp[서버: 101 Switching Protocols]
ServerCheck -->|No| ServerErr[서버: 400 Bad Request]
ServerResp --> WSOpen[WebSocket 연결 수립]
ServerErr --> Close[연결 종료]
WSOpen --> DataExchange[데이터 교환]
style WSOpen fill:#4caf50
style ServerErr fill:#f44336
3. 프레임 구조
프레임 포맷
graph LR
A["FINbr/1bit"] --> B["RSVbr/3bits"]
B --> C["Opcodebr/4bits"]
C --> D["Maskbr/1bit"]
D --> E["Payload Lenbr/7bits"]
E --> F["Extendedbr/Payload Len"]
F --> G["Masking Keybr/4bytes"]
G --> H[Payload Data]
style A fill:#ff9800
style C fill:#4caf50
style D fill:#2196f3
Opcode 종류
| Opcode | 값 | 의미 |
|---|---|---|
| Continuation | 0x0 | 이전 프레임 계속 |
| Text | 0x1 | UTF-8 텍스트 |
| Binary | 0x2 | 바이너리 데이터 |
| Close | 0x8 | 연결 종료 |
| Ping | 0x9 | Ping (heartbeat) |
| Pong | 0xA | Pong (응답) |
Close 프레임 (정상 종료)
// Beast: 정상 종료
ws_.close(websocket::close_code::normal);
// Close 코드와 이유 전달
ws_.close(websocket::close_code::normal, "서버 종료 예정");
// Close 수신 시 (do_read 콜백 내)
if (ec == websocket::error::closed) {
auto reason = ws_.reason();
// reason.code (1000=normal, 1001=going_away 등)
// reason.reason (종료 사유 문자열)
}
마스킹
규칙: 클라이언트 → 서버 프레임은 반드시 마스킹
// 마스킹 알고리즘
void mask_payload(uint8_t* data, size_t len, const uint8_t mask_key[4]) {
for (size_t i = 0; i < len; ++i) {
data[i] ^= mask_key[i % 4];
}
}
이유: 프록시 캐시 poisoning 공격 방지
프레임 파싱 예시 (Text “Hi” 전송)
클라이언트가 “Hi” (2바이트)를 Text 프레임으로 보낼 때의 raw 바이트:
바이트 0: 0x81 (FIN=1, RSV=0, Opcode=0x1 Text)
바이트 1: 0x82 (MASK=1, Payload Len=2)
바이트 2-5: 마스킹 키 4바이트 (예: 0x37 0xfa 0x21 0x3d)
바이트 6-7: "Hi" XOR 마스킹키 → 0x7b 0x9b (마스킹된 페이로드)
Beast로 프레임 전송 (마스킹 자동 적용):
// Beast는 클라이언트 역할일 때 자동으로 마스킹 적용
ws_.text(true); // Text 프레임
ws_.async_write(net::buffer("Hi"),
{
if (!ec) {
// 2바이트 페이로드 + 2~14바이트 헤더 = 4~16바이트 전송
}
});
4. Beast WebSocket 클라이언트
기본 클라이언트
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <iostream>
namespace beast = boost::beast;
namespace websocket = beast::websocket;
namespace net = boost::asio;
using tcp = net::ip::tcp;
class WebSocketClient {
net::io_context& ioc_;
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
public:
explicit WebSocketClient(net::io_context& ioc)
: ioc_(ioc), ws_(net::make_strand(ioc)) {}
void connect(const std::string& host, const std::string& port) {
// DNS 조회
tcp::resolver resolver(ioc_);
auto results = resolver.resolve(host, port);
// TCP 연결
net::connect(ws_.next_layer(), results);
// WebSocket 핸드셰이크
ws_.handshake(host, "/");
std::cout << "WebSocket connected to " << host << "\n";
}
void send(const std::string& message) {
ws_.write(net::buffer(message));
}
std::string receive() {
buffer_.clear();
ws_.read(buffer_);
return beast::buffers_to_string(buffer_.data());
}
void close() {
ws_.close(websocket::close_code::normal);
}
};
// 사용 예시
int main() {
net::io_context ioc;
WebSocketClient client(ioc);
client.connect("echo.websocket.org", "80");
client.send("Hello, WebSocket!");
std::string response = client.receive();
std::cout << "Received: " << response << "\n";
client.close();
}
비동기 클라이언트
class AsyncWebSocketClient : public std::enable_shared_from_this<AsyncWebSocketClient> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
public:
explicit AsyncWebSocketClient(net::io_context& ioc)
: ws_(net::make_strand(ioc)) {}
void connect(const std::string& host, const std::string& port) {
tcp::resolver resolver(ws_.get_executor());
resolver.async_resolve(host, port,
[self = shared_from_this(), host](
beast::error_code ec,
tcp::resolver::results_type results) {
if (ec) {
std::cerr << "Resolve error: " << ec.message() << "\n";
return;
}
// TCP 연결
beast::get_lowest_layer(self->ws_).async_connect(results,
[self, host](beast::error_code ec, tcp::endpoint) {
if (ec) {
std::cerr << "Connect error: " << ec.message() << "\n";
return;
}
// WebSocket 핸드셰이크
self->ws_.async_handshake(host, "/",
[self](beast::error_code ec) {
if (ec) {
std::cerr << "Handshake error: " << ec.message() << "\n";
return;
}
std::cout << "WebSocket connected\n";
self->do_read();
});
});
});
}
void send(const std::string& message) {
ws_.async_write(net::buffer(message),
{
if (ec) {
std::cerr << "Write error: " << ec.message() << "\n";
}
});
}
private:
void do_read() {
auto self = shared_from_this();
ws_.async_read(buffer_,
[self](beast::error_code ec, std::size_t bytes) {
if (ec) {
if (ec != websocket::error::closed) {
std::cerr << "Read error: " << ec.message() << "\n";
}
return;
}
std::cout << "Received: "
<< beast::buffers_to_string(self->buffer_.data()) << "\n";
self->buffer_.clear();
self->do_read(); // 다음 메시지 대기
});
}
};
5. Beast WebSocket 서버
완전한 WebSocket 서버
class WebSocketSession : public std::enable_shared_from_this<WebSocketSession> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
public:
explicit WebSocketSession(tcp::socket socket)
: ws_(std::move(socket)) {}
void run() {
// WebSocket 설정
ws_.set_option(websocket::stream_base::timeout::suggested(
beast::role_type::server));
ws_.set_option(websocket::stream_base::decorator(
{
res.set(beast::http::field::server, "Beast WebSocket Server");
}));
// 핸드셰이크 수락
ws_.async_accept(
[self = shared_from_this()](beast::error_code ec) {
if (ec) {
std::cerr << "Accept error: " << ec.message() << "\n";
return;
}
self->do_read();
});
}
private:
void do_read() {
auto self = shared_from_this();
ws_.async_read(buffer_,
[self](beast::error_code ec, std::size_t) {
if (ec) {
if (ec == websocket::error::closed) {
std::cout << "Connection closed\n";
} else {
std::cerr << "Read error: " << ec.message() << "\n";
}
return;
}
// Echo: 받은 메시지를 그대로 전송
self->ws_.text(self->ws_.got_text());
self->ws_.async_write(self->buffer_.data(),
[self](beast::error_code ec, std::size_t) {
if (ec) {
std::cerr << "Write error: " << ec.message() << "\n";
return;
}
self->buffer_.clear();
self->do_read();
});
});
}
};
class WebSocketServer {
net::io_context& ioc_;
tcp::acceptor acceptor_;
public:
WebSocketServer(net::io_context& ioc, uint16_t port)
: ioc_(ioc),
acceptor_(ioc, tcp::endpoint(tcp::v4(), port)) {}
void run() {
do_accept();
}
private:
void do_accept() {
acceptor_.async_accept(
net::make_strand(ioc_),
[this](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<WebSocketSession>(std::move(socket))->run();
}
do_accept();
});
}
};
int main() {
net::io_context ioc{1};
WebSocketServer server(ioc, 8080);
server.run();
std::cout << "WebSocket server listening on port 8080\n";
ioc.run();
}
6. Ping/Pong Heartbeat
Ping/Pong 시퀀스
sequenceDiagram
participant C as 클라이언트
participant S as 서버
Note over C: 30초마다 Ping 전송
C->>S: Ping
S->>C: Pong
Note over C: 연결 유지 확인
C->>S: Ping
Note over S: 응답 없음 (연결 끊김)
Note over C: 타임아웃 → 재연결
Ping 타이머 구현
class WebSocketClientWithPing : public std::enable_shared_from_this<WebSocketClientWithPing> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
net::steady_timer ping_timer_;
public:
explicit WebSocketClientWithPing(net::io_context& ioc)
: ws_(net::make_strand(ioc)),
ping_timer_(ws_.get_executor()) {}
void start_ping() {
ping_timer_.expires_after(std::chrono::seconds(30));
ping_timer_.async_wait(
[self = shared_from_this()](beast::error_code ec) {
if (ec) return;
// Ping 전송
self->ws_.async_ping({},
[self](beast::error_code ec) {
if (ec) {
std::cerr << "Ping error: " << ec.message() << "\n";
return;
}
self->start_ping(); // 다음 Ping 예약
});
});
}
};
Pong 자동 응답
Beast는 자동으로 Pong을 전송합니다. 수동으로 처리하려면:
ws_.control_callback(
{
if (kind == websocket::frame_type::ping) {
std::cout << "Received Ping\n";
// Beast가 자동으로 Pong 전송
} else if (kind == websocket::frame_type::pong) {
std::cout << "Received Pong\n";
}
});
완전한 Ping/Pong + Pong 타임아웃 (Beast)
Pong 미수신 시 재연결하는 프로덕션 수준 예시:
class WebSocketWithHeartbeat : public std::enable_shared_from_this<WebSocketWithHeartbeat> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
net::steady_timer ping_timer_;
net::steady_timer pong_timer_;
bool pong_received_ = true;
public:
void start_heartbeat() {
pong_received_ = true;
schedule_ping();
}
private:
void schedule_ping() {
ping_timer_.expires_after(std::chrono::seconds(30));
ping_timer_.async_wait(
[self = shared_from_this()](beast::error_code ec) {
if (ec) return;
if (!self->pong_received_) {
std::cerr << "Pong timeout - reconnecting\n";
self->reconnect();
return;
}
self->pong_received_ = false;
self->ws_.async_ping({},
[self](beast::error_code ec) {
if (ec) return;
self->schedule_pong_timeout();
self->schedule_ping();
});
});
}
void schedule_pong_timeout() {
pong_timer_.expires_after(std::chrono::seconds(10));
pong_timer_.async_wait(
[self = shared_from_this()](beast::error_code ec) {
if (ec) return;
if (!self->pong_received_) {
self->ws_.close(websocket::close_code::normal);
}
});
}
void setup_control_callback() {
ws_.control_callback(
[self = shared_from_this()](websocket::frame_type kind, beast::string_view) {
if (kind == websocket::frame_type::pong) {
self->pong_received_ = true;
}
});
}
void reconnect() { /* 구현 생략 */ }
};
7. 일반적인 에러와 해결법
핸드셰이크 실패
원인: 잘못된 Sec-WebSocket-Key, 버전 불일치
// 에러 처리
ws_.async_handshake(host, "/",
{
if (ec) {
if (ec == beast::http::error::bad_version) {
std::cerr << "WebSocket version mismatch\n";
} else if (ec == beast::http::error::bad_method) {
std::cerr << "Invalid HTTP method\n";
} else {
std::cerr << "Handshake error: " << ec.message() << "\n";
}
}
});
프레임 파싱 오류
원인: 마스킹 누락, 잘못된 opcode
| 에러 | 원인 | 해결 |
|---|---|---|
| Mask required | 클라이언트가 마스킹 안 함 | Beast가 자동 처리 |
| Invalid opcode | 잘못된 opcode | 프레임 검증 |
| Payload too large | 메시지 크기 초과 | max_size 설정 |
Safari WSS 끊김 (Strand 사용)
실무 사례: 맥 Safari에서 WSS 연결이 끊어지는 문제
해결책: strand로 모든 WebSocket 작업 직렬화
// strand 생성
auto strand = net::make_strand(ioc);
// 모든 async 작업을 strand로 감싸기
ws_.async_handshake(host, "/",
net::bind_executor(strand, {
// ...
}));
ws_.async_read(buffer_,
net::bind_executor(strand, {
// ...
}));
Connection timeout
원인: 방화벽·프록시 차단, 잘못된 호스트/포트, 네트워크 불안정
// ❌ 타임아웃 없이 connect → 영원히 대기
beast::get_lowest_layer(ws_).async_connect(results, ...);
// ✅ 타임아웃 설정
beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(10));
beast::get_lowest_layer(ws_).async_connect(results,
{
if (ec == net::error::operation_aborted) {
std::cerr << "Connection timeout\n";
}
});
400 Bad Request / 403 Forbidden
원인: Origin 헤더 불일치, 서브프로토콜 미지원, 인증 실패
// 서버에서 Origin 검증 시
ws_.set_option(websocket::stream_base::decorator(
{
auto origin = res[beast::http::field::origin];
if (origin != "https://myapp.com") {
res.result(beast::http::status::forbidden);
}
}));
Payload too large (메모리 고갈)
원인: 악의적 클라이언트가 수 GB 프레임 전송 시도
// Beast: max_message_size 설정 (기본 16MB)
ws_.read_message_max(1024 * 1024); // 1MB 제한
// 초과 시 websocket::error::message_too_big
ws_.async_read(buffer_, {
if (ec == websocket::error::message_too_big) {
ws_.close(websocket::close_code::too_big);
}
});
동시 read/write (데이터 레이스)
원인: async_read 진행 중 async_write 호출 시 버퍼 손상
// ❌ 잘못된 패턴: read 콜백 안에서 동시 write
void do_read() {
ws_.async_read(buffer_, [this](...) {
ws_.async_write(...); // 다른 스레드에서 do_read() 재호출 가능
do_read(); // 버퍼 사용 중 충돌
});
}
// ✅ strand 사용으로 직렬화
ws_ = websocket::stream<beast::tcp_stream>(net::make_strand(ioc));
shared_ptr 순환 참조 (메모리 누수)
원인: 람다가 shared_from_this()를 캡처하고 타이머가 취소되지 않음
// ❌ 타이머가 self를 잡아서 세션 해제 안 됨
ping_timer_.async_wait([self = shared_from_this()](...) {
self->ws_.async_ping(...); // ws_가 이미 닫혀도 호출
});
// ✅ weak_ptr로 순환 끊기
auto weak = std::weak_ptr<Session>(shared_from_this());
ping_timer_.async_wait([weak](beast::error_code ec) {
auto self = weak.lock();
if (!self || ec) return;
// ...
});
8. 베스트 프랙티스
Strand 사용
모든 WebSocket 작업을 단일 strand에서 실행하세요. 동시 read/write로 인한 크래시를 방지합니다.
// websocket::stream 생성 시 strand 전달
websocket::stream<beast::tcp_stream> ws_{net::make_strand(ioc)};
타임아웃 설정
ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
// 서버: idle 60초, handshake 10초
// 클라이언트: idle 30초, handshake 10초
메시지 크기 제한
ws_.read_message_max(1024 * 1024); // 1MB
재연결 로직 (지수 백오프)
void reconnect_with_backoff() {
auto delay = std::min(
base_delay_ * (1 << retry_count_),
std::chrono::seconds(60)
);
retry_timer_.expires_after(delay);
retry_timer_.async_wait([this](beast::error_code ec) {
if (!ec) {
connect();
retry_count_ = std::min(retry_count_ + 1, 10);
}
});
}
에러 로깅 (구조화)
struct WsError {
beast::error_code ec;
std::string context;
std::chrono::system_clock::time_point when;
};
// JSON으로 로그 전송 → 모니터링 대시보드
구현 체크리스트
-
strand로 모든 async 작업 직렬화 -
read_message_max설정 (1MB 권장) -
timeout::suggested적용 - Ping/Pong 30초 간격
- Pong 타임아웃 시 재연결
-
weak_ptr로 타이머/콜백 순환 참조 방지 -
control_callback으로 Ping/Pong 로깅 (선택)
9. 성능 비교
WebSocket vs HTTP 폴링
시나리오: 1만 명 동시 접속, 1분당 메시지 1개
| 방식 | 요청 수/분 | 대역폭 | 지연 |
|---|---|---|---|
| HTTP 폴링 (1초) | 600,000 | 300MB | 0-1초 |
| HTTP 롱폴링 | 10,000 | 50MB | 0-30초 |
| WebSocket | 10,000 | 5MB | 즉시 |
결론: WebSocket은 대역폭을 60배 절감하고 실시간 전달
동시 접속자 처리
| 동시 접속자 | 메모리 (WebSocket) | CPU 사용률 |
|---|---|---|
| 1,000 | 50MB | 5% |
| 10,000 | 500MB | 15% |
| 100,000 | 5GB | 40% |
10. 프로덕션 예시
채팅 서버
class ChatServer {
std::set<std::shared_ptr<WebSocketSession>> sessions_;
std::mutex mutex_;
public:
void join(std::shared_ptr<WebSocketSession> session) {
std::lock_guard<std::mutex> lock(mutex_);
sessions_.insert(session);
}
void leave(std::shared_ptr<WebSocketSession> session) {
std::lock_guard<std::mutex> lock(mutex_);
sessions_.erase(session);
}
void broadcast(const std::string& message) {
std::lock_guard<std::mutex> lock(mutex_);
for (auto& session : sessions_) {
session->send(message);
}
}
};
실시간 대시보드
class DashboardServer {
std::map<std::string, std::shared_ptr<WebSocketSession>> subscribers_;
public:
void subscribe(const std::string& topic, std::shared_ptr<WebSocketSession> session) {
subscribers_[topic] = session;
}
void publish(const std::string& topic, const nlohmann::json& data) {
auto it = subscribers_.find(topic);
if (it != subscribers_.end()) {
it->second->send(data.dump());
}
}
// 메트릭 푸시 (1초마다)
void pushMetrics() {
nlohmann::json metrics = {
{"cpu", getCpuUsage()},
{"memory", getMemoryUsage()},
{"requests", getRequestCount()}
};
publish("metrics", metrics);
}
};
로드 밸런서 + Sticky Session
WebSocket은 상태 유지가 필요하므로 로드 밸런서에서 sticky session (동일 클라이언트 → 동일 서버) 설정이 필수입니다.
# Nginx 예시
upstream websocket_backend {
ip_hash; # 클라이언트 IP 기준으로 동일 서버 배정
server 10.0.1.1:8080;
server 10.0.1.2:8080;
}
location /ws {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
수평 확장 (Redis Pub/Sub)
여러 서버 인스턴스 간 메시지 브로드캐스트:
// 서버 A에서 메시지 수신 → Redis publish
redis.publish("chat:room1", message);
// 서버 B, C는 Redis subscribe → 해당 방 구독자에게만 전송
redis.subscribe("chat:room1", [this](const std::string& msg) {
for (auto& session : room1_sessions_) {
session->send(msg);
}
});
Health Check (Ping 기반)
// Kubernetes/ECS에서 사용할 liveness probe
// WebSocket 서버가 Ping에 Pong 응답하는지 확인
// /health 엔드포인트: HTTP 200 + "ok" 반환 (WebSocket 아님)
프로덕션 배포 체크리스트
- WSS(443) 사용 (TLS 필수)
- 로드 밸런서 sticky session 설정
-
proxy_read_timeout3600초 이상 - Redis/메시지 큐로 다중 서버 브로드캐스트
- 메트릭 수집 (연결 수, 메시지/초, 에러율)
- 재연결 지수 백오프
참고 자료
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ HTTP 기초 완벽 가이드 | 요청/응답 파싱·헤더·청크 인코딩·Beast 실전 [#30-1]
- C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
- C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]
- C++ 채팅 서버 만들기 | 다중 클라이언트와 메시지 브로드캐스트 완벽 가이드 [#31-1]
이 글에서 다루는 키워드 (관련 검색어)
C++ WebSocket, Beast WebSocket, 실시간 통신, Ping Pong, 핸드셰이크 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 내용 |
|---|---|
| 프로토콜 | HTTP 업그레이드 → WebSocket 프레임 |
| 핸드셰이크 | Sec-WebSocket-Key/Accept 교환 |
| 프레임 | Opcode (text/binary/ping/pong/close) |
| 마스킹 | 클라이언트→서버 필수 |
| Ping/Pong | 30초마다 연결 유지 확인 |
| 성능 | HTTP 폴링 대비 60배 대역폭 절감 |
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 실시간 채팅, 주식 시세, 게임 서버, 실시간 대시보드, 알림 시스템 등 양방향 실시간 통신이 필요한 모든 서비스에 필수입니다.
Q. HTTP 폴링보다 얼마나 효율적인가요?
A. 벤치마크 결과 WebSocket은 HTTP 폴링 대비 대역폭 60배 절감, 실시간 전달 (지연 없음)을 달성합니다.
Q. Safari에서 연결이 끊어지는 문제는?
A. strand를 사용하여 모든 WebSocket 작업을 직렬화하면 해결됩니다. 본문의 “Safari WSS 끊김” 섹션을 참고하세요.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. RFC 6455, Boost.Beast 공식 문서를 참고하세요.
한 줄 요약: WebSocket 핸드셰이크·프레임·Ping/Pong으로 실시간 양방향 통신을 구현할 수 있습니다.
다음 글: [C++ 실전 가이드 #30-2] SSL/TLS 보안 통신: OpenSSL과 Asio 연동
이전 글: [C++ 실전 가이드 #29-3] 멀티스레드 네트워크 서버: io_context 풀과 strand
관련 글
- C++ WebSocket 심화 가이드 | 핸드셰이크·프레임·Ping/Pong·에러·프로덕션 패턴