libp2p/tutorials/ping.rs
1// Copyright 2021 Protocol Labs.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the "Software"),
5// to deal in the Software without restriction, including without limitation
6// the rights to use, copy, modify, merge, publish, distribute, sublicense,
7// and/or sell copies of the Software, and to permit persons to whom the
8// Software is furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19// DEALINGS IN THE SOFTWARE.
20
21//! # Ping Tutorial - Getting started with rust-libp2p
22//!
23//! This tutorial aims to give newcomers a hands-on overview of how to use the
24//! Rust libp2p implementation. People new to Rust likely want to get started on
25//! [Rust](https://www.rust-lang.org/) itself, before diving into all the
26//! networking fun. This library makes heavy use of asynchronous Rust. In case
27//! you are not familiar with this concept, the Rust
28//! [async-book](https://rust-lang.github.io/async-book/) should prove useful.
29//! People new to libp2p might prefer to get a general overview at
30//! [libp2p.io](https://libp2p.io/)
31//! first, although libp2p knowledge is not required for this tutorial.
32//!
33//! We are going to build a small `ping` clone, sending a ping to a peer,
34//! expecting a pong as a response.
35//!
36//! ## Scaffolding
37//!
38//! Let's start off by
39//!
40//! 1. Updating to the latest Rust toolchain, e.g.: `rustup update`
41//!
42//! 2. Creating a new crate: `cargo init rust-libp2p-tutorial`
43//!
44//! 3. Adding `libp2p` as well as `futures` as dependencies in the
45//! `Cargo.toml` file. Current crate versions may be found at
46//! [crates.io](https://crates.io/).
47//! We will also include `tokio` with the
48//! "attributes" feature to allow for an `async main`.
49//! At the time of writing we have:
50//!
51//! ```yaml
52//! [package]
53//! name = "rust-libp2p-tutorial"
54//! version = "0.1.0"
55//! edition = "2021"
56//!
57//! [dependencies]
58//! libp2p = { version = "0.54", features = ["noise", "ping", "tcp", "tokio", "yamux"] }
59//! futures = "0.3.30"
60//! tokio = { version = "1.37.0", features = ["full"] }
61//! tracing-subscriber = { version = "0.3", features = ["env-filter"] }
62//! ```
63//!
64//! ## Network identity
65//!
66//! With all the scaffolding in place, we can dive into the libp2p specifics.
67//! First we need to create a network identity for our local node in `async fn
68//! main()`, annotated with an attribute to allow `main` to be `async`.
69//! Identities in libp2p are handled via a public/private key pair.
70//! Nodes identify each other via their [`PeerId`](crate::PeerId) which is
71//! derived from their public key. Now, replace the contents of main.rs by:
72//!
73//! ```rust
74//! use std::error::Error;
75//!
76//! use tracing_subscriber::EnvFilter;
77//!
78//! #[tokio::main]
79//! async fn main() -> Result<(), Box<dyn Error>> {
80//! let _ = tracing_subscriber::fmt()
81//! .with_env_filter(EnvFilter::from_default_env())
82//! .try_init();
83//!
84//! let mut swarm = libp2p::SwarmBuilder::with_new_identity();
85//!
86//! Ok(())
87//! }
88//! ```
89//!
90//! Go ahead and build and run the above code with: `cargo run`. Nothing happening thus far.
91//!
92//! ## Transport
93//!
94//! Next up we need to construct a transport. Each transport in libp2p provides encrypted streams.
95//! E.g. combining TCP to establish connections, NOISE to encrypt these connections and Yamux to run
96//! one or more streams on a connection. Another libp2p transport is QUIC, providing encrypted
97//! streams out-of-the-box. We will stick to TCP for now. Each of these implement the [`Transport`]
98//! trait.
99//!
100//! ```rust
101//! use std::error::Error;
102//!
103//! use libp2p::{noise, tcp, yamux};
104//! use tracing_subscriber::EnvFilter;
105//!
106//! #[tokio::main]
107//! async fn main() -> Result<(), Box<dyn Error>> {
108//! let _ = tracing_subscriber::fmt()
109//! .with_env_filter(EnvFilter::from_default_env())
110//! .try_init();
111//!
112//! let mut swarm = libp2p::SwarmBuilder::with_new_identity()
113//! .with_tokio()
114//! .with_tcp(
115//! tcp::Config::default(),
116//! noise::Config::new,
117//! yamux::Config::default,
118//! )?;
119//!
120//! Ok(())
121//! }
122//! ```
123//!
124//! ## Network behaviour
125//!
126//! Now it is time to look at another core trait of rust-libp2p: the
127//! [`NetworkBehaviour`]. While the previously introduced trait [`Transport`]
128//! defines _how_ to send bytes on the network, a [`NetworkBehaviour`] defines
129//! _what_ bytes and to _whom_ to send on the network.
130//!
131//! To make this more concrete, let's take a look at a simple implementation of
132//! the [`NetworkBehaviour`] trait: the [`ping::Behaviour`](crate::ping::Behaviour).
133//! As you might have guessed, similar to the good old ICMP `ping` network tool,
134//! libp2p [`ping::Behaviour`](crate::ping::Behaviour) sends a ping to a peer and expects
135//! to receive a pong in turn. The [`ping::Behaviour`](crate::ping::Behaviour) does not care _how_
136//! the ping and pong messages are sent on the network, whether they are sent via
137//! TCP, whether they are encrypted via [noise](crate::noise) or just in
138//! [plaintext](crate::plaintext). It only cares about _what_ messages and to _whom_ to sent on the
139//! network.
140//!
141//! The two traits [`Transport`] and [`NetworkBehaviour`] allow us to cleanly
142//! separate _how_ to send bytes from _what_ bytes and to _whom_ to send.
143//!
144//! With the above in mind, let's extend our example, creating a
145//! [`ping::Behaviour`](crate::ping::Behaviour) at the end:
146//!
147//! ```rust
148//! use std::error::Error;
149//!
150//! use libp2p::{noise, ping, tcp, yamux};
151//! use tracing_subscriber::EnvFilter;
152//!
153//! #[tokio::main]
154//! async fn main() -> Result<(), Box<dyn Error>> {
155//! let _ = tracing_subscriber::fmt()
156//! .with_env_filter(EnvFilter::from_default_env())
157//! .try_init();
158//!
159//! let mut swarm = libp2p::SwarmBuilder::with_new_identity()
160//! .with_tokio()
161//! .with_tcp(
162//! tcp::Config::default(),
163//! noise::Config::new,
164//! yamux::Config::default,
165//! )?
166//! .with_behaviour(|_| ping::Behaviour::default())?;
167//!
168//! Ok(())
169//! }
170//! ```
171//!
172//! ## Swarm
173//!
174//! Now that we have a [`Transport`] and a [`NetworkBehaviour`], we can build the [`Swarm`]
175//! which connects the two, allowing both to make progress. Put simply, a [`Swarm`] drives both a
176//! [`Transport`] and a [`NetworkBehaviour`] forward, passing commands from the [`NetworkBehaviour`]
177//! to the [`Transport`] as well as events from the [`Transport`] to the [`NetworkBehaviour`].
178//!
179//! ```rust
180//! use std::error::Error;
181//!
182//! use libp2p::{noise, ping, tcp, yamux};
183//! use tracing_subscriber::EnvFilter;
184//!
185//! #[tokio::main]
186//! async fn main() -> Result<(), Box<dyn Error>> {
187//! let _ = tracing_subscriber::fmt()
188//! .with_env_filter(EnvFilter::from_default_env())
189//! .try_init();
190//!
191//! let mut swarm = libp2p::SwarmBuilder::with_new_identity()
192//! .with_tokio()
193//! .with_tcp(
194//! tcp::Config::default(),
195//! noise::Config::new,
196//! yamux::Config::default,
197//! )?
198//! .with_behaviour(|_| ping::Behaviour::default())?
199//! .build();
200//!
201//! Ok(())
202//! }
203//! ```
204//!
205//! ## Idle connection timeout
206//!
207//! Now, for this example in particular, we need set the idle connection timeout.
208//! The default connection timeout is 10 seconds.
209//!
210//! Whether you need to set this in your application too depends on your usecase.
211//! Typically, connections are kept alive if they are "in use" by a certain protocol.
212//! The ping protocol however is only an "auxiliary" kind of protocol.
213//! Thus, without any other behaviour in place, we would not be able to observe any pings after 10s.
214//!
215//! ```rust
216//! use std::{error::Error, time::Duration};
217//!
218//! use libp2p::{noise, ping, tcp, yamux};
219//! use tracing_subscriber::EnvFilter;
220//!
221//! #[tokio::main]
222//! async fn main() -> Result<(), Box<dyn Error>> {
223//! let _ = tracing_subscriber::fmt()
224//! .with_env_filter(EnvFilter::from_default_env())
225//! .try_init();
226//!
227//! let mut swarm = libp2p::SwarmBuilder::with_new_identity()
228//! .with_tokio()
229//! .with_tcp(
230//! tcp::Config::default(),
231//! noise::Config::new,
232//! yamux::Config::default,
233//! )?
234//! .with_behaviour(|_| ping::Behaviour::default())?
235//! .with_swarm_config(|cfg| {
236//! cfg.with_idle_connection_timeout(Duration::from_secs(u64::MAX))
237//! }) // Allows us to observe pings indefinitely.
238//! .build();
239//!
240//! Ok(())
241//! }
242//! ```
243//!
244//! ## Multiaddr
245//!
246//! With the [`Swarm`] in place, we are all set to listen for incoming
247//! connections. We only need to pass an address to the [`Swarm`], just like for
248//! [`std::net::TcpListener::bind`]. But instead of passing an IP address, we
249//! pass a [`Multiaddr`] which is yet another core concept of libp2p worth
250//! taking a look at.
251//!
252//! A [`Multiaddr`] is a self-describing network address and protocol stack that
253//! is used to establish connections to peers. A good introduction to
254//! [`Multiaddr`] can be found at
255//! [docs.libp2p.io/concepts/addressing](https://docs.libp2p.io/concepts/addressing/)
256//! and its specification repository
257//! [github.com/multiformats/multiaddr](https://github.com/multiformats/multiaddr/).
258//!
259//! Let's make our local node listen on a new socket.
260//! This socket is listening on multiple network interfaces at the same time. For
261//! each network interface, a new listening address is created. These may change
262//! over time as interfaces become available or unavailable.
263//! For example, in case of our TCP transport it may (among others) listen on the
264//! loopback interface (localhost) `/ip4/127.0.0.1/tcp/24915` as well as the local
265//! network `/ip4/192.168.178.25/tcp/24915`.
266//!
267//! In addition, if provided on the CLI, let's instruct our local node to dial a
268//! remote peer.
269//!
270//! ```rust
271//! use std::{error::Error, time::Duration};
272//!
273//! use libp2p::{noise, ping, tcp, yamux, Multiaddr};
274//! use tracing_subscriber::EnvFilter;
275//!
276//! #[tokio::main]
277//! async fn main() -> Result<(), Box<dyn Error>> {
278//! let _ = tracing_subscriber::fmt()
279//! .with_env_filter(EnvFilter::from_default_env())
280//! .try_init();
281//!
282//! let mut swarm = libp2p::SwarmBuilder::with_new_identity()
283//! .with_tokio()
284//! .with_tcp(
285//! tcp::Config::default(),
286//! noise::Config::new,
287//! yamux::Config::default,
288//! )?
289//! .with_behaviour(|_| ping::Behaviour::default())?
290//! .with_swarm_config(|cfg| {
291//! cfg.with_idle_connection_timeout(Duration::from_secs(u64::MAX))
292//! }) // Allows us to observe pings indefinitely.
293//! .build();
294//!
295//! // Tell the swarm to listen on all interfaces and a random, OS-assigned
296//! // port.
297//! swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
298//!
299//! // Dial the peer identified by the multi-address given as the second
300//! // command-line argument, if any.
301//! if let Some(addr) = std::env::args().nth(1) {
302//! let remote: Multiaddr = addr.parse()?;
303//! swarm.dial(remote)?;
304//! println!("Dialed {addr}")
305//! }
306//!
307//! Ok(())
308//! }
309//! ```
310//!
311//! ## Continuously polling the Swarm
312//!
313//! We have everything in place now. The last step is to drive the [`Swarm`] in
314//! a loop, allowing it to listen for incoming connections and establish an
315//! outgoing connection in case we specify an address on the CLI.
316//!
317//! ```no_run
318//! use std::{error::Error, time::Duration};
319//!
320//! use futures::prelude::*;
321//! use libp2p::{noise, ping, swarm::SwarmEvent, tcp, yamux, Multiaddr};
322//! use tracing_subscriber::EnvFilter;
323//!
324//! #[tokio::main]
325//! async fn main() -> Result<(), Box<dyn Error>> {
326//! let _ = tracing_subscriber::fmt()
327//! .with_env_filter(EnvFilter::from_default_env())
328//! .try_init();
329//!
330//! let mut swarm = libp2p::SwarmBuilder::with_new_identity()
331//! .with_tokio()
332//! .with_tcp(
333//! tcp::Config::default(),
334//! noise::Config::new,
335//! yamux::Config::default,
336//! )?
337//! .with_behaviour(|_| ping::Behaviour::default())?
338//! .with_swarm_config(|cfg| {
339//! cfg.with_idle_connection_timeout(Duration::from_secs(u64::MAX))
340//! }) // Allows us to observe pings indefinitely.
341//! .build();
342//!
343//! // Tell the swarm to listen on all interfaces and a random, OS-assigned
344//! // port.
345//! swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
346//!
347//! // Dial the peer identified by the multi-address given as the second
348//! // command-line argument, if any.
349//! if let Some(addr) = std::env::args().nth(1) {
350//! let remote: Multiaddr = addr.parse()?;
351//! swarm.dial(remote)?;
352//! println!("Dialed {addr}")
353//! }
354//!
355//! loop {
356//! match swarm.select_next_some().await {
357//! SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"),
358//! SwarmEvent::Behaviour(event) => println!("{event:?}"),
359//! _ => {}
360//! }
361//! }
362//! }
363//! ```
364//!
365//! ## Running two nodes
366//!
367//! For convenience the example created above is also implemented in full in
368//! `examples/ping.rs`. Thus, you can either run the commands below from your
369//! own project created during the tutorial, or from the root of the rust-libp2p
370//! repository. Note that in the former case you need to ignore the `--example
371//! ping` argument.
372//!
373//! You need two terminals. In the first terminal window run:
374//!
375//! ```sh
376//! cargo run --example ping
377//! ```
378//!
379//! It will print the new listening addresses, e.g.
380//! ```sh
381//! Listening on "/ip4/127.0.0.1/tcp/24915"
382//! Listening on "/ip4/192.168.178.25/tcp/24915"
383//! Listening on "/ip4/172.17.0.1/tcp/24915"
384//! Listening on "/ip6/::1/tcp/24915"
385//! ```
386//!
387//! In the second terminal window, start a new instance of the example with:
388//!
389//! ```sh
390//! cargo run --example ping -- /ip4/127.0.0.1/tcp/24915
391//! ```
392//!
393//! Note: The [`Multiaddr`] at the end being one of the [`Multiaddr`] printed
394//! earlier in terminal window one.
395//! Both peers have to be in the same network with which the address is associated.
396//! In our case any printed addresses can be used, as both peers run on the same
397//! device.
398//!
399//! The two nodes will establish a connection and send each other ping and pong
400//! messages every 15 seconds.
401//!
402//! [`Multiaddr`]: crate::core::Multiaddr
403//! [`NetworkBehaviour`]: crate::swarm::NetworkBehaviour
404//! [`Transport`]: crate::core::Transport
405//! [`PeerId`]: crate::core::PeerId
406//! [`Swarm`]: crate::swarm::Swarm