C++ WebSocket 완벽 가이드 | Beast 핸드셰이크·프레임·Ping/Pong [#30-1]

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


목차

  1. WebSocket 프로토콜 구조
  2. 핸드셰이크 과정
  3. 프레임 구조
  4. Beast WebSocket 클라이언트
  5. Beast WebSocket 서버
  6. Ping/Pong Heartbeat
  7. 일반적인 에러와 해결법
  8. 베스트 프랙티스
  9. 성능 비교
  10. 프로덕션 예시

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

특성HTTPWebSocket
연결요청마다 새 연결한 번 연결 유지
방향단방향 (요청→응답)양방향
오버헤드헤더 크기 큼프레임 헤더 작음 (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의미
Continuation0x0이전 프레임 계속
Text0x1UTF-8 텍스트
Binary0x2바이너리 데이터
Close0x8연결 종료
Ping0x9Ping (heartbeat)
Pong0xAPong (응답)

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,000300MB0-1초
HTTP 롱폴링10,00050MB0-30초
WebSocket10,0005MB즉시

결론: WebSocket은 대역폭을 60배 절감하고 실시간 전달

동시 접속자 처리

동시 접속자메모리 (WebSocket)CPU 사용률
1,00050MB5%
10,000500MB15%
100,0005GB40%

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_timeout 3600초 이상
  • 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/Pong30초마다 연결 유지 확인
성능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·에러·프로덕션 패턴