← All Workshops
MudEngine Part 11: Peer-to-Peer (Hybrid)
Step 6 / 12
The WebSocket relay
The relay bridges browser-based players into the iroh-gossip mesh. It is a standalone tokio task that runs alongside the host's game loop.
It has two jobs:
- Serve the WASM bundle so browser clients can load the game UI
- Accept WebSocket connections and pipe messages to/from iroh-gossip
Add this to src/p2p.rs behind the relay feature flag:
mud-engine/src/p2p.rs
/// Spawn a WebSocket relay that bridges browser clients into iroh-gossip. /// Browser players connect via WebSocket; the relay pipes their messages /// to the gossip topic and forwards gossip events back to them. #[cfg(feature = "relay")] pub async fn spawn_relay( gossip_sink: GossipSender<TopicId>, gossip_stream: broadcast::Receiver<ServerMessage>, relay_port: u16, ) -> anyhow::Result<()> { use futures_lite::StreamExt; use tokio::net::TcpListener; use tokio_tungstenite::accept_async; use tokio_tungstenite::tungstenite::Message; let listener = TcpListener::bind(format!("0.0.0.0:{}", relay_port)).await?; println!("🌐 Relay listening on ws://0.0.0.0:{}/", relay_port); loop { let (stream, _) = listener.accept().await?; let ws_stream = accept_async(stream).await?; let (mut ws_sender, mut ws_receiver) = ws_stream.split(); let sink = gossip_sink.clone(); let mut rx = gossip_stream.resubscribe(); // Forward: gossip → this WebSocket client tokio::spawn(async move { while let Ok(msg) = rx.recv().await { let bytes = serde_json::to_vec(&msg).unwrap(); if ws_sender.send(Message::Binary(bytes.into())).await.is_err() { break; } } }); // Forward: this WebSocket client → gossip tokio::spawn(async move { while let Some(Ok(msg)) = ws_receiver.next().await { if let Message::Binary(data) = msg { let _ = sink.broadcast(data).await; } } }); } }
🔍 Relay dependencies
The relay uses two additional crates:
tokio-tungstenite— WebSocket server implementationfutures-lite— StreamExt for the WebSocket receiver
Add them to Cargo.toml only when the relay feature is enabled:
🎯 Relay vs. fullstack server
The relay is not a game server. It:
- Does not own
GameState - Does not validate moves or quest logic
- Does not persist anything
- Does not know what a "room" or "item" is
Its only job is transport: WebSocket bytes in → gossip bytes out, and vice versa. This is the key insight of the hybrid architecture: the relay is ~30 lines of plumbing, not a fullstack application.
Step 6 / 12