ezsockets icon indicating copy to clipboard operation
ezsockets copied to clipboard

High-level declarative API for building WebSocket Clients and Servers in Rust 🦀

ezsockets

Have you ever struggle with creating a WebSocket server or a client in Rust? This crate is for you.

  • High level abstraction of WebSocket, handling Ping/Pong from both Client and Server.
  • Use of traits to allow declarative and event-based programming.
  • Automatic reconnection of WebSocket Client.

Client

The code below represents simple client that redirects stdin to the WebSocket server.

use async_trait::async_trait;
use ezsockets::ClientConfig;
use std::io::BufRead;
use url::Url;

struct Client {}

#[async_trait]
impl ezsockets::ClientExt for Client {
    type Params = ();

    async fn text(&mut self, text: String) -> Result<(), ezsockets::Error> {
        tracing::info!("received message: {text}");
        Ok(())
    }

    async fn binary(&mut self, bytes: Vec<u8>) -> Result<(), ezsockets::Error> {
        tracing::info!("received bytes: {bytes:?}");
        Ok(())
    }

    async fn call(&mut self, params: Self::Params) -> Result<(), ezsockets::Error> {
        let () = params;
        Ok(())
    }
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();
    let url = Url::parse("ws://localhost:8080/websocket").unwrap();
    let config = ClientConfig::new(url);
    let (handle, future) = ezsockets::connect(|_client| Client { }, config).await;
    tokio::spawn(async move {
        future.await.unwrap();
    });
    let stdin = std::io::stdin();
    let lines = stdin.lock().lines();
    for line in lines {
        let line = line.unwrap();
        tracing::info!("sending {line}");
        handle.text(line).await;
    }
}

Server

To create a simple echo server, you'll need to define a Session struct. The code below represents a simple echo server.

use async_trait::async_trait;
use ezsockets::Session;

type SessionID = u16;

struct EchoSession {
    handle: Session,
    id: SessionID,
}

#[async_trait]
impl ezsockets::SessionExt for EchoSession {
    type ID = SessionID;
    type Args = ();
    type Params = ();

    fn id(&self) -> &Self::ID {
        &self.id
    }

    async fn text(&mut self, text: String) -> Result<(), ezsockets::Error> {
        self.handle.text(text).await; // Send response to the client
        Ok(())
    }

    async fn binary(&mut self, _bytes: Vec<u8>) -> Result<(), ezsockets::Error> {
        unimplemented!()
    }

    async fn call(&mut self, params: Self::Params) -> Result<(), ezsockets::Error> {
        let () = params;
        Ok(())
    }
}

Then, we need to define a Server struct

use async_trait::async_trait;
use ezsockets::Server;
use ezsockets::Session;
use ezsockets::Socket;
use std::net::SocketAddr;

struct EchoServer {}

#[async_trait]
impl ezsockets::ServerExt for EchoServer {
    type Session = EchoSession;
    type Params = ();

    async fn accept(
        &mut self,
        socket: Socket,
        address: SocketAddr,
        _args: (),
    ) -> Result<Session, ezsockets::Error> {
        let id = address.port();
        let session = Session::create(|handle| EchoSession { id, handle }, id, socket);
        Ok(session)
    }

    async fn disconnected(
        &mut self,
        _id: <Self::Session as ezsockets::SessionExt>::ID,
    ) -> Result<(), ezsockets::Error> {
        Ok(())
    }

    async fn call(&mut self, params: Self::Params) -> Result<(), ezsockets::Error> {
        let () = params;
        Ok(())
    }
}

That's all! Now we can start the server. Take a look at the available Server back-ends. For a simple usage, I'd recommend tokio-tungstenite.

Server back-ends

  • [x] tokio-tungstenite, a neat Tokio based WebSocket implementation. However, it does not provide fancy features like routing or authentication.
  • [x] axum, an ergonomic and modular web framework built with Tokio, Tower, and Hyper.
  • [ ] actix-web a powerful, pragmatic, and extremely fast web framework for Rust.

tokio-tungstenite

Enable using

ezsockets = { version = "0.3", features = ["tungstenite"] }
struct EchoServer {}

#[async_trait]
impl ezsockets::ServerExt for EchoServer {
    // ...
}

#[tokio::main]
async fn main() {
    let server = ezsockets::Server::create(|_| EchoServer {});
    ezsockets::tungstenite::run(server, "127.0.0.1:8080", |_socket| async move { Ok(()) })
        .await
        .unwrap();
}

axum

Enable using

ezsockets = { version = "0.3", features = ["axum"] }
struct EchoServer {}

#[async_trait]
impl ezsockets::ServerExt for EchoServer {
    // ...
}

#[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));

    tokio::spawn(async move {
        tracing::debug!("listening on {}", address);
        axum::Server::bind(&address)
            .serve(app.into_make_service_with_connect_info::<SocketAddr>())
            .await
            .unwrap();
    });

}

async fn websocket_handler(
    Extension(server): Extension<ezsockets::Server>,
    ezsocket: Upgrade,
) -> impl IntoResponse {
    ezsocket.on_upgrade(|socket, address| async move {
        server.accept(socket, address, ()).await;
    })
}

actix-web

Work in progress!