웹소켓

웹 소켓

웹 소켓을 사용하면 HTTP의 새로운 요청이나 중단 없이 데이터를 주고 받을 수 있다. 커넥션을 종료 시키지 않은 체 패킷 형태로 이루어지며 양방향 통신이 가능하다. 실시간으로 데이터를 교환하는 시스템이나 주식 트레이딩 시스템에 적합하다.

간단한 예시

1
let socket = new WebSocket("ws://javascript.info");

ws말고 wss:// 라는 프로토콜도 사용하는데 이는 HTTPHTTPS의 관계와 유사하다.

항상 wss://를 사용해라.
ws를 사용하면 데이터가 암호화 되지 않고 전달되는데 오래된 프락시 서버에 경우는 웹솟켓을 알지 못해서 이상한 헤더를 붙혀서 전달하는 일이 발생한다. wss를 사용하면 TLS라는 보안 계층을 통해서 데이터가 암호화 되어서 프락시에서 열어 볼 수 없고, 복호화는 받는 쪽에서 연결 하기 때문에 안전하게 연결할 수 있다.

다음 이벤트를 사용할 수 있다.

  1. open - 커넥션이 제대로 만들어졌을때 발생함.
  2. message - 데이터를 수신하였을때 발생함.
  3. error - 에러가 생겼을때 발생함.
  4. close - 커넥션이 종료되었을때 발생함.

커넥션이 만들어진 상태에서 무엇을 보내고 싶다면 socket.send(data)를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let socket = new WebSocket(
"wss://javascript.info/article/websocket/demo/hello"
);

socket.onopen = function (e) {
alert("[open] 커넥션이 만들어졌습니다.");
alert("데이터를 서버에 전송해봅시다.");
socket.send("My name is Bora");
};

socket.onmessage = function (event) {
alert(`[message] 서버로부터 전송받은 데이터: ${event.data}`);
};

socket.onclose = function (event) {
if (event.wasClean) {
alert(
`[close] 커넥션이 정상적으로 종료되었습니다(code=${event.code} reason=${event.reason})`
);
} else {
// 예시: 프로세스가 죽거나 네트워크에 장애가 있는 경우
// event.code가 1006이 됩니다.
alert("[close] 커넥션이 죽었습니다.");
}
};

socket.onerror = function (error) {
alert(`[error] ${error.message}`);
};

이벤트는 open -> message -> close 순서로 발생한다.

웹소켓 핸드셰이크

new WebSocket(url)를 호출해 소켓을 생성하면 즉시 연결이 시작된다.

브라우저는 서버에게 웹소켓을 지원하는지 묻고 지원한다는 응답이 오면 이제 HTTP가 아닌 WebSocket protocol로 통신한다.

요청 헤더 예시 :

1
2
3
4
5
6
7
GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
  • Origin - 클라이언트 오리진을 나타낸다. 서버는 Origin 헤더를 보고 웹사이트와 소켓통신을 할지 결정하기 때문에 Origin 헤더는 웹소켓 통신에 중요한 역할을 한다.
  • Connection: Upgrade - 클라이언트 측에서 프로토콜을 바꾸고 싶다고 알려줌.
  • Upgrade: websocket - 클라이언트 측에서 요청한 프로토콜은 websocket 이라는 것을 의미한다.
  • Sec-WebSocket-Key - 보안을 위해 브라우저에서 생성한 키를 나타낸다.
  • Sec-WebSocket-Vertion - 웹소켓 프로토콜 버전이 명시된다. 예시는 13버전

웹소켓 핸드셰이크는 모방이 불가능하다. (바닐라 자바스크립트로 헤더를 설정하는 건 기본적으로 막혀있다.)

응답 예시 :

1
2
3
4
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

Sec-WebSocket-AcceptSec-WebSocket-Key와 밀접한 관계가 있다. 브라우저는 이 헤더를 보고 특정한 알고리즘을 사용해 자신이 설정한 key인지 확인한다.

Extensions와 Subprotocols 헤더

기능확장과 서브 프로토콜로 데이터를 전달할때 Sec-WebSocket-ExtenstionsSec-WebSocket-Protocol 헤더를 지원한다.

  • Sec-WebSocket-Extensions : deflate-frame 이 헤더는 데이터 압축을 지원한다는 것을 의미함. 이 헤더는 브라우저에 의해 자동 생성되는데, 그 값엔 데잍터 전송과 관련된 무언가나 웹소켓 프로토콜 기능 확장과 관련된 무언가가 여러개 나열된다.
  • Sec-WebSocket-Protocol: soap, wamp - 이렇게 설정되면 평범한 헤더가 아닌 SOAP, WAMP 프로토콜을 준수하는 데이터를 전송하겠다는 의미이다.

이 헤더는 new WebSocket에 두번째 매겨변수에 값을 넣어서 설정할 수 있다.

1
let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);

이때 서버는 지원 가능한 익스텐션과 프로토콜을 응답 헤더에 담아 클라이언트에 전달해야 한다.

