P2P是Peer-to-peer的简写,一个Peer也就是P2P网络中的一个节点,那整个网络就是一个个等价的节点构成的,而一个节点从启动到关闭,在各个阶段都需要具备一些基本的功能,这些功能的具备也是构建一个完善的P2P网络所必要的条件:
1、初始化
节点初始化的过程,最重要的是监听一个连接端口,供其他节点连接,可以是一个udp端口,也可以是一个tcp端口,例如在tomp2p中就支持使用同一个端口号同时监听udp和tcp连接。当然视具体的设计,可以启动多个端口监听,总之,需要一个其他节点连接本地节点的入口。
初次之外,初始化过程还包括其他动作,包括设置一个唯一的节点id,加载用于处理各类消息的Handler,加载必要的本地数据等等。
public void Peer createPeer(){
//随机初始化一个节点ID
Number160 peerId = new Number160(new Random());
PeerBuilder peerBuilder = new PeerBuilder(peerId);
//设置监听端口为4001,会同时启动udp和tcp监听
peerBuilder.ports(4001);
//这样,就初始化好了一个peer
Peer peer = peerBuilder.start();
return peer;
}
2、接入网络
一个节点初始化之后仍然是一个孤立的节点,需要加入到P2P网络中才能与其他节点通讯,通常的做法是连接到一个或多个已知的节点,再通过这个节点找到其他节点,再通过新找到的节点,以此类推,就能联系到足够多的节点,从而成为网络中的一员,这就是“节点发现”的过程,在TomP2P中称之为引导(bootstrap)。至于一个节点最多能与多少其他节点取得联系,这个由具体的实现决定;极端情况下,所有节点都能与其他所有节点相互通讯,但这是不安全的做法,一般来说都会对通讯节点数做限制。
通常的,一个P2P网络中会有少数几个稳定的超级节点用于帮助普通节点之间相互发现,也就是上面说到的“已知的节点”,类似种子下载过程中的tracker服务器。
public void bootstrapTo(Peer peer, InetSocketAddress remoteAddress){
FutureBootstrap bootstrapFuture = peer.bootstrap()
.inetSocketAddress(remoteAddress) //remoteAddress表示一个已知的节点网络地址
.start();
//等待bootstrap结束
bootstrapFuture.awaitUninterruptibly();
if(futureBootstrap.isSuccess()){
LOGGER.info("bootstrap successfully to remote address: {}", remoteAddress);
}else {
LOGGER.warn("bootstrap failed to remote address: {}.Cause: {}", address, future.failedReason());
}
}
3、维持连接
每个节点自己都维护了一个可用节点的列表,当需要时,需要向这些节点发送请求获取数据。但由于节点分布在互联网的各个角落,而互联网中的网络环境十分复杂,而且变化多端,很有可能两个节点之间上一秒还能正常通讯,下一秒就断开连接了,什么时候才能恢复也不知道。这时候需要及时将失联的节点从可用节点列表中剔除,或者设置为不可用,以保证后续的请求将不再发往这些失联节点。但怎样才能及时知道这些节点已经断连了呢?
这就要用到心跳机制(HeartBeat)了,作为节点本身,一方面,需要定时的像可用节点发送心跳消息,我们通常称之为Ping;另一方面,当发现超过一定时间没有收到其他节点的心跳消息时,则认为这个节点已经离线,那后续将不往该节点发送任何请求,直到该节点重新连上。
以下是MaintenanceTask的部分代码,在一个Peer初始化之后,会启动这样一个定时任务,定时向所有可连接节点发送Ping消息:
public class MaintenanceTask implements Runnable {
...
public void init(Peer peer, ScheduledExecutorService timer) {
this.peer = peer;
scheduledFuture = timer.scheduleAtFixedRate(this, intervalMillis, intervalMillis, TimeUnit.MILLISECONDS);
}
@Override
public void run() {
synchronized (lock) {
//make sure we only have 5 pings in parallel
if (shutdown || COUNTER.get() > MAX_PING) {
return;
}
for (Maintainable maintainable : maintainables) {
PeerStatistic peerStatatistic = maintainable.nextForMaintenance(runningFutures.values());
if(peerStatatistic == null) {
continue;
}
BaseFuture future;
if(peerStatatistic.isLocal()) {
future = peer.localAnnounce().ping().peerAddress(peerStatatistic.peerAddress()).start();
LOG.debug("Maintenance local ping from {} to {}.", peer.peerAddress(), peerStatatistic.peerAddress());
} else {
future = peer.ping().peerAddress(peerStatatistic.peerAddress()).start();
LOG.debug("Maintenance ping from {} to {}.", peer.peerAddress(), peerStatatistic.peerAddress());
}
peer.notifyAutomaticFutures(future);
runningFutures.put(future, peerStatatistic.peerAddress());
COUNTER.incrementAndGet();
future.addListener(new BaseFutureAdapter() {
@Override
public void operationComplete(BaseFuture future) throws Exception {
synchronized (lock) {
runningFutures.remove(future);
COUNTER.decrementAndGet();
}
}
});
}
}
}
...
}
4、响应请求
一个P2P网络不是无端存在,它一定有它所依托的使用场景,就像tomcat一样,我们不会去部署一个没有任何servlet的tomcat容器。这里以tomcat和servlet类比,主要想说明,P2P网络里的节点需要能够对其他节点发来的请求作出相应,例如在合规的情况下,其他节点如果想要获取本地节点的一些信息,那本地节点需要正确的返回信息,在TomP2P中称这个过程为RPC(远程过程调用),事实上,RPC是比Servlet还要标准化的一个词。
TomP2P中的RPC框架原理是,将一个Handler与特定的Command绑定,一个Command表示一类请求类型,当本节点接收到一个请求标志为某个Command类型,这时候会选取对应的Handler进行处理,具体的实现会在后面文章中详细介绍。以下是TomP2P默认启用的RPC:
public class Peer {
...
// RPC
private PingRPC pingRCP;
private QuitRPC quitRPC;
private NeighborRPC neighborRPC;
private DirectDataRPC directDataRPC;
private BroadcastRPC broadcastRPC;
private AnnounceRPC announceRPC;
...
}
5、退出
除了异常退出之外,节点在将要关闭时,更应该通过一个正常的方式主动告知其他节点,本节点即将退出网络,后续将不再响应请求,直到重新启动。
TomP2P中提供了QuitRPC用以向其他节点发送离线通知,最佳的使用方式是在Java的ShutdownHook中将离线通知发送出去。
public void closePeer(Peer peer) {
// 获取所有可连接的节点地址
Set peerAddresses = Sets.newHashSet();
peerMap.peerMapVerified().forEach(map -> {
map.forEach((number160, peerStatistic) -> {
peerAddresses.add(peerStatistic.peerAddress());
});
});
// 一一发送quit消息
if (CollectionUtils.isNotEmpty(peerAddresses)) {
peerAddresses.forEach(remote -> {
ShutdownBuilder shutdownBuilder = new ShutdownBuilder(peer);
FutureChannelCreator fcc = peer.connectionBean().reservation().create(1, 0);
fcc.awaitUninterruptibly();
if (fcc.isSuccess()) {
peer.quitRPC().quit(remote, shutdownBuilder, fcc.channelCreator());
}
});
}
// 释放端口等本地资源
peer.shutdown();
}
总结
本次介绍了P2P中节点在各个生命周期阶段中需要去做的动作,并且以TomP2P的相关代码作为示例,继而展示了TomP2P基本的工作方式。事实上,在TomP2P中关于的Peer的一整套机制还有不少内容可以介绍,包括节点统计、数据安全、节点中继(RelayRPC)等等,会在后面的系列文章中慢慢展开,尽情期待。