본문 바로가기
Projects,Activity/DizzyCode(React)

[Network] Web Socket

by 그냥하는거지뭐~ 2024. 5. 24.
목차
0. 프롤로그
1. 다양한 통신 방법
2. WebSocket 통신 원리 
3. HTTP vs WebSocket
4. WebSocket의 한계와 해결 방법

 

0. 프롤로그

지금까지 프로젝트를 하면서 사용했던 통신은 클라이언트가 먼저 요청을 보내면 서버가 응답을 보내주는 방식이었다. 이 말은, 서버는 클라이언트가 먼저 말을 걸어주지 않으면(요청이 없으면) 선톡을 할 수 없다는 의미이다. 이를 반이중 통신(half duplex communication)이라고 한다.

채팅, 주식과 같이 데이터가 실시간으로 바뀌는 기능. 노션이나 피그마에서 팀원의 마우스가 화면에 보이는 기능은? 이와 같은 실시간 통신을 구현하기 위해 전통적인 HTTP 통신을 선택했다가는 뭐 가능이야 하겠지만 조금만 생각해 봐도 명확한 한계가 보인다. 클라이언트가 요청을 보내지 않아도 서버에서 알아서 데이터를 보내주면 좋겠고, 그 과정에서 많은 비용이 들지 않았으면 좋겠다. 다양한 통신 방법들이 많은데, 그것들로는 실시간 통신을 구현하는 것이 가능할지 알아보고, 웹소켓이 어떤 원리로 이를 가능하게 해 주는지 알아보자. 


1. 다양한 통신 방법

① Polling

클라이언트에서 서버로 주기적으로 요청을 보내는 방법이다. 가장 단순하지만 비효율적이다. 

 

카카오톡을 polling 방법으로 구현했다고 생각해보자. 요청을 보내는 주기를 2초로 설정했다. 상대방의 새로운 메시지가 있어도 나는 최대 2초간 기다려야 한다. 빠른 티키타카가 불가능하다! 즉, 서버에서 새로운 데이터가 있어도 클라이언트에서 요청을 보내기 전까지는 전송해 줄 수 없으므로 지연이 발생한다. 대화를 하고 있지 않을 때가 더 문제다. 서로 채팅을 보내지 않고 있음에도 2초에 한 번씩 새로운 데이터가 있나 요청을 보낸다. 즉, 불필요한 트래픽이 발생하고, 그만큼 서버에 부하가 생긴다. 

이 비효율적인 방법을 어떻게 개선해 볼 수 있을까? 

 

Idea 1) Exponential Backoff 

데이터가 자주 업데이트되지 않는 경우, polling 주기를 exponential하게 늘려서 서버의 부하를 줄이는 방법이다. 몇 시간 동안 채팅을 하지 않고 있을 때 2초에 한 번씩 요청을 보내는 것이 아니라, 2초, 4초, 8초, 16초,... 이렇게 늘려가는 것이다. ((t=b^{c}))

하지만 이 방법을 실시간 통신에서 사용하기에는 무리가 있다. 재시도 간격이 exponentially 증가하기 때문에 장시간 새로운 메시지가 없다가 새 메시지를 보낼 경우 응답 지연이 너무너무 길어질 수 있다. 이와 같이 무한정 간격이 늘어나는 것을 방지하기 위해 최대 시간을 설정할 수 있지만, 여전히 적절한 방법은 아니다. 

알아보니 이 방법은 주로 네트워크 오류나 일시적인 문제로 인해 요청이 실패했을 때, 서버가 과부하 상태일 때 재전송 간격을 조정하기 위해 유용하게 사용된다고 한다. 

 

Idea 2) Conditional Requests

데이터가 변경됐을 경우에만 응답하면 어떨까? 클라이언트가 마지막으로 데이터를 받은 시각을 헤더에 포함시켜 서버에 보내고, 서버는 해당 시각 이후 데이터가 변경되었는지 확인, 변경된 경우에만 데이터를 반환하는 것이다(If-Modified-Since 헤더). 변경되지 않았으면 304 Not Modified를 반환한다(빠른 응답 가능). 

