Framing

We will now apply what we just learned about I/O and implement the Mini-Redis framing layer. Framing is the process of taking a byte stream and converting it to a stream of frames. A frame is a unit of data transmitted between two peers. The Redis protocol frame is defined as follows:

  1. use bytes::Bytes;
  2. enum Frame {
  3. Simple(String),
  4. Error(String),
  5. Integer(u64),
  6. Bulk(Bytes),
  7. Null,
  8. Array(Vec<Frame>),
  9. }

Note how the frame only consists of data without any semantics. The command parsing and implementation happen at a higher level.

For HTTP, a frame might look like:

  1. enum HttpFrame {
  2. RequestHead {
  3. method: Method,
  4. uri: Uri,
  5. version: Version,
  6. headers: HeaderMap,
  7. },
  8. ResponseHead {
  9. status: StatusCode,
  10. version: Version,
  11. headers: HeaderMap,
  12. },
  13. BodyChunk {
  14. chunk: Bytes,
  15. },
  16. }

To implement framing for Mini-Redis, we will implement a Connection struct that wraps a TcpStream and reads/writes mini_redis::Frame values.

  1. use tokio::net::TcpStream;
  2. use mini_redis::{Frame, Result};
  3. struct Connection {
  4. stream: TcpStream,
  5. // ... other fields here
  6. }
  7. impl Connection {
  8. /// Read a frame from the connection.
  9. ///
  10. /// Returns `None` if EOF is reached
  11. pub async fn read_frame(&mut self)
  12. -> Result<Option<Frame>>
  13. {
  14. // implementation here
  15. }
  16. /// Write a frame to the connection.
  17. pub async fn write_frame(&mut self, frame: &Frame)
  18. -> Result<()>
  19. {
  20. // implementation here
  21. }
  22. }

You can find the details of the Redis wire protocol here. The full Connection code is found here.

Buffered reads

The read_frame method waits for an entire frame to be received before returning. A single call to TcpStream::read() may return an arbitrary amount of data. It could contain an entire frame, a partial frame, or multiple frames. If a partial frame is received, the data is buffered and more data is read from the socket. If multiple frames are received, the first frame is returned and the rest of the data is buffered until the next call to read_frame.

To implement this, Connection needs a read buffer field. Data is read from the socket into the read buffer. When a frame is parsed, the corresponding data is removed from the buffer.

We will use BytesMut as the buffer type. This is a mutable version of Bytes.

  1. use bytes::BytesMut;
  2. use tokio::net::TcpStream;
  3. pub struct Connection {
  4. stream: TcpStream,
  5. buffer: BytesMut,
  6. }
  7. impl Connection {
  8. pub fn new(stream: TcpStream) -> Connection {
  9. Connection {
  10. stream,
  11. // Allocate the buffer with 4kb of capacity.
  12. buffer: BytesMut::with_capacity(4096),
  13. }
  14. }
  15. }

Next, we implement the read_frame() method.

  1. use tokio::io::AsyncReadExt;
  2. use bytes::Buf;
  3. use mini_redis::Result;
  4. pub async fn read_frame(&mut self)
  5. -> Result<Option<Frame>>
  6. {
  7. loop {
  8. // Attempt to parse a frame from the buffered data. If
  9. // enough data has been buffered, the frame is
  10. // returned.
  11. if let Some(frame) = self.parse_frame()? {
  12. return Ok(Some(frame));
  13. }
  14. // There is not enough buffered data to read a frame.
  15. // Attempt to read more data from the socket.
  16. //
  17. // On success, the number of bytes is returned. `0`
  18. // indicates "end of stream".
  19. if 0 == self.stream.read_buf(&mut self.buffer).await? {
  20. // The remote closed the connection. For this to be
  21. // a clean shutdown, there should be no data in the
  22. // read buffer. If there is, this means that the
  23. // peer closed the socket while sending a frame.
  24. if self.buffer.is_empty() {
  25. return Ok(None);
  26. } else {
  27. return Err("connection reset by peer".into());
  28. }
  29. }
  30. }
  31. }

Let’s break this down. The read_frame method operates in a loop. First, self.parse_frame() is called. This will attempt to parse a redis frame from self.buffer. If there is enough data to parse a frame, the frame is returned to the caller of read_frame().Otherwise, we attempt to read more data from the socket into the buffer. After reading more data, parse_frame() is called again. This time, if enough data has been received, parsing may succeed.

