공부/컴퓨터 네트워크

[Network] WebSocket

70825 2024. 6. 14. 16:22
반응형

 

 

 

 

1. 웹소켓에 대한 간단한 정보


인터넷에 널리 알려진 내용부터 잠깐 살펴보고 가자. 먼저 웹소켓이 없었을 때부터 이야기할 예정이라 웹소켓만의 특징을 먼저 알고 있는 상태에서 읽는게 도움이 된다.

 

- HTTP와 같은 Application 계층의 프로토콜이다.

- 데이터를 양방향으로 주고 받을 수 있다. 그래서 서버가 먼저 데이터를 보낼 수 있다.

- HTTP / HTTPS에서 Upgrade, Connection 헤더를 사용해 웹소켓을 사용할 수 있다.

 

이번 포스팅은 소켓에 대해 알고 있어어한다. 아래 글을 보고 먼저 소켓에 대해 이해를 한 다음 글을 보는게 좋다.

https://hello70825.tistory.com/598

 

[Network, OS] Socket, I/O Multiplexing, Event Loop

Socket → I/O Multiplexing → Event Loop 순서로 공부한 내용이다.따로따로 내용을 분리하기엔 서로 연결된 내용이 많아서 한 번에 작성했다.   0. UNIX 철학Everything is a file 객체지향에서 "모든 것은 객

hello70825.tistory.com


 

 

 

 

2. HTTP, AJAX, WebSocket 역사


[1] HTTP 이전

 

HTTP가 나오기 이전에는 터미널을 통해서 텍스트를 주고 받았다. 위 사이트는 telnetbbsguide로 가장 위에 있는 사이트를 telnet으로 접속한 사진이다. Telnet도 HTTP와 같은 Application 계층 프로토콜인데, 커맨드 명령어(0xff)를 보내는게 아니면 기본적으로 텍스트만 주고 받는다. 만약 내가 "Hello, World!"를 보내게 된다면 HTTP는 헤더 정보를 같이 보내지만, Telnet은 "Hello, World!"를 UTF-8로 인코딩해서 보내는게 전부이다.


[2] HTML과 HTTP/0.9

HTML 등장 이전엔 SGML이라는 기계도 쉽게 읽고, 인간도 쉽게 읽는 표준화된 마크업 언어가 있었다. 원래 HTML 같은 마크업 언어가 안나왔으면 수십년간 SGML을 사용할 예정이었다. 하지만 SGML을 작성하려면 Document Type Definition이라고 문서 유형을 하나 만들 때마다 유형을 정의해야하는데, 이게 엄밀하게 작성해야하고, 작성하려면 알아야할 내용이 많아서 학습 곡선이 굉장히 높았다. 더군다나 이걸 웹 서버에 보여주려면 서버를 만든 사람이 처리를 해줘야하는데, 역시 이것도 너무 복잡하다는 이야기가 나왔다. 이런 단점으로 인해 더 간단하게 표준화하려는 시도가 있었지만, 결국 HTML이 나오고 나서는 HTML이 완전히 자리잡게 되었다. 그리고 처음 나온 HTTP/0.9는 헤더도 없고, 상태 코드도 없고, GET 요청만 있는 상황에서 HTML 파일만 전송할 수 있게 만들어졌다. 여기서부터 HTTP가 발전하기 시작했다.

 

https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP


[3] HTTP와 AJAX

HTTP 프로토콜의 규칙은 클라이언트는 원하는 URL을 요청하면, 서버는 해당 URL 페이지를 보내주는 것이다. 이게 HTTP의 대전제이다. 우리가 네트워크 공부하면 나오는 3-way handshake도 요청과 응답으로 이루어져 있으니 요청-응답 구조를 모르는 사람은 없을 것이다. 근데 이거 때문에 기술적으로 불편한 점이 생겼다.

 

https://prod.danawa.com/list/?cate=112758

 

