天天看點

剝開比原看代碼03:比原是如何監聽p2p端口的

作者:freewind

比原項目倉庫:

Github位址:https://github.com/Bytom/bytom

Gitee位址:https://gitee.com/BytomBlockchain/bytom

我們知道,在使用

bytomd init --chain_id mainnet/testnet/solonet

初始化比原的時候,它會根據給定的

chain_id

的不同,使用不同的端口(參看 config/toml.go#L29 ):

  1. mainnet

    (連接配接到主網): 

    46657

  2. testnet

    (連接配接到測試網): 

    46656

  3. solonet

    (本地單獨節點): 

    46658

對于我來說,由于隻需要對本地運作的一個比原節點進行分析,是以可以采用第3個

chain_id

,即

solonet

。這樣它啟動之後,不會與其它的節點主動連接配接,可以減少其它節點對于我們的幹擾。

是以在啟動的時候,我的指令是這樣的:

cd cmd/bytomd
./bytomd init --chain_id solonet
./bytomd node
      

它就會監聽

46658

端口,等待其它節點的連接配接。

連上看看

如果這時我們使用

telnet

來連接配接其

46658

端口,成功連接配接上之後,可以看到它會發給我們一些亂碼,大概如下:

$ telnet localhost 46658
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
ט�S��%�z?��_�端��݂���U[e
      

我們也許會好奇,它發給我們的到底是什麼?

但是這個問題留待下次回答,因為首先,比原節點必須能夠監聽這個端口,我們才能連上。是以這次我們的問題是:

比原在代碼中是如何監聽這個端口的?

端口已經寫在

config.toml

在前面,當我們使用

./bytomd init --chain_id solonet

初始化比原以後,比原會在本地的資料目錄中生成一個

config.toml

的配置檔案,内容大約如下:

# 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"
chain_id = "solonet"
[p2p]
laddr = "tcp://0.0.0.0:46658"
seeds = ""
      

其中

[p2p]

下面的

laddr

,就是該節點監聽的位址和端口。

對于

laddr = "tcp://0.0.0.0:46658"

,它是意思是:

  1. 使用的是

    tcp

    協定
  2. 監聽的ip是

    0.0.0.0

    ,是指監聽本機所有ip位址。這樣該節點既允許本地通路,也允許外部主機通路。如果你隻想讓它監聽某一個ip,手動修改該配置檔案即可
  3. 46658

    ,就是我們在這個問題中關注的端口了,它與該節點與其它節點互動資料使用的端口

比原在監聽這個端口的時候,并不是如我最開始預期的直接調用

net.Listen

監聽它。實際的過程要比這個複雜,因為比原設計了一個叫

Switch

的對象,用來統一管理與外界相關的事件,包括監聽、連接配接、發送消息等。而

Switch

這個對象,又是在

SyncManager

中建立的。

啟動直到進入

Switch

是以我們首先需要知道,比原在源代碼中是如何啟動,并且一步步走進了

Switch

的世界。

首先還是當我們

bytomd node

啟動比原時,對應的入口函數如下:

cmd/bytomd/main.go#L54
func main() {
    cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir()))
    cmd.Execute()
}
      

它又會根據傳入的

node

參數,運作下面的函數:

cmd/bytomd/commands/run_node.go#L41
func runNode(cmd *cobra.Command, args []string) error {
    // Create & start node
    n := node.NewNode(config)
    // ...
}
      

我們需要關注的是

node.NewNode(config)

函數,因為是在它裡面建立了

SyncManager

node/node.go#L59
func NewNode(config *cfg.Config) *Node {
    // ...
    syncManager, _ := netsync.NewSyncManager(config, chain, txPool, newBlockCh)
    // ...
}
      

在建立

SyncManager

的時候,又建立了

Switch

:

netsync/handle.go#L42
func NewSyncManager(config *cfg.Config, chain *core.Chain, txPool *core.TxPool, newBlockCh chan *bc.Hash) (*SyncManager, error) {
    // ...
    manager.sw = p2p.NewSwitch(config.P2P, trustHistoryDB)

    // ...
    protocolReactor := NewProtocolReactor(chain, txPool, manager.sw, manager.blockKeeper, manager.fetcher, manager.peers, manager.newPeerCh, manager.txSyncCh, manager.dropPeerCh)
    manager.sw.AddReactor("PROTOCOL", protocolReactor)

    // Create & add listener
    p, address := protocolAndAddress(manager.config.P2P.ListenAddress)
    l := p2p.NewDefaultListener(p, address, manager.config.P2P.SkipUPNP, nil)
    manager.sw.AddListener(l)

    // ...
}
      

