웹에서의 실시간 통신

H Kim·2022년 3월 22일
0

TIL

목록 보기
15/72
post-thumbnail

HTTP는 클라이언트가 보낸 요청에 대해 서버가 응답을 마치면 맺었던 연결을 끊어 버리는 비연결성의 성질을 가지고 있다. 이는 서버가 연결을 유지하기 위한 리소스를 줄이면 더 많은 연결을 할 수 있기 때문이다. 더욱 많은 유저와 연결되기 위해서는 클라이언트 하나와 지속적으로 연결되어 서버라는 자원을 소비하기보다는 단발성으로 최대한 많은 클라이언트의 요청을 처리하여 더 많은 사용자와 연결되는 것이 더욱 효율적인 선택이기 때문이다.

따라서 HTTP는 양방향 통신(full-duplex)이 아닌 request, response 형태의 단방향 통신(half-duplex) 모델이다. 이는 HTTP를 이용한다면 양방향 통신처럼 서로가 지속적으로 연결되어 있는 형태가 아니기 때문에 통신을 할때마다 많은 양의 메타데이터, 즉 Header를 보내야 한다는 것을 의미하게 된다. 그러나 우리는 여러 웹서비스에서 스트리밍 영상, 배달 현황 등 실시간으로 정보를 받아오는 서비스를 필연적으로 자주 이용하게 된다. 이 때에 Push 알람과 같이 실시간으로 변화하는 데이터를 받아오는 것을 위해 나오게 된 방법은 다음과 같다.


Polling


프로그램이나 장치에서 다른 프로그램이나 장치들이 어떤 상태에 있는지를 지속적으로 체크하는 전송제어 방식으로서 버스 카드 단말기, OS 시스템(키보드, 마우스와 같은 입력장치) 등에서 사용된다. 이는 클라이언트와 서버가 실시간 통신을 하는 것처럼 느끼기 위해 클라이언트가 일정 간격 동안 계속해서 요청을 보내는 방식을 뜻한다. 왜냐하면 HTTP 통신에서는 서버는 클라이언트에게 요청을 받기 전까지 절대 원하는 타이밍에 먼저 응답(push)을 줄 수 없기 때문에 이를 보완하기 위해 나온 방법이다.

그러나 polling 방식은 클라이언트가 일정 간격마다 요청을 보내기 때문에 실시간성을 보장한다고 보기 어렵다. 만약 실시간성을 지키기 위해 시간 간격을 줄이게 된다 하더라도 HTTP는 단발성 통신이기 때문에 부가적인 header가 많고 이를 매 요청과 응답마다 중복해서 보내기 때문에 서버에 부하가 가게 된다.


Long Polling


polling과 같이 무한히 서버에게 요청을 보내는 것은 동일하다. 그러나 long polling의 경우 일정 간격마다 요청을 보내지 않고 처음 요청을 보낸 뒤 서버에서 time-out 될 때까지 일정 시간을 기다린다. time-out 시간이 초과되기 전에 서버에서 데이터를 전송하게 되면 클라이언트는 응답을 받고 다시 요청을 보낸다. 만약 보낼 데이터가 없어 time-out이 되면 클라이언트는 다시 요청을 보낸다.

long polling 방식은 변경된 데이터가 있을 시에만 응답이 이루어지기 때문에 polling 방식보다 서버의 부하도 적으며 실시간성도 높다. 하지만 http의 단방향 메시지 교환 규칙을 변경하지 않고 구현했기에 데이터가 자주 바뀔 시(e.g. 여러 명의 유저가 많은 양의 채팅 전송) 여전히 헤더가 포함된 많은 양의 요청을 보내야 한다. 그래서 long polling 방식 역시 http 프로토콜의 한계를 보인다.


Web Socket


프로그램이 네트워크에서 데이터를 송수신 할 수 있도록, "네트워크 환경에서 클라이언트와 서버가 서로 연결할 수 있게 만들어진 연결부"를 의미한다.

웹소켓(WebSocket)은 http의 단방향 연결 방식이 아닌 양방향 연결을 위한 프로토콜을 HTTP 내에서 별도로 정의한 연결부이다. 웹소켓은 TCP 기반에서 작동하는 프로토콜로 http와 같이 OSI 7 계층에서 7번째 애플리케이션 레이어에 속한다. HTTP 포트 80(ws)과 443(wws) 위에 동작하도록 설계되었다. (ws는 http를 대체하고 비슷하게 wss는 https를 대체한다.) 웹소켓은 클라이언트에 의해 먼저 요청을 받는 방식이 아닌, 서버가 내용을 클라이언트에 보내는 방식을 표준화하여 제공하며 또 연결이 유지된 상태에서 메시지들을 오갈 수 있게 허용함으로써 실시간 소통을 가능하게 한다.

