↔️ WebSocket, 실시간 양방향 통신을 위한 프로토콜

최호빈·2024년 11월 15일
1
post-thumbnail

WebSocket이란?

우리가 일반적으로 웹 애플리케이션에서 서버와 HTTP 요청을 보내고 응답을 받고 나면 연결을 끊는데, HTTP의 비연결성 특성 때문이다. 이러한 특성은 채팅같이 실시간으로 데이터 통신이 필요한 경우에는 비효율적이다.

WebSocket이 나오기 전에는 양방향 통신이 필요한 애플리케이션에서는 HTTP를 사용해야 했다. 하지만 위에서 언급한 것처럼 HTTP는 실시간 데이터 통신에 비효율적이다. HTTP 방식으로 서버와 클라이언트가 양방향 통신을 한다면 서버는 각 클라이언트에 대해 여러 TCP 연결을 사용해야 한다. 하나는 클라이언트에 데이터를 보내기 위한 것이고, 또 다른 하나는 클라이언트의 새 요청마다 필요하다. 또한, 각 메시지에는 HTTP 헤더가 포함되어 많은 네트워크 오버헤드가 발생하고 클라이언트 측에서는 여러 연결의 응답을 관리하기 위해 매핑 정보를 가지고 있어야 한다.

HTTP는 원래 클라이언트-서버 간의 단방향 요청-응답 구조로 설계되었기 때문에, 양방향 통신을 하기 위해서는 클라이언트가 주기적으로 서버에 데이터를 요청하는 방법인 폴링(polling) 또는 롱 폴링(long polling)이라는 기법을 사용해야 했다.

폴링 (Polling) : 클라이언트가 일정 간격으로 서버에 요청을 보내는 방식

롱 폴링(long polling) : 서버가 새로운 데이터가 있을 때까지 클라이언트의 요청을 열어두는 방식

WebSocket프로토콜은 이러한 문제를 해결할 수 있는 실시간 통신을 위해 만들어진 프로토콜이다. 클라이언트와 서버가 한 번 연결되면 HTTP처럼 매번 요청을 열 필요 없이 그 연결을 유지하여 양방향 통신을 한다. 즉, 단일 TCP 연결을 통해 양방향 통신이 가능한 것이다.





WebSocket와 HTTP의 관계

WebSocket은 프록시, 필터링, 인증 같은 기존 HTTP 기반 인프라의 이점을 활용하면서 폴링, 롱 폴링같이 HTTP를 전송 계층으로 사용하는 기존 양방향 통신 기술을 대체하도록 설계되었다. 따라서 별도의 인프라를 구출할 필요가 없고 HTTP 핸드셰이크를 통해 연결을 시작하며, 일반 WebSocket 연결에는 포트 80을 사용하고 TLS를 통해 터널링되는 WebSocket 연결에는 포트 443을 사용한다.

하지만 트래픽 패턴이 주기적인 요청-응답 패턴을 따르는 기존 HTTP 트래픽과는 다르기 때문에 HTTP 기반 프록시나 방화벽 등 기존의 구성 요소에서 예상하지 않은 부하를 유발할 수 있다. 즉, HTTP와 달리 WebSocket은 클라이언트와 서버가 서로 자주 데이터를 보내는 패턴이기 때문에 HTTP 기반 인프라가 이를 잘 처리하지 못하거나 비정상적인 부하가 발생할 수 있다는 것이다. (Ex) HTTP 프록시가 주기적으로 연결을 끊는 방식으로 구성된 경우 WebSocket의 유지된 연결을 적절히 처리하지 못할 수 있음)





WebSocket 연결 및 동작 과정

WebSocket Opening Handshake

WebSocket은 기본적으로 HTTP 요청을 업그레이드하여 연결을 시작한다. 즉, 처음에 3 Way Handshake로 HTTP 통신을 위한 연결이 된 상태에서 클라이언트가 서버에게 업그레이드 요청을 보내면, 서버는 이를 받아들여 101 Switching Protocols 상태 코드를 응답하며 WebSocket 연결이 성립되는 것이다. 이 과정을 Opening Handshake라고 하며, Opening Handshake 과정에서 클라이언트와 서버가 주고받는 메시지 형식은 아래와 같다.

이는 HTTP 요청과 비슷한 방식인데, 몇가지 필수 사항들이 있다.

