๐ŸŽ“ A1Learner
๐Ÿ”

Rust Tokio IO

Tokio I/O is reading and writing that pauses a task instead of blocking a thread. One task serves each connection.

Open a server socket

bind opens the door at an address. The await waits for the system, not for a client:

let listener = TcpListener::bind("127.0.0.1:8080").await?;

One task per connection

accept waits for the next client. Each connection gets its own task. So no client waits behind another:

let (socket, addr) = listener.accept().await?;
tokio::spawn(async move { handle(socket).await });

Reading a line needs a buffered reader

let mut lines = BufReader::new(reader).lines();
while let Some(line) = lines.next_line().await? {
    // one whole line per round โ€” the reader buffers the chunks
}

copy pumps the bytes for you

io::copy moves bytes from any reader to any writer. It stops when the reader ends. Here it copies from a byte slice into a Vec:

let mut source: &[u8] = b"ping";
let mut sink: Vec<u8> = Vec::new();
let n = io::copy(&mut source, &mut sink).await?;
println!("copied {n} bytes: {}", String::from_utf8_lossy(&sink));

Flush, or the bytes stay in the buffer

A BufWriter holds small writes back. Until flush, the other side has received nothing:

let mut writer = BufWriter::new(Vec::new());
writer.write_all(b"hello").await?;
println!("sent before flush = {}", writer.get_ref().len());
writer.flush().await?;
println!("sent after flush = {}", writer.get_ref().len());

Example

// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::io;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("echo server listening on 127.0.0.1:8080");

    loop {
        let (mut socket, addr) = listener.accept().await?;
        println!("client connected: {}", addr);
        tokio::spawn(async move {
            let (mut reader, mut writer) = socket.split();
            let _ = io::copy(&mut reader, &mut writer).await;
        });
    }
}

Key points

  • AsyncRead and AsyncWrite are Tokio's two core I/O traits: "can be read without blocking" and "can be written without blocking"
  • Most std io types have Tokio twins: tokio::fs::File, tokio::io::stdin โ€” same ideas, async methods
  • One read never promises one message.
  • A read can return half a frame, or three frames at once.
  • A connection task should return a Result and log errors.
  • It must never panic.
  • A TCP echo server is a server that sends back whatever it receives.
  • Ten thousand idle connections then need ten thousand tasks, not ten thousand threads.

In this example, bind opens the server socket, and accept waits โ€” asynchronously โ€” for a client. Each connection is handed to its own spawned task, so many clients can be served at once. Inside the task, the stream is split in two. io::copy echoes bytes from the reader straight to the writer. It stops when the client hangs up. Try it yourself: run the server, type nc 127.0.0.1 8080 in another terminal, and watch your words come back.

Note

One bad client must not kill the server.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

Why spawn a task for each accepted connection?