TT Lab
Get started
Learn Learning paths Courses

Real-Time Communication — WebSocket, gRPC Streaming and WebRTC

Reading WebSocket byte by byte

Continue in TT Lab

In one line

WebSocket starts as HTTP and switches over with a 101, then carries messages in both directions in small frames that only the client masks, and leaves the reason a connection ended in a close code.

Why this was needed

HTTP has the shape of the client asking and the server answering, so for a long time the ability for the server to speak first was filled in with workarounds. Long polling holds a response open, so it sent a new request for every message, and the headers were bigger than the messages. WebSocket in RFC 6455 keeps one connection open and lets both sides send messages whenever they like. And it starts as an HTTP request so that it passes through existing HTTP infrastructure — ports 80 and 443, proxies, cookies — as it is.

Libraries do all of this for you, but failure records usually leave only a single line like "it was cut with 1006." To read that line, you need to know what the bytes look like.

How it works

The opening handshake (section 4) is an ordinary HTTP/1.1 GET. The client sends Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Version: 13, and a Sec-WebSocket-Key holding 16 random bytes written in base64. The server appends the fixed GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 to that key string, hashes it with SHA-1, puts those 20 bytes in base64 into Sec-WebSocket-Accept, and answers with 101 Switching Protocols. The example key dGhlIHNhbXBsZSBub25jZQ== in section 1.3 becomes s3pPLMBiTxaQ9kYGzzhZRbK+xOo=. This calculation is not to keep a secret but to confirm that the other side is a server that really understands WebSocket. If the version does not match, the server answers with 426 and tells you the versions it supports.

From then on, it is frames (section 5.2).

 0               1               2               3
|F|R|R|R| opcode|M| 길이(7)     | 확장 길이(0·2·8바이트) ...
|I|S|S|S|  (4)  |A|             |
|N|V|V|V|       |S|             | 마스크 열쇠(마스킹할 때 4바이트) | 본문 ...

The opcode is 0 (continuation), 1 (text), 2 (binary), 8 (close), 9 (ping), or 10 (pong). If the length is 125 or less, it is written as is in the 7 bits, if 126, in the following 2 bytes, and if 127, in the following 8 bytes, and the most significant bit of an 8-byte length must be 0. The RSV bits can be set only when an extension (for example, the compression extension permessage-deflate) has been agreed, and if you receive a frame with them set without agreement, you must fail the connection.

Frames the client sends must always be masked, and frames the server sends are not masked (section 5.1). Masking is just XORing the i-th byte of the payload with the (i mod 4)-th byte of the key, and the key travels in the frame as it is. The purpose is not secrecy but preventing the intermediary cache poisoning attack explained in section 10.3. If bytes chosen by an attacker go out on the wire as they are, a transparent proxy that does not know WebSocket might mistake them for an HTTP request and response and put them in its cache. The server must close the connection if it receives an unmasked client frame.

Control frames (close, ping, pong) must have a payload of 125 bytes or less and must not be fragmented, and they can be inserted between the fragments of a fragmented data message. A pong returns the payload of the received ping as it is. Text messages must be UTF-8.

Close (section 7) consists of a 2-byte status code and an optional UTF-8 reason. When one side sends a close, the other side answers with a close too, and after that the server closes TCP first.

Code Meaning
1000 Normal closure
1001 Going away (server shutdown, page navigation)
1002 Protocol error
1006 Dropped without a close — a name the receiving side applies, and it must not be sent
1007 Message content does not fit the format (non-UTF-8 text)
1008 Policy violation
1009 Message too big
1011 Internal server error
4000–4999 The range left for applications to use

1012 (service restart) and 1013 (try again later) are values listed not in the RFC body but in IANA's WebSocket close code registry.

What it looks like in the field

"It can't connect only behind the proxy" is usually the case where the proxy does not pass the Upgrade and Connection headers, so the handshake ends with a 400 or a 200. In nginx, you have to configure it to pass the two headers explicitly. The browser's WebSocket API cannot attach custom request headers, so authentication is done with a cookie, the first message, or a one-time token in the URL. This is also why the web terminal of this lab platform verifies identity with a cookie.

What you will do in the next lab

You build, with the standard library, everything from the accept key calculation through frame encoding and parsing, the handshake response, an echo server, and close codes. The grader connects with a raw socket and checks the server frames' MASK bit and the close codes at the byte level, and at the end it sees whether a client from the websockets library accepts your server.