Expand description

FLNet - sending data over WebRTC in libc and wasm

FLNet is used in fledger to communicate between browsers. It uses the WebRTC protocol and a signalling-server with websockets. The outstanding feature of this implementation is that it works both for WASM and libc with the same interface. This allows to write the core code once, and then re-use it for both WASM and libc.

WebRTC in short

WebRTC is an amazing technique that has been invented to let browsers exchange data without a third party server. There are many protocols involved in making this happen, and the first use-case of WebRTC was to share video and audio directly between two browsers. But WebRTC also allows for a ‘data’ channel where you can send any data you want.

For WebRTC to work, the following is needed:

  • a WebRTC implementation on both ends: WebRTC exists for browsers, node, and since the end of 2021 also as a rust-crate
  • a signalling server for the initial setup: as most browsers are behind a router, it is not possible to connect directly without some help. The signalling server is needed to exchange the information for setting up the communication
  • a STUN server: for WebRTC to work also behind a NAT, somebody needs to tell the browser which address it has. The STUN server does amazing things like poking holes through firewalls, but this doesn’t always work, this is why you need an optional
  • a TURN server: if WebRTC doesn’t manage to connect two WebRTC endpoints with each other, the last resort is using a TURN server. This is the old-school way of connecting two endpoints using a server that forwards traffic in both directions

Here is a docker-compose.signalling.yaml ready to run the flsignal and coturn.

For a more in-depth presentation, including some pitfalls, I liked this article: What is WebRTC and how to avoid its 3 deadliest pitfalls

Example client/server

Here is a short example of how two nodes can communicate with each other. First of all you need a locally runing signalling server that you can spin up with:

cd cli; cargo run --bin flsignal -- -vv

For more debugging, it is sometimes useful to add -vvv, or even -vvvv, but then it gets very verbose!

Here is a small example of how to setup a server that listens to messages from clients.

// The server waits for connections by the clients and prints the messages received.
async fn server() -> Result<(), BrokerError> {
    // Create a random node-configuration. It uses serde for easy serialization.
    let nc = flnet::config::NodeConfig::new();
    // Connect to the signalling server and wait for connection requests.
    let mut net = flnet::network_start(nc.clone(), "ws://localhost:8765")
        .await
        .expect("Starting network");

    // Print our ID so it can be copied to the server
    log::info!("Our ID is: {:?}", nc.info.get_id());
    loop {
        // Wait for messages and print them to stdout.
        log::info!("Got message: {:?}", net.recv().await);
    }
}

It is contacted by the client node, which needs the ID of the server to know where to connect to:

// A client sends a single message to a server and then disconnects.
async fn client(server_id: &str) -> Result<(), BrokerError> {
    // Create a random node-configuration. It uses serde for easy serialization.
    let nc = flnet::config::NodeConfig::new();
    // Connect to the signalling server and wait for connection requests.
    let mut net = flnet::network_start(nc.clone(), "ws://localhost:8765")
        .await
        .expect("Starting network");

    // Need to get the client-id from the message in client()
    let server_id = U256::from_str(server_id).expect("get client id");
    log::info!("Server id is {server_id:?}");
    // This sends the message by setting up a connection using the signalling server.
    // The client must already be running and be registered with the signalling server.
    // Using `SendNodeMessage` will set up a connection using the signalling server, but
    // in the best case, the signalling server will not be used anymore afterwards.
    net.send_msg(server_id, "ping".into())?;

    // Wait for the connection to be set up and the message to be sent.
    wait_ms(1000).await;

    Ok(())
}

You can find this code and more examples in the examples directory here: https://github.com/ineiti/fledger/tree/0.7.0/examples Other examples include a full example with a shared code, a libc and a wasm implementation: Ping-Pong.

Features

FLNet has two features to choose between WASM and libc implementation. Unfortunately it is not detected automatically yet. If you write code that is shared between the two, you can add flnet without any features to your Cargo.toml. The appropriate feature will then be added once you compile the final code.

The wasm feature enables the WASM backend, which only includes the WebRTC connections and the signalling client:

flnet = {features = ["wasm"], version = "0.7"}

With the libc feature, the libc backend is enabled, which has the WebRTC connections, and both the signalling client and server part:

flnet = {features = ["libc"], version = "0.7"}

Cross-platform usage

If you build a software that will run both in WASM and with libc, one way to do so is to use trunk, which allows you to write the wasm part very similar to the libc part.

In fledger, this is accomplished with the following structure:

  flbrowser  fledger-cli
        \    /
        shared
          |
        flnet

The shared code only depends on the flnet crate without a given feature. It is common between the WASM and the libc implementation. Only the flbrowser and fledger-cli depend on the flnet crate with the appropriate feature set.

You can find an example in the ping-pong directory.

libc

The libc code is made possible by the excellent webrtc library. The libc feature enables both the WebSocket-server and the WebRTC for libc-systems.

wasm

This feature implements the WebSocket and WebRTC trait for the wasm platform. It can be used for the browser or for node.

Modules

Broker is an actor implementation for wasm and libc
Configuration structures that define a node, including old versions for migrations. Also configuration for starting a new node.
Network related structures
The signalling server implementation
WebRTC connection setup
Websocket structures for the broker implementations

Structs

A list of node IDs with some useful methods.
Nicely formatted 256 bit structure

Enums

Errors when setting up a new network connection

Functions

Starts a new broker::Broker<NetworkMessage> with a given node- and connection-configuration. This returns a raw broker which is mostly suited to connect to other brokers. If you need an easier access to the WebRTC network, use network_start, which returns a structure with a more user-friendly API.
Starts a new connection to the signalling server using the node- and connection-configuration. It returns a user-friendly API that can be used to send and receive messages. If you want to connect the network with other brokers, then use the network_broker_start method.

Type Definitions

A node ID for a node, which is a U256.