12장 : HTTP/2, HTTP/3, WebSocket
WebSocket과 실시간 통신
HTTP는 클라이언트가 요청하면 서버가 응답하는 단방향 모델입니다. 서버가 먼저 클라이언트에게 데이터를 보낼 수 없습니다. 채팅, 실시간 알림, 주식 시세처럼 서버에서 클라이언트로 즉각적으로 데이터를 밀어넣어야 하는 경우, HTTP의 요청-응답 모델은 근본적으로 적합하지 않습니다.
HTTP로 실시간을 흉내내는 방법들
폴링 (Polling)
Client: "새 메시지?" Server: "없음" (1초 후)
Client: "새 메시지?" Server: "없음" (1초 후)
Client: "새 메시지?" Server: "있음!" ← 90% 낭비
→ 불필요한 요청으로 서버 부하
롱 폴링 (Long Polling)
Client: "새 메시지?" ─────────→ Server: (대기...)
(30초간 대기)
←───────── Server: "메시지 도착!"
Client: "다음 메시지?" ────────→ Server: (대기...)
→ 폴링보다 효율적이지만, 매번 HTTP 연결 재수립
WebSocket
Client ──── HTTP Upgrade ────→ Server
Client ←════ 양방향 프레임 ════→ Server (영구 연결)
→ 프로토콜 레벨에서 양방향 지원WebSocket 핸드셰이크
WebSocket 연결은 HTTP Upgrade 메커니즘으로 시작합니다.
클라이언트 ──────────────────→ 서버
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket ← 프로토콜 전환 요청
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
서버 ──────────────────────→ 클라이언트
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
이후: HTTP가 아닌 WebSocket 프레임 전송
┌────────────────────────────────────┐
│ 같은 TCP 연결 위에서 │
│ 클라이언트 ←── 프레임 ──→ 서버 │
│ 양방향, 전이중 │
└────────────────────────────────────┘Sec-WebSocket-Accept는 클라이언트가 보낸 키에 매직 문자열 258EAFA5-E914-47DA-95CA-C5AB0DC85B11을 결합하고 SHA-1 해시를 취한 값입니다.
WebSocket 프레임 구조
HTTP 요청 (매번)
GET /api/data HTTP/1.1\r\n
Host: example.com\r\n
Authorization: Bearer xxx...\r\n
Content-Type: application/json\r\n
Cookie: session=abc123\r\n
\r\n
→ 헤더만 200~800 bytes
WebSocket 프레임
┌──────┬────────┬──────────────┐
│ FIN │ Opcode │ Payload Len │ 2~14 bytes
│ 1bit │ 4bits │ 7+bits │
├──────┴────────┴──────────────┤
│ Masking Key (클라이언트만) │ 4 bytes
├──────────────────────────────┤
│ Payload Data │
└──────────────────────────────┘
→ 오버헤드 2~14 bytes!
1초마다 작은 메시지 전송 시
HTTP: 200 bytes * 60/분 = 12KB/분
WebSocket: 6 bytes * 60/분 = 360B/분
→ 33배 차이| Opcode | 프레임 유형 | 용도 |
|---|---|---|
| 0x0 | Continuation | 이전 프레임의 계속 |
| 0x1 | Text | UTF-8 텍스트 메시지 |
| 0x2 | Binary | 바이너리 데이터 |
| 0x8 | Close | 연결 종료 |
| 0x9 | Ping | 연결 상태 확인 |
| 0xA | Pong | Ping에 대한 응답 |
WebSocket 구현
import asyncio
import websockets
connected = set()
async def handler(websocket):
connected.add(websocket)
try:
async for message in websocket:
# 받은 메시지를 모든 클라이언트에게 브로드캐스트
for ws in connected:
if ws != websocket:
await ws.send(message)
finally:
connected.discard(websocket)
async def main():
async with websockets.serve(handler, "0.0.0.0", 8080):
print("WebSocket server on ws://0.0.0.0:8080")
await asyncio.Future() # 서버 유지
asyncio.run(main())const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {
console.log("Connected");
ws.send("Hello!");
};
ws.onmessage = (event) => {
console.log("Received:", event.data);
};
ws.onclose = () => {
console.log("Disconnected");
};
ws.onerror = (error) => {
console.error("Error:", error);
};SSE (Server-Sent Events)
모든 실시간 통신에 WebSocket이 필요한 것은 아닙니다. SSE(Server-Sent Events)는 서버에서 클라이언트로의 단방향 스트리밍을 지원합니다.
SSE (단방향: 서버 → 클라이언트)
Client ────── HTTP GET ─────→ Server
Client ←── data: event1\n\n ── Server
Client ←── data: event2\n\n ── Server
Client ←── data: event3\n\n ── Server
(HTTP 연결 유지, 서버가 계속 push)
WebSocket (양방향)
Client ←═══ 프레임 ═══→ Server
Client ←═══ 프레임 ═══→ Server
(양쪽 모두 언제든 전송 가능)| 항목 | WebSocket | SSE | Long Polling |
|---|---|---|---|
| 방향 | 양방향 | 서버→클라이언트 | 서버→클라이언트 |
| 프로토콜 | ws:// | HTTP | HTTP |
| 자동 재연결 | 직접 구현 | 내장 | 직접 구현 |
| 바이너리 | 지원 | 텍스트만 | 지원 |
| HTTP/2 호환 | 별도 | 완벽 | 완벽 |
| 프록시 호환 | 낮음 | 높음 | 높음 |
| 적합한 사례 | 채팅, 게임 | 알림, 피드 | 레거시 호환 |
Socket.IO의 역할
Socket.IO는 WebSocket을 감싸는 라이브러리로, 실시간 통신의 실무적인 문제들을 해결합니다.
Socket.IO가 해결하는 문제
1. 폴백: WebSocket 불가 시 → Long Polling 자동 전환
2. 재연결: 끊어지면 자동 재연결 + 큐잉
3. 방(Room): 특정 그룹에만 메시지 전송
4. 네임스페이스: /chat, /game 등 논리적 분리
5. ACK: 메시지 수신 확인
주의: Socket.IO ≠ WebSocket
Socket.IO 서버 ←✗→ 일반 WebSocket 클라이언트
자체 프로토콜 사용, 양쪽 다 Socket.IO 필요
선택 기준
순수 WebSocket
* 표준 호환 필요
* 번들 크기 최소화
* 직접 제어 원함
Socket.IO
* 빠른 개발
* 폴백/재연결 자동화
* Room/네임스페이스 필요다음 장에서는 코드를 넘어 실무 네트워크 인프라 — CDN, 로드 밸런서, 프록시, 클라우드 네트워크 — 를 다루겠습니다.