먼저, 핸드셰이크는 유효한 HTTP 요청이어야 한다.

요청 방법은 GET이어야 하며, HTTP 버전은 1.1 이상이어야 한다.

양방향 통신이 성공적으로 시작되기 위해 아래의 예시와 같은 특정 헤더가 필요하다. 클라이언트는 특정 헤더(Upgrade, Connection 등)을 통해 WebSocket 전환을 요청한다.

(더 자세한 필수 사항들은 [RFC6455] 4.1 참고)

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
  • ws://server.example.com/chat 로 접속하려고 한 경우의 예시
  • GET /chat HTTP/1.1 을 보면 HTTP 요청 Request-Line 형식을 따르는 것을 볼 수 있다.
  • 헤더 설명
    • Upgrade : 프로토콜을 전환하기 위해 사용하는 헤더. 웹소켓 요청시에는 websocket 이라는 값을 가지며, 만약 아니라면 cross-protocol attack 이라고 간주하여 연결이 중지된다.
    • Connection : 현재의 전송이 완료된 후 네트워크 접속을 유지할 것인지에 대한 정보. 웹 소켓 요청 시에는 반드시 Upgrade 라는 값을 가진다. 만약 아니라면 연결이 중지된다.
    • Sec-WebSocket-Key : 클라이언트가 서버에 전송하는 무작위 값으로, 서버는 XMLHttpRequest 공격 등을 방지하기 위해 이를 전역 고유 식별자, GUID와 결합해 서버의 핸드셰이크에 반환함으로써 WebSocket 연결을 인증한다.
    • Sec-WebSocket-Protocol : 클라이언트와 서버 간의 하위 프로토콜을 정의하는 데 사용된다.

서버는 요청이 유효한 경우, 101 Switching Protocols 상태 코드로 응답하여 WebSocket 연결을 승인한다. (아래 예시 참고)

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
  • HTTP/1.1 101 Switching Protocols 을 보면 HTTP 응답 Status-Line 형식을 따르는 것을 볼 수 있다.
  • Sec-WebSocket-Accept : 요청 헤더의 Sec-WebSocket-Key에 GUID를 더해서 SHA-1로 해싱한 후 base64로 인코딩한 결과이다. 이는 서버가 클라이언트의 WebSocket Handshake를 수신했음을 클라이언트에 증명하는 것이고 웹소켓이 정상적으로 연결되었음을 의미한다.



WebSocket Data Transfer

클라이언트와 서버가 모두 handshake를 보내고 handshake가 성공하면 양방향 데이터 전송 부분인 data transfer 가 시작된다. 이제 클라이언트와 서버가 독립적으로 데이터를 전송할 수 있게 된다. 이 데이터는 메시지 단위로 전송되며, 메시지는 하나 이상의 프레임으로 구성된다. 각 프레임에는 특정 데이터 유형이 설정되어 있으며, 동일한 메시지에 속한 모든 프레임은 모두 동일한 데이터 유형을 가진다.

기본 Frame 구조

  • FIN (1비트): 메시지의 마지막 프레임임을 나타낸다.
  • RSV (각 1비트, RSV1, RSV2, RSV3): 확장용으로 예약된 비트이며, 기본적으로 0으로 설정되어 있다.
  • Opcode (4비트): 프레임의 데이터 유형(Payload data의 해석)을 지정한다. 알 수 없는 opcode이 수신되면 WebSocket 연결은 실패한다. (Ex) 0x1은 텍스트 데이터, 0x2는 바이너리 데이터, 0x8은 연결 종료, 0x9는 핑(ping), 0xA는 퐁(pong))
  • Mask (1비트): Payload data가 마스크 되는지의 여부를 정의한다. 클라이언트에서 서버로 전송되는 모든 프레임에는 마스킹이 되어야 한다. 즉, 이 Mask 필드가 1로 설정되어야 한다.
    • WebSocket에서 모든 클라이언트에서 전송되는 프레임은 32비트의 무작위 마스킹 키를 사용해 데이터가 마스킹된다. 이 마스킹 과정은 네트워크 상에서 발생할 수 있는 보안 문제(Ex) 데이터 가로채기)를 방지하기 위해 수행되며, 서버가 마스킹되지 않은 프레임을 수신할 경우 오류 코드(1002)를 반환하고 연결을 종료한다.
  • Payload 길이 (7비트, 7+16비트 또는 7+64비트): 0-125이면 페이로드 길이이고, 126인 경우 16비트 부호 없는 정수로 해석되는 다음 2바이트가 페이로드 길이이다. 127인 경우 64비트 부호 없는 정수(가장 중요한 비트는 0이어야 함)로 해석되는 다음 8바이트가 페이로드 길이이다.
  • 마스킹 키 (0 또는 4바이트): 클라이언트에서 서버로 전송되는 프레임에 포함되며, 데이터 마스킹에 사용된다. 마스크 비트가 1로 설정되면 존재하고 마스크 비트가 0으로 설정되면 존재하지 않는다.
  • Payload 데이터: 실제 전송 데이터로, 확장 데이터와 애플리케이션 데이터로 구성된다.

