socketioxide

repository·main·Indexed 23 days ago

https://github.com/totodore/socketioxide

A high-performance Socket.io server implementation for Rust built on the Tokio and Tower ecosystems. It is compatible with web frameworks such as Axum, Warp, Salvo, Viz, and Hyper. Socketioxide supports namespaces, rooms, binary packets, polling, and websocket transports, with horizontal scaling capabilities via pluggable adapters for Redis, MongoDB, and PostgreSQL. It includes engineioxide, an Engine.io server implementation as a Tower Service.

Tokens
42.1K
Snippets
73
Records
208
Agent score
81%

What's inside socketioxide

  1. What is Engineioxide?

    main
    Engineioxide is the core engine that powers socketioxide. It provides the heavy lifting for Socket.io server implementations in Rust and is designed to integrate seamlessly with the tower stack. While it is primarily used as a dependency for socketioxide, it can also be used as a standalone crate to communicate with an Engine.io client.
  2. Overview of Socketioxide

    main
    Socketioxide is a Socket.io server implementation for Rust that integrates with the Tokio stack and the Tower ecosystem. It is designed to work seamlessly with popular tower-based web frameworks like Axum, Warp, Salvo, Viz, or Hyper. It supports standard Socket.io features such as namespaces, rooms, binary packets, and both polling and websocket transports. It also allows for horizontal scaling via pluggable adapters like Redis, MongoDB, or PostgreSQL.
  3. Socketioxide Scaling and Middleware

    main

    Horizontal Scaling

    You can achieve effortless horizontal scaling by using pluggable adapters:

    • socketioxide-redis (Redis / Valkey)
    • socketioxide-mongodb (MongoDB)
    • socketioxide-postgres (PostgreSQL)

    Middleware

    Because Socketioxide integrates with the Tower ecosystem, you can use any tower-http middleware, such as:

    • CORS
    • Compression
    • Authorization
  4. Broadcast messages to sockets

    main

    Socketioxide provides two ways to broadcast messages depending on whether you want to include the sender (the current socket) in the broadcast:

    1. Exclude the current socket: Use the .broadcast() operator on a SocketRef. This sends the message to all sockets in the current namespace except the one that triggered the handler.
    2. Include the current socket: Use the .emit() method directly on the SocketIo global context. This sends the message to all sockets in the namespace, including the sender.

    Both methods are asynchronous and require .await.

    async fn handler(io: SocketIo, socket: SocketRef, Data(data): Data::<Value>) {
        // This message will be broadcast to all sockets in this namespace except this one.
        socket.broadcast().emit("test", &data).await;
    
        // This message will be broadcast to all sockets in this namespace, including this one.
        io.emit("test", &data).await;
    }
  5. Emit messages with volatile behavior

    main

    You can use the .volatile() operator to emit events that may be dropped if the client is not ready to receive them (e.g., if the connection is buffering or disconnected). This is ideal for non-critical data like real-time position updates in games.

    Important Considerations:

    • Ordering: Volatile events use a separate channel that bypasses the main mpsc buffer. Consequently, they may arrive out of order relative to regular events emitted around the same time. Only use this when ordering relative to regular events is not important.
    • Acknowledgements: The volatile operator has no effect if used with emit_with_ack().
    #[derive(Serialize)]
    struct GameState { x: f64, y: f64 }
    
    let (_, io) = SocketIo::new_svc();
    io.ns("/", async |socket: SocketRef| {
        // Direct volatile emit — may be dropped if the socket is not ready
        socket.volatile().emit("position", &GameState { x: 1.0, y: 2.0 }).ok();
    
        // Volatile broadcast to a room
        socket.volatile().to("game_room").emit("update", &42).await.ok();
    });
  6. Integrate Socketioxide with Axum

    main

    To use Socketioxide with Axum, you can use SocketIo::new_layer() to create a Tower layer that can be added to an Axum Router. This allows you to handle Socket.io connections alongside your standard HTTP routes. You can define namespaces using io.ns() and attach event handlers to them.

    use axum::routing::get;
    use serde_json::Value;
    use socketioxide::{
        extract::{AckSender, Bin, Data, SocketRef},
        SocketIo,
    };
    use tracing::info;
    use tracing_subscriber::FmtSubscriber;
    
    fn on_connect(socket: SocketRef, Data(data): Data<Value>) {
        info!("Socket.IO connected: {:?} {:?}", socket.ns(), socket.id);
        socket.emit("auth", data).ok();
    
        socket.on(
            "message",
            |socket: SocketRef, Data::<Value>(data), Bin(bin)| {
                info!("Received event: {:?} {:?}", data, bin);
                socket.bin(bin).emit("message-back", data).ok();
            },
        );
    
        socket.on(
            "message-with-ack",
            |Data::<Value>(data), ack: AckSender, Bin(bin)| {
                info!("Received event: {:?} {:?}", data, bin);
                ack.bin(bin).send(data).ok();
            },
        );
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        tracing::subscriber::set_global_default(FmtSubscriber::default())?;
    
        let (layer, io) = SocketIo::new_layer();
    
        io.ns("/", on_connect);
        io.ns("/custom", on_connect);
    
        let app = axum::Router::new()
            .route("/", get(|| async { "Hello, World!" }))
            .layer(layer);
    
        info!("Starting server");
    
        let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
        axum::serve(listener, app).await.unwrap();
    
        Ok()
    }
  7. Emit messages to clients

    main

    Use the emit method to send data to one or many clients.

    Argument Handling

    • Multiple Arguments: If you provide tuple-like data (such as tuples or arrays), they are treated as multiple distinct arguments in the payload.
    • Sending an Array as the First Argument: To send an array as the single first argument of a payload, you must wrap it in another array or a tuple.
    • Vec Behavior: A Vec is always treated as a single argument.

    Broadcasting and Scaling

    When using broadcast methods (like .to(), .except(), etc.), the emit method returns a Future that must be awaited. This is because socketioxide may need to communicate with remote instances when using horizontal scaling via remote adapters.

  8. Emit binary data

    main

    To emit binary data correctly, you must use a data type that implements Serialize as binary data.

    Warning: Using Vec<u8> will result in the data being treated as a sequence of numbers rather than actual binary data.

    1. Use Bytes: Use the bytes::Bytes crate.
    2. Use serde_bytes: Use the serde_bytes crate.
    3. Generic Binary Data: If you need to emit generic data that might contain binary, use rmpv::Value instead of serde_json::Value. serde_json::Value will serialize binary data as a sequence of numbers.
    use bytes::Bytes;
    
    // Emitting multiple arguments including binary data
    async fn handler(socket: SocketRef, Data(data): Data::<(String, Bytes, Bytes)>) {
        socket.emit("test", &("world", "hello", Bytes::from_static(&[1, 2, 3, 4]))).ok();
    }
  9. Broadcast to all sockets on the local node using `.local()`

    main

    The .local() operator allows you to broadcast messages to all sockets within a namespace that are connected specifically to the current node.

    Important Note: When using the default in-memory adapter, this operator behaves as a no-op (it does nothing) because all sockets are inherently on the same node. This operator is primarily useful when using distributed adapters (like Redis) to limit the scope of a broadcast to the local instance rather than the entire cluster.

    # use socketioxide::{SocketIo, extract::*};
    # use serde_json::Value;
    async fn handler(socket: SocketRef, Data(data): Data::<Value>) {
        // This message will be broadcast to all sockets in this
        // namespace that are connected to this node
        socket.local().emit("test", &data).await;
    }
    
    let (_, io) = SocketIo::new_svc();
    io.ns("/", async |s: SocketRef| s.on("test", handler));
  10. Perform actions directly using operators instead of fetching sockets

    main

    When you need to act on a group of sockets (e.g., emitting an event or disconnecting them), do not fetch the sockets into a list to iterate over them. Instead, chain the action directly to the operator chain. This is the correct and more efficient approach.

    Incorrect (Inefficient): Fetching sockets and then looping to emit/leave.

    Correct (Efficient): Chaining .emit() or .disconnect() directly to the selection operator.

    // Correct Approach: Chain actions directly
    io.within("room1").emit("foo", "bar").await.unwrap();
    io.within("room1").disconnect().await.unwrap();
    
    // Incorrect Approach: Fetching and looping
    let sockets = io.within("room1").fetch_sockets().await.unwrap();
    for socket in sockets {
        socket.emit("test", &"Hello").await.unwrap();
        socket.leave("room1").await.unwrap();
    }