Introduction to WebSocket in Rust: Why Professionals Choose It
Modern web applications require instant data transfer. Whether it's an online chat, a trading terminal, a multiplayer game, or a monitoring system — the WebSocket protocol has become the standard for real-time communication. Rust, with its emphasis on performance and memory safety, is an ideal platform for building such systems.
In this article, we will walk through creating WebSocket applications in Rust from scratch. You will learn how the main libraries work, how to handle multiple connections, and how to build scalable real-time systems. The material is intended for developers familiar with the basics of Rust and asynchronous programming.
1. WebSocket Basics in Rust: Choosing a Library
The Rust ecosystem offers several mature solutions for working with WebSocket. The most popular ones are:
- tokio-tungstenite — an asynchronous WebSocket library based on tokio, low-level and flexible
- warp — a web framework with built-in WebSocket support
- actix-web — a powerful framework with WebSocket support
For our guide, we will choose tokio-tungstenite as the most versatile solution.
Installing Dependencies
Create a new project and add to Cargo.toml:
[dependencies]tokio = { version = "1", features = ["full"] }tokio-tungstenite = "0.21"futures-util = "0.3"serde = { version = "1", features = ["derive"] }serde_json = "1"Basic WebSocket Server
Let's write a simple echo server that sends back the received message:
use futures_util::{SinkExt, StreamExt};use tokio::net::TcpListener;use tokio_tungstenite::accept_async;use tokio_tungstenite::tungstenite::Message;
#[tokio::main]async fn main() { let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); println!("Server started on ws://127.0.0.1:8080");
while let Ok((stream, addr)) = listener.accept().await { println!("New connection: {}", addr); tokio::spawn(handle_connection(stream)); }}
async fn handle_connection(stream: tokio::net::TcpStream) { let ws_stream = accept_async(stream).await.unwrap(); let (mut write, mut read) = ws_stream.split();
while let Some(Ok(msg)) = read.next().await { if let Message::Text(text) = msg { println!("Received: {}", text); let response = format!("Echo: {}", text); write.send(Message::Text(response.into())).await.unwrap(); } }}This code demonstrates the basic structure: an asynchronous TCP server, accepting WebSocket connections, and handling messages in separate tasks.
2. Architecture of a Real-Time Chat in Rust
Now let's build a full-fledged chat where messages from one client are broadcast to all connected users. For this, we need shared state — a list of active connections.
Implementation with Shared State
use std::collections::HashMap;use std::sync::Arc;use tokio::sync::{Mutex, broadcast};
type Tx = broadcast::Sender<String>;type Clients = Arc<Mutex<HashMap<String, Tx>>>;
#[tokio::main]async fn main() { let clients: Clients = Arc::new(Mutex::new(HashMap::new())); let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
while let Ok((stream, addr)) = listener.accept().await { let clients = clients.clone(); tokio::spawn(handle_client(stream, addr, clients)); }}
async fn handle_client( stream: tokio::net::TcpStream, addr: std::net::SocketAddr, clients: Clients) { let ws_stream = accept_async(stream).await.unwrap(); let (mut write, mut read) = ws_stream.split();
// Create a broadcast channel