제어 프레임 (Control Frames)

WebSocket에서 연결 상태를 전달하는데 사용된다. opcode의 최상위 비트가 1인 opcode로 식별되며 조각난 메시지 중간에 삽입될 수 있다. 모든 제어 프레임의 페이로드 길이는 125바이트 이하여야 하며 제어 프레임이 조각화되어서는 안된다.

대표적으로 아래와 같은 제어 프레임이 있다.

  • Close (0x8): 연결을 종료하는 데 사용되며, 선택적으로 종료 이유를 포함할 수 있다.
  • Ping (0x9): 연결이 살아 있는지 확인하는 용도이며, 이를 받으면 상대방은 Pong 프레임(0xA)으로 응답해야 한다.
  • Pong (0xA): Ping 프레임에 대한 응답으로 사용된다. Ping 프레임의 애플리케이션 데이터와 동일한 데이터를 포함해야 하며, 요청 없이도 보낼 수 있다.

데이터 프레임 (Data Frames)

WebSocket을 통해 애플리케이션 계층 데이터를 전송하는 데 사용된다. opcode의 최상위 비트가 0인 opcode로 식별되며 텍스트 데이터(0x1) 또는 바이너리 데이터(0x2)가 포함된다.

대표적으로 아래와 같은 데이터 프레임이 있다.

  • 텍스트 프레임 (0x1) : UTF-8로 인코딩된 텍스트 데이터를 포함한다. 프레임이 여러 조각으로 나뉠 수 있으며, 모든 조각을 합친 메시지는 유효한 UTF-8이어야 한다.
  • 바이너리 프레임 (0x2) : 바이너리 데이터가 포함되며, 해석은 애플리케이션에 따라 다르다.



WebSocket Closing Handshake

이제 WebSocket 연결을 종료하고 싶을 때 Closing Handshake 과정을 통해 통신을 안전하게 종료할 수 있다. 클라이언트와 서버(피어라고도 부른다.) 중 연결을 종료하려는 피어는 제어 프레임을 보내 종료 의사를 표시한다. 이 프레임을 받은 다른 피어는, 자신이 아직 종료 프레임을 보내지 않았다면, 응답으로 Close 프레임을 보낸다. Close 프레임이 서로 교환되면, 각 피어는 더 이상 데이터를 보내지 않고 TCP 연결을 종료하게 된다. 이때, Close 프레임을 보낸 후 서로 더 이상 데이터 프레임을 보내면 안된다.

각 피어가 상대방의 Close 프레임을 수신하면 연결을 종료하는 방식이기 때문에 클라이언트와 서버는 동시에 닫는 핸드셰이크를 시작해도 안전하다.

Closing Handshake 과정은 2가지의 장점이 있다.

  1. TCP 연결 종료(FIN/ACK)를 보완해준다.

    클라이언트와 서버 사이에 프록시 서버같이 중개자가 있는 경우 TCP 닫기 신호(FIN/ACK)가 종단 간(end-to-end)으로 전달되지 못할 수 있다. 따라서 WebSocket 자체에서 Close 프레임을 사용해 명확하게 종료 상태를 전달해준다.

  2. 데이터 손실을 방지한다.

    위에서 말했듯이, 각 피어가 상대방의 Close 프레임을 수신하면 연결을 종료하는 방식이기 때문에 남아있는 데이터를 확실히 수신한 후 연결을 닫아 데이터 손실을 방지할 수 있다.





WebSocket과 Socket의 차이

Socket이란?

