golang

Golang: Websocket 이해하기

주먹불끈 2021. 11. 17. 15:58

Photo by Christopher Robin Ebbinghaus on Unsplash

 

간단하게 메시지를 주고 받는 websocket server 만들어 일이 있었다. gorilla/webscoket 예제를 참고하여 server, 테스트를 위한 client까지 동작시켜 보았는데 이참에 개념을 좀더 들여다보자 싶어져서 자료를 찾아 정리해보았다. 이어서  gorilla/webscoket 패키지의 구현을 들여다보려 한다

 

참고 링크(라고 하고 거의 번역을 수준): https://sookocheff.com/post/networking/how-do-websockets-work/

GitHub(gorilla/websocket): https://github.com/gorilla/websocket

 

Websocket 개요

 

Websocket protocol 클라이언트와 서버간에 byte 혹은 UTF-8 text 메시지를 쉽게 주고받으려고 만든 것이다.

HTTP(그리고 아래에 하나의 TCP/IP 소켓) connection 이용하며, bidirectional full-duplex 통신을 지원한다.

 

HTTP 클라이언트의 요청에 서버가 응답을 한다. request 하나에 response하나를 받는 것이다. HTTP websocket처럼 메시지를 주고 받으려면 클라이언트가 주기적으로 request 보내거나, 미리 보내놓고 서버에서 보낼 생기면 response하게 해야 한다.(long polling).  Websocket 이런 HTTP 아쉬운 지점을 해소해주는 대안이다. UDP처럼 메시지 단위로 주고 받지만 TCP 신뢰성을 가진다. TCP 사용하니 당연하다

 

Websocket protocol 간단하다

 

Websocket 정의부터 보자. 

 

The protocol consists of an opening handshake followed by basic message framing, layered over TCP.
 RFC 6455 - The WebSocket Protocol

 

처음에 handshake 하고, keep-alive 되어있는 TCP connection 위에서 기본적인 메시지 프레임을 주고 받는게 전부이다

 

1) 클라이언트가 websocket 열어달라고 HTTP request 보내고, 서버가 websocket 지원한다면 websocket 열어주며 response한다

2) 이제 이렇게 연결된 TCP/IP(keep-alive) websocket connection으로 이용하여 메시지를 주고 받는다

3) 양쪽에서 모두 connection close 동의하면 하면 TCP connection 끊긴다

 

Websocket 시작하기 위한 handshake

 

클라이언트는 보내고, 서버는 어떻게 답하는 걸까?

 

클라이언트 request header 보자

 

Connection: Upgrade

    - 연결을 유지하며(keep-alive) HTTP 아닌 request 쓰고 싶다

Upgrade: websocket

    - websocket connection 하고 싶다

Sec-WebSocket-Key

    - 일회성 랜덤값 16바이트를 base64 인코딩한 값이다

Sec-WebSocket-Version: 13

    - 버전은 13이라고 적으면 된다

 

서버는 아래와 같이 response 한다

 

클라이언트가 원하는 회신은 HTTP 101 Switching Protocols 이다. 클라이언트가 요청한 Upgrade websocket으로 프로토콜을 swithinng 한다는 것이다.

 

Upgrade, Connection

    - 클라이언트가 보낸 값을 그대로 보낸다.

Sec-WebSocket-Accept

    - 클라이언트가 보낸 Sec-WebSocket-Key 문자열에 상수값 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 붙여서 (RFC 6455) SHA-1 해싱 + base64 해준 것이다

    - 참고: https://stackoverflow.com/a/35989898

    - 예제코드: https://play.golang.org/p/zdMo8mv_rSJ

Websocket protocol

 

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

 

Websocket 이용해 클라이언트와 서버는 UTF-8 text 또는 byte 메시지를 주고받는다고 하였다.

Websocket framed protocol이다. 메시지를 프레임에 담아서 보내며, 메시지가 크다면 여러 프레임으로 나눠서 보낸다.

 

1. FIN bit 1이면 마지막 프레임이라는 것이다. 계속 0으로 보내다가 마지막 프레임에 1 표시하면 되겠다. framed protocol이니 이게 필요한 것이다.

2. RSV1,RSV2, RSV3 reserved bits

3. opcode 아래에 따로 언급

4. MASK bit payload data masking 하는지를 의미하며 1이면 masking enabled이며, 아래의 Masking-key (32bits) 이용하여 XOR masking 해준다. cache poisoning 대비한 보안강화라고만 언급해둔다.

5. Payload len, Extended payload length 아래와 같은 의미를 가진다

    1) Payload len <= 125: Payload Data 길이는 Payload len 7bit 값이다

    2) Payload len == 126: Extended payload length 16bits uint16으로 계산해서 Payload Data 길이를 알아낸다

    3) Payload len == 127: Extended payload length 64bits uint64 계산해서 Payload Data 길이를 알아낸다

6. Masking-key 4 MASK bit 1일때에 값이 있다

7. payload data App에서 추상화한 의미로 사용하는 것이다. 예를 들어 JSON, protobuf serialize해서 보내고 받을 있다. 여기에 더해서 websocket protocol 확장하여 사용할 수도 있다. handshake시에 합의를 하여 사용하면 된다

 

3. opcode payload data 어떻게 이해하면 될지를 알려준다.

    - 0x00: 프레임이거나 앞의 프레임에 이어서 보내는 payload라는 의미

    - 0x01/0x02: 각각 UTF-8 text/binary frame이라는 . websocket 두가지 타입의 메시지를 주고 받을 있다.

    - 0x08: 클라이언트가 connection close 하고 싶다고 말하는

    - 0x09: ping이다. 상대는(클라이언트건 서버건) pong 보내줘야 한다

    - 0x0a: pong이다. 상대는 이번에는 ping 보내줘야 한다 

웹소켓을 닫기

 

Closing frame opcode 0x08 보내며 바디에는 closing하는지 정보를 담을 있다. 클라이언트든 서버든 이걸 보내면, 상대는 response 마찬가지로 0x08 보내야 한다. 하지만 TCP connection close 항상 서버가 한다.

 

이어서 보려는 내용

- gorilla/webscoket 패키지에서 websocket protocol 어떻게 구현되어 있는지 코드레벨에서 보기

- cache poisoning 무엇이며 masking으로 cache poisoning 막을 있는 알아보기

 

반응형