Crate socketioxide

source ·
Expand description

Socketioxide is a socket.io server implementation that works as a tower layer/service. It integrates nicely with the rest of the tower/tokio/hyper ecosystem.

§Table of contents

§Features

  • Easy to use flexible axum-like API
  • Fully compatible with the official socket.io client
  • Support for the previous version of the protocol (v4).
  • State Management
  • Namespaces
  • Rooms
  • Acknowledgements
  • Polling & Websocket transports

§Compatibility

Because it works as a tower layer/service or an hyper service you can use it with any http server frameworks that works with tower/hyper:

  • Axum
  • Warp (Not supported with socketioxide >= 0.9.0 as long as warp doesn’t migrate to hyper v1)
  • Hyper
  • Salvo

Check the examples for more details on frameworks integration.

§Usage

The API tries to mimic the equivalent JS API as much as possible. The main difference is that the default namespace / is not created automatically, you need to create it manually.

§Basic example with axum:
use axum::routing::get;
use socketioxide::{
    extract::SocketRef,
    SocketIo,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (layer, io) = SocketIo::new_layer();

    // Register a handler for the default namespace
    io.ns("/", |s: SocketRef| {
        // For each "message" event received, send a "message-back" event with the "Hello World!" event
        s.on("message", |s: SocketRef| {
            s.emit("message-back", "Hello World!").ok();
        });
    });

    let app = axum::Router::new()
    .route("/", get(|| async { "Hello, World!" }))
    .layer(layer);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();

    Ok(())
}

§Initialisation

The SocketIo struct is the main entry point of the library. It is used to create a Layer or a Service. Later it can be used as the equivalent of the io object in the JS API.

When creating your SocketIo instance, you can use the builder pattern to configure it with the SocketIoBuilder struct.

  • See the SocketIoBuilder doc for more details on the available configuration options.
  • See the layer module doc for more details on layers.
  • See the service module doc for more details on services.
§Tower layer example with custom configuration:
use socketioxide::SocketIo;
let (layer, io) = SocketIo::builder()
    .max_payload(10_000_000) // Max HTTP payload size of 10M
    .max_buffer_size(10_000) // Max number of packets in the buffer
    .build_layer();
§Tower standalone service example with default configuration:
use socketioxide::SocketIo;
let (svc, io) = SocketIo::new_svc();

§Handlers

Handlers are functions or clonable closures that are given to the io.ns, the socket.on and the socket.on_disconnect fns. They can be async or sync and can take from 0 to 16 arguments that implements the FromConnectParts trait for the ConnectHandler, the FromMessageParts for the MessageHandler and the FromDisconnectParts for the DisconnectHandler. They are greatly inspired by the axum handlers.

If they are async, a new task will be spawned for each incoming connection/message so it doesn’t block the event management task.

  • Check the handler::connect module doc for more details on the connect handler and connect middlewares.
  • Check the handler::message module doc for more details on the message handler.
  • Check the handler::disconnect module doc for more details on the disconnect handler.
  • Check the handler::extract module doc for more details on the extractors.

§Extractors

Handlers params are called extractors and are used to extract data from the incoming connection/message. They are inspired by the axum extractors. An extractor is a struct that implements the FromConnectParts trait for the ConnectHandler the FromMessageParts for the MessageHandler and the FromDisconnectParts for the DisconnectHandler.

Here are some examples of extractors:

  • Data: extracts and deserialize to json any data, if a deserialize error occurs the handler won’t be called
  • TryData: extracts and deserialize to json any data but with a Result type in case of error
  • SocketRef: extracts a reference to the Socket
  • Bin: extract a binary payload for a given message. Because it consumes the event it should be the last argument
  • AckSender: Can be used to send an ack response to the current message event
  • ProtocolVersion: extracts the protocol version of the socket
  • TransportType: extracts the transport type of the socket
  • DisconnectReason: extracts the reason of the disconnection
  • State: extracts a reference to a state previously set with SocketIoBuilder::with_state.

§Extractor order

Extractors are run in the order of their declaration in the handler signature. If an extractor returns an error, the handler won’t be called and a tracing::error! call will be emitted if the tracing feature is enabled.

For the MessageHandler, some extractors require to consume the event and therefore only implement the FromMessage trait, like the Bin extractor, therefore they should be the last argument.

Note that any extractors that implement the FromMessageParts also implement by default the FromMessage trait.

§Events

There are three types of events:

  • The connect event is emitted when a new connection is established. It can be handled with the ConnectHandler and the io.ns method.
  • The message event is emitted when a new message is received. It can be handled with the MessageHandler and the socket.on method.
  • The disconnect event is emitted when a socket is closed. It can be handled with the DisconnectHandler and the socket.on_disconnect method.