하지만 이 방법도 결국 주기적인 요청이 필요하고, 데이터 변경 시까지 지연이 있을 수 있다는 polling의 단점을 극복하지 못했다. 

더 적절한 방법이 있을지 다른 통신 방법들에 대해서도 알아보자. 

 

② Long Polling

클라이언트가 요청을 보내면, 서버는 새로운 데이터가 생길 때까지 응답을 지연시킨다. 실시간 성능을 향상하면서, 서버의 부하를 줄일 수 있다. 

① 클라 -> 서버로 요청을 보낸다. 

② 서버에서는 새로운 데이터가 생길 때까지 연결을 끊지 않고 대기한다. 

③ 새 데이터가 생기면 그제야 응답한다. 

④ 클라이언트는 응답을 받은 즉시 새 요청을 보낸다. 

 

Long polling은 데이터가 변경될 때마다 즉시 응답을 받을 수 있고, regular polling과 비교했을 때 트래픽이 훨씬 적다. 하지만 연결을 끊지 않고 유지해야 하기 때문에 서버 자원이 더 많이 필요할 수 있다. 

 

③ Server Sent Event (SSE)

SSE는 서버가 클라이언트에게 지속적으로 데이터를 전송하는 방법이다 (단방향). 

 

① Connection: 서버에 연결을 열고 EventSource 객체를 생성하여 SSE 스트림을 수신할 준비 

② 서버는 클라이언트와 연결을 통해 지속적으로 데이터를 전송한다. (텍스트 형식)

③ 클라이언트는 수신된 이벤트를 처리한다. 

 

이 방법은 서버에서 클라이언트로 단방향 데이터 스트림을 제공하며 연결이 끊어질 경우 클라이언트가 자동으로 재연결을 시도한다. 또한 HTTP 기반이므로 별도의 프로토콜을 구현할 필요 없이 사용할 수 있으며 대부분의 브라우저에서 지원한다. 실시간 푸시 알림이나 주식 가격의 실시간 업데이트. 또는 뉴스, 블로그에서 실시간으로 게시물이 올라오거나 댓글을 업데이트하는 경우 유용하게 사용할 수 있다. 

 

하지만 SSE로 채팅을 구현하기에는 한계점이 명확하다. 단방향 통신이므로, 클라이언트에서 서버로 데이터를 전송해야 할 경우 별도의 HTTP 요청을 해야 하며, 많은 클라이언트와의 지속적인 연결을 유지해야 할 때 제한이 있을 수 있다. 

 


2. WebSocket 통신 원리 

 

RFC 6455: The WebSocket Protocol

The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host that has opted-in to communications from that code. The security model used for this is the origin-based security mode

datatracker.ietf.org

RFC 6455는 WebSocket 프로토콜의 공식 사양을 정의하고 있다. 이 문서를 기반으로 내가 이해한 내용을 정리해 보겠다. 

 

지금까지 알아본 통신 방법들은 모두 HTTP 기반에서 단방향으로 데이터를 전송한다. 반면 웹소켓은 양방향 통신을 가능하게 하는 프로토콜이며, HTTP와 함께 OSI 7계층(Application Layer)에 위치하고 있고 TCP의 양방향 전이중 통신을 사용하여 transport layer(OSI 4계층)에 의존한다. 웹소켓의 동작 원리는 크게 세 단계로 이루어진다: ① Handshake, ② Data transfer, ③ Connection Termination. 

 

2.1. Handshake

먼저 HTTP handshake를 통해 시작된다. 클라이언트는 서버에 웹소켓 연결을 요청하고, 서버는 이를 승인한다. 

 

2.1.1. Client Request

HTTP header에 다음을 포함한다. 

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://localhost:9000