위 사진은 노트북을 살 때 사람들이 많이 보는 다나와 사이트인데, 노트북을 구매한다면 위와 같이 여러 조건을 선택해서 내가 원하는 제품을 선택적으로 볼 수 있었다. 내가 특정 조건을 선택하면 사진 아래에 있는 노트북 목록만 바뀔 뿐이다. 하지만 이게 HTTP만 그대로 사용하게 되면 노트북 목록만 바뀌는게 아니라 전체 HTML 코드를 새로 받아야 했다. 그래서 서버든 클라이언트든 컴퓨터 리소스 낭비가 심하고, 클라이언트가 서버에게 무언가를 요청하려면 무조건 페이지를 이동해야했다. 그래서 만약 HTTP 초기에 다나와 사이트를 만든다고 가정하면 제조사만 자유롭게 선택하더라도 720개의 URL과 거기에 맞는 HTML 코드를 만들어야했다.

 

과거에는 저 노트북 목록만 따로 보여주려면 iframe 태그를 사용해서 노트북 목록에만 구멍을 뚫고, URL을 따로 만들어서 보여준다거나, 사이트 자체를 플래시로 만들거나, 우리가 잘 알고 있는 ActiveX도 이런 웹 개발 기술 한계를 돌파할 수 있는 기술이었다. 그래서 이것도 HTML이 나오기 전까지 여러가지 기술이 혼합된 시대가 열렸지만, 구글에서 AJAX를 발표하면서 일단락되었다.

 

자바스크립트에는 XMLHttpRequest API가 있었는데, AJAX는 이걸 활용해 페이지 일부를 동적으로 업데이트할 수 있게 되었다. 그래서 AJAX가 나오고 나서부터는 iframe처럼 구멍을 뚫거나, 플래시로 사이트를 만들거나, 사용자들에게 ActiveX 설치를 강요할 필요가 없어졌다. 이것으로 인해 웹 프로그래밍이 굉장히 많이 발전했지만, 아직도 HTTP로 인해 클라이언트가 요청해야 서버가 응답하는 방식은 그대로였다.


[4] HTTP Polling, HTTP Streaming

웹소켓 활용처라고하면 대표적으로 채팅방 기능이 떠오른다. 웹소켓이 없었을 때에도 채팅방이 물론 존재했는데, 이때의 자세한 작동방식은 크게 HTTP 폴링과 HTTP 스트리밍으로 나뉜다. 여기서도 HTTP 대전제의 '클라이언트가 요청해야 서버가 응답할 수 있다'를 기억하고 아래 내용을 보자

 

https://www.slideshare.net/slideshow/111015-html5-1/9726213

위 사진을 보면 철수와 영희가 채팅방에 들어왔는데, 서버에서 "영희님도 접속했습니다."라는 문구를 철수에게 알려줘야 하는 상황이 발생한다. 이후 철수가 채팅방에 "앗 영희야 안녕~"을 보내면 서버는 영희에게 "앗 영희야 안녕~"을 보내줘야하는데, HTTP 대전제와 맞지 않게 응답에 필요한 요청이 없는 상황이다. 사용자의 입장에서는 이렇게 보이지만, 기술적으로는 크게 두 가지 방법으로 문제를 해결했었다.

 

- HTTP Polling / HTTP Long Polling

첫번째 방법은 클라이언트가 주기적으로 요청을 보내는 방식이다. "영희님도 접속했습니다"로 응답을 보내기 전에 철수 클라이언트에서 먼저 서버에게 데이터가 들어왔는지 요청을 하게 된다. 이로 인한 문제점은 너무 쓸모 없는 요청이 많아지게 되고, 서버의 부담이 커지게 된다는 점이다. 이것보다 발전된게 HTTP Long Polling이다. 서버에서 바로 응답을 하지 않고 연결을 계속 유지하고 있다가 전달할 내용이 있어야 응답을 보내는 방식이다. Polling 방식보다는 많은 요청을 받지 않아도 되지만, 이번엔 커넥션을 계속 열어둬야 한다는 문제점이 존재한다. 참고로 당근마켓의 채팅서버가 원래 Polling 방식으로 되어 있다가 웹소켓으로 바꿨다.

 

 

- HTTP Streaming (Comet)

