[WebSocket] Stomp 프로토콜 1.2 명세 번역

웹 개발

2021. 12. 19.

주의: 이 문서는 명세서 원본이 아닙니다. 문서를 읽어보면서 개략적으로 개인 공부를 위해 한글로 옮긴 것입니다, 명세의 모든 문장을 포함하지 않았습니다.

STOMP 프로토콜

배경

STOMP는 스크립트 언어(Ruby, Python, Perl 등)에서 엔터프라이즈 메시지 브로커와 연결할 필요성이 있어 탄생했다. STOMP는 스크립트 환경에서 "신뢰 가능한 단일 메시지 전송 & 연결 해제"와 "특정 목적지에 쌓여있는 메시지를 모두 읽어오기" 같은 일들을 수행하는, 흔하면서도 논리적으로 단순한 연산들이다.

프로토콜 개요

프레임 기반 프로토콜

STOMP는 HTTP 위에 올라가는 프레임으로 모델링된다. 프레임은 하나의 커맨드, 선택적인 헤더들, 그리고 선택적인 바디를 갖는다. STOMP가 사용하는 기본 인코딩은 UTF-8. 텍스트 기반 메시지를 지원하지만, 바이너리 메시지를 전송하기 위한 명세도 있다.

STOMP 서버

서버는 목적지(destination) 여럿을 보유한다. 메시지는 이 목적지를 향하여 전송된다. STOMP 프로토콜은 목적지 문자열을 이해하지 않는다. 왜냐하면 목적지 문자열의 신택스는 서버의 구현을 따르기 때문이다. 또 STOMP는 목적지의 메시지 교환 시멘틱(delivery semantics)에 관해서도 관심이 없다. 메시지 교환 시멘틱은 서버 별로, 목적지 별로 다양하다. 이 탓에 서버는 STOMP를 지원하는 다양한 시멘틱을 창의적으로 구성할 수 있을 것이다.

STOMP 클라이언트

클라이언트는 2가지 역할을 수행한다. (병행 가능)

  • 생산자: 메시지를 목적지로 보낸다. SEND 프레임을 사용한다.
  • 소비자: 목적지를 구독하기 위한 SUBSCRIBE 프레임을 보낸다. 그리고 서버로부터 MESSAGE 프레임에 담긴 메시지를 수신한다.

설계 철학

단순함상호운용성: STOMP는 경량 프로토콜로서 클라이언트와 서버 양측이 다양한 언어로 손쉽게 구현할 수 있어야 한다. 즉 프로토콜은 서버의 아키텍쳐에 대해 많은 제한이나 기능을 요구하지 않는다. 목적지 네이밍이나 신뢰 시멘틱 등이 그 예다.

STOMP 프레임

STOMP는 프레임 기반 프로토콜이며 신뢰할 수 있는 양방향 스트리밍 네트워크를 보장하는 프로토콜이다. 클라이언트와 서버는 STOMP 프레임 형식을 스트림으로 전송하며 통신한다.

프레임 구조

COMMAND
header1:value1
header2:value2

Body^@

바디 직후에는 NULL 아스키 문자가 위치해야한다. 명세 문서에서는 이를 가시적으로 표현하기 위해^@로 표기한다.

인코딩

커맨드와 헤더는 UTF-8로 인코딩한다. CONNECTCONNECTED 프레임을 제외, 헤더에 존재하는 캐리지 리턴, 라인 피드, 콜론 문자를 이스케이프 처리한다.

C 스타일의 문자 이스케이프를 사용한다. 프레이믈 인코딩할 때 다음과 같이 이스케이프를 적용해야 한다.

  • 캐리지 리턴 → \r
  • 라인 피드 → \n
  • 옥텟 58 → \c
  • 역슬래시 → \\

프레임을 디코딩 할 때도 마찬가지이다. 열거하지 않은 이스케이프를 발견할 경우엔 치명적인 프로토콜 에러로 취급해야 한다.

바디

SEND, MESSAGE, ERROR 프레임만이 바디를 보유할 수 있다. 이외 프레임은 바디를 가질 수 없다.

