A secure, real-time, low-latency binary WebSocket RPC subprotocol.

Overview

HardLight

A secure, real-time, low-latency binary WebSocket RPC subprotocol.

HardLight has two data models:

  • RPC: a client connects to a server, and can call functions on the server
  • Events: the server can push events of specified types to clients

An example: multiple clients subscribe to a "chat" event using Events. The connection state holds user info and what chat threads the user wants events for. Clients use an RPC function send_message to send a message, which will then persisted by the server and sent to subscribed clients.

HardLight is named after the fictional Forerunner technology that "allows light to be transformed into a solid state, capable of bearing weight and performing a variety of tasks".

While there isn't an official "specification", we take a similar approach to Bitcoin Core, where the protocol is defined by the implementation. This implementation should be considered the "reference implementation" of the protocol, and ports should match the behaviour of this implementation.

Features

  • Feature sets: depending on the context, certain endpoints can be disabled or enabled
    • Example: An unauthenticated client only has access to RPC methods to authenticate, and reconnects with authentication with the full set of RPC methods
  • Concurrent RPC: up to 256 RPC calls can be occuring at the same time on a single connection
    • This doesn't include subscriptions, for which there are no hard limits
  • Subscriptions: the server can push events to clients

Install

cargo add hardlight

Why WebSockets?

WebSockets actually have very little abstraction over a TCP stream. From RFC6455:

Conceptually, WebSocket is really just a layer on top of TCP that does the following:

  • adds a web origin-based security model for browsers
  • adds an addressing and protocol naming mechanism to support multiple services on one port and multiple host names on one IP address
  • layers a framing mechanism on top of TCP to get back to the IP packet mechanism that TCP is built on, but without length limits
  • includes an additional closing handshake in-band that is designed to work in the presence of proxies and other intermediaries

In effect, we gain the benefits of TLS, wide adoption & firewall support (it runs alongside HTTPS on TCP 443) while having little downsides. This means HardLight is usable in browsers, which was a requirement we had for the framework. In fact, we officially support using HardLight from browsers using the "wasm" feature.

At Valera, we use HardLight to connect clients to our servers, and for connecting some of our services to each another.

Usage

HardLight is designed to be simple, secure and fast. We take advantage of Rust's trait system to allow you to define your own RPC methods, and then use the #[derive(RPC)] macro to generate the necessary code to make it work.

Here's a very simple example of a counter service:

use hardlight::{RPC, ConnectionState};

/// These RPC methods are executed on the server and can be called by clients.
#[derive(RPC)]
trait Counter {
    async fn increment(&self, amount: u32) -> u32;
    async fn decrement(&self, amount: u32) -> u32;
    async fn get(&self) -> u32;
}

/// We store the counter in connection state (so each connection has its own counter)
/// In reality, you'd probably want to store this in a database and use subscriptions
/// to push updates to clients. Connection state is ephemeral (per connection)
/// and is not persisted.
#[derive(ConnectionState)]
struct State {
    counter: u32,
}

The Counter trait is shared between clients and servers. Any inputs or outputs have to support rkyv's Serialize, Deserialize, Archive and CheckBytes traits. This means you can use any primitive type, or any type that implements these traits.

The #[derive(RPC)] macro will generate:

  • a Client struct
  • a Handler struct
  • an enum, Method, of all the RPC method identifiers (e.g. Increment, Decrement, Get)
  • input structs for each RPC method (e.g. struct IncrementInput { amount: u32 })
  • output types for each RPC method (e.g. type IncrementOutput = u32)

You'd be encouraged to put this trait in a separate crate or module, so you can use Counter::Handler and Counter::Client etc.

Both structs will expose methods for holding connection state in a hashmap. You can store anything serializable in connection state, and it will be available to all RPC methods. This is useful for storing authentication tokens, or other state that you want to be available to all RPC methods.

Subscriptions are a little more complex. They expand to traits for both the client and server, which are implemented by you. However, they don't have their own servers & clients - they're attached to RPC servers and clients.

The external runtime will handle networking, encryption and connection management.

The Handler

The RPC handler generated by the macro expands to a struct that you implement your RPC methods on. It isn't a server itself, only exposing a function the runtime calls. Each connection has one handler.

This function has the signature async fn handle_rpc_call(&mut self, input: &[u8]) -> Result<Vec<u8>, Error>. It deserializes any inputs, matches the method to the function, calls the appropriate method, and serializes the output.

Connection state

As each connection has its own handler, we provide connection state in each handler's self.state. Here, you can control extra metadata for the connection. A typical use of this would be storing authentication data. Cookies are exposed here (for Handler).

Internally, we use a parking_lot::Mutex, as a handler can be called from multiple threads (which allows us to make RPC calls concurrently over a single connection). When using state, you should lock it to the current thread using let mut state = self.state.lock();.

Clients cannot update connection state after the connection is established, but they can call RPC methods which can update the state from the server. This protects against race conditions.

