libp2p 教程: 用Rust构建一个peer-to-peer应用示例

1,063 阅读20分钟

在过去的几年里,很大程度上由于围绕区块链和加密货币的炒作,去中心化应用程序获得了相当大的发展势头。推动去中心化兴趣激增的另一个因素是,人们越来越意识到,将大部分网络交由少数公司掌控,在数据隐私和垄断化方面存在的缺点。

无论如何,最近在去中心化软件领域出现了一些非常有趣的发展,这些发展甚至与所有的加密货币和区块链技术无关。

值得注意的例子包括IPFS(行星际文件系统);全新的分布式编码平台Radicle;去中心化社交网络Scuttlebutt;以及Fediverse中的许多其他应用程序,比如Mastodon。

在本教程中,我们将向您展示如何使用Rust和极好的libp2p库构建一个非常简单的点对点应用程序,该库对广泛的语言处于不同的成熟阶段。

我们将构建一个烹饪食谱应用程序,它具有简单的命令行界面,使我们能够:

  • 创建食谱
  • 发布食谱
  • 列出本地食谱
  • 列出我们在网络中发现的其他对等方
  • 列出给定对等方发布的食谱
  • 列出我们所知道的所有对等方的所有食谱

我们将使用大约300行Rust代码完成所有这些工作。让我们开始吧!

安装 Rust

要跟随本教程,您只需要安装最新版的 Rust(1.47+ 版本以上)。

首先,创建一个新的 Rust 项目:

shell
cargo new rust-p2p-example
cd rust-p2p-example

接下来,编辑 Cargo.toml 文件并添加您将需要的依赖项:

