Skip to content

Latest commit

 

History

History
108 lines (64 loc) · 8.44 KB

introduction_zh.md

File metadata and controls

108 lines (64 loc) · 8.44 KB

P2P 介绍

目标

本项目旨在实现一个轻量、简洁、可靠、高性能、对用户友好的 P2P 框架,它的理念基础源自于 libp2p spec,但是本项目并没有完全依据 spec 的实现,在介绍的最后,将列举出一些不兼容的地方,仅供参考。

理念

首先,我们对于框架的理解是,用户能够快速根据框架实现自己的上层业务,对用户来说,框架的使用难易程度决定了这个框架是否能够称得上是框架。简单的说,易用性是在功能实现的完全的前提下首要考虑的指标(如果用户为了使用框架不得不完全了解底层实现,那框架的存在将毫无意义)。

其次,我们不打算显式使用锁作为多线程数据共享的方案,我们认为使用 channel 作为多线程状态同步是一种更加优雅、清晰的解决方案,所以在代码实现中,我们会大量使用 channel。

再次,本库虽然底层使用 tokio 的异步逻辑作为性能的保证,但是对于用户来说,异步(或者说当前的异步)的思路会干扰业务层逻辑的实现,所以我们在最初就对外提供同步的接口,方便用户使用,要注意的是,虽然接口是同步的,但它异步调用的一部分,如果用户在其中写入 block 的代码段,会导致整个服务的卡顿,我们建议如果是确实有 io 任务,请使用异步的方式去做。

核心实现

作为一个能够挂载多协议的 P2P 框架,最重要、最核心的功能就是合理的将一个真实的连接(TCP/UDP/WebSocket等等)拆分成多个子连接,与此同时,将每个子连接分配给每个协议,并且尽可能地保证每个协议占用的时间片相对来说公平,即不能因为某个协议的消息过多导致其他协议的消息被卡主,并且保证消息的分发准确无误。这里看上去像一个路由器,实际上差不多,库中真正核心的功能就是这个。

库中为了实现多路复用,首先实现了一个多路复用协议——yamux,该协议规定了有效消息的类型、行为、格式等等内容,是用于实现网络协议多路复用的标准协议之一,也是 libp2p spec 中的一部分;接下来,我们在 yamux 协议之上,做一个抽象层,将 yamux 的多路复用绑定到自定义协议上,从而实现了多协议并存;之后,我们参照 libp2p,在 yamux 之下,真实连接之上,挂载了一个加密协议(secio),从而实现了消息的加密通信。也就是说,加密与不加密的本质区别就是,yamux 到底基于真实连接进行拆分子连接,还是基于 secio 进行拆分。

库实现介绍

yamux

作为库的核心依赖,yamux 的实现完全参照其标准,主要是为了兼容,让其他语言更容易实现。

作为 Rust 版本的实现,我们做了两个抽象,一个是 Session<T>,一个是 StreamHandle

  • Session:对应真实连接或者是加密连接,同时,它可以产出任意个子流,每个子流通过 channel 与 session 通信,session 负责发送数据到底层通道和转发数据到对应子流
  • StreamHandle:这个结构就是子流的抽象,它是一个实现了 WriteReadAsyncWriteAsyncRead 的结构,意味着在 Rust 中对它像文件一样可以进行读写操作

库中其他的部分就是对 yamux 协议的实现,比如 frame 是如何编码解码、config 是一些可以调整的配置项(是否要定时 Ping、动态窗口大小等等)。

整个库是用基于异步逻辑实现的,底层是 tokio 的异步框架。

secio

加密通信相对于 yamux 来说会稍显复杂一点,它是参照 libp2p spec 和 rust-libp2p 的实现而实现的一个加密通信协议。

首先,加密通信必然要有一个初始化过程,即握手过程,握手的目的是交换 nonce、双方公钥及支持的加密算法等关键信息,正常期待能够交换成功(握手成功),这时候,需要将商量得出的临时对称加密私钥保存好,同时对上层输出远端的公钥、加密流(类似于 yamux 库中的 StreamHandle),上层能够通过加密流传输数据,加密流自动加解密数据进行传输;

