← Week 1: Tokio Fundamentals

Day 3: Tokio I/O — TcpListener and AsyncRead/Write

Phase 2 · Jun 12, 2026

← Week 1: Tokio Fundamentals

Agenda (2–3 hours)

  • Read (45 min): Tokio I/O tutorial; tokio::net and tokio::io module docs
  • Study (45 min): Read the source of tokio::io::copy — how does it handle backpressure? Why does it use a fixed buffer?
  • Practice (45 min): Build a TCP server that echoes each line prefixed with the client's address; handle graceful shutdown with Ctrl+C
  • Challenge (30 min): Implement a proxy server: accept on port 8080, forward to 127.0.0.1:8081, bidirectionally copy bytes using tokio::io::copy_bidirectional
← Week 1: Tokio Fundamentals

TcpListener

use tokio::net::{TcpListener, TcpStream};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("0.0.0.0:8080").await?;
    loop {
        let (stream, addr) = listener.accept().await?;
        tokio::spawn(async move {
            handle_connection(stream, addr).await;
        });
    }
}

Each accepted connection is spawned as an independent task. Thousands of connections run concurrently on a handful of threads.

← Week 1: Tokio Fundamentals

AsyncRead and AsyncWrite

use tokio::io::{AsyncReadExt, AsyncWriteExt};

async fn handle_connection(mut stream: TcpStream) {
    let mut buf = [0u8; 1024];
    loop {
        match stream.read(&mut buf).await {
            Ok(0) => break, // EOF
            Ok(n) => {
                stream.write_all(&buf[..n]).await.unwrap();
            }
            Err(e) => { eprintln!("error: {}", e); break; }
        }
    }
}

AsyncReadExt and AsyncWriteExt provide convenient helper methods over the raw AsyncRead/AsyncWrite traits.

← Week 1: Tokio Fundamentals

BufReader and BufWriter

use tokio::io::{BufReader, AsyncBufReadExt};

async fn handle_lines(stream: TcpStream) {
    let reader = BufReader::new(stream);
    let mut lines = reader.lines();
    while let Some(line) = lines.next_line().await.unwrap() {
        println!("Got: {}", line);
    }
}

BufReader adds buffering — prevents issuing a syscall for every byte. Always wrap unbuffered streams when doing line-by-line or small reads.

← Week 1: Tokio Fundamentals

Split: Independent Read and Write Halves

use tokio::io::AsyncWriteExt;

let (mut reader, mut writer) = stream.split();
// Now reader and writer can be used independently
// (useful for bidirectional pipes with separate tasks)

let (owned_reader, owned_writer) = stream.into_split();
// into_split() gives OwnedHalves with 'static lifetime
// suitable for moving into separate tasks

split() returns borrows (same lifetime as stream). into_split() consumes the stream and gives owned halves you can move into separate tokio::spawn calls.

← Week 1: Tokio Fundamentals

Key Takeaways

  • TcpListener::accept() + tokio::spawn = the standard pattern for concurrent connections
  • AsyncRead/AsyncWrite are the core traits; extension traits (AsyncReadExt) add convenience methods
  • Always use BufReader/BufWriter to avoid syscall-per-byte
  • Use split() or into_split() when you need independent read/write access in separate tasks

Tomorrow: Tokio channels — communicating between tasks.