[Daily morning study] 웹소켓(WebSocket) 프로토콜 동작 방식

#daily morning study

Image


WebSocket이란

HTTP는 기본적으로 클라이언트가 요청을 보내고, 서버가 응답을 돌려주는 단방향 구조다. 연결이 끊기면 다시 요청해야 한다. 실시간으로 데이터를 주고받아야 하는 서비스(채팅, 실시간 알림, 게임, 금융 시세 등)에서는 이 구조가 비효율적이다.

WebSocket은 클라이언트와 서버 사이에 지속적인 양방향 연결 채널을 제공하는 프로토콜이다. 한 번 연결이 맺어지면, 어느 쪽이든 먼저 데이터를 보낼 수 있고, 연결을 닫기 전까지 유지된다.

  • RFC 6455 표준으로 정의되어 있다.
  • ws:// 또는 wss://(TLS 암호화) 스킴을 사용한다.
  • HTTP 포트(80, 443)를 그대로 활용한다.

HTTP와의 비교

항목HTTPWebSocket
연결 방식요청-응답 후 종료핸드셰이크 후 지속 유지
방향성단방향 (클라이언트 → 서버)양방향 (Full-Duplex)
헤더 오버헤드매 요청마다 전체 헤더 전송연결 이후 헤더 없이 데이터 프레임만 전송
실시간성Polling, Long-polling으로 흉내진정한 실시간
사용 사례일반 웹 요청, REST API채팅, 실시간 피드, 멀티플레이 게임

연결 수립: 핸드셰이크 과정

WebSocket은 HTTP Upgrade 메커니즘을 통해 연결을 시작한다. 처음에는 HTTP 요청을 보내고, 서버와 합의하면 프로토콜을 WebSocket으로 업그레이드한다.

1단계: 클라이언트 → 서버 업그레이드 요청

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 — 클라이언트가 생성한 무작위 Base64 값 (보안 검증용)
  • Sec-WebSocket-Version: 13 — WebSocket 버전

2단계: 서버 → 클라이언트 업그레이드 수락

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • 101 Switching Protocols — 업그레이드 수락
  • Sec-WebSocket-Accept — 클라이언트의 Key에 특정 값을 합쳐 SHA-1 해싱한 결과 (브라우저가 자동으로 검증)

핸드셰이크가 완료되면 HTTP 연결은 WebSocket 연결로 전환된다. 이후부터는 HTTP가 아닌 WebSocket 프레임으로 통신한다.


데이터 전송: 프레임 구조

WebSocket은 메시지를 프레임(Frame) 단위로 전송한다. 큰 메시지는 여러 프레임으로 분할(fragmentation)될 수 있다.

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)    |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Masking-key (if MASK set, 32 bits)                        |
+---------------------------------------------------------------+
|    Payload Data ...                                           |
+---------------------------------------------------------------+

주요 필드:

  • FIN: 마지막 프레임인지 여부 (1이면 메시지의 끝)
  • Opcode: 프레임 종류
    • 0x0 — 연속 프레임 (fragmentation)
    • 0x1 — 텍스트 데이터
    • 0x2 — 바이너리 데이터
    • 0x8 — 연결 종료 (Close)
    • 0x9 — Ping
    • 0xA — Pong
  • MASK: 클라이언트에서 서버로 보낼 때는 반드시 마스킹 적용 (서버 → 클라이언트는 마스킹 안 함)
  • Payload len: 페이로드 길이 (7비트, 최대 126/127 이상이면 추가 필드 사용)

연결 종료: Close 핸드셰이크

연결을 닫을 때도 절차가 있다.

  1. 먼저 닫고 싶은 쪽이 Close 프레임(Opcode 0x8) 을 전송한다.
  2. 상대방도 Close 프레임으로 응답한다.
  3. 그 이후 TCP 연결이 종료된다.

Close 프레임에는 이유 코드를 포함할 수 있다:

코드의미
1000정상 종료
1001서버 or 클라이언트가 떠남 (Going Away)
1003지원하지 않는 데이터 형식
1008정책 위반
1011서버 내부 오류

브라우저에서 WebSocket 사용하기