**the HTTP version must be 1.1 or greater, and the method must be GET

Upgrade: websocket
: 클라이언트가 WebSocket 프로토콜로 업그레이드 요청을 함

Connection: Upgrade
: 연결을 업그레이드하기 위한 요청임을 나타냄

Sec-WebSocket-Key
: 클라이언트가 생성한 임의의 Base64 인코딩 된 키
Sec-WebSocket-Protocol
: 클라이언트가 요청하는 프로토콜들로, 순서에 따라 우선권이 부여됨. 

Sec-WebSocket-Version
: WebSocket 프로토콜 버전
Origin
: 클라이언트의 주소. 해당 헤더가 없는 경우 요청이 거부될 수 있음(CORS 정책)

 

 

2.1.2. Server Response

연결을 승인하면, 다음과 같이 응답한다. 

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
101 Switching Protocols
: 프로토콜이 WebSocket으로 성공적으로 업그레이드되었음을 나타냄
Sec-WebSocket-Accept
: 클라이언트에서 받은 Sec-WebSocket-Key 값을 기반으로 SHA-1 해시를 계산하고, Base64로 인코딩한 값

 

Sec-WebSocket-Accept가 왜 필요하지? 역할이 뭐지? MDN은 다음과 같이 설명한다. 

출처: MDN

HTTP와 WebSocket은 다른 프로토콜이므로, handshake 과정을 통해 프로토콜 전환을 확실히 해서 혼동을 방지해야 한다. Sec-WebSocket-Accept 키는 서버가 클라이언트에서 받은 Sec-WebSocket-Key를 기반으로 계산한 값이다. 따라서 클라이언트는 이를 통해 서버가 WebSocket을 지원하는지 확인할 수 있는 것이다. 클라이언트는 이 값을 직접 해독하지는 않지만, 브라우저나 WebSocket 클라이언트 라이브러리가 자동으로 유효성 검사를 한다고 한다. 

 

2.2. Data Transfer

자, 이제 TCP처럼 전이중통신(full-duplex communication)이 가능해졌다. URI는 http, https 대신 ws, wss를 써야 한다. 

데이터는 프레임 단위로 전송하게 되는데, 프레임은 header와 payload로 구성된다. 

 

2.2.1. 프레임의 구조

프레임은 다음과 같은 구조로 구성되어 있다.

  0               1               2               3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 +---------------------------------------------------------------+

 

FIN (1 bit) (*2.2.2. 참고)
: 이 프레임이 메시지의 마지막 부분인지의 여부를 나타냄. (1: 마지막 / 0: 뒤에 프레임 더 있음)
RSV1, RSV2, RSV3 (각 1 bit)
: 특정한 확장이 정의되지 않는 한 항상 0으로 설정됨. 예를 들어 RSV1가 1이면 압축된 데이터임을 나타냄. 

Opcode (4 bits)
: 이 프레임의 유형을 나타냄. (*2.2.3. 참고)

Mask (1 bit)
: 1이면 client -> server, 0이면 server -> client (*2.2.4. 참고)
Payload Length (7 bits, 7+16 bits, 7+64 bits)

: payload 데이터의 길이
Masking key (32 bits, optional)
: Mask가 1일 때, 즉 클라이언트에서 서버로 전송될 때 데이터의 마스크 키. 

Payload data (variable length)
: 실제 전송되는 데이터 

 

2.2.2. FIN 비트의 필요성

대용량의 메시지를 처리할 때 메시지를 청크로 나누어서 보내고 서버에서 합치는 식의 전략을 사용한다. 이때 FIN이 중요한 역할을 하는데, FIN 비트를 통해 각 프레임이 메시지의 끝인지 여부를 지정할 수 있으며, 이를 통해 클라이언트와 서버는 대용량의 메시지를 효율적으로 전송하고 조립할 수 있다. FIN이 1일 때의 프레임을 통해 전체 메시지가 완전히 수신되었음을 확인한다.

 

