EP 02 / TCP CHAT SERVER  ·  BUILD

TCP Chat Server: Async, Tokio, Broadcast

Jun 3, 2026 · 22 min

Build a multi-client chat server from scratch in Rust. Every client gets its own task; messages fan out over a broadcast channel. We use tokio::select! to multiplex reading from the socket and receiving from the broadcast channel simultaneously.

What this episode covers
Code
use std::net::SocketAddr;

use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::{TcpListener, TcpStream},
    sync::broadcast::{self, Receiver, Sender},
};

async fn handle_client(
    mut socket: TcpStream,
    addr: SocketAddr,
    tx: Sender<(SocketAddr, String)>,
    mut rx: Receiver<(SocketAddr, String)>,
) -> std::io::Result<()> {
    let mut buffer = [0; 1024];
    loop {
        tokio::select! {
            result = socket.read(&mut buffer) => {
                let n = match result {
                    Ok(0) => return Ok(()),
                    Ok(n) => n,
                    Err(e) => {
                        eprintln!("read error from {addr}: {e}");
                        return Ok(());
                    }
                };

                let message = String::from_utf8_lossy(&buffer[..n]).into_owned();
                println!("{addr}: {}", message.trim_end());
                let _ = tx.send((addr, message));
            }
            result = rx.recv() => {
                match result {
                    Ok((from, message)) if from != addr => {
                        let line = format!("{from}: {message}");
                        if socket.write_all(line.as_bytes()).await.is_err() {
                            return Ok(());
                        }
                    },
                    Ok(_) => {},
                    Err(broadcast::error::RecvError::Lagged(n)) => {
                        eprintln!("client {addr} lagged, missed {n} messages");
                    },
                    Err(broadcast::error::RecvError::Closed) => return Ok(()),
                }
            }
        }
    }
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8081").await?;
    let (tx, _) = broadcast::channel::<(SocketAddr, String)>(16);
    println!("server is listening on {:?}", listener.local_addr()?);
    loop {
        let (socket, addr) = listener.accept().await?;
        println!("client {addr} connected");
        tokio::spawn(handle_client(socket, addr, tx.clone(), tx.subscribe()));
    }
}
Get the code