johan helsing.studio

Announcing Matchbox 0.6

Matchbox is a solution for painless peer-to-peer networking in rust web assembly (and native!). 0.6 is its biggest release yet. We added support for multiple data channels with configurable reliability, support for peer disconnections, a bevy plugin, a new signaling server crate, and fixed quite a few bugs.

What is Matchbox?

I created Matchbox because I wanted to use bevy_ggrs on wasm, and for that I needed a socket capable of doing unreliable, unordered full-mesh peer-to-peer connections, similar to std::net::UdpSocket. The only way to do that with web technology was with WebRTC’s data channels, and since setting it up is not exactly trivial, I made a library for it, Matchbox, to make this use case dead simple to implement for others.

Widening the scope of the project

Since then, a number of extra features have been requested, and I think it’s a balancing act between supporting new use cases while maintaining simplicity for the original use case of the API.

For instance, in 0.5 we completed full support for native sessions, including cross-platform connections, meaning it's definitely not a WASM-only crate anymore.

Adding support for multiple reliable and unreliable channels

Another frequently requested feature, is the ability to send reliable and ordered messages between peers. On one hand the original focus of Matchbox was to enable UDP-like, unordered, unreliable messages, however, in practice, at least with the GGRS use case, you usually still need a way to send some reliable data; be it chat messages, seeds, player names, or status updates. So opening additional channels with different reliability guarantees was a relatively tempting and low-hanging fruit.

Heartlabs picked up this task, and restructured the internals of Matchbox so it now supports an arbitrary number of data channels with custom config for reliability and ordering. And garryod improved the user-facing APIs by using a builder pattern for constructing WebRtcSocket.

// create a socket with three data channels
let socket = WebRtcSocket::builder("example.matchbox.com")
    .add_reliable_channel() // adds a reliable data channel (ideal for commands/rpc/chat etc.)
    .add_unreliable_channel() // adds an unreliable data channel (udp or udp-like)
    .add_ggrs_channel() // adds a channel ideal for use with ggrs
    .add_channel(ChannelConfig {
        ordered: true,
        max_retransmits: Some(2)
    })
    .build();

You can now send on a specific channel by doing:

const RELIABLE_CHANNEL: usize = 0; // first channel added
socket.channel(RELIABLE_CHANNEL).send(data, peer_id);

garryod also implemented support for taking a channel out of a socket. This is useful when used with GGRS, which needs to take ownership of the socket.

const GGRS_CHANNEL: usize = 1; // second channel added
let ggrs_channel = socket.take_channel(GGRS_CHANNEL).unwrap();

// hand the channel over to ggrs
let ggrs_session = ggrs_session_builder.start_p2p_session(ggrs_channel).unwrap();

// we can still send on the remaining channels
socket.channel(RELIABLE_CHANNEL).send(data, peer_id);

// however, the ggrs channel is gone from the socket
assert!(socket.get_channel(GGRS_CHANNEL).is_err());

The important thing here is that all peers need to agree on what the configuration is and add the same channels in the same order. If you’re doing a p2p game, this shouldn’t be a problem, since all peers would probably be running the same code anyway.

We might want to iterate on the exact API to support custom channel configs, but at least this makes the socket flexible enough to support more advanced use cases.

Maintaining simplicity

While support for multiple data channels is nice, keeping the simple use cases dead simple has been a high priority. So we made sure there is still a simple constructor to open a single unreliable unordered channel, it's just been renamed to new_unreliable in order to prevent confusion about reliability guarantees.

let socket = WebRtcSocket::new_unreliable("example.matchbox.com");
socket.send(data, peer_id);

This opens a single unreliable, unordered channel, and the send and receive are then still available directly on the socket itself. That means if you don’t care for any of these new features, you don’t have to change anything except using new_unreliable instead of new, and everything should work practically the same as before.

Bevy integration

While using Matchbox with Bevy was previously not too difficult to use, it led to a bit of boiler-plate... And that's where bevy_matchbox comes in. It provides a MatchboxSocket type that implements Resource and Component, and makes sure the socket future is properly awaited in Bevy's IoTaskPool.

If you enable the ggrs feature, creating a new GGRS-compatible socket is now simply:

