0%

Frames, Byte by Byte · practice

Encode a Small Frame

The RFC’s simplest example is a whole text message, “Hello”, in one unmasked . Section 5.7 gives these seven bytes, written here as hex pairs:

81 05 48 65 6c 6c 6f

You can read the first two already: 81 is FIN set with 1, and 05 is clear with a payload length of 5. The five bytes after them are the letters of “Hello” in . This lesson writes the that builds that frame, and frames like it, from a message.

Text and binary

messages come in two kinds. A text frame, opcode 1, carries text encoded as UTF-8. A binary frame, opcode 2, carries any bytes at all, such as an image or a number packed with struct. On the wire both are just a payload of bytes; the opcode tells the receiver how to treat them.

encode_frame(opcode, payload) accepts either kind of payload. When payload is a str, the function encodes it with UTF-8 first; when it is already bytes, it uses it as it is. isinstance(payload, str) tells the two apart. So encode_frame(OP_TEXT, "Hello") and encode_frame(OP_TEXT, b"Hello") return the same frame.

Count bytes, not characters

The length in byte 1 counts payload bytes, not characters. For “Hello” the two numbers agree, but, as Networks with Python Chapter 2 showed, UTF-8 uses more than one byte for many characters. é takes two bytes and ✓ takes three, so "café ✓" is 6 characters and 9 bytes:

Try it

The frame starts 81 09: the second byte is 9, the length of what is actually sent. A frame that said 6 would leave three bytes behind, and the receiver would read them as the start of the next frame.

encode_frame(OP_TEXT, "café ✓") builds a frame. What is its second byte?

Build byte 0 and byte 1

The first byte combines the FIN flag and the opcode, with the tools from Lesson 1. Start from the opcode, then set the top bit with | 0x80 when fin is true. The opcode stays in the low four bits, where parse_header_bytes looks for it.

fin is a keyword-only with the default True, so an ordinary call such as encode_frame(OP_TEXT, "Hello") builds a final frame, and only a caller that writes fin=False gets a frame that is not final. You will not need fin=False in this chapter, but it shows the bit is really yours to set: encode_frame(OP_TEXT, "Hel", fin=False) is 01 03 48 65 6c, the first fragment of a message the RFC sends in two frames.

What does this print?

print(hex(0x80 | 0x2), hex(0x0 | 0x1))

The second byte is the payload’s length. bytes([b0, length]) turns a of numbers from 0 to 255 into a bytes with one byte per number, and + payload adds the payload after it. That is the whole frame.

Only small frames, for now

A 7-bit length reaches 127, and as Lesson 2 said, 126 and 127 have another meaning. So this lesson’s encode_frame handles payloads of 0 to 125 bytes and refuses anything longer with a that says Lesson 4 adds the longer forms. It also refuses an opcode that is not in KNOWN_OPCODES, because no reader would accept the frame.

Your task

In ws_frames.py, write encode_frame(opcode, payload, *, fin=True):

  1. raise ValueError if opcode is not in KNOWN_OPCODES;

  2. if payload is a str, encode it with UTF-8;

  3. raise ValueError if the payload is longer than 125 bytes, with a message that names Lesson 4;

  4. build the first byte from the opcode, with 0x80 added by | when fin is true;

  5. return the first byte, the length byte and the payload as one bytes value.

Leave parse_header_bytes and the stubs for later lessons as they are.

Press Run. The Lesson 3 section prints your frame for “Hello”, and it should match the RFC’s bytes exactly. The Lesson 4 section now runs your function too, on a 65536-byte payload, and your own refusal appears there, after ValueError:. That is expected: frames_demo.py catches the ValueError and prints its message. With the reference wording, the output from Lesson 3 on reads:

Lesson 3: a text frame for Hello
81 05 48 65 6c 6c 6f
Lesson 4: the start of a 65536-byte frame
  ValueError: payloads over 125 bytes need a longer length form (Lesson 4)
Lesson 5: Hello decoded one byte at a time
  not written yet

Then press Submit. The check compares your frames with bytes it builds itself, for a text you have not seen with characters that take more than one byte, a 125-byte binary payload, an empty text, a frame with fin=False, and a , and it expects ValueError for two reserved opcodes and a 126-byte payload.

Your frames stop at 125 bytes. The next lesson removes that ceiling with the two longer length forms.

Task

Write encode_frame(opcode, payload, *, fin=True) in ws_frames.py. It returns one unmasked as bytes:

  • a first byte holding the in its low four bits, with the top bit 0x80 set when fin is true;

  • a second byte holding the payload’s length in bytes;

  • the payload, encoded with first when it is a str, and used as it is when it is bytes.

Raise for an opcode that is not in KNOWN_OPCODES, and for a payload longer than 125 bytes, with a message that names Lesson 4.

Press Run: the Lesson 3 section should print 81 05 48 65 6c 6c 6f, and the Lesson 4 section prints your ValueError message. Then press Submit. The check compares your frames with bytes it builds itself: “Hello” as str and as bytes, a text you have not seen whose UTF-8 is longer than its character count, a 125-byte binary payload, an empty text, fin=False, and a . It expects ValueError for opcode 3, opcode 0xB and a 126-byte payload. Any way of joining the bytes is fine.