作者:freewind
比原項目倉庫:
Github位址:https://github.com/Bytom/bytom
Gitee位址:https://gitee.com/BytomBlockchain/bytom
比原啟動後去哪裡連接配接别的節點
最開始我對于這個問題一直有個疑惑:區塊鍊是一個分布式的網絡,那麼一個節點啟動後,它怎麼知道去哪裡找别的節點進而加入網絡呢?
看到代碼之後,我才明白,原來在代碼中寫死了一些種子位址,這樣在啟動的時候,可以先通過種子位址加入網絡。雖然整個網絡是分布式的,但是最開始還是需要一定的中心化。
預編碼内容
對于配置檔案
config.toml
,比原的代碼中寫死了配置檔案内容:
config/toml.go#L22-L45var defaultConfigTmpl = `# This is a TOML config file.
# For more information, see https://github.com/toml-lang/toml
fast_sync = true
db_backend = "leveldb"
api_addr = "0.0.0.0:9888"
`
var mainNetConfigTmpl = `chain_id = "mainnet"
[p2p]
laddr = "tcp://0.0.0.0:46657"
seeds = "45.79.213.28:46657,198.74.61.131:46657,212.111.41.245:46657,47.100.214.154:46657,47.100.109.199:46657,47.100.105.165:46657"
`
var testNetConfigTmpl = `chain_id = "testnet"
[p2p]
laddr = "tcp://0.0.0.0:46656"
seeds = "47.96.42.1:46656,172.104.224.219:46656,45.118.132.164:46656"
`
var soloNetConfigTmpl = `chain_id = "solonet"
[p2p]
laddr = "tcp://0.0.0.0:46658"
seeds = ""
`
可以看出,對于不同的
chain_id
,預設的種子是不同的。
當然,如果我們自己知道某些節點的位址,也可以在初始化生成
config.toml
後,手動修改該檔案添加進去。
啟動 syncManager
syncManager
那麼,比原在代碼中是使用這些種子位址并連接配接它們的呢?關鍵在于,連接配接的代碼位于
SyncManager
中,是以我們要找到啟動
syncManager
的地方。
首先,當我們使用
bytomd node
啟動後,下面的函數将被調用:
cmd/bytomd/commands/run_node.go#L41func runNode(cmd *cobra.Command, args []string) error {
// Create & start node
n := node.NewNode(config)
if _, err := n.Start(); err != nil {
// ...
}
// ...
}
這裡調用了
n.Start
,其中的
Start
方法,來自于
Node
所嵌入的
cmn.BaseService
:
node/node.go#L39type Node struct {
cmn.BaseService
// ...
}
是以
n.Start
對應的是下面這個方法:
vendor/github.com/tendermint/tmlibs/common/service.go#L97func (bs *BaseService) Start() (bool, error) {
// ...
err := bs.impl.OnStart()
// ...
}
在這裡,由于
bs.impl
對應于
Node
,是以将繼續調用
Node.OnStart()
:
node/node.go#L169func (n *Node) OnStart() error {
// ...
n.syncManager.Start()
// ...
}
可以看到,我們終于走到了調用了
syncManager.Start()
syncManager
中的處理
syncManager
然後就是在
syncManager
内部的一些處理了。
它主要是除了從
config.toml
中取得種子節點外,還需要把以前連接配接過并儲存在本地的
AddressBook.json
中的節點也拿出來連接配接,這樣就算預設的種子節點失敗了,也還是有可能連接配接上網絡(部分解決了前面提到的中心化的擔憂)。
syncManager.Start()
對應于:
netsync/handle.go#L141func (sm *SyncManager) Start() {
go sm.netStart()
// ...
}
其中
sm.netStart()
,對應于:
netsync/handle.go#L121func (sm *SyncManager) netStart() error {
// ...
// If seeds exist, add them to the address book and dial out
if sm.config.P2P.Seeds != "" {
// dial out
seeds := strings.Split(sm.config.P2P.Seeds, ",")
if err := sm.DialSeeds(seeds); err != nil {
return err
}
}
// ...
}
其中的
sm.config.P2P.Seeds
就對應于
config.toml
中的
seeds
。關于這兩者是怎麼對應起來的,會在後面文章中詳解。
緊接着,再通過
sm.DialSeeds(seeds)
去連接配接這些seed,這個方法對應的代碼位于:
netsync/handle.go#L229func (sm *SyncManager) DialSeeds(seeds []string) error {
return sm.sw.DialSeeds(sm.addrBook, seeds)
}
其實是是調用了
sm.sw.DialSeeds
,而
sm.sw
是指
Switch
。這時可以看到,有一個叫
addrBook
的東西參與了進來,它儲存了該結點之前成功連接配接過的節點位址,我們這裡暫不多做讨論。
Switch.DialSeeds
p2p/switch.go#L311 func (sw *Switch) DialSeeds(addrBook *AddrBook, seeds []string) error {
// ...
perm := rand.Perm(len(netAddrs))
for i := 0; i < len(perm)/2; i++ {
j := perm[i]
sw.dialSeed(netAddrs[j])
}
// ...
}
這裡引入了随機數,是為了将發起連接配接的順序打亂,這樣可以讓每個種子都獲得公平的連接配接機會。
sw.dialSeed(netAddrs[j])
p2p/switch.go#L342 func (sw *Switch) dialSeed(addr *NetAddress) {
peer, err := sw.DialPeerWithAddress(addr, false)
// ...
}
sw.DialPeerWithAddress(addr, false)
又對應于:
p2p/switch.go#L351func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) (*Peer, error) {
// ...
log.WithField("address", addr).Info("Dialing peer")
peer, err := newOutboundPeerWithConfig(addr, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, sw.peerConfig)
// ...
}
persistent
參數如果是
true
的話,表明這個peer比較重要,在某些情況下如果斷開連接配接後,還會嘗試重連。如果
persistent
為
false
的,就沒有這個待遇。
newOutboundPeerWithConfig
p2p/peer.go#L69 func newOutboundPeerWithConfig(addr *NetAddress, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) {
conn, err := dial(addr, config)
// ...
}
繼續
dial
,加入了逾時:
p2p/peer.go#L284func dial(addr *NetAddress, config *PeerConfig) (net.Conn, error) {
conn, err := addr.DialTimeout(config.DialTimeout * time.Second)
if err != nil {
return nil, err
}
return conn, nil
}
addr.DialTimeout
p2p/netaddress.go#L141 func (na *NetAddress) DialTimeout(timeout time.Duration) (net.Conn, error) {
conn, err := net.DialTimeout("tcp", na.String(), timeout)
if err != nil {
return nil, err
}
return conn, nil
}
終于到了
net
包的調用,開始真正去連接配接這個種子節點了,到這裡,我們可以認為這個問題解決了。