네트워크 상에서 두 컴퓨터 간에 데이터를 전송하기 위한 종단점이다. IP 주소와 포트 번호를 통해 데이터를 주고받을 수 있는 통로라고 생각할 수 있다. TCP/IP, UDP/IP Socket이 존재한다.

WebSocket과 Socket은 무엇이 다를까

우선 동작 계층의 차이가 있다. Socket은 TCP, UDP가 속한 4계층에 위치하며 WebSocket은 HTTP에 기반하므로 7계층에 위치한다. 그리고 Socket은 네트워크 통신을 위한 종단점이고 WebSocket은 브라우저와 서버 간의 실시간 양방향 통신 프로토콜이므로 개념 자체가 다르다.

내가 사용했던 Socket.io Socket이 아닌 WebSocket이었다…😇

IPC를 공부하면서 Socket에 대해 다뤘었는데, 그 예시로 아래 코드를 가져왔었다.

import { createServer } from 'http';
import { Server } from 'socket.io';
const server = createServer(app);
const io = new Server(server);

// 소켓 연결 처리
io.on('connection', (socket) => {
  console.log('클라이언트가 연결되었습니다.');
  // 메시지 수신 및 브로드캐스트
  socket.on('chat message', (msg) => {
    io.emit('chat message', msg);
  });
  // 연결 종료 처리
  socket.on('disconnect', () => {
    console.log('클라이언트 연결이 종료되었습니다.');
  });
});

// 서버 시작
const PORT = 3000;
server.listen(PORT, () => {
  console.log(`서버가 http://localhost:${PORT} 에서 실행 중입니다.`);
});

하지만 내가 사용한 Socket.io 라이브러리는 양방향 통신을 하기 위해 WebSocket 기술을 활용하는 Node.js 라이브러리로, Socket이 아니었다.

조금만 더 찾아봤으면 둘의 차이점을 알고 헷갈리지 않았을텐데,,, 그래도 지금이라도 알았으니 다행이다! 😅





WebSocket과 HTTP Keep-Alive 구분하기

HTTP Keep-Alive란?

HTTP 요청 간 연결 유지를 위해 HTTP 요청/응답 간 연결을 재사용하는 것이다.(지속적 연결 방식이라고도 부름)송신자가 연결에 대한 타임아웃과 요청 최대 개수를 어떻게 정했는지에 대해 알려준다.

클라이언트와 서버가 연결을 유지한 상태라는 것은 WebSocket과 같은데, 그럼 꼭 WebSocket을 써야할까? HTTP를 사용하면서 Connection: keep-alive 헤더를 설정해주면 되는 것 아닌가? HTTP/1.1은 keep-alive 헤더가 기본으로 설정되어 있으니 말이다. 🧐

keep-alive 헤더의 주요 목적은 각 HTTP 요청에 대해 TCP 연결을 열 필요가 없도록 하는 것이다. 즉, 연결이 유지되는 동안은 3-way Handshake가 발생하지 않아 그에 따른 오버헤드가 발생하지 않는다. 이때 클라이언트와 서버 간의 통신을 위한 프로토콜은 여전히 기본 HTTP 요청/응답 패턴을 따르므로 서버 측은 원할 때 클라이언트에게 데이터를 보낼 수 없다.
하지만, WebSocket은 양방향 전송 프로토콜이므로 클라이언트와 서버 모두 원할 때 데이터를 주고받을 수 있다. keep-alive 는 HTTP에 종속적인 기술이므로 HTTP의 한계를 벗어날 수 없고 WebSocket을 대체할 수 없다. 이 둘은 완전히 다른 메커니즘이다.


+) Keep-Alive에 따르면, 현재는 Deprecated된 것을 볼 수 있다.
Keep-Alive는 HTTP/2에서 무시되며, 실제 프로덕션 환경에서 주의가 필요하다는 경고를 하고 있다.

+) 추가로 읽어보면 좋은 글
https://sabarada.tistory.com/262
https://velog.io/@kyle-log/HTTP-keep-alive




참고 자료

RFC 6455: The WebSocket Protocol

RFC 6455 한글 번역 문서

WebSocket이란? 개념과 동작 과정 (+socket.io, Polling, Streaming...)

Difference HTTP Keep Alive & WebSocket

Keep-Alive - HTTP | MDN

0개의 댓글