프로젝트를 진행하면서 실시간 채팅과 같은 사용자간 통신에서 WebSocket에 대한 이야기가 나온 적 있다. 분명 전공교육때 배웠었을텐데 정확히 어떤건지 기억이 잘 나지않아서 별다른 의견을 못냈던 기억이 있다.
따라서, 이번 포스팅에서는 WebSocket이 어떤 것인지 알아볼 예정이다.
일반적인 웹 애플리케이션 환경에서 서버의 변경사항을 주기적으로 확인하기 위해서는 어떻게 해야할까?
가장 먼저생각나는 방법은 아마 주기적으로 요청을 보내고, 현재 값과 비교하는 방법일 것이다.
변경 추적하기 위해 수정날짜와 같은 가벼운 항목을 검사하고, 현재값과 다르면 변경할 값을 가져오는 식으로 작성할 수 있다.
이 방법을 폴링(Polling) 이라고 부르는데, 여기엔 몇가지 문제점이 존재한다.
먼저, TCP통신에서 매 요청들은 헤더를 포함하고 있다. 이를 반복적으로 주고받는 것은 네트워크에 부담을 증가시킨다.
또한, 클라이언트 마다 1.변경사항 확인을 위한 요청
, 2.데이터 교환을 위한 요청
총 2개의 연결을 구성해야한다.
이 외에도 서버에 변경사항이 없더라도 주기적으로 요청이 발생하므로, 불필요한 요청이 발생한다는 문제점이 있다.
keep-alive
기능을 일정시간동안 연결을 유지시킬 수 있지만, 주기적인 요청 자체가 실시간성이 떨어진다는 단점, 그리고 서버에서 연결유지를 위한 관리자원이 늘어나는 문제가 있다.
이를 해결하기 위해 등장한 양방향 통신 프로토콜이 WebSocket이다.
WebSocket은 통신 프로토콜로, 하나의 TCP 연결로 클라이언트와 호스트간 양방향 통신을 가능하게 해준다.
브라우저 기반의 애플리케이션에서 단일 TCP 연결로 서버와의 양방향 통신 매커니즘을 제공하는 것이 목적인 프로토콜이다.
웹 브라우저에서 사용되는 출처(origin) 기반의 보안모델이 적용되므로, 설정을 잘못하면 CORS에러가 뜰 가능성이 있다.
기본적으로 HTTP에서의 양방향 통신을 목적으로 구성되었기 때문에 80 혹은 443 포트에서 작동한다.
하지만, HTTP에 제한되어있지는 않으므로 전용포트를 구성해 연결을 구성할 수도 있다.
WebSocket은 크게 2가지의 파트로 구분된다.
첫 번째는 연결구성과 관련된 handshake, 두 번째는 데이터 전송이다.
handshake는 연결을 시작하는 Opening Handshake와 연결을 마무리 짓는 Closing Handshake로 나뉜다. WebSocket은 TCP위에 존재하므로, 이 handshake 과정은 TCP의 그것과 유사하다.
handshake를 위해서 클라이언트는 WebSocket 연결을 위한 요청 URI를 알고있어야한다.
WebSocket 연결을 위한 요청 URI 형태
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
ws-URI
는 80포트를 사용하는 HTTP 통신, wss-URI
는 443포트를 사용하는 HTTPS 통신에 사용된다.
localhost:8080
을 기준으로 예시를 든다면 ws://localhost:8080/{path}
의 형태가 될 것이다.
이렇게 클라이언트가 생성하는 요청은 아래와 같은 형태로 서버에 전송된다.
GET /chat HTTP/1.1 // GET + HTTP 1.1이어야 함
Host: server.example.com
Upgrade: websocket // 필수
Connection: Upgrade // 필수
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 각 연결마다 무작위인 16바이트 값의 base64 인코딩된 nonce 값
Origin: http://example.com // 필수
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13 // 필수
이후, 서버에서 오게되는 응답은 아래의 형태이다.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Opening Handshake을 통해 클라이언트와 서버간 연결이 수립되면 데이터 전송이 이뤄진다.
WebSocket에서 데이터는 일련의 프레임을 사용하여 전송된다.
프레임은 위와 같은 형태로 구성된다.
FIN
: 마지막 프레임 표시 ( 값이 1
인 경우 마지막 프레임 )opcode
: payload 데이터 유형 표시 ( 0x1 : 텍스트
, 0x2 : 바이너리
, 0x8 : 연결 종료
... )mask
: payload 데이터의 마스킹 여부 표시Payload Length
: payload 데이터의 크기실제로 WebSocket에서 주고받는 개념적 단위는
Message
다.프레임은 메시지를 잘게 나누어 보내는 전송단위를 의미한다.
서버 혹은 클라이언트에서 1000번대의 상태코드와 함께 opcode
가 0x8
인 Close Frame을 보냄으로써 이뤄진다.
상대측에서도 Close Frame을 보내면 TCP연결이 종료된다.
아래는 사용되는 상태코드 종류의 예시다.
1000
: 정상종료1001
: 서버 다운 혹은 페이지 벗어남1002
: 프로토콜 오류1003
: 수신할 수 없는 데이터타입
STOMP( Simple (or Streaming) Text Orientated Messaging Protocol )는
메시지 지향 미들웨어( RabbitMQ, ActiveMQ 등 )과 함께 동작하도록 설계된 단순 텍스트 기반 프로토콜을 뜻한다.
HTTP를 모델로 한 프레임기반의 프로토콜이기에 주로 WebSocket과 함께 사용된다.
간단하게 표현하면 WebSocket에서 전송되는 메시지들의 형식을 결정짓는다.
COMMAND
header1:value1
header2:value2
Body^@
STOMP에서 사용되는 프레임은 Command, Header, Body의 구조를 띄며,
SEND, MESSAGE, CONNECT, CONNECTED
등의 Command를 이용해 현재 메시지가 어떤 타입인지 나타낸다.
이때, SEND, MESSAGE, ERROR
프레임만이 Body를 가진다.
STOMP는 Client와 Server로 구분해여 생각할 수 있다.
각 서버와 클라이언트 목록은 공식문서에 소개된다.
STOMP Client는 SEND
프레임을 통해 메시지를 서버로 보내거나,
SUBSCRIBE
프레임을 통해 메시지를 수신받은 목적지를 지정하여 메시지를 수신하는 기능이 있다.
STOMP Server는 메시지를 보낼 수 있는 대상집합으로 모델링된다.
즉, 클라이언트에서 목적지로 삼을 수 있는 대상들로 구성된다.
정리하자면 STOMP는 HTTP와 TCP처럼, 주로 WebSocket과 함께 사용되는 프토토콜이다.
WebSocket에서 프레임을 이용해 클라이언트와 서버간 메시지를 주고받을 때,
프레임의 payload 부분을 어떤 형식으로 보낼지를 정하며, pub/sub 모델에서 목적지를 정의하는 역할을 한다.
메시지 지향 미들웨어와 함께 동작하도록 설계되었기 때문에 여러 Message Broker들과 함께 사용할 수 있다.
Spring 기준으로 Spring에 내장된 SimpleBroker를 사용하여 구독, 메시지 저장, 목적지로의 메시지 전송이 가능하다. 하지만, SimpleBroker는 클러스터링에 적합하지 않으므로, 더 많은 기능을 가진 외부 Message Broker(RabbitMQ, ActiveMQ 등)을 사용할 수 있다.
STOMP는 결국 프레임기반의 메시지전달과 관련된 프로토콜이기 때문에, 메시지를 분배하고 관리하는 Message Broker와 함께 사용하게 된다.
브로커들을 이용해 메시지를 topic에 따라 관리하거나, 큐 관리, 전달 순서관리 등의 기능을 지원받는 관계를 띈다.