웹 소켓 (WebSocket)
1. 웹 소켓 (WebSocket) 개념
웹소켓(WebSocket)은 양방향 통신을 지원하는 네트워크 프로토콜로, 클라이언트와 서버 간의 실시간 데이터 전송을 위해 사용된다.
기존의 HTTP 프로토콜은 클라이언트가 서버에 요청을 보내고 서버가 응답을 반환하는 단방향 통신을 지원했지만, 웹소켓은 이와 달리 양방향 통신을 가능하게 한다.
웹소켓은 주로 웹 애플리케이션에서 실시간 통신을 구현하는 데 사용된다.
(1) 웹소켓의 특징
- 양방향 통신
- 웹소켓은 클라이언트와 서버 간에 데이터를 양방향으로 실시간으로 주고받을 수 있다. 이를 통해 실시간 채팅, 멀티플레이어 게임, 주식 시세 업데이트 등 다양한 응용 프로그램을 개발할 수 있다.
- 낮은 지연
- 웹소켓은 TCP 연결을 사용하며, 연결을 유지한 상태에서 데이터를 교환하기 때문에 데이터 전송에 있어서 높은 지연 없이 실시간 통신이 가능하다.
- 단일 연결 유지
- 웹소켓은 클라이언트와 서버 간의 단일 연결을 유지하면서 여러 번의 메시지 교환을 처리할 수 있다. 이는 HTTP와 달리 매번 새로운 연결을 맺지 않아도 되므로 연결 설정의 오버헤드를 줄여준다.
- 프로토콜 확장 기능
- 웹소켓은 초기에 정의된 프로토콜을 기반으로 하며, 필요할 경우 확장하여 추가적인 기능을 지원할 수 있다.
(2) 웹소켓 통신 절차
- 클라이언트가 서버에 웹소켓 연결을 요청한다.
- HTTP 형식으로 요청해야 하며 반드시 GET Method를 통해서만 진행해야 한다. (핸드 셰이크 요청)
- 클라이언트와 서버간의 신원 확인 후
- 서버에서 웹소켓 연결을 수락하면 프로토콜에 ws로 변환된다.
- HTTP 요청이 upgrade 된다는 의미
- 업그레이드 후에는 http://~ 가 아닌 ws://~ 로 요청을 보내야 한다.
- 양방향 웹소켓 통신 시작한다.
- 연결을 종료시키기 위해서는 Closing 핸드셰이크가 필요하다.
(2-1) HandShake 과정 예시
(ws://localhost:8080/chat)으로 접속하려고 한다고 가정한다.
🌻 요청 (Request) 헤더
GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://localhost:9090
- GET / chat HTTP/1.1
- 웹소켓의 통신 요청에서, HTTP 버전은 1.1 이상이어야 하고 GET 메서드를 사용해야 한다.
- Upgrade
- 이미 생성된 커넥션을 다른 프로토콜로 업그레이드/변경, 즉,프로토콜을 전환하기 위해 사용하는 헤더
- 웹소켓 요청시에는 반스에 websocket이라는 값을 가지며, 이 값이 없거나 다른 값이면 cross-protocol attack이라고 간주하여 웹소켓 접속을 중지시킨다.
- 앞쪽에 배치할수록 우선순위가 높음
- Connection
- 현재의 전송이 완료된 후 네트워크 접속을 유지할 것인가에 대한 정보
- 웹소켓 요청시에는 반드시 Upgrade 라는 값을 가진다
- Upgrade와 마찬가지로 이 값이 없거나 다른 값이면 웹소켓 접속을 중지시킨다.
- Sec-WebSocket-Key
- 유효한 요청인지 확인하기 위해 사용하는 키 값
- Sec-WebSocket-Version
- 클라이언트가 사용하고자 하는 웹소켓 프로토콜 버전
- Origin
- CORS 정책으로 만들어진 헤더
🌻 응답(Response) 헤더
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
- HTTP/1.1 101 Switching Protocols
- 101은 HTTP에서 WS로 프로토콜 전환이 승인되었다는 응답코드
- Sec-WebSocket-Accept
- 요청 헤더의 Sec-WebSocket-Key에 유니크 아이디를 더해서 SHA-1로 해싱한 후 base64로 인코딩한 결과
- 웹소켓 연결이 개시되었음을 알린다.
(3) 메시지 브로커(Message Broker)
- 메시지 기반의 통신에서 중간에 위치하여 메시지의 송수신을 관리하고 조정하는 시스템이나 컴포넌트
- 메시지 브로커는 다수의 클라이언트나 서버 간에 메시지를 안전하고 효율적으로 전달하기 위한 역할을 수행한다.
- 구독자는 특정 topic을 구독하고 발행자는 해당 topic으로 메시지를 날린다.
- 메시지 브로커는 이 메시지를 관리하여 이를 구독 중인 구독자들에게 메시지를 전송한다.
- 즉, 메시지를 바로 전달하는 것이 아니라 중간에 존재하는 메시지 브로커에게 전달하고 이 브로커가 구독자들에게 전달해주는 형태
(4) STOMP (Streaming Text Oriented Messaging Protocol) 프로토콜
- 실시간 메시징 시스템을 위한 간단하고 텍스트 기반의 프로토콜
- STOMP는 웹소켓을 비롯한 다양한 메시징 시스템에서 사용되며, 메시지 브로커와 클라이언트 간의 상호 작용을 효과적으로 구성할 수 있도록 설계되었다.
- STOMP 프로토콜은 WebSocket 위에서 동작하며, 클라이언트와 서버가 전송할 메시지의 유형, 형식, 내용들을 정의하는 메커니즘이다.
- 메시지를 공급하는 주체와 소비하는 주체를 분리하여 제공하는 메시징 방법인 publish(발행)/subscribe(구독) 방식을 기반으로 동작한다. 이를 통해 메시징 전송을 효율적으로 할 수 있다.
- STOMP를 이용하면 메시지의 헤더에 값을 줄 수 있어 헤더 값을 기반으로 통신 시 인증 처리를 구현하는 것도 가능하며, STOMP 스펙에 의한 규칙만 잘 지키면 여러 언어 및 플랫폼 간 메시지를 상호 운영할 수 있다.
- STOMP 형식
COMMAND
header1:value1
header2:value2
Body^@
- 클라이언트는 메시지를 전송하기 위해 COMMAND로 SEND 또는 SUBSCRIBE 명령을 사용하며, header와 value로 메시지의 수신 대상과 메시지에 대한 정보를 설정할 수 있다.
- destination는 의도적으로 정보를 불분명하게 정의해 STOMP 구현체에서 구문에 따라 직접 의미를 부여할 수 있다. 따라서 destination 정보는 STOMP 서버 구현체마다 달라질 수 있기 때문에 각 구현체의 스펙을 살펴봐야 한다.
- 일반적으로 아래의 형식을 따른다
- "topic/.." -> publish-subscribe (1:N)
- "queue/..." -> point-to-point (1:1)
- 예) 클라이언트A 가 5번 채팅방 구독 즉, 5번 채팅방 입장
SUBSCRIBE
destination: /topic/chat/room/5
id: sub-1
^@
- 예) 클라이언트B가 채팅 메시지 전송
SEND
destination: /pub/chat
content-type: application/json
{"chatRoomId": 5, "type": "MESSAGE", "writer": "clientB"} ^@
- STOMP 서버는 모든 구독자에게 메시지를 브로드캐스팅(BroadCasting)하기 위해 MESSAGE COMMAND를 사용할 수 있다.
- 서버는 내용을 기반으로 메시지를 전송할 브로커에 전달한다.
MESSAGE
destination: /topic/chat/room/5
message-id: d4c0d7f6-1
subscription: sub-1
{"chatRoomId": 5, "type": "MESSAGE", "writer": "clientB"} ^@
서버는 불분명한 메시지를 전송할 수 없기 때문에 서버의 모든 메시지는 특정 클라이언트 구독에 응답해야 하고, 서버 메시지의 "subscription-id" 헤더는 클라이언트 구독 "id" 헤더와 일치해야한다.
Flow of Messages
- Message : headers와 payload를 포함하는 메시지의 표현
- MessageHandler : Message 처리에 대한 계약
- SimpleAnnotationMethod : @MessageMapping 등 Client의 SEND를 받아서 처리
- channel
- clientInboundChannel : Websocket Client로부터 들어오는 요청을 전달하며, WebSocketMessageBrokerConfigurer를 통해 intercept, taskExecutor를 설정할 수 있다
- 클라이언트로부터 받은 메시지를 서버로 전달
- clientOutboundChannel : WebSocket Client로 Server의 메시지를 내보내며, WebSocketMessageBrokerConfigurer를 통해 intercept, taskExecutor를 설정할 수 잇다.
- 클라이언트에게 메시지 전달
- brokerChannel : Server 내부에서 사용하는 채널, 이를 통해 SimpleAnnotationMethod는 SimpleBroker의 존재를 직접 알지 못해도 메시지를 전달할 수 있다.
- 서버의 어플리케이션 코드 내에서 브로커에게 메시지 전달
- clientInboundChannel : Websocket Client로부터 들어오는 요청을 전달하며, WebSocketMessageBrokerConfigurer를 통해 intercept, taskExecutor를 설정할 수 있다
💡 Spring에서 지원하는 STOMP를 사용하면 외부 메시지 브로커의 사용 없이 STOMP의 기능 중 Simple In-Memory Broker를 이용해 SUBSCRIBER 중인 다른 클라이언트에게 메시지를 전송할 수 있다.
💡 " /sub/..." 경로로 메시지를 보내면 이 메시지는 브로커로 향하게 되고 브로커는 이 경로를 구독중인 메시지를 발송한다. 즉, 바로 구독자에게 메시지가 전달된다.
💡 "/pub/..." 경로로 메시지를 보내면 이 메시지는 @MessageMapping 어노테이션이 붙은 곳으로 향하게 된다. 즉, 메시지를 가공할 수 있다.
👍 STOMP Benefits
Spring framework 및 Spring Security는 STOMP를 사용하여 WebSocket만 사용할 때보다 더 다채로운 모델링을 할 수 있다.
- Messagin Protocol을 만들고 메시지 형식을 커스터마이징 할 필요가 없다.
- RabbitMQ, ActiveMQ와 같은 Message Broker를 이용해, Subscription(구독)을 관리하고 메시지를 브로드캐스팅할 수 있다.
- WebSocket 기반으로 Connection(연결)할때마다 WebSocketHandler를 구현하는 것보다 @Controller 된 객체를 이용해 조직적으로 관리할 수 있다.
- 즉, 메시지는 STOMP의 "destination" 헤더를 기반으로 @Controller 객체의 @MethodMapping 메서드로 라우팅된다.
- STOMP의 "destination" 및 Message Type을 기반으로 메시지를 보호하기 위해 Spring Security를 사용할 수 있다.