Session

session-rs

A lightweight, async WebSocket protocol for Rust.

Introduction

session-rs is a small request/response + notification protocol that runs over WebSockets, with typed methods on both ends.

  • Typed methods: requests, responses and errors are (de)serialized for you.
  • Both directions: either peer can send requests and notifications.
  • Transport-agnostic: the protocol runs over any Sink/Stream of frames. Adapters ship for tokio-tungstenite and axum.
  • Bounded: the built-in server limits message and frame sizes (1 MiB by default).

Installation

cargo add session-rs
Feature Default Enables
server yes SessionServer, a standalone WebSocket server
client yes Session::connect for ws:// URLs
rustls / native-tls no wss:// URLs in Session::connect
axum no Session::from_axum for axum WebSocket upgrades

Usage

Define a method once and share it between both peers:

#[derive(Debug, Serialize, Deserialize)]
struct Data;

impl Method for Data {
    const NAME: &'static str = "data";
    type Request = String;
    type Response = String;
    type Error = String;
}

Server

let server = SessionServer::bind("127.0.0.1:8080").await?;

server
    .session_loop(async |session, addr| {
        // Runs for every new client. Register handlers here; the session
        // starts reading once this returns, so no message arrives early.
        session
            .on_request::<Data, _>(async |_id, req| Ok(format!("echo: {req}")))
            .await;

        Ok(())
    })
    .await?;

Use SessionServer::with_config(ServerConfig { .. }) to change the size limits or the handshake timeout.

Client

let session = Session::connect("ws://127.0.0.1:8080").await?;
session.start_receiver();

let reply = session.request::<Data>("Hello".to_string()).await?; // Ok("echo: Hello")

axum

With the axum feature, a session can share a router with ordinary HTTP routes, such as a health check:

async fn ws(upgrade: WebSocketUpgrade) -> Response {
    upgrade.max_message_size(1 << 20).on_upgrade(async |socket| {
        let session = Session::from_axum(socket);
        session.on_request::<Data, _>(async |_, req| Ok(req)).await;
        session.start_receiver();
    })
}

let app = Router::new()
    .route("/", get(ws))
    .route("/health", get(async || "ok"));

Other transports

Session::from_transport(sink, stream) accepts any Sink<Frame> and Stream<Item = Result<Frame, E>>, and Session::from_tungstenite wraps an existing tokio-tungstenite stream (e.g. one accepted over TLS). The transport must answer pings itself.

Semantics

  • Incoming requests and notifications are handled one at a time, in arrival order. Responses to your own requests are delivered independently, so a handler can request from its peer.
  • A request for an unknown method, or with data that doesn't deserialize, gets an error response instead of no reply.
  • A handler that panics fails only its own request.
  • request fails with Error::ConnectionClosed if the session closes first; request_timeout adds a deadline.
  • on_close runs exactly once, whichever side closes. start_ping(interval, timeout) closes peers that stop answering pings.

Protocol

Every message is a JSON text frame with a type tag.

Request

The id is chosen by the sender and increments per peer.

{ "type": "request", "id": 1, "method": "data", "data": "Hello from client" }

Response

A response must carry the id of the request it answers.

{ "type": "response", "id": 1, "result": "Hello from server" }

Error response

{ "type": "errorresponse", "id": 1, "error": "Invalid data" }

Notification

A notification is fire-and-forget and gets no response.

{ "type": "notification", "method": "data", "data": "Hello from server" }
S
Description
A lightweight async protocol for WebSocket.
Readme Apache-2.0
144 KiB
Languages
Rust 100%