這裡需要注意一下,上面建立的

protocolReactor

對象,是用來處理當有節點連接配接上端口後,雙方如何互動的事情。跟這次問題“監聽端口”沒有直接關系,但是這裡也可以注意一下。

然後又建立了一個

DefaultListener

對象,而監聽端口的動作,就是在它裡面發生的。Listener建立之後,将會添加到

manager.sw

(即

Switch

)中,用于在那邊進行外界資料與事件的互動。

監聽端口

NewDefaultListener

中做的事情比較多,是以我們把它分成幾塊說:

p2p/listener.go#L52
func NewDefaultListener(protocol string, lAddr string, skipUPNP bool, logger tlog.Logger) Listener {
    // Local listen IP & port
    lAddrIP, lAddrPort := splitHostPort(lAddr)

    // Create listener
    var listener net.Listener
    var err error
    for i := 0; i < tryListenSeconds; i++ {
        listener, err = net.Listen(protocol, lAddr)
        if err == nil {
            break
        } else if i < tryListenSeconds-1 {
            time.Sleep(time.Second * 1)
        }
    }
    if err != nil {
        cmn.PanicCrisis(err)
    }

    // ...
      

上面這部分就是真正監聽的代碼了。通過Go語言提供的

net.Listen

函數,監聽了指定的位址。另外,在監聽的時候,進行了多次嘗試,因為當一個剛剛被使用的端口被放開後,還需要一小段時間才能真正釋放,是以這裡需要多嘗試幾次。

tryListenSeconds

是一個常量,值為

5

,也就是說,大約會嘗試5秒鐘,要是都綁定不上,才會真正失敗,抛出錯誤。

後面省略了一些代碼,主要是用來擷取目前監聽的實際ip以及外網ip,并記錄在日志中。本想在這裡簡單講講,但是發現還有點麻煩,是以打算放在後面專開一個問題。

其實本次問題到這裡就已經結束了,因為已經完成了“監聽”。但是後面還有一些初始化操作,是為了讓比原可以跟連接配接上該端口的節點進行互動,也值得在這裡講講。

接着剛才的方法,最後的部分是:

dl := &DefaultListener{
        listener:    listener,
        intAddr:     intAddr,
        extAddr:     extAddr,
        connections: make(chan net.Conn, numBufferedConnections),
    }
    dl.BaseService = *cmn.NewBaseService(logger, "DefaultListener", dl)
    dl.Start() // Started upon construction
    return dl
}
      

需要注意的是

connections

,它是一個帶有緩沖的channel(

numBufferedConnections

值為

10

),用來存放連接配接上該端口的連接配接對象。這些操作将在後面的

dl.Start()

中執行。

dl.Start()

将調用

DefaultListener

對應的

OnStart

方法,如下:

p2p/listener.go#L114
func (l *DefaultListener) OnStart() error {
    l.BaseService.OnStart()
    go l.listenRoutine()
    return nil
}
      

其中的

l.listenRoutine

,就是執行前面所說的向

connections

 channel裡放入連接配接的函數:

p2p/listener.go#L126
func (l *DefaultListener) listenRoutine() {
    for {
        conn, err := l.listener.Accept()
        // ...
        l.connections <- conn
    }

    // Cleanup
    close(l.connections)

    // ...
}
      

Switch

SyncManager

啟動的時候會被啟動,在它的

OnStart

方法中,會拿到所有Listener(即監聽端口的對象)中

connections

channel中的連接配接,與它們互動。

https://github.com/freewind/bytom-v1.0.1/blob/master/p2p/switch.go#L498

func (sw *Switch) listenerRoutine(l Listener) {
    for {
        inConn, ok := <-l.Connections()
        if !ok {
            break
        }
        // ...

        err := sw.addPeerWithConnectionAndConfig(inConn, sw.peerConfig)
        // ...
    }
      

sw.addPeerWithConnectionAndConfig

就是與對應節點進行互動的邏輯所在,但是這已經超出了本次問題的範疇,下次再講。

到此為止,本次的問題,應該已經講清楚了。

繼續閱讀