#events #client-connection #networking #server-connection #gamedev

bevy_simplenet_events

Event-based networking API built on bevy_simplenet

6 releases (3 breaking)

0.3.0 Feb 23, 2024
0.2.0 Feb 1, 2024
0.1.2 Jan 19, 2024
0.0.1 Jan 2, 2024

#334 in Game dev

47 downloads per month

MIT/Apache

84KB
1.5K SLoC

Bevy Simplenet Events

Provides an events-based API for handling a networked connection, built on top of bevy_simplenet.

This crate requires nightly rust.

Usage notes

  • Client connection events, client/server message events, and server responses can be iterated with event readers in multiple systems. Client requests can be drained with ServerRequestSource in one system.
  • An event 'channel' of a single type is FIFO, however different event channels will not be synchronized with each other. This crate is not well-suited for users who want global FIFO ordering for all client/server outputs (use bevy_simplenet directly instead).
  • We assume the user's connection-event handlers are scheduled after RefreshSet in schedule First and before other event handlers.
  • Events must be registered in the same order on the server and client.

Synchronization guarantees

This crate's API is highly opinionated to facilitate precise handling of reconnects.

We update the client and server state every tick in RefreshSet in schedule First. All old events are cleared, and new events are inserted. If a user's connection-event handlers are scheduled before other event handlers as expected, then we guarantee the following:

Clients

  • ClientMessageReader will only read server messages from the current connection session. Old messages (from before the last disconnect) are discarded.
  • ClientResponseReader will only emit ServerResponse::Response or ServerResponse::Ack for responses received in the current connection session. All other responses will fail with one of the response-fail variants (rejected/send failed/response lost). Note that we guarantee a response of some kind will be emitted for every client request sent.
  • Client messages/requests will silently fail to send or error-out if the most recent ClientReport::Connected has not been read by ClientConnectionReader at least once (TODO: there is an upstream race condition), or if the client is not connected. Message statuses can be monitored with the MessageSignal returned from EventClient::send, and request statuses can be monitored with the RequestSignal returned from EventClient::request or you can wait for a result to be emitted as an event. We include this guarantee to reduce the chance of clients sending messages based on stale client state while in the middle of handling connection events.

Servers

  • ServerMessageReader and ServerRequestSource will only read client messages and requests from a client's current connection session. Old messages (from before the last disconnect) will be discarded.
  • Server messages for a client will silently fail to send or error-out if the most recent ServerReport::Connected for that client has not been read by ServerConnectionReader at least once (TODO: there is an upstream race condition), or if the client is not connected. We include this guarantee to reduce the chance of servers sending messages based on stale server state while in the middle of handling connection events. Note that responses from old connection sessions always fail to send to new sessions.

Performance

This crate is less efficient than bevy_simplenet.

  • Events are serialized and deserialized twice to enable ad-hoc event types.
  • The client and server have additional indirection and copying to transmit messages from the internal client/server to the user.
  • Events are exposed by reference rather than by value (except for client requests, which are drained by value on the server).

Creating a channel

Shared

Prepare message types and the channel tag that implements EventPack.

#[derive(SimplenetEvent, Serialize, Deserialize)]
struct DemoMsg1(usize);

#[derive(SimplenetEvent, Serialize, Deserialize)]
struct DemoMsg2(usize);

#[derive(SimplenetEvent, Serialize, Deserialize)]
struct DemoRequest(usize);

#[derive(SimplenetEvent, Serialize, Deserialize)]
struct DemoResponse(usize);

#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
struct DemoConnectMsg(String);

#[derive(Debug, Clone)]
struct DemoChannel;
impl EventPack for DemoChannel
{
    type ConnectMsg = DemoConnectMsg;
}

Prepare event setup function. This should be called on both the server and client apps.

fn event_setup(app: &mut App)
{
    app
        .register_simplenet_client_message::<DemoChannel, DemoMsg1>()
        .register_simplenet_client_message::<DemoChannel, DemoMsg2>()

        .register_simplenet_server_message::<DemoChannel, DemoMsg1>()
        .register_simplenet_server_message::<DemoChannel, DemoMsg2>()

        .register_simplenet_request_response::<DemoChannel, DemoRequest, DemoResponse>()
        ;
}

Server

Prepare server factory.

type DemoServerReport = bevy_simplenet::ServerReport<DemoConnectMsg>;