Changes to connection state are sent to the client automatically without any extra code. This works through the locking mechanism, which wraps the Mutex's lock. This is (basically) how it works:

  1. You call let mut state = self.state.lock(); (which is a StateGuard)
  2. Internally, we lock the mutex, and store the lock and starting data in the StateGuard.
  3. You do whatever you want with the state, and then drop the StateGuard.
  4. We compare the current values to the previous state. Any differences are sent to the client by calling to the parent connection.

As HardLight ultimately uses TCP, changes will properly happen in order, even if the client sends multiple RPC calls at once and packets are reordered.

Implementing a handler

You then impl Counter for Handler to add your functionality. For example:

impl Counter for Handler {
    async fn increment(&self, amount: u32) -> u32 {
        // lock the state to the current thread
        let mut state = self.state.lock().unwrap();
        state.counter += amount;
        counter
    } // state is automatically unlocked here; any changes are sent to the client automagically ✨

    async fn decrement(&self, amount: u32) -> u32 {
        let mut state = self.state.lock().unwrap();
        state.counter -= amount;
        counter
    }

    async fn get(&self) -> u32 {
        let state = self.state.lock().unwrap();
        state.counter
    }
}

Events

Events are a little different from other subscription models like GraphQL. Instead of the client setting up subscriptions to topics, you define the types of events that can be sent over HardLight connections. The server decides what and when to send events to clients, normally based on the connection state. For example a client might specify what chat threads its subscribed to, or what financial accounts it wants realtime transactions for. This logic is handled by your app, not HardLight itself.

You attach handlers for each message type in the client.

Our general (conceptual) architecture at Valera looks like:

UI <> Logic <> State <-------------> Logic <> State
|     frontend     |    HardLight    |  backend   |

We provide handlers to HardLight that modify state inside the frontend. The UI logic then updates the UI layer based on the state.


You might also like...
The ever fast websocket tunnel built on top of lightws