2.2.3. 프레임의 유형

프레임은 Opcode를 통해 식별되는데, 

0x0: Continuation
0x1: Text
0x2: Binary
0x8: Connection Close
0x9: Ping
0xA: Pong

 

크게 text frame, binary frame, control frame으로 구분할 수 있다. control frame은 연결 관리에 사용되며, ping/pong (*2.2.5. 참고)이나 close frame 등이 여기에 속한다. Ping/pong이 무엇인지는 2.2.5에서 자세히 다루겠다. 

 

이제 어떤 식으로 프레임이 구성되는지 알아보자. 예를 들어, 클라이언트에서 서버로 'Hello'라는 텍스트를 전달하는 경우에는 다음과 같은 프레임 구조가 만들어진다. 

0               1               2               3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|1|0|0|0| 0x1   |1|     5       |  Masking Key (4 bytes)        |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Masking Key (continued)   | Masked Payload Data ("Hello") |
+-------------------------------+-------------------------------+
FIN: 1 (이 프레임이 메시지의 마지막 프레임임)
RSV1, RSV2, RSV3: 0 (사용되지 않음)
Opcode: 0x1 (텍스트 프레임)
Mask: 1 (클라이언트 -> 서버. 마스크 처리됨)
Payload length: 5 ("Hello"의 길이)
Masking key: 4 byte의 임의 값
Payload data: 마스크 처리된 "Hello"의 UTF-8 인코딩 된 바이트

 

웹소켓은 텍스트와 바이너리 데이터만 전송할 수 있다. 하지만 프로젝트를 진행하다 보면 다양한 형태의 메시지를 주고받아야 할 때가 있다. 예를 들어 Pub/Sub 모델을 구현해야 하는 경우, 즉 채팅 어플에서 특정 채널에 메시지를 게시하고, 그 채널에 구독된 모든 클라이언트에게 메시지를 전달하는 경우, 웹소켓만으로는 이런 라우팅을 직접 구현해야 하는데 이 과정이 상당히 복잡하다. 이럴 때 STOMP와 같은 서브 프로토콜을 사용해 메시지를 더 쉬운 방식으로 전송할 수 있다. 이에 대한 내용은 뒤에 나올 4. 웹소켓의 한계와 해결 방법의 두 번째 섹션에서 더 자세히 다루겠다. 

 

 

2.2.4. Mask에 관하여 

클라이언트에서 서버로 전송되는 프레임의 payload 부분은 마스크 처리가 된다(1). 하지만 서버에서 클라이언트로 전송되는 payload는 마스크 처리가 되지 않는다(0). 이를 어길 시, 예를 들어 클라이언트에서 서버로 전달되는 프레임이 마스킹처리가 되지 않거나 서버에서 클라이언트로 전달되는 프레임이 마스크처리가 되어있다면, 즉시 연결을 종료해야 한다. 왜 클라->서버로의 프레임만 마스크처리하는 것일까? 서버->클라 프레임은 왜 마스크처리를 하지 않는 것일까?  

 

효율성과 성능 측면에서 보면, 서버는 n개의 클라이언트와 연결되어 있으며, 많은 양의 데이터를 처리하고 전송해야 한다. 만약 서버가 클라이언트로 전송하는 모든 데이터를 추가적인 연산을 통해 마스크 처리해야 한다면, CPU 사용률을 높이고 서버의 부하가 크게 증가할 수 있다. 

보안 측면에서도 접근해 보자. 일반적으로 클라이언트는 대상이 불특정 다수이고, 다양한 환경에서 실행되기 때문에 중간에서 공격을 받을 수도 있고, 데이터가 변조될 위험 등 다양한 보안 위협에 노출될 수 있다. 따라서 클라이언트에서 서버로 전송되는 데이터는 마스크를 통해 보안을 강화해야 한다. 하지만 서버는 중앙화된 신뢰할 수 있는 엔티티로 간주된다. 그래서 서버에서 전송되는 데이터는 보안 위협에 노출되지 않는다. 

 

