做者:freewindnode
比原項目倉庫:react
Github地址:https://github.com/Bytom/bytomgit
Gitee地址:https://gitee.com/BytomBlockchain/bytomgithub
最開始我對於這個問題一直有個疑惑:區塊鏈是一個分佈式的網絡,那麼一個節點啓動後,它怎麼知道去哪裏找別的節點從而加入網絡呢?json
看到代碼以後,我才明白,原來在代碼中硬編碼了一些種子地址,這樣在啓動的時候,能夠先經過種子地址加入網絡。雖然整個網絡是分佈式的,可是最開始仍是須要必定的中心化。api
對於配置文件config.toml
,比原的代碼中硬編碼了配置文件內容:網絡
var 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
的地方。
首先,當咱們使用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
:
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()
:
func (n *Node) OnStart() error { // ... n.syncManager.Start() // ... }
能夠看到,咱們終於走到了調用了syncManager.Start()
的地方。
syncManager
中的處理而後就是在syncManager
內部的一些處理了。
它主要是除了從config.toml
中取得種子節點外,還須要把之前鏈接過並保存在本地的AddressBook.json
中的節點也拿出來鏈接,這樣就算預設的種子節點失敗了,也仍是有可能鏈接上網絡(部分解決了前面提到的中心化的擔心)。
syncManager.Start()
對應於:
func (sm *SyncManager) Start() { go sm.netStart() // ... }
其中sm.netStart()
,對應於:
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,這個方法對應的代碼位於:
func (sm *SyncManager) DialSeeds(seeds []string) error { return sm.sw.DialSeeds(sm.addrBook, seeds) }
實際上是是調用了sm.sw.DialSeeds
,而sm.sw
是指Switch
。這時能夠看到,有一個叫addrBook
的東西參與了進來,它保存了該結點以前成功鏈接過的節點地址,咱們這裏暫很少作討論。
Switch.DialSeeds
對應於:
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])
對應於:
func (sw *Switch) dialSeed(addr *NetAddress) { peer, err := sw.DialPeerWithAddress(addr, false) // ... }
sw.DialPeerWithAddress(addr, false)
又對應於:
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
對應於:
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
,加入了超時:
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
對應於:
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
包的調用,開始真正去鏈接這個種子節點了,到這裏,咱們能夠認爲這個問題解決了。