The limits of request and response

In a request/response framework the server cannot start a conversation. The usual workarounds all have costs. Polling wastes requests and adds latency. Server-streaming methods, as gRPC offers them, cover subscriptions, but the stream is tied to the method that opened it and cannot be passed around, and they still do not let the server invoke anything on the client. A second connection in the reverse direction requires the client to be reachable, which fails as soon as it sits behind NAT or a firewall.

Channels make push natural

In Remoc, channel endpoints are values that calls can return and accept. A method returns an rch::mpsc::Receiver and the client has a subscription; it returns an rch::watch::Receiver and the client always sees the latest value; the client puts a sender into a request and the server reports progress into it. Each channel keeps working after the call that created it has returned.

For real callbacks, the roles can simply be reversed. The client implements a trait carrying #[rtc::remote] itself and sends its client object to the server, which then invokes methods on it like on any remote object. Communication is therefore fully bidirectional over the single connection that the client opened, so a device behind NAT can expose an interface to its server.

A pushing method is just a signature

This trait is from the counter example. The watch method returns a watch channel receiver, giving the client immediate notification of every change.

Service definition
#[rtc::remote(server(SharedMut))]
pub trait Counter {
    /// Obtain the current value of the counter.
    async fn value(&self) -> Result<u32, rtc::CallError>;

    /// Watch the current value of the counter for immediate
    /// notification when it changes.
    async fn watch(&mut self)
        -> Result<rch::watch::Receiver<u32>, rtc::CallError>;
}

Complete trait calling example → Pipelined calls save the round trips →

Questions

Does server push work when the client is behind NAT?

Yes. Everything runs over the one connection that the client opened, and channels and calls work in both directions on it. The server never has to connect to the client, so NAT and firewalls on the client side are no obstacle.

How is this different from gRPC server streaming?

A gRPC stream is tied to the method that declared it. A Remoc channel is a value: a call can return it, a message can carry it, and it can be stored and used long after the call completed. Additionally the client can serve a trait of its own, which gRPC has no equivalent for.

What happens when the client disconnects?

Sending into a channel whose remote end is gone fails with an error, and calls on a disconnected client fail likewise. The server notices at the point of use and can drop its state for that client.

Can the server push without the client asking first?

The client has to hand the server a way in once, by calling a method that returns a channel, by including a sender in a message or by providing a client for its own trait. From then on the server pushes whenever it wants.

Can I use this between Rust and another language?

No. Remoc's protocol is defined by Rust types serialized with Serde, so both peers must be Rust programs. For cross-language services a schema-based system such as gRPC is the better fit.

Where to go next

Remoc vs. other RPC libraries positions this model against tarpc, gRPC and Cap'n Proto. Channels between processes and machines explains the channels that make the pushing work, and for a daemon notifying local clients see IPC between Rust processes.