Tokio / Chapter 3 of 6

I/O and networking

Read and buffer files with Tokio, then build a TCP echo server and client. Rust examples use AsyncReadExt, AsyncWriteExt, and TcpListener.

2 topicsUpdated
12 / 26

Files, byte streams, and buffering#

Async file and socket APIs follow AsyncRead/AsyncWrite. Import extension traits for convenient methods.

Tokio file operations often use blocking threads under the hood. They let your async task wait without occupying a runtime worker, but they do not turn disk hardware into truly nonblocking I/O. read_to_string loads the entire file; for large streams, use bounded buffers or line iteration.

RustRead and write a small file
use tokio::{fs, io};

#[tokio::main]
async fn main() -> io::Result<()> {
    fs::write("message.txt", "hello\n").await?;
    let text = fs::read_to_string("message.txt").await?;
    println!("{}", text.trim_end());
    fs::remove_file("message.txt").await?;
    Ok(())
}
RustCopy without reading everything into memory
use tokio::{fs::File, io};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut source = File::open("input.bin").await?;
    let mut target = File::create("output.bin").await?;
    let bytes = io::copy(&mut source, &mut target).await?;
    println!("copied {bytes} bytes");
    Ok(())
}
RustRead lines from standard input
use tokio::io::{self, AsyncBufReadExt, BufReader};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut lines = BufReader::new(io::stdin()).lines();
    while let Some(line) = lines.next_line().await? {
        println!("line: {line}");
    }
    Ok(())
}
I/O return values
read() may return fewer bytes than requested; Ok(0) means EOF. write() may write only part of a buffer; use write_all() when the whole buffer must be sent.
13 / 26

TCP servers and clients#

Bind a listener, accept connections, and give each connection its own task.

The echo server copies bytes until the client closes its write side. tokio::io::copy handles partial reads and writes while the listener accepts other connections.

Rustsrc/main.rs — split read and write halves
use tokio::{io, net::TcpListener};

#[tokio::main]
async fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7000").await?;
    loop {
        let (stream, peer) = listener.accept().await?;
        tokio::spawn(async move {
            let (mut reader, mut writer) = stream.into_split();
            if let Err(error) = io::copy(&mut reader, &mut writer).await {
                eprintln!("{peer}: {error}");
            }
        });
    }
}
RustTCP client — send a line and read reply
use tokio::{io::{self, AsyncReadExt, AsyncWriteExt}, net::TcpStream};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut stream = TcpStream::connect("127.0.0.1:7000").await?;
    stream.write_all(b"hello\n").await?;
    let mut response = [0u8; 6];
    stream.read_exact(&mut response).await?;
    println!("{}", String::from_utf8_lossy(&response));
    Ok(())
}
Connection limits

The server accepts connections without a cap. Add a semaphore, timeouts, cancellation, and task tracking before using this pattern in production.