Server Sent Event (SSE) 방식이다. 처음엔 TCP 3-way handshake를 한 뒤에 클라이언트가 요청을 보낼 때, Accept: text/event-stream과 함께 보내면 서버에서는 Content-Type: text/event-stream으로 HTTP를 보내게 된다. 단점이라면 위 사진처럼 단방향으로 데이터 통신이 진행되기 때문에 2개의 커넥션이 필요하다. 1개는 클라이언트가 서버로 보내는 커넥션이고, 나머지 하나는 SSE 방식으로 데이터를 받을 커넥션이다. 추가적으로 UDP는 사용하지 않는 이유는 소켓과 관련이 있다. TCP에서 사용하는 Stream Socket은 요청이 들어오면 새로운 소켓이 만들어져서 클라이언트와 새로운 소켓이 1대1로 매칭되어 데이터를 주고 받기 때문에 소켓을 닫지 않으면 연결이 유지된다. 하지만 UDP가 사용하는 Datagram Socket은 1개의 소켓으로 데이터가 오는 족족 처리하기 때문에 연결이라는 개념이 존재하지 않기 때문이다. 거기다가 데이터 순서도 뒤죽박죽으로 올 수 있는 위험성도 존재한다. 이 이유는 웹소켓이 TCP 연결만 사용하는 이유와 똑같다.  HTTP Streaming은 여러 이름으로 불리는데 Comet, SSE, Reverse Ajax 다 똑같은 기술을 말하는 것이다. RFC 문서에서는 HTTP Streaming으로 명시해놨다.

 

이를 통해 HTTP Polling과 HTTP Streaming으로 문제를 어느정도 해결했지만, 근본적으로 '하나의 연결로 클라이언트와 서버가 서로 마음대로 데이터를 보낼 수 있는 구조'는 해결되지 않았다. 이전 내용을 살펴보면 무엇을 하든 HTTP 대전제는 지켜야했기 때문에 아예 HTTP 방식을 벗어난 방법을 고안하게 되었고, 그래서 나온 것이 WebSocket이다.


 

 

 

3. WebSocket


[1] 웹소켓이 만들어진 이유

웹소켓은 의외로 최신 기술에 속한다. 2010년에 구현된 기술이고, 2011년에 RFC 6455 - WebSocket 문서로 표준화가 되었다. 그리고 RFC 6455 문서를 보면 WebSocket이 위의 HTTP 문제를 해결한 기술이라는 것을 강조하기 위해 문서 시작부터 아래 내용이 나와있다.

과거에는 클라이언트와 서버 간에 양방향 통신이 필요한 웹 애플리케이션을 만들기 위해 HTTP를 남용하여 서버에 업데이트를 폴링하면서 별도의 HTTP 호출로 업스트림 알림을 보내야했다.

이로 인해 여러가지 문제가 발생한다.
- 서버는 각 클라이언트에 대해 여러 개의 다른 기본 TCP 커넥션을 사용해야한다.
- 클라이언트-서버 간 메시지에 HTTP 헤더가 있어 오버헤드가 높다.
- 클라이언트는 응답을 얻기 위해 커넥션을 유지해야한다. (서버도 마찬가지)

더 간단한 해결책은 양방향 트래픽을 위해 단일 TCP 연결을 사용하는 것이다. 이것이 WebSocket 프로토콜이 제공하는 것이다.

...

WebSocket 프로토콜은 기존 인프라(프록시, 필터링, 인증)의 이점을 활용하기 위해 HTTP를 전송 계층으로 사용하는 기존의 양방향 통신 기술을 대체하도록 설계되었다. 이 기술은 효율성과 신뢰성 사이의 절충안으로 구현된 것으로, HTTP는 원래 양방향 통신을 위해 설계된 것이 아니기 때문이다. WebSocket 프로토콜은 기존 HTTP 인프라 내에서 기존 양방향 HTTP 기술의 목표를 해결하려고 시도한다. 따라서 HTTP 포트 번호인 80번 포트와 443 포트에서 작동하며, HTTP 프록시와 중간 장치를 지원하도록 설계됐다.

...

이 설계는 WebSocket을 HTTP로 제한하지 않고, 향후 구현에서 전체 프로토콜을 다시 설계하지 않고도 전용 포트에서 더 간단한 핸드셰이크를 사용할 수 있다. 이 점이 중요한 이유는 대화형 메시징의 트래픽 패턴이 표준 HTTP 트래픽과 일치하지 않으며 일부 요소에서 비정상적인 부하를 유발할 수 있기 때문이다.

 

위의 대화형 메시징이라는 단어는 주식/코인 거래, 실시간 게임과 같이 실시간으로 짧은 시간에 많은 데이터가 오가는 것을 뜻한다. 여기서 HTTP를 사용하면 요청 → 응답 → 요청 → 응답 → 요청 → ... 이 빠른 시간안에 일어나고, 요청과 응답은 헤더 + 데이터로 이루어진 패킷이기 때문에 CPU 작업을 많이 잡아먹게 된다.

 

