Lesson 30 of 44
Codecs with tokio-util
A codec converts between raw bytes and typed messages. tokio-util wraps a byte stream and a codec into a Stream of messages and a Sink of messages, so the receive buffer, partial-frame handling, and flushing from Bytes and Framing are written once. After this lesson you can read and write lines and length-delimited frames, write a codec for your own protocol, and handle codec errors and end-of-stream correctly.
| Item | Role |
|---|---|
Decoder |
turns a BytesMut buffer into Option<Item> |
Encoder<Item> |
appends the encoding of an item to a BytesMut |
FramedRead, FramedWrite, Framed |
attach a codec to an AsyncRead, an AsyncWrite, or both |
LinesCodec, BytesCodec, LengthDelimitedCodec |
ready-made codecs |
SinkExt::send, feed, flush, close |
write to a Framed |
StreamReader, ReaderStream |
convert between streams of Bytes and AsyncRead |
The examples use these crates:
[dependencies]
tokio = { version = "1", features = ["full", "test-util"] }
tokio-util = { version = "0.7", features = ["full"] }
futures = "0.3"
bytes = "1"The codec types live in tokio_util::codec. Framed* values implement futures::Stream and futures::Sink, so the examples import futures::StreamExt and futures::SinkExt. Streams explains the Stream trait and why one StreamExt is imported per file.
Example 1: FramedRead with LinesCodec
FramedRead::new(reader, codec) reads bytes from any AsyncRead and yields decoded items as a stream. LinesCodec splits at \n, removes a trailing \r, and yields each line as a String. Each item is a Result because decoding can fail. A &[u8] is an AsyncRead, so the example needs no socket.
use futures::StreamExt;
use tokio_util::codec::{FramedRead, LinesCodec};
#[tokio::main]
async fn main() {
let data: &[u8] = b"first\nsecond\r\nlast without newline";
let mut lines = FramedRead::new(data, LinesCodec::new());
while let Some(line) = lines.next().await {
println!("{line:?}");
}
}At end of input, LinesCodec also returns the final text that has no newline. This comes from Decoder::decode_eof (Example 10), which LinesCodec overrides.
Example 2: FramedWrite and SinkExt
FramedWrite::new(writer, codec) encodes items into an internal buffer and writes the buffer to an AsyncWrite. It implements Sink, and futures::SinkExt supplies the calls:
| Method | Effect |
|---|---|
feed(item) |
encode the item into the buffer; write to the transport only when the buffer passes the backpressure boundary (Example 13) |
flush() |
write everything buffered and flush the transport |
send(item) |
feed then flush |
close() |
flush then shut down the transport |
Vec<u8> implements AsyncWrite, so the example can inspect the output.
use futures::SinkExt;
use tokio_util::codec::{FramedWrite, LinesCodec};
#[tokio::main]
async fn main() {
let mut sink = FramedWrite::new(Vec::new(), LinesCodec::new());
sink.send("hello").await.unwrap();
sink.send(String::from("world")).await.unwrap();
println!("after send: {} bytes buffered", sink.write_buffer().len());
sink.feed("a").await.unwrap();
sink.feed("b").await.unwrap();
println!("after feed: {} bytes buffered", sink.write_buffer().len());
sink.flush().await.unwrap();
println!("after flush: {} bytes buffered", sink.write_buffer().len());
let written = sink.into_inner();
println!("{:?}", String::from_utf8(written).unwrap());
}LinesCodec implements Encoder<T> for any T: AsRef<str>, so both &str and String work. It appends \n to each item.
Example 3: Framed, both directions on one connection
Framed::new(io, codec) gives one value that is both a Stream and a Sink, for a transport that is AsyncRead + AsyncWrite. tokio::io::duplex provides an in-memory connection. Each side wraps its end in a Framed.
use futures::{SinkExt, StreamExt};
use tokio::io::duplex;
use tokio_util::codec::{Framed, LinesCodec};
#[tokio::main]
async fn main() {
let (client_io, server_io) = duplex(1024);
let server = tokio::spawn(async move {
let mut framed = Framed::new(server_io, LinesCodec::new());
while let Some(Ok(line)) = framed.next().await {
framed.send(line.to_uppercase()).await.unwrap();
}
});
let mut client = Framed::new(client_io, LinesCodec::new());
for word in ["ping", "hello framed"] {
client.send(word).await.unwrap();
let reply = client.next().await.unwrap().unwrap();
println!("sent {word:?}, got {reply:?}");
}
drop(client);
server.await.unwrap();
println!("server task finished");
}Dropping client closes its end. The server's next() then returns None, the while let ends, and the task finishes.
Example 4: Splitting a Framed into a sink and a stream
StreamExt::split() (from futures) divides a Framed into a SplitSink and a SplitStream. The two halves can move into separate tasks: one reads while the other writes.
use futures::{SinkExt, StreamExt};
use tokio::io::duplex;
use tokio_util::codec::{Framed, LinesCodec};
#[tokio::main]
async fn main() {
let (a, b) = duplex(1024);
let (mut a_sink, mut a_stream) = Framed::new(a, LinesCodec::new()).split();
let mut b = Framed::new(b, LinesCodec::new());
let writer = tokio::spawn(async move {
for n in 1..=3 {
a_sink.send(format!("message {n}")).await.unwrap();
}
a_sink.close().await.unwrap();
});
while let Some(line) = b.next().await {
println!("b received {:?}", line.unwrap());
}
writer.await.unwrap();
println!("a received {:?}", a_stream.next().await.map(|r| r.unwrap()));
}close() flushes and shuts down the write half, so b sees end of stream after the third line. Nothing was sent from b to a, so the read half of a returns None too, because b is dropped at the end of main... in this example b is still alive, so a_stream.next() would wait. Only the end-of-stream line above is printed because b finished reading after a shut down, and b was not written to. The last line shows the result of the read on a's side.
Example 5: LinesCodec and the maximum line length
LinesCodec::new() has no limit: a peer that never sends \n makes the buffer grow without bound. LinesCodec::new_with_max_length(n) fails with LinesCodecError::MaxLineLengthExceeded when a line is longer than n bytes (not counting the newline). After the error, the codec discards input up to the next newline, and decoding resumes with the line after it. The error is the same defence as the maximum frame size in Bytes and Framing.
use futures::StreamExt;
use tokio_util::codec::{FramedRead, LinesCodec};
#[tokio::main]
async fn main() {
let data: &[u8] = b"ok\nthis line is much too long\nnext\n";
let mut lines = FramedRead::new(data, LinesCodec::new_with_max_length(8));
for _ in 0..5 {
match lines.next().await {
Some(Ok(line)) => println!("line {line:?}"),
Some(Err(e)) => println!("error: {e}"),
None => println!("end"),
}
}
}Example 6: BytesCodec
BytesCodec does no framing. Each item is whatever bytes were in the buffer when the decoder ran, as a BytesMut. It encodes Bytes or BytesMut by copying them to the buffer. It suits code that wants a stream of chunks, such as a proxy, and reads the same way as Example 1 of Bytes and Framing: chunk sizes have no relation to write sizes.
use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use tokio::io::{AsyncWriteExt, duplex};
use tokio_util::codec::{BytesCodec, FramedRead, FramedWrite};
#[tokio::main]
async fn main() {
let (mut w, r) = duplex(64);
w.write_all(b"ab").await.unwrap();
w.write_all(b"cd").await.unwrap();
let mut chunks = FramedRead::new(r, BytesCodec::new());
println!("chunk: {:?}", chunks.next().await.unwrap().unwrap());
w.write_all(b"ef").await.unwrap();
drop(w);
println!("chunk: {:?}", chunks.next().await.unwrap().unwrap());
println!("end: {:?}", chunks.next().await.is_none());
let mut sink = FramedWrite::new(Vec::new(), BytesCodec::new());
sink.send(Bytes::from_static(b"xyz")).await.unwrap();
println!("{:?}", sink.into_inner());
}Example 7: LengthDelimitedCodec
LengthDelimitedCodec is the length-prefix framing from Bytes and Framing as a ready-made codec. Its defaults are a 4-byte big-endian length field and a maximum frame length of 8 MiB (8 * 1024 * 1024 bytes), read from tokio-util 0.7.19. The decoder yields the payload as a BytesMut, without the header. The encoder accepts Bytes and prepends the header.
The writer here sends the encoded bytes 3 at a time, so frames arrive in pieces. Each sleep under the paused clock makes the reader see exactly one piece per read.
use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use std::time::Duration;
use tokio::io::{AsyncWriteExt, duplex};
use tokio::time::sleep;
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let mut sink = FramedWrite::new(Vec::new(), LengthDelimitedCodec::new());
sink.send(Bytes::from_static(b"hello")).await.unwrap();
sink.send(Bytes::from_static(b"codec")).await.unwrap();
let wire = sink.into_inner();
println!("wire: {:02x?}", wire);
let (mut client, server) = duplex(1024);
tokio::spawn(async move {
for piece in wire.chunks(3) {
client.write_all(piece).await.unwrap();
sleep(Duration::from_millis(5)).await;
}
});
let mut frames = FramedRead::new(server, LengthDelimitedCodec::new());
while let Some(frame) = frames.next().await {
println!("frame: {:?}", frame.unwrap());
}
}The 20 bytes on the wire (two frames of 4 + 5 bytes, plus one more) arrive in 7 pieces. The codec buffers them and yields a frame only when it is complete. No decoding code appears in the program.
Example 8: Builder options
LengthDelimitedCodec::builder() describes other layouts. Each option changes both the decoder and the encoder unless noted.
| Option | Default | Meaning |
|---|---|---|
length_field_length(n) |
4 | width of the length field in bytes, 1 to 8 (panics otherwise) |
length_field_type::<u16>() |
the same, chosen by an integer type | |
big_endian(), little_endian(), native_endian() |
big-endian | byte order of the length field |
length_field_offset(n) |
0 | bytes before the length field; the decoder skips them, the encoder does not write them |
length_adjustment(n) |
0 | added to the field value on decode to get the number of bytes that follow the header; subtracted on encode |
num_skip(n) |
offset + field length | bytes dropped from the front of each decoded frame |
max_frame_length(n) |
8 MiB | largest payload accepted |
new_codec(), new_read(io), new_write(io), new_framed(io) |
build the codec, or a FramedRead, FramedWrite, or Framed directly |
The encoder is a plain function, so the example calls it directly on a BytesMut to show the wire bytes.
use bytes::{Bytes, BytesMut};
use tokio_util::codec::{Encoder, LengthDelimitedCodec};
fn wire(mut codec: LengthDelimitedCodec) -> String {
let mut dst = BytesMut::new();
codec.encode(Bytes::from_static(b"hello"), &mut dst).unwrap();
format!("{:02x?}", &dst[..])
}
fn main() {
println!("default: {}", wire(LengthDelimitedCodec::new()));
println!(
"2-byte length: {}",
wire(LengthDelimitedCodec::builder().length_field_length(2).new_codec())
);
println!(
"little-endian: {}",
wire(LengthDelimitedCodec::builder().little_endian().new_codec())
);
println!(
"includes header: {}",
wire(
LengthDelimitedCodec::builder()
.length_field_length(2)
.length_adjustment(-2)
.new_codec()
)
);
println!(
"u8 length: {}",
wire(LengthDelimitedCodec::builder().length_field_type::<u8>().new_codec())
);
}With length_adjustment(-2) and a 2-byte field, the field holds payload length plus 2, so it counts its own bytes. Protocols that define "length includes the header" use this setting.
Example 9: Decoding a layout with a header before the length
Some protocols put a version or type byte before the length. length_field_offset(1) tells the decoder to look for the length after one byte. By default the decoder drops the offset bytes and the length field. num_skip(0) keeps them, so the caller sees the whole frame.
use bytes::BytesMut;
use tokio_util::codec::{Decoder, LengthDelimitedCodec};
fn main() {
let wire = [0x07, 0x00, 0x03, b'a', b'b', b'c', 0x07, 0x00, 0x01, b'z'];
let mut default_skip = LengthDelimitedCodec::builder()
.length_field_offset(1)
.length_field_length(2)
.new_codec();
let mut buf = BytesMut::from(&wire[..]);
while let Some(frame) = default_skip.decode(&mut buf).unwrap() {
println!("default skip: {:?}", &frame[..]);
}
let mut keep_header = LengthDelimitedCodec::builder()
.length_field_offset(1)
.length_field_length(2)
.num_skip(0)
.new_codec();
let mut buf = BytesMut::from(&wire[..]);
while let Some(frame) = keep_header.decode(&mut buf).unwrap() {
println!("num_skip(0): {:02x?}", &frame[..]);
}
}A Decoder is an ordinary trait, so decode can be called directly on a BytesMut, without any I/O. That is also the way to unit-test a codec, as Example 12 shows.
Example 10: The maximum frame length
max_frame_length(n) is the defence from Bytes and Framing. The decoder compares the length field with the limit as soon as the header is readable and returns an io::Error with kind InvalidData if it is larger, before waiting for the payload. The encoder rejects payloads over the limit with kind InvalidInput.
use bytes::{Bytes, BytesMut};
use tokio_util::codec::{Decoder, Encoder, LengthDelimitedCodec};
fn main() {
let mut codec = LengthDelimitedCodec::builder().max_frame_length(16).new_codec();
println!("limit: {}", codec.max_frame_length());
let mut ok = BytesMut::from(&[0, 0, 0, 2, b'h', b'i'][..]);
println!("small: {:?}", codec.decode(&mut ok).unwrap());
let mut hostile = BytesMut::from(&[0xFF, 0xFF, 0xFF, 0xFF][..]);
let err = codec.decode(&mut hostile).unwrap_err();
println!("hostile header: kind={:?} message={err}", err.kind());
let mut dst = BytesMut::new();
let err = codec.encode(Bytes::from(vec![0u8; 17]), &mut dst).unwrap_err();
println!("oversized encode: kind={:?} message={err}", err.kind());
println!("bytes written: {}", dst.len());
}After the decoder accepts a header, it reserves room for the payload in the buffer. With the default limit, one connection can therefore make the server reserve up to 8 MiB. Servers with many connections lower the limit to the largest legitimate message.
Example 11: Writing a custom codec
A custom codec implements Decoder and Encoder<Item>. Decoder needs an Item type, an Error type that implements From<io::Error>, and decode(&mut self, src: &mut BytesMut) -> Result<Option<Item>, Error>. The contract:
| Situation | Return | Buffer |
|---|---|---|
| not enough bytes for an item | Ok(None) |
leave unchanged (or call reserve) |
| a complete item | Ok(Some(item)) |
remove exactly the item's bytes |
| invalid data | Err(error) |
anything |
Framed calls decode repeatedly after each read until it returns Ok(None). Encoder<Item> needs an Error type that implements From<io::Error> and encode(&mut self, item: Item, dst: &mut BytesMut).
The protocol here has three commands. Each starts with a tag byte. Get (tag 1) has a 1-byte key length and the key. Set (tag 2) has a key the same way, then a 2-byte big-endian value length and the value. Quit (tag 3) has no fields. The value length is checked against a limit as soon as it is readable.
use bytes::{Buf, BufMut, BytesMut};
use std::io;
use tokio_util::codec::{Decoder, Encoder};
const MAX_VALUE: usize = 1024;
#[derive(Debug, PartialEq)]
enum Command {
Get(String),
Set(String, String),
Quit,
}
#[derive(Debug)]
enum CodecError {
Io(io::Error),
UnknownTag(u8),
ValueTooLarge(usize),
KeyTooLong(usize),
BadUtf8,
}
impl From<io::Error> for CodecError {
fn from(e: io::Error) -> Self {
CodecError::Io(e)
}
}
fn utf8(bytes: &[u8]) -> Result<String, CodecError> {
std::str::from_utf8(bytes)
.map(str::to_owned)
.map_err(|_| CodecError::BadUtf8)
}
struct CommandCodec;
impl Decoder for CommandCodec {
type Item = Command;
type Error = CodecError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Command>, CodecError> {
let Some(&tag) = src.first() else {
return Ok(None);
};
match tag {
3 => {
src.advance(1);
Ok(Some(Command::Quit))
}
1 => {
if src.len() < 2 {
return Ok(None);
}
let key_len = src[1] as usize;
if src.len() < 2 + key_len {
return Ok(None);
}
let key = utf8(&src[2..2 + key_len])?;
src.advance(2 + key_len);
Ok(Some(Command::Get(key)))
}
2 => {
if src.len() < 2 {
return Ok(None);
}
let key_len = src[1] as usize;
let value_head = 2 + key_len;
if src.len() < value_head + 2 {
return Ok(None);
}
let value_len = u16::from_be_bytes([src[value_head], src[value_head + 1]]) as usize;
if value_len > MAX_VALUE {
return Err(CodecError::ValueTooLarge(value_len));
}
let total = value_head + 2 + value_len;
if src.len() < total {
src.reserve(total - src.len());
return Ok(None);
}
let key = utf8(&src[2..value_head])?;
let value = utf8(&src[value_head + 2..total])?;
src.advance(total);
Ok(Some(Command::Set(key, value)))
}
other => Err(CodecError::UnknownTag(other)),
}
}
}
impl Encoder<Command> for CommandCodec {
type Error = CodecError;
fn encode(&mut self, item: Command, dst: &mut BytesMut) -> Result<(), CodecError> {
match item {
Command::Quit => dst.put_u8(3),
Command::Get(key) => {
let key_len = u8::try_from(key.len()).map_err(|_| CodecError::KeyTooLong(key.len()))?;
dst.put_u8(1);
dst.put_u8(key_len);
dst.put_slice(key.as_bytes());
}
Command::Set(key, value) => {
let key_len = u8::try_from(key.len()).map_err(|_| CodecError::KeyTooLong(key.len()))?;
if value.len() > MAX_VALUE {
return Err(CodecError::ValueTooLarge(value.len()));
}
dst.put_u8(2);
dst.put_u8(key_len);
dst.put_slice(key.as_bytes());
dst.put_u16(value.len() as u16);
dst.put_slice(value.as_bytes());
}
}
Ok(())
}
}
fn main() {
let mut codec = CommandCodec;
let mut wire = BytesMut::new();
codec.encode(Command::Set("color".into(), "blue".into()), &mut wire).unwrap();
codec.encode(Command::Get("color".into()), &mut wire).unwrap();
codec.encode(Command::Quit, &mut wire).unwrap();
println!("{} bytes: {:02x?}", wire.len(), &wire[..]);
while let Some(cmd) = codec.decode(&mut wire).unwrap() {
println!("{cmd:?}");
}
println!("left over: {}", wire.len());
let long_key = "k".repeat(300);
let mut dst = BytesMut::new();
println!("{:?}", codec.encode(Command::Get(long_key), &mut dst));
}decode never consumes bytes until a whole command is present, so the same Set can be retried after more bytes arrive. Checking the value length before waiting for the value keeps a hostile length from reserving memory.
Example 12: Testing a codec against split input
Because decode takes a plain buffer, a test can feed the same bytes cut at every possible position and check that the decoded commands are always the same. This is the split-invariance property from Bytes and Framing. The codec from Example 11 appears again in shortened form so the program is complete.
use bytes::{Buf, BufMut, BytesMut};
use std::io;
use tokio_util::codec::Decoder;
#[derive(Debug, PartialEq, Clone)]
struct Pair {
key: String,
value: String,
}
struct PairCodec;
impl Decoder for PairCodec {
type Item = Pair;
type Error = io::Error;
// Wire: key_len u8, key, value_len u8, value.
fn decode(&mut self, src: &mut BytesMut) -> io::Result<Option<Pair>> {
let Some(&key_len) = src.first() else {
return Ok(None);
};
let key_len = key_len as usize;
let Some(&value_len) = src.get(1 + key_len) else {
return Ok(None);
};
let total = 2 + key_len + value_len as usize;
if src.len() < total {
return Ok(None);
}
let key = String::from_utf8_lossy(&src[1..1 + key_len]).into_owned();
let value = String::from_utf8_lossy(&src[2 + key_len..total]).into_owned();
src.advance(total);
Ok(Some(Pair { key, value }))
}
}
fn main() {
let mut wire = BytesMut::new();
for (k, v) in [("a", "1"), ("name", "tokio"), ("", "empty key")] {
wire.put_u8(k.len() as u8);
wire.put_slice(k.as_bytes());
wire.put_u8(v.len() as u8);
wire.put_slice(v.as_bytes());
}
let decode_all = |pieces: Vec<&[u8]>| -> Vec<Pair> {
let mut codec = PairCodec;
let mut buf = BytesMut::new();
let mut out = Vec::new();
for piece in pieces {
buf.extend_from_slice(piece);
while let Some(pair) = codec.decode(&mut buf).unwrap() {
out.push(pair);
}
}
assert!(buf.is_empty());
out
};
let expected = decode_all(vec![&wire[..]]);
println!("whole buffer: {} pairs", expected.len());
let mut splits_checked = 0;
for at in 0..=wire.len() {
let (left, right) = wire.split_at(at);
assert_eq!(decode_all(vec![left, right]), expected);
splits_checked += 1;
}
for size in 1..=wire.len() {
assert_eq!(decode_all(wire.chunks(size).collect()), expected);
splits_checked += 1;
}
println!("{splits_checked} different splits gave identical results");
println!("{:?}", expected[2]);
}The two loops cover every two-piece split and every fixed chunk size from 1 byte up to the whole message. A decoder that consumes bytes before a frame is complete, or misjudges a boundary, fails one of these assertions.
Example 13: Errors end the stream, and EOF in the middle of a frame
When decode returns Err, FramedRead yields that error as an item and then returns None on the next poll. Code that loops while let Some(item) = framed.next().await sees the error item first, so it must handle the Err and stop. When the transport ends with bytes still in the buffer, decode_eof runs. Its default calls decode and, if bytes are left over, returns an io::Error with the message bytes remaining on stream.
use futures::StreamExt;
use tokio::io::{AsyncWriteExt, duplex};
use tokio_util::codec::{FramedRead, LengthDelimitedCodec};
#[tokio::main]
async fn main() {
let (mut w, r) = duplex(64);
w.write_all(&[0, 0, 0, 2, b'o', b'k', 0, 0, 0, 9, b'x']).await.unwrap();
drop(w);
let mut frames = FramedRead::new(r, LengthDelimitedCodec::new());
println!("1: {:?}", frames.next().await);
let second = frames.next().await.unwrap();
println!("2: kind={:?} message={}", second.as_ref().unwrap_err().kind(), second.unwrap_err());
println!("3: {:?}", frames.next().await.is_none());
let (mut w, r) = duplex(64);
w.write_all(&[0xFF, 0xFF, 0xFF, 0xFF]).await.unwrap();
let mut frames = FramedRead::new(r, LengthDelimitedCodec::new());
let err = frames.next().await.unwrap().unwrap_err();
println!("oversized header: {err}");
println!("after error: {:?}", frames.next().await.is_none());
}The first connection ends after a complete frame and a partial one. The partial one turns into the bytes remaining on stream error at end of input. In the second, the header claims 4,294,967,295 bytes. The default limit is 8 MiB, so the decoder errors on the header alone. An error from the decoder is not recoverable in general, because the position of the next frame is unknown, so a server closes the connection after the first error.
Example 14: A TCP server with Framed
Everything combines in a server. Framed<TcpStream, LengthDelimitedCodec> gives frames in and frames out. The server binds port 0 and the client reads the address from the listener. Only the frame contents are printed, so the output does not depend on the port.
use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use tokio::net::{TcpListener, TcpStream};
use tokio_util::codec::{Framed, LengthDelimitedCodec};
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (socket, _) = listener.accept().await.unwrap();
let mut framed = Framed::new(socket, LengthDelimitedCodec::new());
while let Some(frame) = framed.next().await {
let frame = frame.unwrap();
let reply = format!("echo {} bytes: {}", frame.len(), String::from_utf8_lossy(&frame));
framed.send(Bytes::from(reply)).await.unwrap();
}
});
let socket = TcpStream::connect(addr).await.unwrap();
let mut framed = Framed::new(socket, LengthDelimitedCodec::new());
for message in ["one", "two words", ""] {
framed.send(Bytes::from(message)).await.unwrap();
let reply = framed.next().await.unwrap().unwrap();
println!("{}", String::from_utf8_lossy(&reply));
}
drop(framed);
server.await.unwrap();
println!("server saw the client disconnect");
}Each send writes a frame and each next waits for a whole reply frame. The empty message is a valid frame with a zero-length payload. When the client drops its Framed, the socket closes and the server's next() returns None.
Example 15: Backpressure in the write buffer
FramedWrite and Framed keep an internal write buffer. feed adds an encoded item to it and does not touch the transport until the buffer reaches the backpressure boundary, which is 8192 bytes by default (INITIAL_CAPACITY in the tokio-util source). At that point poll_ready flushes first, so a caller that feeds faster than the transport accepts is made to wait. set_backpressure_boundary changes the boundary.
The transport in the example is a duplex whose reader does not read, so it stops accepting data after its own 16-byte buffer is full.
use bytes::Bytes;
use futures::SinkExt;
use std::time::Duration;
use tokio::io::duplex;
use tokio_util::codec::{FramedWrite, LengthDelimitedCodec};
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let (writer, _reader_that_never_reads) = duplex(16);
let mut sink = FramedWrite::new(writer, LengthDelimitedCodec::new());
println!("default boundary: {}", sink.backpressure_boundary());
sink.set_backpressure_boundary(32);
let mut fed = 0;
let outcome = tokio::time::timeout(Duration::from_secs(1), async {
loop {
sink.feed(Bytes::from_static(b"0123456789")).await.unwrap();
fed += 1;
}
})
.await;
println!("feed calls that completed: {fed}");
println!("stalled: {}", outcome.is_err());
println!("buffered in sink: {} bytes", sink.write_buffer().len());
}Each frame is 14 bytes. Feeding continues without touching the transport until the buffer holds 32 bytes or more. Then feed must flush, the flush writes what the 16-byte duplex accepts, and it waits for a reader that never reads. The timeout cancels the stalled feed. A producer awaiting feed therefore slows to the speed of the peer, and memory use stays near the boundary plus one frame.
Example 16: Switching codecs mid-connection
Some protocols begin with a text handshake and continue in binary. Framed::map_codec swaps the codec and keeps the transport and the read buffer, including bytes already received. Here the first line arrives together with a length-prefixed frame in one write.
use futures::StreamExt;
use tokio::io::{AsyncWriteExt, duplex};
use tokio_util::codec::{Framed, LengthDelimitedCodec, LinesCodec};
#[tokio::main]
async fn main() {
let (mut peer, io) = duplex(256);
peer.write_all(b"HELLO v1\n\x00\x00\x00\x03abc").await.unwrap();
drop(peer);
let mut framed = Framed::new(io, LinesCodec::new());
let greeting = framed.next().await.unwrap().unwrap();
println!("greeting: {greeting:?}");
println!("already buffered: {} bytes", framed.read_buffer().len());
let mut framed = framed.map_codec(|_lines| LengthDelimitedCodec::new());
let frame = framed.next().await.unwrap().unwrap();
println!("frame: {:?}", frame);
println!("end: {:?}", framed.next().await.is_none());
}into_parts() is the lower-level form: it returns the transport, the codec, and both buffers as a FramedParts value, and Framed::from_parts rebuilds a Framed from it. TLS upgrades use this to move a plain connection into an encrypted one without losing buffered bytes.
Example 17: StreamReader and ReaderStream
Two adapters convert between the stream world and the byte-reader world:
| Adapter | From | To |
|---|---|---|
tokio_util::io::StreamReader |
a Stream of Result<impl Buf, io::Error> |
AsyncRead (and AsyncBufRead) |
tokio_util::io::ReaderStream |
an AsyncRead |
a Stream of Result<Bytes, io::Error> |
StreamReader lets code that expects an AsyncRead (such as read_to_string or a LinesCodec FramedRead) consume chunks that arrive from a channel or an HTTP body. ReaderStream does the reverse: it reads in chunks of up to its capacity (4096 bytes by default; with_capacity sets it) and yields each chunk as Bytes.
use bytes::Bytes;
use futures::StreamExt;
use std::io;
use tokio::io::AsyncReadExt;
use tokio_util::io::{ReaderStream, StreamReader};
#[tokio::main]
async fn main() {
let chunks = futures::stream::iter([
Ok::<_, io::Error>(Bytes::from_static(b"hel")),
Ok(Bytes::from_static(b"lo, ")),
Ok(Bytes::from_static(b"stream")),
]);
let mut reader = StreamReader::new(chunks);
let mut text = String::new();
reader.read_to_string(&mut text).await.unwrap();
println!("StreamReader produced: {text:?}");
let source: &[u8] = b"0123456789";
let mut stream = ReaderStream::with_capacity(source, 4);
while let Some(chunk) = stream.next().await {
println!("chunk: {:?}", chunk.unwrap());
}
}Example 18: Codecs available in tokio-util
| Codec | Item | Framing |
|---|---|---|
LinesCodec |
String |
\n, optional maximum length |
AnyDelimiterCodec |
Bytes |
any of a set of delimiter bytes; named only here |
BytesCodec |
BytesMut |
none; whatever is buffered |
LengthDelimitedCodec |
BytesMut |
length prefix, configurable |
SyncIoBridge (in tokio_util::io) wraps an AsyncRead or AsyncWrite so that blocking code, such as a synchronous parser or hasher, can use it from a thread that is allowed to block. It is named here and not used, because it requires spawn_blocking (Blocking and CPU-Bound Work) and is outside this lesson's scope.
For typed messages on the wire (JSON, MessagePack, or a binary format), the usual arrangement is LengthDelimitedCodec for the frames and a serialization step on each payload, as in Example 17 of Bytes and Framing. A custom Decoder that also parses the payload merges the two steps.
Interview points
- A
DecoderreturnsOk(None)when it needs more bytes,Ok(Some(item))after removing an item's bytes, andErrfor invalid data.Framedcalls it in a loop after each read. Decoder::ErrorandEncoder::Errormust implementFrom<io::Error>, so transport errors and protocol errors share one error type.Framedis aStreamplus aSink.FramedReadis only aStream.FramedWriteis only aSink.split()divides aFramedfor use in two tasks.sendisfeedplusflush.feedalone buffers until the backpressure boundary (8192 bytes by default) is reached.- The stream yields the decoder's error and then ends. Servers close the connection after a codec error because the next frame boundary is unknown.
LengthDelimitedCodecdefaults: 4-byte big-endian length, 8 MiB maximum frame length. The decoder checks the limit on the header, before the payload arrives.LinesCodec::new()has no length limit. Usenew_with_max_lengthfor input from untrusted peers.decode_eofhandles the end of the transport. Its default reportsbytes remaining on streamwhen a partial frame is left.- A codec is a plain trait, so
decodecan be unit-tested on aBytesMutwith input cut at every position. StreamReaderturns a stream of chunks intoAsyncRead.ReaderStreamturnsAsyncReadinto a stream ofBytes.SyncIoBridgelets blocking code read an async source from a blocking thread.
Summary
tokio-util'scodecmodule separates framing (Decoder,Encoder) from I/O (Framed,FramedRead,FramedWrite).LinesCodec,BytesCodec, andLengthDelimitedCodeccover text lines, raw chunks, and length-prefixed frames.LengthDelimitedCodec::builder()sets field width, byte order, offset, adjustment, skipped bytes, and the maximum frame length.- A custom codec implements
decode(peek, check limits, consume only complete items) andencode(append to the destination buffer). - Codec errors are yielded once and end the stream. End of input with leftover bytes is an error.
- The write side buffers and applies backpressure at a configurable boundary;
SinkExt::sendflushes each item. map_codecandinto_partschange codecs on a live connection without losing buffered bytes.StreamReaderandReaderStreamconvert betweenStream<Item = Result<Bytes, _>>andAsyncRead.
