When channels meet a process boundary
Programs get split for good reasons. Work moves to another machine, a
privileged part is separated from a user interface, or a component becomes a
service of its own. The channels that connected the tasks cannot cross the
new boundary, and what used to be a single send now requires a
socket, a wire format, framing and dispatch code.
The usual answers change the shape of your program. A hand-rolled protocol grows a message enum and match statements on both sides, and every new interaction touches all of them. An RPC framework such as gRPC restructures the code into request and response services, which is a different model than the tasks and channels you started with.
The same model, over a connection
Remoc provides MPSC, oneshot, watch and broadcast channels with the API you already know from Tokio. Their endpoints are ordinary Serde values, so you can send a channel over the network inside a message, like you would hand its endpoint to another task. Each channel is typed and all channels between two peers are multiplexed over one connection, which may be TCP, TLS, a WebSocket, a UNIX socket or a pipe.
The program keeps its architecture. Tasks still talk through channels; some of those channels now happen to cross a process or machine boundary.
What changes in your code
The channel type comes from remoc::rch instead of
tokio::sync and the message derives Serde. Sending and
receiving stay the same.
use tokio::sync::mpsc;
struct Request {
reply_tx: mpsc::Sender<Item>,
}
let (tx, mut rx) = mpsc::channel(16);
task_tx.send(Request { reply_tx: tx }).await?;
while let Some(item) = rx.recv().await {
// ...
}
use remoc::prelude::*;
#[derive(Serialize, Deserialize)]
struct Request {
reply_tx: rch::mpsc::Sender<Item>,
}
let (tx, mut rx) = rch::mpsc::channel(16);
remote_tx.send(Request { reply_tx: tx }).await?;
while let Some(item) = rx.recv().await? {
// ...
}
Questions
Can I send a tokio::sync sender or receiver directly?
No. Tokio's channel endpoints are bound to one process and cannot be serialized. Instead you declare the field with the corresponding remoc::rch type, which can travel inside messages. Its usage stays as you know it from Tokio.
What happens when the connection is lost?
Pending and future send and receive calls fail with an error, so both sides notice. Channels do not silently drop data. If you need the connection itself to survive link failures, Aggligator provides a transport that does that.
Does every channel need its own connection?
No. All channels between two peers are multiplexed over one connection. Opening another channel just sends its endpoint inside a message; there is no additional socket or handshake.
Is there backpressure?
Yes. Each channel has its own bounded buffer and flow control, like a Tokio channel with a capacity. A receiver that stops reading only stalls its own sender, and large messages are sent in chunks so they do not block other channels.
Which transports can I use?
Any ordered byte or packet stream, for example TCP, TLS, a WebSocket, a UNIX socket, a pipe to a child process or a serial link. Remoc implements no transport itself and runs over the connection you already have.
Where to go next
Multiplexing typed channels explains how the channels share one connection, and IPC between Rust processes covers the local case of a daemon and its clients. Building upon the channels, the Remoc overview shows remote trait calls and observable collections.