정리하면 웹소켓은 다음과 같이 요약할 수 있다. 

- 기존엔 양방향 통신을 만든다면 HTTP 설계 의도와 맞지 않게 사용해야한다. 그래서 양방향 통신 프로토콜을 따로 만들게 됐다.

- 양방향 통신 설계의 간단한 방법은 단일 TCP 연결을 사용하는 것이다.

- HTTP로 만들어진 프록시, 필터링, 인증과 같은 부가 기능을 활용하기 위해 웹소켓은 HTTP, HTTPS에서 핸드셰이킹을 진행한다.

 

이제부터 어떤 간단한 hanshake를 통해서 HTTP에서 WebSocket으로 변경되는 것인지 알아보자


[2] Opening Handshake, Closing Handshake

TCP 연결을 하면 연결을 열 때, 3-way handshake를 사용한다. 하지만 웹소켓은 "지금 HTTP 통신을 WebSocket으로 업그레이드하고 싶어요"라고 보내면, 서버에서 "네~ HTTP 통신을 WebSocket으로 변경했습니다~"라는 응답을 보내면 끝이다.

 

https://datatracker.ietf.org/doc/html/rfc6455#section-1.2

먼저 클라이언트가 서버에게 웹소켓으로 업그레이드를 요청한다.

 

- Upgrade : 이미 설정된 연결을 다른 프로토콜로 업그레이드하는데 사용한다. (ex. http → websocket, http/1.1 → http/2.0)

- Connection : 현재 패킷을 전송한 후에 네트워크 연결을 계속 열려있게할지 여부를 제어한다. 

- Sec-WebSocket-Key : 서버가 업그레이드 패킷을 정상적으로 수신했음을 확인하는 용도

- Sec-WebSocket-Protocol : 여러 웹소켓 하위 프로토콜중에 클라이언트는 어떤 것을 우선순위로 사용하면 좋겠는지 명시한다.

- Sec-WebSocket-Version : 웹소켓 버전을 명시한다. 무조건 값이 13으로 고정되어 있다.

 

이외에도 Sec-WebSocket-Extension 헤더 필드가 있다.

- Sec-WebSocket-Extension: 웹소켓 프로토콜 확장자를 지원한다. (ex. 압축 알고리즘 적용 - RFC 7692 참조)

https://datatracker.ietf.org/doc/html/rfc6455#section-1.2

서버에서 패킷을 수신하면 클라이언트에게 다음과 같은 메시지를 보낸다.

 

- 101 Switching Protocols : 프로토콜이 변경됐음을 알리는 상태 코드

- Sec-WebSocket-Accept : Sec-WebSocket-Key를 기반으로 업그레이드 패킷을 정상적으로 수신했음을 알린다.

- Sec-WebSocket-Protocol : 클라이언트에게 받은 우선순위중에 지원할 수 있는 제일 처음 프로토콜을 선택해서 보내준다.

 

Closing Handshake

 

Opening Handshake와 비슷하게 Closing Handshake도 종료하고 싶은 상대가 먼저 "저 종료할게요~"라고 하면 "알겠습니다~ 저도 종료할게요~"라고 보내고 끝난다. 이거는 딱히 쓸 말이 없는게 패킷에 opcode를 0x8로 설정해 보내게 되면 똑같이 상대방도 opcode를 0x8로 설정하고 보내서 종료하게 된다. 이거는 SSL Handshake를 종료할 때 close notify와 비슷하다.


[3] 프레임 구조

Opening Handshake가 끝나면 웹소켓은 프레임 단위로 데이터를 주고 받게 된다. 프레임 단위라고 하는건 Transport Layer의 TCP 패킷 구조, UDP 패킷 구조처럼 프레임이라는 패킷 구조로 데이터를 보낸다는 뜻이다.

 

https://jaehyeon48.github.io/network/websocket-protocol/

 

 

여기서 중요한 내용은 Closing Handshake에 나온 opcode이다.

https://datatracker.ietf.org/doc/html/rfc6455#section-5.2

opcode 값에 따라 Payload Data가 달라지게 된다.

 

- 0x0: 이전 프레임과 이어지는 데이터

