
우리가 일반적으로 웹 애플리케이션에서 서버와 HTTP 요청을 보내고 응답을 받고 나면 연결을 끊는데, HTTP의 비연결성 특성 때문이다. 이러한 특성은 채팅같이 실시간으로 데이터 통신이 필요한 경우에는 비효율적이다.
WebSocket이 나오기 전에는 양방향 통신이 필요한 애플리케이션에서는 HTTP를 사용해야 했다. 하지만 위에서 언급한 것처럼 HTTP는 실시간 데이터 통신에 비효율적이다. HTTP 방식으로 서버와 클라이언트가 양방향 통신을 한다면 서버는 각 클라이언트에 대해 여러 TCP 연결을 사용해야 한다. 하나는 클라이언트에 데이터를 보내기 위한 것이고, 또 다른 하나는 클라이언트의 새 요청마다 필요하다. 또한, 각 메시지에는 HTTP 헤더가 포함되어 많은 네트워크 오버헤드가 발생하고 클라이언트 측에서는 여러 연결의 응답을 관리하기 위해 매핑 정보를 가지고 있어야 한다.
HTTP는 원래 클라이언트-서버 간의 단방향 요청-응답 구조로 설계되었기 때문에, 양방향 통신을 하기 위해서는 클라이언트가 주기적으로 서버에 데이터를 요청하는 방법인 폴링(polling) 또는 롱 폴링(long polling)이라는 기법을 사용해야 했다.


WebSocket프로토콜은 이러한 문제를 해결할 수 있는 실시간 통신을 위해 만들어진 프로토콜이다. 클라이언트와 서버가 한 번 연결되면 HTTP처럼 매번 요청을 열 필요 없이 그 연결을 유지하여 양방향 통신을 한다. 즉, 단일 TCP 연결을 통해 양방향 통신이 가능한 것이다.
WebSocket은 프록시, 필터링, 인증 같은 기존 HTTP 기반 인프라의 이점을 활용하면서 폴링, 롱 폴링같이 HTTP를 전송 계층으로 사용하는 기존 양방향 통신 기술을 대체하도록 설계되었다. 따라서 별도의 인프라를 구출할 필요가 없고 HTTP 핸드셰이크를 통해 연결을 시작하며, 일반 WebSocket 연결에는 포트 80을 사용하고 TLS를 통해 터널링되는 WebSocket 연결에는 포트 443을 사용한다.
하지만 트래픽 패턴이 주기적인 요청-응답 패턴을 따르는 기존 HTTP 트래픽과는 다르기 때문에 HTTP 기반 프록시나 방화벽 등 기존의 구성 요소에서 예상하지 않은 부하를 유발할 수 있다. 즉, HTTP와 달리 WebSocket은 클라이언트와 서버가 서로 자주 데이터를 보내는 패턴이기 때문에 HTTP 기반 인프라가 이를 잘 처리하지 못하거나 비정상적인 부하가 발생할 수 있다는 것이다. (Ex) HTTP 프록시가 주기적으로 연결을 끊는 방식으로 구성된 경우 WebSocket의 유지된 연결을 적절히 처리하지 못할 수 있음)

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를 수신했음을 클라이언트에 증명하는 것이고 웹소켓이 정상적으로 연결되었음을 의미한다.클라이언트와 서버가 모두 handshake를 보내고 handshake가 성공하면 양방향 데이터 전송 부분인 data transfer 가 시작된다. 이제 클라이언트와 서버가 독립적으로 데이터를 전송할 수 있게 된다. 이 데이터는 메시지 단위로 전송되며, 메시지는 하나 이상의 프레임으로 구성된다. 각 프레임에는 특정 데이터 유형이 설정되어 있으며, 동일한 메시지에 속한 모든 프레임은 모두 동일한 데이터 유형을 가진다.

0x1은 텍스트 데이터, 0x2는 바이너리 데이터, 0x8은 연결 종료, 0x9는 핑(ping), 0xA는 퐁(pong))제어 프레임 (Control Frames)
WebSocket에서 연결 상태를 전달하는데 사용된다. opcode의 최상위 비트가 1인 opcode로 식별되며 조각난 메시지 중간에 삽입될 수 있다. 모든 제어 프레임의 페이로드 길이는 125바이트 이하여야 하며 제어 프레임이 조각화되어서는 안된다.
대표적으로 아래와 같은 제어 프레임이 있다.
데이터 프레임 (Data Frames)
WebSocket을 통해 애플리케이션 계층 데이터를 전송하는 데 사용된다. opcode의 최상위 비트가 0인 opcode로 식별되며 텍스트 데이터(0x1) 또는 바이너리 데이터(0x2)가 포함된다.
대표적으로 아래와 같은 데이터 프레임이 있다.
이제 WebSocket 연결을 종료하고 싶을 때 Closing Handshake 과정을 통해 통신을 안전하게 종료할 수 있다. 클라이언트와 서버(피어라고도 부른다.) 중 연결을 종료하려는 피어는 제어 프레임을 보내 종료 의사를 표시한다. 이 프레임을 받은 다른 피어는, 자신이 아직 종료 프레임을 보내지 않았다면, 응답으로 Close 프레임을 보낸다. Close 프레임이 서로 교환되면, 각 피어는 더 이상 데이터를 보내지 않고 TCP 연결을 종료하게 된다. 이때, Close 프레임을 보낸 후 서로 더 이상 데이터 프레임을 보내면 안된다.
각 피어가 상대방의 Close 프레임을 수신하면 연결을 종료하는 방식이기 때문에 클라이언트와 서버는 동시에 닫는 핸드셰이크를 시작해도 안전하다.
Closing Handshake 과정은 2가지의 장점이 있다.
TCP 연결 종료(FIN/ACK)를 보완해준다.
클라이언트와 서버 사이에 프록시 서버같이 중개자가 있는 경우 TCP 닫기 신호(FIN/ACK)가 종단 간(end-to-end)으로 전달되지 못할 수 있다. 따라서 WebSocket 자체에서 Close 프레임을 사용해 명확하게 종료 상태를 전달해준다.
데이터 손실을 방지한다.
위에서 말했듯이, 각 피어가 상대방의 Close 프레임을 수신하면 연결을 종료하는 방식이기 때문에 남아있는 데이터를 확실히 수신한 후 연결을 닫아 데이터 손실을 방지할 수 있다.
네트워크 상에서 두 컴퓨터 간에 데이터를 전송하기 위한 종단점이다. IP 주소와 포트 번호를 통해 데이터를 주고받을 수 있는 통로라고 생각할 수 있다. TCP/IP, UDP/IP 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이 아니었다.
조금만 더 찾아봤으면 둘의 차이점을 알고 헷갈리지 않았을텐데,,, 그래도 지금이라도 알았으니 다행이다! 😅
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
WebSocket이란? 개념과 동작 과정 (+socket.io, Polling, Streaming...)