C++ WebSocket 심화 가이드 | 핸드셰이크·프레임·Ping/Pong·에러·프로덕션 패턴

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는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.


목차

  1. 핸드셰이크 완전 분석
  2. 프레임 구조와 완전 예제
  3. Ping/Pong Heartbeat 완전 구현
  4. Beast WebSocket 완전 예제
  5. 자주 발생하는 에러
  6. 베스트 프랙티스
  7. 프로덕션 패턴
  8. 구현 체크리스트

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: websocketWebSocket 프로토콜로 업그레이드 요청
Connection: UpgradeHTTP 연결 업그레이드
Sec-WebSocket-Key랜덤 16바이트 Base64 (프록시 캐시 방지)
Sec-WebSocket-Version: 13WebSocket 버전 (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 RequestSec-WebSocket-Key 누락, Upgrade 헤더 오류
403 ForbiddenOrigin 검증 실패
426 Upgrade RequiredSec-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의미방향
Continuation0x0이전 프레임의 연속양방향
Text0x1UTF-8 텍스트양방향
Binary0x2바이너리 데이터양방향
Close0x8연결 종료양방향
Ping0x9Heartbeat 요청양방향
Pong0xAHeartbeat 응답양방향

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:

코드의미
1000Normal Closure
1001Going Away (서버 종료 등)
1002Protocol Error
1003Unsupported Data
1006Abnormal Closure (Close 프레임 없이 끊김)
1007Invalid payload (인코딩 오류)
1011Internal 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-Key 16바이트 랜덤 Base64
  • Sec-WebSocket-Accept SHA1+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/Pong30초 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 |