作者:freewind
比原項目倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockc...
最開始我對於這個問題一直有個疑惑:區塊鏈是一個分布式的網路,那麼一個節點啟動後,它怎麼知道去哪裡找別的節點從而加入網路呢?
看到代碼之後,我才明白,原來在代碼中寫入程式碼了一些種子地址,這樣在啟動的時候,可以先通過種子地址加入網路。雖然整個網路是分布式的,但是最開始還是需要一定的中心化。
預編碼內容
對於設定檔config.toml,比原的代碼中寫入程式碼了設定檔內容:
config/toml.go#L22-L45
var defaultConfigTmpl = `# This is a TOML config file.# For more information, see https://github.com/toml-lang/tomlfast_sync = truedb_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的地方。
首先,當我們使用bytomd node啟動後,下面的函數將被調用:
cmd/bytomd/commands/run_node.go#L41
func 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#L39
type Node struct { cmn.BaseService // ...}
所以n.Start對應的是下面這個方法:
vendor/github.com/tendermint/tmlibs/common/service.go#L97
func (bs *BaseService) Start() (bool, error) { // ... err := bs.impl.OnStart() // ...}
在這裡,由於bs.impl對應於Node,所以將繼續調用Node.OnStart():
node/node.go#L169
func (n *Node) OnStart() error { // ... n.syncManager.Start() // ...}
可以看到,我們終於走到了調用了syncManager.Start()的地方。
syncManager中的處理
然後就是在syncManager內部的一些處理了。
它主要是除了從config.toml中取得種子節點外,還需要把以前串連過並儲存在本地的AddressBook.json中的節點也拿出來串連,這樣就算預設的種子節點失敗了,也還是有可能串連上網路(部分解決了前面提到的中心化的擔憂)。
syncManager.Start()對應於:
netsync/handle.go#L141
func (sm *SyncManager) Start() { go sm.netStart() // ...}
其中sm.netStart(),對應於:
netsync/handle.go#L121
func (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#L229
func (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#L351
func (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#L284
func 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包的調用,開始真正去串連這個種子節點了,到這裡,我們可以認為這個問題解決了。