剝開比原看程式碼07:比原節點收到“請求區塊資料”的資訊後如何應答?
作者:freewind
比原專案倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockchain/bytom
在上一篇,我們知道了比原是如何把“請求區塊資料”的資訊BlockRequestMessage
傳送給peer節點的,那麼本文研究的重點就是,當peer節點收到了這個資訊,它將如何應答?
那麼這個問題如果細分的話,也可以分為三個小問題:
- 比原節點是如何收到對方發過來的資訊的?
- 收到
BlockRequestMessage
後,將會給對方傳送什麼樣的資訊? - 這個資訊是如何傳送出去的?
我們先從第一個小問題開始。
比原節點是如何接收對方發過來的資訊的?
如果我們在程式碼中搜尋BlockRequestMessage
,會發現只有在ProtocolReactor.Receive
方法中針對該資訊進行了應答。那麼問題的關鍵就是,比原是如何接收對方發過來的資訊,並且把它轉交給ProtocolReactor.Receive
的。
如果我們對前一篇《比原是如何把請求區塊資料的資訊發出去的》有印象的話,會記得比原在傳送資訊時,最後會把資訊寫入到MConnection.bufWriter
中;與之相應的,MConnection
還有一個bufReader
,用於讀取資料,它也是與net.Conn
繫結在一起的:
func NewMConnectionWithConfig(conn net.Conn, chDescs []*ChannelDescriptor, onReceive receiveCbFunc, onError errorCbFunc, config *MConnConfig) *MConnection {
mconn := &MConnection{
conn: conn,
bufReader: bufio.NewReaderSize(conn, minReadBufferSize),
bufWriter: bufio.NewWriterSize(conn, minWriteBufferSize),
(其中minReadBufferSize
的值為常量1024
)
所以,要讀取對方發來的資訊,一定會讀取bufReader
。經過簡單的搜尋,我們發現,它也是在MConnection.Start
中啟動的:
func (c *MConnection) OnStart() error {
// ...
go c.sendRoutine()
go c.recvRoutine()
// ...
}
其中的c.recvRoutine()
就是我們本次所關注的。它上面的c.sendRoutine
是用來傳送的,是前一篇文章中我們關注的重點。
繼續c.recvRoutine()
:
func (c *MConnection) recvRoutine() {
// ...
for {
c.recvMonitor.Limit(maxMsgPacketTotalSize, atomic.LoadInt64(&c.config.RecvRate), true)
// ...
pktType := wire.ReadByte(c.bufReader, &n, &err)
c.recvMonitor.Update(int(n))
// ...
switch pktType {
// ...
case packetTypeMsg:
pkt, n, err := msgPacket{}, int(0), error(nil)
wire.ReadBinaryPtr(&pkt, c.bufReader, maxMsgPacketTotalSize, &n, &err)
c.recvMonitor.Update(int(n))
// ...
channel, ok := c.channelsIdx[pkt.ChannelID]
// ...
msgBytes, err := channel.recvMsgPacket(pkt)
// ...
if msgBytes != nil {
// ...
c.onReceive(pkt.ChannelID, msgBytes)
}
// ...
}
}
// ...
}
經過簡化以後,這個方法分成了三塊內容:
- 第一塊就限制接收速率,以防止惡意結點突然傳送大量資料把節點撐死。跟傳送一樣,它的限制是
500K/s
- 第二塊是從
c.bufReader
中讀取出下一個資料包的型別。它的值目前有三個,兩個跟心跳有關:packetTypePing
和packetTypePong
,另一個表示是正常的資訊資料型別packetTypeMsg
,也是我們需要關注的 - 第三塊就是繼續從
c.bufReader
中讀取出完整的資料包,然後根據它的ChannelID
找到相應的channel去處理它。ChannelID
有兩個值,分別是BlockchainChannel
和PexChannel
,我們目前只需要關注前者即可,它對應的reactor是ProtocolReactor
。當最後呼叫c.onReceive(pkt.ChannelID, msgBytes)
時,讀取的二進位制資料msgBytes
就會被ProtocolReactor.Receive
處理
我們的重點是看第三塊內容。首先是channel.recvMsgPacket(pkt)
,即通道是怎麼從packet包裡讀取到相應的二進位制資料的呢?
func (ch *Channel) recvMsgPacket(packet msgPacket) ([]byte, error) {
// ...
ch.recving = append(ch.recving, packet.Bytes...)
if packet.EOF == byte(0x01) {
msgBytes := ch.recving
// ...
ch.recving = ch.recving[:0]
return msgBytes, nil
}
return nil, nil
}
這個方法我去掉了一些錯誤檢查和關於效能方面的註釋,有興趣的同學可以點接上方的原始碼檢視,這裡就忽略了。
這段程式碼主要是利用了一個叫recving
的通道,把packet
中持有的位元組陣列加到它後面,然後再判斷該packet是否代表整個資訊結束了,如果是的話,則把ch.recving
的內容完整返回,供呼叫者處理;否則的話,返回一個nil
,表示還沒拿完,暫時處理不了。在前一篇文章中關於傳送資料的地方可以與這裡對應,只不過傳送方要麻煩的多,需要三個通道sendQueue
、sending
和send
才能實現,這邊接收方就簡單了。
然後回到前面的方法MConnection.recvRoutine
,我們繼續看最後的c.onReceive
呼叫。這個onReceive
實際上是一個由別人賦值給該channel的一個函式,它位於MConnection
建立的地方:
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)
}
邏輯也比較簡單,就是當前面的c.onReceive(pkt.ChannelID, msgBytes)
呼叫時,它會根據傳入的chID
找到相應的Reactor
,然後執行其Receive
方法。對於本文來說,就會進入到ProtocolReactor.Receive
。
那我們繼續看ProtocolReactor.Receive
:
netsync/protocol_reactor.go#L179-L247
func (pr *ProtocolReactor) Receive(chID byte, src *p2p.Peer, msgBytes []byte) {
_, msg, err := DecodeMessage(msgBytes)
// ...
switch msg := msg.(type) {
case *BlockRequestMessage:
// ...
}
其中的DecodeMessage(...)
就是把傳入的二進位制資料反序列化成一個BlockchainMessage
物件,該物件是一個沒有任何內容的interface
,它有多種實現型別。我們在後面繼續對該物件進行判斷,如果它是BlockRequestMessage
型別的資訊,我們就會繼續做相應的處理。處理的程式碼我在這裡暫時省略了,因為它是屬於下一個小問題的,我們先不考慮。
好像不知不覺我們就把第一個小問題的後半部分差不多搞清楚了。那麼前半部分是什麼?我們在前面說,讀取bufReader
的程式碼的起點是在MConnection.Start
中,那麼前半部分就是:比原從啟動開始中,是在什麼情況下怎樣一步步走到MConnection.Start
的呢?
好在前半部分的問題我們在前一篇文章《比原是如何把請求區塊資料的資訊發出去的》中進行了專門的討論,這裡就不講了,有需要的話可以再過去看一下(可以先看最後“總結”那一小節)。
下面我們進入第二個小問題:
收到BlockRequestMessage
後,將會給對方傳送什麼樣的資訊?
這裡就是接著前面的ProtocolReactor.Receive
繼續向下講了。首先我們再貼一下它的較完整的程式碼:
netsync/protocol_reactor.go#L179-L247
func (pr *ProtocolReactor) Receive(chID byte, src *p2p.Peer, msgBytes []byte) {
_, msg, err := DecodeMessage(msgBytes)
// ...
switch msg := msg.(type) {
case *BlockRequestMessage:
var block *types.Block
var err error
if msg.Height != 0 {
block, err = pr.chain.GetBlockByHeight(msg.Height)
} else {
block, err = pr.chain.GetBlockByHash(msg.GetHash())
}
// ...
response, err := NewBlockResponseMessage(block)
// ...
src.TrySend(BlockchainChannel, struct{ BlockchainMessage }{response})
// ...
}
可以看到,邏輯還是比較簡單的,即根據對方發過來的BlockRequestMessage
中指定的height
或者hash
資訊,在本地的區塊鏈資料中找到相應的block,組成BlockResponseMessage
發過去就行了。
其中chain.GetBlockByHeight(...)
和chain.GetBlockByHash(...)
如果詳細說明的話,需要深刻理解區塊鏈資料在比原節點中是如何儲存的,我們在本文先不講,等到後面專門研究。
在這裡,我覺得我們只需要知道我們會查詢區塊資料並且構造出一個BlockResponseMessage
,再通過BlockchainChannel
這個通道傳送出去就可以了。
最後一句程式碼中呼叫了src.TrySend
方法,它是把資訊向對方peer傳送過去。(其中的src
就是指的對方peer)
那麼,它到底是怎麼傳送出去的呢?下面我們進入最後一個小問題:
這個BlockResponseMessage
資訊是如何傳送出去的?
我們先看看peer.TrySend
程式碼:
func (p *Peer) TrySend(chID byte, msg interface{}) bool {
if !p.IsRunning() {
return false
}
return p.mconn.TrySend(chID, msg)
}
它在內部將會呼叫MConnection.TrySend
方法,其中chID
是BlockchainChannel
,也就是它對應的Reactor是ProtocolReactor
。
再接著就是我們熟悉的MConnection.TrySend
,由於它在前一篇文章中進行了全面的講解,在本文就不提了,如果需要可以過去翻看一下。
那麼今天的問題就算是解決啦。
到這裡,我們總算能夠完整的理解清楚,當我們向一個比原節點請求“區塊資料”,我們這邊需要怎麼做,對方節點又需要怎麼做了。
相關文章
- 剝開比原看程式碼05:如何從比原節點拿到區塊資料?
- 剝開比原看程式碼06:比原是如何把請求區塊資料的資訊發出去的
- 剝開比原看程式碼04:如何連上一個比原節點
- 剝開比原看程式碼02:比原啟動後去哪裡連線別的節點
- 剝開比原看程式碼08:比原的Dashboard是怎麼做出來的?
- 剝開比原看程式碼14:比原的挖礦流程是什麼樣的?
- 剝開比原看程式碼03:比原是如何監聽p2p埠的
- 剝開比原看程式碼11:比原是如何通過介面/create-account建立帳戶的
- 剝開比原看程式碼01:初始化時生成的配置檔案在哪兒
- 一比一還原axios原始碼(二)—— 請求響應處理iOS原始碼
- 2019年全球按地區劃分的專利申請佔比(附原資料表)
- bitcoin: 透過 rpc 請求節點資料RPC
- 區塊鏈與分散式資料庫的比較區塊鏈分散式資料庫
- 一比一還原axios原始碼(一)—— 發起第一個請求iOS原始碼
- 2021年全球各世代節日購物方式佔比(附原資料表)
- 比原鏈CTO James | Go語言成為區塊鏈主流開發語言的四點理由Go區塊鏈
- python收到MQTT訊息後再發http請求PythonMQQTHTTP
- 區塊鏈資料管理平臺開發,多節點聯盟區塊鏈搭建區塊鏈
- iPhone 12 Pro 的元件供應商按國家和地區分佈佔比(附原資料表) iPhone元件
- Ajax請求後臺資料
- 使用RestTemplate,顯示請求資訊,響應資訊REST
- 2019比原鏈全球開發者大會落幕:高舉開源旗幟,聚焦區塊鏈應用落地區塊鏈
- 2019年全球按收入劃分的專利申請數量佔比(附原資料表)
- Derek解讀Bytom原始碼-protobuf生成比原核心程式碼原始碼
- Nodejs教程07:處理接收到的POST資料NodeJS
- 如何避免舊請求的資料覆蓋掉最新請求
- IBM申請區塊鏈專利,通過節點資料確保交易合規性IBM區塊鏈
- 2019年按地區劃分的專利授權數量佔比(附原資料表)
- vue使用axios請求後端資料VueiOS後端
- 前端請求後端資料的三種方式!前端後端
- 2021年中國自主研發移動遊戲海外重點地區收入佔比(附原資料表) 遊戲
- 答疑 - SAP OData 框架處理 Metadata 後設資料請求的實現細節框架
- 非同步請求xhr、ajax、axios與fetch的區別比較非同步iOS
- 2019年全球主要銅礦開採國及開採量佔比(附原資料表)
- 2021上半年全球獨角獸佔比(附原資料表)
- [求助] 使用 python 第三方庫 pycryptodome 進行 RSA 加密得到的結果,發起請求,介面接收到請求後,開發 Java 程式碼私鑰解密後請求引數會亂碼Python加密Java解密
- 2020年-2023年全球假期被剝奪比例(附原資料表)
- Spring系列 SpringMVC的請求與資料響應SpringMVC