웹소켓
더 많은 작업
웹소켓(WebSocket)은 웹 브라우저와 서버가 HTTP로 연결을 연 뒤 그 TCP 연결을 계속 유지하면서, 양쪽이 언제든 메시지를 주고받는 전이중(full-duplex) 양방향 통신 프로토콜이다.
HTTP는 클라이언트가 요청해야만 서버가 응답하는 요청-응답 구조라서, 서버가 먼저 데이터를 보내야 하는 채팅·알림·실시간 시세 같은 서비스를 만들기 어려웠다. 그래서 주기적으로 묻는 폴링, 응답을 일부러 미루는 롱 폴링 같은 우회 방법을 썼지만, 요청마다 HTTP 헤더가 붙고 연결을 반복해서 맺어야 해 지연과 낭비가 컸다.
웹소켓은 처음 한 번만 HTTP로 업그레이드 핸드셰이크를 하고, 그 뒤에는 같은 TCP 연결 위에서 가벼운 프레임으로 양방향 메시지를 주고받는다. 2011년 IETF가 RFC 6455로 프로토콜을 표준화했고, 브라우저 API는 W3C와 WHATWG가 정했다. 주소 체계는 ws://(기본 포트 80)와 TLS로 암호화한 wss://(기본 포트 443)를 쓴다. 기존 웹 포트와 프록시 서버를 그대로 지나갈 수 있다는 점이 별도 TCP 소켓을 여는 것과 다른 장점이다.
클라이언트가 보통의 HTTP/1.1 GET 요청에 업그레이드 헤더를 실어 보낸다.
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Origin: https://example.com
서버가 받아들이면 상태 코드 101 Switching Protocols로 답한다.
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept는 클라이언트가 보낸 키 뒤에 고정 GUID를 붙여 SHA-1로 해시한 값을 Base64로 인코딩한 것이다. 이 값으로 클라이언트는 상대가 웹소켓을 이해하는 서버인지 확인한다. 이 응답 이후로 연결은 더 이상 HTTP가 아니다.
핸드셰이크 뒤에는 프레임 단위로 메시지를 주고받는다.
| 필드 | 설명 |
|---|---|
| FIN | 메시지의 마지막 프레임인지 표시. 큰 메시지는 여러 프레임으로 쪼갤 수 있다 |
| Opcode | 프레임 종류. 텍스트(0x1), 바이너리(0x2), 연결 종료(0x8), Ping(0x9), Pong(0xA), 연속 프레임(0x0) |
| MASK | 페이로드가 마스킹되었는지. 클라이언트→서버 프레임은 반드시 마스킹한다(중간 캐시 오염 공격 방지) |
| Payload length | 7비트, 확장 16비트 또는 64비트 길이 |
| Masking key | 4바이트 마스킹 키 |
| Payload | 실제 데이터 |
헤더가 2~14바이트에 불과해, 매번 수백 바이트의 헤더가 붙는 HTTP 요청보다 오버헤드가 훨씬 작다. Ping·Pong 프레임으로 연결이 살아 있는지 확인한다.
어느 쪽이든 Close 프레임(상태 코드 포함)을 보내고 상대가 Close로 답하면 TCP 연결을 닫는다.
const ws = new WebSocket("wss://example.com/chat");
ws.onopen = () => ws.send("hello");
ws.onmessage = (e) => console.log(e.data);
ws.onclose = () => console.log("closed");
| 단계 | TCP 소켓 | 웹소켓 |
|---|---|---|
| 연결 준비 | 서버: socket() → bind() → listen() → accept(). 클라이언트: socket() → connect() | TCP 3-way 핸드셰이크(wss면 TLS 핸드셰이크까지) |
| 프로토콜 협상 | 없음. 연결 즉시 바이트 스트림 송수신 | HTTP Upgrade 요청 → 101 Switching Protocols |
| 데이터 단위 | 경계 없는 바이트 스트림. 메시지 구분은 응용이 직접 처리 | 프레임과 메시지. 메시지 경계가 보존된다 |
| 종료 | close() 후 FIN/ACK 교환 | Close 프레임 교환 후 TCP 종료 |
| 구분 | TCP 소켓 | 웹소켓 |
|---|---|---|
| 계층 | 전송 계층 API(TCP를 직접 사용) | 응용 계층 프로토콜(TCP 위에서 동작) |
| 사용 환경 | 운영체제의 네이티브 프로그램 | 웹 브라우저와 웹 서버(네이티브에서도 사용 가능) |
| 연결 수립 | connect() | HTTP 핸드셰이크 후 업그레이드 |
| 포트·방화벽 | 임의 포트, 방화벽 설정 필요 | 80·443을 그대로 사용 |
| 보안 | 응용이 직접 구현 | wss(TLS), Origin 헤더 확인 |
| 데이터 형식 | 바이트 스트림 | 텍스트·바이너리 메시지 |
| 구분 | HTTP(요청-응답) | 웹소켓 |
|---|---|---|
| 통신 방향 | 단방향 요청-응답. 서버가 먼저 보낼 수 없다 | 양방향 전이중. 서버가 언제든 보낼 수 있다 |
| 연결 | 요청 단위(지속 연결이 있어도 요청마다 응답) | 한 번 맺은 연결을 계속 유지(stateful) |
| 헤더 오버헤드 | 요청마다 큰 헤더 | 핸드셰이크 이후 2~14바이트 |
| 지연 | 요청이 있어야 응답 | 이벤트 발생 즉시 전송 |
| 상태 | 무상태(stateless) | 연결 상태 유지 |
| 캐시·확장 | 캐시, REST, 로드 밸런싱이 쉽다 | 캐시 불가, 연결을 유지해야 해 서버 확장 시 세션 고정·메시지 브로커가 필요 |
| 적합한 용도 | 문서·자원 조회, API 호출 | 채팅, 실시간 알림, 협업 편집, 게임, 주식 시세, IoT 대시보드 |
- wss 사용 : 평문 ws는 도청과 중간자 공격에 노출된다.
- Origin 확인 : 웹소켓에는 CORS가 적용되지 않는다. 서버가 Origin 헤더를 검사하지 않으면, 다른 사이트가 사용자의 쿠키를 실어 연결을 여는 교차 사이트 웹소켓 하이재킹(CSWSH)이 가능하다.
- 인증 : 핸드셰이크 때 토큰·쿠키로 인증하고, 연결 중에도 권한 만료를 처리한다.
- 입력 검증 : 메시지도 일반 요청처럼 검증하고, 메시지 크기와 연결 수를 제한해 서비스 거부를 막는다.