其次,加密流的实现,分成了两个部分,一个是 SecureStream,一个是 StreamHandle,它们的实现手法与 yamux 一致,都是一个对应真实流,一个给用户(上层)读写,与 yamux 实现中不同的是,这里两个结构只存在一对一的关系,它不能生成一对多的映射。

P2P

上面两个库属于 P2P 实现的基建部分,而 P2P 则是对上面两个库的进一步封装。目的有两个,一是抽象出对用户友好的接口,二是支持多协议的加载,在 yamux 层只有子流概念,并没有协议的概念,自定义协议的定义和使用都是在 P2P 层实现的。同时为了方便用户更好地使用库,P2P 对自定义协议的行为做了一些简单的约束(trait)。

每个协议都会有自己独特的 handle 需要实现,handle 能够感知到协议的开启、关闭、通信等行为。我们认为,handle 可以简单地分成两种类型:全局级 handle 和 连接级(session) handle,它们的区别如下:

  • 全局协议 handle:当第一个连接被打开时,该连接的协议打开的同时,会生成一个全局唯一的协议 handle,它的生命周期与 Service 相同,这意味着其内部可以存储各种想要的状态,比如有多少个节点被连接,每个节点的特征是什么等等;
  • 连接级(session)独占协议 handle:每个协议在打开时,会生成一个 session 级别的协议 handle,当协议被关闭或者 session 被断开时,该 handle 将被清理掉,这意味着,这个 handle 是无状态的 handle,不能存储对应 session 打开之前和关闭之后的状态,它只知道每个协议打开和关闭之间的所有信息,可以说十分轻量。

任何自定义协议都可以同时实现这两种 handle,或者实现其中的一种,P2P 保证每个 handle 的行为与描述完全一致。

而对于 Service 可能会产生的一些错误信息,我们又单独定义了一个 ServiceHandle,它将负责把错误信息交给用户去处理,毕竟,全局级的错误不能指定任何协议去处理它。

我们也在 P2P 层简单定义了一些防重复连接、身份匹配的机制,将一些简单的治理过程内置与框架中,对于用户来说,在上层去实现这些东西会很麻烦甚至难以完成。

整体数据流

经过上面的讲解,相信大家都对这个框架有了基本的了解,下面是一个数据发送过程的介绍:

  1. 数据从用户层发送到 Service 进行统一分流处理
  2. 数据经过分流后,进入 yamux 的子流中
  3. 子流将数据发送给 yamuxSession 结构
  4. Session 将接到的各种子流数据发送给 secio 的 handle
  5. handle 将数据发给 SecureStream,经加密后发送给远端
  6. 远端接收与发送相反,最后通过 Service 交给用户层

可以看出,实际上对于所有连接来说,数据流是以 聚合->分散->再聚合 的方式在工作。如果脑海中能够构造一个这样的场景,对于理解整个框架会有极大的帮助。

不兼容

握手

  1. 握手期间的 exchange 和 propose 目前使用 flatbuffers 进行序列化和反序列化,libp2p 使用 protobuf;
  2. 握手目前只支持 Secp256k1 算法的公钥交换;
  3. order 的确定,使用原始的 public key 与 nonce,libp2p 使用 protobuf bytes 的 public key;

多路复用协议

只支持 yamux ,并没有 mplex 的实现,这意味着,并没有 yamux 或者 mplex 的选择握手过程。

自定义协议选择过程

每个连接打开协议的过程也是一个握手过程,通信的格式为 flatbuffer,结构为:

table ProtocolInfo {
    name: string;
    support_versions: [string];
}

协商的发起方为主动拨通方(client),连接建立完成后随即进入协议开启的协商过程,监听方接到协商信息后,判断本方是否支持,支持则打开对应协议并开始通信,不支持则通知对方断开。