Tokio provides everything you need to perform input and output asynchronously. Unlike synchronous I/O that blocks the thread, Tokio’s async I/O yields to the scheduler when I/O is not ready, allowing other tasks to run.
Core I/O traits
Tokio defines asynchronous versions of the standard library’s I/O traits:
These traits are always available and don’t require any feature flags.
Extension traits
Utility methods are provided through extension traits:
These traits are automatically implemented for all types that implement the corresponding base trait.
Reading files
Reading a file asynchronously is similar to the standard library:
use tokio::io::{self, AsyncReadExt};
use tokio::fs::File;
#[tokio::main]
async fn main() -> io::Result<()> {
let mut f = File::open("foo.txt").await?;
let mut buffer = [0; 10];
// Read up to 10 bytes
let n = f.read(&mut buffer).await?;
println!("The bytes: {:?}", &buffer[..n]);
Ok(())
}
Notice the .await after I/O operations. This is where the task yields to the runtime if I/O is not ready.
Writing files
Writing to files works similarly:
use tokio::io::{self, AsyncWriteExt};
use tokio::fs::File;
#[tokio::main]
async fn main() -> io::Result<()> {
let mut file = File::create("output.txt").await?;
file.write_all(b"Hello, world!").await?;
file.flush().await?;
Ok(())
}
Buffered I/O
Buffered readers and writers reduce system calls:
BufReader
use tokio::io::{self, BufReader, AsyncBufReadExt};
use tokio::fs::File;
#[tokio::main]
async fn main() -> io::Result<()> {
let f = File::open("foo.txt").await?;
let mut reader = BufReader::new(f);
let mut buffer = String::new();
// Read a line into buffer
reader.read_line(&mut buffer).await?;
println!("First line: {}", buffer);
Ok(())
}
BufWriter
use tokio::io::{self, BufWriter, AsyncWriteExt};
use tokio::fs::File;
#[tokio::main]
async fn main() -> io::Result<()> {
let f = File::create("output.txt").await?;
let mut writer = BufWriter::new(f);
// Write a byte to the buffer
writer.write(&[42u8]).await?;
// Flush before dropping
writer.flush().await?;
Ok(())
}
Always flush BufWriter before it goes out of scope. Buffered data is discarded on drop if not flushed.
Network I/O
Tokio provides async versions of TCP, UDP, and Unix domain sockets.
TCP server
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, _) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = [0; 1024];
loop {
let n = match socket.read(&mut buf).await {
Ok(0) => return, // Connection closed
Ok(n) => n,
Err(e) => {
eprintln!("failed to read: {:?}", e);
return;
}
};
// Echo the data back
if let Err(e) = socket.write_all(&buf[0..n]).await {
eprintln!("failed to write: {:?}", e);
return;
}
}
});
}
}
TCP client
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
// Write some data
stream.write_all(b"hello world").await?;
// Read the response
let mut buffer = [0; 1024];
let n = stream.read(&mut buffer).await?;
println!("Received: {:?}", &buffer[..n]);
Ok(())
}
UDP sockets
use tokio::net::UdpSocket;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let socket = UdpSocket::bind("127.0.0.1:8080").await?;
let mut buf = [0; 1024];
loop {
let (len, addr) = socket.recv_from(&mut buf).await?;
println!("Received {} bytes from {}", len, addr);
// Echo back
socket.send_to(&buf[..len], addr).await?;
}
}
Unix domain sockets
On Unix platforms, Tokio supports domain sockets:
use tokio::net::{UnixListener, UnixStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = UnixListener::bind("/tmp/tokio.sock")?;
loop {
let (mut socket, _) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = [0; 1024];
match socket.read(&mut buf).await {
Ok(n) if n > 0 => {
socket.write_all(&buf[..n]).await.ok();
}
_ => {}
}
});
}
}
Standard I/O
Tokio provides async versions of stdin, stdout, and stderr:
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
#[tokio::main]
async fn main() -> io::Result<()> {
let mut stdout = io::stdout();
let stdin = io::stdin();
let mut reader = BufReader::new(stdin);
stdout.write_all(b"Enter your name: ").await?;
stdout.flush().await?;
let mut name = String::new();
reader.read_line(&mut name).await?;
stdout.write_all(format!("Hello, {}!", name.trim()).as_bytes()).await?;
Ok(())
}
Standard I/O functions must be called from within a Tokio runtime. They will panic if called outside the runtime.
Filesystem operations
The tokio::fs module provides async filesystem operations:
use tokio::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Read entire file
let contents = fs::read_to_string("foo.txt").await?;
// Write entire file
fs::write("bar.txt", b"Hello, world!").await?;
// Copy file
fs::copy("foo.txt", "foo_copy.txt").await?;
// Remove file
fs::remove_file("bar.txt").await?;
// Create directory
fs::create_dir("my_dir").await?;
// Read directory
let mut entries = fs::read_dir(".").await?;
while let Some(entry) = entries.next_entry().await? {
println!("Found: {:?}", entry.path());
}
Ok(())
}
Splitting readers and writers
Many I/O types can be split into separate read and write halves:
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let stream = TcpStream::connect("127.0.0.1:8080").await?;
let (mut reader, mut writer) = stream.into_split();
// Spawn a task for reading
tokio::spawn(async move {
let mut buf = [0; 1024];
loop {
match reader.read(&mut buf).await {
Ok(0) => break,
Ok(n) => println!("Read {} bytes", n),
Err(e) => {
eprintln!("Error: {:?}", e);
break;
}
}
}
});
// Write from the current task
writer.write_all(b"hello").await?;
Ok(())
}
Copy utilities
Tokio provides utilities for copying data:
use tokio::io;
use tokio::fs::File;
#[tokio::main]
async fn main() -> io::Result<()> {
let mut reader = File::open("input.txt").await?;
let mut writer = File::create("output.txt").await?;
// Copy all bytes
io::copy(&mut reader, &mut writer).await?;
Ok(())
}
How async I/O works
When you call an async I/O operation:
- If data is immediately available, the operation completes without yielding
- If I/O would block, the task yields to the runtime
- The runtime registers interest in the I/O event with the OS
- When the OS signals that I/O is ready, the task is woken
- The task is scheduled and the I/O operation completes
This is powered by:
- I/O driver - Backed by the OS event queue (
epoll on Linux, kqueue on macOS/BSD, IOCP on Windows)
- Waker system - Notifies the runtime when I/O is ready
Tokio’s I/O is zero-cost: there’s no overhead when I/O is immediately ready.
Feature flags
net - Enables TcpStream, TcpListener, UdpSocket, and Unix sockets
fs - Enables filesystem operations
io-util - Enables I/O utility traits and functions
io-std - Enables stdin, stdout, and stderr
- Runtime - Understanding the I/O driver
- Tasks - Spawning concurrent I/O operations