표준 헤더

content-length

모든 프레임은 content-length 헤더를 명시할 수 있다. 메시지 바디의 octect 카운트를 센 값을 나타낸다. 이 헤더가 명시되었다면 수신 측은 바디에서 명시된 만큼 옥텟을 읽어들이려고 시도해야한다.

content-type

프레임에 바디가 존재하는 SEND, MESSAGE, ERROR 프레임의 경우에 content-type 헤더를 명시하여 수신 측이 바디를 해석할 수 있도록 도움을 주어야 한다. 이 헤더가 명시되어 있다면 바디의 값을 MIME 타입으로 인지해야 한다. 헤더가 명시되어 있지 않다면 바디를 바이너리 blob으로 인지해야 한다.

  • MIME 타입 설명
    • text/ 로 시작하는 타입: UTF-8로 인코딩 된 것으로 가정한다. 만약 다른 인코딩의 텍스트를 사용했을 경우 ;charset<encoding> 을 추가적으로 덧붙여 준다. text/html;charset=utf-16 처럼.
    • text/ 로 시작하지 않는 타입: 이런 타입도 텍스트로 해석될 여지가 있다면 application/xml;charset-utf-8 인코딩을 덧붙여 표시한다.

서버와 클라이언트는 반드시 UTF-8 인코딩 및 디코딩을 할 수 있어야 하며, 상호운용성을 최대화하기 위해 UTF-8로 인코딩된 텍스트를 사용하길 권장한다.

receipt

CONNECT를 제외한 프레임은 receipt를 명시하고 임의의 값을 배정할 수 있다. 이에 서버는 클라이언트 프레임을 처리한 것에 대한 ACK로서 RECEIPT 프레임을 회신한다.

SEND
destination:/queue/a
receipt:message-12345

hello queue a^@

반복적인 헤더 엔트리

메시징 시스템이 토폴로지를 구성할 수 있기에, 메시지는 소비자에게 도착하기 까지 복수의 메시징 서버를 지나다닐 수 있다. 각 STOMP 서버는 헤더 값을 업데이트 하고 싶다면 두 방법 중 하나를 택한다.

  1. 동일 헤더를 앞쪽에 추가하기
  2. 헤더 값을 직접 바꾸기

클라이언트 혹은 서버는 프레임에서 반복된 헤더 엔트리를 발견하게 된다. 이 경우 가장 처음 엔트리를 의미 있는 것으로 생각해야 한다.

MESSAGE
foo:World
food:Hello

^@

크기 제한

클라이언트가 서버 쪽의 메모리 활용에 문제를 야기하지 않도록, 서버는 프레임 크기 제한을 걸어도 된다.

  • 한 프레임이 포함할 수 있는 헤더의 개수
  • 헤더 영역의 최대 라인
  • 바디의 최대 크기

제한을 어긴 프레임을 발견할 경우 ERROR 프레임으로 회신하면 된다.

연결의 잔류

STOMP 서버는 클라이언트가 빠른 속도로 연결 - 연결 해제 하는 동작을 지원해야 한다.

즉 서버는 닫힌 연결을 오직 짧은 시간 동안만 잔류하도록 하여, 연결이 재설정되기 전에 제거해야 한다.

결국 클라이언트는 서버가 보낸 맨 마지막 프레임을 못 받게 될 수도 있다. (예를 들면, DISCONNECT 프레임에 대한 서버의 ERROR 회신 혹은 RECEIPT 회신)

연결하기

스트림 혹은 TCP 위에서 클라이언트는 CONNECT 프레임을 서버에 전송한다.

CONNECT
accecpt-version:1.0,1.1,1.2
host:stomp.github.org

^@

서버가 연결을 수락하면 CONNECTED 프레임을 회신한다. 연결을 거부할 경우 ERROR 프레임에 거절 사유를 포함하여 회신한다.

CONNECTED
VERSION:1.1

^@

하트 비팅

원격지 간 TCP 연결의 헬스 체크를 진행하기 위해 Heart-beating을 이용할 수 있다.

