0%

Masks and Control Frames · practice

Ping, Pong, and Other Control Frames

Your codec now reads and writes masked and unmasked in both directions, but it treats every the same way. A close, a or a pong goes through decode_frame exactly like a text message. These three are different: they are control frames, which manage the connection itself instead of carrying the application’s messages, and RFC 6455 holds them to stricter rules.

The upper half of the opcode table

Chapter 3 split the sixteen opcodes into two groups. The lower half, 0x0 to 0x7, is for data: continuation, text, binary, and five reserved values. The upper half, 0x8 to 0xF, is for control:

OpcodeFrame
0x8close
0x9ping
0xApong
0xB to 0xFreserved for future control frames

Every opcode in the upper half has the bit 0x8 set, and none in the lower half does, so “is this a control frame?” is one comparison: the opcode is 8 or more. parse_header_bytes already refuses the reserved opcodes, so in practice a control frame is a close, a ping or a pong.

Three rules for control frames

RFC 6455 section 5.5 gives control frames three rules that data frames do not have:

  1. At most 125 bytes of payload. So a control frame always uses the 7-bit length; the markers 126 and 127 never appear in one.

  2. Never fragmented. A control frame always has FIN set. Lesson 5 shows what fragments are; a control frame is never one.

  3. Allowed between fragments. A control frame may arrive in the middle of a message that is split across several frames. Lesson 5 shows why that matters.

The first two can be checked from the first two bytes alone. Byte 0 holds FIN and the opcode, and byte 1 holds the 7-bit length, which is above 125 only when it is one of the markers. So decode_frame can refuse a control frame that breaks either rule right after parse_header_bytes, before it reads any more. Like the size limit in Chapter 3, refusing from the header means a peer cannot make your program wait for or hold a payload it will refuse anyway. The encoder follows the same rules, so your own code can never build such a frame by mistake.

A frame starts 89 7e 00 c8. What does the decoder do?

Ping and pong

A ping asks the other side “are you still there?”. The other side answers with a pong that carries exactly the same payload, the application data of the ping (sections 5.5.2 and 5.5.3). The RFC says the receiver should answer “as soon as is practical”; it promises no particular time. Section 5.7 prints a ping carrying “Hello” and the masked pong a client sends back with the key 37 fa 21 3d, the first and last lines below. The middle line is not in the RFC: it is the unmasked pong a server would send, computed with the same rules:

89 05 48 65 6c 6c 6f
8a 05 48 65 6c 6c 6f
8a 85 37 fa 21 3d 7f 9f 4d 51 58

Either side may send a ping, and the payload is the sender’s choice, often empty. The RFC also allows a pong nobody asked for, an unsolicited pong, which works as a one-way “I am still here” and needs no answer. Chapter 8 uses pings to notice a peer that has gone silent; here you only answer them.

Your client receives a pong it never asked for. What should it do?

Who masks the pong

A pong is a frame like any other, so the direction rule applies to it. A client’s pong is masked, as every client frame is, and a server’s pong is not. That is why reply_to_ping(frame, *, mask_key=None) takes a mask_key and passes it on to encode_frame: a server calls it without a key, and a client calls it with a fresh one. The frame is the decode_frame returns, so the pong’s payload is frame["payload"], and a frame whose opcode is not a ping has nothing to answer.

What does this print?

print(0x89 & 0x0F, (0x8A & 0x0F) >= 8)

Your task

In ws_frames.py:

  1. Write is_control(opcode): True for an opcode of 8 or more, False below.

  2. Write reply_to_ping(frame, *, mask_key=None): raise unless frame["opcode"] is OP_PING, then return encode_frame(OP_PONG, frame["payload"], mask_key=mask_key).

  3. In decode_frame, right after the direction rule, raise ValueError for a control frame whose 7-bit length is over 125 or whose FIN bit is clear. The check must come before the payload is read.

  4. In encode_frame, raise ValueError for a control frame with more than 125 bytes of payload or with fin=False.

Data frames keep their old rules: a text frame of 126 bytes, or one with FIN clear, still encodes and decodes.

Press Run. The Lesson 3 section decodes the RFC’s ping as a client reads it, then answers it the way a client must, with your reply_to_ping and the RFC’s key. The result is the RFC’s masked pong:

Lesson 3: the client's pong for the server's ping
8a 85 37 fa 21 3d 7f 9f 4d 51 58
Lesson 4: close 1000 and back
  not written yet

Then press Submit. The check calls is_control for all sixteen opcodes. It decodes pings and pongs of up to 125 bytes, masked and unmasked, delivered whole, one byte and two bytes at a time, and expects ValueError for a ping or pong that announces more, and for a ping or close with FIN clear, with no read for the payload. It asks encode_frame for control frames that break the rules, answers pings you have not seen with and without a key, and expects ValueError from reply_to_ping for a pong, a text frame and a close. The close frame is the one control frame whose payload means something; the next lesson reads it.

Task

In ws_frames.py:

  1. Write is_control(opcode): True for an of 8 or more (close, , pong and the reserved control opcodes), False below.

  2. Write reply_to_ping(frame, *, mask_key=None): frame is a as decode_frame returns it. Raise unless its opcode is OP_PING; otherwise return encode_frame(OP_PONG, frame["payload"], mask_key=mask_key).

  3. In decode_frame, raise ValueError for a control whose 7-bit length is over 125 or whose FIN bit is clear, before reading its payload.

  4. In encode_frame, raise ValueError for a control frame with more than 125 payload bytes or fin=False.

Press Run: the Lesson 3 section should print 8a 85 37 fa 21 3d 7f 9f 4d 51 58. Then press Submit. The check calls is_control for every opcode from 0 to 15; decodes pings and pongs of up to 125 bytes, masked and unmasked, whole, one byte and two bytes at a time; expects ValueError with no read for the payload for a ping or pong announcing more than 125 bytes and a ping or close with FIN clear; expects text frames of 126 bytes or with FIN clear to keep working; expects ValueError from encode_frame for a 126-byte ping or pong and a close with fin=False; and answers the RFC’s ping, an empty ping and an unseen 125-byte ping with and without a key, expecting ValueError for a pong, a text frame or a close.