- 0x1 : UTF-8로 인코딩된 텍스트 데이터를 전송한다는 뜻

- 0x2 : 바이너리 데이터를 전송한다는 뜻 (이미지, 파일, ..)

- 0x3 ~ 0x7 : 아직 정해지지 않은 값이지만, 텍스트 / 바이너리 같은 데이터 프레임 설정에 쓰일 예정

- 0x8 : 웹소켓 연결 종료

- 0x9 : 단순히 데이터를 보내보는 용도. 주로 웹소켓 연결이 예기치 못한 네트워크 오류로 죽었는지 살아있는지 판단할 때 사용함

- 0xA : Ping을 보내면 무조건 Pong을 보내야함. 이때 데이터는 Ping으로 보낸 데이터를 그대로 보냄

- 0xB ~ 0xF : 아직 정해지지 않은 값이지만, 웹소켓의 상태를 전달하는 제어 프레임에 쓰일 예정

 

Masking은 데이터를 암호화하는 것인데, Masking Key가 프레임에 있고, 페이로드 데이터의 각 바이트를 XOR 연산하는거라 보안이 제공하는 것은 아니다. 실제 보안은 SSL을 적용해두었다. 그래서 암호화되지 않은 웹소켓 통신은 ws로 접속하고, 암호화된 웹소켓 통신은 wss로 접속하게 된다. HTTP와 HTTPS 차이와 비슷하다.


 

 

 

4. 이외의 내용


[1] 실제 웹소켓 패킷 확인하기

아래 링크에서 보는걸 추천한다.

https://blog.naver.com/websearch/221136852414

 

[WebSocket] WebSocket 패킷 덤프

WebSocket 프로토콜에 대한 패킷 덤프한 결과는 아래와 그림과 같습니다. 분홍색이 클라이언트...

blog.naver.com

 

 

[2] 프로그래밍 언어에서 지원하는 웹소켓 코드

이거는 소켓 프로그래밍과 비슷하다. 코드는 Go 언어에서 가지고 왔다.

 

https://github.com/gorilla/websocket/blob/main/examples/chat/client.go#L41

 

웹소켓으로 데이터가 오면 읽는 작업이 펼쳐진다. 중요한 부분은 readPump 함수의 for문 안에 c.conn.ReadMessage()이다. c.conn은 Client의 websocket.conn이므로 이게 무엇으로 이루어져 있는지 찾아보자

 

https://github.com/gorilla/websocket/blob/main/conn.go#L242

conn은 net.Conn이라고 한다. 그러면 net에 있는 Conn 클래스를 찾아보자

 

https://github.com/golang/go/blob/master/src/net/net.go#L119

찾아보니 Conn은 인터페이스이다. Conn 구현체 코드는 바로 아래 있었다.

 

https://github.com/golang/go/blob/master/src/net/net.go#L185

fd라는게 나오는데 소켓에 대해 아는 분들은 File Descriptor인 것을 알 수 있다.

파일은 운영체제마다 값이 다르므로 UNIX를 찾아보았다.

 

https://github.com/golang/go/blob/master/src/net/fd_unix.go#L26

여기까지 보면 SOCK_STREAM, SOCK_DGRAM을 보면 소켓을 사용한다는걸 볼 수 있다. 이제 충분하지만 더 살펴보자. pfd를 poll에서 FD로 가져오는데, 이것도 fd_unix로 구현된 코드를 가지고 왔다.

 

https://github.com/golang/go/blob/master/src/internal/poll/fd_unix.go#L19

 

여기서 완전히 File Descriptor를 언급하고 있다.


 

 

 

출처 및 참고하면 좋은 내용


2. HTTP, AJAX, WebSocket 역사

- HTTP5를 여행하는 비 웹 개발자를 위한 안내서 - 1부 웹소켓

- RFC 6202 - Known Issues and Best Practice for the Use of Long Polling and Streaming in Bidirectional HTTP

- ably - The history of WebSockets

- 테코톡 - 주드의 Server Sent Events

 

3. WebSocket

- RFC 6455 - WebSocket

- JOINC - websocket

 

웹소켓에 대해 공부하면서 번역 기여도 같이 진행중이다.

- MDN - 프로토콜 업그레이드 메커니즘

- MDN - Upgrade 헤더

 

4. WebSocket 코드 확인하기

- github - golang Repository


반응형