剝開比原看程式碼05:如何從比原節點拿到區塊資料?
作者:freewind
比原專案倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockchain/bytom
在前一篇中,我們已經知道如何連上一個比原節點的p2p埠,並與對方完成身份驗證。此時,雙方結點已經建立起來了信任,並且連線也不會斷開,下一步,兩者就可以繼續交換資料了。
那麼,我首先想到的就是,如何才能讓對方把它已有的區塊資料全都發給我呢?
這其實可以分為三個問題:
- 我需要發給它什麼樣的資料?
- 它在內部由是如何應答的呢?
- 我拿到資料之後,應該怎麼處理?
由於這一塊的邏輯還是比較複雜的,所以在本篇我們先回答第一個問題:
我們要傳送什麼樣的資料請求,才能讓比原節點把它持有的區塊資料發給我?
找到傳送請求的程式碼
首先我們先要在程式碼中定位到,比原到底是在什麼時候來向對方節點傳送請求的。
在前一篇講的是如何建立連線並驗證身份,那麼發出資料請求的操作,一定在上次的程式碼之後。按照這個思路,我們在SyncManager
類中Switch
啟動之後,找到了一個叫BlockKeeper
的類,相關的操作是在它裡面完成的。
下面是老規矩,還是從啟動開始,但是會更簡化一些:
func main() {
cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir()))
cmd.Execute()
}
cmd/bytomd/commands/run_node.go#L41
func runNode(cmd *cobra.Command, args []string) error {
n := node.NewNode(config)
if _, err := n.Start(); err != nil {
// ...
}
[node/node.go#L169](https://github.com/freewind/bytom-v1.0.1/blob/master/node/node.go#L169)
func (n *Node) OnStart() error {
// ...
n.syncManager.Start()
// ...
}
func (sm *SyncManager) Start() {
go sm.netStart()
// ...
go sm.syncer()
}
注意sm.netStart()
,我們在一篇中建立連線並驗證身份的操作,就是在它裡面完成的。而這次的這個問題,是在下面的sm.syncer()
中完成的。
另外注意,由於這兩個函式呼叫都使用了goroutine,所以它們是同時進行的。
sm.syncer()
的程式碼如下:
func (sm *SyncManager) syncer() {
sm.fetcher.Start()
defer sm.fetcher.Stop()
// ...
for {
select {
case <-sm.newPeerCh:
log.Info("New peer connected.")
// Make sure we have peers to select from, then sync
if sm.sw.Peers().Size() < minDesiredPeerCount {
break
}
go sm.synchronise()
// ..
}
}
這裡混入了一個叫fetcher
的奇怪的東西,名字看起來好像是專門去抓取資料的,我們要找的是它嗎?
可惜不是,fetcher
的作用是從多個peer那裡拿到了區塊資料之後,對資料進行整理,把有用的放到本地鏈上。我們在以後會研究它,所以這裡不展開討論。
接著是一個for
迴圈,當發現通道newPeerCh
有了新資料(也就是有了新的節點連線上了),會判斷一下當前自己連著的節點是否夠多(大於等於minDesiredPeerCount
,值為5
),夠多的話,就會進入sm.synchronise()
,進行資料同步。
這裡為什麼要多等幾個節點,而不是一連上就馬上同步呢?我想這是希望有更多選擇的機會,找到一個資料夠多的節點。
sm.synchronise()
還是屬於SyncManager
的方法。在真正呼叫到BlockKeeper
的方法之前,它還做了一些比如清理已經斷開的peer,找到最適合同步資料的peer等。其中“清理peer”的工作涉及到不同的物件持有的peer集合間的同步,略有些麻煩,但對當前問題幫助不大,所以我打算把它們放在以後的某個問題中回答(比如“當一個節點斷開了,比原會有什麼樣的處理”),這裡就先省略。
sm.synchronise()
程式碼如下:
func (sm *SyncManager) synchronise() {
log.Info("bk peer num:", sm.blockKeeper.peers.Len(), " sw peer num:", sm.sw.Peers().Size(), " ", sm.sw.Peers().List())
// ...
peer, bestHeight := sm.peers.BestPeer()
// ...
if bestHeight > sm.chain.BestBlockHeight() {
// ...
sm.blockKeeper.BlockRequestWorker(peer.Key, bestHeight)
}
}
可以看到,首先是從眾多的peers中,找到最合適的那個。什麼叫Best呢?看一下BestPeer()
的定義:
func (ps *peerSet) BestPeer() (*p2p.Peer, uint64) {
// ...
for _, p := range ps.peers {
if bestPeer == nil || p.height > bestHeight {
bestPeer, bestHeight = p.swPeer, p.height
}
}
return bestPeer, bestHeight
}
其實就是持有區塊鏈資料最長的那個。
找到了BestPeer之後,就呼叫sm.blockKeeper.BlockRequestWorker(peer.Key, bestHeight)
方法,從這裡,正式進入BlockKeeper
-- 也就是本文的主角 -- 的世界。
BlockKeeper
blockKeeper.BlockRequestWorker
的邏輯比較複雜,它包含了:
- 根據自己持有的區塊資料來計算需要同步的資料
- 向前面找到的最佳節點傳送資料請求
- 拿到對方發過來的區塊資料
- 對資料進行處理
- 廣播新狀態
- 處理各種出錯情況,等等
由於本文中只關注“傳送請求”,所以一些與之關係不大的邏輯我會忽略掉,留待以後再講。
在“傳送請求”這裡,實際也包含了兩種情形,一種簡單的,一種複雜的:
- 簡單的:假設不存在分叉,則直接檢查本地高度最高的區塊,然後請求下一個區塊
- 複雜的:考慮分叉的情況,則當前本地的區塊可能就存在分叉,那麼到底應該請求哪個區塊,就需要慎重考慮
由於第2種情況對於本文來說過於複雜(因為需要深刻理解比原鏈中分叉的處理邏輯),所以在本文中將把問題簡化,只考慮第1種。而分叉的處理,將放在以後講解。
下面是把blockKeeper.BlockRequestWorker
中的程式碼簡化成了只包含第1種情況:
func (bk *blockKeeper) BlockRequestWorker(peerID string, maxPeerHeight uint64) error {
num := bk.chain.BestBlockHeight() + 1
reqNum := uint64(0)
reqNum = num
// ...
bkPeer, ok := bk.peers.Peer(peerID)
swPeer := bkPeer.getPeer()
// ...
block, err := bk.BlockRequest(peerID, reqNum)
// ...
}
在這種情況下,我們可以認為bk.chain.BestBlockHeight()
中的Best
,指的是本地持有的不帶分叉的區塊鏈高度最高的那個。(需要提醒的是,如果存在分叉情況,則Best
不一定是高度最高的那個)
那麼我們就可以直接向最佳peer請求下一個高度的區塊,它是通過bk.BlockRequest(peerID, reqNum)
實現的:
func (bk *blockKeeper) BlockRequest(peerID string, height uint64) (*types.Block, error) {
var block *types.Block
if err := bk.blockRequest(peerID, height); err != nil {
return nil, errReqBlock
}
// ...
for {
select {
case pendingResponse := <-bk.pendingProcessCh:
block = pendingResponse.block
// ...
return block, nil
// ...
}
}
}
在上面簡化後的程式碼中,主要分成了兩個部分。一個是傳送請求bk.blockRequest(peerID, height)
,這是本文的重點;它下面的for-select
部分,已經是在等待並處理對方節點的返回資料了,這部分我們今天先略過不講。
bk.blockRequest(peerID, height)
這個方法,從邏輯上又可以分成兩部分:
- 構造出請求的資訊
- 把資訊傳送給對方節點
構造出請求的資訊
bk.blockRequest(peerID, height)
經過一連串的方法呼叫之後,使用height
構造出了一個BlockRequestMessage
物件,程式碼如下:
func (bk *blockKeeper) blockRequest(peerID string, height uint64) error {
return bk.peers.requestBlockByHeight(peerID, height)
}
func (ps *peerSet) requestBlockByHeight(peerID string, height uint64) error {
peer, ok := ps.Peer(peerID)
// ...
return peer.requestBlockByHeight(height)
}
func (p *peer) requestBlockByHeight(height uint64) error {
msg := &BlockRequestMessage{Height: height}
p.swPeer.TrySend(BlockchainChannel, struct{ BlockchainMessage }{msg})
return nil
}
到這裡,終於構造出了所需要的BlockRequestMessage
,其實主要就是把height
告訴peer。
然後,通過Peer
的TrySend()
把該資訊發出去。
傳送請求
在TrySend
中,主要是通過github.com/tendermint/go-wire
庫將其序列化,再傳送給對方。看起來應該是很簡單的操作吧,先預個警,還是挺繞的。
當我們進入TrySend()
後:
func (p *Peer) TrySend(chID byte, msg interface{}) bool {
if !p.IsRunning() {
return false
}
return p.mconn.TrySend(chID, msg)
}
發現它把鍋丟給了p.mconn.TrySend
方法,那麼mconn
是什麼?chID
又是什麼?
mconn
是MConnection
的例項,它是從哪兒來的?它應該在之前的某個地方初始化了,否則我們沒法直接呼叫它。所以我們先來找到它初始化的地方。
經過一番尋找,發現原來是在前一篇之後,即比原節點與另一個節點完成了身份驗證之後,具體的位置在Switch
類啟動的地方。
我們這次直接從Swtich
的OnStart
作為起點:
func (sw *Switch) OnStart() error {
//...
// Start listeners
for _, listener := range sw.listeners {
go sw.listenerRoutine(listener)
}
return nil
}
func (sw *Switch) listenerRoutine(l Listener) {
for {
inConn, ok := <-l.Connections()
// ...
err := sw.addPeerWithConnectionAndConfig(inConn, sw.peerConfig)
// ...
}
}
func (sw *Switch) addPeerWithConnectionAndConfig(conn net.Conn, config *PeerConfig) error {
// ...
peer, err := newInboundPeerWithConfig(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, config)
// ...
}
func newInboundPeerWithConfig(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) {
return newPeerFromConnAndConfig(conn, false, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config)
}
[p2p/peer.go#L91](https://github.com/freewind/bytom-v1.0.1/blob/master/p2p/peer.go#L91)
func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) {
conn := rawConn
// ...
if config.AuthEnc {
// ...
conn, err = MakeSecretConnection(conn, ourNodePrivKey)
// ...
}
// Key and NodeInfo are set after Handshake
p := &Peer{
outbound: outbound,
conn: conn,
config: config,
Data: cmn.NewCMap(),
}
p.mconn = createMConnection(conn, p, reactorsByCh, chDescs, onPeerError, config.MConfig)
p.BaseService = *cmn.NewBaseService(nil, "Peer", p)
return p, nil
}
終於找到了。上面方法中的MakeSecretConnection
就是與對方節點交換公鑰並進行身份驗證的地方,下面的p.mconn = createMConnection(...)
就是建立mconn
的地方。
繼續進去:
func createMConnection(conn net.Conn, p *Peer, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), config *MConnConfig) *MConnection {
onReceive := func(chID byte, msgBytes []byte) {
reactor := reactorsByCh[chID]
if reactor == nil {
if chID == PexChannel {
return
} else {
cmn.PanicSanity(cmn.Fmt("Unknown channel %X", chID))
}
}
reactor.Receive(chID, p, msgBytes)
}
onError := func(r interface{}) {
onPeerError(p, r)
}
return NewMConnectionWithConfig(conn, chDescs, onReceive, onError, config)
}
原來mconn
是MConnection
的例項,它是通過NewMConnectionWithConfig
建立的。
看了上面的程式碼,發現這個MConnectionWithConfig
與普通的net.Conn
並沒有太大的區別,只不過是當收到了對方發來的資料後,會根據指定的chID
呼叫相應的Reactor
的Receive
方法來處理。所以它起到了將資料分發給Reactor
的作用。
為什麼需要這樣的分發操作呢?這是因為,在比原中,節點之間交換資料,有多種不同的方式:
- 一種是規定了詳細的資料互動協議(比如有哪些資訊型別,分別代表什麼意思,什麼情況下發哪個,如何應答等),在
ProtocolReactor
中實現,它對應的chID
是BlockchainChannel
,值為byte(0x40)
- 另一種使用了與BitTorrent類似的檔案共享協議,叫PEX,在
PEXReactor
中實現,它對應的chID
是PexChannel
,值為byte(0x00)
所以節點之間傳送資訊的時候,需要知道對方發過來的資料對應的是哪一種方式,然後轉交給相應的Reactor
去處理。
在比原中,前者是主要的方式,後者起到輔助作用。我們目前的文章中涉及到的都是前者,後者將在以後專門研究。
p.mconn.TrySend
當我們知道了p.mconn.TrySend
中的mconn
是什麼,並且在什麼時候初始化以後,下面就可以進入它的TrySend
方法了。
func (c *MConnection) TrySend(chID byte, msg interface{}) bool {
// ...
channel, ok := c.channelsIdx[chID]
// ...
ok = channel.trySendBytes(wire.BinaryBytes(msg))
if ok {
// Wake up sendRoutine if necessary
select {
case c.send <- struct{}{}:
default:
}
}
return ok
}
可以看到,它找到相應的channel後(在這裡應該是ProtocolReactor
對應的channel),呼叫channel的trySendBytes
方法。在傳送資料的時候,使用了github.com/tendermint/go-wire
庫,將msg
序列化為二進位制陣列。
func (ch *Channel) trySendBytes(bytes []byte) bool {
select {
case ch.sendQueue <- bytes:
atomic.AddInt32(&ch.sendQueueSize, 1)
return true
default:
return false
}
}
原來它是把要傳送的資料,放到了該channel對應的sendQueue
中,交由別人來傳送。具體是由誰來傳送,我們馬上要就找到它。
細心的同學會發現,Channel
除了trySendBytes
方法外,還有一個sendBytes
(在本文中沒有用上):
func (ch *Channel) sendBytes(bytes []byte) bool {
select {
case ch.sendQueue <- bytes:
atomic.AddInt32(&ch.sendQueueSize, 1)
return true
case <-time.After(defaultSendTimeout):
return false
}
}
它們兩個的區別是,前者嘗試把待傳送資料bytes
放入ch.sendQueue
時,如果能放進去,則返回true
,否則馬上失敗,返回false
,所以它是非阻塞的。而後者,如果放不進去(sendQueue
已滿,那邊還沒處理完),則等待defaultSendTimeout
(值為10
秒),然後才會失敗。另外,sendQueue
的容量預設為1
。
到這裡,我們其實已經知道比原是如何向其它節點請求區塊資料,以及何時把資訊傳送出去。
本想在本篇中就把真正傳送資料的程式碼也一起講了,但是發現它的邏輯也相當複雜,所以就另開一篇講吧。
再回到本文問題,再強調一下,我們前面說了,對於向peer請求區塊資料,有兩種情況:一種是簡單的不考慮分叉的,另一種是複雜的考慮分叉的。在本文只考慮了簡單的情況,在這種情況下,所謂的bestHeight
就是指的最高的那個區塊的高度,而在複雜情況下,它就不一定了。這就留待以後我們再詳細討論,本文的問題就算是回答完畢了。
相關文章
- 剝開比原看程式碼07:比原節點收到“請求區塊資料”的資訊後如何應答?
- 剝開比原看程式碼04:如何連上一個比原節點
- 剝開比原看程式碼06:比原是如何把請求區塊資料的資訊發出去的
- 剝開比原看程式碼02:比原啟動後去哪裡連線別的節點
- 剝開比原看程式碼08:比原的Dashboard是怎麼做出來的?
- 剝開比原看程式碼14:比原的挖礦流程是什麼樣的?
- 剝開比原看程式碼03:比原是如何監聽p2p埠的
- 剝開比原看程式碼11:比原是如何通過介面/create-account建立帳戶的
- 剝開比原看程式碼01:初始化時生成的配置檔案在哪兒
- 比原鏈CTO James | Go語言成為區塊鏈主流開發語言的四點理由Go區塊鏈
- Derek解讀Bytom原始碼-protobuf生成比原核心程式碼原始碼
- 2021年全球各世代節日購物方式佔比(附原資料表)
- 區塊鏈資料管理平臺開發,多節點聯盟區塊鏈搭建區塊鏈
- 區塊鏈與分散式資料庫的比較區塊鏈分散式資料庫
- 2019比原鏈全球開發者大會落幕:高舉開源旗幟,聚焦區塊鏈應用落地區塊鏈
- 2021年中國自主研發移動遊戲海外重點地區收入佔比(附原資料表) 遊戲
- 從模運算的角度看原碼和補碼
- 一比一還原axios原始碼(零)—— 概要iOS原始碼
- 2019年全球按地區劃分的專利申請佔比(附原資料表)
- 寫在凌晨3點–從世界金融史來看區塊鏈投資區塊鏈
- 2019年全球主要銅礦開採國及開採量佔比(附原資料表)
- 如何讓 MGR 不從 Primary 節點克隆資料?
- DesignPattern系列__05開閉原則
- 2021上半年全球獨角獸佔比(附原資料表)
- 2019年按地區劃分的專利授權數量佔比(附原資料表)
- 一比一還原axios原始碼(八)—— 其他功能iOS原始碼
- 一比一還原axios原始碼(四)—— Axios類iOS原始碼
- 一比一還原axios原始碼(六)—— 配置化iOS原始碼
- 2020年-2023年全球假期被剝奪比例(附原資料表)
- iPhone 12 Pro 的元件供應商按國家和地區分佈佔比(附原資料表) iPhone元件
- 區塊鏈課堂|讀懂公鏈學開發:深入淺出剖析比原鏈技術特性(線上免費)區塊鏈
- 2022年歐洲油氣市場收入佔比(附原資料表)
- 2021年全球智慧手機使用者佔比(附原資料表)
- 一比一還原axios原始碼(五)—— 攔截器iOS原始碼
- 外包區塊鏈開發比建立內部團隊更好區塊鏈
- 2020Q1-2021財年Q2 Salesforce主要地區營收佔比(附原資料表) Salesforce營收
- 2020與2024年亞太地區電子商務支付方式佔比(附原資料表)
- 從爆彈槍到不屈時代 ——《戰錘 40K:暗潮》無與倫比的細節還原