Only one handler can exist for an event so registering a new handler for an event will replace the previous one.

§Middlewares

When providing a ConnectHandler for a namespace you can add any number of ConnectMiddleware in front of it. It is useful to add authentication or logging middlewares.

A middleware must return a Result<(), E> where E: Display.

  • If the result is Ok(()), the next middleware is called or if there is no more middleware, the socket is connected and the ConnectHandler is called.
  • If the result is an error, the namespace connection will be refused and the error will be returned with a connect_error event and a message field with the error.
Because the socket is not yet connected to the namespace, you can't send messages to it from the middleware.
See the [`handler::connect`](handler::connect#middleware) module doc for more details on middlewares and examples.

§Emiting data

Data can be emitted to a socket with the Socket::emit method. It takes an event name and a data argument. The data argument can be any type that implements the serde::Serialize trait.

You can emit from the SocketIo handle or the SocketRef. The difference is that you can move the io handle everywhere because it is a cheaply cloneable struct. The SocketRef is a reference to the socket and cannot be cloned.

Moreover the io handle can emit to any namespace while the SocketRef can only emit to the namespace of the socket.

When using any emit fn, if you provide array-like data (tuple, vec, arrays), it will be considered as multiple arguments. Therefore if you want to send an array as the first argument of the payload, you need to wrap it in an array or a tuple.

§Emit errors

If the data can’t be serialized to json, an serde_json::Error will be returned.

If the socket is disconnected or the internal channel is full, a SendError will be returned and the provided data will be given back. Moreover, a tracing log will be emitted if the tracing feature is enabled.

§Emitting with operators

To configure the emit, you can chain Operators methods to the emit call. With that you can easily configure the following options:

  • rooms: emit, join, leave to specific rooms
  • namespace: emit to a specific namespace (only from the SocketIo handle)
  • timeout: set a custom timeout when waiting for an ack
  • binary: emit a binary payload with the message
  • local: broadcast only to the current node (in case of a cluster)

Check the operators module doc for more details on operators.

§Acknowledgements

You can ensure that a message has been received by the client/server with acknowledgements.

§Server acknowledgements

They are implemented with the AckSender extractor. You can send an ack response with an optional binary payload with the AckSender::send method. If the client doesn’t send an ack response, the AckSender::send method will do nothing.

§Client acknowledgements

If you want to emit/broadcast a message and await for a/many client(s) acknowledgment(s) you can use:

§State management

There are two ways to manage the state of the server:

§Per socket state

You can enable the extensions feature and use the extensions field on any socket to manage the state of each socket. It is backed by a dashmap so you can safely access it from multiple threads. Beware that deadlocks can easily occur if you hold a value ref and try to remove it at the same time. See the extensions module doc for more details.

§Global state

You can enable the state feature and use SocketIoBuilder::with_state method to set multiple global states for the server. You can then access them from any handler with the State extractor.

Because the global state is staticaly defined, beware that the state map will exist for the whole lifetime of the program even if you drop everything and close you socket.io server. This is a limitation because of the impossibility to have extractors with lifetimes, therefore state references must be 'static.

Another limitation is that because it is common to the whole server. If you build a second server, it will share the same state. Also if the first server is already started you won’t be able to add new states because states are frozen at the start of the first server.

§Adapters

This library is designed to work with clustering. It uses the Adapter trait to abstract the underlying storage. By default it uses the LocalAdapter which is a simple in-memory adapter. Currently there is no other adapters available but more will be added in the future.

§Feature flags

  • v4: enable support for the socket.io protocol v4
  • tracing: enable logging with tracing calls
  • extensions: enable per-socket state with the extensions module
  • state: enable global state management

Re-exports§

Modules§

  • Acknowledgement related types and functions.
  • Adapters are responsible for managing the state of the server. When a socket joins or leaves a room, the adapter is responsible for updating the state. The default adapter is the LocalAdapter, which stores the state in memory. Other adapters can be made to share the state between multiple servers.
  • extensionsextensions
    Extensions used to store extra data in each socket instance.
  • Functions and types used to handle incoming connections and messages. There is three main types of handlers: ConnectHandler, MessageHandler and DisconnectHandler. All handlers can be async or not.
  • A tower Layer for socket.io so it can be used as a middleware with frameworks supporting layers.
  • Operators are used to select sockets to send a packet to, or to configure the packet that will be emitted.
  • Socket.io packet implementation. The Packet is the base unit of data that is sent over the engine.io socket. It should not be used directly except when implementing the Adapter trait.
  • A Tower Service and Hyper Service for socket.io so it
  • A Socket represents a client connected to a namespace. The socket struct itself should not be used directly, but through a SocketRef.

Structs§

Enums§