---
# System prepended metadata

title: 网络

---

# 网络
## 概述
P2P是一种分布式网络，网络的参与者共享他们所拥有的一部分硬件资源，比如处理能力、存储能力、网络连接能力、打印机等，这些共享的资源需要由网络提供服务和内容，能被其它对等节点直接访问而无需经过中间实体。在此网络中的参与者既是服务和内容提供者，又是服务和内容获取者。

区别于传统的Client/Server中央服务器结构，P2P网络中的每个节点的地位都是对等的。每个节点在充当客户端的同时，也可以作为服务端给其它节点提供服务，极大地提高了资源的利用率。


### 区块链网络
P2P 是区块链结构中的网络层，网络层的主要目的是实现区块链网络中节点之间的信息传播、验证和交流。区块链网络本质上是一个P2P网络，每个节点既能接受信息也能产生信息。节点之间通过维护一个共同的区块链数据来保持通信。

P2P网络作为区块链的基础，为区块链带来如下优点：

* 防止单点攻击
* 高容错性
* 较好的兼容性与可扩展性

### TRON网络
TRON架构图如下：
![](https://i.imgur.com/UQFYBCc.png)


P2P网络作为TRON的最底层模块，直接决定了整个区块链网络的稳定性。网络模块按照功能可以划分成以下四部分：

* [节点发现](#节点发现)
* [节点连接](#节点连接)
* [区块同步](#区块同步)
* [区块和交易广播](#区块和交易广播)

下面将分别介绍这四个功能部分。

## 节点发现
节点发现是区块链节点接入区块链网络的第一步。区块链网络是一种结构化的P2P网络。结构化网络会将所有节点按照某种结构有序的组织起来，比如形成一个环状网络或者树状的网络。 结构化网络普遍基于DHT(Distributed Hash Table，分布式哈希表) 算法实现。具体的实现方案有 Chord、Pastry、CAN、Kademlia 等算法。TRON 网络采用 Kademlia 算法。

### Kademlia 算法
Kademlia 是分布式散列表(DHT，Distributed Hash Table)的一种实现，是去中心化 P2P 网络中最核心的一种路由寻址技术，可以在无中心服务器的情况下，在网络中快速找到目标节点。



关于算法的详细介绍请参考 [Kademlia](https://zh.m.wikipedia.org/zh-hans/Kademlia)。

### TRON 实现
TRON 实现的 Kademlia 算法，要点如下：

* 节点ID：随机产生的512bit ID
* 节点距离：节点距离通过两个节点的ID异或运算得到，公式为：`节点距离 = 256 - 节点ID异或结果的前导0的个数`，如果计算结果为负数，距离等于0。
* k桶：即节点路由表。根据节点距离的远近，将远端节点划分到不同的桶中，与本节点距离相同的远端节点被记录在相同的桶中，每个桶最多容纳16个节点。根据节点距离的计算公式就可以看出，TRON 实现的 Kademlia 算法一共维护256个桶。

TRON 节点发现协议包括以下四种UDP消息：

* `DISCOVER_PING` - 用于探测⼀个节点是否在线
* `DISCOVER_PONG` - 用于响应 `DISCOVER_PING` 消息
* `DISCOVER_FIND_NODE` - 用于查找与⽬标节点距离最近的其它节点
* `DISCOVER_NEIGHBORS` - 用于响应 `DISCOVER_FIND_NODE` 消息，会返回一个或者多个节点，最多16个

#### 初始化K桶
节点启动后，会读取节点配置文件中配置的种子节点，以及数据库中记录的对等节点，然后向它们发送 `DISCOVER_PING` 消息，如果收到回复 `DISCOVER_PONG` ，在K桶没满的情况下，将相应节点写入K桶；如果相应桶已经达到了16个节点，则向桶中最早的节点发起挑战，挑战成功，将被挑战节点删除，将挑战节点加入K桶。K桶初始化完成后，进行节点发现流程。
![](https://i.imgur.com/Oyj4cJN.png)





#### 发送DISCOVER_FIND_NODE消息，发现更多节点

节点发现服务会开启两个节点发现定时任务（`DiscoverTask`和`RefreshTask`），周期执行节点发现过程来更新k桶。

* `DiscoverTask` 是发现更多距离自己近的节点，每30s执行一次，执行流程如下图所示:
    ![](https://i.imgur.com/rVV5tek.png)


* `RefreshTask` 是通过随机节点ID来扩充本地k桶，即发现距离随机节点ID更近的节点，每7.2s执行一次，执行流程如下图所示:
    ![](https://i.imgur.com/MhqaaH0.png)




`DiscoverTask`和`RefreshTask`中使用的节点发现算法，在一次调用中会执行8轮，每轮向K桶中距离目标节点 ID 最近的3个节点发送 `DISCOVER_FIND_NODE` 消息，并等待回复。


#### 接收到neighbors消息，更新K桶
当本节点接收到远端节点回复的 `DISCOVER_NEIGHBORS` 消息后，会依次向收到的邻居节点发送 `DISCOVER_PING` 消息，接下来如果收到了回复消息`DISCOVER_PONG`，则判断相应的K桶是否装满，如果K桶未满，则将新节点加入K桶，如果K桶满了，则向其中的某个节点发起挑战，若挑战成功(向被挑战的节点发送`DISCOVER_PING` 消息，如果未能收到其回复`DISCOVER_PONG`，则为挑战成功，否则挑战失败)，则将旧节点从K桶中删除，将新节点加入K桶。
![](https://i.imgur.com/rVfvCvq.png)





节点周期的执行节点发现任务，不断的更新K桶，构建自己的节点路由表。接下来就是节点间建立连接的过程了。

## 节点连接

在了解节点间如何建立TCP连接之前，您需要首先了解peer节点类型。
### peer节点管理
节点需要对peer节点进行管理分类，以进行高效稳定的节点连接。远端节点可以分为如下几类：
* Active nodes：通过配置文件指定，系统启动后，会主动与其建立连接的节点。如果连接建立失败，则在每个TCP连接定时任务中都会重新尝试与其建立连接。
* Passive nodes：通过配置文件指定，被动接受连接的节点。
* Trust nodes：通过配置文件指定，Active nodes和Passive nodes都是trust nodes。当收到trust node的连接请求时，会跳过其它的条件检查，直接接受请求。
* badNodes：当收到异常的协议报文时，会将节点加入到badNodes，生效时长1个小时，当收到 badNodes 的连接请求，会直接拒绝请求
* recentlyDisconnectedNodes：当断开某条连接后，会把节点加入到recentlyDisconnectedNodes，有效时长30s，当收到 recentlyDisconnectedNodes 的连接请求，会直接拒绝请求

### 建立节点间TCP连接
在节点启动后，会创建一个建立节点间TCP连接的定时任务`poolLoopExecutor`，用于选择节点，并与之建立连接。建立TCP连接定时任务，工作过程如下：
![](https://i.imgur.com/02dmehV.png)



TCP连接主要分为两步：首先，确定要与之建立连接的节点的列表；列表中需要包含active nodes中还没有成功建立连接的节点，然后计算还需要建立连接的数量，从邻居发现节点列表里面根据 [节点过滤策略](#) 过滤出满足要求的节点，再根据 [节点打分策略](#) 对节点打分排序，将最高的相应数量的节点加入到请求列表中。最后，与请求列表中的节点建立TCP连接。

#### 节点过滤策略
建立节点连接时，需过滤掉如下几类节点，并判断节点自己的连接数是否达到了最大连接。
* 自身节点
* recentlyDisconnectedNodes列表中的节点
* badNodes列表中的节点
* 已经建立了连接的节点
* 与该节点ip建立的连接数已经达到了上限值maxConnectionsWithSameIp的节点

但是对于可信节点，会忽略掉部分过滤策略，始终与其建立连接。
![](https://i.imgur.com/mfnJsUm.png)






#### 节点打分策略
节点分数用于确定建立连接的优先级，分数越高，优先级越高。打分维度包括：

* 丢包率：丢包率越低，说明数据通信质量越好。分数与丢包率成反比，最高得分为100，最低为0
* 网络延迟：网络延时越小，说明网络质量越好。分数与平均网络延时成反比，最高得分为20，最低为0
* TCP流量：TCP流量越大，说明通信比较活跃。分数与TCP流量成正比，最高得分为20，最低为0
* 断开连接次数：断开连接次数越少，说明节点越稳定。断开连接分数为负数，且与断开连接次数为正比，值为断开连接次数的10倍
* Handshake：曾经handshake成功过的节点，表示有相同的区块链信息，因此优先选择和他们建立连接。Handshake成功次数大于0时，Handshake得分为20，否则得分为0
* 处罚状态：处于处罚状态的节点，分数为0，不参与其它维度打分，包含以下几种情况：
    * 节点断开连接时间不到60s
    * 节点在badNodes列表中
    * 区块链信息不一致

计算节点分数时，首先判断节点是否为处罚状态，如果是，则分数计为0，否则，节点分数为各个维度的得分之和。


### 握手

TCP 连接建立成功后，主动发起TCP连接请求的节点，会向邻居节点发送握手消息 `P2P_HELLO`，目的为了确认节点间的链路信息是否一致，以及是否需要发起区块同步流程。

当邻居节点收到 `P2P_HELLO` 后，会与本地信息做比较，比如检查p2p version、创世块信息是否一致，若一致，还需要检查固化快，以及判断是否是重复的连接、恶意节点等。若所有的检查条件都通过，则会回复`P2P_HELLO`消息，然后进行区块同步或广播；否则，断开连接。


### 信道保活
信道保活是通过 `P2P_PING`、`P2P_PONG` TCP报文来完成的。当节点与邻居节点建立了TCP连接并握手成功后，节点会为连接开启一个线程 `pingTask` 定期发送 `P2P_PING` 消息以维护该TCP连接，每10s调度一次。如果在超时时间内未收到节点回复的 `P2P_PONG` 消息，则断开连接。

## 区块同步

在与对方节点完成握手后，如果发现对方节点的区块链比本地的区块链要长，根据最长链原则，会触发区块同步的处理流程`syncService.startSync`。同步过程中的报文交互如下图：

![](https://i.imgur.com/V2KBk8b.png)



节点A向对方节点B发送 `SYNC_BLOCK_CHAIN` 消息，以宣告本地链的摘要信息。对方节点B收到后，计算出节点A缺失的区块清单，并将缺失区块的id列表通过 `BLOCK_CHAIN_INVENTORY` 消息发送给节点A，一次最多携带2000个区块id。

节点A收到 `BLOCK_CHAIN_INVENTORY` 消息后，取出缺失区块id，并通过异步的方式向节点B发送 `FETCH_INV_DATA` 消息以请求缺少的区块，一次最多请求100个区块。如果还有需要同步的区块（即 `BLOCK_CHAIN_INVENTORY` 报文中的remain_num大于0），会触发新一轮的区块同步流程。

节点B收到节点A的 `FETCH_INV_DATA` 报文后，通过 `BLOCK` 消息将区块发送给节点A。节点A收到 `BLOCK` 报文后，异步处理该区块。

### 链摘要及缺失区块清单
下面根据几个不同的区块同步场景示例，说明链摘要和节点缺少的区块清单的生成。

* 链摘要：有序的区块blockID列表，包括：最高固化块、最高非固化块，以及之间二分法对应的区块
* 缺失区块清单：邻居节点根据链摘要与自己链进行比较，确定对方缺失的区块清单，返回一组连续的区块blockID以及剩余的块数
#### 普通场景

本地头块高度为1018，固化块高度为1000，两个节点刚建立连接，所以共同块高度为0，通过二分方式获取到节点A的本地链摘要为：1000、1010、1015、1017、1018。

节点B收到节点A的链摘要后，结合本地链可以生产节点A缺少的区块清单为：1018、1019、1020、1021。之后节点A根据缺失区块清单，请求同步区块1019、1020、1021。
![](https://i.imgur.com/GWcyD35.png)




#### 切链场景

本地主链头块高度为1018，固化块高度为1000，两个节点刚建立连接，所以共同块高度为0，通过二分方式获取到节点A的本地链摘要为：1000、1010、1015、1017、1018。

节点B收到节点A的链摘要后，发现本地主链跟节点A的主链不在同一条链上，对比节点A的链摘要，找到共同块高度为1015，则认为节点A缺少的区块清单为：1015、1016'、1017'、1018'，1019'。之后节点A根据缺失区块清单，请求同步区块1018'，1019'。
![](https://i.imgur.com/6sn3YRq.png)



另一种切链场景，本地主链头块高度为1018，固化块高度为1000，共同块为1017‘，位于fork链上，通过二分方式获取到节点A的本地链摘要为：1000、1009、1014、1016'、1017'。

节点B收到节点A的链摘要后，结合本地链可以生产节点A缺少的区块清单为：1017'、1018'、1019'。之后节点A根据缺失区块清单，请求同步区块1018'，1019'。
![](https://i.imgur.com/uOrSxE0.png)



## 区块和交易广播

当超级代表节点生产出新的区块，或者全节点接收到用户发起的新交易时，会发起交易&区块广播流程。当节点接收到新区块或者新交易时，会转发相应的区块或交易，转发与广播的流程一样。报文交互如下图所示：
![](https://i.imgur.com/LcLkfpZ.png)



其中涉及到的消息类型包括：

* INVENTORY - 广播清单：区块或者交易id列表
* FETCH_INV_DATA - 请求需要获取的清单数据：区块或者交易id列表
* BLOCK - 区块数据 
* TRXS - 交易数据

节点A将待广播交易或区块通过 `INVENTORY` 清单消息发送到节点B。节点B收到 `INVENTORY` 清单消息后，需要检查对方节点的状态，如果可以接收该消息，则将清单中的区块/交易放入待获取队列 `invToFetch` 中。如果是区块清单，还会立即触发"获取区块&交易任务"，来向节点A发送 `FETCH_INV_DATA` 消息获取区块&交易。

节点A收到 `FETCH_INV_DATA` 消息后，会检查是否发送过清单消息给对方，如果发送过，则根据清单数据，向节点B发送交易或者区块消息。节点B收到交易或者区块消息后，处理消息，并触发转发流程。

## 总结
本文介绍了TRON最底层模块-P2P网络相关的实现细节，包括节点发现、节点连接、区块同步、区块和交易广播流程，希望通过阅读本文能帮助开发者进一步了解和开发java-tron网络相关模块。