웹소켓 (WebSocket)
웹소켓 딥다이브
웹소켓(WebSocket)은 HTML5 표준의 하나로, 클라이언트와 서버 간의 양방향 통신을 가능하게 하는 네트워크 프로토콜이다. HTTP 기반의 요청-응답 방식의 한계를 극복하고 실시간 통신을 구현하기 위해 도입되었다. 웹소켓 연결을 통해 한 번의 핸드셰이크로 연결이 성립되면 클라이언트와 서버는 별도의 요청 없이 양방향 데이터를 자유롭게 주고받을 수 있다. 이는 채팅 애플리케이션, 실시간 알림, 게임, 주식 시세, IoT 제어 시스템 등에 매우 유용하게 활용된다.
웹소켓의 주요 특징
웹소켓은 HTTP와 동일한 포트(80/443)를 사용하며, 다음과 같은 특징이 있다:
- 양방향 통신: 클라이언트와 서버가 동시에 데이터를 전송하고 받을 수 있다.
- 저지연성: 지속적인 연결을 유지하여 네트워크 대기 시간을 줄인다.
- 헤더 오버헤드 감소: 초기 핸드셰이크 이후엔 데이터 프레임에 최소한의 헤더 정보만 포함되므로 전송량이 줄어든다.
- 이벤트 기반: 서버는 클라이언트의 요청 없이도 데이터를 푸시할 수 있다.
웹소켓 통신의 동작 과정
웹소켓 연결은 기본적으로 다음과 같은 과정을 통해 이루어진다:
- 핸드셰이크:
- 클라이언트가 서버에 HTTP 프로토콜을 통해 연결을 요청한다.
- 클라이언트는 요청 헤더에
Upgrade
와Connection
헤더를 설정하여 웹소켓 프로토콜로 전환을 요청한다. - 서버가 요청을 수락하면 HTTP 연결이 웹소켓으로 업그레이드된다.
- 데이터 프레임 전송:
- 핸드셰이크 이후, 클라이언트와 서버는 양방향으로 데이터를 전송할 수 있다.
- 각 메시지는 프레임 단위로 전송되며, 텍스트 또는 바이너리 데이터 형식을 가진다.
- 연결 종료:
- 클라이언트 또는 서버는 언제든지 연결을 종료할 수 있다.
- 종료 메시지를 통해 연결을 정상적으로 종료하며, 강제로 종료할 수도 있다.
웹소켓 프레임 구조
웹소켓은 프레임 단위로 데이터를 전송하며, 각 프레임은 다음과 같은 구조를 가진다:
- FIN: 메시지의 끝을 표시하는 비트이다.
- Opcode: 프레임의 유형을 지정하는 4비트 필드로, 텍스트, 바이너리, 종료, 핑, 퐁 등의 프레임 유형을 나타낸다.
- Payload length: 전송되는 데이터의 길이를 지정한다.
- Masking key: 클라이언트에서 서버로 전송되는 데이터에 대해 마스킹 처리를 위한 4바이트 키이다.
- Payload data: 실제 전송되는 데이터이다.
웹소켓의 연결과정
초기 HTTP 요청과 웹소켓 업그레이드
웹소켓 연결은 HTTP 기반으로 시작된다. 클라이언트가 서버에 HTTP 요청을 보내면서 연결을 웹소켓으로 업그레이드하도록 요청하는 방식으로 이루어진다.
-
클라이언트가 서버로 연결 요청을 보냄
클라이언트는 HTTP GET 요청을 통해 서버에 연결을 요청하며, 웹소켓 전환에 필요한 특수 헤더를 포함한다. 이 헤더에는Upgrade
,Connection
,Sec-WebSocket-Key
,Sec-WebSocket-Version
등이 있다.예시 요청 헤더:
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
Upgrade: websocket
: 이 헤더는 클라이언트가 웹소켓 프로토콜로 업그레이드를 요청하고 있음을 나타낸다.Connection: Upgrade
: 연결을 업그레이드하도록 지시하며, HTTP 연결을 웹소켓으로 전환하는 데 필요한 필수 조건이다.Sec-WebSocket-Key
: 클라이언트가 임의의 16바이트 값(Base64로 인코딩)을 생성하여 포함하는 헤더로, 서버는 이 값을 기반으로 인증을 수행하여 핸드셰이크의 무결성을 보장한다.Sec-WebSocket-Version
: 클라이언트가 지원하는 웹소켓 프로토콜의 버전이다. 현재 대부분의 서버와 클라이언트는 버전 13을 사용한다.
서버의 핸드셰이크 응답
서버는 클라이언트의 요청을 검토하고 업그레이드가 가능하면 HTTP 응답을 통해 연결 업그레이드를 승인한다.
-
웹소켓 업그레이드 응답 헤더
서버는 101 Switching Protocols 상태 코드와 함께 응답을 보내며, 이 응답에는 다음과 같은 헤더가 포함된다.HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
101 Switching Protocols
: 서버가 연결을 웹소켓으로 성공적으로 전환했음을 나타낸다.Sec-WebSocket-Accept
: 서버가 클라이언트의Sec-WebSocket-Key
값에 RFC6455의 표준 연산을 적용하여 생성한 값이다. 클라이언트가 보낸 키에 문자열258EAFA5-E914-47DA-95CA-C5AB0DC85B11
을 결합하여 SHA-1 해시를 적용하고 Base64로 인코딩한 결과이다. 이를 통해 클라이언트는 서버가 요청을 신뢰할 수 있는지를 확인한다.
핸드셰이크 후 연결 상태 유지
핸드셰이크 과정이 성공적으로 완료되면, 클라이언트와 서버는 새로운 소켓을 사용하여 양방향 통신을 시작한다. HTTP 연결이 아닌 웹소켓 연결이므로, 이후의 통신은 전통적인 HTTP의 요청-응답 패턴을 따르지 않고 지속적인 데이터 송수신을 허용한다.
- 연결 유지 및 Heartbeat
서버와 클라이언트는 연결이 유지되는 동안 상태를 확인하기 위해ping
과pong
프레임을 주고받는다. 이를 통해 네트워크 장애를 조기에 감지할 수 있으며, 네트워크 상태에 따라 연결이 종료될 수도 있다.
데이터 프레임 전송
웹소켓 프로토콜은 메시지를 작은 프레임 단위로 쪼개어 전송한다. 각 프레임은 헤더를 포함하며, 웹소켓 프로토콜에 정의된 구조에 맞춰 데이터를 인코딩하여 전달한다.
- 프레임 구조
각 프레임은 FIN 비트, Opcode, Payload Length, Masking Key, Payload Data 등으로 구성된다.- FIN 비트: 해당 프레임이 메시지의 마지막 조각임을 나타낸다.
- Opcode: 프레임의 데이터 유형을 정의한다. 예를 들어 텍스트 프레임(1), 바이너리 프레임(2), 연결 종료(8), ping(9), pong(10) 등이 있다.
- Payload Length: 페이로드 데이터의 길이를 지정하며, 데이터의 크기에 따라 가변적이다.
- Masking Key: 클라이언트가 전송하는 데이터에 마스킹을 적용하여 전송한다. 이는 보안상의 이유로 사용되며, 서버로부터 클라이언트로 전송되는 프레임에는 포함되지 않는다.
- Payload Data: 실제 데이터 내용으로, 클라이언트와 서버 간에 전송되는 메시지 데이터이다.
연결 종료
웹소켓 연결은 클라이언트와 서버 중 어느 한쪽에서 종료할 수 있으며, 종료 요청을 위해 opcode 8
을 사용한다.
-
연결 종료 절차
연결 종료를 요청할 때는Close
프레임을 전송한다. 이 프레임에는 상태 코드와 종료 이유를 포함할 수 있으며, 상대방은 동일한Close
프레임을 반환하여 연결 종료를 확인한다. 이후 TCP 연결도 함께 종료된다.- 상태 코드
1000
: 정상 종료1001
: 클라이언트가 서버를 떠남 (예: 페이지 종료)1002
: 프로토콜 오류1003
: 수신한 데이터가 서버에서 처리할 수 없는 데이터 유형임을 나타냄
- 상태 코드
웹소켓의 연결 과정은 초기 핸드셰이크와 지속적인 프레임 전송, 연결 종료의 세 단계로 이루어진다. 이 과정을 통해 클라이언트와 서버는 연결이 성립되면 비동기적이고 양방향으로 데이터를 자유롭게 교환할 수 있다.
웹소켓의 인증
인증을 언제 수행할 것인가.
- 초기 연결 시 인증
- 과정: 클라이언트가 웹소켓 핸드셰이크 요청에 사용자 인증 정보를 포함하여 서버에 인증을 요청. 서버가 이를 검증한 후 연결을 허용.
- 장점:
- 성능 우수: 매번 인증을 수행하지 않으므로 오버헤드가 적음.
- 사용자 경험 향상: 지속적인 연결이 원활하게 유지됨.
- 단점:
- 보안 위험 증가: 연결 도중 인증 정보 유출 시 위험.
- 지속적인 연결 감시 필요.
- 모든 전송에 대한 인증 수행
- 과정: 클라이언트가 각 메시지에 JWT나 세션 토큰을 포함하여 서버에 전송. 서버가 이를 검증하여 인증 유효성을 확인.
- 장점:
- 보안성 높음: 각 전송에 대해 인증을 수행하여 유효하지 않은 접근 차단.
- 동적 보안 요구 사항에 유연하게 대응 가능.
- 단점:
- 성능 저하: 매번 인증을 수행해야 하므로 속도가 느려질 수 있음.
- 사용자 경험 저하: 지속적인 인증 요구로 번거로움 초래.
인증을 어떻게 수행할 것인가
websocket은 대부분의 브라우저에서 커스텀 헤더를 지원하지 않는다. 즉 auth헤더에 인증토큰을 사용하는 rest api스러운 방식으로는 인증을 수행할 수 없다. 이에 대한 인증 방식에 대하여 4가지의 공식문서 권유사항을 정리한다.
- URL에 쿼리스트링으로 jwt 포함
https://github.com/whatwg/websockets/issues/16 handshake에서도 커스텀 헤더를 허용하게 해달라는 이슈에 대하여 크롬 웹소캣 개발자가 쿼리스트링으로 사용하라고 주장하는 글이다.
- WebSocket 초기 연결 후 첫 번째 메시지로 인증 토큰 전송
서버의 연결 이후 인증 여부를 확인하는 것은 권한없음 에러를 ‘연결’이라는 오버헤드가 큰 동작 이후에 수행함으로 서버측의 부담이 예상된다.
- Secondary Token 도입
JWT를 사용하여 ST 발급을 요청하면 이를 검증한 후 유효기간이 짧은 secondary token을 발행하고 클라이언트가 이 토큰을 url을 통해 전송하는 방식을 이용한다.
- STOMP등 내부적인 프로토콜이 추가된 경우에는 이 stomp헤더에 인증토큰을 담아 전송하면 된다. STOMP는 텍스트 기반 프로토콜로, 메시지 프레임 내에서 헤더를 통해 다양한 메타데이터를 전달할 수 있는데, 이 헤더에 토큰이나 인증 관련 정보를 포함하는 방식으로 인증을 구현한다.
웹소켓 통신 구현 예제
클라이언트 (JavaScript)
클라이언트 측에서는 JavaScript에서 WebSocket 객체를 사용하여 웹소켓 연결을 쉽게 구현할 수 있다.
// 클라이언트 측 웹소켓 연결
const socket = new WebSocket("ws://example.com/socket");
// 연결이 열렸을 때
socket.onopen = (event) => {
console.log("Connected to WebSocket server");
socket.send("Hello Server!");
};
// 메시지를 수신했을 때
socket.onmessage = (event) => {
console.log("Message from server: ", event.data);
};
// 오류가 발생했을 때
socket.onerror = (error) => {
console.error("WebSocket error: ", error);
};
// 연결이 닫혔을 때
socket.onclose = (event) => {
console.log("WebSocket connection closed: ", event);
};
서버 (Node.js)
Node.js의 ws 라이브러리를 사용하여 서버 측에서 웹소켓 서버를 구현할 수 있다.
const WebSocket = require("ws");
// WebSocket 서버 생성
const wss = new WebSocket.Server({ port: 8080 });
wss.on("connection", (ws) => {
console.log("New client connected");
// 클라이언트로부터 메시지를 수신할 때
ws.on("message", (message) => {
console.log("Received message:", message);
ws.send(`Server received: ${message}`);
});
// 연결이 닫혔을 때
ws.on("close", () => {
console.log("Client disconnected");
});
// 오류가 발생했을 때
ws.on("error", (error) => {
console.error("WebSocket error:", error);
});
});
console.log("WebSocket server is running on ws://localhost:8080");
이 예제에서는 서버가 8080 포트에서 웹소켓 연결을 기다린다. 클라이언트가 연결되면, 서버는 message 이벤트를 통해 클라이언트로부터 메시지를 수신하고, send 메서드를 통해 응답을 보낸다.
웹소켓과 HTTP 비교
기능 | HTTP | WebSocket |
---|---|---|
통신 방식 | 요청-응답 방식 | 양방향 통신 (Full-duplex) |
연결 지속성 | 요청마다 연결, 끊김 | 한 번 연결 후 지속 |
헤더 오버헤드 | 매 요청마다 헤더 포함 | 핸드셰이크 이후 최소한의 헤더 |
전송 속도 | 지연 발생 가능성 있음 | 낮은 지연 시간 |
사용 예시 | REST API, 정적 콘텐츠 제공 | 채팅, 게임, 실시간 데이터 전송 |