한 번의 HTTP 업그레이드로 같은 TCP 연결을 양방향 통로로 바꾸는 프로토콜(RFC 6455).
핸드셰이크 — 왜 101인가
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=
2xx 가 아니라 1xx 인 이유 2xx 는 "요청을 처리해 응답을 준다" 는 뜻이다 101 은 "응답이 아니라, 지금부터 이 연결의 규약을 바꾼다" 는 뜻이다
- 이 시점 이후로는 HTTP 가 아니다. 요청·응답 짝이라는 개념 자체가 사라진다
Sec-WebSocket-Accept는 클라이언트가 보낸 키에 고정 GUID(258EAFA5-E914-…)를 붙여 SHA-1 후 base64 한 값이다. 서버가 이 규약을 아는지 확인하는 절차로, 캐시나 중간 프록시가 예전 응답을 재사용하는 것을 막는다.
프레임 — 오버헤드가 극적으로 줄어든다
[FIN][opcode][MASK][payload len][masking key][payload]
- opcode — 0x1 텍스트 · 0x2 바이너리 · 0x8 close · 0x9 ping · 0xA pong
- 헤더 크기 — 2 ~ 14바이트
비교: HTTP 폴링은 요청마다 헤더·쿠키가 수백 바이트~수 KB
- 초당 10회 폴링 × 1KB = 10KB/s 를 '아무 일이 없어도' 쓴다
클라이언트는 반드시 마스킹한다
브라우저가 보내는 프레임은 4바이트 키로 XOR 마스킹해야 한다 (MASK=1) 보안이 목적이 아니다 — 암호화가 아니라 XOR 일 뿐이다
목적: 중간 프록시가 페이로드를 HTTP 요청으로 오인해 캐시를 오염시키는
- 공격(cache poisoning)을 막는 것 서버→클라이언트 방향은 마스킹하지 않는다
연결이 조용히 죽는다 — ping/pong이 필요한 이유
TCP 는 상대가 사라져도 데이터를 보내기 전까지 모른다 중간의 로드밸런서·NAT 는 유휴 연결을 말없이 끊는다
-
AWS ALB 기본 유휴 타임아웃 60초
-
많은 리버스 프록시가 60~300초
-
30~45초 간격 ping/pong 으로 살아 있음을 알린다
-
클라이언트는 지수 백오프 + 지터로 재연결한다
- (전원이 한꺼번에 재연결하면 서버가 재기동 직후 무너진다)
스케일아웃 — 연결은 한 서버에 고정된다
-
문제 — WebSocket 은 상태를 가진 연결이다
- A 서버에 붙은 사용자에게 B 서버가 메시지를 보낼 방법이 없다
-
① 스티키 세션 — LB 가 같은 클라이언트를 같은 인스턴스로 보낸다
- 붙는 곳만 해결한다. 그 인스턴스가 죽으면 전부 끊긴다
- 배포마다 전 연결이 재연결된다
-
② 백플레인(권장) — Redis Pub/Sub · Kafka · NATS 를 뒤에 둔다
- 각 인스턴스는 자기에게 붙은 연결만 알고,
- 전체 팬아웃은 백플레인이 맡는다
- 세션 어피니티가 필요 없어진다. 아무 인스턴스나 받아도 된다
-
실무 감각: 소켓은 메모리에, 토폴로지(누가 어디 붙었나)는 외부 저장소에
배포·운영에서 실제로 걸리는 것
- 리버스 프록시가 Upgrade·Connection 헤더를 전달하지 않으면 400/502
- (nginx 는 proxy_set_header Upgrade/Connection 을 명시해야 한다)
- 인증 — 브라우저 WebSocket API 는 커스텀 헤더를 못 붙인다
- 쿠키, 또는 첫 메시지로 토큰 전달, 또는 짧은 수명의 티켓 발급 후 쿼리스트링
- (쿼리스트링 토큰은 접근 로그에 남는다 — 수명을 짧게)
- 쿠키, 또는 첫 메시지로 토큰 전달, 또는 짧은 수명의 티켓 발급 후 쿼리스트링
- Origin 헤더 검증이 없으면 다른 사이트가 사용자 쿠키로 연결한다
- (CSWSH — WebSocket 에는 동일 출처 정책이 적용되지 않는다)
- 파일 디스크립터 한도(ulimit -n)와 커널 TCP 파라미터를 함께 올려야 한다
지금 어디까지 왔나
-
RFC 6455 — HTTP/1.1 위. 브라우저 지원 99% 이상
-
RFC 8441 — HTTP/2 위 (CONNECT + :protocol). 실사용 중
-
RFC 9220 — HTTP/3 위. 2022년 표준화됐지만 주요 브라우저·서버 미구현
-
WebTransport — HTTP/3(QUIC) 기반의 후속. 다중 스트림과 '신뢰성 없는 데이터그램'을
- 함께 제공한다 — WebSocket 으로는 불가능했던 영역
- 2026년 들어 주요 브라우저 지원이 갖춰지는 중
-
결론: 지금 만드는 서비스는 여전히 WebSocket 이 기본값이다
- WebTransport 는 실시간 게임·미디어처럼 '늦은 패킷은 차라리 버리는 게 나은'
- 워크로드에서 먼저 의미가 생긴다
면접 함정
- ❌ "실시간이면 WebSocket" → 서버→클라이언트 단방향이면 SSE가 더 싸고 단순하다.
- ❌ "연결됐으니 계속 살아 있다" → 중간 장비가 유휴 연결을 끊는다. 하트비트가 필수다.
- ❌ "HTTP 위에서 도니 CORS가 지켜 준다" → 동일 출처 정책이 적용되지 않는다.
Origin을 직접 검증해야 한다.