Skip to content

AlexSilver9/s9_websocket

Repository files navigation

Silver9 WebSocket

Crates.io Documentation License Rust

A simplified high-performance low-latency Rust WebSocket client library providing three distinct implementations: async/threaded with channels, non-blocking with callbacks, and blocking with callbacks.

Features

  • Low latency - Built with Rust as a thin layer over tungstenite-rs and optional for crossbeam-channel
  • 🚀 Non-blocking, Blocking, Async Channels - Choose the right approach for your use case
  • 🔒 TLS backend - Support for native-tls
  • 📡 Event-driven architecture - With either handler callbacks or channels
  • 🎯 Type-safe API - Leverage Rust's type system for correctness
  • 📊 Built-in tracing - Comprehensive logging support
  • 🤖 AI Coding Ready - Support for AI-assisted development with Claude (Vibe Coding)

Design Principles

  • 🕑 Predictable latency - Direct system calls and callbacks, means lower baseline latency
  • 📋 Low overhead - No task scheduling, futures, or waker infrastructure
  • 🤯 Simple model - Straightforward usage
  • 📐 Deterministic behavior - Optional custom spin-wait duration gives precise control over CPU/latency tradeoff
  • 🚫️ No runtime - No async runtime or framework overhead, no poll()/epoll_wait()/kevent()
  • 🔁 Async message passing - Optional message passing through channels
  • ⛔️ Graceful Shutdown - Process all messages util connection is closed by server on graceful shutdown
  • 📛 Forcibly Shutdown - Break the client's event loop immediately

Disadvantages:

  • Scalability - None of the clients scale to thousands of connections (see Scalability Constraints)
  • CPU overhead - The fastest mode non-blocking without spin-wait utilizes 100% CPU core usage by design

Installation

Add this to your Cargo.toml:

[dependencies]
s9_websocket = "0.0.2"

TLS Backend Selection

The library uses native-tls.

Quick Start

Inspect examples for various usages and use cases.

Three Client Options

Choose the client that fits your application architecture:

  1. S9NonBlockingWebSocketClient - Runs on your thread, receive events via callbacks (lowest latency)
  2. S9BlockingWebSocketClient - Runs on your thread, receive events via callbacks
  3. S9AsyncNonBlockingWebSocketClient - Spawns background thread, receive events via channels (easiest usage)

Non-blocking Client (with handler callbacks)

Pure non-blocking client that runs on caller's thread using handler callbacks for events.

Socket Mode: Uses non-blocking socket I/O (set_nonblocking(true)) internally. The name "NonBlocking" refers to the socket I/O mode, not the behavior of run() which blocks the calling thread indefinitely.

Flushing: All send methods flush immediately after write

Key Features

  • Runs entirely on caller's thread
  • Receive events through handler callbacks (zero copy, zero allocation)
  • Call client methods directly from handler callbacks (send, close, force_quit)
  • Lowest possible latency
  • Configurable CPU/latency trade-off via spin-wait duration
  • Configurable socket options like TCP_NODELAY, TTL, etc
use s9_websocket::{S9NonBlockingWebSocketClient, S9WebSocketClientHandler, NonBlockingOptions};
use std::time::Duration;

// Implement the handler trait
struct MyHandler {
   message_count: usize,
}

impl S9WebSocketClientHandler<S9NonBlockingWebSocketClient> for MyHandler {
   // Only override the methods you care about
   fn on_text_message(&mut self, client: &mut S9NonBlockingWebSocketClient, data: &[u8]) {
      let text = String::from_utf8_lossy(data);
      println!("Received: {}", text);
      self.message_count += 1;

      if self.message_count >= 2 {
         println!("Closing connection...");
         client.close();
      } else {
         // Send another message
         client.send_text_message(&format!("Echo: {}", text)).ok();
      }
   }

    fn on_connection_closed(&mut self, _client: &mut S9NonBlockingWebSocketClient, reason: Option<String>) {
        println!("Connection closed: {:?}", reason);
    }

