https://github.com/zduny/zzrpc-tutorial
Tutorial for zzrpc.
https://github.com/zduny/zzrpc-tutorial
rpc rpc-framework zzrpc
Last synced: 2 months ago
JSON representation
Tutorial for zzrpc.
- Host: GitHub
- URL: https://github.com/zduny/zzrpc-tutorial
- Owner: zduny
- License: apache-2.0
- Created: 2022-12-27T13:59:26.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2023-01-02T07:57:26.000Z (over 2 years ago)
- Last Synced: 2025-02-10T11:44:47.960Z (4 months ago)
- Topics: rpc, rpc-framework, zzrpc
- Language: Rust
- Homepage: https://github.com/zduny/zzrpc
- Size: 27.3 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# zzrpc-tutorial
Tutorial example for [zzrpc](https://github.com/zduny/zzrpc).
# usage
Build:
```bash
cargo build
```Start server:
```bash
cd server
cargo run
```Open new terminal tab/window and start client:
```bash
cd client
cargo run
```# tutorial
## Step 0 (optional) - `common` crate
Create separate `common` crate for definition of your api.
This step is optional, but doing so makes it easier to share code between client and server
implementations, especially when they're targeting different platforms (for example: server
targeting native platform and client targeting WebAssembly running in a browser).```bash
cargo new --lib common
```## Step 1 - dependencies
Add [`mezzenger`](https://crates.io/crates/mezzenger), [`serde`](https://crates.io/crates/serde) with `derive` feature enabled and finally [`zzrpc`](https://crates.io/crates/zzrpc) to `Cargo.toml`:
```toml
# ...[dependencies]
# ...
mezzenger = "0.1.3"
serde = { version = "1.0.150", features = ["derive"] }
zzrpc = "0.1.2"
```For your server/clients you'll also need some [`kodec`](https://crates.io/crates/kodec),
some `mezzenger` transport implementation
(here: [`mezzenger-tcp`](https://crates.io/crates/mezzenger-tcp)),
[`futures`](https://crates.io/crates/futures) and on native server/client
[`tokio`](https://crates.io/crates/tokio):```toml
# ...[dependencies]
# ...
mezzenger = "0.1.3"
serde = { version = "1.0.150", features = ["derive"] }
zzrpc = "0.1.2"
kodec = { version = "0.1.0", features = ["binary"] }
mezzenger-tcp = "0.1.2"
futures = "0.3.25"
tokio = { version = "1.23.0", features = ["full"] } # only when targeting native platforms
tokio-stream = { version = "0.1.11", features = ["sync"] } # optional but useful when creating stream responses
```If you followed **Step 0** then add `common` to your client/server crate dependencies:
```toml
# ...[dependencies]
# ...
common = { path = "../common" }
mezzenger = "0.1.3"
serde = { version = "1.0.150", features = ["derive"] }
zzrpc = "0.1.2"
kodec = { version = "0.1.0", features = ["binary"] }
mezzenger-tcp = "0.1.2"
futures = "0.3.25"
tokio = { version = "1.23.0", features = ["full"] } # only when targeting native platforms
tokio-stream = { version = "0.1.11", features = ["sync"] } # optional but useful when creating stream responses
```## Step 2 - api definition
Define your api:
```rust
use zzrpc::api;/// Service API.
#[api]
pub trait Api {
/// Print "Hello World!" message on the server.
async fn hello_world(&self);/// Add two integers together and return result.
async fn add_numbers(&self, a: i32, b: i32) -> i32;/// Concatenate two strings and return resulting string.
async fn concatenate_strings(&self, a: String, b: String) -> String;/// Send (string) message to server.
async fn message(&self, message: String);/// Stream of messages.
async fn messages(&self) -> impl Stream;
}
```Notice the `#[api]` macro attribute at the top.
Methods must be marked with `async`.
There are two types of supported methods:
- regular "value" methods like `hello_world`, `add_numbers`, `concatenate_strings`, `message` above,
- streaming methods - where single request instructs producer to send multiple values (not necessarily
at once) without consumer having to request them individually with separate requests. Return types of streaming request methods must follow form: `impl Stream`.## Step 3 - producing
Now, let's create a server for defined api.
### Step 3a - server
We'll use TCP for communication, first we have to accept TCP connections.
Also we'll create common state shared state by our producers.```rust
use std::sync::Arc;
use tokio::{
net::TcpListener,
select, spawn,
sync::RwLock,
};
use futures::{pin_mut, Stream};
use kodec::binary::Codec;
use mezzenger_tcp::Transport;#[derive(Debug)]
struct State {
// ... to do in next steps
}#[tokio::main]
async fn main() {
let state = Arc::new(RwLock::new(State {}));let listener = TcpListener::bind("127.0.0.1").await
.expect("failed to bind tcp listener to specified address");
let break_signal = tokio::signal::ctrl_c();
pin_mut!(break_signal);loop {
select! {
listener_result = listener.accept() => {
let (stream, _address) = listener_result.expect("failed to connect client");
let state = state.clone();
spawn(async move {
// ... to do in next steps
});
},
break_result = &mut break_signal => {
break_result.expect("failed to listen for break signal event");
break;
}
}
}
}
```### Step 3b - producer
Now it's a good time to implement a producer for your api:
```rust
use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, StreamExt};
use zzrpc::Produce;
use common::api::{impl_produce, Request, Response}; // or simply: use common::api::*;// ...
#[derive(Debug, Produce)]
struct Producer {
state: Arc>,
}// Note we're not implementing any trait here - zzrpc is designed like that
// on purpose to avoid dealing with current Rust's async trait troubles.
//
// Instead simply copy your method signatures from the api's trait, add method
// bodies and implement them - `Produce` derive macro will do the rest for you.
impl Producer {
/// Print "Hello World!" message on the server.
async fn hello_world(&self) {
println!("Hello World!");
}/// Add two integers together and return result.
async fn add_numbers(&self, a: i32, b: i32) -> i32 {
a + b
}/// Concatenate two strings and return resulting string.
async fn concatenate_strings(&self, a: String, b: String) -> String {
format!("{a}{b}")
}/// Send (string) message to server.
async fn message(&self, message: String) {
println!("Message received: {message}");
let _ = self.state.read().await.sender.send(message);
}/// Stream of messages.
async fn messages(&self) -> impl Stream {
BroadcastStream::new(self.state.read().await.sender.subscribe()).filter_map(Result::ok)
}
}
```Remember to update `State` struct:
```rust
// ...
#[derive(Debug)]
struct State {
sender: broadcast::Sender,
}#[tokio::main]
async fn main() {
let (sender, _) = broadcast::channel(16);
let state = Arc::new(RwLock::new(State { sender }));
// ...
```### Step 3c - serving
Now we can serve our clients:
```rust
// ...
let (stream, _address) = listener_result.expect("failed to connect client");
let state = state.clone();
spawn(async move {
// create mezzenger transport wrapping a TCP stream.
let transport = Transport::new(stream, Codec::default());// create producer.
let producer = Producer { state };// produce for the transport using default producer configuration.
producer.produce(transport, Configuration::default());
});
// ...
```## Step 4 - consuming
Let's write a client for our service:
```rust
use std::time::Duration;use futures::StreamExt;
use kodec::binary::Codec;
use mezzenger_tcp::Transport;
use tokio::{net::TcpStream, spawn, time::sleep};
use zzrpc::{consumer::Configuration, Consume};use common::api::{Api, Consumer};
#[tokio::main]
async fn main() {
// first connect to the server.
let stream = TcpStream::connect("127.0.0.1:8080")
.await
.expect("failed to connect to server");
let address = stream.local_addr().expect("failed to get address");// wrap TCP stream in mezzenger transport.
let transport = Transport::new(stream, Codec::default());// create consumer communicating over transport and using default configuration.
let consumer = Consumer::consume(transport, Configuration::default());// now we can make our first request.
consumer.hello_world().await.expect("failed to call method");// another request, this time with return value.
let result = consumer
.add_numbers(2, 3)
.await
.expect("failed to call method");
println!("2 + 3 = {result}");let result = consumer
.concatenate_strings("Hello ".to_string(), "World".to_string())
.await
.expect("failed to call method");
println!("'Hello ' + 'World' = '{result}'");// let's create a message stream.
let mut messages = consumer
.messages()
.await
.expect("failed get message stream");// send some messages to server.
consumer
.message(format!("{address} - Message 1"))
.await
.expect("failed to call method");consumer
.message(format!("{address} - Message 2"))
.await
.expect("failed to call method");consumer
.message(format!("{address} - Message 3"))
.await
.expect("failed to call method");let aborter = messages.aborter();
spawn(async move {
// abort stream after 30 seconds
sleep(Duration::from_secs(30)).await;
aborter.abort();
});// loop over and print received messages.
while let Some(message) = messages.next().await {
println!("Received message: {message}");
}
}
```## Limitations
Have in mind following limitations of `zzrpc`'s `#[api]` macro:
- generic traits are not supported,
- generic methods are not supported,
- only one api is allowed per module
(but multiple apis in separate modules of the same crate are fine),
- only one producer implementation is allowed per module
(again: if you need more you can simply define them in separate modules).