웹소켓 프로토콜(ws, wss)은 대부분의 브라우저(2020년 기준 90%의 브라우저)에서 지원하며 온라인 게임이나 주식 트레이딩 시스템같이 데이터 교환이 지속적으로 이뤄져야 하는 실시간 서비스에서 많이 사용한다. (보안, 신뢰성 측면에서 wss 프로토콜을 사용하는 것을 권장한다.)


WebSocket Handshake


웹소켓은 HTTP 프로토콜과 호환되기에 웹 소켓 핸드쉐이크 과정에서 HTTP upgrade 헤더를 사용하여 HTTP 내의 웹 소켓 프로토콜로 변경한다. 핸드쉐이크 과정은 브라우저가 "Upgrade: WebSocket" 헤더 등과 함께 랜덤하게 생성한 키를 서버에 보내면 웹 서버는 이 키를 바탕으로 토큰을 생성한 후 브라우저에 돌려주는 방식으로 작동한다.

  • 핸드쉐이크: 정상적인 통신이 시작되기 전에 양측 간에 조건에 합의해가는 정보 교환 과정
  • 핸드쉐이크 요청
    • HTTP 1.1로 요청을 하고, 웹 소켓 프로토콜로 Upgrade : websocket 해줄 것을 요청
    • 요청 헤더에는 소켓 버전과 소켓 비밀키 정보 등을 포함
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
  • 핸드쉐이크 응답
    • http 1.1에서 WebSocket으로 프로토콜 업그레이드가 성공되면 HTTP status 101 응답을 전송
    • 만약 연결이 정상적으로 이루어진다면 서버와 클라이언트 간에 WebSocket 연결이 이루어지고 일정 시간이 지나면 HTTP 연결은 자동으로 끊어짐

Socket.IO


브라우저와 서버 간의 실시간 양방향 이벤트 기반 통신을 가능하게 하는 JavaScript 라이브러리로서 클라이언트, 서버 두 부분으로 구성되어 있다. 자세히는 브라우저에서 실행되는 클라이언트 측 라이브러리와 Node.js 용 서버 측 라이브러리의 두 부분으로 구성된다. Socket.io는 WebSocket뿐만 아니라, WebSocket을 사용할 수 없는 브라우저에서는 HTTP Long Polling 기법을 사용하는 등, 여러 가지를 합쳐 추상화 시켜놓았기에 보다 쉽게 실시간 통신을 구현할 수 있다.

그렇기 때문에 WebSocket 프로토콜을 사용하지 않고도 Socket.IO에서 HTTP Long Polling과 같은 방식을 통해 실시간 통신을 할 수 있다. 이는 Socket.IO 전송 시에 WebSocket 핸드쉐이크를 위한 헤더 이외에도 부가적인 메타데이터가 추가됨을 의미한다.

WebSocket = Protocol, Socket.IO = LIbrary
WebSocket Client - WebSocket Server
Socket.IO Client - Socket.IO Server

따라서 WebSocket 클라이언트가 Socket.IO 서버에 연결할 수 없고, Socket.IO 클라이언트 역시 WebSocket 서버에 연결할 수 없다. Socket.IO로 통신을 하기 위해선 클라이언트와 서버 모두 Socket.IO를 사용해야 한다.

  • 안정성: 웹소켓 연결이 성립되지 않을 시 HTTP long polling 방식으로 대체됨.
  • 자동 재접속: 연결이 끊어질 시 자동으로 재연결 해줌.
  • 패킷 버퍼링: 기본적으로 소켓이 연결되지 않은 동안 발생하는 모든 이벤트는 다시 연결할 때까지 버퍼링 됨.
  • Acknowledgements: request-response의 형태로 API를 사용하고 싶을 때, emit()의 마지막 인수로 callback을 전달할 수 있음. 이 callback은 상대방이 이벤트를 승인하면 호출됨.
// server-side
io.on("connection", (socket) => {
  socket.on("update item", (arg1, arg2, callback) => {
    console.log(arg1); // 1
    console.log(arg2); // { name: "updated" }
    callback({
      status: "ok"
    });
  });
});

// client-side
socket.emit("update item", "1", { name: "updated" }, (response) => {
  console.log(response.status); // ok
});
  • Room: namespace의 하위 개념으로 모든 클라이언트 또는 클라이언트의 하위 집합에 브로드캐스트 할 수 있음. (e.g. 챗룸)
  • Namespace: Room에 접속하기 이전에 접속하는 것으로, Namespace 별로 구분하여 통신이 가능.
    • 소켓 연결 3단계: Socket → Namespace → Room
    • 먼저 Socket 단계에서 현재 접속한 Socket들을 구분할 수 있음.
    • Namespace는 그 Socket들이 접속한 Namespace로 Socket들을 구분할 수 있음.
    • 마지막으로 Room은 접속한 Socket들 중에, 같은 Namespace이며, 같은 Room인 Socket들과 통신할 수 있음.

0개의 댓글