Lesson 18 of 44
AsyncRead and AsyncWrite
Every byte stream in Tokio (a TCP connection, a file, a pipe, a child process's output) is used through two traits: AsyncRead for reading bytes and AsyncWrite for writing bytes. This lesson covers both traits, the helper methods built on them, what a partial read is, how end of file (EOF) is reported, and how to test I/O code without a network by using in-memory readers and tokio::io::duplex.
| Item | Purpose |
|---|---|
AsyncRead, AsyncWrite |
the traits every async byte stream implements |
AsyncReadExt |
read, read_exact, read_to_end, read_to_string, read_u32 and other integer readers, take, chain |
AsyncWriteExt |
write, write_all, write_u32 and other integer writers, flush, shutdown |
BufReader, AsyncBufReadExt |
buffered reading, read_line, lines |
BufWriter |
buffered writing |
io::copy, io::split |
move all bytes from a reader to a writer, split a stream into two halves |
io::duplex |
an in-memory pair of connected streams |
&[u8], Cursor, Vec<u8> |
in-memory readers and writers |
io::empty, io::sink, io::repeat |
readers and writers with fixed behavior |
All examples need the io-util feature (included in full). Each example imports the extension traits it uses, because the helper methods exist only when the trait is in scope.
Example 1: Read from a byte slice
AsyncRead is implemented for &[u8], so a byte string can stand in for a network stream. read copies bytes into a buffer you provide and returns how many bytes it copied.
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut source: &[u8] = b"hello world";
let mut buf = [0u8; 32];
let n = source.read(&mut buf).await?;
println!("read {n} bytes: {:?}", std::str::from_utf8(&buf[..n]).unwrap());
Ok(())
}read 11 bytes: "hello world"read returns a usize inside an io::Result. The buffer is 32 bytes, the source holds 11, so one call read everything the source had.
Example 2: The two traits
AsyncRead and AsyncWrite each have poll methods, in the style of the Future trait from Futures and async/await. This block shows the core method of each; it is a reading aid, not a program to run.
pub trait AsyncRead {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>>;
}
pub trait AsyncWrite {
fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8])
-> Poll<io::Result<usize>>;
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>>;
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>>;
}ReadBuf is Tokio's wrapper around a byte buffer that tracks which part has been filled. The result of poll_read follows three cases from the trait documentation:
| Result | Meaning |
|---|---|
Ready(Ok(())) |
bytes were placed in the buffer. No new bytes means EOF, or a buffer with no free space. |
Pending |
nothing to read now. The waker is registered and the task is woken when the stream becomes readable. |
Ready(Err(e)) |
an I/O error |
Application code rarely calls poll_* directly. It calls the methods of AsyncReadExt and AsyncWriteExt, which return futures. Writing your own implementation is covered in Writing Futures and Streams by Hand.
AsyncWrite::poll_write returns how many bytes it accepted, which can be fewer than offered. poll_flush pushes buffered data toward the destination. poll_shutdown signals that no more data will be written.
Example 3: Partial reads
A single read call returns whatever is available, up to the buffer size. It does not fill the buffer. The call below uses a 4-byte buffer on 11 bytes of data, so it needs three calls.
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut source: &[u8] = b"hello world";
let mut buf = [0u8; 4];
for call in 1..=3 {
let n = source.read(&mut buf).await?;
println!("call {call}: {n} bytes {:?}", std::str::from_utf8(&buf[..n]).unwrap());
}
Ok(())
}call 1: 4 bytes "hell"
call 2: 4 bytes "o wo"
call 3: 3 bytes "rld"Here the limit is the buffer. On a network connection the limit is often the data that has arrived so far: a sender that writes 1,000 bytes can be read as 400 and then 600 bytes, or in any other split. Code that assumes one read returns one whole message is wrong even if it passes on a fast local machine.
Example 4: EOF is a read of zero bytes
When the source has no more data and will never have more, read returns Ok(0). This is EOF (end of file, or end of stream for a socket). A loop that reads until it sees 0 handles streams of any size.
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut source: &[u8] = b"abcdefghij";
let mut buf = [0u8; 4];
let mut total = 0;
loop {
let n = source.read(&mut buf).await?;
if n == 0 {
println!("EOF");
break;
}
total += n;
println!("chunk {:?}", std::str::from_utf8(&buf[..n]).unwrap());
}
println!("total {total}");
Ok(())
}chunk "abcd"
chunk "efgh"
chunk "ij"
EOF
total 10The chunks are 4, 4, and 2 bytes. The final chunk is shorter than the buffer, which is normal and does not signal EOF. Only the next call, returning 0, does.
Example 5: A zero result also comes from an empty buffer
Ok(0) has two causes: EOF, and a buffer with no room. A loop that passes an empty buffer would mistake it for EOF.
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut source: &[u8] = b"data";
let n = source.read(&mut []).await?;
println!("empty buffer: {n}");
let mut buf = [0u8; 8];
let n = source.read(&mut buf).await?;
println!("real buffer: {n}");
Ok(())
}empty buffer: 0
real buffer: 4The first call returned 0 even though 4 bytes were waiting, and it consumed nothing. Keep read buffers non-empty.
Example 6: read_exact and unexpected EOF
read_exact fills the whole buffer, calling read as many times as needed. If EOF arrives first, it returns an error of kind UnexpectedEof. This is the tool for fixed-size fields such as a 4-byte header.
use std::io::ErrorKind;
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() {
let mut source: &[u8] = b"0123456789";
let mut header = [0u8; 4];
source.read_exact(&mut header).await.unwrap();
println!("header {:?}", std::str::from_utf8(&header).unwrap());
let mut big = [0u8; 8];
let err = source.read_exact(&mut big).await.unwrap_err();
println!("kind {:?}", err.kind());
println!("is UnexpectedEof: {}", err.kind() == ErrorKind::UnexpectedEof);
println!("message: {err}");
}header "0123"
kind UnexpectedEof
is UnexpectedEof: true
message: early eofOnly 6 bytes remained for an 8-byte buffer. The error does not report how many bytes were read, and the contents of the buffer after an error are unspecified. Treat the bytes as lost and stop.
Example 7: read_to_end and read_to_string
read_to_end appends everything up to EOF to a Vec<u8> and returns the count. read_to_string does the same into a String and returns an InvalidData error if the bytes are not valid UTF-8.
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut source: &[u8] = b"first part, ";
let mut bytes = Vec::new();
let n = source.read_to_end(&mut bytes).await?;
println!("read_to_end: {n} bytes, vec len {}", bytes.len());
let mut text_source: &[u8] = b"line one\nline two\n";
let mut text = String::new();
let n = text_source.read_to_string(&mut text).await?;
println!("read_to_string: {n} bytes");
print!("{text}");
let mut bad: &[u8] = &[0xff, 0xfe, 0xfd];
let mut sink = String::new();
let err = bad.read_to_string(&mut sink).await.unwrap_err();
println!("invalid utf-8: {:?}", err.kind());
Ok(())
}read_to_end: 12 bytes, vec len 12
read_to_string: 18 bytes
line one
line two
invalid utf-8: InvalidDataBoth methods wait for EOF, so on a socket they return only after the peer closes its sending side. They also grow the buffer without a limit. The take adapter in Example 16 bounds the size.
Example 8: Reading and writing integers
AsyncReadExt has read_u8, read_u16, read_u32, read_u64, and signed and float versions, each reading a fixed number of bytes in big-endian order (most significant byte first, the usual order for network protocols). Each has a _le variant for little-endian. AsyncWriteExt has matching write_u32 and similar methods. These call read_exact internally, so a short stream gives UnexpectedEof.
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut out: Vec<u8> = Vec::new();
out.write_u8(7).await?;
out.write_u16(0x0102).await?;
out.write_u32(0xDEADBEEF).await?;
out.write_u16_le(0x0102).await?;
println!("{:02x?}", out);
let mut input: &[u8] = &out;
let a = input.read_u8().await?;
let b = input.read_u16().await?;
let c = input.read_u32().await?;
let d = input.read_u16_le().await?;
println!("{a} {b:#06x} {c:#x} {d:#06x}");
let err = input.read_u32().await.unwrap_err();
println!("after the end: {:?}", err.kind());
Ok(())
}[07, 01, 02, de, ad, be, ef, 02, 01]
7 0x0102 0xdeadbeef 0x0102
after the end: UnexpectedEofVec<u8> implements AsyncWrite, so it works as a writer that collects bytes. The 16-bit value 0x0102 is written as 01 02 in big-endian and 02 01 in little-endian.
Example 9: write, write_all, and partial writes
write returns how many bytes the destination accepted, which can be fewer than offered. write_all loops until every byte is accepted. tokio::io::duplex(n) creates two connected in-memory streams; bytes written to one end are read from the other, and each direction holds at most n bytes. With n = 4, a write of 10 bytes is accepted only in part.
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (mut a, mut b) = duplex(4);
let n = a.write(b"0123456789").await?;
println!("write accepted {n} of 10 bytes");
let mut buf = [0u8; 16];
let n = b.read(&mut buf).await?;
println!("peer read {:?}", std::str::from_utf8(&buf[..n]).unwrap());
let n = a.write(b"456789").await?;
println!("second write accepted {n} of 6 bytes");
Ok(())
}write accepted 4 of 10 bytes
peer read "0123"
second write accepted 4 of 6 bytesThe first write accepted 4 bytes, the capacity of the pipe. After the peer drained the pipe, space was free again. Code that calls write and ignores the returned count silently drops data. write_all exists to prevent that.
Example 10: Backpressure with write_all on a full pipe
When the pipe is full, write_all waits until the reader frees space. This is backpressure: a slow reader slows the writer instead of letting memory grow. A 4-byte pipe carrying 26 bytes needs the reader to run concurrently, so the reader runs in a spawned task (see Spawning Tasks).
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (mut tx, mut rx) = duplex(4);
let reader = tokio::spawn(async move {
let mut received = Vec::new();
rx.read_to_end(&mut received).await.unwrap();
received
});
tx.write_all(b"abcdefghijklmnopqrstuvwxyz").await?;
tx.shutdown().await?;
let received = reader.await.unwrap();
println!("{}", String::from_utf8(received).unwrap());
Ok(())
}abcdefghijklmnopqrstuvwxyzwrite_all returned only after the reader took all 26 bytes through a 4-byte window. Without the spawned reader, the writer would wait forever on the full pipe. shutdown (Example 12) is what lets read_to_end see EOF.
Example 11: Dropping one end of a pipe
If one end of a duplex pair is dropped, the other end can still read what was already written, then sees EOF. Writes to the surviving end fail with BrokenPipe. Sockets behave in a similar way (see TCP Clients and Servers).
use std::io::ErrorKind;
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (mut a, mut b) = duplex(64);
a.write_all(b"last words").await?;
drop(a);
let mut buf = [0u8; 32];
let n = b.read(&mut buf).await?;
println!("data before close: {:?}", std::str::from_utf8(&buf[..n]).unwrap());
let n = b.read(&mut buf).await?;
println!("then EOF: {n}");
let err = b.write_all(b"anyone there?").await.unwrap_err();
println!("write to closed peer: {:?}", err.kind());
println!("is BrokenPipe: {}", err.kind() == ErrorKind::BrokenPipe);
Ok(())
}data before close: "last words"
then EOF: 0
write to closed peer: BrokenPipe
is BrokenPipe: trueBuffered bytes are not lost when the writer drops. The reader drains them and then gets 0.
Example 12: flush and shutdown
flush asks the writer to push any buffered bytes to the destination. shutdown flushes and then closes the write direction, which the peer sees as EOF. After shutdown, the other direction still works: this is the half-close used by protocols where one side finishes sending and then waits for a reply.
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (mut client, mut server) = duplex(64);
client.write_all(b"request").await?;
client.flush().await?;
client.shutdown().await?;
let mut request = Vec::new();
server.read_to_end(&mut request).await?;
println!("server got {:?}", String::from_utf8(request).unwrap());
server.write_all(b"response").await?;
server.shutdown().await?;
let mut response = Vec::new();
client.read_to_end(&mut response).await?;
println!("client got {:?}", String::from_utf8(response).unwrap());
Ok(())
}server got "request"
client got "response"The server's read_to_end could finish because the client called shutdown. The client could still read the response afterwards because shutdown closes only the client's sending direction.
Example 13: BufReader and read_line
Reading a byte at a time from a slow source means many small reads. BufReader wraps a reader with an internal buffer (8 KiB by default), fills it with large reads, and serves small requests from memory. It also adds AsyncBufReadExt, whose read_line reads up to and including the next \n.
use tokio::io::{AsyncBufReadExt, BufReader};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let data: &[u8] = b"GET /index\nHost: local\n\nbody without newline";
let mut reader = BufReader::new(data);
let mut line = String::new();
loop {
line.clear();
let n = reader.read_line(&mut line).await?;
if n == 0 {
println!("EOF");
break;
}
println!("{n} bytes: {:?}", line);
}
Ok(())
}11 bytes: "GET /index\n"
12 bytes: "Host: local\n"
1 bytes: "\n"
20 bytes: "body without newline"
EOFread_line appends to the string, so the loop clears it each time. The line keeps its \n. The empty line returns 1 byte, not 0. The last line has no \n and is still returned, because EOF ends it. Only a return of 0 means EOF.
Example 14: lines
lines() turns a BufRead source into an async iterator: next_line().await returns Ok(Some(String)) per line, with the line ending removed, and Ok(None) at EOF.
use tokio::io::{AsyncBufReadExt, BufReader};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let data: &[u8] = b"alpha\nbeta\r\n\ngamma";
let mut lines = BufReader::new(data).lines();
while let Some(line) = lines.next_line().await? {
println!("{:?}", line);
}
println!("done");
Ok(())
}"alpha"
"beta"
""
"gamma"
doneBoth \n and \r\n are removed. The empty line comes back as an empty string, and the unterminated final line is still returned. lines has no maximum line length, so a peer that never sends \n makes the string grow until memory runs out. Bytes and Framing covers limits.
Example 15: BufWriter
BufWriter collects small writes in an 8 KiB buffer and passes them on in larger writes. The data reaches the inner writer only when the buffer fills, when flush is called, or when shutdown is called. Dropping a BufWriter does not flush it, because a drop cannot wait.
use tokio::io::{AsyncWriteExt, BufWriter};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut writer = BufWriter::new(Vec::<u8>::new());
writer.write_all(b"one ").await?;
writer.write_all(b"two ").await?;
println!("inner after two writes: {}", writer.get_ref().len());
writer.flush().await?;
println!("inner after flush: {}", writer.get_ref().len());
writer.write_all(b"three").await?;
let inner = writer.into_inner();
println!("inner after into_inner without flush: {}", inner.len());
Ok(())
}inner after two writes: 0
inner after flush: 8
inner after into_inner without flush: 8The inner Vec stayed empty until flush. into_inner returns the inner writer without flushing, so the 5 bytes of three were discarded. Always call flush or shutdown before finishing with a BufWriter. Many small writes to a socket are costly because each is a system call; BufWriter reduces the count.
Example 16: take and chain
take(n) limits a reader to at most n bytes and then reports EOF. chain(other) reads the first reader to EOF and then the second. Together they bound input and glue pieces.
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let source: &[u8] = b"0123456789";
let mut limited = source.take(4);
let mut out = String::new();
limited.read_to_string(&mut out).await?;
println!("take(4): {out}");
println!("limit left: {}", limited.limit());
let head: &[u8] = b"HEAD|";
let tail: &[u8] = b"tail";
let mut both = head.chain(tail);
let mut joined = String::new();
both.read_to_string(&mut joined).await?;
println!("chain: {joined}");
Ok(())
}take(4): 0123
limit left: 0
chain: HEAD|tailtake is the standard guard against a peer that sends too much: stream.take(MAX).read_to_end(&mut buf) never stores more than MAX bytes.
Example 17: io::copy
tokio::io::copy(&mut reader, &mut writer) reads from the reader and writes to the writer until the reader reaches EOF, and returns the number of bytes copied. At EOF it flushes the writer, but it does not shut the writer down.
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> io::Result<()> {
let mut source: &[u8] = b"copy these bytes";
let mut dest: Vec<u8> = Vec::new();
let copied = io::copy(&mut source, &mut dest).await?;
println!("copied {copied} bytes: {:?}", String::from_utf8(dest.clone()).unwrap());
let (mut a, mut b) = io::duplex(8);
let writer = tokio::spawn(async move {
let mut data: &[u8] = b"through a small pipe";
let n = io::copy(&mut data, &mut a).await.unwrap();
a.shutdown().await.unwrap();
n
});
let mut received = String::new();
b.read_to_string(&mut received).await?;
println!("received {received:?}, writer copied {}", writer.await.unwrap());
Ok(())
}copied 16 bytes: "copy these bytes"
received "through a small pipe", writer copied 20The second half copies 20 bytes through an 8-byte pipe, with the reader running at the same time. The writer task calls shutdown after copy because copy only flushes, and without the shutdown read_to_string would wait forever.
Example 18: io::split
A stream that is both AsyncRead and AsyncWrite can be split into a ReadHalf and a WriteHalf with tokio::io::split. Each half can move into its own task, so one task reads while another writes. The halves synchronize internally, which costs a lock; TCP streams have a cheaper split, covered in TCP Clients and Servers.
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> io::Result<()> {
let (client, server) = io::duplex(64);
let (mut server_read, mut server_write) = io::split(server);
// The server echoes every byte back, converted to upper case.
let echo = tokio::spawn(async move {
let mut buf = [0u8; 16];
loop {
let n = server_read.read(&mut buf).await.unwrap();
if n == 0 {
break;
}
buf[..n].make_ascii_uppercase();
server_write.write_all(&buf[..n]).await.unwrap();
}
server_write.shutdown().await.unwrap();
});
let (mut client_read, mut client_write) = io::split(client);
client_write.write_all(b"hello halves").await?;
client_write.shutdown().await?;
let mut reply = String::new();
client_read.read_to_string(&mut reply).await?;
println!("{reply}");
echo.await.unwrap();
Ok(())
}HELLO HALVESThis is the shape of an echo server: read until 0, write back, then close. The client shut down its write half so the server saw EOF, and the server shut down its write half so the client saw EOF.
Example 19: empty, sink, and repeat
Three fixed streams help in tests and tools:
| Function | Behavior |
|---|---|
io::empty() |
reads return EOF at once, writes discard the data and report success |
io::sink() |
writes discard the data and report success |
io::repeat(byte) |
reads an endless supply of one byte |
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> io::Result<()> {
let mut empty = io::empty();
let mut buf = Vec::new();
println!("empty: {} bytes", empty.read_to_end(&mut buf).await?);
let mut sink = io::sink();
sink.write_all(b"vanishes").await?;
println!("sink accepted the write");
let mut zeros = io::repeat(0).take(5);
let mut five = Vec::new();
zeros.read_to_end(&mut five).await?;
println!("repeat.take(5): {:?}", five);
let copied = io::copy(&mut io::repeat(b'x').take(1000), &mut io::sink()).await?;
println!("copied {copied} bytes into a sink");
Ok(())
}empty: 0 bytes
sink accepted the write
repeat.take(5): [0, 0, 0, 0, 0]
copied 1000 bytes into a sinkrepeat never reaches EOF, so it is always paired with take. Copying into a sink measures how many bytes a reader produces.
Example 20: Cursor as a reader and writer
std::io::Cursor wraps a buffer with a position. Tokio implements AsyncRead for Cursor<T: AsRef<[u8]>> and AsyncWrite for Cursor<Vec<u8>> and Cursor<&mut [u8]>. Unlike &[u8], a cursor keeps the original data and lets you read its position.
use std::io::Cursor;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut reader = Cursor::new(b"abcdef".to_vec());
let mut two = [0u8; 2];
reader.read_exact(&mut two).await?;
println!("after 2 bytes: position {}", reader.position());
reader.set_position(4);
let mut rest = String::new();
reader.read_to_string(&mut rest).await?;
println!("from position 4: {rest}");
println!("data still there: {:?}", reader.get_ref());
let mut writer = Cursor::new(Vec::new());
writer.write_all(b"written").await?;
println!("wrote {} bytes: {:?}", writer.position(), writer.get_ref());
Ok(())
}after 2 bytes: position 2
from position 4: ef
data still there: [97, 98, 99, 100, 101, 102]
wrote 7 bytes: [119, 114, 105, 116, 116, 101, 110]&[u8] advances by shrinking the slice, so the consumed bytes are gone from the variable. Cursor moves an index instead.
Example 21: Functions generic over the traits
A function that accepts impl AsyncRead + Unpin works with a socket, a file, a duplex end, or a byte slice. Unpin is the marker that lets the helper methods take &mut self without pinning; sockets, files, &[u8], Cursor, and duplex ends are all Unpin. This is what makes I/O code testable: the test passes bytes, the service passes a socket.
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, AsyncBufReadExt};
// Reads lines until EOF. Returns how many lines it saw and the longest one.
async fn summarize<R: AsyncRead + Unpin>(input: R) -> std::io::Result<(usize, usize)> {
let mut lines = BufReader::new(input).lines();
let (mut count, mut longest) = (0, 0);
while let Some(line) = lines.next_line().await? {
count += 1;
longest = longest.max(line.len());
}
Ok((count, longest))
}
// Writes a length-prefixed message: a u32 length, then the payload.
async fn write_message<W: AsyncWrite + Unpin>(out: &mut W, payload: &[u8]) -> std::io::Result<()> {
out.write_u32(payload.len() as u32).await?;
out.write_all(payload).await?;
out.flush().await
}
// Reads one length-prefixed message.
async fn read_message<R: AsyncRead + Unpin>(input: &mut R) -> std::io::Result<Vec<u8>> {
let len = input.read_u32().await? as usize;
let mut payload = vec![0u8; len];
input.read_exact(&mut payload).await?;
Ok(payload)
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let stats = summarize(&b"one\ntwo words\nthree"[..]).await?;
println!("lines={} longest={}", stats.0, stats.1);
let (mut a, mut b) = tokio::io::duplex(1024);
write_message(&mut a, b"first").await?;
write_message(&mut a, b"second message").await?;
drop(a);
println!("{:?}", String::from_utf8(read_message(&mut b).await?).unwrap());
println!("{:?}", String::from_utf8(read_message(&mut b).await?).unwrap());
let end = read_message(&mut b).await.unwrap_err();
println!("third read: {:?}", end.kind());
Ok(())
}lines=3 longest=9
"first"
"second message"
third read: UnexpectedEofread_message uses read_u32 and read_exact because both wait for the full field, however many partial reads that takes. At a clean end of stream, read_u32 fails with UnexpectedEof. A real protocol reader treats that error at a message boundary as "the peer finished" and the same error in the middle of a message as a truncated message.
Example 22: A reader that returns data in small pieces
A spawned task writes three pieces into a duplex pipe, one after another. The reader sees the data split by the writer's writes and by its own buffer size. A parser that works only when the whole message arrives in one read would fail here.
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
use tokio::task::yield_now;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (mut tx, mut rx) = duplex(64);
let writer = tokio::spawn(async move {
for piece in ["HEL", "LO, ", "WORLD"] {
tx.write_all(piece.as_bytes()).await.unwrap();
yield_now().await;
}
});
let mut message = Vec::new();
let mut buf = [0u8; 64];
loop {
let n = rx.read(&mut buf).await?;
if n == 0 {
break;
}
message.extend_from_slice(&buf[..n]);
}
writer.await.unwrap();
println!("message {:?}", String::from_utf8(message).unwrap());
Ok(())
}message "HELLO, WORLD"The number of reads depends on scheduling, so the example prints only the assembled message. This is how to write tests for I/O code. Assert on the reassembled bytes, never on how many read calls it took.
Example 23: A line-based protocol handler, tested in memory
The handler reads commands line by line and writes a reply per command. Because it takes AsyncRead + AsyncWrite, the same function serves a socket in TCP Clients and Servers and a duplex end here.
use tokio::io::{
duplex, split, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader,
};
async fn serve<S: AsyncRead + AsyncWrite + Unpin>(stream: S) -> std::io::Result<()> {
let (read_half, mut write_half) = split(stream);
let mut lines = BufReader::new(read_half).lines();
let mut total: i64 = 0;
while let Some(line) = lines.next_line().await? {
let reply = match line.split_once(' ') {
Some(("ADD", n)) => match n.parse::<i64>() {
Ok(n) => {
total += n;
format!("OK {total}")
}
Err(_) => "ERR not a number".to_string(),
},
None if line == "TOTAL" => format!("TOTAL {total}"),
_ => "ERR unknown command".to_string(),
};
write_half.write_all(reply.as_bytes()).await?;
write_half.write_all(b"\n").await?;
}
write_half.shutdown().await
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (mut client, server) = duplex(256);
let handler = tokio::spawn(serve(server));
client.write_all(b"ADD 5\nADD 7\nADD x\nHELLO\nTOTAL\n").await?;
client.shutdown().await?;
let mut replies = String::new();
client.read_to_string(&mut replies).await?;
print!("{replies}");
handler.await.unwrap()?;
Ok(())
}OK 5
OK 12
ERR not a number
ERR unknown command
TOTAL 12The client wrote all five commands at once and closed its sending side. The handler processed them in order and, after EOF, closed its own side, which ended read_to_string. Replies are in command order because a single task handles the stream.
Interview points
readreturnsOk(n)with0 < n <= buf.len(). It does not fill the buffer, and it does not preserve message boundaries.Ok(0)means EOF unless the buffer was empty.read_exactreturnsUnexpectedEofif the stream ends first.read_to_endandread_to_stringwait for EOF and have no size limit.take(n)adds one.writemay accept fewer bytes than offered.write_allloops. Neither flushes a buffering writer.flushpushes buffered bytes onward.shutdownflushes and closes the write direction, which the peer sees as EOF. Dropping aBufWriterdoes not flush.BufReaderreduces the number of reads on the inner source, andlinesandread_lineneed it (or anotherAsyncBufRead).read_linekeeps the newline andlinesremoves it. Neither limits the line length.- Integer helpers such as
read_u32andwrite_u32are big-endian._levariants use little-endian. io::copyruns until the reader reaches EOF, flushes the writer, and does not shut it down. It uses an 8 KiB buffer.- Cancel safety (from the method documentation):
read,write, andflushare cancel safe.read_exact,write_all, and the multi-byte integer readers are not, because part of the data may already be consumed or written when the future is dropped. select!: Racing Futures defines cancel safety. io::splitworks for anyAsyncRead + AsyncWriteand uses an internal lock.duplex(n)buffersnbytes per direction; a full pipe makes the writer wait; a dropped end gives the peer EOF after the buffered data, and gives writesBrokenPipe.- Generic functions over
AsyncRead + UnpinandAsyncWrite + Unpincan be tested with&[u8],Cursor, andduplexwithout sockets.
Summary
AsyncReadandAsyncWriteare the poll-based traits for byte streams.AsyncReadExtandAsyncWriteExtsupply the methods used in application code.- A read can return fewer bytes than requested at any time. EOF is a read that returns 0 into a non-empty buffer.
- Use
read_exactfor fixed-size fields andwrite_allfor complete writes. - Wrap slow or small-grained sources in
BufReader, and small writes inBufWriterwith an explicitflush. tokio::io::duplex,&[u8], andCursormake I/O code deterministic to test; assert on the reassembled bytes, not on the number of reads.
