Rust Tokio Project
A chat server is a program that forwards every message from one client to all the other clients. A broadcast channel carries every message to every client task.
One channel for every client
The whole chat hangs on this one channel โ every client task sends to it and hears from it:
let (tx, _rx) = broadcast::channel::<String>(32);Subscribe first, then listen
A receiver only hears messages sent after it subscribes, so subscribe before the work starts:
let mut rx = tx.subscribe();
tx.send("hello everyone".to_string()).unwrap();
println!("got = {}", rx.recv().await.unwrap());The Listener / Handler split
struct Listener { listener: TcpListener, /* shared state, channels */ }
struct Handler { connection: Connection, /* per-client state */ }
// Listener::run accepts; Handler::run serves one client.Split the socket in two
Each client task splits its socket: the read half feeds lines in, the write half sends other people's lines out:
let (reader, mut writer) = socket.into_split();
let mut lines = BufReader::new(reader).lines();Example
// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio::sync::broadcast;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
let (tx, _rx) = broadcast::channel::<String>(32);
println!("chat server on 127.0.0.1:8080 - connect with: nc 127.0.0.1 8080");
loop {
let (socket, addr) = listener.accept().await?;
let tx = tx.clone();
let mut rx = tx.subscribe();
tokio::spawn(async move {
let (reader, mut writer) = socket.into_split();
let mut lines = BufReader::new(reader).lines();
let name = format!("{}", addr);
loop {
tokio::select! {
line = lines.next_line() => {
match line {
Ok(Some(text)) => { let _ = tx.send(format!("{}: {}", name, text)); }
_ => break,
}
}
msg = rx.recv() => {
if let Ok(text) = msg {
if !text.starts_with(&name) {
let _ = writer.write_all(text.as_bytes()).await;
let _ = writer.write_all(b"\n").await;
}
}
}
}
}
});
}
}Key points
- When a client disconnects, its line loop ends and its task simply finishes โ no cleanup code needed
- Build it step by step.
- First accept one client and echo.
- Then add broadcast.
- Then add the names.
- Ideas to extend it: try a /quit command.
- Or add a short message history for new joiners.
- Or add graceful shutdown with ctrl_c from the Cancellation lesson.
- Give the chat one shared map and it becomes Mini-Redis.
- The state would live behind the Db handle from the Shared State lesson.
- Each typed line would be parsed by the Commands lesson before touching it.
- This is a real, runnable program โ the longest in the course.
- A chat server forwards each message to every connected client.
- One listener accepts new clients.
- One task serves each client.
In this example, the server binds a listener and creates one broadcast channel shared by every client task. When a client connects, its socket is split: one half reads lines, the other writes. A select! loop watches both directions at once. A typed line goes into the broadcast channel. Messages from the channel are written to the socket. The ones this client sent are skipped. Open two terminals with nc 127.0.0.1 8080 and type in one: the words appear in the other.
Note
It needs a Cargo project with tokio = { version = "1", features = ["full"] }.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Rust Playground
Exercise
In the chat server, why does each client task use select!?