When reading from the stream, a return value of 0 indicates that no more data will be received from the peer. If the read buffer still has data in it, this indicates a partial frame has been received and the connection is being terminated abruptly. This is an error condition and Err is returned.

The Buf trait

When reading from the stream, read_buf is called. This version of the read function takes a value implementing BufMut from the bytes crate.

First, consider how we would implement the same read loop using read(). Vec<u8> could be used instead of BytesMut.

  1. use tokio::net::TcpStream;
  2. pub struct Connection {
  3. stream: TcpStream,
  4. buffer: Vec<u8>,
  5. cursor: usize,
  6. }
  7. impl Connection {
  8. pub fn new(stream: TcpStream) -> Connection {
  9. Connection {
  10. stream,
  11. // Allocate the buffer with 4kb of capacity.
  12. buffer: vec![0; 4096],
  13. cursor: 0,
  14. }
  15. }
  16. }

And the read_frame() function on Connection:

  1. use mini_redis::{Frame, Result};
  2. pub async fn read_frame(&mut self)
  3. -> Result<Option<Frame>>
  4. {
  5. loop {
  6. if let Some(frame) = self.parse_frame()? {
  7. return Ok(Some(frame));
  8. }
  9. // Ensure the buffer has capacity
  10. if self.buffer.len() == self.cursor {
  11. // Grow the buffer
  12. self.buffer.resize(self.cursor * 2, 0);
  13. }
  14. // Read into the buffer, tracking the number
  15. // of bytes read
  16. let n = self.stream.read(
  17. &mut self.buffer[self.cursor..]).await?;
  18. if 0 == n {
  19. if self.cursor == 0 {
  20. return Ok(None);
  21. } else {
  22. return Err("connection reset by peer".into());
  23. }
  24. } else {
  25. // Update our cursor
  26. self.cursor += n;
  27. }
  28. }
  29. }

When working with byte arrays and read, we must also maintain a cursor tracking how much data has been buffered. We must make sure to pass the empty portion of the buffer to read(). Otherwise, we would overwrite buffered data. If our buffer gets filled up, we must grow the buffer in order to keep reading. In parse_frame() (not included), we would need to parse data contained by self.buffer[..self.cursor].

Because pairing a byte array with a cursor is very common, the bytes crate provides an abstraction representing a byte array and cursor. The Buf trait is implemented by types from which data can be read. The BufMut trait is implemented by types into which data can be written. When passing a T: BufMut to read_buf(), the buffer’s internal cursor is automatically updated by read_buf. Because of this, in our version of read_frame, we do not need to manage our own cursor.

Additionally, when using Vec<u8>, the buffer must be initialized. vec![0; 4096] allocates an array of 4096 bytes and writes zero to every entry. When resizing the buffer, the new capacity must also be initialized with zeros. The initialization process is not free. When working with BytesMut and BufMut, capacity is uninitialized. The BytesMut abstraction prevents us from reading the uninitialized memory. This lets us avoid the initialization step.

Parsing

Now, let’s look at the parse_frame() function. Parsing is done in two steps.

  1. Ensure a full frame is buffered and find the end index of the frame.
  2. Parse the frame.

The mini-redis crate provides us with a function for both of these steps:

  1. Frame::check
  2. Frame::parse

We will also reuse the Buf abstraction to help. A Buf is passed into Frame::check. As the check function iterates the passed in buffer, the internal cursor will be advanced. When check returns, the buffer’s internal cursor points to the end of the frame.

For the Buf type, we will use std::io::Cursor<&[u8]>.

  1. use mini_redis::{Frame, Result};
  2. use mini_redis::frame::Error::Incomplete;
  3. use bytes::Buf;
  4. use std::io::Cursor;
  5. fn parse_frame(&mut self)
  6. -> Result<Option<Frame>>
  7. {
  8. // Create the `T: Buf` type.
  9. let mut buf = Cursor::new(&self.buffer[..]);
  10. // Check whether a full frame is available
  11. match Frame::check(&mut buf) {
  12. Ok(_) => {
  13. // Get the byte length of the frame
  14. let len = buf.position() as usize;
  15. // Reset the internal cursor for the
  16. // call to `parse`.
  17. buf.set_position(0);
  18. // Parse the frame
  19. let frame = Frame::parse(&mut buf)?;
  20. // Discard the frame from the buffer
  21. self.buffer.advance(len);
  22. // Return the frame to the caller.
  23. Ok(Some(frame))
  24. }
  25. // Not enough data has been buffered
  26. Err(Incomplete) => Ok(None),
  27. // An error was encountered
  28. Err(e) => Err(e.into()),
  29. }
  30. }

