Connect two peers
Print a dialable PeerAddr and measure a libp2p Ping RTT between two local processes.
This quickstart creates one small Cargo project with two binaries. The listener prints a paste-ready peer address; the dialer connects to it and measures a Ping round trip.
Before starting, install minip2p with Rust 1.91 or newer.
Create the project
Create a binary package
cargo new minip2p-hello
cd minip2p-hello
cargo add minip2p-rs
mkdir -p src/binAdd the listener
Create src/bin/listener.rs:
use minip2p::{Endpoint, EndpointWaitOutcome, PeerAddr, WaitDeadline};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut node = Endpoint::builder()
.agent_version("minip2p-hello/listener")
.listen_default()?
.bind()?;
println!("peer={}", node.peer_id());
for address in node.listen_all()? {
println!("listen={}", local_dialable(&address));
}
loop {
if let EndpointWaitOutcome::Event(event) = node.wait(WaitDeadline::NEVER)? {
println!("{event:?}");
}
}
}
fn local_dialable(address: &PeerAddr) -> String {
address
.to_string()
.replace("/ip4/0.0.0.0/", "/ip4/127.0.0.1/")
.replace("/ip6/::/", "/ip6/::1/")
}Add the dialer
Create src/bin/dialer.rs:
use std::time::{Duration, Instant};
use minip2p::{ConnectOutcome, Endpoint, EndpointEvent, EndpointWaitOutcome, PeerAddr};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// A complete peer address, e.g. /ip4/127.0.0.1/udp/4001/quic-v1/p2p/12D3KooW...
let target: PeerAddr = std::env::args()
.nth(1)
.ok_or("usage: dialer <peer-address>")?
.parse()?;
let peer = target.peer_id().clone();
let mut node = Endpoint::builder()
.agent_version("minip2p-hello/dialer")
.listen_default()?
.bind()?;
// One Connection attempt, one Connect ID, one terminal event.
let connect_id = node.connect(target)?;
let mut deadline = Instant::now() + Duration::from_secs(10);
loop {
let event = match node.wait(deadline)? {
EndpointWaitOutcome::Event(event) => event,
// Another thread woke the wait; nothing to service here.
EndpointWaitOutcome::Deadline => {
return Err("peer did not answer in time".into());
}
_ => continue, // interrupted by a WaitHandle
};
match event {
EndpointEvent::ConnectSettled {
connect_id: id,
outcome,
..
} if id == connect_id => {
if !matches!(outcome, ConnectOutcome::Connected { .. }) {
return Err(format!("connect failed: {outcome:?}").into());
}
}
EndpointEvent::PeerReady { peer_id, .. } if peer_id == peer => {
node.ping(&peer)?;
// The ping gets its own budget, however long setup took.
deadline = Instant::now() + Duration::from_secs(5);
}
EndpointEvent::PingRttMeasured { peer_id, rtt_ms, .. } if peer_id == peer => {
println!("peer={peer} rtt={rtt_ms}ms");
return Ok(());
}
// Everything else is unrelated to this exchange.
_ => {}
}
}
}Run it
In terminal one:
$ cargo run --bin listener
peer=12D3KooW…
listen=/ip4/127.0.0.1/udp/54321/quic-v1/p2p/12D3KooW…
listen=/ip6/::1/udp/54322/quic-v1/p2p/12D3KooW…
Keep the listener running. In terminal two, paste one complete listen= value:
$ cargo run --bin dialer -- /ip4/127.0.0.1/udp/54321/quic-v1/p2p/12D3KooW…
peer=12D3KooW… rtt=2ms
connect returns a Connect ID immediately; the attempt ends with exactly one ConnectSettled event carrying that ID. The dialer’s loop is the canonical pattern: match the events it is waiting for by ID or peer, and let everything else pass. It pings after PeerReady (Identify finished) only because that is a convenient milestone for this demo; open_stream does not require Identify for a protocol both peers already registered.
listen_default binds dual-stack QUIC wildcard sockets. The listener rewrites only the printed local-demo addresses to loopback; the endpoint itself continues listening on both wildcard sockets.
If it does not connect
The address fails to parse
Copy the entire value after listen=. A PeerAddr needs the terminal
/p2p/<peer-id> component as well as the QUIC transport address.
The peer never becomes ready
Keep the listener process running. minip2p is caller-driven, so the
listener must continue calling wait to accept the connection,
complete Identify, and answer Ping.
The IPv6 address does not work
Use the printed IPv4 loopback address instead. listen_default
binds separate IPv4 and IPv6 sockets, but the host still needs the
corresponding address family available.
Next: Concepts or Register a protocol.