The problem with one stream
A TCP or TLS connection provides a single byte stream. As soon as two independent conversations share it, for example commands and a bulk file transfer, or requests and server-initiated events, your application needs message framing, tags to route each message to its handler, and care that a large transfer does not starve everything queued behind it. Opening a separate connection for each conversation avoids this, but multiplies handshakes and TLS sessions, causes trouble with firewalls and NATs, and the connections no longer fail together.
Stream multiplexers such as yamux or the streams of QUIC solve one half of the problem by providing many byte streams over one connection. However, the streams still carry raw bytes, so serialization, framing and matching a stream to its purpose remain your job.
Channels instead of byte streams
Remoc multiplexes at the level that Rust programs already use internally: channels. A connection starts with a base channel. To open a further channel, you just send one of its endpoints inside a message, like you would hand a Tokio channel endpoint to another task. There is no port to open, no stream number to agree on and no registry to consult; the endpoint simply arrives where it is needed.
Each channel is typed and carries values of a single Serde type, serialized with the Postbag codec, so the compiler checks what may be sent over it. Every channel has its own flow control, meaning that a receiver which stops reading only stalls its own sender and does not affect unrelated traffic. Large messages are transmitted in chunks to avoid one channel blocking another. Building upon its channels, Remoc also provides remote functions, RPC on traits and observable collections over the same connection.
What it looks like
A request type can carry the sender of a new channel and the server replies into it. Setting up a new communication path over the existing connection takes two lines.
#[derive(Serialize, Deserialize)]
struct CountReq {
up_to: u32,
// A sender, on its way to the peer.
seq_tx: rch::mpsc::Sender<u32>,
}
Questions
How is this different from yamux or QUIC streams?
Stream multiplexers such as yamux and QUIC provide additional byte streams, so serialization, framing and dispatch remain the job of the application. Remoc multiplexes typed channels instead. Each channel carries values of one Serde type, and remote functions, trait calls and observable collections build on the same connection.
Does a slow channel block the others?
No. Each channel has its own flow control, so a receiver that stops reading only stalls its own sender. The channels still share the underlying transport and therefore its total bandwidth.
Which transports can carry the multiplexed connection?
Any ordered byte or packet stream can be used, for example TCP, TLS, a WebSocket, a UNIX pipe or a serial link. Remoc's channel multiplexer runs on top of it and does not depend on a particular transport type.
Do both ends have to be written in Rust?
Yes. The protocol is defined by the Rust types serialized with Serde, so both peers must be Rust programs. For services that cross language boundaries a schema-based system such as gRPC is the better fit.
How many channels can one connection carry?
As many as you like. Channels are cheap values rather than sockets; opening one just sends its endpoint inside a message, without a port, handshake or registration. It is perfectly normal to create one channel per request or per subscriber.
Where to go next
Channels between processes and machines shows how little code the channels need, and the Remoc overview walks through remote trait calls and pipelining. Remoc vs. other RPC libraries compares it with tarpc, gRPC and Cap'n Proto, and the benchmarks compare a Remoc channel with serializing directly to plain TCP.