toml
[dependencies]
libp2p = { version = "0.31", features = ["tcp-tokio", "mdns-tokio"] }
tokio = { version = "0.3", features = ["io-util", "io-std", "stream", "macros", "rt", "rt-multi-thread", "fs", "time", "sync"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
once_cell = "1.5"
log = "0.4"
pretty_env_logger = "0.4"

如上所述,我们将使用 libp2p 来处理点对点网络部分。更具体地说,我们将与 Tokio 异步运行时一起使用它。我们将使用 Serde 进行 JSON 序列化和反序列化,并使用一些辅助库来处理日志记录和初始化状态。

请注意,Cargo.toml 中的依赖项列表包括了版本号和特性标志,这些特性是根据项目需求来指定的。例如,libp2p 依赖项启用了 TCP 传输和 mDNS 服务发现的特定功能。tokio 依赖项则启用了异步 I/O、宏、多线程运行时等特性。serdeserde_json 用于处理 JSON 数据,而 once_cell 用于状态的惰性初始化。logpretty_env_logger 用于日志记录。

什么是 libp2p?

libp2p 是一套用于构建点对点应用程序的协议,专注于模块化设计。

有几种语言的库实现,如 JavaScript、Go 和 Rust。这些库都实现了相同的 libp2p 规范,因此用 Go 构建的 libp2p 客户端可以与用 JavaScript 编写的另一个客户端无缝交互,只要它们在所选协议栈方面兼容即可。这些协议涵盖了从基本的网络传输协议到安全层协议和多路复用等多种范围。

在这篇文章中,我们不会深入探讨 libp2p 的细节,但如果您有兴趣深入了解,官方的 libp2p 文档提供了非常好的概览,介绍了我们将在途中遇到的各种概念。

libp2p 如何工作 要看到 libp2p 的实际应用,让我们启动我们的食谱应用程序。我们将首先定义一些我们需要的常量和类型:

rust
const STORAGE_FILE_PATH: &str = "./recipes.json";

type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;

static KEYS: Lazy<identity::Keypair> = Lazy::new(|| identity::Keypair::generate_ed25519());
static PEER_ID: Lazy<PeerId> = Lazy::new(|| PeerId::from(KEYS.public()));
static TOPIC: Lazy<Topic> = Lazy::new(|| Topic::new("recipes"));

我们将在名为 recipes.json 的简单 JSON 文件中存储本地食谱,应用程序期望该文件与可执行文件位于同一文件夹中。我们还定义了一个辅助类型 Result,它允许我们传播任意错误。

然后,我们使用 once_cell::Lazy 来延迟初始化一些事情。首先,最重要的是,我们用它来生成一个密钥对以及从公钥派生的所谓 PeerId。我们还创建了一个 Topic,这是 libp2p 的另一个关键概念。

所有这些意味着什么?简而言之,PeerId 只是一个特定对等方在点对点网络中的一个唯一标识符。我们从密钥对中派生它以确保其唯一性。此外,密钥对使我们能够安全地与网络的其余部分通信,确保没有人可以冒充我们。

另一方面,Topic 是来自 Floodsub 的概念,它是 libp2p pub/sub 接口的实现。Topic 是我们可以订阅并向其发送消息的东西——例如,只监听 pub/sub 网络上的一部分流量。

我们还需要一些类型来定义食谱:

rust
type Recipes = Vec<Recipe>;

#[derive(Debug, Serialize, Deserialize)]
struct Recipe {
    id: usize,
    name: String,
    ingredients: String,
    instructions: String,
    public: bool,
}

食谱的结构相当简单。它有一个 ID、一个名称、一些成分和执行它的指令。此外,我们添加了一个公共标志,以便我们可以区分我们想要分享的食谱和我们想要自己保留的食谱。

如前所述,我们可以通过两种方式从其他对等方获取列表:从所有对等方或从一个对等方,这由 ListMode 枚举表示。

ListRequestListResponse 类型只是这些类型及其使用它们的数据的包装器。

EventType 枚举区分来自另一个对等方的响应和我们自己的输入。我们稍后将看到为什么这种区别是相关的。

创建 libp2p 客户端

让我们开始编写主函数,以在点对点网络中设置一个对等方。

rust
#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    info!("Peer Id: {}", PEER_ID.clone());
    let (response_sender, mut response_rcv) = mpsc::unbounded_channel();

    let auth_keys = Keypair::<X25519Spec>::new()
        .into_authentic(&KEYS)
        .expect("can create auth keys");

我们初始化日志记录,并创建一个异步通道,以便应用程序的不同部分之间进行通信。我们将在稍后使用此通道将来自 libp2p 网络栈的响应发送回我们的应用程序以进行处理。

我们还为 Noise 加密协议创建了一些认证密钥,这将用于保护网络内的流量。为此,我们创建了一个新的密钥对,并使用 into_authentic 函数用我们的身份密钥对其进行签名。

下一步非常重要,涉及到 libp2p 的一些核心概念:创建所谓的 Transport(传输)。

rust
let transp = TokioTcpConfig::new()
    .upgrade(upgrade::Version::V1)
    .authenticate(NoiseConfig::xx(auth_keys).into_authenticated())
    .multiplex(mplex::MplexConfig::new())
    .boxed();

传输是一组网络协议,它使对等方之间能够进行面向连接的通信。也可以在同一个应用程序中使用多个传输 —— 例如,同时使用 TCP/IP 和 Websockets,或根据不同用例同时使用 UDP。

在这个例子中,我们将使用 Tokio 的异步 TCP 作为基础。一旦建立了 TCP 连接,我们将升级它以使用 Noise 进行安全通信。一个基于 Web 的例子是在 HTTP 上使用 TLS 来创建安全连接。

我们使用 NoiseConfig:xx 握手模式,这是三个选项之一,因为它是唯一保证与其他 libp2p 应用程序互操作的选项。

libp2p 的好处之一是,我们可以编写一个 Rust 客户端,另一个人可以编写一个 JavaScript 客户端,只要两个版本的库都实现了协议,它们就可以轻松地进行通信。

最后,我们还对传输进行了多路复用,这使我们能够在同一个传输上多路复用多个子流或连接。

哇,这是相当多的理论!但所有这些都可以 libp2p 文档中找到。这只是创建点对点传输的众多方法之一。

下一个概念是 NetworkBehaviour(网络行为)。这是 libp2p 的一部分,实际上定义了网络和所有对等方的逻辑 —— 例如,如何处理传入的事件以及要发送哪些事件。

rust
let mut behaviour = RecipeBehaviour {
    floodsub: Floodsub::new(PEER_ID.clone()),
    mdns: TokioMdns::new().expect("can create mdns"),
    response_sender,
};

behaviour.floodsub.subscribe(TOPIC.clone());

在这种情况下,正如上文提到的,我们将使用 FloodSub 协议来处理事件。我们还将使用 mDNS,这是一种在本地网络上发现其他对等方的协议。我们还将在这里放入我们通道的发送端,以便我们可以使用它将事件传播回应用程序的主要部分。

我们之前创建的 FloodSub 主题现在将从我们的行为中订阅,这意味着我们将接收事件,并可以发送该主题上的事件。

来自 LogRocket 的更多优秀文章:

  • 不要错过《重播》(The Replay),这是 LogRocket 的一份精选通讯。
  • 了解 LogRocket 的伽利略(Galileo)如何穿透噪音,主动解决您应用程序中的问题。
  • 使用 React 的 useEffect 来优化您的应用程序性能。
  • 在多个 Node 版本之间切换。
  • 发现如何使用 TypeScript 与 React 的 children 属性。
  • 探索如何使用 CSS 创建自定义鼠标光标。
  • 咨询委员会不仅适用于高管。加入 LogRocket 的内容咨询委员会。您将帮助我们确定要创建的内容类型,并获取独家聚会、社交认证和礼品。
  • 我们几乎完成了 libp2p 设置。我们需要的最后一个概念是 Swarm(群)。
rust
let mut swarm = SwarmBuilder::new(transp, behaviour, PEER_ID.clone())
    .executor(Box::new(|fut| {
        tokio::spawn(fut);
    }))
    .build();

Swarm 管理使用传输创建的连接,并执行我们创建的网络行为,触发和接收事件,并为我们提供了一种从外部获取它们的方式。

我们使用我们的传输、行为和对等 ID 创建 Swarm。执行器部分简单地告诉 Swarm 使用 Tokio 运行时来运行内部,但我们也可以在这里使用其他异步运行时。

剩下要做的就是启动我们的 Swarm:

rust
Swarm::listen_on(
    &mut swarm,
    "/ip4/0.0.0.0/tcp/0"
        .parse()
        .expect("can get a local socket"),
)
.expect("swarm can be started");

类似于启动,例如,一个 TCP 服务器,我们只需使用本地 IP 调用 listen_on,让操作系统为我们决定端口。这将启动 Swarm 并使用我们所有的设置,但我们还实际上没有定义任何逻辑。

让我们从处理用户输入开始。

在 libp2p 中处理输入

对于用户输入,我们将简单地依赖于传统的 STDIN。所以在 Swarm::listen_on 调用之前,我们将添加:

rust
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines();

这定义了一个 STDIN 上的异步读取器,它逐行读取流。因此,如果我们按下回车,将会有一个新的传入消息。

接下来,我们将创建我们的事件循环,它将监听来自 STDIN、Swarm 以及我们上面定义的响应通道的事件。

rust
loop {
    let evt = {
        tokio::select! {
            line = stdin.next_line() => Some(EventType::Input(line.expect("can get line").expect("can read line from stdin"))),
            event = swarm.next() => {
                info!("Unhandled Swarm Event: {:?}", event);
                None
            },
            response = response_rcv.recv() => Some(EventType::Response(response.expect("response exists"))),
        }
    };
    ...
}
}

我们使用 Tokio 的 select 宏来等待多个异步进程,处理第一个完成的进程。我们不对 Swarm 事件做任何操作;这些在我们稍后将看到的 RecipeBehaviour 中处理,但我们仍然需要调用 swarm.next() 来推动 Swarm 前进。

让我们在 ... 的位置添加一些事件处理逻辑:

rust
if let Some(event) = evt {
    match event {
        EventType::Response(resp) => {
           ...
        }
        EventType::Input(line) => match line.as_str() {
            "ls p" => handle_list_peers(&mut swarm).await,
            cmd if cmd.starts_with("ls r") => handle_list_recipes(cmd, &mut swarm).await,
            cmd if cmd.starts_with("create r") => handle_create_recipe(cmd).await,
            cmd if cmd.starts_with("publish r") => handle_publish_recipe(cmd).await,
            _ => error!("unknown command"),
        },
    }
}

如果有事件,我们对其进行匹配,看看它是 Response 事件还是 Input 事件。现在,让我们只看看 Input 事件。

我们支持以下几个选项:

  • ls p 列出所有已知的对等方
  • ls r 列出本地食谱
  • ls r {peerId} 列出某个特定对等方发布的食谱
  • ls r all 列出所有已知对等方发布的食谱
  • publish r {recipeId} 发布给定的食谱
  • create r {recipeName}|{recipeIngredients}|{recipeInstructions} 使用给定的数据和递增的 ID 创建一个新食谱

在这种情况下,这意味着向我们的对等方发送食谱请求,等待他们响应,并显示结果。在点对点网络中,这可能需要一段时间,因为一些对等方可能在地球的另一边,我们不知道他们是否真的会响应我们。这与向 HTTP 服务器发送请求有很大的不同。

让我们先看看列出对等方的逻辑:

rust
async fn handle_list_peers(swarm: &mut Swarm<RecipeBehaviour>) {
    info!("Discovered Peers:");
    let nodes = swarm.mdns.discovered_nodes();
    let mut unique_peers = HashSet::new();
    for peer in nodes {
        unique_peers.insert(peer);
    }
    unique_peers.iter().for_each(|p| info!("{}", p));
}

在这种情况下,我们可以使用 mDNS 给我们所有发现的节点,迭代并显示它们。简单。

接下来,让我们在处理列表命令之前,了解创建和发布食谱的逻辑:

rust
async fn handle_create_recipe(cmd: &str) {
    ...
}

async fn handle_publish_recipe(cmd: &str) {
    ...
}

在这两种情况下,我们需要解析字符串以获取到以 | 分隔的数据,或者在发布的情况下是给定的食谱 ID,并在给定的输入无效时记录错误。

在创建的情况下,我们使用给定的数据调用 create_new_recipe 辅助函数。让我们看看我们需要的所有辅助函数,以便与我们简单的本地 JSON 存储食谱进行交互:

rust
async fn create_new_recipe(name: &str, ingredients: &str, instructions: &str) -> Result<()> {
    ...
}

async fn publish_recipe(id: usize) -> Result<()> {
    ...
}

async fn read_local_recipes() -> Result<Recipes> {
    ...
}

async fn write_local_recipes(recipes: &Recipes) -> Result<()> {
    ...
}

最基本的构建块是 read_local_recipeswrite_local_recipes,它们简单地从存储文件中读取和反序列化或序列化并写入食谱。

publish_recipe 辅助函数从文件中获取所有食谱,查找给定 ID 的食谱,并将其实例公有标志设置为 true。

在创建食谱时,我们也从文件中获取所有食谱,在末尾添加一个新食谱,然后重新写入整个数据,覆盖文件。这不是很高效,但它简单且有效。

使用 libp2p 发送消息

让我们接下来看看列表命令,并探索我们如何向其他对等方发送消息。

在列表命令中,有三种可能的情况:

rust
async fn handle_list_recipes(cmd: &str, swarm: &mut Swarm<RecipeBehaviour>) {
    let rest = cmd.strip_prefix("ls r ");
    match rest {
        Some("all") => {
            let req = ListRequest {
                mode: ListMode::ALL,
            };
            let json = serde_json::to_string(&req).expect("can jsonify request");
            swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
        }
        Some(recipes_peer_id) => {
            let req = ListRequest {
                mode: ListMode::One(recipes_peer_id.to_owned()),
            };
            let json = serde_json::to_string(&req).expect("can jsonify request");
            swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
        }
        None => {
            match read_local_recipes().await {
                Ok(v) => {
                    info!("Local Recipes ({})", v.len());
                    v.iter().for_each(|r| info!("{:?}", r));
                }
                Err(e) => error!("error fetching local recipes: {}", e),
            };
        }
    };
}

我们解析传入的命令,剥离 "ls r" 部分,并检查剩余的内容。如果命令中没有其他内容,我们可以简单地获取我们的本地食谱并使用上一节中定义的辅助函数打印它们。

如果我们遇到 "all" 关键字,我们创建一个 ListRequest 并将 ListMode 设置为 ALL,将其序列化为 JSON,并使用我们的 Swarm 中的 FloodSub 实例将其发布到前面提到的主题。

如果我们在命令中遇到一个对等方 ID,情况也是一样,只是我们会发送带有该对等方 ID 的 ListMode::One 模式。我们可以检查它是否是有效的对等方 ID,或者它是否是我们发现的对等方 ID,但让我们保持简单:如果没有人听它,就不会发生任何事情。

这就是我们发送网络消息所需做的一切。现在的问题是,这些消息发生了什么?它们在哪里被处理?

在点对点应用程序的情况下,请记住,我们都是事件的发送者和接收者,因此我们需要在我们的实现中处理发出和传入的事件。

使用 libp2p 响应消息

这是我们的 RecipeBehaviour 起作用的部分。让我们定义它:

rust
#[derive(NetworkBehaviour)]
struct RecipeBehaviour {
    floodsub: Floodsub,
    mdns: TokioMdns,
    #[behaviour(ignore)]
    response_sender: mpsc::UnboundedSender<ListResponse>,
}

行为本身只是一个结构体,但我们使用了 libp2p 的 NetworkBehaviour 派生宏,因此我们不必手动实现所有的 trait 函数。

这个派生宏为结构体的成员实现了 NetworkBehaviour trait 的函数,这些成员没有用 behaviour(ignore) 注解。我们的通道在这里被忽略了,因为它与我们的行为没有直接关系。

剩下的是为 FloodsubEventMdnsEvent 实现 inject_event 函数。

让我们从 mDNS 开始:

rust
impl NetworkBehaviourEventProcess<MdnsEvent> for RecipeBehaviour {
    fn inject_event(&mut self, event: MdnsEvent) {
        match event {
            MdnsEvent::Discovered(discovered_list) => {
                for (peer, _addr) in discovered_list {
                    self.floodsub.add_node_to_partial_view(peer);
                }
            }
            MdnsEvent::Expired(expired_list) => {
                for (peer, _addr) in expired_list {
                    if !self.mdns.has_node(&peer) {
                        self.floodsub.remove_node_from_partial_view(&peer);
                    }
                }
            }
        }
    }
}

当这个处理器的事件进来时,会调用 inject_event 函数。在 mDNS 方面,只有两个事件,Discovered 和 Expired,它们在我们在网络上看到一个新的对等方或一个现有的对等方离开时触发。在这两种情况下,我们要么将其添加到要么从我们的 FloodSub “局部视图”中删除,这是一个要传播我们消息的节点列表。

pub/sub 事件的 inject_event 有点复杂。我们需要对传入的 ListRequest 和 ListResponse 负载做出反应。如果我们发送了一个 ListRequest,接收请求的对等方将获取其本地发布的食谱,然后需要一种方法将它们发送回来。

将它们发送回请求对等方的唯一方法是在网络上发布它们。由于 pub/sub 确实是我们拥有的唯一机制,我们需要对传入的请求和响应都做出反应。

让我们看看这是如何工作的:

rust
impl NetworkBehaviourEventProcess<FloodsubEvent> for RecipeBehaviour {
    fn inject_event(&mut self, event: FloodsubEvent) {
        match event {
            FloodsubEvent::Message(msg) => {
                if let Ok(resp) = serde_json::from_slice::<ListResponse>(&msg.data) {
                    if resp.receiver == PEER_ID.to_string() {
                        info!("Response from {}:", msg.source);
                        resp.data.iter().for_each(|r| info!("{:?}", r));
                    }
                } else if let Ok(req) = serde_json::from_slice::<ListRequest>(&msg.data) {
                    match req.mode {
                        ListMode::ALL => {
                            info!("Received ALL req: {:?} from {:?}", req, msg.source);
                            respond_with_public_recipes(
                                self.response_sender.clone(),
                                msg.source.to_string(),
                            );
                        }
                        ListMode::One(ref peer_id) => {
                            if peer_id == &PEER_ID.to_string() {
                                info!("Received req: {:?} from {:?}", req, msg.source);
                                respond_with_public_recipes(
                                    self.response_sender.clone(),
                                    msg.source.to_string(),
                                );
                            }
                        }
                    }
                }
            }
            _ => (),
        }
    }
}

我们匹配传入的消息,尝试将其反序列化为请求或响应。在响应的情况下,我们简单地打印响应以及调用者的对等方 ID,我们使用 msg.source 获取它。当我们收到传入的请求时,我们需要区分 ALL 和 One 情况。

在 One 情况下,我们检查给定的对等方 ID 是否与我们的相同——即请求实际上是针对我们的。如果是,我们返回我们发布的食谱,这也是我们在 ALL 情况下的响应。

在这两种情况下,我们都调用 respond_with_public_recipes 辅助函数:

rust
fn respond_with_public_recipes(sender: mpsc::UnboundedSender<ListResponse>, receiver: String) {
    tokio::spawn(async move {
        match read_local_recipes().await {
            Ok(recipes) => {
                let resp = ListResponse {
                    mode: ListMode::ALL,
                    receiver,
                    data: recipes.into_iter().filter(|r| r.public).collect(),
                };
                if let Err(e) = sender.send(resp) {
                    error!("error sending response via channel, {}", e);
                }
            }
            Err(e) => error!("error fetching local recipes to answer ALL request, {}", e),
        }
    });
}

在这个辅助方法中,我们使用 Tokio 的 spawn 异步执行一个 future,该 future 读取所有本地食谱,从数据创建 ListResponse,并通过 channel_sender 发送这些数据到我们的事件循环,我们像这样处理它:

rust
EventType::Response(resp) => {
    let json = serde_json::to_string(&resp).expect("can jsonify response");
    swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
}

如果我们注意到一个通过 Response 事件“内部”发送的事件,我们将其序列化为 JSON 并将其发送到网络上。

使用 libp2p 进行测试

实现部分到此为止。现在让我们来测试一下。

要检查我们的实现是否有效,让我们在几个终端中使用以下命令启动应用程序:

shell
RUST_LOG=info cargo run

请记住,应用程序期望在启动它的目录中有一个名为 recipes.json 的文件。

当应用程序启动后,我们会得到以下日志,打印出我们的对等方 ID:

plaintext
INFO  rust_peer_to_peer_example > Peer Id: 12D3KooWDc1FDabQzpntvZRWeDZUL351gJRy3F4E8VN5Gx2pBCU2

现在我们需要按回车键来启动事件循环。

输入 ls p,我们会得到我们发现的对等方列表:

shell
ls p
INFO  rust_peer_to_peer_example > Discovered Peers:
INFO  rust_peer_to_peer_example > 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
INFO  rust_peer_to_peer_example > 12D3KooWLGN85pv5XTDALGX5M6tRgQtUGMWXWasWQD6oJjMcEENA

输入 ls r,我们会得到本地食谱的列表:

shell
ls r
INFO  rust_peer_to_peer_example > Local Recipes (3)
INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
INFO  rust_peer_to_peer_example > Recipe { id: 1, name: " Tea", ingredients: "Tea, Water", instructions: "Boil Water, add tea", public: false }
INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

调用 ls r all 会触发向其他对等方发送请求并返回他们的食谱:

shell
ls r all
INFO  rust_peer_to_peer_example > Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

如果我们使用对等方 ID 和 ls r,同样的事情也会发生:

shell
ls r 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
INFO  rust_peer_to_peer_example > Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

它有效!您也可以在同一网络上尝试使用大量的客户端。

您可以在 GitHub 上找到完整的示例代码。

总结

在这篇文章中,我们介绍了如何使用 Rust 和 libp2p 构建一个小型的去中心化网络应用程序。

如果您来自网络背景,许多网络概念可能会有一些熟悉,但构建点对点应用程序仍然需要一种根本不同的设计和构建方法。

libp2p 库相当成熟,并且由于 Rust 在加密领域内的受欢迎程度,有一个正在出现的丰富的库生态系统,用于构建强大的去中心化应用程序。

LogRocket:

为 Rust 应用程序提供完整的 Web 前端可见性 调试 Rust 应用程序可能很困难,特别是当用户遇到难以重现的问题时。如果您对监控和跟踪 Rust 应用程序的性能、自动浮现错误以及跟踪慢速网络请求和加载时间感兴趣,请尝试 LogRocket。

LogRocket 提供的仪表板免费试用横幅 LogRocket 就像一个为 Web 和移动应用程序准备的 DVR,记录 Rust 应用程序上发生的每一件事。与其猜测问题发生的原因,不如在问题发生时聚合和报告应用程序的状态。LogRocket 还监控您的应用程序性能,报告客户端 CPU 负载、客户端内存使用情况等指标。

现代化您调试 Rust 应用程序的方式 —— 开始免费监控。

原文链接:blog.logrocket.com/libp2p-tuto…