// 연결 생성
const ws = new WebSocket('wss://example.com/chat');

// 연결 완료 이벤트
ws.addEventListener('open', () => {
    console.log('WebSocket 연결됨');
    ws.send(JSON.stringify({ type: 'hello', message: 'Hi!' }));
});

// 메시지 수신
ws.addEventListener('message', (event) => {
    const data = JSON.parse(event.data);
    console.log('수신:', data);
});

// 오류 처리
ws.addEventListener('error', (event) => {
    console.error('WebSocket 오류:', event);
});

// 연결 종료
ws.addEventListener('close', (event) => {
    console.log(`연결 종료 (코드: ${event.code}, 이유: ${event.reason})`);
});

// 명시적 종료
ws.close(1000, 'Done');

연결 상태 (readyState)

상수의미
0WebSocket.CONNECTING연결 중
1WebSocket.OPEN연결 완료, 통신 가능
2WebSocket.CLOSING연결 종료 중
3WebSocket.CLOSED완전히 닫힌 상태

Node.js 서버 예시 (ws 라이브러리)

const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws, req) => {
    console.log('클라이언트 연결:', req.socket.remoteAddress);

    ws.on('message', (data) => {
        const message = data.toString();
        console.log('수신:', message);

        // 모든 클라이언트에 브로드캐스트
        wss.clients.forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(message);
            }
        });
    });

    ws.on('close', (code, reason) => {
        console.log(`연결 종료: ${code} ${reason}`);
    });

    // Ping 주기적으로 보내서 연결 유지
    const interval = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
            ws.ping();
        } else {
            clearInterval(interval);
        }
    }, 30000);
});

Ping / Pong: 연결 유지

WebSocket 연결은 유휴 상태가 지속되면 중간 프록시나 방화벽이 끊어버릴 수 있다. 이를 방지하기 위해 Heartbeat 메커니즘으로 Ping/Pong을 사용한다.

  • 서버가 Ping 프레임을 보내면, 클라이언트는 자동으로 Pong으로 응답한다.
  • 브라우저는 Ping에 자동 Pong 응답하므로, 서버 측에서 주기적으로 Ping을 보내 연결이 살아있는지 확인하는 패턴을 많이 쓴다.

WebSocket vs Server-Sent Events (SSE)

항목WebSocketSSE
방향양방향서버 → 클라이언트 단방향
프로토콜별도 WS 프로토콜HTTP 기반
재연결직접 구현 필요브라우저 자동 재연결
브라우저 지원모든 주요 브라우저IE 미지원
사용 사례채팅, 게임, 협업 도구뉴스피드, 알림, 실시간 로그

서버에서 클라이언트로 데이터를 푸시하는 것만 필요하다면 SSE가 구현이 더 단순하다. 양방향 통신이 필요하면 WebSocket을 선택한다.


보안 고려사항

  • wss:// (WebSocket Secure) 사용: TLS로 암호화. ws://는 평문 전송이므로 프로덕션에서 사용하면 안 된다.
  • Origin 검증: 핸드셰이크 시 Origin 헤더를 확인해서 허가된 출처만 연결을 허용해야 한다.
  • 인증: HTTP 쿠키나 핸드셰이크 요청의 쿼리 파라미터(토큰)로 인증한다. WebSocket 자체에는 인증 헤더를 추가하는 표준 방법이 없다.
  • 메시지 크기 제한: 대용량 메시지를 무제한 수신하면 서버 메모리가 고갈될 수 있다. 최대 페이로드 크기를 제한해야 한다.
  • Rate Limiting: 클라이언트가 과도하게 메시지를 보내는 것을 막는 처리가 필요하다.

정리

WebSocket은 HTTP Upgrade 핸드셰이크로 연결을 시작하고, 이후에는 경량 프레임으로 양방향 통신을 한다. 실시간 기능이 필요한 서비스에서 HTTP Polling에 비해 훨씬 적은 오버헤드로 빠른 통신을 구현할 수 있다. 다만 연결 수가 많아지면 서버 자원 관리가 중요하고, 재연결 로직과 보안 설정은 직접 구현해야 한다.