1、P2P原理及协议概述
P2P 主要存在四种不同的网络模型,也代表着 P2P 技术的四个发展阶段:集中式、纯分布式、混合式和结构化模型。不过需要指出的是,这里所说的网络模型主要是指路由查询结构,即不同节点之间如何建立连接通道,两个节点之间一旦建立连接,具体传输什么数据则是两个节点之间的事情了。
最简单的路由方式就是集中式,即存在一个中心节点保存了其他所有节点的索引信息,索引信息一般包括节点 IP 地址、端口、节点资源等。集中式路由的优点就是结构简单、实现容易。但缺点也很明显,由于中心节点需要存储所有节点的路由信息,当节点规模扩展时,就很容易出现性能瓶颈;而且也存在单点故障问题。
那第二种路由结构则是纯分布式的,移除了中心节点,在 P2P 节点之间建立随机网络,就是在一个新加入节点和 P2P 网络中的某个节点间随机建立连接通道,从而形成一个随机拓扑结构。新节点加入该网络的实现方法也有很多种,最简单的就是随机选择一个已经存在的节点并建立邻居关系。像比特币的话,则是使用 DNS 的方式来查询其他节点,DNS 一般是硬编码到代码里的,这些 DNS 服务器就会提供比特币节点的 IP 地址列表,从而新节点就可以找到其他节点建立连接通道。新节点与邻居节点建立连接后,还需要进行全网广播,让整个网络知道该节点的存在。全网广播的方式就是,该节点首先向邻居节点广播,邻居节点收到广播消息后,再继续向自己的邻居节点广播,以此类推,从而广播到整个网络。这种广播方法也称为泛洪机制。纯分布式结构不存在集中式结构的单点性能瓶颈问题和单点故障问题,具有较好的可扩展性,但泛洪机制引入了新的问题,主要是可控性差的问题,包括两个较大的问题,一是容易形成泛洪循环,比如节点 A 发出的消息经过节点 B 到 节点 C,节点 C 再广播到节点 A,这就形成了一个循环;另一个棘手问题则是响应消息风暴问题,如果节点 A 想请求的资源被很多节点所拥有,那么在很短时间内,会出现大量节点同时向节点 A 发送响应消息,这就可能会让节点 A 瞬间瘫痪。
再来看看第三种路由结构:混合式。混合式其实就是混合了集中式和分布式结构,如下图所示,网络中存在多个超级节点组成分布式网络,而每个超级节点则有多个普通节点与它组成局部的集中式网络。一个新的普通节点加入,则先选择一个超级节点进行通信,该超级节点再推送其他超级节点列表给新加入节点,加入节点再根据列表中的超级节点状态决定选择哪个具体的超级节点作为父节点。这种结构的泛洪广播就只是发生在超级节点之间,就可以避免大规模泛洪存在的问题。在实际应用中,混合式结构是相对灵活并且比较有效的组网架构,实现难度也相对较小,因此目前较多系统基于混合式结构进行开发实现。其实,比特币网络如今也是这种结构,后面再细说。
image.png最后一种网络则是结构化 P2P 网络,它也是一种分布式网络结构,但与纯分布式结构不同。纯分布式网络就是一个随机网络,而结构化网络则将所有节点按照某种结构进行有序组织,比如形成一个环状网络或树状网络。而结构化网络的具体实现上,普遍都是基于 **DHT(Distributed Hash Table,分布式哈希表) **算法思想。DHT 只是提出一种网络模型,并不涉及具体实现,主要想解决如何在分布式环境下快速而又准确地路由、定位数据的问题。具体的实现方案有 Chord、Pastry、CAN、Kademlia 等算法,其中 Kademlia 也是以太坊网络的实现算法,很多常用的 P2P 应用如 BitTorrent、电驴等也是使用 Kademlia。因为篇幅有限,就不展开讲这些算法的具体原理了。目前,我们主要理解 DHT 的核心思想即可。
在 P2P 网络中,可以抽象出两种空间:资源空间和节点空间。资源空间就是所有节点保存的资源集合,节点空间就是所有节点的集合。对所有资源和节点分别进行编号,如把资源名称或内容用 Hash 函数变成一个数值(这也是 DHT 常用的一种方法),这样,每个资源就有对应的一个 ID,每个节点也有一个 ID,资源 ID 和节点 ID 之间建立起一种映射关系,比如,将资源 n 的所有索引信息存放到节点 n 上,那要搜索资源 n 时,只要找到节点 n 即可,从而就可以避免泛洪广播,能更快速而又准确地路由和定位数据。当然,在实际应用中,资源 ID 和节点 ID 之间是无法做到一一对应的,但因为 ID 都是数字,就存在大小关系或偏序关系等,基于这些关系就能建立两者的映射关系。这就是 DHT 的核心思想。DHT 算法在资源编号和节点编号上就是使用了分布式哈希表,使得资源空间和节点空间的编号有唯一性、均匀分布式等较好的性质,能够适合结构化分布式网络的要求。
综上,这就是 P2P 网络的一点理论基础,不同的区块链可能会使用不一样的网络模型,但基本原理是一样的。后面分别讲解下最有代表性的两个区块链的网络:比特币网络和以太坊网络。
2、比特币和以太坊中的P2P网络整体对比分析
2.1 通信协议层面
- 比特币的P2P网络完全基于TCP构建,主网默认通信端口是8333。
- 以太坊的P2P网络是一个完全加密的网络,提供UDP和TCP两种连接方式,主网默认TCP端口30303,推荐UDP发现端口为30301.
2.2 初始节点发现
比特币网络中,初始节点发现有两种方式:
- DNS-seed,又称为DNS种子节点,比特币的社区会维护一些域名,通过nslookup该域名可解析出数十个A记录的主机IP。例如:
nc -nvv 149.202.179.35 8333
found 0 associations
found 1 connections:
1: flags=82<CONNECTED,PREFERRED>
outif en0
src 192.168.1.104 port 62125
dst 149.202.179.35 port 8333
rank info not available
TCP aux info available
Connection to 149.202.179.35 port 8333 [tcp/*] succeeded!
- 硬编码一些seed-node,当所有的种子节点全失效时,全节点会尝试连接这些种子节点。
在以太坊中,思路也大致相同,也是在代码中硬编码(hard-code)了一些种子节点做类似的工作。
2.3 启动后节点发现
在 Bitcoin 的网络中,一个节点可以将自己维护的对等节点列表 (peer list) 发送给临近节点,所以在初始节点发现之后,你的节点要做的第一件事情就是向对方要列表:“快把你的节点列表给我复制一份。”
所以在每次需要发送协议消息的时候,它会花费固定的时间尝试和已存的节点列表中的节点建立链接,如果有任何一个节点在超时之前可以连接上,就不用去 DNS seed 获取地址,一般来说,这种可能性很小,尤其是全节点数目非常多的情况下。
而在以太坊网络中,也会维护类似的一个节点列表 (NodeTable),但是这个节点列表与比特币的简单维护不同,它采用了 P2P 网络协议中一个成熟的算法,叫做 Kademlia 网络,简称 KAD 网络。
它使用了 DHT 来定位资源,全称 Distributed Hash Table,中文名为分布式哈希表。KAD 网络会维护一个路由表,用于快速定位目标节点。由于 KAD 网络基于 UDP 通信协议,所以以太坊节点的节点发现是基于 UDP 的,如果找到节点以后,数据交互又会切换到 TCP 协议上。
2.4 NAT穿透
局域网中的主机IP与公网IP建立P2P连接的解决方案是做NAT穿透。
比特币和以太坊均使用了 UPnP (Universal Plug and Play)协议作为局域网穿透工具,只要局域网中的路由设备支持 NAT 网关功能、支持 UPnP 协议,即可将你的区块链节点自动映射到公网上。
UPnP 是通用即插即用(Universal Plug and Play)的缩写,它主要用于设备的智能互联互通,所有在网络上的设备马上就能知道有新设备加入。
2.5 节点交互协议
一旦节点建立连接以后,节点之间的交互是遵循一些特定的命令,这些命令写在消息的头部,消息体写的则是消息内容。
命令分为两种,一种是请求命令,一种是数据交互命令。
节点连接完成要做的第一件事情叫做握手操作。这一点在比特币和以太坊上的流程是差不多的,就是相互问候一下,提供一些简要信息。
比如先交换一下版本号,看看是否兼容。只是以太坊为握手过程提供了对称加密,而比特币没有。
握手完毕之后,无论交互什么信息,都是需要保持长连接的,在比特币上有 PING/PONG 这两种类型的消息,这很明显就是用于保持节点之间长连接的心跳而设计的;而在以太坊的设计中,将 PING/PONG 协议移到了节点发现的过程中。
请求命令一般分为发起者请求,比如比特币中的 getaddr 命令是为了获取对方的可用节点列表,inv 命令则提供了数据传输,消息体中会包含一个数据向量。
我们说区块链最重要的功能就是同步区块链,而同步区块恰巧是最考验 P2P 网络能力的。区块同步方式分为两种,第一种叫做 HeaderFirst,它提供了区块头先同步,同步完成以后再从其他节点获得区块体。
第二种叫做 BlockFirst,这种区块同步的方式比较简单粗暴,就是从其他节点获取区块必须是完整的。第一种方案提供了较好的交互过程,减轻了网络负担。这两种同步方式会直接体现在节点交互协议上,他们使用的命令逻辑完全不同。
一般 P2P 网络技术要解决两个主要问题,第一是资源定位,第二是资源获取,这一篇文章也是主要围绕这两点展开,其中节点发现和局域网穿透是属于资源定位问题,节点交互协议是属于资源获取问题。
3、比特币中的P2P网络详解
比特币网络
首先,比特币网络中的节点主要有四大功能:钱包、挖矿、区块链数据库、网络路由。每个节点都会具备路由功能,但其他功能不一定都具备,不同类型的节点可能只包含部分功能,一般只有比特币核心(bitcoin core)节点才会包含所有四大功能。
所有节点都会参与校验和广播交易及区块信息,且会发现和维持与其他节点的连接。有些节点会包含完整的区块链数据库,包括所有交易数据,这种节点也称为全节点(Full Node)。另外一些节点只存储了区块链数据库的一部分,一般只存储区块头而不存储交易数据,它们会通过“简化交易验证(SPV)”的方式完成交易校验,这样的节点也称为 SPV节点或轻节点(Lightweight Node)。钱包一般是 PC 或手机客户端的功能,用户通过钱包查看自己的账户金额、管理钱包地址和私钥、发起交易等。除了比特币核心钱包是全节点之外,大部分钱包都是轻节点。挖矿节点则通过解决工作量证明(PoW)算法问题,与其他挖矿节点相互竞争创建新区块。有些挖矿节点同时也是全节点,即也存储了完整的区块链数据库,这种节点一般都是独立矿工(Solo Miner)。还有一些挖矿节点不是独立挖矿的,而是和其他节点一起连接到矿池,参与集体挖矿,这种节点一般也称为矿池矿工(Pool Miner)。这会形成一个局部的集中式矿池网络,中心节点是一个矿池服务器,其他挖矿节点全部连接到矿池服务器。矿池矿工和矿池服务器之间的通信也不是采用标准的比特币协议,而是使用矿池挖矿协议,而矿池服务器作为一个全节点再与其他比特币节点使用主网络的比特币协议进行通信。
在整个比特币网络中,除了不同节点间使用比特币协议作为通信协议的主网络,也存在很多扩展网络,包括上面提到的矿池网络。不同的矿池网络可能还会使用不同的矿池挖矿协议,目前主流的具体矿池协议应该是 Stratum协议,该协议除了支持挖矿节点,也支持瘦客户端钱包。一个包含了比特币协议主网络各种节点和 Stratum 网络,以及其他矿池网络的扩展比特币网络大概如下图所示:
image.png另外,挖矿这块还有特殊需求。我们知道,矿工创建新区块后,是需要广播给全网所有节点的,当全网都接受了该区块,给矿工的挖矿奖励才算是有效的,这之后才好开始下一个区块 Hash 的计算。所以矿工必须最大限度缩短新区块的广播和下一个区块 Hash 计算之间的时间。如果矿工之间传播区块只采用上图所示的比特币协议网络,那无疑会有很高的网络延迟,所以,需要一个专门的传播网络用来加快新区块在矿工之间的同步传播,这个专门网络也叫比特币传播网络或比特币中继网络(Bitcoin Relay Network)。
4、以太坊中的P2P网络详解
以太坊网络
和比特币一样,以太坊的节点也具备钱包、挖矿、区块链数据库、网络路由四大功能,也同样存在很多不同类型的节点,除了主网络之外也同样存在很多扩展网络。但与比特币不同的,比特币主网的 P2P 网络是无结构的,但以太坊的 P2P 网络是有结构的。前面我们已经提过,以太坊的 P2P 网络主要采用了 Kademlia(简称 Kad) 算法实现,Kad 是一种分布式哈希表(DHT)技术,使用该技术,可以实现在分布式环境下快速而又准确地路由、定位数据的问题。所以,下面主要讲解下以太坊的 Kad 网络。
在 Kad 网络中,每个节点都具有一个唯一的节点 ID。另外,也会计算不同节点之间的距离,但这个距离不是物理上的距离,而是逻辑上的距离,是通过对两个节点 ID 进行 异或(符号为^) 计算得到的,即 A、B 两节点之间的距离的计算公式为:D(A,B) = A.ID^B.ID。异或有一个重要的性质:假设 a、b、c 为任意三个数,如果 a^b = a^c 成立,那就一定 b = c。因此,如果给定一个结点 a 和距离 L,那就有且仅有一个结点 b, 会使得 D(a,b) = L。通过这种方式,就能有效度量 Kad 网络中不同节点之间的逻辑距离。
在异或距离度量的基础上,Kad 还可以将整个网络拓扑组织成如下图所示的一个二叉前缀树,每个 NodeID 会映射到二叉树上的某个叶子。
image.png映射规则主要是:
- 将 NodeID 以二进制形式表示,然后从高到低对每一位的 0 或 1 依次处理;
- 二进制的第 n 位就对应了二叉树的第 n 层;
- 如果该位是 0,进入左子树,是 1 则进入右子树(反过来也可以);
- 全部位都处理完后,这个 NodeID 就对应了二叉树上的某个叶子。
在这种二叉树结构下,对每个节点来说,离它越近的节点异或距离也是越近的。接着,可以按照离自己异或距离的远近,对整颗二叉树进行拆分。拆分规则是:从根节点开始,将不包括自己的那颗子树拆分出来,然后在包含自己的子树中,把不包括自己的下一层子树再拆分出来,以此类推,直到只剩下自己。以上图的 110 节点为例,从根节点开始,由于 110 节点在右子树,所以将左边的整颗子树拆分出来,即包含 000、001、010 这三个节点的这颗子树;接着,到第二层子树,将不包含 110 节点的左子树再拆分出来,即包含 100 和 101 这两个节点的子树;最后,再将 111 拆分出来。这样,就将 110 节点之外的整个二叉树拆分出了三颗子树。
完成子树拆分后,只要知道每个子树里面的其中一个节点,就可以进行递归路由实现整颗二叉树所有节点的遍历。但在实际场景下,由于节点是动态变化的,所以一般不会只知道每个子树的一个节点,而是需要知道多个节点。因此,Kad 中有一个叫 K-桶(K-bucket)的概念,每个桶会记录每颗子树里所知道的多个节点。其实,一个K-桶就是一张路由表,如果拆分出来有 m 颗子树,那对应节点就需要维护 m 个路由表。每个节点都会各自维护自己的 m 个 K-桶,每个 K-桶里记录的节点信息一般会包括 NodeID、IP、Endpoint、与 Target 节点(即维护该 K-桶的节点)的异或距离等信息。以太坊中,每个节点维护的 K-桶数量为 256 个,这 256 个 K-桶会根据与 Target 节点的异或距离进行排序,每个 K-桶保存的节点数量上限是 16。
在以太坊的 Kad 网络中,节点之间的通信是基于 UDP 的,另外设置了 4 个主要的通信协议:
- Ping:用于探测一个节点是否在线
- Pong:用于响应 Ping 命令
- FindNode:用于查找与 Target 节点异或距离最近的其他节点
- Neighbours:用于响应 FindNode 命令,会返回一或多个节点
通过以上 4 个命令,就可以实现新节点的加入、K-桶的刷新等机制。具体的实现流程就不细讲了,留给大伙自己去思考。
总结
不同结构的 P2P 网络,会有不同的优点和缺点。比特币网络的结构明显容易理解,实现起来也相对容易得多,而以太坊网络引入了异或距离、二叉前缀树、K-桶等,结构上复杂不少,但在节点路由上的确会比比特币快很多。另外,不管是比特币还是以太坊,其实都只是一种或多种协议的集合,不同节点其实可以用不同的具体实现,比如,比特币就有用 C++ 实现的 Bitcoin Core,还有用 Java 实现的 BitcoinJ;以太坊也有用 Go 语言实现的 go-ethereum,也有用 C++ 实现的 go-ethereum,还有用 Java 实现的 Ethereum(J)。
思考和实践
在以太坊的 Kad 网络中,新节点的加入和 K-桶的刷新流程是怎样的?比特币的新节点加入流程又是怎样的?哈希表有哪些实现方式?
网友评论