WebSocket 프로토콜 설계자들은 이러한 이유 때문에 클라->서버로의 데이터에만 마스크를 처리하는 것이 적합하다고 판단했다. 이를 통해 프로토콜의 복잡성도 줄이고 보안도 유지할 수 있다. 다시 말해, 효율성과 성능, 보안 측면에서 client-server model의 특성을 고려한 결정이라고 볼 수 있겠다. 

 

 

2.2.5. Ping/Pong

Ping/pong frame은 웹소켓 프로토콜에서 연결 상태를 확인하고, 연결이 유효한지 유지하기 위해 사용되는 control frame이다. 즉, 웹소켓 프로토콜에서는 ping/pong frame이 heartbeat 역할을 하며 생존 신호를 주고받는다. 클라이언트나 서버가 상대방의 연결 상태를 확인하기 위해 ping frame(Opcode=0x9)을 보낸다. 여기에는 페이로드가 비어있을 수도 있고, 짧은 페이로드를 포함할 수 있다. 보내는 쪽은 상대방의 pong frame을 기다린다. ping frame을 받은 쪽은 최대한 빨리 pong frame으로 응답해야 하는데, 여기의 페이로드는 ping frame의 페이로드와 일치해야 한다. 

왜 이런 작업이 필요할까? 가장 큰 목적은 서로의 연결 상태를 확인하기 위함이다. pong frame을 받았다는 뜻은 연결이 유효하다는 뜻이고, 일정 시간 pong frame을 받지 못하면 연결이 끊어졌거나 네트워크에 문제가 있다고 판달할 수 있다(타임아웃). 이를 통해 연결을 다시 설정하는 로직을 추가할 수 있다. 또한, 클라이언트와 서버 간의 상태를 동기화하는 데 도움이 된다. 예를 들어, 클라이언트가 주기적으로 서버와의 연결 상태를 확인해서 애플리케이션 상태를 유지할 수 있으며, ping frame을 통해 문제를 조기에 감지해서 해결함으로써 사용자 경험을 향상할 수 있다. 

 

 

2.3. Connection Termination

WebSocket 연결을 종료할 때는 클라이언트 또는 서버가 클로즈 프레임(Opcode = 0x8)을 전송한다. 클로즈 프레임에는 선택적으로 상태 코드와 종료 이유를 포함할 수 있다. 

+-------+------+-------------+------------------+
| FIN=1 | 0x8  | Payload Len |  Status Code,    |
|       |      |             |  Optional Reason |
+-------+------+-------------+------------------+

클로즈 프레임을 수신한 쪽은 동일한 클로즈 프레임으로 응답한다. 이 과정이 완료되면 TCP 연결이 종료된다.


3. HTTP vs WebSocket

HTTP와 WebSocket을 비교해 보자. 

