counter/Cargo.toml
Its dependencies.
[package]
name = "counter"
version = "0.1.0"
edition = "2024"
[dependencies]
remoc = { version = "0.20" }
serde = { version = "1.0", features = ["derive"] }
The server maintains a counter that clients can query, increase and watch.
The #[rtc::remote] attribute generates a client that implements
the trait and server types that dispatch calls to the trait implementation.
Calling a method on the client runs it on the server and returns its result.
The watch() method returns a
watch
receiver and count_to_value() an
mpsc
receiver. Both receivers continue to deliver values after the method
call returns.
Clone the repository:
git clone https://github.com/remoc-rs/remoc
cd remoc
Start the server:
cargo run --manifest-path examples/rtc/Cargo.toml -p counter-server
Then, in another terminal, start the client:
cargo run --manifest-path examples/rtc/Cargo.toml -p counter-client
The shared crate defines the remote trait, and the other two crates provide the server and client binaries.
Defines the remote trait shared by the client and server.
counter/Cargo.tomlIts dependencies.
[package]
name = "counter"
version = "0.1.0"
edition = "2024"
[dependencies]
remoc = { version = "0.20" }
serde = { version = "1.0", features = ["derive"] }
counter/src/lib.rsDefines the trait carrying #[rtc::remote] and the error type returned by its methods. Two methods return channel receivers.
//! This library crate defines the remote counting service.
//!
//! The client and server depend on it.
#![warn(missing_docs)]
use remoc::prelude::*;
use std::time::Duration;
/// TCP port the server is listening on.
pub const TCP_PORT: u16 = 9871;
/// Increasing the counter failed.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum IncreaseError {
/// An overflow would occur.
Overflow {
/// The current value of the counter.
current_value: u32,
},
/// The RTC call failed.
Call(rtc::CallError),
}
impl From<rtc::CallError> for IncreaseError {
fn from(err: rtc::CallError) -> Self {
Self::Call(err)
}
}
/// Remote counting service.
#[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>;
/// Increase the counter's value by the provided number.
async fn increase(&mut self, #[doc = "increment value"] by: u32) -> Result<(), IncreaseError>;
/// Counts to the current value of the counter with the specified
/// delay between each step.
async fn count_to_value(
&self, step: u32, delay: Duration,
) -> Result<rch::mpsc::Receiver<u32>, rtc::CallError>;
}
Holds the counter and serves it over each accepted connection.
counter-server/Cargo.tomlIts dependencies.
[package]
name = "counter-server"
version = "0.1.0"
edition = "2024"
[dependencies]
counter = { path = "../counter" }
remoc = { version = "0.20" }
tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "time"] }
tracing = "0.1"
tracing-subscriber = "0.3.22"
counter-server/src/main.rsImplements the trait on the counter state and runs a generated server for each connection.
//! This crate implements the server of the remote counting service.
#![warn(missing_docs)]
use remoc::{codec, prelude::*};
use std::{net::Ipv4Addr, sync::Arc, time::Duration};
use tokio::{net::TcpListener, sync::RwLock, time::sleep};
use tracing::{Instrument, info_span};
use counter::{Counter, CounterServerSharedMut, IncreaseError, TCP_PORT};
/// Server object for the counting service, keeping the state.
#[derive(Default)]
pub struct CounterObj {
/// The current value.
value: u32,
/// The subscribed watchers.
watchers: Vec<rch::watch::Sender<u32>>,
}
/// Implementation of remote counting service.
impl Counter for CounterObj {
async fn value(&self) -> Result<u32, rtc::CallError> {
Ok(self.value)
}
async fn watch(&mut self) -> Result<rch::watch::Receiver<u32>, rtc::CallError> {
// Create watch channel.
let (tx, rx) = rch::watch::channel(self.value);
// Keep the sender half in the watchers vector.
self.watchers.push(tx);
// And return the receiver half.
Ok(rx)
}
async fn increase(&mut self, by: u32) -> Result<(), IncreaseError> {
tracing::info!(%by, "increase");
// Perform the addition if it does not overflow the counter.
match self.value.checked_add(by) {
Some(new_value) => self.value = new_value,
None => return Err(IncreaseError::Overflow { current_value: self.value }),
}
// Notify all watchers and keep only the ones that are not disconnected.
let value = self.value;
self.watchers.retain(|watch| !watch.send(value).into_disconnected().unwrap());
Ok(())
}
async fn count_to_value(
&self, step: u32, delay: Duration,
) -> Result<rch::mpsc::Receiver<u32>, rtc::CallError> {
// Create mpsc channel for counting.
let (tx, rx) = rch::mpsc::channel();
// Spawn a task to perform the counting.
let value = self.value;
tokio::spawn(async move {
// Counting loop.
for i in (0..value).step_by(step as usize) {
// Send the value.
if tx.send(i).await.into_disconnected().unwrap() {
// Abort the counting if the client dropped the
// receive half or disconnected.
break;
}
// Wait the specified delay.
sleep(delay).await;
}
});
// Return the receive half of the counting channel.
Ok(rx)
}
}
#[tokio::main]
async fn main() {
// Initialize logging.
tracing_subscriber::fmt::init();
// Create a counter object that will be shared between all clients.
// You could also create one counter object per connection.
let counter_obj = Arc::new(RwLock::new(CounterObj::default()));
// Listen to TCP connections using Tokio.
// In reality you would probably use TLS or WebSockets over HTTPS.
println!("Listening on port {}. Press Ctrl+C to exit.", TCP_PORT);
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, TCP_PORT)).await.unwrap();
loop {
// Accept an incoming TCP connection.
let (socket, addr) = listener.accept().await.unwrap();
let (socket_rx, socket_tx) = socket.into_split();
println!("Accepted connection from {}", addr);
// Create a new shared reference to the counter object.
let counter_obj = counter_obj.clone();
// Spawn a task for each incoming connection.
tokio::spawn(
async move {
tracing::info!("serving");
// Create a server proxy and client for the accepted connection.
//
// The server proxy executes all incoming method calls on the shared counter_obj
// with a request queue length of 1.
//
// Current limitations of the Rust compiler require that we explicitly
// specify the codec.
let (server, client) = CounterServerSharedMut::<_, codec::Default>::new(counter_obj);
// Establish a Remoc connection with default configuration over the TCP connection and
// provide (i.e. send) the counter client to the client.
remoc::Connect::io(remoc::Cfg::default(), socket_rx, socket_tx).provide(client).await.unwrap();
// Serve incoming requests from the client on this task.
// `true` indicates that requests are handled in parallel.
server.serve().await.unwrap();
}
.instrument(info_span!("incoming", %addr)),
);
}
}
Exercises the counter: reads it, increases it, watches it and streams from it.
counter-client/Cargo.tomlIts dependencies.
[package]
name = "counter-client"
version = "0.1.0"
edition = "2024"
[dependencies]
counter = { path = "../counter" }
remoc = { version = "0.20" }
tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "time"] }
tracing-subscriber = "0.3.22"
counter-client/src/main.rsUses the generated client to call the trait methods and receive updates.
//! This crate implements the client of the remote counting service.
#![warn(missing_docs)]
use remoc::prelude::*;
use std::{net::Ipv4Addr, time::Duration};
use tokio::net::TcpStream;
use counter::{Counter, CounterClient, TCP_PORT};
#[tokio::main]
async fn main() {
// Initialize logging.
tracing_subscriber::fmt::init();
// Establish TCP connection to server.
let socket = TcpStream::connect((Ipv4Addr::LOCALHOST, TCP_PORT)).await.unwrap();
let (socket_rx, socket_tx) = socket.into_split();
// Establish a Remoc connection with default configuration over the TCP connection and
// consume (i.e. receive) the counter client from the server.
let mut client: CounterClient =
remoc::Connect::io(remoc::Cfg::default(), socket_rx, socket_tx).consume().await.unwrap();
// Subscribe to the counter watch and print each value change.
println!("Subscribing to counter change notifications");
let mut watch_rx = client.watch().await.unwrap();
let watch_task = tokio::spawn(async move {
while watch_rx.changed().await.is_ok() {
let value = watch_rx.borrow_and_update().unwrap();
println!("Counter change notification: {}", *value);
}
});
println!("Done!");
// Print current value.
let value = client.value().await.unwrap();
println!("Current counter value is {}\n", value);
// Increase counter value.
println!("Increasing counter value by 5");
client.increase(5).await.unwrap();
println!("Done!\n");
// Print new value.
let value = client.value().await.unwrap();
println!("New counter value is {}\n", value);
// Let the server count to the current value.
println!("Asking the server to count to the current value with a step delay of 300ms...");
let mut rx = client.count_to_value(1, Duration::from_millis(300)).await.unwrap();
while let Ok(Some(i)) = rx.recv().await {
println!("Server counts {}", i);
}
println!("Server is done counting.\n");
// Wait for watch task.
println!("Server exercise is done.");
println!("You can now press Ctrl+C to exit or continue watching for value ");
println!("change notification caused by other clients.");
watch_task.await.unwrap();
}