WebSocket 프로토콜: 핸드셰이크·프레임·연결 수명주기
WebSocket 프로토콜: 핸드셰이크·프레임·연결 수명주기
WebSocket은 웹 브라우저(또는 다른 클라이언트 애플리케이션)와 웹 서버 사이에 전이중(full-duplex) 통신을 가능하게 하는 프로토콜이에요. 한 번 연결을 맺으면 요청-응답을 반복하는 HTTP와 달리, 양쪽이 언제든 자유롭게 데이터를 보낼 수 있어요. 이 글에서는 연결을 여는 핸드셰이크, 실제 데이터를 담는 프레임 구조, 그리고 연결을 닫는 수명주기까지 한 흐름으로 따라가 볼게요.
본문
ws와 wss URI 스킴
WebSocket 프로토콜은 ws(WebSocket)와 wss(WebSocket Secure) 두 가지 URI 스킴을 정의해요. ws는 암호화되지 않은 연결, wss는 암호화된 연결에 각각 쓰여요. 두 스킴 모두 HTTP/1.1의 업그레이드(Upgrade) 메커니즘을 이용해 WebSocket 프로토콜로 전환합니다.
오프닝 핸드셰이크 (Opening handshake)
클라이언트가 WebSocket 서버에 연결하고 싶으면 HTTP 요청으로 핸드셰이크를 시작해요. 이 요청에는 연결을 업그레이드하겠다는 의도를 밝히는 Upgrade·Connection 헤더와, 서버가 응답을 검증하는 데 쓰는 Sec-WebSocket-Key, 그리고 버전을 알리는 Sec-WebSocket-Version 등이 들어갑니다.
예시 요청:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
서버는 요청이 유효하면 101 Switching Protocols로 응답하고, Sec-WebSocket-Key를 바탕으로 만든 Sec-WebSocket-Accept 값을 돌려줘요. 이때 Sec-WebSocket-Protocol은 클라이언트가 보낸 목록 중 하나를 골라 정확히 하나만 응답해야 합니다.
예시 응답:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
프레임 기반 메시지와 프레임 구조
오프닝 핸드셰이크가 끝나면 클라이언트와 서버는 언제든 서로에게 데이터 메시지(텍스트 또는 바이너리)와 제어 메시지(Close, Ping, Pong)를 보낼 수 있어요. 메시지는 분할되지 않았다면 프레임 하나, 분할됐다면 프레임 두 개 이상으로 구성됩니다.
프레임의 첫 두 바이트는 프레임의 종류와 길이 정보를 담아요. 첫 바이트의 최상위 비트가 FIN(마지막 프레임 여부), 하위 4비트가 opcode(프레임 종류)예요. 두 번째 바이트의 최상위 비트는 MASK(마스킹 여부)이고, 나머지 7비트는 페이로드 크기입니다. 클라이언트가 보내는 모든 프레임은 마스킹해야 하고, 제어 프레임(8 이상)은 분할할 수 없어요(FIN=1이어야 한다는 뜻).
opcode는 메시지의 종류를 구분해요:
| Opcode | 종류 | 설명 | 목적 |
|---|---|---|---|
| 0 | Continuation | 분할된 메시지의 첫 프레임이 아닌 이어지는 프레임 | 메시지 분할 |
| 1 | Text | send()·onmessage에 해당, UTF-8로 인코딩된 텍스트 |
데이터 메시지 |
| 2 | Binary | 바이너리 데이터 | 데이터 메시지 |
| 8 | Close | 연결 종료 요청 | 제어 메시지 |
| 9 | Ping | 연결 상태 확인 | 제어 메시지 |
| 10 | Pong | Ping에 대한 응답 | 제어 메시지 |
닫기 핸드셰이크와 연결 상태
연결 상태는 아래 네 단계로 나뉘어요.
| 상수 | 값 | 설명 |
|---|---|---|
WebSocket.CONNECTING |
0 | 오프닝 핸드셰이크 진행 중. 연결의 초기 상태 |
WebSocket.OPEN |
1 | 오프닝 핸드셰이크 성공. 쌍방이 메시지를 주고받을 수 있음 |
WebSocket.CLOSING |
2 | 닫기 핸드셰이크 진행 중. ws.close()가 호출됐거나 Close 메시지를 받음 |
WebSocket.CLOSED |
3 | 연결이 닫힘 |
ws.readyState가 OPEN이거나 CONNECTING일 때 ws.close()를 호출하면 상태가 CLOSING으로 바뀌며 닫기 핸드셰이크가 시작돼요. 이미 CLOSING이거나 CLOSED라면 아무 일도 일어나지 않아요(닫기 핸드셰이크가 이미 시작됐기 때문이에요).
WSS와 프록시 통과
wss:// 연결은 TLS 위에서 동작하므로 ws://보다 안전해요. 클라이언트 구현은 프록시를 사용하도록 설정돼 있는지 감지하고, 프록시가 있다면 HTTP CONNECT 메서드로 터널을 만들어 WebSocket 통신을 전달합니다. 이 터널은 WebSocket Secure 클라이언트와 WebSocket 서버 사이에 저수준의 종단 간 TCP 통신을 제공해요. 브라우저가 프록시 서버를 인지하지 못하는 투명 프록시(transparent proxy)의 경우에는 HTTP CONNECT를 보내지 않아요.
더 알아보기
- MDN — WebSocket API
- ws — Node.js WebSocket 라이브러리
- websockets — Python WebSocket 라이브러리