Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
109 lines (64 sloc) 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),连接建立完成后随即进入协议开启的协商过程,监听方接到协商信息后,判断本方是否支持,支持则打开对应协议并开始通信,不支持则通知对方断开。

You can’t perform that action at this time.