Kaminari The ever fast websocket tunnel built on top of lightws. Intro Client side receives tcp then sends [tcp/ws/tls/wss]. Server side receives [tcp

Lightweight websocket implement for stream transmission.

Lightws Lightweight websocket implement for stream transmission. Features Avoid heap allocation. Avoid buffering frame payload. Use vectored-io if ava

websocket client

#websocket client async fn test_websocket()-anyhow::Result() { wasm_logger::init(wasm_logger::Config::default()); let (tx, rx) = futures_c

WebSocket-to-HTTP reverse proxy

websocket-bridge This is a simple reverse proxy server which accepts WebSocket connections and forwards any incoming frames to backend HTTP server(s)

Get unix time (nanoseconds) in blazing low latency with high precision

RTSC Get unix time (nanoseconds) in blazing low latency with high precision. About 5xx faster than SystemTime::now(). Performance OS CPU benchmark rts

Extremely low-latency chain data to Stackers, with a dose of mild humour on the side
Extremely low-latency chain data to Stackers, with a dose of mild humour on the side

Ronin Hello there! Ronin is a ultra-speed Stacks API server. It's super lightweight, but scales easily. Why are we making this? Because we don't like

Extreme Bevy is what you end up with by following my tutorial series on how to make a low-latency p2p web game.

Extreme Bevy Extreme Bevy is what you end up with by following my tutorial series on how to make a low-latency p2p web game. There game can be played

 Serenade: Low-Latency Session-Based Recommendations
Serenade: Low-Latency Session-Based Recommendations

Serenade: Low-Latency Session-Based Recommendations This repository contains the official code for session-based recommender system Serenade, which em

A vertically scalable stream processing framework focusing on low latency, helping you scale and consume financial data feeds.

DragonflyBot A vertically scalable stream processing framework focusing on low latency, helping you scale and consume financial data feeds. Design The

Skybase is an extremely fast, secure and reliable real-time NoSQL database with automated snapshots and SSL
Skybase is an extremely fast, secure and reliable real-time NoSQL database with automated snapshots and SSL

Skybase The next-generation NoSQL database What is Skybase? Skybase (or SkybaseDB/SDB) is an effort to provide the best of key/value stores, document

Skytable is an extremely fast, secure and reliable real-time NoSQL database with automated snapshots and TLS
Skytable is an extremely fast, secure and reliable real-time NoSQL database with automated snapshots and TLS

Skytable is an effort to provide the best of key/value stores, document stores and columnar databases, that is, simplicity, flexibility and queryability at scale. The name 'Skytable' exemplifies our vision to create a database that has limitless possibilities. Skytable was previously known as TerrabaseDB (and then Skybase) and is also nicknamed "STable", "Sky" and "SDB" by the community.

NSE is a rust cli binary and library for extracting real-time data from National Stock Exchange (India)

NSE Check out the sister projects NsePython and SaveKiteEnctoken which are Python & Javascript libraries to use the NSE and Zerodha APIs respectively

A single-binary, GPU-accelerated LLM server (HTTP and WebSocket API) written in Rust
A single-binary, GPU-accelerated LLM server (HTTP and WebSocket API) written in Rust

Poly Poly is a versatile LLM serving back-end. What it offers: High-performance, efficient and reliable serving of multiple local LLM models Optional

Scalable and fast data store optimised for time series data such as financial data, events, metrics for real time analysis

OnTimeDB Scalable and fast data store optimised for time series data such as financial data, events, metrics for real time analysis OnTimeDB is a time

A webserver and websocket pair to stop your viewers from spamming !np and
A webserver and websocket pair to stop your viewers from spamming !np and "what's the song?" all the time.

spotify-np 🦀 spotify-np is a Rust-based local webserver inspired by l3lackShark's gosumemory application, but the catch is that it's for Spotify! 🎶

Easy to use cryptographic framework for data protection: secure messaging with forward secrecy and secure data storage. Has unified APIs across 14 platforms.
Easy to use cryptographic framework for data protection: secure messaging with forward secrecy and secure data storage. Has unified APIs across 14 platforms.

Themis provides strong, usable cryptography for busy people General purpose cryptographic library for storage and messaging for iOS (Swift, Obj-C), An

ARM TrustZone-M example application in Rust, both secure world side and non-secure world side

ARM TrustZone-M example application in Rust, both secure world side and non-secure world side; projects are modified from generated result of cortex-m-quickstart.

User-friendly secure computation engine based on secure multi-party computation
User-friendly secure computation engine based on secure multi-party computation

CipherCore If you have any questions, or, more generally, would like to discuss CipherCore, please join the Slack community. See a vastly extended ver

Binary Field Encodings (BFE) for Secure Scuttlebutt (SSB)

ssb-bfe-rs Binary Field Encodings (BFE) for Secure Scuttlebutt (SSB). Based on the JavaScript reference implementation: ssb-bfe (written according to

Comments
  • Track in-flight RPC requests, validate version during handshake

    Track in-flight RPC requests, validate version during handshake

    Closes #7, closes #4

    Also adds additional tracing to the server, renames Server.handler to Server.factory and adds CounterHandler::init() for ergonomics

    documentation server rpc 
    opened by 617a7a 0
  • Track in-flight RPC calls on `Server`

    Track in-flight RPC calls on `Server`

    We currently can have a maximum of 256 (u8::MAX) active RPC calls on a single connection. handle_connection should ensure it doesn't spawn multiple RPC calls on the same ID.

    server rpc 
    opened by 617a7a 0
  • Scope out the Events feature

    Scope out the Events feature

    RPC currently has a decently stable implementation and an idea of how and why it works.

    The Events feature has yet to be expanded on, so we need to work out what the API for it will look like for developers so we can start on implementation.

    good first issue help wanted events 
    opened by 617a7a 0
Owner
valera
an early-stage payment technology firm developing cutting-edge hardware and software using Bitcoin, the internet's native currency. est. 722,913.
valera
A WebSocket (RFC6455) library written in Rust

Rust-WebSocket Note: Maintainership of this project is slugglish. You may want to use tungstenite or tokio-tungstenite instead. Rust-WebSocket is a We

Rust Websockets 1.3k Jan 6, 2023
Lightweight stream-based WebSocket implementation for Rust.

Tungstenite Lightweight stream-based WebSocket implementation for Rust. use std::net::TcpListener; use std::thread::spawn; use tungstenite::server::ac

Snapview GmbH 1.3k Jan 2, 2023
Websocket generic library for Bitwyre WS-API

Websocket Core (Rust) Websocket generic server library for: Periodic message broadcast Eventual (Pubsub) message broadcast Async request reply Authors

Bitwyre 13 Oct 28, 2022
Jamsocket is a lightweight framework for building WebSocket-based application backends.

Jamsocket is a lightweight framework for building services that are accessed through WebSocket connections.

null 94 Dec 30, 2022
A CLI development tool for WebSocket APIs

A CLI development tool for WebSocket APIs

Espen Henriksen 622 Dec 26, 2022
Spawn process IO to websocket with full PTY support.

Cliws Spawn process IO to websocket with full PTY support. Features Any process IO through Websocket Full pty support: VIM, SSH, readline, Ctrl+X Auto

B23r0 91 Jan 5, 2023
A lightweight framework for building WebSocket-based application backends.

Jamsocket Jamsocket is a lightweight framework for building services that are accessed through WebSocket connections. Services can either be native Ru

drifting in space 94 Dec 30, 2022
A WebSocket (RFC6455) library written in Rust

Rust-WebSocket Rust-WebSocket is a WebSocket (RFC6455) library written in Rust. Rust-WebSocket provides a framework for dealing with WebSocket connect

Jason N 19 Aug 22, 2022
An aria2 websocket jsonrpc in Rust.

aria2-ws An aria2 websocket jsonrpc in Rust. Built with tokio. Docs.rs aria2 RPC docs Features Almost all methods and structed responses Auto reconnec

null 8 Sep 7, 2022
A simple toy websocket client to connect to Bitstamp.net and print the live order book written in Rust.

A simple toy websocket client to connect to Bitstamp.net and print the live order book written in Rust.

Nate Houk 1 Feb 14, 2022