    fn on_error(&mut self, _client: &mut S9NonBlockingWebSocketClient, error: String) {
        eprintln!("Error: {}", error);
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
   // Configure options
   let options = NonBlockingOptions::new()
           .spin_wait_duration(Some(Duration::from_millis(10)))?;

   // Connect to WebSocket server
   let mut client = S9NonBlockingWebSocketClient::connect("wss://echo.websocket.org", options)?;

   // Send initial message
   client.send_text_message("Hello!")?;

   // Create handler
   let mut handler = MyHandler { message_count: 0 };

   // Run the non-blocking event loop (blocks on this thread)
   client.run(&mut handler);

   Ok(())
}

Blocking Client

Synchronous client that runs on caller's thread using handler callbacks for events.

Socket Mode: Uses blocking socket I/O (standard socket reads/writes) internally, with optional timeouts. The name "Blocking" refers to the socket I/O mode, not the behavior of run() which blocks the calling thread indefinitely (same as S9NonBlockingWebSocketClient).

Flushing: All send methods flush immediately after write

Key Features

  • Runs entirely on caller's thread
  • Receive events through handler callbacks (zero copy, zero allocation)
  • Call client methods directly from handler callbacks (send, close, force_quit)
  • Optional read/write timeouts for responsive control message handling (simulates non-blocking behavior)
  • Configurable CPU/latency trade-off via spin-wait duration
  • Configurable socket options like TCP_NODELAY, TTL, etc
use s9_websocket::{S9BlockingWebSocketClient, S9WebSocketClientHandler, BlockingOptions};

// Implement the handler trait
struct MyHandler {
   message_count: usize,
}

impl S9WebSocketClientHandler<S9BlockingWebSocketClient> for MyHandler {
   // Only override the methods you care about
   fn on_text_message(&mut self, client: &mut S9BlockingWebSocketClient, data: &[u8]) {
      let text = String::from_utf8_lossy(data);
      println!("Received: {}", text);
      self.message_count += 1;

      if self.message_count >= 2 {
         println!("Closing connection...");
         client.close();
      } else {
         // Send another message
         client.send_text_message(&format!("Echo: {}", text)).ok();
      }
   }

   fn on_connection_closed(&mut self, _client: &mut S9BlockingWebSocketClient, reason: Option<String>) {
      println!("Connection closed: {:?}", reason);
   }

   fn on_error(&mut self, _client: &mut S9BlockingWebSocketClient, error: String) {
      eprintln!("Error: {}", error);
   }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
   // Configure with default blocking behavior
   let options = BlockingOptions::new();

   // Connect to WebSocket server
   let mut client = S9BlockingWebSocketClient::connect("wss://echo.websocket.org", options)?;

   // Send initial message
   client.send_text_message("Hello!")?;

   // Create handler
   let mut handler = MyHandler { message_count: 0 };

   // Run the blocking event loop (blocks on this thread)
   // Handler can call client methods directly from callbacks
   client.run(&mut handler);

   Ok(())
}

Async Non-blocking Client (with channels)

Spawns a background thread for socket operations and communicates via channels.

Socket Mode: Uses non-blocking socket I/O (set_nonblocking(true)) internally. The name "NonBlocking" refers to the socket I/O mode, not the behavior of run() which returns immediately after spawning the thread.

Flushing: All send methods flush immediately after write

Key Features

  • Background thread handles all socket operations
  • Receive events through built-in channels (event_rx)
  • Send commands through built-in channels (control_tx)
  • Thread-safe for multi-threaded applications
  • Configurable CPU/latency trade-off via spin-wait duration
  • Configurable socket options like TCP_NODELAY, TTL, etc
use s9_websocket::{S9AsyncNonBlockingWebSocketClient, WebSocketEvent, ControlMessage, NonBlockingOptions};
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
   // Configure options
   let options = NonBlockingOptions::new()
           .spin_wait_duration(Some(Duration::from_millis(10)))?;

   // Connect to WebSocket server
   let mut client = S9AsyncNonBlockingWebSocketClient::connect("wss://echo.websocket.org", options)?;

   // Start the event loop (spawns thread)
   let _handle = client.run()?;

   // Send a message via control channel
   client.control_tx.send(ControlMessage::SendText("Hello, WebSocket!".to_string()))?;