양측은 CONNECTCONNECTED 헤더에 heart-beat 헤더를 추가하고 두 개의 양수를 콤마로 구분해 표기한다.

  • 첫 번째 양수
  • 본인이 하트 비트를 몇 ms 주기로 보낼 수 있는지 명시한다. 0을 기입할 경우 하트 비트를 전송할 수 없음을 의미.
  • 두 번째 양수
  • 상대방의 하트 비트를 몇 ms 주기로 수신하고 싶은지 명시한다. 0을 기입할 경우 하트 비트를 수신하고 싶지 않다는 뜻.

이 헤더는 선택사항이다. 하지만 이 헤더를 명시하지 않았을 경우 heart-beat:0,0 으로 취급하여 상호 헬스 체크를 절대 진행하지 말아야 한다.

하트 비트 정책 파악 방법

CONNECT
heart-beat:<cx>,<cy>

CONNECTED
heart-beat:<sx>,<sy>
  • <cx> 혹은 <sy> 가 0이면 클라이언트는 하트 비트를 보내지 않는다.
  • 그렇지 않다면 MAX(<cx>, <sy>)ms의 주기로 클라이언트는 하트 비트를 보낸다.
  • 서버 측 하트 비트 전송 정책은 <sx>, <cy> 로 치환하여 생각하면 된다.

하트 비트에 대하여

일단 상대로부터 어떤 데이터를 전송 받았다면, 이로부터 상대방은 살아있는 상태라고 인지할 수 있다.

만약 n 밀리초마다 하트 비트를 수신하고 싶다고 가정하자.

  • 상대방은 매 n 밀리초 이내에 새로운 데이터를 보내와야 함.
  • 그렇다고 해서 상대방은 보내야 할 STOMP 프레임이 없을 수도 있음. 그렇다면 end-of-line 를 대신 보내면 된다.
  • 상대방이 n 밀리초 윈도우 동안 아무 데이터를 수신하지 않았으면, 상대방이 죽은 상태라고 인지할 수 있다.
  • 하지만 타이밍이 부정확할 수도 있으니, 수신자 측은 에러 마진을 두고 좀 더 인내하는 정책을 사용하길 권장한다.

클라이언트의 프레임

  • SEND
    SEND
    destination:/queue/a
    content-type:text/plain
    
    hello queue a
    ^@
  • 서버의 목적지로 메시지를 보낸다. 목적지 네이밍은 서버에서 정의한다. 딜리버리 시멘틱을 명시하는 네이빙 방식을 서버에서 구현해야 한다. 신뢰 시멘틱 역시 서버에서 구현을 정한다. 에를 들어 transaction 같은 추가적인 헤더를 사용할 수 있다.
  • SUBSCRIBEid는 해당 구독에 식별자를 부여한다. 그러므로 추후 발생하는 MESSAGEUNSUBSCRIBE 프레임이 해당 ID에 연관되도록 한다. 한 커넥션에서 여러 구독을 신청하는 경우 ID를 다르게 사용해야 한다.
    SUBSCRIBE
    id:0
    destination:/queue/foo
    ack:client
    
    ^@
  • ack는 auto, client, client-individual 값을 갖는다. ACK 프레임 전송에 대한 정책을 표시하는 것이다. 클라이언트는 서버가 보내온 구독 메시지를 처리하고 ACK 프레임을 전송할 수 있다.
  • 서버의 목적지를 구독한다. 구독한 목적지의 자료를 서버에서 MESSAGE 프레임으로 지속 전송할 것이다.
  • UNSUBSCRIBE
  • UNSUBSRIBE id:0 ^@
  • BEGIN
    BEGIN
    transaction:tx1
    
    ^@
  • 트랜잭션 시작을 알린다. 여기서 tx1은 트랜잭션 동안 SENDACK 메시지의 트랜잭션 헤더에 사용되며, 트랜잭션 동안의 메시지들은 원자적으로 처리된다.
  • COMMIT
    COMMIT
    transaction:tx1
    
    ^@
  • 진행중인 트랜잭션을 커밋한다.
  • ABORT
    ABORT
    transaction:tx1
    
    ^@
  • 진행죽인 트랜잭션을 파기하고 롤백한다.
  • ACK
    ACK
    id:12345
    transation:tx1
    
    ^@
  • 클라이언트가 구독을 할 때 ack:client, client-individual 헤더를 명시했다면, 구독 메시지를 수신한 다음 ACK를 날릴 수 있다. ACK 프레임엔 id 헤더를 꼭 포함해야 한다. transaction 헤더는 ACK가 언급된 트랜잭션에 포함될 때 쓴다.
  • NACK
  • ACK의 반대이다. 메시지 소비를 실패했다고 서버에게 보낸다.
  • DISCONNECT
    DISCONNECT
    receipt:77
    ^@
    그럼 다시 클라이언트는 영수증 프레임을 대기하고, 이를 수신하여 소켓을 회수한다.
  • RECEIPT receipt-id:77 ^@
  • 연결을 해제한다. 여기서는 서버에게 연결을 끊고 영수증을 보내 달라고 요청하고 있다.

