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协议消息解析》中介绍。

==大家可以关注我的微信公众号,后续文章将在公众号中同步更新:==
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容

  • 上一篇[https://www.jianshu.com/p/66dc6f1ea05a]文章我们介绍了Peer收发消...
    oceanken阅读 992评论 0 4
  • 前面的系列文章中我们介绍了Bitcoin网络中节点对区块的存取机制,本文开始我们将介绍Btcd节点如何组成P2P网...
    oceanken阅读 1,528评论 0 5
  • 上一篇文章中,我们介绍完了Peer的start()方法,本文将深入start()里的调用方法来分析Peer的收发消...
    oceanken阅读 2,080评论 0 8
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,111评论 18 139
  • 我第一次见他照片,是在有他专栏的杂志上面,八卦的那一部分。 清秀少年的模样,嘴里含一颗棒棒糖,天真无邪。那时候《小...
    何子初阅读 360评论 1 0