   // Handle events from channel
   loop {
      match client.event_rx.recv() {
         Ok(WebSocketEvent::Activated) => {
            println!("WebSocket connection activated");
         },
         Ok(WebSocketEvent::TextMessage(data)) => {
            let text = String::from_utf8_lossy(&data);
            println!("Received: {}", text);

            client.control_tx.send(ControlMessage::Close())?;
         },
         Ok(WebSocketEvent::Quit) => {
            println!("Client quit");
            break;
         },
         _ => {}
      }
   }
   Ok(())
}

Advanced Usage

Custom Headers

use std::collections::HashMap;

let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer token123".to_string());
headers.insert("X-Custom-Header".to_string(), "value".to_string());

let client = S9NonBlockingWebSocketClient::connect_with_headers(
    "wss://api.example.com/ws",
    &headers
)?;

Handling Ping/Pong

impl S9WebSocketClientHandler<S9NonBlockingWebSocketClient> for MyHandler {
    fn on_ping(&mut self, _client: &mut S9NonBlockingWebSocketClient, data: &[u8]) {
        println!("Received ping: {:?}", data);
    }

    fn on_pong(&mut self, _client: &mut S9NonBlockingWebSocketClient, data: &[u8]) {
        println!("Received pong: {:?}", data);
    }
}

Non-blocking Configuration

use std::time::Duration;

// Maximum performance (no sleep between reads, high CPU usage)
let options = NonBlockingOptions::new(None)?;

// Balanced (10ms sleep between reads)
let options = NonBlockingOptions::new(Some(Duration::from_millis(10)))?;

// Low CPU usage (100ms sleep between reads, higher latency)
let options = NonBlockingOptions::new(Some(Duration::from_millis(100)))?;

Accessing the Underlying Socket

All clients provide low-level access to the underlying tungstenite WebSocket for advanced use cases:

use s9_websocket::{S9NonBlockingWebSocketClient, NonBlockingOptions};

let options = NonBlockingOptions::new();
let mut client = S9NonBlockingWebSocketClient::connect("wss://echo.websocket.org", options)?;

// Get immutable reference to the socket
let socket = client.get_socket();

// Get mutable reference to the socket for advanced operations
let socket_mut = client.get_socket_mut();

Note for S9AsyncNonBlockingWebSocketClient: Socket access returns Option<&WebSocket> because the socket is moved to event loop thread after calling run(). Socket access is only available before run() is called.

Use with caution: Direct manipulation of the underlying socket may interfere with the client's operation.

Logging

The library uses the 'tracing' crate for logging. Enable logging in your application: use tracing_subscriber;

tracing_subscriber::fmt()
    .with_max_level(tracing::Level::INFO)
    .init();

Performance Tips

  1. Choose the right client:
    • Use non-blocking client for low-latency applications
    • Use async client for easiest embedding in multithreaded applications
  2. Tune spin wait duration:
    • None: Best latency, highest CPU usage
    • Some(Duration::from_millis(1-10)): Good balance for up to 100 message/sec
    • Some(Duration::from_millis(50-100)): Lower CPU, higher latency, less throughput
  3. Tune no_delay:
    • None: use OS default
    • Some(true): socket write operations are immediately executed
    • Some(false): socket write operations are scheduled by OS

Scalability Constraints

None of the clients scale to thousands of connections.

The library's architecture requires one OS thread per connection because each client's run() method either spawns a dedicated thread (async client) or blocks the caller's thread indefinitely (non-blocking and blocking client). There is no I/O multiplexing support to run multiple connections on a single thread. For 1000+ connections, use async/await libraries like tokio-tungstenite or async-tungstenite that provide true async I/O multiplexing.

API documentation

Full API documentation is available at https://docs.rs/s9_websocket/latest/s9_websocket.

Contributing

Contributions are welcome! Please feel free to submit bugs and make feature requests here Further information like coding conventions are currently maintained in CLAUDE.md

License

This project is licensed under the APACHE / MIT License - see the LICENSE files for details.

Project URL

Project source code is available at https://github.com/AlexSilver9/s9_websocket.

Authors

Alexander Silvennoinen

Acknowledgments

Built on top of:

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

No packages published

Languages