예시 :
클라이언트 :

1
2
3
4
5
6
7
8
9
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp

서버 :

1
2
3
4
5
6
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap

데이터 전송

웹소켓 통신은 프레임(frame)이라 불리는 데이터 조각을 사용해 이루어진다. 데이터 종류에 따라 다음과 같이 나뉜다.

  • 텍스트 프레임 (text frame) - 텍스트 데이터가 담긴 프레임
  • 이진 데이터 프레임 (binary data frame) - 이진 데이터가 담긴 프레임
  • 핑 또는 퐁 프레임 (ping/pong frame) - 커넥션이 유지되고 있는지 확인할 때 사용하는 프레임으로 서버나 브라우저에서 자동 생성해서 보내는 프레임
  • 이 외에도 커넥션 종료 프레임(connection close frame) 등 다양한 프레임이 있음.

브라우저에서 개발자는 텍스트나 이진 프레임만 다루게 된다. 이유는 socket.send(body) 에서 body 에 올수 있는 데이터가 string 과 blog, arraybuffer 같은 형태이기 때문이다.

받을 때는 텍스트 일경우 문자열로 들어온다. 이진데이터일 경우에는 선택할 수 있다.
이진 데이터를 받을 때는 socket.binaryType 프로퍼티를 사용하여 Blob 이나 ArrayBuffer 포맷 중 하나를 고룰 수 있다.

1
2
3
4
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
// event.data는 (텍스트인 경우) 문자열이거나 (이진 데이터인 경우) arraybuffer 입니다.
};

Rate limiting (속도 제한)

느린 데이터 환경에서 데이터를 전송하고 있다고 생각해 보자. 사용자는 계속해서 send 하겠지만 데이터는 버퍼링 되어 메모리에 저장되고 충분히 전달하기에 빠른 환경에 있을때 보내지게 될 것이다.

socket.bufferedAmount 프로퍼티는 보내고 있는 시점에서 남아있는 바이트나 버퍼를 저장하고 있다. 이 정보를 활용해서 버퍼에 데이터가 없을때 send 하는 로직을 구성할 수 있다.

1
2
3
4
5
6
7
// every 100ms examine the socket and send more data
// only if all the existing data was sent out
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(moreData());
}
}, 100);

Connection close

일반적으로 연결을 종료하고 싶을때 숫자 코드와 이유가 포함된 연결 종료 프레임을 보낸다.

1
socket.close([code], [reason]);
  • code : 정해진 숫자 코드 (optional)
  • reason : 이유를 설하는 문자열 (optional)

예시 :

1
2
3
4
5
6
7
8
9
// closing party:
socket.close(1000, "Work complete");

// the other party
socket.onclose = (event) => {
// event.code === 1000
// event.reason === "Work complete"
// event.wasClean === true (clean close)
};

많이 사용하는 코드는 다음과 같다.

  • 1000 : default, normal closure
  • 1006 : 브라우저 구현에 의해 연결이 비정상적으로 (로컬로) 닫혔음을 의미하는 특수 코드

다른 코드는 다음과 같다.

  • 1001 : 서버가 꺼지거나 브라우저에서 페이지를 떠났다.
  • 1009 : 메세지가 처리하기에 너무 크다.
  • 1011 : 서버에 알수 없는 에러

WebSocket Code 는 HTTP 코드와 비슷한듯 다르다. 특별히 1000 미만의 코드는 미리 예약되어 있으며 설정하려고 하면 에러를 발생시킨다.

1
2
3
4
5
6
// in case connection is broken
socket.onclose = (event) => {
// event.code === 1006
// event.reason === ""
// event.wasClean === false (no closing frame)
};

Connection state

socket.readyState 프로퍼티를 사용하여서 연결 상태를 얻을 수 있다.

  • 0 : “CONNECTiNG” : 연결 중 아직 연결된 것은 아님
  • 1 : “OPEN”: communicating
  • 2 : “CLOSING” : the connection is closing
  • 3 : “CLOSED” : the connection is closed.

Chat example

웹소켓을 사용하는 체팅 예제를 살펴보자. 클라이언트에 집중해서 볼 것이지만, 서버도 간단하게 구현 가능하다.

메세지를 담을 form 이 필요하고, 메세지를 표시할 div 가 필요하다.

1
2
3
4
5
6
7
8
<!-- message form -->
<form name="publish">
<input type="text" name="message" />
<input type="submit" value="Send" />
</form>

<!-- div with messages -->
<div id="messages"></div>

자바스크립트를 통해서 해야 할것 :

  1. 연결
  2. socket.send(message)
  3. 수신 메세지 div에 표시

구현 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws")
// send message from the form
document.forms.publish.onsubmit = function() => {
let outgoingMessage = this.message.value;
socket.send(outgoingMessage);
return false;
}

// message received - show the message in div
socket.onmessage = function(event) => {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
docuemnt.getElementById('messages').prepend(messageElem);
}