Rust Axum WebSocket Example: A Comprehensive Guide

Rust is gaining traction as a powerful systems programming language, and frameworks like Axum simplify building web applications. One popular feature is WebSocket support, which enables real-time, bidirectional communication between the client and server. This guide walks you through setting up a WebSocket server using Axum in Rust.


What Is Axum?

Axum is a web framework built on Tokio and Hyper, designed for performance and ergonomics. It integrates seamlessly with async programming in Rust, making it ideal for real-time applications like WebSocket communication.


What Are WebSockets?

WebSockets provide a persistent, full-duplex communication channel between the client and server. Unlike HTTP, which requires a request-response model, WebSockets allow the server to send messages to the client without being prompted.


Setting Up a Rust Project with Axum

Step 1: Create a New Rust Project

Run the following command to create a new Rust project:

cargo new axum_websocket_example
cd axum_websocket_example

Step 2: Add Dependencies

Update the Cargo.toml file with the required dependencies:

[dependencies]
axum = "0.6"
tokio = { version = "1.0", features = ["full"] }
tower = "0.4"
hyper = { version = "0.14", features = ["websocket"] }

Run cargo build to download and compile the dependencies.


Building a WebSocket Server with Axum

Step 3: Create a WebSocket Handler

Add the following code in src/main.rs:

use axum::{
    extract::WebSocketUpgrade,
    response::IntoResponse,
    routing::get,
    Router,
};
use tokio::net::TcpListener;
use tokio::sync::broadcast;
use tokio::sync::Mutex;
use tokio_tungstenite::tungstenite::Message;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    // Create a shared broadcast channel for messages
    let (tx, _rx) = broadcast::channel::<String>(100);
    let state = Arc::new(Mutex::new(tx));

    // Build the Axum application
    let app = Router::new()
        .route("/ws", get({
            let state = state.clone();
            move |ws: WebSocketUpgrade| handle_websocket(ws, state.clone())
        }));

    // Start the server
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    println!("Listening on http://127.0.0.1:3000");

    axum::Server::from_tcp(listener)
        .unwrap()
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handle_websocket(
    ws: WebSocketUpgrade,
    state: Arc<Mutex<broadcast::Sender<String>>>,
) -> impl IntoResponse {
    ws.on_upgrade(move |socket| handle_socket(socket, state))
}

async fn handle_socket(
    mut socket: axum::extract::ws::WebSocket,
    state: Arc<Mutex<broadcast::Sender<String>>>,
) {
    let tx = state.lock().await.clone();
    let mut rx = tx.subscribe();

    // Spawn a task to receive messages from other clients
    tokio::spawn(async move {
        while let Ok(msg) = rx.recv().await {
            if let Ok(_) = socket.send(Message::Text(msg)).await {
                continue;
            }
            break;
        }
    });

    // Read messages from the client and broadcast them
    while let Some(Ok(msg)) = socket.recv().await {
        if let Message::Text(text) = msg {
            let _ = tx.send(text);
        }
    }
}

How the Code Works

  1. WebSocketUpgrade: The /ws route upgrades HTTP requests to a WebSocket connection.
  2. Broadcast Channel: Uses tokio::sync::broadcast to share messages between all connected WebSocket clients.
  3. Async Communication:
    • Listens for client messages and broadcasts them to all subscribers.
    • Sends broadcast messages back to the client.

Testing the WebSocket Server

Using a WebSocket Client in JavaScript

To test the WebSocket server, create a simple HTML file (index.html) with the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Test</title>
</head>
<body>
    <h1>WebSocket Client</h1>
    <textarea id="output" cols="50" rows="10" readonly></textarea><br>
    <input type="text" id="input" placeholder="Type a message" />
    <button onclick="sendMessage()">Send</button>

    <script>
        const ws = new WebSocket("ws://127.0.0.1:3000/ws");
        const output = document.getElementById("output");
        const input = document.getElementById("input");

        ws.onmessage = (event) => {
            output.value += `\n${event.data}`;
        };

        function sendMessage() {
            const message = input.value;
            ws.send(message);
            input.value = "";
        }
    </script>
</body>
</html>

Run the WebSocket Client

  1. Open the HTML file in your browser.
  2. Connect to the WebSocket server (ws://127.0.0.1:3000/ws).
  3. Send messages using the input box and observe real-time communication.

Best Practices

  1. Error Handling:
    • Add proper error handling for connection issues and unexpected input.
  2. Scalability:
    • Use a load balancer or scale horizontally for high traffic.
  3. Security:
    • Implement authentication and SSL/TLS to secure WebSocket communication.

Conclusion

This example demonstrates how to create a WebSocket server using Axum in Rust. By leveraging async capabilities and tools like broadcast channels, you can efficiently build real-time, scalable applications. Experiment with this code to customize your WebSocket server for chat apps, live notifications, or other real-time services.