前面的系列文章中我們介紹了Bitcoin網(wǎng)絡(luò)中節(jié)點對區(qū)塊的存取機制侠草,本文開始我們將介紹Btcd節(jié)點如何組成P2P網(wǎng)絡(luò)以及區(qū)塊如何在P2P網(wǎng)絡(luò)中傳播。區(qū)塊在網(wǎng)絡(luò)上的傳遞過程涉及節(jié)點之間的連接管理、地址管理房蝉、Peer節(jié)點的管理和Peer之間同步區(qū)塊的協(xié)議等問題颊郎,與之相關(guān)的代碼在如下幾個包或者文件中:
- btcd/wire: 實現(xiàn)了Bitcoin網(wǎng)絡(luò)協(xié)議,定義了Peers之間的協(xié)議消息他炊、消息格式及包的封裝和解析等;
- btcd/peer: 實現(xiàn)了P2P網(wǎng)絡(luò)中Peer之間維持連接及收發(fā)wire協(xié)議消息的機制;
- btcd/connmgr: 管理Peer節(jié)點之間的TCP連接争剿,包括監(jiān)聽本地端口等待其他節(jié)點連接和主動連接Peer節(jié)點等等;
- btcd/addrmgr: 收集、存儲Bitcoin網(wǎng)絡(luò)上的節(jié)點的地址痊末,隨機從地址集中選擇路由可達的地址建立Peer連接蚕苇,這些地址包括IPv4、IPv6及洋蔥地址(.onion address)等;
- btcd/server.go: Btcd節(jié)點啟動后執(zhí)行的主要入口凿叠,定義了server及serverPeer類型涩笤,負責(zé)初始化和啟動connmgr、addrmgr盒件,以及響應(yīng)Peer的協(xié)議消息等蹬碧。
這些模塊之間的關(guān)系如下圖所示:
接下來,我們將逐一閱讀和分析各個包中的代碼炒刁。我們首先來了解P2P網(wǎng)絡(luò)是如何組網(wǎng)恩沽,然后再進一步了解Bitcoin網(wǎng)絡(luò)協(xié)議的實現(xiàn)。btcd/peer是實現(xiàn)Bitcoin P2P網(wǎng)絡(luò)的核心模塊翔始,我們先從它開始介紹飒筑。
Clone完btcd/peer的代碼后,我們可以發(fā)現(xiàn)它包含如下一些文件:
- peer.go: 包含了幾乎全部的Peer相關(guān)邏輯實現(xiàn)代碼绽昏;
- mruinvmap.go: 實現(xiàn)了一個簡易的緩存Inverntory的LRU Cache协屡。請注意,雖然文件名由mru開頭全谤,但它實際上實現(xiàn)的是LRU Cache肤晓。Inventory是節(jié)點向Peer節(jié)點回復(fù)或者通知的己方已經(jīng)知道的Tx或者Block,它通過inv協(xié)議消息以Inventory Vectors的形式發(fā)往Peer節(jié)點,我們將在介紹網(wǎng)絡(luò)協(xié)議時進一步介紹它补憾。之所以要使用緩存漫萄,是為了防止向Peer重復(fù)發(fā)送已經(jīng)發(fā)送過的Inventory;
- mrunoncemap.go: 與mruinvmap.go類似,它實現(xiàn)了一個緩存nonce值的LRU Cache盈匾。這里的nonce值是一個64位隨機整數(shù)值腾务,用于填充Peer間握手交換Version信息的nonce字段,nonce字段用來判斷欲連接的Peer是否就是自己:節(jié)點發(fā)出Version消息時削饵,會填入一個隨機的nonce值岩瘦,連接不同Peer節(jié)點時,節(jié)點會緩存所有為Version消息生成的nonce值窿撬;如果節(jié)點收到了一個Version消息且其中的nonce值是自己緩存的nonce中的一個启昧,那么可以判斷這個Version消息由自己發(fā)送給自己了;
- log.go: 提供一些log方法;
- doc.go: 包btcd/peer的doc文件;
- example_test.go、export_test.go劈伴、mruinvmap_test.go密末、mrunoncemap_test.go、peer_test.go: 定義一些測試方法;
其中主要的類型為Peer跛璧、Config和MessageListeners严里,Peer類型定義了Peer相關(guān)的屬性和方法,Config類型定義了與Peer相關(guān)的配置追城,MessageListeners定義了響應(yīng)Peer消息的回調(diào)函數(shù)田炭。它們定義的成員字段比較多,我們不打算一一介紹漓柑,將在分析具體代碼時解釋其字段的意義教硫。我們先從創(chuàng)建Peer對象的newPeerBase()方法入手來分析Peer:
//btcd/peer/peer.go
// newPeerBase returns a new base bitcoin peer based on the inbound flag. This
// is used by the NewInboundPeer and NewOutboundPeer functions to perform base
// setup needed by both types of peers.
func newPeerBase(origCfg *Config, inbound bool) *Peer {
// Default to the max supported protocol version if not specified by the
// caller.
cfg := *origCfg // Copy to avoid mutating caller.
......
p := Peer{
inbound: inbound,
knownInventory: newMruInventoryMap(maxKnownInventory),
stallControl: make(chan stallControlMsg, 1), // nonblocking sync
outputQueue: make(chan outMsg, outputBufferSize),
sendQueue: make(chan outMsg, 1), // nonblocking sync
sendDoneQueue: make(chan struct{}, 1), // nonblocking sync
outputInvChan: make(chan *wire.InvVect, outputBufferSize),
inQuit: make(chan struct{}),
queueQuit: make(chan struct{}),
outQuit: make(chan struct{}),
quit: make(chan struct{}),
cfg: cfg, // Copy so caller can't mutate.
services: cfg.Services,
protocolVersion: cfg.ProtocolVersion,
}
return &p
}
從上面創(chuàng)建Peer的代碼中可以看出,Peer中的關(guān)鍵字段包括:
- inbound: 用于指示Peer是inbound還是outbound辆布。如果當(dāng)前節(jié)點主動連接Peer瞬矩,則Peer為OutbandPeer;如果Peer主動連接當(dāng)前節(jié)點锋玲,則Peer為InboundPeer;
- knownInventory: 已經(jīng)發(fā)送給Peer的Inventory的緩存;
- stallControl: 帶緩沖的stallControlMsg chan景用,在收、發(fā)消息的goroutine和超時控制goroutine之間通信惭蹂,后面會進一步介紹;
- outputQueue: 帶緩沖的outMsg chan伞插,實現(xiàn)了一個發(fā)送隊列;
- sendQueue: 緩沖大小為1的outMsg chan,用于將outputQueue中的outMsg按加入發(fā)送隊列的順序發(fā)送給Peer;
- sendDoneQueue: 帶緩沖的channel盾碗,用于通知維護發(fā)送隊列的goroutine上一個消息已經(jīng)發(fā)送完成媚污,應(yīng)該取下一條消息發(fā)送;
- outputInvChan: 實現(xiàn)發(fā)送inv消息的發(fā)送隊列,該隊列以10s為周期向Peer發(fā)送inv消息;
- inQuit: 用于通知收消息的goroutine已經(jīng)退出;
- outQuit: 用于通知發(fā)消息的goroutine已經(jīng)退出廷雅,當(dāng)收耗美、發(fā)消息的goroutine均退出時京髓,超時控制goroutine也將退出;
- quit:用于通知所有處理事務(wù)的goroutine退出;
- cfg: 與Peer相關(guān)的Config,其中比較重要是的Config中的MessageListeners商架,指明了處理從Peer收到的消息的響應(yīng)函數(shù);
- services: 用于記錄Peer支持的服務(wù)堰怨,如: SFNodeNetwork表明Peer是一個全節(jié)點,SFNodeGetUTXO表明Peer支持getutxos和utxos命令蛇摸,SFNodeBloom表明Peer支持Bloom過濾;
- protocolVersion:用于記錄Peer所用的協(xié)議版本;
上述有些字段與Peer實現(xiàn)的消息收發(fā)機制有關(guān)系备图,讀者可能會有些疑惑,我們將在下面詳細介紹赶袄。同時揽涮,這里涉及到了Go中的channel和goroutine,它們是golang簡化并發(fā)編程的關(guān)鍵工具弃鸦,剛開始接觸Go的讀者可能覺得不好理解绞吁,也會對理解Peer的代碼造成一些障礙幢痘。由于篇幅原因唬格,本文不深入介紹Go的并發(fā)編程,讀者可以翻閱相關(guān)書籍颜说。為了方便大家理解代碼购岗,我們引用golang官方對它的并發(fā)機制的總結(jié)作一個啟示:
Do not communicate by sharing memory; instead, share memory by communicating.
channel就是用來在goroutine之間進行通信的數(shù)據(jù)類型。接下來门粪,我們來看Peer的start()方法以進一步了解它的實現(xiàn):
// start begins processing input and output messages.
func (p *Peer) start() error {
log.Tracef("Starting peer %s", p)
negotiateErr := make(chan error)
go func() {
if p.inbound {
negotiateErr <- p.negotiateInboundProtocol()
} else {
negotiateErr <- p.negotiateOutboundProtocol()
}
}()
// Negotiate the protocol within the specified negotiateTimeout.
select {
case err := <-negotiateErr:
if err != nil {
return err
}
case <-time.After(negotiateTimeout):
return errors.New("protocol negotiation timeout")
}
log.Debugf("Connected to %s", p.Addr())
// The protocol has been negotiated successfully so start processing input
// and output messages.
go p.stallHandler()
go p.inHandler()
go p.queueHandler()
go p.outHandler()
go p.pingHandler()
// Send our verack message now that the IO processing machinery has started.
p.QueueMessage(wire.NewMsgVerAck(), nil)
return nil
}
上述代碼主要包含:
- 起了一個goroutine來與Peer交換Version消息喊积,調(diào)用goroutine與新的goroutine通過negotiateErr channel同步,調(diào)用goroutine阻塞等待Version握手完成;
- 如果Version握手失敗或者超時玄妈,則返回錯誤乾吻,Peer關(guān)系建立失敗;
- 如果握手成功,則啟動5個新的goroutine來收發(fā)消息拟蜻。其中绎签,stallHandler()用于處理消息超時,inHandler()用于接收Peer消息酝锅,queueHandler()用于維護消息發(fā)送列隊诡必,outHandler用于向Peer發(fā)送消息,pingHandler()用于向Peer周期性地發(fā)送心跳搔扁;
- 最后爸舒,向Peer發(fā)送verack消息,雙方完成握手稿蹲。
Peer start()成功后扭勉,節(jié)點間的Peer關(guān)系便成功建立,可以進一步交換其他協(xié)議消息了苛聘,如果節(jié)點與不同的其他節(jié)點建立了Peer關(guān)系剖效,其他節(jié)點又與新的節(jié)點建立Peer關(guān)系嫉入,所有節(jié)點會逐漸形成一張P2P網(wǎng)絡(luò)。然而璧尸,節(jié)點在交換Version時從對方獲取了哪些信息咒林,為什么要等到Version交換完成后Peer關(guān)系才能正常建立?各個Handler又是如何實現(xiàn)收發(fā)消息的呢爷光?我們將在下一篇文章《Btcd區(qū)塊在P2P網(wǎng)絡(luò)上的傳播之Peer》中詳細介紹垫竞。