#websocket-server #bevy-networking #networking #tls-server #gamedev #wasm

nightly bevy_simplenet

Simple server/client channel implemented over websockets with support for Bevy

20 releases (9 breaking)

0.10.0 Apr 11, 2024
0.9.2 Mar 8, 2024
0.9.1 Feb 23, 2024
0.5.3 Dec 19, 2023
0.5.1 Nov 29, 2023

#79 in Game dev

Download history 30/week @ 2023-12-30 16/week @ 2024-01-13 2/week @ 2024-01-20 9/week @ 2024-01-27 163/week @ 2024-02-17 147/week @ 2024-02-24 126/week @ 2024-03-02 46/week @ 2024-03-09 3/week @ 2024-03-16 16/week @ 2024-03-30 105/week @ 2024-04-06

126 downloads per month
Used in bevy_simplenet_events

MIT/Apache

140KB
2K SLoC

Bevy Simplenet

Provides a bi-directional server/client channel implemented over websockets. This crate is suitable for user authentication, talking to a matchmaking service, communicating between micro-services, games that don't have strict latency requirements, etc.

  • Client/server channel includes one-shot messages and a request/response API.
  • Client message statuses can be tracked.
  • Clients automatically work on native and WASM targets.
  • Clients can be authenticated by the server (WIP).
  • Provides optional server TLS.

Check out the example for a demonstration of how to build a Bevy client using this crate. NOTE: The example is currently broken until bevy_lunex updates to Bevy v0.13.

Check out bevy_simplenet_events for an event-based framework for networking that builds on this crate.

This crate requires nightly rust.

Features

  • default: includes bevy, client, server features
  • bevy: derives Resource on Client and Server
  • client: enables clients (native and WASM targets)
  • server: enables servers (native-only targets)
  • tls-rustls: enables TLS for servers via rustls
  • tls-openssl: enables TLS for servers via OpenSSL

WASM

On WASM targets the client backend will not update while any other tasks are running. You must either build an IO-oriented application that naturally spends a lot of time polling tasks, or manually release the main thread periodically (e.g. with web_sys::Window::set_timeout_with_callback_and_timeout_and_arguments_0()). For Bevy apps the latter happens automatically at the end of every app update/tick (see the bevy::app::ScheduleRunnerPlugin implementation).

Usage notes

  • Servers and clients must be created with enfync runtimes. The backend is ezsockets.
  • A client's AuthRequest type must match the corresponding server's Authenticator type.
  • Client ids are defined by clients via their AuthRequest when connecting to a server. This means multiple sessions from the same client will have the same session id. Connections will be rejected if an id is already connected.
  • Client connect messages will be cloned for all reconnect attempts, so they should be treated as static data.
  • Server or client messages may fail to send if the underlying connection is broken. Clients can use the signals returned from Client::send() and Client::request() to track the status of a message. Client request results will always be emitted by Client::next(). Message tracking is not available for servers.
  • Tracing levels assume the server is trusted and clients are not trusted.

Example

Setup

Common

Define a channel.

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TestConnectMsg(pub String);

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TestServerMsg(pub u64);

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TestServerResponse(pub u64);

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TestClientMsg(pub u64);

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TestClientRequest(pub u64);

#[derive(Debug, Clone)]
pub struct TestChannel;
impl ChannelPack for TestChannel
{
    type ConnectMsg = TestConnectMsg;
    type ServerMsg = TestServerMsg;
    type ServerResponse = TestServerResponse;
    type ClientMsg = TestClientMsg;
    type ClientRequest = TestClientRequest;
}

Server

Prepare to make servers.

type TestServerEvent = ServerEventFrom<TestChannel>;

fn server_factory() -> ServerFactory<TestChannel>
{
    // It is recommended to make server/client factories with baked-in protocol versions (e.g.
    //   with env!("CARGO_PKG_VERSION")).
    ServerFactory::<TestChannel>::new("test")
}

Make a server and insert it into an app.

fn setup_server(mut commands: Commands)
{
    let server = server_factory().new_server(
            enfync::builtin::native::TokioHandle::default(),
            "127.0.0.1:0",
            AcceptorConfig::Default,
            Authenticator::None,
            ServerConfig::default(),
        );
    commands.insert_resource(server);
}

Client

Prepare to make clients.

type TestClientEvent = ClientEventFrom<TestChannel>;

fn client_factory() -> ClientFactory<TestChannel>
{
    // You must use the same protocol version string as the server factory.
    ClientFactory::<TestChannel>::new("test")
}

Make a client and insert it into an app.

fn setup_client(mut commands: Commands)
{
    let client_id = 0u128;
    let client = client_factory().new_client(
            enfync::builtin::Handle::default(),  //automatically selects native/WASM runtime
            server.url(),
            AuthRequest::None{ client_id },
            ClientConfig::default(),
            TestConnectMsg(String::from("hello"))
        );
    commands.insert_resource(client);
}

Sending from the client

Send a message.

fn send_client_message(client: Client<TestChannel>)
{
    let message_signal = client.send(TestClientMsg(42));
}

Send a request.

fn send_client_request(client: Client<TestChannel>)
{
    let request_signal = client.request(TestClientRequest(24));
}

Sending from the Server

Send a message.

fn send_server_message(server: Server<TestChannel>)
{
    server.send(0u128, TestServerMsg(111));
}

Send a response.

fn send_server_response(In(token): In<RequestToken>, server: Server<TestChannel>)
{
    server.respond(token, TestServerResponse(1));
}

Reading on the client

fn read_on_client(client: &mut Client<TestChannel>)
{
    while let Some(client_event) = client.next()
    {
        match client_event
        {
            TestClientEvent::Report(connection_report) => match connection_report
            {
                ClientReport::Connected                => todo!(),
                ClientReport::Disconnected             => todo!(),
                ClientReport::ClosedByServer(reason)   => todo!(),
                ClientReport::ClosedBySelf             => todo!(),
                ClientReport::IsDead(pending_requests) => todo!(),
            }
            TestClientEvent::Msg(message)                   => todo!(),
            TestClientEvent::Response(response, request_id) => todo!(),
            TestClientEvent::Ack(request_id)                => todo!(),
            TestClientEvent::Reject(request_id)             => todo!(),
            TestClientEvent::SendFailed(request_id)         => todo!(),
            TestClientEvent::ResponseLost(request_id)       => todo!(),
        }
    }
}

Reading on the server

fn read_on_server(server: &mut Server<TestChannel>)
{
    while let Some((session_id, server_event)) = server.next()
    {
        match server_event
        {
            TestServerEvent::Report(connection_report) => match connection_report
            {
                ServerReport::Connected(env, message) => todo!(),
                ServerReport::Disconnected            => todo!(),
            }
            TestServerEvent::Msg(message)            => todo!(),
            TestServerEvent::Request(token, request) => todo!(),
        }
    }
}

TODOs

  • Fix linker errors when the bevy/dynamic_linking feature is enabled.
  • Implement AuthToken for client/server authentication.
  • Add server shut down procedure.
  • Use const generics to bake protocol versions into Server and Client directly, instead of relying on factories (currently blocked by lack of robust compiler support).
  • Move to stable rust once HashMap::extract_if() is stabilized.

Bevy compatability

bevy bevy_simplenet
0.13 v0.9.0 - master
0.12 v0.5.0 - v0.8.0
0.11 v0 - v0.4.0

Dependencies

~5–23MB
~333K SLoC