C++ WebSocket 심화 가이드 | 핸드셰이크·프레임·Ping/Pong·에러·프로덕션 패턴
이 글의 핵심
C++ WebSocket 심화 가이드에 대해 정리한 개발 블로그 글입니다. NAT 테이블과 방화벽은 유휴(idle) TCP 연결을 일정 시간 후 정리합니다. WebSocket은 한 번 연결하면 오랫동안 데이터를 주고받지 않을 수 있어, 중간 장비가 "사용하지 않는 연결"로 판단해 끊어버립니다. 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드:…
들어가며: “WebSocket 연결이 자꾸 끊겨요”
문제 시나리오 1: 연결이 30초마다 끊김
// ❌ 문제: NAT/방화벽이 유휴 연결을 끊음
// 클라이언트가 30초 이상 메시지를 보내지 않으면
// 중간 라우터/방화벽이 TCP 연결을 종료
ws_.async_read(buffer_, {
// ec == connection_reset 또는 connection_aborted
});
왜 이런 일이 발생할까요?
NAT 테이블과 방화벽은 유휴(idle) TCP 연결을 일정 시간 후 정리합니다. WebSocket은 한 번 연결하면 오랫동안 데이터를 주고받지 않을 수 있어, 중간 장비가 “사용하지 않는 연결”로 판단해 끊어버립니다.
해결책: Ping/Pong heartbeat를 20~30초 간격으로 전송해 “연결이 살아 있음”을 증명합니다.
문제 시나리오 2: 핸드셰이크 400 Bad Request
// ❌ 문제: 서버가 400 Bad Request 반환
ws_.async_handshake(host, "/chat",
{
// ec == bad_request
// 서버 로그: "Missing Sec-WebSocket-Key"
});
원인: Sec-WebSocket-Key 누락, 잘못된 Upgrade 헤더, 버전 불일치 등. RFC 6455를 정확히 따르지 않으면 핸드셰이크가 실패합니다.
문제 시나리오 3: 대용량 메시지로 메모리 폭발
// ❌ 문제: 100MB 메시지 수신 시 OOM
ws_.async_read(buffer_, {
// bytes == 100 * 1024 * 1024
// buffer가 100MB 할당 → 서버 크래시
});
해결책: read_message_max로 최대 메시지 크기 제한. Beast 기본값은 16MB이지만, 서비스 특성에 맞게 조정해야 합니다.
추가 문제 시나리오
시나리오 4: Safari/Chrome에서 WSS 연결 끊김
멀티스레드 io_context에서 WebSocket 작업이 여러 스레드에 분산되면, 일부 브라우저에서 타이밍 이슈로 연결이 끊깁니다. strand로 직렬화하면 해결됩니다.
시나리오 5: 재연결 시 무한 루프
연결 실패 시 즉시 재연결을 시도하면 서버에 부하가 집중됩니다. 지수 백오프(exponential backoff)로 재시도 간격을 늘려야 합니다.
시나리오 6: 브로드캐스트 시 쓰기 경합
수천 개 세션에 동시에 async_write를 호출하면 io_context 큐가 폭주합니다. 큐 기반 전송 또는 조건부 전송으로 백프레셔를 적용해야 합니다.
목표:
- 핸드셰이크 바이트 단위 분석
- 프레임 구조 완전 예제 (Text, Binary, Ping, Pong, Close)
- Ping/Pong heartbeat 완전 구현
- 일반적인 에러와 해결법
- 베스트 프랙티스 (재연결, 백프레셔, strand)
- 프로덕션 패턴 (모니터링, graceful shutdown)
요구 환경: Boost.Beast 1.70+, C++17 이상
개념을 잡는 비유
소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
목차
- 핸드셰이크 완전 분석
- 프레임 구조와 완전 예제
- Ping/Pong Heartbeat 완전 구현
- Beast WebSocket 완전 예제
- 자주 발생하는 에러
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
1. 핸드셰이크 완전 분석
HTTP 업그레이드 요청 (클라이언트 → 서버)
WebSocket 연결은 HTTP Upgrade 요청으로 시작합니다. RFC 6455를 따르는 정확한 요청 예시입니다.
GET /chat HTTP/1.1
Host: example.com:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
핵심 헤더 설명:
| 헤더 | 필수 | 설명 |
|---|---|---|
Upgrade: websocket | ✅ | WebSocket 프로토콜로 업그레이드 요청 |
Connection: Upgrade | ✅ | HTTP 연결 업그레이드 |
Sec-WebSocket-Key | ✅ | 랜덤 16바이트 Base64 (프록시 캐시 방지) |
Sec-WebSocket-Version: 13 | ✅ | WebSocket 버전 (13만 지원) |
Origin | 권장 | CORS 검증용 (브라우저) |
Sec-WebSocket-Protocol | 선택 | 서브프로토콜 (예: chat, json) |
Sec-WebSocket-Key 생성
#include <random>
#include <boost/beast/core/detail/base64.hpp>
// RFC 6455: 16바이트 랜덤 → Base64
std::string generate_websocket_key() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 255);
unsigned char key[16];
for (int i = 0; i < 16; ++i) {
key[i] = static_cast<unsigned char>(dis(gen));
}
std::string result;
result.resize(boost::beast::detail::base64::encoded_size(16));
result.resize(boost::beast::detail::base64::encode(
&result[0], key, 16));
return result;
}
서버 응답 (101 Switching Protocols)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept 계산
#include <openssl/sha.h>
#include <boost/beast/core/detail/base64.hpp>
std::string compute_accept(const std::string& key) {
const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
std::string input = key + magic;
unsigned char hash[SHA_DIGEST_LENGTH];
SHA1(reinterpret_cast<const unsigned char*>(input.data()),
input.size(), hash);
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;
}
알고리즘: SHA1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") → Base64
핸드셰이크 시퀀스 다이어그램
sequenceDiagram
participant C as 클라이언트
participant S as 서버
C->>S: TCP 연결
C->>S: HTTP GET + Upgrade + Sec-WebSocket-Key
S->>S: Key 검증, Accept 계산
S->>C: HTTP 101 + Sec-WebSocket-Accept
Note over C,S: WebSocket 연결 수립
C->>S: WebSocket Frame (데이터)
S->>C: WebSocket Frame (데이터)
핸드셰이크 실패 케이스
| 응답 | 원인 |
|---|---|
| 400 Bad Request | Sec-WebSocket-Key 누락, Upgrade 헤더 오류 |
| 403 Forbidden | Origin 검증 실패 |
| 426 Upgrade Required | Sec-WebSocket-Version 불일치 |
| 503 Service Unavailable | 서버 과부하, 연결 수 제한 |
2. 프레임 구조와 완전 예제
프레임 레이아웃 (RFC 6455)
graph LR
subgraph Header
A[FIN 1bit] --> B[RSV 3bit]
B --> C[Opcode 4bit]
C --> D[Mask 1bit]
D --> E[Payload Len 7bit]
end
E --> F[Extended 0/2/8 byte]
F --> G[Mask Key 0/4 byte]
G --> H[Payload Data]
Opcode 완전 목록
| Opcode | 값 | 의미 | 방향 |
|---|---|---|---|
| Continuation | 0x0 | 이전 프레임의 연속 | 양방향 |
| Text | 0x1 | UTF-8 텍스트 | 양방향 |
| Binary | 0x2 | 바이너리 데이터 | 양방향 |
| Close | 0x8 | 연결 종료 | 양방향 |
| Ping | 0x9 | Heartbeat 요청 | 양방향 |
| Pong | 0xA | Heartbeat 응답 | 양방향 |
Text 프레임 예시 (마스킹 O)
클라이언트 → 서버: “Hello” (5바이트)
바이트 0: 0x81 (FIN=1, Opcode=0x1 Text)
바이트 1: 0x85 (Mask=1, Payload Len=5)
바이트 2-5: Masking Key (4바이트 랜덤)
바이트 6-10: "Hello" XOR Masking Key
// 마스킹 알고리즘 (클라이언트 → 서버 필수)
void mask_payload(uint8_t* data, size_t len, const uint8_t key[4]) {
for (size_t i = 0; i < len; ++i) {
data[i] ^= key[i % 4];
}
}
마스킹 이유: 프록시 캐시 poisoning 공격 방지. 오래된 프록시가 WebSocket 트래픽을 HTTP로 오인해 캐시할 수 있어, 마스킹으로 “캐시 불가” 형태로 만듭니다.
Ping 프레임 예시
바이트 0: 0x89 (FIN=1, Opcode=0x9 Ping)
바이트 1: 0x00 (Mask=0 서버→클라이언트, Payload Len=0)
Payload가 있으면 Pong에 그대로 반환합니다.
Pong 프레임 예시
바이트 0: 0x8A (FIN=1, Opcode=0xA Pong)
바이트 1: 0x00 (Payload Len=0)
Close 프레임 예시
바이트 0: 0x88 (FIN=1, Opcode=0x8 Close)
바이트 1: 0x02 (Payload Len=2)
바이트 2-3: Close Code (예: 1000=정상, 1001=이동, 1002=프로토콜 에러)
바이트 4-: UTF-8 이유 문자열 (선택)
주요 Close Code:
| 코드 | 의미 |
|---|---|
| 1000 | Normal Closure |
| 1001 | Going Away (서버 종료 등) |
| 1002 | Protocol Error |
| 1003 | Unsupported Data |
| 1006 | Abnormal Closure (Close 프레임 없이 끊김) |
| 1007 | Invalid payload (인코딩 오류) |
| 1011 | Internal Error |
Beast로 프레임 타입 처리
ws_.control_callback(
{
switch (kind) {
case websocket::frame_type::ping:
// Beast가 자동으로 Pong 전송
break;
case websocket::frame_type::pong:
// heartbeat 응답 수신
break;
case websocket::frame_type::close:
// 연결 종료 요청
break;
}
});
3. Ping/Pong Heartbeat 완전 구현
Ping/Pong 시퀀스
sequenceDiagram
participant C as 클라이언트
participant S as 서버
loop 30초마다
C->>S: Ping
S->>C: Pong (자동)
end
Note over C: Pong 미수신 10초
C->>C: 연결 끊김 판단 → 재연결
클라이언트: Ping 전송 + Pong 타임아웃
class WebSocketClientWithHeartbeat
: public std::enable_shared_from_this<WebSocketClientWithHeartbeat> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
net::steady_timer ping_timer_;
net::steady_timer pong_timeout_;
bool pong_received_ = false;
public:
explicit WebSocketClientWithHeartbeat(net::io_context& ioc)
: ws_(net::make_strand(ioc)),
ping_timer_(ws_.get_executor()),
pong_timeout_(ws_.get_executor()) {}
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;
self->send_ping();
});
}
void send_ping() {
pong_received_ = false;
pong_timeout_.expires_after(std::chrono::seconds(10));
pong_timeout_.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;
}
});
ws_.async_ping({},
[self = shared_from_this()](beast::error_code ec) {
if (ec) {
std::cerr << "Ping failed: " << ec.message() << "\n";
return;
}
self->schedule_ping();
});
}
void on_pong() {
pong_received_ = true;
pong_timeout_.cancel();
}
void reconnect() {
// 재연결 로직 (지수 백오프 권장)
}
};
서버: Ping 수신 시 Pong 자동 응답
Beast는 기본적으로 Ping에 자동 Pong을 보냅니다. 수동 처리 예시:
ws_.control_callback(
[self = shared_from_this()](
websocket::frame_type kind, beast::string_view payload) {
if (kind == websocket::frame_type::ping) {
// Beast가 자동으로 Pong 전송
// 수동: ws_.async_pong(payload);
} else if (kind == websocket::frame_type::pong) {
// 클라이언트가 보낸 Pong (서버가 Ping 보낸 경우)
self->on_pong_received();
}
});
서버 → 클라이언트 Ping (선택)
서버가 클라이언트 연결 상태를 확인하려면 서버에서 Ping을 보낼 수 있습니다.
void server_send_ping() {
ws_.async_ping("heartbeat",
[self = shared_from_this()](beast::error_code ec) {
if (ec) {
// 전송 실패 = 연결 끊김
self->close_session();
}
});
}
4. Beast WebSocket 완전 예제
완전한 비동기 클라이언트 (핸드셰이크 + 읽기 + Ping)
#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 CompleteWebSocketClient
: public std::enable_shared_from_this<CompleteWebSocketClient> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
net::steady_timer ping_timer_;
std::string host_;
std::string path_;
public:
explicit CompleteWebSocketClient(net::io_context& ioc)
: ws_(net::make_strand(ioc)),
ping_timer_(ws_.get_executor()) {}
void connect(const std::string& host, const std::string& port,
const std::string& path = "/") {
host_ = host;
path_ = path;
tcp::resolver resolver(ws_.get_executor());
resolver.async_resolve(host, port,
beast::bind_front_handler(&CompleteWebSocketClient::on_resolve,
shared_from_this()));
}
private:
void on_resolve(beast::error_code ec,
tcp::resolver::results_type results) {
if (ec) {
std::cerr << "Resolve: " << ec.message() << "\n";
return;
}
beast::get_lowest_layer(ws_).async_connect(results,
beast::bind_front_handler(&CompleteWebSocketClient::on_connect,
shared_from_this()));
}
void on_connect(beast::error_code ec,
tcp::resolver::results_type::endpoint_type ep) {
if (ec) {
std::cerr << "Connect: " << ec.message() << "\n";
return;
}
ws_.async_handshake(host_, path_,
beast::bind_front_handler(&CompleteWebSocketClient::on_handshake,
shared_from_this()));
}
void on_handshake(beast::error_code ec) {
if (ec) {
std::cerr << "Handshake: " << ec.message() << "\n";
return;
}
std::cout << "WebSocket connected\n";
do_read();
start_ping();
}
void do_read() {
ws_.async_read(buffer_,
beast::bind_front_handler(&CompleteWebSocketClient::on_read,
shared_from_this()));
}
void on_read(beast::error_code ec, std::size_t bytes) {
if (ec) {
if (ec != websocket::error::closed) {
std::cerr << "Read: " << ec.message() << "\n";
}
return;
}
std::cout << "Received: "
<< beast::buffers_to_string(buffer_.data()) << "\n";
buffer_.consume(buffer_.size());
do_read();
}
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;
self->ws_.async_ping({},
[self](beast::error_code ec) {
if (!ec) self->start_ping();
});
});
}
};
완전한 비동기 서버 (Echo + Ping/Pong)
class CompleteWebSocketSession
: public std::enable_shared_from_this<CompleteWebSocketSession> {
websocket::stream<beast::tcp_stream> ws_;
beast::flat_buffer buffer_;
public:
explicit CompleteWebSocketSession(tcp::socket socket)
: ws_(std::move(socket)) {}
void run() {
ws_.set_option(websocket::stream_base::timeout::suggested(
beast::role_type::server));
ws_.read_message_max(64 * 1024); // 64KB 제한
ws_.async_accept(
beast::bind_front_handler(&CompleteWebSocketSession::on_accept,
shared_from_this()));
}
private:
void on_accept(beast::error_code ec) {
if (ec) {
std::cerr << "Accept: " << ec.message() << "\n";
return;
}
do_read();
}
void do_read() {
ws_.async_read(buffer_,
beast::bind_front_handler(&CompleteWebSocketSession::on_read,
shared_from_this()));
}
void on_read(beast::error_code ec, std::size_t) {
if (ec) {
if (ec == websocket::error::closed) {
std::cout << "Connection closed normally\n";
} else {
std::cerr << "Read: " << ec.message() << "\n";
}
return;
}
ws_.text(ws_.got_text());
ws_.async_write(buffer_.data(),
beast::bind_front_handler(&CompleteWebSocketSession::on_write,
shared_from_this()));
}
void on_write(beast::error_code ec, std::size_t) {
if (ec) {
std::cerr << "Write: " << ec.message() << "\n";
return;
}
buffer_.consume(buffer_.size());
do_read();
}
};
5. 자주 발생하는 에러
에러 1: Handshake 400 Bad Request
증상: beast::http::error::bad_request
원인:
Sec-WebSocket-Key누락 또는 형식 오류Upgrade: websocket대소문자 오류Connection: Upgrade누락
해결:
// Beast는 자동으로 올바른 헤더 생성
// 수동 구현 시 반드시 RFC 6455 준수
ws_.async_handshake(host, path,
{
if (ec == beast::http::error::bad_request) {
std::cerr << "Check Upgrade, Connection, Sec-WebSocket-Key\n";
}
});
에러 2: bad_version (426 Upgrade Required)
증상: 서버가 426 응답
원인: Sec-WebSocket-Version이 13이 아님
해결: Beast는 기본적으로 13 사용. 수동 구현 시 Sec-WebSocket-Version: 13 필수.
에러 3: connection_reset / connection_aborted
증상: 읽기/쓰기 중 연결 끊김
원인:
- NAT/방화벽 유휴 타임아웃
- 서버 재시작
- 네트워크 불안정
해결: Ping/Pong heartbeat + 재연결 로직
ws_.async_read(buffer_,
[self = shared_from_this()](beast::error_code ec, std::size_t) {
if (ec) {
if (ec == net::error::connection_reset ||
ec == net::error::connection_aborted) {
self->schedule_reconnect();
}
return;
}
// ...
});
에러 4: frame too big / payload too large
증상: websocket::error::message_too_big
원인: 메시지가 read_message_max 초과
해결:
// 서버: 최대 메시지 크기 제한
ws_.read_message_max(1024 * 1024); // 1MB
// 클라이언트도 동일하게 설정
ws_.read_message_max(1024 * 1024);
에러 5: Safari/Chrome WSS 끊김 (멀티스레드)
증상: 맥 Safari, 일부 Chrome에서 WSS 연결이 불규칙하게 끊김
원인: 여러 스레드가 동시에 같은 WebSocket에 접근
해결: strand로 모든 WebSocket 작업 직렬화
// strand로 감싼 WebSocket
auto strand = net::make_strand(ioc);
websocket::stream<beast::tcp_stream> ws_(strand);
// 모든 async 작업이 strand에서 실행됨
ws_.async_read(buffer_, net::bind_executor(strand, { ... }));
에러 6: Mask required (클라이언트 → 서버)
증상: 서버가 클라이언트 프레임 수신 시 에러
원인: RFC 6455에 따라 클라이언트 → 서버 프레임은 반드시 마스킹
해결: Beast 클라이언트는 자동 마스킹. 수동 구현 시 mask 플래그 설정.
에러 7: Invalid UTF-8 (Text 프레임)
증상: websocket::error::bad_payload
원인: Text 프레임에 비유효 UTF-8 포함
해결:
// Binary로 전송하거나, UTF-8 검증 후 전송
ws_.binary(true);
ws_.async_write(net::buffer(data), ...);
에러 8: Double read (동시 async_read)
증상: undefined behavior, 크래시
원인: async_read 완료 전에 또 async_read 호출
해결: 읽기 완료 핸들러에서만 다음 do_read() 호출
void on_read(beast::error_code ec, std::size_t) {
if (ec) return;
// 처리 ...
do_read(); // 여기서만 다음 읽기 시작
}
6. 베스트 프랙티스
1. 메시지 크기 제한
// 서비스별 권장값
// 채팅: 64KB
// JSON API: 1MB
// 바이너리 스트림: 10MB (주의)
ws_.read_message_max(64 * 1024);
2. 재연결: 지수 백오프
void schedule_reconnect() {
static int attempt = 0;
auto delay = std::min(
std::chrono::seconds(1) << attempt,
std::chrono::seconds(60));
++attempt;
reconnect_timer_.expires_after(delay);
reconnect_timer_.async_wait(
[this](beast::error_code ec) {
if (!ec) {
connect(host_, port_, path_);
attempt = 0; // 성공 시 리셋
}
});
}
3. Graceful Close
void close() {
ws_.async_close(websocket::close_code::normal,
[self = shared_from_this()](beast::error_code ec) {
if (ec) {
beast::get_lowest_layer(self->ws_).close();
}
});
}
4. 브로드캐스트 백프레셔
// ❌ 나쁜 예: 동시에 수천 개 write
for (auto& session : sessions_) {
session->ws_.async_write(...); // 큐 폭주
}
// ✅ 좋은 예: 큐 기반 순차 전송
void broadcast(const std::string& msg) {
for (auto& session : sessions_) {
session->enqueue(msg);
}
}
void enqueue(const std::string& msg) {
bool was_empty = write_queue_.empty();
write_queue_.push(msg);
if (was_empty) do_write();
}
void do_write() {
if (write_queue_.empty()) return;
ws_.async_write(net::buffer(write_queue_.front()),
[this](beast::error_code ec, std::size_t) {
if (!ec) {
write_queue_.pop();
do_write();
}
});
}
5. 타임아웃 설정
websocket::stream_base::timeout opt{
std::chrono::seconds(30), // handshake timeout
std::chrono::seconds(30), // idle timeout
false // keepalive pings
};
ws_.set_option(opt);
6. 로깅
ws_.async_handshake(host, path,
[host, path](beast::error_code ec) {
if (ec) {
spdlog::error("WebSocket handshake failed: {} {} {}",
host, path, ec.message());
}
});
7. 프로덕션 패턴
패턴 1: 연결 수 제한
class WebSocketServer {
std::atomic<int> connection_count_{0};
static constexpr int max_connections_ = 10000;
void do_accept() {
acceptor_.async_accept(
[this](beast::error_code ec, tcp::socket socket) {
if (ec) return;
if (connection_count_.load() >= max_connections_) {
socket.close();
spdlog::warn("Connection limit reached");
} else {
connection_count_++;
std::make_shared<Session>(std::move(socket),
[this]() { connection_count_--; })->run();
}
do_accept();
});
}
};
패턴 2: Graceful Shutdown
void shutdown() {
acceptor_.close();
for (auto& session : sessions_) {
session->ws_.async_close(websocket::close_code::going_away,
{});
}
work_guard_.reset();
ioc_.stop();
}
패턴 3: 메트릭 수집
struct WebSocketMetrics {
std::atomic<uint64_t> connections_total{0};
std::atomic<uint64_t> connections_active{0};
std::atomic<uint64_t> messages_received{0};
std::atomic<uint64_t> messages_sent{0};
std::atomic<uint64_t> errors_handshake{0};
std::atomic<uint64_t> errors_read{0};
};
// Prometheus/Grafana 등으로 노출
void on_handshake(beast::error_code ec) {
if (ec) {
metrics_.errors_handshake++;
return;
}
metrics_.connections_active++;
}
패턴 4: Subprotocol 협상
// 클라이언트
ws_.set_option(websocket::stream_base::decorator(
{
req.set(beast::http::field::sec_websocket_protocol,
"chat, json");
}));
// 서버: Accept 시 선택
ws_.set_option(websocket::stream_base::decorator(
{
res.set(beast::http::field::sec_websocket_protocol, "chat");
}));
패턴 5: WSS (TLS) 연동
using ssl_stream = boost::asio::ssl::stream<beast::tcp_stream>;
websocket::stream<ssl_stream> wss_(ssl_ctx, net::make_strand(ioc));
// TLS 핸드셰이크 후 WebSocket 핸드셰이크
ssl_stream_.async_handshake(ssl::stream_base::client,
[this](beast::error_code ec) {
if (!ec) {
wss_.async_handshake(host_, path_, ...);
}
});
8. 구현 체크리스트
핸드셰이크
-
Sec-WebSocket-Key16바이트 랜덤 Base64 -
Sec-WebSocket-AcceptSHA1+magic+Base64 -
Upgrade: websocket,Connection: Upgrade -
Sec-WebSocket-Version: 13
프레임
- 클라이언트 → 서버: 마스킹 필수
- Text 프레임: UTF-8 검증
-
read_message_max설정 - Close 프레임: 코드 + 이유
Ping/Pong
- 20~30초 간격 Ping
- Pong 타임아웃 (10초) 후 재연결
- 서버: Ping 수신 시 Pong 자동 응답
에러 처리
-
connection_reset→ 재연결 -
message_too_big→ 크기 제한 - 핸드셰이크 실패 → 로깅, 재시도
프로덕션
- strand 사용 (멀티스레드)
- 지수 백오프 재연결
- 연결 수 제한
- Graceful shutdown
- 메트릭/로깅
참고 자료
정리
| 항목 | 내용 |
|---|---|
| 핸드셰이크 | HTTP Upgrade + Sec-WebSocket-Key/Accept |
| 프레임 | FIN, Opcode, Mask, Payload |
| 마스킹 | 클라이언트→서버 필수 |
| Ping/Pong | 30초 heartbeat, 10초 타임아웃 |
| 에러 | 400/426, connection_reset, message_too_big |
| 프로덕션 | strand, 백프레셔, 지수 백오프, 메트릭 |
자주 묻는 질문 (FAQ)
Q. 핸드셰이크가 400을 반환해요.
A. Sec-WebSocket-Key, Upgrade, Connection 헤더를 확인하세요. Beast를 사용하면 자동으로 올바르게 설정됩니다.
Q. Safari에서 WSS 연결이 끊겨요.
A. net::make_strand(ioc)로 WebSocket을 감싸 모든 작업을 직렬화하세요.
Q. 대용량 브로드캐스트 시 서버가 느려져요.
A. 세션별 쓰기 큐를 두고 순차 전송하는 백프레셔 패턴을 적용하세요.
Q. 선행으로 읽으면 좋은 글은?
A. cpp-series-30-1-websocket에서 WebSocket 기본을 먼저 학습하세요.
한 줄 요약: 핸드셰이크·프레임·Ping/Pong을 완전히 이해하고, 에러 처리와 프로덕션 패턴으로 안정적인 WebSocket 서비스를 구축하세요.
다음 글: [C++ 실전 가이드 #30-3] 프로토콜과 직렬화
이전 글: [C++ 실전 가이드 #30-1] WebSocket 완벽 가이드
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ WebSocket 완벽 가이드 | Beast 핸드셰이크·프레임·Ping/Pong [#30-1]
- C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]
- C++ 채팅 서버 만들기 | 다중 클라이언트와 메시지 브로드캐스트 완벽 가이드 [#31-1]
이 글에서 다루는 키워드 (관련 검색어)
C++, WebSocket, Beast, 핸드셰이크, 프레임, Ping, Pong, 실시간통신, 프로덕션, 에러처리 등으로 검색하시면 이 글이 도움이 됩니다.
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |