Golang: Websocket 이해하기
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을 막을 수 있는 지 알아보기