在介绍Btcd的Peer和ConnMgr时,我们提到节点会维护一个记录网络节点地址的地址仓库。节点与Peer交换getaddr和addr消息来同步各自已知的节点地址,一段时间后,节点将获知大量的节点地址,它需要用一个“仓库”来记录这些地址,并且在节点需要与新的节点建立Peer关系时能够随机选择可用的地址以供连接。AddrManager完成了这些功能,本文将分析它的代码来提示上述功能是如何实现的。
btcd/addrmgr包含的文件有:
- addrmanager.go: 实现Peer地址的存取以及随机选择策略,是AddrManager的主要模块,它将地址集合以特定的形式存于peers.json文件中;
- knownaddress.go: 定义了KnownAddress类型,即地址仓库中每条地址记录的格式;
- network.go: 定义不同IP地址类型,并提供类型判断方法;
- log.go: 提供logger初始化和设置方法;
- doc.go: 包btcd/addrmanager的doc文档;
- cov_report.sh: 调用gocov生成测试覆盖报告的脚本;
- addrmanager_test.go、internal_test.go、knownaddress_test.go、network_test.go: 定义对应的测试方法;
AddrManager主要将节点通过addr消息获知的地址存入本地的peers.json文件,为了便于理解后面代码,我们先来看看peers.json的格式:
//peers.json
{
"Version": 1,
"Key": [233,19,87,131,183,155,......,231,78,82,150,10,102],
"Addresses": [
{
"Addr": "109.157.120.169:8333",
"Src": "104.172.5.90:8333",
"Attempts": 0,
"TimeStamp": 1514967959,
"LastAttempt": -62135596800,
"LastSuccess": -62135596800
},
......
],
"NewBuckets": [
[
"[2001:0:9d38:78cf:3cb1:bb2:ab6f:e8b4]:8333",
"196.209.239.229:8333",
......
"65.130.177.198:8333"
],
......
[
"125.227.159.115:8333",
......
"alhlegtjkdmbqsvt.onion:8333",
......
"79.250.188.226:8333"
]
],
"TriedBuckets": [
[
"5.9.165.181:8333",
......
"5.9.17.24:8333"
],
[
"95.79.50.90:8333",
......
"[2a02:c207:2008:9136::1]:8333"
]
]
}
可以看出,地址仓库(peers.json)中包含version,随机序列key及Addresses、NewBuckets和TriedBuckets等,这些可以对应到serializedAddrManager的定义:
//btcd/addrmgr/addrmanager.go
type serializedAddrManager struct {
Version int
Key [32]byte
Addresses []*serializedKnownAddress
NewBuckets [newBucketCount][]string // string is NetAddressKey
TriedBuckets [triedBucketCount][]string
}
其中,serializedKnownAddress的定义如下:
//btcd/addrmgr/addrmanager.go
type serializedKnownAddress struct {
Addr string
Src string
Attempts int
TimeStamp int64
LastAttempt int64
LastSuccess int64
// no refcount or tried, that is available from context.
}
它对应于peers.json中的Addresses字段记录的地址集。serializedKnownAddress对应的实例化类型是KnownAddress,其定义如下:
//btcd/addrmgr/knownaddress.go
// KnownAddress tracks information about a known network address that is used
// to determine how viable an address is.
type KnownAddress struct {
na *wire.NetAddress
srcAddr *wire.NetAddress
attempts int
lastattempt time.Time
lastsuccess time.Time
tried bool
refs int // reference count of new buckets
}
其各字段意义如下:
- na: 从addr消息获知的节点的IPv4或者IPv6地址,请注意,我们看到KnownAddress序列化后,在peers.json中有“.onion”的地址,它是由特定的支持Tor的IPv6地址转换而来,我们将在后面介绍;
- srcAddr: addr消息的源,也是当前节点的Peer;
- attempts: 连接成功之前尝试连接的次数;
- lastattempt: 最近一次尝试连接的时间点;
- lastsuccess: 最近一次尝试连接成功的时间点;
- tried: 标识是否已经尝试连接过,已经tried过的地址将被放入TriedBuckets;
- refs: 该地址所属的NewBucket的个数,默认最大个数是8。读者可能会有疑问,为什么同一地址会放入不同的NewBucket,这是因为NewBucket的索引包含srcAddr的因子,同一地址可能从不同的srcAddr的Peer获知,导致同一地址的NewBucket的索引可能不同;
了解了AddrManager的地址仓库的形式和它管理的地址类型的定义后,我们就来看看AddrManager是如何存取这些地址。首先我们来看看AddrManager的定义:
//btcd/addrmgr/addrmanager.go
// AddrManager provides a concurrency safe address manager for caching potential
// peers on the bitcoin network.
type AddrManager struct {
mtx sync.Mutex
peersFile string
lookupFunc func(string) ([]net.IP, error)
rand *rand.Rand
key [32]byte
addrIndex map[string]*KnownAddress // address key to ka for all addrs.
addrNew [newBucketCount]map[string]*KnownAddress
addrTried [triedBucketCount]*list.List
started int32
shutdown int32
wg sync.WaitGroup
quit chan struct{}
nTried int
nNew int
lamtx sync.Mutex
localAddresses map[string]*localAddress
}
其各字段的意义如下:
- mtx: AddrManager的对象锁,保证addrManager是并发安全的;
- peersFile: 存储地址仓库的文件名,默认为“peers.json”。请注意,Bitcoind中的文件名为“peers.data”;
- lookupFunc: 进行DNS Lookup的函数值;
- rand: 随机数生成器;
- key: 32字节的随机数数序列,用于计算NewBucket和TriedBucket的索引;
- addrIndex: 缓存所有KnownAddress的map;
- addrNew: 缓存所有新地址的map slice;
- addrTried: 缓存所有已经Tried的地址的list slice。请注意与addrNew用到map不同,这里用到了list,然而从AddrManager的实现上看,addrNew和addrTired分别用map和list的差别并不大,一个可能是原因是在GetAddress()中从NewBucket或才TriedBucket选择地址时,list可能按顺序访问,而map通过range遍历元素的顺序是随机的;
- started: 用于标识addrmanager已经启动;
- shutdown: 用于标识addrmanager已经停止;
- wg: 用于同步退出,addrmanager停止时等待工作协程退出;
- quit: 用于通知工作协程退出;
- nTried: 记录Tried地址个数;
- nNew: 记录New地址个数;
- lamtx: 保护localAddresses的互斥锁;
- localAddresses: 保存已知的本地地址;
接下来,我们主要分析AddrManager的Start()、AddAddress()及GetAddress()、Good()等方法来了解其主要工作机制。我们先来看看Start():
//btcd/addrmgr/addrmanager.go
// Start begins the core address handler which manages a pool of known
// addresses, timeouts, and interval based writes.
func (a *AddrManager) Start() {
// Already started?
if atomic.AddInt32(&a.started, 1) != 1 {
return
}
log.Trace("Starting address manager")
// Load peers we already know about from file.
a.loadPeers()
// Start the address ticker to save addresses periodically.
a.wg.Add(1)
go a.addressHandler()
}
可以看出,其主要过程是调用loadPeers()来将peers.json文件中的地址集实例化,然后启动工作协程addressHandler来周期性性向文件保存新的地址。loadPeers()主要是调用deserializePeers()将文件反序列化:
//btcd/addrmgr/addrmanager.go
func (a *AddrManager) deserializePeers(filePath string) error {
......
r, err := os.Open(filePath)
......
defer r.Close()
var sam serializedAddrManager
dec := json.NewDecoder(r)
err = dec.Decode(&sam)
......
if sam.Version != serialisationVersion {
return fmt.Errorf("unknown version %v in serialized "+
"addrmanager", sam.Version)
}
copy(a.key[:], sam.Key[:])
for _, v := range sam.Addresses {
ka := new(KnownAddress)
ka.na, err = a.DeserializeNetAddress(v.Addr)
......
ka.srcAddr, err = a.DeserializeNetAddress(v.Src)
......
ka.attempts = v.Attempts
ka.lastattempt = time.Unix(v.LastAttempt, 0)
ka.lastsuccess = time.Unix(v.LastSuccess, 0)
a.addrIndex[NetAddressKey(ka.na)] = ka
}
for i := range sam.NewBuckets {
for _, val := range sam.NewBuckets[i] {
ka, ok := a.addrIndex[val]
......
if ka.refs == 0 {
a.nNew++
}
ka.refs++
a.addrNew[i][val] = ka
}
}
for i := range sam.TriedBuckets {
for _, val := range sam.TriedBuckets[i] {
ka, ok := a.addrIndex[val]
......
ka.tried = true
a.nTried++
a.addrTried[i].PushBack(ka)
}
}
// Sanity checking.
for k, v := range a.addrIndex {
if v.refs == 0 && !v.tried {
return fmt.Errorf("address %s after serialisation "+
"with no references", k)
}
if v.refs > 0 && v.tried {
return fmt.Errorf("address %s after serialisation "+
"which is both new and tried!", k)
}
}
return nil
}
其主要过程为:
- 读取文件,并通过json解析器将json文件实例化为serializedAddrManager对象;
- 校验版本号,并读取随机数序列Key;
- 将serializedKnownAddress解析为KnownAddress,并存入a.addrIndex中。需要注意的是,serializedKnownAddress中的地址均是string,而KnownAddress对应的地址是wire.NetAddress类型,在转换过程中,如果serializedKnownAddress为“.onion”的洋葱地址,则将“.onion”前的字符串转换成大写后进行base32解码,并添加“fd87:d87e:eb43”前缀转换成IPv6地址;如果是hostname,则调用lookupFunc将将解析为IP地址;同时,addrIndex的key是地址的string形式,如果是IP:Port的形式,则直接将IP和Port转换为对应的数字字符,如果是以“fd87:d87e:eb43”开头的IPv6地址,则将该地址的后10位进行base32编码并转成小写后的字符串,加上“.onion”后缀转换为洋葱地址形式。具体转换过程在ipString()和HostToNetAddress()中实现;
- 以serializedAddrManager的NewBuckets和TriedBuckets中的地址为Key,查找addrIndex中对应的KnownAddress后,填充addrNew和addrTried;
- 最后对实例化的结果作Sanity检查,保证一个地址要么在NewBuckets中,要么在TridBuckets中;
AddrManager启动后通过loadPeers()将文件中的记录实例化后,接着就启动了一个工作协程addressHandler,我们来看看它的实现:
//btcd/addrmgr/addrmanager.go
// addressHandler is the main handler for the address manager. It must be run
// as a goroutine.
func (a *AddrManager) addressHandler() {
dumpAddressTicker := time.NewTicker(dumpAddressInterval)
defer dumpAddressTicker.Stop()
out:
for {
select {
case <-dumpAddressTicker.C:
a.savePeers()
case <-a.quit:
break out
}
}
a.savePeers()
a.wg.Done()
log.Trace("Address handler done")
}
可以看出,它的主要执行过程就是每隔dumpAddressInterval(值为10分钟)调用savePeers()将addrMananager中的地址集写入文件,savePeers()是与deserializePeers()对应的实例化方法,我们不再分析它的实现,读者可以自行分析。
节点与Peer之间交换getaddr和addr消息时,会收到来自Peer告知的地址信息,这些地址会通过addrManager的AddAddress()或者AddAddresses()方法添加到addrManager的地址集合中。实际上,AddAddress()或者AddAddresses()会调用updateAddress()来作实际更新操作:
//btcd/addrmgr/addrmanager.go
// updateAddress is a helper function to either update an address already known
// to the address manager, or to add the address if not already known.
func (a *AddrManager) updateAddress(netAddr, srcAddr *wire.NetAddress) {
// Filter out non-routable addresses. Note that non-routable
// also includes invalid and local addresses.
if !IsRoutable(netAddr) { (1)
return
}
addr := NetAddressKey(netAddr)
ka := a.find(netAddr)
if ka != nil {
// TODO: only update addresses periodically.
// Update the last seen time and services.
// note that to prevent causing excess garbage on getaddr
// messages the netaddresses in addrmaanger are *immutable*,
// if we need to change them then we replace the pointer with a
// new copy so that we don't have to copy every na for getaddr.
if netAddr.Timestamp.After(ka.na.Timestamp) || (2)
(ka.na.Services&netAddr.Services) !=
netAddr.Services {
naCopy := *ka.na
naCopy.Timestamp = netAddr.Timestamp
naCopy.AddService(netAddr.Services)
ka.na = &naCopy
}
// If already in tried, we have nothing to do here.
if ka.tried { (3)
return
}
// Already at our max?
if ka.refs == newBucketsPerAddress { (4)
return
}
// The more entries we have, the less likely we are to add more.
// likelihood is 2N.
factor := int32(2 * ka.refs)
if a.rand.Int31n(factor) != 0 { (5)
return
}
} else {
// Make a copy of the net address to avoid races since it is
// updated elsewhere in the addrmanager code and would otherwise
// change the actual netaddress on the peer.
netAddrCopy := *netAddr (6)
ka = &KnownAddress{na: &netAddrCopy, srcAddr: srcAddr}
a.addrIndex[addr] = ka
a.nNew++
// XXX time penalty?
}
bucket := a.getNewBucket(netAddr, srcAddr) (7)
// Already exists?
if _, ok := a.addrNew[bucket][addr]; ok {
return
}
// Enforce max addresses.
if len(a.addrNew[bucket]) > newBucketSize {
log.Tracef("new bucket is full, expiring old")
a.expireNew(bucket) (8)
}
// Add to new bucket.
ka.refs++
a.addrNew[bucket][addr] = ka (9)
log.Tracef("Added new address %s for a total of %d addresses", addr,
a.nTried+a.nNew)
}
其主要步骤为:
- 判断欲添加的地址netAddr是否是可路由的地址,即除了保留地址以外的地址,如果是不可以路由的地址,则不加入地址仓库;
- 查询欲添加的地址是否已经在地址集中,如果已经在,且它的时间戳更新或者有支持新的服务,则更新地址集中KnownAddress,如代码(2)所示。请注意,这里的时间戳是指节点最近获知该地址的时间点;
- 代码(3)检查如果地址已经在TriedBucket中,则不更新地址仓库;代码(4)处检查如果地址已经位于8个不同的NewBucket中,也不更新仓库;代码(5)处根据地址已经被NewBucket引用的个数,来随机决定是否继续添加到NewBucket中;
- 如果欲添加的地址不在现有的地址集中,则需要将其添加到NewBucket中,如代码(6)处所示;
- 经过上述检查后,如果确定需要添加地址,则调用getNewBucket()找到NewBucket的索引,如代码(7)处所示;
- 确定了NewBucket的索引后,进一步检查欲添加的地址是否已经在对应的NewBucket时,如果是,则不再加入;
- 如果欲放置新地址的NewBucket的Size已经超过newBucketSize(默认值为64),则调用expireNew()来释放该Bucket里的一些记录,如代码(8)处所示。expireNew()的主要思想是将Bucket中时间戳最早的地址或者时间戳是未来时间点、或时间戳是一个月以前、或者尝试连接失败超过3次且没有成功过的地址、或最近一周连接失败超过10次的地址移除。
- 最后,将新地址添加到NewBucket里,如代码(9)处所示;
我们来看看getNewBucket()是如何确定Bucket的索引的:
//btcd/addrmgr/addrmanager.go
func (a *AddrManager) getNewBucket(netAddr, srcAddr *wire.NetAddress) int {
// bitcoind:
// doublesha256(key + sourcegroup + int64(doublesha256(key + group + sourcegroup))%bucket_per_source_group) % num_new_buckets
data1 := []byte{}
data1 = append(data1, a.key[:]...)
data1 = append(data1, []byte(GroupKey(netAddr))...)
data1 = append(data1, []byte(GroupKey(srcAddr))...)
hash1 := chainhash.DoubleHashB(data1)
hash64 := binary.LittleEndian.Uint64(hash1)
hash64 %= newBucketsPerGroup
var hashbuf [8]byte
binary.LittleEndian.PutUint64(hashbuf[:], hash64)
data2 := []byte{}
data2 = append(data2, a.key[:]...)
data2 = append(data2, GroupKey(srcAddr)...)
data2 = append(data2, hashbuf[:]...)
hash2 := chainhash.DoubleHashB(data2)
return int(binary.LittleEndian.Uint64(hash2) % newBucketCount)
}
可以看到,正如注释中所说,NewBucket的索引由AddrManager的随机序列key、地址newAddr及通告该地址的Peer的地址srcAddr共同决定。TriedBucket的索引也采用类似的方式决定。
当有地址添加或者更新时,会在下一次dumpAddressTicker被写入到文件中。除了收到addr消息后,主动调用AddAddress()或者AddAddresses()来更新地址集外,在节点选择地址并建立Peer关系成功后,也会调用Good()来将地址从NewBucket移入TriedBucket。
//btcd/addrmgr/addrmanager.go
// Good marks the given address as good. To be called after a successful
// connection and version exchange. If the address is unknown to the address
// manager it will be ignored.
func (a *AddrManager) Good(addr *wire.NetAddress) {
a.mtx.Lock()
defer a.mtx.Unlock()
ka := a.find(addr) (1)
if ka == nil {
return
}
// ka.Timestamp is not updated here to avoid leaking information
// about currently connected peers.
now := time.Now() (2)
ka.lastsuccess = now
ka.lastattempt = now
ka.attempts = 0
// move to tried set, optionally evicting other addresses if neeed.
if ka.tried {
return
}
// ok, need to move it to tried.
// remove from all new buckets.
// record one of the buckets in question and call it the `first'
addrKey := NetAddressKey(addr)
oldBucket := -1
for i := range a.addrNew {
// we check for existence so we can record the first one
if _, ok := a.addrNew[i][addrKey]; ok {
delete(a.addrNew[i], addrKey) (3)
ka.refs--
if oldBucket == -1 {
oldBucket = i (4)
}
}
}
a.nNew--
if oldBucket == -1 {
// What? wasn't in a bucket after all.... Panic?
return
}
bucket := a.getTriedBucket(ka.na) (5)
// Room in this tried bucket?
if a.addrTried[bucket].Len() < triedBucketSize {
ka.tried = true
a.addrTried[bucket].PushBack(ka) (6)
a.nTried++
return
}
// No room, we have to evict something else.
entry := a.pickTried(bucket)
rmka := entry.Value.(*KnownAddress)
// First bucket it would have been put in.
newBucket := a.getNewBucket(rmka.na, rmka.srcAddr) (7)
// If no room in the original bucket, we put it in a bucket we just
// freed up a space in.
if len(a.addrNew[newBucket]) >= newBucketSize {
newBucket = oldBucket (8)
}
// replace with ka in list.
ka.tried = true
entry.Value = ka (9)
rmka.tried = false
rmka.refs++
// We don't touch a.nTried here since the number of tried stays the same
// but we decemented new above, raise it again since we're putting
// something back.
a.nNew++
rmkey := NetAddressKey(rmka.na)
log.Tracef("Replacing %s with %s in tried", rmkey, addrKey)
// We made sure there is space here just above.
a.addrNew[newBucket][rmkey] = rmka (10)
}
其主要过程如下:
- 查询连成功的地址是否在地址集中,如果不在,则不作处理,如代码(1)处所示;
- 如果地址在地址集中,则更新该地址的lastsuccess和lastattempt为当前时间点,且将连败重试次数attempts重置,如代码(2)处所示;
- 如果地址已经在TrieBucket中,则只更新lastsuccess、lastattempt和attempts即可,我们将在GetAddress()中看到,AddrManager选择地址建Peer时,会随机地从NewBucket和TriedBucket中选择;
- 如果地址在NewBucket中,则将其从对应的Bucket中移除,如代码(3)处所示;请注意,这里记录下了地址所处的NewBucket的索引号oldBucket,如代码(4)处所示,它将在后面用到;
- 代码(5)处选择一个TriedBucket的索引号,用于将地址添加进对应的Bucket;
- 如果选择的TriedBucket未填满(容量为256),则将地址添加到Bucket,如代码(6)处所示;
- 如果选择的TriedBucket已经填满,则调用pickTried()从其中选择一个地址,准备将其移动到NewBucket中以腾出空间,随后代码(7)处为该地址选择一个NewBucket;
- 如果欲移入的NewBucket已经满,则将选择的地址从TriedBucket中移入索引号为oldBucket的NewBucket中,即移入刚刚移除了addr的NewBucket中,如代码(8)所示;
- 代码(9)将连接成功的地址添加到选择的TriedBucket中,通过将listElement的Value直接更新为对应的ka来实现;
- 代码(10)处将从TriedBucket中移出的地址移入选择的NewBucket中;
最后,我们来分析AddrManage是如何选择一个地址,以供节点建立Peer连接的,它是在GetAddress()中实现的。
//btcd/addrmgr/addrmanager.go
// GetAddress returns a single address that should be routable. It picks a
// random one from the possible addresses with preference given to ones that
// have not been used recently and should not pick 'close' addresses
// consecutively.
func (a *AddrManager) GetAddress() *KnownAddress {
// Protect concurrent access.
a.mtx.Lock()
defer a.mtx.Unlock()
if a.numAddresses() == 0 {
return nil
}
// Use a 50% chance for choosing between tried and new table entries.
if a.nTried > 0 && (a.nNew == 0 || a.rand.Intn(2) == 0) { (1)
// Tried entry.
large := 1 << 30
factor := 1.0
for {
// pick a random bucket.
bucket := a.rand.Intn(len(a.addrTried)) (2)
if a.addrTried[bucket].Len() == 0 {
continue
}
// Pick a random entry in the list
e := a.addrTried[bucket].Front()
for i :=
a.rand.Int63n(int64(a.addrTried[bucket].Len())); i > 0; i-- { (3)
e = e.Next()
}
ka := e.Value.(*KnownAddress)
randval := a.rand.Intn(large)
if float64(randval) < (factor * ka.chance() * float64(large)) { (4)
log.Tracef("Selected %v from tried bucket",
NetAddressKey(ka.na))
return ka
}
factor *= 1.2 (5)
}
} else {
// new node.
// XXX use a closure/function to avoid repeating this.
large := 1 << 30
factor := 1.0
for {
// Pick a random bucket.
bucket := a.rand.Intn(len(a.addrNew)) (6)
if len(a.addrNew[bucket]) == 0 {
continue
}
// Then, a random entry in it.
var ka *KnownAddress
nth := a.rand.Intn(len(a.addrNew[bucket]))
for _, value := range a.addrNew[bucket] { (7)
if nth == 0 {
ka = value
}
nth--
}
randval := a.rand.Intn(large)
if float64(randval) < (factor * ka.chance() * float64(large)) { (8)
log.Tracef("Selected %v from new bucket",
NetAddressKey(ka.na))
return ka
}
factor *= 1.2 (9)
}
}
}
其主要步骤为:
- 如地址集中NewBucket和TriedBucket,即既有已经尝试连接过的“老”地址,也有未连接过的“新”地址,则按50%的概率随机地从NewBucket或TriedBucket中选择;
- 如果决定从TriedBucket中选择,则随机选择一个TriedBucket,如代码(2)处所示;
- 从随机选择的TriedBucket中,再随机地选择一个地址,如代码(3)处所示;
- 再判断选择的地址是否满足一个随机条件,如果满足则返回该地址,如代码(4)处所示;如果不满足,则增加factor因子以增加满足随机条件的概率,并重复2-4步骤,如代码(5)处所示。这个随机条件是: 从0 ~ 102410241024 范围内随机选择一个数,这个随机数是否小于它乘以factor和ka.chance()的结果。可以看到,factor或者ka.chance越大,该条件成立的概率越大;
- 如果决定从NewBucket中选择,则采取与TriedBucket相似的步骤随机选择地址,如果代码 (6) - (9) 所示;
在从NewBucket或TriedBucket中随机选择地址是,ka.chance()的值为影响地址被选中的概率,我们来看看它的实现:
//btcd/addrmgr/knownaddress.go
// chance returns the selection probability for a known address. The priority
// depends upon how recently the address has been seen, how recently it was last
// attempted and how often attempts to connect to it have failed.
func (ka *KnownAddress) chance() float64 {
now := time.Now()
lastAttempt := now.Sub(ka.lastattempt)
if lastAttempt < 0 {
lastAttempt = 0
}
c := 1.0
// Very recent attempts are less likely to be retried.
if lastAttempt < 10*time.Minute {
c *= 0.01
}
// Failed attempts deprioritise.
for i := ka.attempts; i > 0; i-- {
c /= 1.5
}
return c
}
可以看到,如果10分钟之内尝试连接过,地址的选择概率将降为1%;同时,每尝试失败一次,则被选中的概率降为原来的2/3。也就是说,如果10分钟之内尝试连接失败过,或者多次连接失败,则该地址被选中的概率大大降低。
到此,我们就了解了AddrManager的工作机制,它主要负责将从Peer“学习”到的地址分类为“新”地址和“老”地址,并分别通过NewBucket和TriedBucket来管理,同时周期性地将地址集写入文件存储。更重要地,它提供了从地址集中随机选择地址的策略,使得节点可以随机地选择Peer,从而避免了恶意节点的“钓鱼”攻击。
我们介绍完AddrManger、ConnManager和Peer后,大家可以了解P2P网络建立的基础过程: 即先通过AddrManger选择Peer的地址,并通过ConnManager建立TCP连接,然后通过Peer开始收发协议消息。那么,Peer之间会交换哪些消息呢?前面的介绍中,我们提到过Peer节点会交换getaddr和addr消息来同步地址信息,除此之外,它们之间还会交换哪些消息呢?我们将在下一篇文章《Btcd协议消息解析》中介绍。