기존의 HTTP는 요청을 보내야 응답이 오는 connectionless와 stateless 방식
새로운 정보를 받아오려면 먼저 요청을 보내야만 합니다.
지속적으로 받아올 새로운 정보가 있는지 확인해야 하는 경우에는 계속 요청을 보내야 합니다.
날씨나 차트 같은 실시간 정보를 모니터링 하려면 주기적으로 요청을 보내서 데이터를 갱신해야 합니다.
이러한 단점을 보완하기 위해 웹소켓이 탄생합니다.
웹소켓은 Transport protocol의 일종으로 양방향 소통을 위한 규약입니다.
요청을 먼저 보내지 않으면 데이터를 받아올 수 없었던 기존의 방식에서 벗어나
실시간 양방향 소통이 가능하게 해줍니다.
채팅이나 게임, 증권거래 등 실시간 서비스에서 많이 사용됩니다.
처음 연결 방법은 HTTP를 사용합니다.
HTTP 80 or 443 를 통해 핸드셰이킹을 먼저 실시하고
이후에는 웹소켓 프로토콜로 통신합니다.
Request:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example. com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
GET : 메소드는 GET
HTTP/1.1 : HTTP 1.1 이상
Upgrade : 웹소켓 프로토콜로 변경한다는 의미
Connection : Upgrade 헤더를 사용한다는 의미
Sec-WebSocket-Key : 서버와 클라이언트 간 인증에 사용
Sec-WebSocket-Protocol : 추가로 사용하고 싶은 서브프로토콜
Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
101 : 상태코드가 101이 아니면 핸드셰이킹이 실패 (계속 HTTP로 통신)
Sec-WebSocket-Accept : 서버와 클라이언트 간 인증에 사용
핸드셰이킹이 성공적으로 끝나면 이후로는 ws 프로토콜로 통신합니다.
ws:// 의 기본 포트는 80
wss:// 의 기본 포트는 443
http 기본 포트와 동일합니다.
웹소켓에서는 frame 단위로 데이터를 주고받습니다.
frame은 구조는 아래와 같습니다.
0 1 2 3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|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| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
4 5 6 7
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
8 9 10 11
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
12 13 14 15
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
FIN : 이 메세지가 마지막임을 나타냅니다. FIN이 0이라면 다음 메세지까지 계속 수신해야 합니다.
RSV : 사용하지 않습니다. 나중에 기능확장을 위한 여분의 자리입니다.
opcode : payload 데이터의 포멧을 나타냅니다. 0x0은 continuation, 0x1은 텍스트(UTF-8), 0x2 바이너리 데이터
MASK : 메세지가 마스킹 되어있는지를 나타냅니다. 클라이언트가 서버에 보내는 메세지는 항상 마스킹 되어있어야 합니다.
payload : 보내고자 하는 데이터입니다.
FIN과 opcode를 통해 분할된 메세지 전송 예시입니다.
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
첫번째 메세지에는 FIN=1이므로 전체 메세지가 도착했다는 의미이며 서버가 응답했습니다.
두번째 메세지에는 FIN=0이므로 계속 다음 메세지를 수신합니다.
세번째 메세지 처럼 계속 이어지는 메세지에는 opcode=0x0으로 들어갑니다.
네번째 메세지에서 FIN=1로 메세지를 마무리 합니다.
연결 종료는 opcode=0x8로 요청합니다.
웹소켓이 연결되어 있는 상태라면 ping 패킷을 보낼 수 있습니다. (opcode=0x9)
ping을 받으면 최대한 빨리 받은 payload 데이터를 그대로 다시 담아서 pong을 보내야 합니다. (opcode=0xA)
ping pong을 활용해 웹소켓 연결이 유지되어 있는지 주기적으로 확인이 가능합니다.
웹소켓에도 Status Code가 있습니다.
1000 : 정상
1001 : 연결 주체 중 한쪽이 떠남
1009 : 메시지가 너무 커서 처리하지 못함
1011 : 서버 측에서 비정상적인 에러 발생
...
https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
웹소켓은 payload에 문자열을 담아 전송하지만 일정한 형식이 정해져 있지 않습니다.
서버와 클라이언트에서 문자열을 해석하려면 추가적인 작업이 필요합니다.
때문에 서브 프로토콜을 통해 주고 받는 메세지의 형식을 정해서 사용하는 경우가 많습니다.
주로 사용하는 서브 프로토콜에는 STOMP가 있습니다.
STOMP는 프레임 기반 프로토콜이며, command-header-Body 구조입니다.
COMMAND
header1:value1
header2:value2
Body^@
클라이언트 command는 CONNECT, SEND, SUBSCRIBE, UNSUBSCRIBE, BEGIN, COMMIT, ABORT, ACK, NACK, DISCONNECT 등이 있습니다.
서버 command에는 CONNECTED, MESSAGE, RECEIPT, ERROR 등이 있습니다.
CONNECT
CONNECT
accept-version:1.0,1.1,2.0
host:stomp.github.org
^@
STOMP 클라이언트는 CONNECT 프레임을 전송하여 서버와 연결을 시작합니다.
CONNECTED
version:1.2
^@
서버가 연결 시도를 수락하면 CONNECTED 프레임으로 응답합니다.
ERROR
version:1.2,2.1
content-type:text/plain
Supported protocol versions are 1.2 2.1^@
서버가 연결을 거부한 경우 ERROR 프레임에 거부된 이유를 담아서 응답하는걸 권장합니다.
SEND
SEND
destination:/queue/a
content-type:text/plain
hello queue a
^@
SEND 커맨드로 메세지를 전송할 수 있습니다.
destination 헤더로 메세지 목적지를 지정합니다.
content-type 헤더로 바디 타입을 설정합니다.
SUBSCRIBE
SUBSCRIBE
id:0
destination:/queue/foo
ack:client
^@
SUBSCRIBE 커맨드로 메세지를 구독할 수 있습니다.
destination 헤더로 구독할 대상을 지정합니다.
ack 헤더는 메세지 승인 모드를 제어합니다.
HTTP에 비해 구현이 어렵습니다.
HTTP는 request가 오면 response만 해주면 끝나는 반면
웹소켓은 statefull 하기 때문에 서버와 클라이언트 간의 연결을 항상 유지해야합니다.
만약 갑작스럽게 연결이 끊기는 경우를 대비해 예외처리를 적절하게 해주어야 합니다.
리소스를 많이 소모합니다.
하나의 서버에 많은 클라이언트들이 연결을 계속 유지하고 있다면 서버에 부담이 점점 늘어납니다.
과도한 트래픽이 몰리면 장애가 발생할 확률이 올라갑니다.
HTML5 이전에서는 웹소켓을 지원하지 않습니다.
오래된 브라우저의 경우 웹소켓을 지원하지 않기도 합니다.
하지만 하위호환성을 지원하는 socket.io 등을 활용하면 단점을 극복할 수 있습니다.
https://www.rfc-editor.org/rfc/rfc6455
https://developer.mozilla.org/ko/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
https://stomp.github.io/stomp-specification-1.2.html