연결 방식 비연결형 (Connectionless) 연결형 (Connection-oriented)
통신 방식 요청/응답 양방향
상태 유지 stateless stateful
프로토콜 헤더 비교적 큰 헤더 작은 헤더
데이터 전송 텍스트 및 바이너리 텍스트 및 바이너리
실시간성 낮음 높음
사용 사례 best for RESTful application real-time, gaming, chat applications
프레임 방식 없음 프레임 단위 전송
포트 80 (HTTP), 443 (HTTPS) 80 (ws://), 443 (wss://)

 

3.1. Handshake 관점에서의 비교

프로토콜 비연결형 (Connectionless) 연결형 (Connection-oriented)
핸드셰이크 빈도 각 요청마다 핸드셰이크 초기 핸드셰이크 후 지속적인 연결
핸드셰이크 과정 TCP 핸드셰이크 + HTTP 요청/응답 HTTP 업그레이드 요청/응답 + WebSocket 연결
연결 유지 시간 각 요청 후 종료 초기 연결 후 지속
상태 유지 무상태 (Stateless) 상태 유지 (Stateful)

 

HTTP는 모든 요청마다 새로운 handshake를 수행해야 한다. 각 요청이 독립적이고, 연결이 request/response 후 종료되는 비연결형 프로토콜이라는 뜻이다. 반면 WebSocket은 초기 HTTP handshake를 통해 연결을 설정한 후에는 지속적인 연결을 유지한다. 이를 통해 양방향 통신을 가능하게 하며 효율적인 통신을 할 수 있다. 

 

3.2. 메시지 크기

헤더 오버헤드 작음
연결 설정 각 요청마다 TCP 연결 설정 필요 초기 핸드셰이크 후 지속적인 연결 유지
데이터 전송 요청/응답 모델로 큰 헤더 포함 프레임 단위로 작은 헤더와 함께 데이터 전송
효율성 오버헤드가 큼 오버헤드가 적어 효율적
연결 유지 각 요청 후 연결 종료 지속적인 연결 유지로 효율적인 데이터 전송

 

"Hello"라는 메시지를 전송하는 경우를 비교해보자. HTTP 요청/응답의 경우 요청헤더(약 200byte) + 응답헤더(약 100byte) + payload (5byte) = 총 305 byte이다. WebSocket 프레임의 크기는 헤더(6byte) + payload(5byte) = 11byte이다. 차이가 확실히 보인다. HTTP는 각 요청마다 큰 헤더 오버헤드를 가지고, 각 크기는 매우 크다. WebSocket은 초기 handshake 이후에는 작은 헤더 오버헤드를 가지고, 지속적인 연결로 효율이 높다. 

 


4. WebSocket의 한계와 해결 방법

4.1. 브라우저 호환성 문제 

- Problem

모든 브라우저가 웹소켓을 지원하지 않는다. Can I use 사이트에서 확인해 봤을 때 최신 브라우저는 대부분 지원하지만, 오래된 버전이나 특정 환경에서는 지원하지 않는 것을 볼 수 있다. 

https://caniuse.com/?search=websocket

 

- Solution: fallback mechanism 

웹소켓을 지원하지 않는 환경에서는 polling, long polling, streaming 등의 대체 기술을 사용한다. 이를 폴백 메커니즘이라고 하는데, SockJSSocket.IO와 같은 라이브러리가 바로 폴백 메커니즘을 사용하여 브라우저 호환성 문제를 해결한다. 

 

Socket.IO가 어떤 방식으로 동작하는지 뜯어보자. Socket.IO의 공식문서를 보면, long polling을 fallback으로 사용한다고 명시되어 있다. 더 자세히는, 초기에 long polling으로 연결을 시도하고, 웹소켓 연결이 가능해지면 업그레이드하는 방식이다. 왜냐하면 모든 브라우저가 웹소켓을 지원하지 않기 때문인데, 지원하지 않는 곳에서 처음부터 웹소켓 연결을 시도하면 최대 10초 정도 기다려야 하는 일이 벌어질 수도 있기 때문이다. 

1. initial GET request: long polling 방식으로 연결 시도

2. 클라 -> 서버 데이터 전송 (long-polling)

3. 데이터 수신 (long-polling)

4. WebSocket upgrade 시도

5. upgrade 성공. 후기존의 long polling 연결을 종료하기 위한 마지막 요청. 이후의 모든 통신은 웹소켓을 통해 이루어짐. 

 

4.2. 데이터가 너무 날것이다

- Problem

WebSocket 프로토콜은 텍스트와 바이너리 프레임만 전송할 수 있으며, 메시지의 형식이나 구조가 정해져 있지 않다. 따라서 프로젝트가 커짐에 따라 클라-서버 간에 어떤 형식으로 메시지를 주고받을지, 주고받는 메시지의 타입은 무엇인지, 어떻게 이를 파싱 할지의 로직을 직접 설계해야 한다. 즉, 데이터 형식과 메시지 구조는 자유롭지만 이를 정의하고 관리하는 부담이 크다. 

 

- Solution: STOMP

STOMP를 사용하면 메시지를 더 구조화된 형태로 관리할 수 있어서 명확한 소통이 가능해진다. STOMP는 Simple Text Oriented Messaging Protocol의 약자로, 웹소켓과 함께 사용할 수 있는 서브 프로토콜이다. STOMP는 COMMAND, Headers, Body를 포함한 프레임 형식을 가진다. 

COMMAND: 메시지의 유형. ex) CONNECT, SEND, SUBSCRIBE
Headers: 메시지의 메타데이터
Body: 실제 데이터

 