The full Frame::check function can be found here. We will not cover it in its entirety.

The relevant thing to note is that Buf‘s “byte iterator” style APIs are used. These fetch data and advance the internal cursor. For example, to parse a frame, the first byte is checked to determine the type of the frame. The function used is Buf::get_u8. This fetches the byte at the current cursor’s position and advances the cursor by one.

There are more useful methods on the Buf trait. Check the API docs for more details.

Buffered writes

The other half of the framing API is the write_frame(frame) function. This function writes an entire frame to the socket. In order to minimize write syscalls, writes will be buffered. A write buffer is maintained and frames are encoded to this buffer before being written to the socket. However, unlike read_frame(), the entire frame is not always buffered to a byte array before writing to the socket.

Consider a bulk stream frame. The value being written is Frame::Bulk(Bytes). The wire format of a bulk frame is a frame head, which consists of the $ character followed by the data length in bytes. The majority of the frame is the contents of the Bytes value. If the data is large, copying it to an intermediate buffer would be costly.

To implement buffered writes, we will use the BufWriter struct. This struct is initialized with a T: AsyncWrite and implements AsyncWrite itself. When write is called on BufWriter, the write does not go directly to the inner writer, but to a buffer. When the buffer is full, the contents are flushed to the inner writer and the inner buffer is cleared. There are also optimizations that allow bypassing the buffer in certain cases.

We will not attempt a full implementation of write_frame() as part of the tutorial. See the full implementation here.

First, the Connection struct is updated:

  1. use tokio::io::BufWriter;
  2. use tokio::net::TcpStream;
  3. use bytes::BytesMut;
  4. pub struct Connection {
  5. stream: BufWriter<TcpStream>,
  6. buffer: BytesMut,
  7. }
  8. impl Connection {
  9. pub fn new(stream: TcpStream) -> Connection {
  10. Connection {
  11. stream: BufWriter::new(stream),
  12. buffer: BytesMut::with_capacity(4096),
  13. }
  14. }
  15. }

Next, write_frame() is implemented.

  1. use tokio::io::{self, AsyncWriteExt};
  2. use mini_redis::Frame;
  3. async fn write_frame(&mut self, frame: &Frame)
  4. -> io::Result<()>
  5. {
  6. match frame {
  7. Frame::Simple(val) => {
  8. self.stream.write_u8(b'+').await?;
  9. self.stream.write_all(val.as_bytes()).await?;
  10. self.stream.write_all(b"\r\n").await?;
  11. }
  12. Frame::Error(val) => {
  13. self.stream.write_u8(b'-').await?;
  14. self.stream.write_all(val.as_bytes()).await?;
  15. self.stream.write_all(b"\r\n").await?;
  16. }
  17. Frame::Integer(val) => {
  18. self.stream.write_u8(b':').await?;
  19. self.write_decimal(*val).await?;
  20. }
  21. Frame::Null => {
  22. self.stream.write_all(b"$-1\r\n").await?;
  23. }
  24. Frame::Bulk(val) => {
  25. let len = val.len();
  26. self.stream.write_u8(b'$').await?;
  27. self.write_decimal(len as u64).await?;
  28. self.stream.write_all(val).await?;
  29. self.stream.write_all(b"\r\n").await?;
  30. }
  31. Frame::Array(_val) => unimplemented!(),
  32. }
  33. self.stream.flush().await;
  34. Ok(())
  35. }

The functions used here are provided by AsyncWriteExt. They are available on TcpStream as well, but it would not be advisable to issue single byte writes without the intermediate buffer.

The function ends with a call to self.stream.flush().await. Because BufWriter stores writes in an intermediate buffer, calls to write do not guarantee that the data is written to the socket. Before returning, we want the frame to be written to the socket. The call to flush() writes any data pending in the buffer to the socket.

Another alternative would be to not call flush() in write_frame(). Instead, provide a flush() function on Connection. This would allow the caller to write queue multiple small frames in the write buffer then write them all to the socket with one write syscall. Doing this complicates the Connection API. Simplicity is one of Mini-Redis’ goals, so we decided to include the flush().await call in fn write_frame().