Websockets with Tokio Tungstenite

WebSockets revolutionize web communication by establishing persistent, full-duplex connections between a client (typically a browser) and a server. Unlike traditional HTTP, WebSockets enable real-time, bi-directional data exchange, ideal for interactive applications like live chat, gaming, and financial tickers. By maintaining a constant connection, WebSockets eliminate the need for frequent client polling, reducing latency and server load.

For more background info check out the RFC 6455 and the wikipedia page

This technology supports seamless, instant updates and responsive user experiences, making it a powerful tool for modern web development.”

updates

Regular updates!

This introduction provides a brief overview of what WebSockets are, their advantages over traditional HTTP, and their applications in enhancing user interaction on the web. Full code provided for you to use and copy.

Watch a demonstration

WebSocket Server in Rust

p.s If you want a fast VPS server with Python installed check out Webdock:
https://webdock.io/en?maff=wdaff–170

The Rust code

[dependencies]
tokio = { version = "1.38.1", features = ["full"] }
tokio-tungstenite = "0.23.1"
futures = "0.3.30"

Line 14listener.accept() is a method provided by Tokio’s TcpListener within the tokio::net module. It’s used to asynchronously accept incoming TCP connections in Rust applications leveraging Tokio’s asynchronous runtime capabilities.

The while let Ok((stream, _)) = listener.accept().await loop ensures that the server continuously accepts incoming connections without blocking the main execution thread.

When a new connection is accepted, it returns a tuple wrapped in a Result. The tuple contains:

  • stream: A tokio::net::TcpStream representing the newly accepted TCP connection.
  • _: Ignored in this context, typically used for storing connection metadata.

Line 20 – 21

Handle Client Function (async fn handle_client()):

  • Accepts a tokio::net::TcpStream as input parameter representing a client connection.
  • Calls accept_async(stream).await.unwrap() to accept the WebSocket handshake asynchronously. This function blocks until the handshake completes and returns a tokio_tungstenite::WebSocketStream, which is then unwrapped to extract the WebSocket stream (ws_stream).
  • Splits the ws_stream into separate read and write halves ((mut write, mut read)). This allows separate handling of incoming and outgoing WebSocket messages.
  • Logs a message indicating a new WebSocket connection has been established.

Line 24No explicit closure: There’s no explicit closure like (move || { ... }) defined, but async move implicitly creates one. The closure encapsulates the entire async block, which includes the loop and the logic inside it.

Line 29 interval.tick().await; is specific to the Tokio asynchronous runtime for Rust. It is used to asynchronously wait for a predefined interval to elapse before continuing execution. This functionality is part of Tokio’s time module, providing utilities for managing time-related operations in async Rust applications.

use std::time::Duration;
use tokio::time;
use futures::SinkExt;
use tokio::net::TcpListener;
use tokio_tungstenite::accept_async;
use futures::StreamExt;
use tokio_tungstenite::tungstenite::Message;

#[tokio::main]
async fn main() {
    let addr = "127.0.0.1:8080";
    let listener = TcpListener::bind(addr).await.unwrap();
    println!("{}", "running websocket server...connect from index.html");
    while let Ok((stream, _)) = listener.accept().await {
        tokio::spawn(handle_client(stream));
    }
}

async fn handle_client(stream: tokio::net::TcpStream) {
    let ws_stream = accept_async(stream).await.unwrap();
    let (mut write, mut read) = ws_stream.split();

    // Create a task to periodically send updates
    tokio::spawn(async move {
        let mut score = 0;
        let mut interval = time::interval(Duration::from_secs(30));
        
        loop {
            interval.tick().await;
            score += 1;
            let message = format!("Goal!: 0 - {} - {}", score, "England");

            // send the message and continue if sending is successful.
            if write.send(Message::Text(message)).await.is_err() {
                break;
            }
        }
    });

    // Handle incoming messages (if necessary)
    while let Some(Ok(_msg)) = read.next().await {
        // In this example, we don't need to handle incoming messages
    }
}

Under the hood, WebSocket libraries like Tokio-Tungstenite handle the splitting of streams based on WebSocket protocol specifications. Here’s how it works in more detail:

WebSocket Protocol and Stream Management

  1. WebSocket Framing:
  • WebSocket communication is structured into frames. Each frame can be a control frame (like ping, pong) or a data frame (text, binary).
  • Frames have specific headers that indicate their type, length, and whether they are the final fragment of a message or part of a larger message.
  1. Stream Splitting:
  • When a WebSocket connection is established (accept_async in Tokio-Tungstenite), it creates a WebSocketStream.
  • The WebSocketStream manages the TCP connection and handles the reading and writing of WebSocket frames.
  • The split() method on WebSocketStream divides this single stream into two parts:
    • SplitSink: This part allows for sending WebSocket frames (Message objects) to the client. It implements the Sink trait from Tokio’s futures crate.
    • SplitStream: This part allows for receiving WebSocket frames (Message objects) from the client. It implements the Stream trait from Tokio’s futures crate.
  1. Frame Processing:
  • As data is received over the TCP connection, Tokio-Tungstenite reads these bytes and parses them according to the WebSocket protocol.
  • It identifies the boundaries between frames based on the WebSocket framing rules (start and end markers, length fields in headers).
  • For outgoing messages, Tokio-Tungstenite serializes WebSocket messages into frames and sends them over the TCP connection.
  1. Efficiency and Reliability:
  • The splitting of the stream (split()) allows Tokio-Tungstenite to manage asynchronous reading and writing of WebSocket frames efficiently.
  • It ensures that incoming messages are buffered and processed correctly, and outgoing messages are queued and sent in the correct order.

Tokio-Tungstenite Internals

  • Packet Inspection: Tokio-Tungstenite inspects incoming TCP packets to identify WebSocket frames. It checks for the WebSocket protocol headers and manages frame boundaries accordingly.
  • State Management: Keeps track of WebSocket connection state (handshake, open, closing) and manages errors or abnormal terminations.

Conclusion

In summary, Tokio-Tungstenite splits the stream based on the WebSocket protocol specifications and manages the parsing and serialization of WebSocket frames. It uses efficient async I/O mechanisms provided by Tokio to handle incoming and outgoing WebSocket data, ensuring reliable and performant communication between WebSocket clients and servers in Rust applications.

! What actually triggers the updating, the JS or the Websocket server?

In the context of a WebSocket-based application where updates are sent every 30 seconds, the mechanism for initiating and timing these updates typically resides on the WebSocket server side rather than in JavaScript running on the client side. Here’s how it typically works:

  1. WebSocket Server (Backend):
  • The WebSocket server is responsible for managing the WebSocket connections with clients.
  • It decides when and how frequently to send updates to connected clients.
  • Using timers or schedulers (e.g., Tokio’s tokio::time::interval in Rust), the server can schedule tasks to send updates at regular intervals, such as every 30 seconds.
  • When the timer expires, the server sends a message (e.g., score update, notification) to all connected clients through their respective WebSocket connections.
  1. JavaScript (Client Side):
  • JavaScript running on the client side listens for incoming messages from the WebSocket server.
  • When a message (update) is received from the server, JavaScript processes it and updates the UI or performs any necessary actions based on the content of the message.
  • The client-side JavaScript does not initiate the updates or control their timing. It reacts to messages sent by the server whenever they are received.

Clarification:

  • Server Initiated: In your scenario, where updates are sent every 30 seconds, it’s the WebSocket server that knows when to send these updates. It manages the timing and content of messages sent to clients.
  • Client Role: The JavaScript running in the client’s browser listens for these updates and reacts accordingly, updating the UI or triggering specific behaviors in response to the messages received.

Benefits:

  • Efficiency: Server-initiated updates via WebSockets reduce the need for constant client polling, optimizing network and server resources.
  • Real-Time Interaction: Enables real-time updates and interactive features in web applications, enhancing user experience and responsiveness.

In essence, while JavaScript on the client side handles incoming messages and updates the UI accordingly, it’s the WebSocket server that orchestrates the timing and content of these updates, ensuring synchronized communication across all connected clients.

Final thoughts :

Websockets maintain a persistent connection like keeping a phone call open. This allows continuous communication without reconnecting for each message, akin to staying on a call with intermittent chatting, saving resources and latency. If you want to try a different crate there is also : https://crates.io/crates/fastwebsockets

You may wish to research TLS as well.