๐ŸŽ“ A1Learner
๐Ÿ”

Rust Tokio Framing

Framing is the step that turns a stream of byte chunks into whole messages. TCP reads return chunks of any size.

The problem: chunks, not messages

One read can return any amount of data โ€” half a frame, or several frames at once.

let n = stream.read(&mut buf).await?;
// n bytes arrived: maybe half a frame,
// maybe three frames stuck together.

A frame is just data

The tutorial's Frame type for the Redis protocol holds data only. Its meaning comes one level up.

enum Frame {
    Simple(String),
    Error(String),
    Integer(u64),
    Bulk(Bytes),
    Null,
    Array(Vec<Frame>),
}

Keep a buffer on the connection

pub struct Connection {
    stream: BufWriter<TcpStream>,
    buffer: BytesMut,
}

Try to parse, then read more

read_frame loops. It parses what you have. If the frame is incomplete, it reads more bytes and tries again.

if let Some(frame) = self.parse_frame() {
    return Ok(Some(frame));
}
// Not a full frame yet โ€” read more into the buffer.
if 0 == self.stream.read_buf(&mut self.buffer).await? {
    // 0 means the peer closed the connection.
}

Cut the frame out of the buffer

A check pass finds the frame's end. advance then throws those bytes away. It keeps the rest for the next frame.

let len = buf.position() as usize; // end of the frame
let frame = Frame::parse(&mut buf)?;
self.buffer.advance(len); // discard the frame's bytes

Writing a frame, piece by piece

A bulk frame on the wire is a $ head, the length, then the data. Buffer the pieces, flush once.

self.stream.write_u8(b'$').await?;
self.stream.write_all(len.as_bytes()).await?;
self.stream.write_all(&data).await?;
self.stream.flush().await?;

Example

// Cargo.toml: bytes = "1" (framing needs no socket โ€” just the buffer)
use bytes::BytesMut;

fn encode(buffer: &mut BytesMut, frame: &str) {
    buffer.extend_from_slice(frame.as_bytes());
    buffer.extend_from_slice(b"\n");
}

fn next_frame(buffer: &mut BytesMut) -> Option<String> {
    let newline = buffer.iter().position(|b| *b == b'\n')?;
    let mut line = buffer.split_to(newline + 1);
    line.truncate(newline);
    Some(String::from_utf8(line.to_vec()).unwrap())
}

fn main() {
    let mut buffer = BytesMut::new();
    encode(&mut buffer, "hello");
    encode(&mut buffer, "tokio framing");
    while let Some(frame) = next_frame(&mut buffer) {
        println!("frame: {}", frame);
    }
    println!("buffer bytes left = {}", buffer.len());
}

Key points

  • Framing: turning a stream of bytes into a stream of frames โ€” one frame is one whole message
  • A frame carries data only, no meaning.
  • The Frame enum is just Simple, Error, Integer, Bulk, Null, or Array.
  • Commands are parsed one level up.
  • read_buf reads straight into a BytesMut.
  • It moves the cursor for you.
  • There is no hand-kept position.
  • And there is no zero-filling of fresh capacity.
  • A read returning 0 means the peer closed the connection.
  • An empty buffer is a clean end.
  • A non-empty buffer means a frame was cut in half.
  • That is an error.
  • Writes are buffered too.
  • A BufWriter collects the small pieces of a frame and flushes once.
  • It does not make one system call per piece.
  • For ready-made framing, the tokio-util crate offers Framed.
  • It also offers a Decoder/Encoder pair โ€” a codec.
  • So you describe the frame shape once.
  • You do not hand-write the loop.
  • TCP has no message boundaries at all.

In this example, encode writes two text frames into one BytesMut buffer. Each frame ends with a newline. That is the simplest frame shape there is: a line. next_frame looks for the next newline in the buffer. If there is none, it returns None. That is the "not a full frame yet" answer. A read_frame would get it before reading more. When a newline is found, split_to cuts that line off the front of the buffer. The newline is included in the cut. And truncate drops the newline itself. The loop prints both frames: hello, then tokio framing. The buffer ends empty. Every byte was consumed by exactly one frame. On a real socket, the same cut-and-keep-the-rest logic sits inside read_frame, with reads refilling the buffer.

Note

Any "messages" in a TCP program exist because the framing code created them.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

One read from a socket returns 100 bytes. What can you say about the frames inside?