ezsockets

repository·master·Indexed 42 days ago

https://github.com/gbaranski/ezsockets

A collection of tools and examples for simplifying WebSocket server and client implementations in Rust. Version 0.7.2 provides high-level abstractions for managing Ping/Pong frames, automatic client reconnections, and event-based programming via the ClientExt trait. It includes WASM-compatible clients using ClientConnectorWasm and various server implementations, including support for Axum via the WebSocketUpgrade extractor and TLS.

Tokens
10.9K
Snippets
24
Records
34
Agent score
54%

What's inside ezsockets

  1. Run the chat-client-wasm demo

    master

    To run the WebAssembly (WASM) chat client demo, you must first have wasm-server-runner installed. Follow these steps:

    1. Install wasm-server-runner from https://github.com/jakobhellermann/wasm-server-runner.
    2. Navigate to the examples/chat-client-wasm directory.
    3. Execute the demo using Cargo with the wasm32-unknown-unknown target.
    4. Open the address provided in the terminal in a new browser window.
    5. Open the browser's developer console to view the chat logs.

    Note: This specific demo is read-only; it does not currently support sending messages into the chat.

    # 1. Install wasm-server-runner (external dependency)
    # 2. Navigate to the directory
    cd examples/chat-client-wasm
    
    # 3. Run the WASM target
    cargo run --target wasm32-unknown-unknown
  2. Core WebSocket abstractions in ezsockets

    master

    The ezsockets crate provides high-level abstractions for WebSocket communication in Rust, designed to simplify handling Ping/Pong frames, implementing event-based programming via traits, and managing automatic client reconnections.

    Key types include:

    • Socket: The primary abstraction for WebSocket communication.
    • Message, RawMessage, and MessageSignal: Types for handling different levels of WebSocket message data.
    • CloseCode and CloseFrame: Used for managing connection termination.
    • Sink and Stream: Traits/types for sending and receiving data.
    • SocketConfig: Configuration settings for a socket.
  3. How SessionExt and Session work together

    master

    The ezsockets session model uses an actor pattern to decouple the public Session handle from the actual WebSocket processing logic.

    1. The Session<I, C> Handle: This is a lightweight, clonable object containing message senders. It is what the rest of your application interacts with to send data or custom commands.
    2. The SessionExt Trait: This is your implementation of the business logic. It defines how the session reacts to events (text, binary, or custom calls).
    3. The SessionActor: When Session::create is called, a background task is spawned running a SessionActor. This actor manages the lifecycle of the Socket, listens for incoming messages from the network, and routes them to your SessionExt implementation. It also listens for messages sent via the Session handle and forwards them to the socket or the extension logic.
  4. How ServerExt works

    master

    The ServerExt trait is the primary way to extend the server's behavior. It is an asynchronous trait that requires two associated types:

    • type Session: A type that implements SessionExt.
    • type Call: A type representing custom messages sent to the server via Server::call.

    Required methods:

    • on_connect: Triggered when a new socket connection is accepted. You must return a Session instance or an error/close frame to reject the connection.
    • on_disconnect: Triggered when a session ends. It provides the session ID and the reason for disconnection.
    • on_call: Triggered when a custom Call is sent to the server using the Server::call method. This is useful for cross-thread communication or triggering server-wide logic.
    #[async_trait]
    pub trait ServerExt: Send {
        type Session: SessionExt;
        type Call: Send;
    
        async fn on_connect(
            &mut self,
            socket: Socket,
            request: Request,
            address: SocketAddr,
        ) -> Result<Session<<Self::Session as SessionExt>::ID, <Self::Session as SessionExt>::Call>, Option<CloseFrame>>;
    
        async fn on_disconnect(
            &mut self,
            id: <Self::Session as SessionExt>::ID,
            reason: Result<Option<CloseFrame>, Error>,
        ) -> Result<(), Error>;
    
        async fn on_call(&mut self, call: Self::Call) -> Result<(), Error>;
    }
  5. Implement a WebSocket Server with ServerExt

    master

    To create a WebSocket server, you must implement the ServerExt trait. This trait defines how your server handles new connections, disconnections, and custom calls. You also need to define a Session type that implements SessionExt to manage individual client interactions.

    1. Implement SessionExt: Define how a session handles text, binary, and custom calls.
    2. Implement ServerExt: Define on_connect (to create and return a Session), on_disconnect, and on_call (to handle custom messages).
    3. Start the Server: Use Server::create with a closure that initializes your ServerExt implementation.
    use async_trait::async_trait;
    use ezsockets::{Server, ServerExt, Session, SessionExt, Socket, Request, Error, CloseFrame};
    use std::net::SocketAddr;
    
    type SessionID = u16;
    type SessionType = Session<SessionID, ()>;
    
    struct EchoSession {
        handle: SessionType,
        id: SessionID,
    }
    
    #[async_trait]
    impl SessionExt for EchoSession {
        type ID = SessionID;
        type Call = ();
    
        fn id(&self) -> &Self::ID {
            &self.id
        }
    
        async fn on_text(&mut self, text: ezsockets::Utf8Bytes) -> Result<(), Error> {
            self.handle.text(text);
            Ok(())
        }
    
        async fn on_binary(&mut self, _bytes: ezsockets::Bytes) -> Result<(), Error> {
            unimplemented!()
        }
    
        async fn on_call(&mut self, _call: Self::Call) -> Result<(), Error> {
            Ok(())
        }
    }
    
    struct EchoServer;
    
    #[async_trait]
    impl ServerExt for EchoServer {
        type Session = EchoSession;
        type Call = ();
    
        async fn on_connect(
            &mut self,
            socket: Socket,
            request: Request,
            address: SocketAddr,
        ) -> Result<SessionType, Option<CloseFrame>> {
            let id = address.port();
            let session = Session::create(|handle| EchoSession { id, handle }, id, socket);
            Ok(session)
        }
    
        async fn on_disconnect(
            &mut self,
            _id: <Self::Session as SessionExt>::ID,
            _reason: Result<Option<CloseFrame>, Error>,
        ) -> Result<(), Error> {
            Ok(())
        }
    
        async fn on_call(&mut self, _call: Self::Call) -> Result<(), Error> {
            Ok(())
        }
    }
    
    #[tokio::main]
    async fn main() {
        let (server, _handle) = Server::create(|server| EchoServer);
        // Use server.accept(...) with a backend like tungstenite or axum
    }
  6. Use WebSocketUpgrade to establish WebSocket connections in Axum

    master

    The WebSocketUpgrade extractor allows you to upgrade an HTTP connection to a WebSocket connection in an Axum handler. It provides a builder pattern to configure WebSocket parameters like message size, frame size, and supported protocols.

    To use it, include WebSocketUpgrade in your handler arguments. You can then configure the upgrade behavior and provide a callback via .on_upgrade() that receives a WebSocket object. You can also provide a callback via .on_failed_upgrade() to handle errors that occur during the background upgrade process.

    Note that the upgrade process is performed in a background task. The response produced by .on_upgrade() (or .on_failed_upgrade().on_upgrade()) must be returned from your Axum handler.

    use axum::response::Response;
    use ezsockets::axum_tungstenite::WebSocketUpgrade;
    
    async fn handler(ws: WebSocketUpgrade) -> Response {
        ws.on_failed_upgrade(|error| {
            // Handle upgrade failure (e.g., logging)
            report_error(error);
        })
        .on_upgrade(|socket| async { 
            // Handle the established WebSocket connection
            // 'socket' is a WebSocket instance
        })
    }
    
    fn report_error(_: hyper::Error) {}
  7. Connect to a WebSocket server using the default client

    master

    If you are using the native_client feature and running within a tokio runtime, you can use the high-level connect function. This function manages the connection lifecycle and reconnection logic automatically.

    connect returns a tuple containing:

    1. A Client<E> handle used to send messages and calls.
    2. A Future that resolves when the client actor eventually stops (e.g., due to a fatal error or manual closure).

    Note: The client_fn is a closure that takes the Client<E> handle and returns your initialized state implementing ClientExt.

    use ezsockets::{ClientConfig, connect, ClientExt};
    
    struct MyClient {}
    
    #[async_trait::async_trait]
    impl ClientExt for MyClient {
        type Call = ();
        async fn on_text(&mut self, _text: ezsockets::Utf8Bytes) -> Result<(), ezsockets::Error> { Ok(()) }
        async fn on_binary(&mut self, _bytes: ezsockets::Bytes) -> Result<(), ezsockets::Error> { Ok(()) }
        async fn on_call(&mut self, _call: Self::Call) -> Result<(), ezsockets::Error> { Ok(()) }
    }
    
    #[tokio::main]
    async fn main() {
        let config = ClientConfig::new("ws://localhost:8080");
        let (handle, future) = connect(|client| MyClient {}, config).await;
        
        // Run the client in the background
        tokio::spawn(async move {
            let _ = future.await;
        });
    
        // Use the handle to send messages
        handle.text("Hello Server").unwrap();
    }
  8. Integrate ezsockets with Axum using the Upgrade extractor

    master

    To use ezsockets within an Axum application, you must enable the axum feature. You can then use the Upgrade extractor in your route handlers to transition a standard HTTP GET request into a WebSocket connection managed by an ezsockets::Server.

    Requirements:

    1. The route must use the GET method.
    2. The Axum service must be created using .into_make_service_with_connect_info::<SocketAddr>() to ensure the client's SocketAddr is available to the extractor.
    3. The ezsockets::Server instance should be provided to the handler via Axum's Extension layer.

    Workflow:

    1. Create an ezsockets::Server instance.
    2. Add it to your Axum Router using .layer(Extension(server.clone())).
    3. Define a handler that accepts Upgrade and the Extension<Server<E>>.
    4. Call upgrade.on_upgrade(server) and return the resulting Response from your handler.
    use axum::routing::get;
    use axum::Extension;
    use axum::response::IntoResponse;
    use ezsockets::axum::Upgrade;
    use ezsockets::Server;
    
    // ... define your ServerExt and SessionExt implementations ...
    
    #[tokio::main]
    async fn main() {
        let (server, _) = ezsockets::Server::create(|_| EchoServer {});
        let app = axum::Router::new()
            .route("/websocket", get(websocket_handler))
            .layer(Extension(server.clone()));
    
        let address = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
        let listener = tokio::net::TcpListener::bind(address).await.unwrap();
        axum::serve(
            listener,
            app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
        )
        .await
        .unwrap();
    }
    
    async fn websocket_handler(
        Extension(server): Extension<Server<EchoServer>>,
        ezsocket: Upgrade,
    ) -> impl IntoResponse {
        ezsocket.on_upgrade(server)
    }
  9. Run a WebSocket server using `ezsockets::tungstenite::run`

    master

    To run a standard WebSocket server over plain TCP, use ezsockets::tungstenite::run. This requires the tungstenite feature to be enabled in your Cargo.toml. You must provide an instance of ezsockets::Server (created via Server::create) and a target address that implements ToSocketAddrs (e.g., "127.0.0.1:8080").

    Note: This function only works within a tokio runtime.

    #[tokio::main]
    async fn main() {
        // Create the server with your custom ServerExt implementation
        let (server, _) = ezsockets::Server::create(|_| MyServer {});
        
        // Run the tungstenite server on the specified address
        ezsockets::tungstenite::run(server, "127.0.0.1:8080").await.unwrap();
    }
  10. Configure socket heartbeat and timeout

    master

    Use SocketConfig to control the connection's keep-alive behavior.

    • heartbeat: The interval between heartbeat checks.
    • timeout: The maximum duration allowed without new stream messages before the connection is considered timed out and closed.
    • heartbeat_ping_msg_fn: A function used to generate the ping message. By default, it creates a RawMessage::Ping containing the current timestamp in milliseconds as big-endian bytes. This is useful for environments like browsers where standard WebSocket Ping/Pong frames might not be directly accessible.

    Default values are heartbeat: 5s and timeout: 10s.

    let config = SocketConfig {
        heartbeat: Duration::from_secs(5),
        timeout: Duration::from_secs(15),
        heartbeat_ping_msg_fn: Arc::new(|timestamp: Duration| {
            // Custom ping implementation
            RawMessage::Text("ping".into())
        }),
    };
  11. Configure a WebSocket client with ClientConfig

    master

    Use ClientConfig to define connection parameters such as the URL, authentication headers, and reconnection logic.

    Available configuration methods:

    • new(url): Creates a new config. Panics if the URL is invalid. For safety, parse the URL yourself and pass a url::Url.
    • basic(username, password): Sets a Basic Authentication header.
    • bearer(token): Sets a Bearer Authentication header. Panics if the token contains non-ASCII characters.
    • header(key, value): Sets a custom HTTP header.
    • query_parameter(key, value): Appends a query parameter to the connection URI.
    • max_initial_connect_attempts(usize): Sets limit for the first connection attempt (default: infinite).
    • max_reconnect_attempts(usize): Sets limit for subsequent reconnection attempts (default: infinite).
    • reconnect_interval(Duration): Sets the delay between reconnection attempts (default: 5 seconds).
    • socket_config(SocketConfig): Sets low-level socket configuration.

    Defaults:

    • reconnect_interval: Duration::new(5, 0)
    • max_initial_connect_attempts: usize::MAX
    • max_reconnect_attempts: usize::MAX
    let config = ClientConfig::new("ws://localhost:8080/websocket")
        .bearer("my_token")
        .query_parameter("api_key", "12345")
        .reconnect_interval(std::time::Duration::from_secs(10))
        .max_reconnect_attempts(5);
  12. Connect to a WebSocket server in WASM

    master

    To connect to a server using the WASM connector, call the connect method on an instance of ClientConnectorWasm with a ClientConfig containing your target URL.

    // Note: This is a conceptual usage pattern based on the ClientConnector trait implementation
    let connector = ClientConnectorWasm::default();
    let config = ClientConfig::new("wss://example.com/ws");
    
    // This will panic if config.headers() is not empty
    let socket = connector.connect(&config).await?;
    // Conceptual usage based on the ClientConnector trait implementation
    let connector = ClientConnectorWasm::default();
    let config = ClientConfig::new("wss://example.com/ws");
    
    // This will panic if config.headers() is not empty
    let socket = connector.connect(&config).await?;