a70e68c7bd
* Smolmix documentation * Add smolmix docs: landing page, tutorials, and developer page links * Add Exit Gateway services page (NR vs IPR) and link from existing docs * Update auto-generated command and API outputs * Reorg of tutorials and architecture pages * License information + remove TODO from docs.rs visibile comment + reorg readme * Add versions file for doc-wide versioning * Relative -> absolute links * Relative -> absolute links * Update license + add old tutorial code as examples * Streamline smolmix docs * Clippy * Clean up doc comments * Last pass * Add larger file download to list * set new versions * Clippy * Remove blake pin from docs + add version range to root Cargo.toml * Format example logging * Remove crate blocked component * Loose whitespace * Add doc verification script for inline mdx * Formatting * Components regen * Reorg + tighten text * Voicing cohesion pass + remove bloated examples * Voicing cont. * Reduce max download size * Small suggested clarifications * Max/docs voicing consistency (#6769) * Reduce max download size * voicing consistency across docs * New landing order w smolmix * Tweaks * Final tweaks
176 lines
6.4 KiB
Rust
176 lines
6.4 KiB
Rust
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
//! Demonstrates concurrent streams over the Mixnet.
|
|
//!
|
|
//! One sender opens streams to two receivers. Both receivers accept, read,
|
|
//! and reply concurrently using `AsyncRead + AsyncWrite`, the same traits
|
|
//! as TCP sockets.
|
|
//!
|
|
//! ## What this demonstrates
|
|
//!
|
|
//! - `client.listener()` activates stream mode and returns a `MixnetListener`
|
|
//! - `listener.accept()` blocks until a remote peer opens a stream
|
|
//! - `client.open_stream(recipient, surbs)` opens an outbound stream
|
|
//! - `MixnetStream` implements `AsyncRead + AsyncWrite`. Standard tokio
|
|
//! I/O (`read`, `write_all`, `flush`) works unchanged
|
|
//! - Multiple streams are multiplexed over a single client connection
|
|
//! - Replies travel via SURBs, so receivers never learn the sender's address
|
|
//! - Streams deregister on `drop`; no close handshake is needed
|
|
//!
|
|
//! ```sh
|
|
//! cargo run --example stream_simple_read_write
|
|
//! ```
|
|
|
|
use nym_sdk::mixnet;
|
|
use std::time::Duration;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
const TIMEOUT: Duration = Duration::from_secs(60);
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
nym_bin_common::logging::setup_tracing_logger();
|
|
|
|
// Connect three ephemeral clients: one sender, two receivers.
|
|
let mut sender = mixnet::MixnetClient::connect_new().await.unwrap();
|
|
println!("Sender address: {}", sender.nym_address());
|
|
|
|
let mut receiver_a = mixnet::MixnetClient::connect_new().await.unwrap();
|
|
let addr_a = *receiver_a.nym_address();
|
|
println!("Receiver A address: {addr_a}");
|
|
|
|
let mut receiver_b = mixnet::MixnetClient::connect_new().await.unwrap();
|
|
let addr_b = *receiver_b.nym_address();
|
|
println!("Receiver B address: {addr_b}");
|
|
|
|
// Each receiver creates a listener (activates stream mode).
|
|
// listener() can only be called once per client.
|
|
let mut listener_a = receiver_a.listener().unwrap();
|
|
let mut listener_b = receiver_b.listener().unwrap();
|
|
|
|
// The sender opens a stream to each receiver.
|
|
// Each stream gets a random StreamId for multiplexing.
|
|
println!("\nOpening streams to both receivers...");
|
|
let mut stream_to_a = sender.open_stream(addr_a, None).await.unwrap();
|
|
println!("Stream to A opened: {}", stream_to_a.id());
|
|
|
|
let mut stream_to_b = sender.open_stream(addr_b, None).await.unwrap();
|
|
println!("Stream to B opened: {}", stream_to_b.id());
|
|
|
|
// Both receivers accept the incoming streams concurrently.
|
|
println!("\nWaiting for both receivers to accept...");
|
|
let (inbound_a, inbound_b) = tokio::try_join!(
|
|
async {
|
|
tokio::time::timeout(TIMEOUT, listener_a.accept())
|
|
.await
|
|
.expect("timed out waiting for A to accept")
|
|
.ok_or("listener A shut down")
|
|
},
|
|
async {
|
|
tokio::time::timeout(TIMEOUT, listener_b.accept())
|
|
.await
|
|
.expect("timed out waiting for B to accept")
|
|
.ok_or("listener B shut down")
|
|
},
|
|
)
|
|
.unwrap();
|
|
println!("A accepted stream: {}", inbound_a.id());
|
|
println!("B accepted stream: {}", inbound_b.id());
|
|
|
|
// Sender writes to both streams using AsyncWrite.
|
|
let msg_a = b"hello receiver A";
|
|
let msg_b = b"hello receiver B";
|
|
|
|
println!("\nSender writing to both streams...");
|
|
stream_to_a.write_all(msg_a).await.unwrap();
|
|
stream_to_a.flush().await.unwrap();
|
|
|
|
stream_to_b.write_all(msg_b).await.unwrap();
|
|
stream_to_b.flush().await.unwrap();
|
|
|
|
// Both receivers read and reply concurrently.
|
|
// Replies travel via SURBs, so receivers never learn the sender's address.
|
|
println!("\nBoth receivers reading and replying concurrently...");
|
|
let reply_a = b"reply from A";
|
|
let reply_b = b"reply from B";
|
|
|
|
let (res_a, res_b) = tokio::join!(
|
|
// Receiver A: read then reply
|
|
async {
|
|
let mut inbound = inbound_a;
|
|
let mut buf = vec![0u8; 1024];
|
|
let n = tokio::time::timeout(TIMEOUT, inbound.read(&mut buf))
|
|
.await
|
|
.expect("A: timed out reading")
|
|
.expect("A: read failed");
|
|
println!("Receiver A got: {:?}", String::from_utf8_lossy(&buf[..n]));
|
|
assert_eq!(&buf[..n], msg_a);
|
|
|
|
inbound.write_all(reply_a).await.unwrap();
|
|
inbound.flush().await.unwrap();
|
|
println!("Receiver A replied");
|
|
inbound
|
|
},
|
|
// Receiver B: read then reply
|
|
async {
|
|
let mut inbound = inbound_b;
|
|
let mut buf = vec![0u8; 1024];
|
|
let n = tokio::time::timeout(TIMEOUT, inbound.read(&mut buf))
|
|
.await
|
|
.expect("B: timed out reading")
|
|
.expect("B: read failed");
|
|
println!("Receiver B got: {:?}", String::from_utf8_lossy(&buf[..n]));
|
|
assert_eq!(&buf[..n], msg_b);
|
|
|
|
inbound.write_all(reply_b).await.unwrap();
|
|
inbound.flush().await.unwrap();
|
|
println!("Receiver B replied");
|
|
inbound
|
|
},
|
|
);
|
|
let inbound_a = res_a;
|
|
let inbound_b = res_b;
|
|
|
|
// Sender reads the replies back.
|
|
println!("\nSender reading replies...");
|
|
tokio::join!(
|
|
async {
|
|
let mut buf = vec![0u8; 1024];
|
|
let n = tokio::time::timeout(TIMEOUT, stream_to_a.read(&mut buf))
|
|
.await
|
|
.expect("timed out reading reply from A")
|
|
.expect("read failed");
|
|
println!(
|
|
"Sender got from A: {:?}",
|
|
String::from_utf8_lossy(&buf[..n])
|
|
);
|
|
assert_eq!(&buf[..n], reply_a);
|
|
},
|
|
async {
|
|
let mut buf = vec![0u8; 1024];
|
|
let n = tokio::time::timeout(TIMEOUT, stream_to_b.read(&mut buf))
|
|
.await
|
|
.expect("timed out reading reply from B")
|
|
.expect("read failed");
|
|
println!(
|
|
"Sender got from B: {:?}",
|
|
String::from_utf8_lossy(&buf[..n])
|
|
);
|
|
assert_eq!(&buf[..n], reply_b);
|
|
},
|
|
);
|
|
|
|
println!("\nConcurrent round-trips successful!");
|
|
|
|
// Clean up: streams deregister from the router on drop.
|
|
// No close message is sent; the remote side sees EOF after idle timeout.
|
|
drop(stream_to_a);
|
|
drop(stream_to_b);
|
|
drop(inbound_a);
|
|
drop(inbound_b);
|
|
sender.disconnect().await;
|
|
receiver_a.disconnect().await;
|
|
receiver_b.disconnect().await;
|
|
}
|