let socket = MatchboxSocket::new_ggrs(url);
commands.insert_resource(socket); // can be directly inserted as a resource

Or it can be used as a component if you prefer that:

let socket = MatchboxSocket::new_ggrs(url));
commands.spawn(socket);

MatchboxSocket implements From<WebRtcSocketBuilder>, which means a socket with a custom number of channels, turn servers, and more, can easily be built using the builder API:

let socket: MatchboxSocket = WebRtcSocket::builder(url)
    .add_unreliable_channel()
    .add_reliable_channel()
    .signaling_keep_alive_interval(None)
    .ice_server(RtcIceServerConfig {
        urls: vec![
            "stun:coturn.example.com:1234",
            "turn:coturn.example.com:1235"
        ],
        username,
        password
    })
    .into();

commands.insert_resource(socket);

Going forward we probably want to provide more convenience to make common usage patterns even simpler.

Thanks to garryod for the initial implementation.

The Extreme Bevy tutorial series has been updated to use the new crate, and is highly recommended if you intend to build a P2P game using Bevy and rollback networking.

A new matchbox_signaling crate

We also have another brand new crate, implemented by simbleau.

While matchbox_server is still available, and serves a lot of general and common use cases, there are several reasons you might want to customize how the signaling server works. In that case, matchbox_signaling is there for you.

For instance, full-mesh connections (everyone is connected to everyone in a room) fit well for projects based on rollback-based networking like GGRS. However, there have been several requests for having a client-server topology available as well. This means one of the webrtc peers will be the host that the others connect to, effectively letting you create a game using a client-server architecture, but still have a really low latency if all the players are relatively close together, e.g. in the same country or even within the same local network, and saving you some cycles since one of the players runs the game.

Using the new crate, you could easily start a client-server signaling server as follows:

let server = SignalingServer::client_server_builder((Ipv4Addr::UNSPECIFIED, 3536))
    .on_connection_request(|connection| {
        info!("Connecting: {connection:?}");
        Ok(true) // Allow all connections
    })
    .on_id_assignment(|(socket, id)| info!("{socket} received {id:?}"))
    .on_host_connected(|id| info!("Host joined: {id:?}"))
    .on_host_disconnected(|id| info!("Host left: {id:?}"))
    .on_client_connected(|id| info!("Client joined: {id:?}"))
    .on_client_disconnected(|id| info!("Client left: {id:?}"))
    .cors() // enable access from other (sub-)domains
    .trace()
    .build();

server.serve().await

As you can see it has a couple of callbacks that could hook into to respond to or customize behavior.

Going the forward, the we want to look into adding support for implementing your own kinds of matchmaking and topologies, and eventually implementing matchbox_server in terms of the new crate, and also provide the mechanisms necessary to enable checking of auth tokens.

Reporting disconnected peers

We added support for detecting peer disconnections. i.e. if another player quits the game, it is now possible to detect that, through the new WebRtcSocket::update_peers method, which replaces the old WebRtcSocket::accept_new_connections.

e.g.:

// regularly call update_peers to update the list of connected peers
for (peer, new_state) in socket.update_peers() {
    // you can also handle the specific dis(connections) as they occur:
    match new_state {
        PeerState::Connected => info!("peer {peer:?} connected"),
        PeerState::Disconnected => info!("peer {peer:?} disconnected"),
    }
}

This means it's finally possible to properly handle players leaving a multiplayer lobby.

Signaling server reconnection attempts

simbleau implemented support for multiple connection attempts to the signaling server. If the first attempt fails, it will retry 0, n or an infinite amount of times. This also exposed through the new builder API:

let socket = WebRtcSocket::builder(url)
    .add_reliable_channel()
    .reconnect_attempts(Some(2)) // if the first attempt fails, try again 2 times
    .build();

Thanks!

A huge thanks to everyone who contributed to this release, and to simbleau and garryod in particular. They implemented and/or reviewed most of the new features, and also greatly improved the internals of the project in many ways I didn't mention here.

For the full list of changes, refer to the v0.6.0 release notes.

If you're interested in trying out the project, and are using Bevy, I definitely recommend Extreme Bevy: Making a peer to peer web game with Bevy, Matchbox and GGRS

If you're doing something else, either refer to the GitHub repository, or the crate documentation

Comments

Loading comments...