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

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

이 글의 핵심

NAT 테이블과 방화벽은 유휴(idle) TCP 연결을 일정 시간 후 정리합니다. WebSocket은 한 번 연결하면 오랫동안 데이터를 주고받지 않을 수 있어, 중간 장비가 사용하지 않는 연결로 판단해 끊어버립니다. 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.

들어가며: “WebSocket 연결이 자꾸 끊겨요”

문제 시나리오 1: 연결이 30초마다 끊김

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 문제: 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

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 문제: 서버가 400 Bad Request 반환
ws_.async_handshake(host, "/chat",
     {
        // ec == bad_request
        // 서버 로그: "Missing Sec-WebSocket-Key"
    });

원인: Sec-WebSocket-Key 누락, 잘못된 Upgrade 헤더, 버전 불일치 등. RFC 6455를 정확히 따르지 않으면 핸드셰이크가 실패합니다.

문제 시나리오 3: 대용량 메시지로 메모리 폭발

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

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

1. 핸드셰이크 완전 분석

HTTP 업그레이드 요청 (클라이언트 → 서버)

WebSocket 연결은 HTTP Upgrade 요청으로 시작합니다. RFC 6455를 따르는 정확한 요청 예시입니다. 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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 생성

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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)

다음은 간단한 text 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept 계산

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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

핸드셰이크 시퀀스 다이어그램

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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바이트) 다음은 간단한 text 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

바이트 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

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 마스킹 알고리즘 (클라이언트 → 서버 필수)
// 실행 예제
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 프레임 예시

다음은 간단한 text 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

바이트 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로 프레임 타입 처리

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 시퀀스

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 타임아웃

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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을 보냅니다. 수동 처리 예시: 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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을 보낼 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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)

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 누락 해결: 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 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 + 재연결 로직 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 초과 해결: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 서버: 최대 메시지 크기 제한
ws_.read_message_max(1024 * 1024);  // 1MB
// 클라이언트도 동일하게 설정
ws_.read_message_max(1024 * 1024);

에러 5: Safari/Chrome WSS 끊김 (멀티스레드)

증상: 맥 Safari, 일부 Chrome에서 WSS 연결이 불규칙하게 끊김 원인: 여러 스레드가 동시에 같은 WebSocket에 접근 해결: strand로 모든 WebSocket 작업 직렬화 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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() 호출 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

void on_read(beast::error_code ec, std::size_t) {
    if (ec) return;
    // 처리 ...
    do_read();  // 여기서만 다음 읽기 시작
}

6. 베스트 프랙티스

1. 메시지 크기 제한

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 서비스별 권장값
// 채팅: 64KB
// JSON API: 1MB
// 바이너리 스트림: 10MB (주의)
ws_.read_message_max(64 * 1024);

2. 재연결: 지수 백오프

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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. 브로드캐스트 백프레셔

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 나쁜 예: 동시에 수천 개 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. 타임아웃 설정

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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. 로깅

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

ws_.async_handshake(host, path,
    [host, path](beast::error_code ec) {
        if (ec) {
            spdlog::error("WebSocket handshake failed: {} {} {}",
                host, path, ec.message());
        }
    });

7. 프로덕션 패턴

패턴 1: 연결 수 제한

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void shutdown() {
    acceptor_.close();
    
    for (auto& session : sessions_) {
        session->ws_.async_close(websocket::close_code::going_away,
             {});
    }
    
    work_guard_.reset();
    ioc_.stop();
}

패턴 3: 메트릭 수집

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 협상

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 클라이언트
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) 연동

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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, 실시간통신, 프로덕션, 에러처리 등으로 검색하시면 이 글이 도움이 됩니다.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3