IPFS基于分布式哈希表,实现了3套接口,PeerRouting、ContentRouting、ValueStore。并基于这些接口实现了节点路由、内容寻址、发布订阅、AutoRelay、IPNS等功能。
分布式哈希表(Distributed Hash Table)简称DHT,主要用于分布式网络的路由寻址,目前有两种主流实现方式Chord和Kademlia。IPFS中的分布式哈希表使用Kademlia协议,简称kad。关于DHT和Kad的基本原理,资料很多,这里不再赘述。本文主要讲解HDT在IPFS中的实现和使用。
IPFS中的节点ID和分布式哈希表中的ID是不一样的,节点ID叫peer.ID,分布式哈希表中的ID叫 kb.ID(K桶ID)。IPFS节点初始化时,会随机成一个私钥,然后对其对应的公钥进行 SHA2_256 哈希,作为peer.ID。
// IDFromPublicKey returns the Peer ID corresponding to the public key pk.
func IDFromPublicKey(pk ic.PubKey) (ID, error) {
b, err := pk.Bytes()
if err != nil {
return "", err
}
var alg uint64 = mh.SHA2_256
if AdvancedEnableInlining && len(b) <= maxInlineKeyLength {
alg = mh.IDENTITY
}
hash, _ := mh.Sum(b, alg, -1)
return ID(hash), nil
}
再对peer.ID 计算 SHA256 就得到 kb.ID
// ConvertPeerID creates a DHT ID by hashing a Peer ID (Multihash)
func ConvertPeerID(id peer.ID) ID {
hash := sha256.Sum256([]byte(id))
return hash[:]
}
因为 kb.ID 是256 bit,所以IPFS 网络最多可以拥有2^256个节点。
kb.ID的异或运算结果就是任意两个节点之间的距离,这里的距离指的是逻辑上的,并非空间上的距离。两个节点kb.ID的公共前缀越多,异或值越小,也就是距离越近。
因为kb.ID是256 bit,所以IPFS的K桶拥有256个桶,每个桶默认最多存放20个活跃节点的信息。第i个桶中存放的是与本节点kb.ID转化成二进制之后公共前缀数量为i的其他节点信息。IPFS节点启动时,通过配置好的引导节点连接到网络,并将发现的活跃节点存到K桶中。随着时间推移,k桶中的节点也会不断变化。这里面有一套更新机制,这里不再赘述。
IPFS分布式哈希表提供3种类型的路由服务接口,即PeerRouting、ContentRouting、ValueStore。IPFS上层的很多功能都是基于此实现的。
// Routing is the combination of different routing types supported by libp2p.
// It can be satisfied by a single item (such as a DHT) or multiple different
// pieces that are more optimized to each task.
type Routing interface {
ContentRouting
PeerRouting
ValueStore
}
通过peer.ID获取节点地址,这是作为一个P2P网络最基本的功能。
// PeerRouting is a way to find address information about certain peers.
// This can be implemented by a simple lookup table, a tracking server,
// or even a DHT.
type PeerRouting interface {
// FindPeer searches for a peer with given ID, returns a peer.AddrInfo
// with relevant addresses.
FindPeer(context.Context, peer.ID) (peer.AddrInfo, error)
}
如果当前节点的K桶中存在目标节点,则返回。若不存在,则从当前节点的K桶中找到离目标节点最近的若干个节点,通过网络请求从这些节点继续找,一直递归,直到没有找到更近的节点为止。为什么可以这样找,因为每个桶的最大容量是固定的20个,但是管理的距离半径却是2倍的关系,离目标节点越近的节点的K桶,拥有更多离目标节点近的节点。查询过程中,每一跳都更接近目标节点,最终收敛于目标节点。
内容寻址包含两部分,发布内容(Provide)和查找内容所有者的节点地址(FindProviders)。
// ContentRouting is a value provider layer of indirection. It is used to find
// information about who has what content.
//
// Content is identified by CID (content identifier), which encodes a hash
// of the identified content in a future-proof manner.
type ContentRouting interface {
// Provide adds the given cid to the content routing system. If 'true' is
// passed, it also announces it, otherwise it is just kept in the local
// accounting of which objects are being provided.
Provide(context.Context, cid.Cid, bool) error
// Search for peers who are able to provide a given key
//
// When count is 0, this method will return an unbounded number of
// results.
FindProvidersAsync(context.Context, cid.Cid, int) <-chan peer.AddrInfo
}
当一个节点拥有某个数据时,它将数据的cid和当前节点地址,使用Provide方法,按照以下步骤发布到网络。
从上面可以看到,只有部分节点知道某个节点拥有某份数据,而且拥有数据的节点需要定时调用Provide告诉其他节点它持续拥有数据。
网络中任何一个节点只要知道cid,可以通过调用FindProvidersAsync方法,通过以下步骤找到持有数据的节点地址
IPFS中使用ContentRouting的功能除了上面说的持有文件数据的发布和查找外,还有几个功能也使用到了。比如发布订阅功能和AutoRelay。
发布订阅是IPFS提供的一个很有用的功能。网络中任何一个节点只要订阅了某个主题(topic),其他节点往这个主题发布消息,订阅的节点就能收到。这个功能极大的简化了开发去中心化即时通信软件的难度。事实上,已经有人这么干了,有个开源项目berty,就是基于此原理实现的。那么IPFS是怎么做到的呢。其实前面讲的内容cid不仅可以是数据的cid,也可以是某个topic的字符串hash。跟前面一样的原理,某个节点订阅消息时,会将topic通过SHA256转化的分布式哈希表中的kb.ID,发送这个kb.ID和本节点地址给距离这个kb.ID最近的若干个节点保存。其他节点发布消息时,就可以根据topic找到订阅的节点,然后直接发送过去。当然IPFS实际实现这个复杂得多,假设同一个topic订阅的节点非常多,这种方式需要每个节点都建立连接,是不太明智的。所以IPFS实现了3中发布消息的路由方式,分别是RandomSubRouter、GossipSubRouter、FloodSubRouter,这里面的细节太多,这里就不展开了。
由于IPFS网络中并不是所有的节点都有公网IP,而IPFS目前还没实现p2p打洞功能,不在同一个网络的节点之间不能直接通信。所以可以将部分拥有公网IP的节点作为中继使用,不能直连的节点间可以通过中继节点转发数据。要开启这个功能需要中继节点和普通节点在启动前做相应的配置。普通节点可以指定固定中继节点作为中继,也可以通过AutoRelay功能。AutoRelay就是让普通节点具备自动发现全网中已存在的中继节点的能力。AutoRelay功能是如果实现的呢?
IPFS中所有的中继节在分布式哈希表中都有一个共同的key,叫RelayRendezvous,同样需要使用SHA256映射到分布式哈希表的kb.ID 。定义如下
const RelayRendezvous = "/libp2p/relay"
与前面的发布订阅功能类似,中继节点启动后,会自动发布自己的地址给距离这个key最近的若干个节点保存。普通节点可以通过这个key从分布式哈希表中拿到全网活跃的中继节点地址。
分布式哈希表还可以作为分布式的ValueStore存储使用。ValueStore实现了两个方法,PutValue和GetValue。
// ValueStore is a basic Put/Get interface.
type ValueStore interface {
// PutValue adds value corresponding to given Key.
PutValue(context.Context, string, []byte, ...Option) error
// GetValue searches for the value corresponding to given Key.
GetValue(context.Context, string, ...Option) ([]byte, error)
}
基于这个接口,IPFS内部实现了一个名字解析系统IPNS (InterPlanetary Name System)。
IPNS包含两个功能,发布(publish)和解析(resolve)。发布指的是将一个IPFS资源路径,绑定一个唯一的key发布到全网,其他节点可以通过key获取资源的路径。其他节点拿到资源路径后就可以通过使用前面讲的ContentRouting找到持有资源的节点,然后通过直连方式或者中继节点下载数据。
IPNS发布流程:
// RecordKey returns the libp2p record key for a given peer ID.
func RecordKey(pid peer.ID) string {
return "/ipns/" + string(pid)
}
IPNS解析就使用GetValue方法通过key取IpnsEntry。从上面可以看到,一个私钥只能生成一个唯一的RecordKey。所以当一个节点需要发布多个资源时,不应该使用节点的私钥,而是要为每个资源指定不同的私钥。
IPFS的DHT内部维护了两个DHT实例,一个公网的WAN,一个内网的LAN。寻找节点时,优先从公网中找,更新时两个网络都更新。
// DHT implements the routing interface to provide two concrete DHT implementationts for use
// in IPFS that are used to support both global network users and disjoint LAN usecases.
type DHT struct {
WAN *dht.IpfsDHT
LAN *dht.IpfsDHT
}
IPFS基于分布式哈希表,实现了3套接口,PeerRouting、ContentRouting、ValueStore。并基于这些接口实现了节点路由、内容寻址、发布订阅、AutoRelay、IPNS等功能。同时针对公网和内网维护了两个表实例。我们也看到了IPFS目前功能上的不足,比如P2P打洞目前还没有实现,希望后续版本能够更完善。
公众号:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!