ezsockets 0.1.0

WebSockets server & client made easy
Documentation

ezsockets

Have you ever had troubles building a WebSocket server or a client in Rust? This crate might come very handy.

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

Client

NOTE: Enable client feature to use it.

Simplest client that redirects stdin to the WebSocket server can be represented by the following code:

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

struct Client {}

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

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

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

    async fn closed(&mut self) -> Result<(), BoxError> {
        Ok(())
    }

    async fn call(&mut self, message: Self::Message) {
        match message {
            () => {}
        }
    }
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();
    let url = format!("ws://127.0.0.1:8080");
    let url = Url::parse(&url).unwrap();
    let config = ClientConfig::new(url);
    let (handle, future) = ezsockets::connect(|_| 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

NOTE: Enable server-<backend> feature to use it.

To create a simple echo server, you'll need to first define a Session struct Simplest echo server can be represented by the following code:

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

type SessionID = u16;

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

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

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

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

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

After that, we'll also need a Server struct

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

struct EchoServer {}

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

    async fn accept(
        &mut self,
        socket: Socket,
        address: SocketAddr,
    ) -> Result<Session, BoxError> {
        let handle = Session::create(
            |handle| EchoSession {
                // use port as the SessionID, since we don't have any other meaningful information about the client
                id: address.port(),
                handle,
            },
            socket,
        );
        Ok(handle)
    }

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

    async fn message(&mut self, message: Self::Message) {
        match message {
            () => {}
        };
    }
}

And that's it! We got that, now we need to start the server somehow, take a look at available Server back-ends, for simplest usage, I'd recommend tokio-tungstenite

Server back-ends

  • tokio-tungstenite, a Tokio based WebSocket implementation, althought it's not possible to use fancy features, like routing or authentication, because it does not use T
  • 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

struct MyServer {}

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

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

axum

struct MyServer {}

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

#[tokio::main]
async fn main() {
    let server = ezsockets::Server::create(|_| MyServer {}).await;
    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<MyServer>>,
    ezsocket: Upgrade,
) -> impl IntoResponse {
    ezsocket.on_upgrade(|socket, address| async move {
        server.accept(socket, address).await;
    })
}

actix-web

Work in progress!