在介紹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協議消息解析》中介紹。