fn demo_server_factory() -> bevy_simplenet::ServerFactory<EventWrapper<DemoChannel>>
{
    bevy_simplenet::ServerFactory::<EventWrapper<DemoChannel>>::new("test")
}

Prepare server setup function (example).

fn setup_server(app: &mut App) -> url::Url
{
    let server = demo_server_factory().new_server(
            enfync::builtin::native::TokioHandle::adopt_or_default(),
            "127.0.0.1:0",
            bevy_simplenet::AcceptorConfig::Default,
            bevy_simplenet::Authenticator::None,
            bevy_simplenet::ServerConfig::default(),
        );
    let url = server.url();

    app.insert_simplenet_server(server);
    event_setup(app);

    url
}

Client

Prepare client factory.

fn demo_client_factory() -> bevy_simplenet::ClientFactory<EventWrapper<DemoChannel>>
{
    bevy_simplenet::ClientFactory::<EventWrapper<DemoChannel>>::new("test")
}

Prepare client setup function (example).

fn setup_client(app: &mut App, url: url::Url, client_id: SessionId, connect_msg: DemoConnectMsg)
{
    let client = demo_client_factory().new_client(
            enfync::builtin::Handle::adopt_or_default(),
            url,
            bevy_simplenet::AuthRequest::None{ client_id },
            bevy_simplenet::ClientConfig::default(),
            connect_msg
        );

    app.insert_simplenet_client(client);
    event_setup(app);
}

Handling connections in the client

Client connection reports must be handled before all other client events each tick.

fn handle_client_connection_reports(reader: ClientConnectionReader<DemoChannel>)
{
    for connection in reader.iter()
    {
        match connection
        {
            bevy_simplenet::ClientReport::Connected         => todo!(),
            bevy_simplenet::ClientReport::Disconnected      => todo!(),
            bevy_simplenet::ClientReport::ClosedByServer(_) => todo!(),
            bevy_simplenet::ClientReport::ClosedBySelf      => todo!(),
            bevy_simplenet::ClientReport::IsDead(_)         => todo!(),
        }
    }
}

Handling connections in the server

Server connection reports must be handled before all other server events each tick.

fn handle_server_connection_reports(reader: ServerConnectionReader<DemoChannel>)
{
    for (session_id, connection) in reader.iter()
    {
        match connection
        {
            bevy_simplenet::ServerReport::<DemoConnectMsg>::Connected(_, _) => todo!(),
            bevy_simplenet::ServerReport::<DemoConnectMsg>::Disconnected    => todo!(),
        }
    }
}

Sending from the client

Any registered message type can be sent.

fn send_client_message(client: EventClient<DemoChannel>)
{
    client.send(DemoMsg1(42));
    client.send(DemoMsg2(24));
}

Sending from the server

Any registered message type can be sent.

fn send_server_message(In(session_id): In<SessionId>, server: EventServer<DemoChannel>)
{
    server.send(session_id, DemoMsg1(42)).unwrap();
    server.send(session_id, DemoMsg2(24)).unwrap();
}

Reading on the server

Client messages

fn read_client_messages(reader: ServerMessageReader<DemoChannel, DemoMsg1>)
{
    for (session_id, message) in reader.iter()
    {
        todo!()
    }
}

Client requests

Draining a request source consumes all requests, since we expect you to do something with the request token.

fn read_client_requests(source: ServerRequestSource<DemoChannel, DemoRequest1, DemoResponse1>)
{
    for (token, request) in source.drain()
    {
        todo!()
    }
}

Reading on the client

Server messages

fn read_server_messages(reader: ClientMessageReader<DemoChannel, DemoMsg1>)
{
    for message in reader.iter()
    {
        todo!()
    }
}

Server responses

fn read_server_responses(reader: ClientResponseReader<DemoChannel, DemoRequest1, DemoResponse1>)
{
    for response in reader.iter()
    {
        match response
        {
            ServerResponse::Response(response, _) => todo!(),
            ServerResponse::Ack(_)                => todo!(),
            ServerResponse::Reject(_)             => todo!(),
            ServerResponse::SendFailed(_)         => todo!(),
            ServerResponse::ResponseLost(_)       => todo!(),
        }
    }
}

Bevy compatability

bevy bevy_simplenet_events
0.13 v0.3.0 - master
0.12 v0.1.0 - v0.2.0

Dependencies

~22–58MB
~1M SLoC