Introduction: Why WebSocket on Rust is a Powerful Tandem
Modern web applications increasingly require instant data transfer: chats, online games, stock quotes, notifications. Traditional HTTP with its request-response model is inefficient here — it creates extra load and delays. The WebSocket protocol comes to the rescue, establishing a persistent bidirectional connection between client and server.
The Rust language, with its emphasis on performance, memory safety, and zero-cost abstractions, is ideal for building high-load real-time systems. In this article, we will explore how to build a WebSocket server in Rust using the tokio-tungstenite library and examine key patterns for real-time applications.
WebSocket Basics and Environment Setup
What is WebSocket?
WebSocket is a protocol that works over TCP. It allows the server and client to exchange messages in real time without repeated HTTP handshakes. The connection starts as an HTTP request with an upgrade to WebSocket, after which data is transmitted in frames.
Installing Dependencies
For our work, we will need tokio (async runtime) and tokio-tungstenite (WebSocket implementation). Create a new project:
cargo new websocket_democd websocket_demoAdd dependencies to Cargo.toml:
[dependencies]tokio = { version = "1", features = ["full"] }tokio-tungstenite = "0.21"futures-util = "0.3"Creating a Simple WebSocket Server
Basic Echo Server
Let's start with a simple server that accepts connections and sends back received messages (echo mode). This is the classic "Hello, World" for WebSocket.
use tokio::net::TcpListener;use tokio_tungstenite::accept_async;use tokio_tungstenite::tungstenite::Message;use futures_util::StreamExt;
#[tokio::main]async fn main() { let addr = "127.0.0.1:8080"; let listener = TcpListener::bind(addr).await.expect("Failed to bind address"); println!("Server started on {}", addr);
while let Ok((stream, peer)) = listener.accept().await { println!("New connection: {}", peer); tokio::spawn(handle_connection(stream)); }}
async fn handle_connection(stream: tokio::net::TcpStream) { let ws_stream = accept_async(stream) .await .expect("Error during WebSocket handshake");
let (_, mut read) = ws_stream.split();
while let Some(msg) = read.next().await { match msg { Ok(Message::Text(text)) => { println!("Received: {}", text); // Here you could send a response, but for simplicity we skip it } Ok(Message::Close(_)) => break, _ => {} } }}This code starts a TCP server on port 8080. For each new connection, an async task handle_connection is created, which performs the WebSocket handshake and reads incoming messages.
Adding Message Sending
To make a full-fledged echo server, we need to split the stream into read and write parts. We use futures_util::StreamExt and SinkExt:
use futures_util::SinkExt;
async fn handle_connection(stream: tokio::net::TcpStream) { let ws_stream = accept_async(stream) .await .expect("Error during WebSocket handshake");
let (mut write, mut read) = ws_stream.split();
while let Some(msg) = read.next().await { match msg { Ok(Message::Text(text)) => { let echo = format!("Echo: {}", text); write.send(Message::Text(echo.into())).await.unwrap(); } Ok(Message::Close(_)) => break, _