"Hello"를 전송하기 위해 웹소켓만 사용하는 경우와 STOMP를 함께 사용하는 경우 데이터가 어떤 형식으로 전달되는지 비교해 보자. 먼저 웹소켓만 사용하는 경우 JSON, XML, text 등을 사용해서 메시지 구조를 애플리케이션에서 직접 정의하고 관리해야 한다. 

// client -> server
{
  "type": "message",
  "content": "Hello"
}

// server -> client
{
  "type": "response",
  "content": "Server received: Hello"
}

 

하지만 STOMP를 함께 사용할 경우, 메시지 구조는 STOMP 프로토콜에 따라 정의된 형식을 따른다. 

// client -> server
SEND
destination:/app/hello
content-type:application/json

{
  "name": "Hello"
}
^@

// server -> client
MESSAGE
subscription:sub-0
message-id:007
destination:/topic/greetings
content-type:application/json

{
  "content": "Hello, Hello!"
}
^@

이처럼 목적지와 메시지 타입을 명확하게 정의해서 일관적으로 관리할 수 있다. STOMP에 대해서는 공부할 것들이 더 있는데, 다른 글에서 더 자세히 다루겠다. => https://hwanheejung.tistory.com/42

 

4.3. 보안 문제 

주의해야 할 몇 가지 보안 문제와 해결 방법에 대해 간단히 알아보자. 

 

ws:// 대신 wss:// 

웹소켓은 ws:// (비암호화)와 wss://(SSL/TLS) 프로토콜을 지원하는데, 비암호화된 ws://를 사용하면 중간에서 변조될 위험이 있다. 따라서 항상 wss://를 사용하여 데이터를 암호화하자! 

 

XSS 공격

XSS 공격은 공격자가 클라이언트 측 스크립트를 웹페이지에 삽입하여 실행하는 공격이다. 따라서 클라이언트에서 오는 모든 데이터를 실행하기 전에 HTML 특수 문자를 이스케이프 처리하는 등 검증할 필요가 있다. 

 

CSRF 공격

CSRF 공격이란, 공격자가 인증된 사용자를 대신해서 요청을 보내는 공격이다. 이를 방지하기 위해 웹소켓 연결을 설정할 때 CSRF 토큰을 사용해서 정당성을 확인하자. 

 

인증 및 권한 부여

웹소켓 연결이 설정된 후, JWT 토큰과 같은 인증 토큰을 사용해서 클라이언트의 신원을 확인하고, 권한을 검증하자. 

 

서버 자원 고갈 문제

웹소켓은 지속적인 연결을 유지하기 때문에 공격자가 웹소켓을 서버가 감당하기 힘들 만큼 많이 열어서 서버 자원이 고갈될 수 있다. 따라서 연결 수를 제한하고, 비정상적인 활동을 감지하여 차단해야 한다. 또한 일정 시간 동안 데이터 통신이 없으면 연결을 종료하여 자원을 효율적으로 관리하자. 

 


Reference

- https://en.wikipedia.org/wiki/Exponential_backoff

- https://javascript.info/long-polling

- https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events

- https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

- https://datatracker.ietf.org/doc/html/rfc6455

- https://www.youtube.com/watch?v=rvss-_t6gzg

- https://socket.io/docs/v4/how-it-works/