本教程旨在为新手提供如何使用 Rust libp2p 实现的动手概述。刚接触 Rust 的人可能想要开始使用 Rust本身,然后再深入了解所有的网络乐趣。这个库大量使用了异步 Rust。如果你不熟悉这个概念,Rust async-book应该很有用。刚接触 libp2p 的人可能更愿意先在libp2p.io上获得一般概述 ,尽管本教程不需要 libp2p 知识。
我们将构建一个小型ping克隆,向对等方发送 ping,期待 pong 作为响应。
一.脚手架
让我们从
- 更新到最新的 Rust 工具链,例如:rustup update
- 创建一个新的箱子:cargo init p2p_tutorial
- 在文件中添加libp2p以及futures依赖 Cargo.toml项。当前的 crate 版本可以在 crates.io找到。我们还将包含async-std“属性”功能以允许async main. 在撰写本文时,我们有:
[package]
name = "p2p_tutorial"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libp2p = "0.43.0"
futures = "0.3.21"
async-std = { version = "1.10.0", features = ["attributes"] }
二. 网络身份
有了所有的脚手架,我们就可以深入了解 libp2p 的细节。首先,我们需要为我们的本地节点创建一个网络身份async fn main(),并用允许的属性进行main注释async。libp2p 中的身份是通过公钥/私钥对处理的。PeerId节点通过从它们的公钥派生的它们相互识别。现在,将 main.rs 的内容替换为:
use libp2p::{identity, PeerId};
use std::error::Error;
#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Local peer id: {:?}", local_peer_id);
Ok(())
}
继续构建并运行上面的代码:cargo run. PeerId应该显示一个唯一的 。
三.运输
接下来我们需要构建一个传输。libp2p 中的传输提供面向连接的通信通道(例如 TCP)以及在认证和加密协议等基础上的升级。从技术上讲,libp2p 传输是实现该Transport特征的任何东西。
我们没有为本教程自己构建传输,而是使用development_transport 创建 TCP 传输的便捷函数来noise进行身份验证加密。
此外,development_transport构建多路复用传输,从而多个逻辑子流可以在同一底层 (TCP) 连接上共存。有关子流多路复用的更多详细信息,请查看 crate::core::muxing和yamux。
use libp2p::{identity, PeerId};
use std::error::Error;
#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Local peer id: {:?}", local_peer_id);
let transport = libp2p::development_transport(local_key).await?;
Ok(())
}
四. 网络行为
现在是时候看看 rust-libp2p 的另一个核心特性: NetworkBehaviour. 前面介绍的 traitTransport 定义了如何在网络上发送字节,而 aNetworkBehaviour定义 了在网络上发送的字节。
为了使这一点更具体,让我们看一下NetworkBehaviourtrait 的简单实现:. 正如您可能已经猜到的那样,类似于老式的 网络工具,libp2p向对等方发送 ping 并期望依次收到 pong。不关心ping 和 pong 消息是如何在 网络上发送的,无论它们是通过 TCP 发送的,它们是通过噪声加密还是 纯文本加密。它只关心网络上发送了什么消息。Ping NetworkBehaviourpingPingPing NetworkBehaviour
这两个特征使我们能够清楚地区分如何发送字节和发送Transport什么字节。NetworkBehaviour
考虑到上述情况,让我们扩展我们的示例, 在最后创建一个:Ping NetworkBehaviour
use libp2p::{identity, PeerId};
use libp2p::ping::{Ping, PingConfig};
use std::error::Error;
#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Local peer id: {:?}", local_peer_id);
let transport = libp2p::development_transport(local_key).await?;
// Create a ping network behaviour.
//
// For illustrative purposes, the ping protocol is configured to
// keep the connection alive, so a continuous sequence of pings
// can be observed.
let behaviour = Ping::new(PingConfig::new().with_keep_alive(true));
Ok(())
}
五. swarm
现在我们有了 aTransport和 a NetworkBehaviour,我们需要一些东西来连接两者,让两者都能取得进展。这项工作由Swarm. 简而言之,aSwarm驱动 a Transport和 aNetworkBehaviour向前,将命令从 the 传递 NetworkBehaviour到 theTransport以及从 the Transport传递事件NetworkBehaviour。
use libp2p::{identity, PeerId};
use libp2p::ping::{Ping, PingConfig};
use libp2p::swarm::Swarm;
use std::error::Error;
#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Local peer id: {:?}", local_peer_id);
let transport = libp2p::development_transport(local_key).await?;
// Create a ping network behaviour.
//
// For illustrative purposes, the ping protocol is configured to
// keep the connection alive, so a continuous sequence of pings
// can be observed.
let behaviour = Ping::new(PingConfig::new().with_keep_alive(true));
let mut swarm = Swarm::new(transport, behaviour, local_peer_id);
Ok(())
}
六. 多地址
就位后Swarm,我们都准备好监听传入的连接。我们只需要将地址传递给Swarm,就像 for 一样 std::net::TcpListener::bind。但是,我们没有传递 IP 地址,而是传递了一个Multiaddrlibp2p 的另一个核心概念,值得一看。
AMultiaddr是一个自描述的网络地址和协议栈,用于建立到对等点的连接。Multiaddr可以在 docs.libp2p.io/concepts/addressing 及其规范存储库 github.com/multiformats/multiaddr找到一个很好的介绍 。
让我们的本地节点监听一个新的套接字。此套接字同时侦听多个网络接口。对于每个网络接口,都会创建一个新的侦听地址。随着接口变得可用或不可用,这些可能会随着时间而改变。例如,在我们的 TCP 传输的情况下,它可能(除其他外)侦听环回接口 (localhost)/ip4/127.0.0.1/tcp/24915以及本地网络/ip4/192.168.178.25/tcp/24915。
此外,如果在 CLI 上提供,让我们指示我们的本地节点拨打远程对等点。
use libp2p::{identity, Multiaddr, PeerId};
use libp2p::ping::{Ping, PingConfig};
use libp2p::swarm::{Swarm, dial_opts::DialOpts};
use std::error::Error;
#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Local peer id: {:?}", local_peer_id);
let transport = libp2p::development_transport(local_key).await?;
// Create a ping network behaviour.
//
// For illustrative purposes, the ping protocol is configured to
// keep the connection alive, so a continuous sequence of pings
// can be observed.
let behaviour = Ping::new(PingConfig::new().with_keep_alive(true));
let mut swarm = Swarm::new(transport, behaviour, local_peer_id);
// Tell the swarm to listen on all interfaces and a random, OS-assigned
// port.
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
// Dial the peer identified by the multi-address given as the second
// command-line argument, if any.
if let Some(addr) = std::env::args().nth(1) {
let remote: Multiaddr = addr.parse()?;
swarm.dial(remote)?;
println!("Dialed {}", addr)
}
Ok(())
}
七. 不断轮询 Swarm
我们现在一切就绪。最后一步是驱动Swarm循环,允许它侦听传入连接并建立传出连接,以防我们在 CLI 上指定地址。
use futures::prelude::*;
use libp2p::ping::{Ping, PingConfig};
use libp2p::swarm::{Swarm, SwarmEvent, dial_opts::DialOpts};
use libp2p::{identity, Multiaddr, PeerId};
use std::error::Error;
#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Local peer id: {:?}", local_peer_id);
let transport = libp2p::development_transport(local_key).await?;
// Create a ping network behaviour.
//
// For illustrative purposes, the ping protocol is configured to
// keep the connection alive, so a continuous sequence of pings
// can be observed.
let behaviour = Ping::new(PingConfig::new().with_keep_alive(true));
let mut swarm = Swarm::new(transport, behaviour, local_peer_id);
// Tell the swarm to listen on all interfaces and a random, OS-assigned
// port.
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
// Dial the peer identified by the multi-address given as the second
// command-line argument, if any.
if let Some(addr) = std::env::args().nth(1) {
let remote: Multiaddr = addr.parse()?;
swarm.dial(remote)?;
println!("Dialed {}", addr)
}
loop {
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {:?}", address),
SwarmEvent::Behaviour(event) => println!("{:?}", event),
_ => {}
}
}
}
八. 运行两个节点
为方便起见,上面创建的示例也在 examples/ping.rs. 因此,您可以从教程中创建的您自己的项目中运行以下命令,也可以从 rust-libp2p 存储库的根目录运行以下命令。请注意,在前一种情况下,您需要忽略该--example ping参数。
你需要两个终端。在第一个终端窗口中运行:
cargo run --example ping
它将打印 PeerId 和新的监听地址,例如
Local peer id: PeerId("12D3KooWT1As4mwh3KYBnNTw9bSrRbYQGJTm9SSte82JSumqgCQG")
Listening on "/ip4/127.0.0.1/tcp/24915"
Listening on "/ip4/192.168.178.25/tcp/24915"
Listening on "/ip4/172.17.0.1/tcp/24915"
Listening on "/ip6/::1/tcp/24915"
在第二个终端窗口中,使用以下命令启动示例的新实例:
cargo run --example ping -- /ip4/127.0.0.1/tcp/24915
注意:Multiaddr最后是Multiaddr终端窗口一中较早打印的内容之一。两个对等点必须位于与地址关联的同一网络中。在我们的例子中,可以使用任何打印的地址,因为两个对等点都在同一设备上运行。
两个节点将建立连接,每 15 秒相互发送一次 ping 和 pong 消息。