Btcd區塊在P2P網絡上的傳播之AddrManager

在介紹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
}

其主要過程為:

  1. 讀取文件,并通過json解析器將json文件實例化為serializedAddrManager對象;
  2. 校驗版本號,并讀取隨機數序列Key;
  3. 將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()中實現;
  4. 以serializedAddrManager的NewBuckets和TriedBuckets中的地址為Key,查找addrIndex中對應的KnownAddress后,填充addrNew和addrTried;
  5. 最后對實例化的結果作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)
}

其主要步驟為:

  1. 判斷欲添加的地址netAddr是否是可路由的地址,即除了保留地址以外的地址,如果是不可以路由的地址,則不加入地址倉庫;
  2. 查詢欲添加的地址是否已經在地址集中,如果已經在,且它的時間戳更新或者有支持新的服務,則更新地址集中KnownAddress,如代碼(2)所示。請注意,這里的時間戳是指節點最近獲知該地址的時間點;
  3. 代碼(3)檢查如果地址已經在TriedBucket中,則不更新地址倉庫;代碼(4)處檢查如果地址已經位于8個不同的NewBucket中,也不更新倉庫;代碼(5)處根據地址已經被NewBucket引用的個數,來隨機決定是否繼續添加到NewBucket中;
  4. 如果欲添加的地址不在現有的地址集中,則需要將其添加到NewBucket中,如代碼(6)處所示;
  5. 經過上述檢查后,如果確定需要添加地址,則調用getNewBucket()找到NewBucket的索引,如代碼(7)處所示;
  6. 確定了NewBucket的索引后,進一步檢查欲添加的地址是否已經在對應的NewBucket時,如果是,則不再加入;
  7. 如果欲放置新地址的NewBucket的Size已經超過newBucketSize(默認值為64),則調用expireNew()來釋放該Bucket里的一些記錄,如代碼(8)處所示。expireNew()的主要思想是將Bucket中時間戳最早的地址或者時間戳是未來時間點、或時間戳是一個月以前、或者嘗試連接失敗超過3次且沒有成功過的地址、或最近一周連接失敗超過10次的地址移除。
  8. 最后,將新地址添加到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. 查詢連成功的地址是否在地址集中,如果不在,則不作處理,如代碼(1)處所示;
  2. 如果地址在地址集中,則更新該地址的lastsuccess和lastattempt為當前時間點,且將連敗重試次數attempts重置,如代碼(2)處所示;
  3. 如果地址已經在TrieBucket中,則只更新lastsuccess、lastattempt和attempts即可,我們將在GetAddress()中看到,AddrManager選擇地址建Peer時,會隨機地從NewBucket和TriedBucket中選擇;
  4. 如果地址在NewBucket中,則將其從對應的Bucket中移除,如代碼(3)處所示;請注意,這里記錄下了地址所處的NewBucket的索引號oldBucket,如代碼(4)處所示,它將在后面用到;
  5. 代碼(5)處選擇一個TriedBucket的索引號,用于將地址添加進對應的Bucket;
  6. 如果選擇的TriedBucket未填滿(容量為256),則將地址添加到Bucket,如代碼(6)處所示;
  7. 如果選擇的TriedBucket已經填滿,則調用pickTried()從其中選擇一個地址,準備將其移動到NewBucket中以騰出空間,隨后代碼(7)處為該地址選擇一個NewBucket;
  8. 如果欲移入的NewBucket已經滿,則將選擇的地址從TriedBucket中移入索引號為oldBucket的NewBucket中,即移入剛剛移除了addr的NewBucket中,如代碼(8)所示;
  9. 代碼(9)將連接成功的地址添加到選擇的TriedBucket中,通過將listElement的Value直接更新為對應的ka來實現;
  10. 代碼(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)
        }
    }
}

其主要步驟為:

  1. 如地址集中NewBucket和TriedBucket,即既有已經嘗試連接過的“老”地址,也有未連接過的“新”地址,則按50%的概率隨機地從NewBucket或TriedBucket中選擇;
  2. 如果決定從TriedBucket中選擇,則隨機選擇一個TriedBucket,如代碼(2)處所示;
  3. 從隨機選擇的TriedBucket中,再隨機地選擇一個地址,如代碼(3)處所示;
  4. 再判斷選擇的地址是否滿足一個隨機條件,如果滿足則返回該地址,如代碼(4)處所示;如果不滿足,則增加factor因子以增加滿足隨機條件的概率,并重復2-4步驟,如代碼(5)處所示。這個隨機條件是: 從0 ~ 102410241024 范圍內隨機選擇一個數,這個隨機數是否小于它乘以factor和ka.chance()的結果。可以看到,factor或者ka.chance越大,該條件成立的概率越大;
  5. 如果決定從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協議消息解析》中介紹。

==大家可以關注我的微信公眾號,后續文章將在公眾號中同步更新:==
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 上一篇[http://www.lxweimin.com/p/66dc6f1ea05a]文章我們介紹了Peer收發消...
    oceanken閱讀 1,077評論 0 4
  • 前面的系列文章中我們介紹了Bitcoin網絡中節點對區塊的存取機制,本文開始我們將介紹Btcd節點如何組成P2P網...
    oceanken閱讀 1,643評論 0 5
  • 上一篇文章中,我們介紹完了Peer的start()方法,本文將深入start()里的調用方法來分析Peer的收發消...
    oceanken閱讀 2,190評論 0 8
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,915評論 18 139
  • 我第一次見他照片,是在有他專欄的雜志上面,八卦的那一部分。 清秀少年的模樣,嘴里含一顆棒棒糖,天真無邪。那時候《小...
    何子初閱讀 380評論 1 0