서버의 프레임

  • MESSAGE메시지 ID는 해당 메시지 프레임에 할당되는 고유한 값이다.
    MESSAGE
    subscription:0
    message-id:007
    destination:/queue/a
    content-type:text/plain
    
    hello queue a^@
  • 클라이언트의 ACK가 기대된다면 ack 헤더를 추가하여 클라이언트가 ack의 ID를 사용할 수 있게 해야한다.
  • 구독의 ID와 메시지 ID를 필수로 기재한다. 구독 ID란 SUBSCRIBE 프레임에 명시된 것과 동일하며,
  • RECEIPT
  • ERROR

프레임 구조 요약

In addition to the standard headers described above (content-length, content-type and receipt), here are all the headers defined in this specification that each frame MUST or MAY use:

  • CONNECTorSTOMP
  • REQUIRED: accept-version, host
    • OPTIONAL: login, passcode, heart-beat
  • CONNECTED
    • REQUIRED: version
    • OPTIONAL: session, server, heart-beat
  • SEND
    • REQUIRED: destination
    • OPTIONAL: transaction
  • SUBSCRIBE
    • REQUIRED: destination, id
    • OPTIONAL: ack
  • UNSUBSCRIBE
    • REQUIRED: id
    • OPTIONAL: none
  • ACKorNACK
    • REQUIRED: id
    • OPTIONAL: transaction
  • BEGINorCOMMITorABORT
    • REQUIRED: transaction
    • OPTIONAL: none
  • DISCONNECT
    • REQUIRED: none
    • OPTIONAL: receipt
  • MESSAGE
    • REQUIRED: destination, message-id, subscription
    • OPTIONAL: ack
  • RECEIPT
    • REQUIRED: receipt-id
    • OPTIONAL: none
  • ERROR
    • REQUIRED: none
    • OPTIONAL: message

BNF 언어

NULL                = <US-ASCII null (octet 0)>
LF                  = <US-ASCII line feed (aka newline) (octet 10)>
CR                  = <US-ASCII carriage return (octet 13)>
EOL                 = [CR] LF 
OCTET               = <any 8-bit sequence of data>

frame-stream        = 1*frame

frame               = command EOL
                      *( header EOL )
                      EOL
                      *OCTET
                      NULL
                      *( EOL )

command             = client-command | server-command

client-command      = "SEND"
                      | "SUBSCRIBE"
                      | "UNSUBSCRIBE"
                      | "BEGIN"
                      | "COMMIT"
                      | "ABORT"
                      | "ACK"
                      | "NACK"
                      | "DISCONNECT"
                      | "CONNECT"
                      | "STOMP"

server-command      = "CONNECTED"
                      | "MESSAGE"
                      | "RECEIPT"
                      | "ERROR"

header              = header-name ":" header-value
header-name         = 1*<any OCTET except CR or LF or ":">
header-value        = *<any OCTET except CR or LF or ":">

License

This specification is licensed under the Creative Commons Attribution v3.0 license.