剝開比原看程式碼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):
mainnet
(連線到主網):46657
testnet
(連線到測試網):46656
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"
,它是意思是:
- 使用的是
tcp
協議 - 監聽的ip是
0.0.0.0
,是指監聽本機所有ip地址。這樣該節點既允許本地訪問,也允許外部主機訪問。如果你只想讓它監聽某一個ip,手動修改該配置檔案即可 46658
,就是我們在這個問題中關注的埠了,它與該節點與其它節點互動資料使用的埠
比原在監聽這個埠的時候,並不是如我最開始預期的直接呼叫net.Listen
監聽它。實際的過程要比這個複雜,因為比原設計了一個叫Switch
的物件,用來統一管理與外界相關的事件,包括監聽、連線、傳送訊息等。而Switch
這個物件,又是在SyncManager
中建立的。
啟動直到進入Switch
所以我們首先需要知道,比原在原始碼中是如何啟動,並且一步步走進了Switch
的世界。
首先還是當我們bytomd node
啟動比原時,對應的入口函式如下:
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
:
func NewNode(config *cfg.Config) *Node {
// ...
syncManager, _ := netsync.NewSyncManager(config, chain, txPool, newBlockCh)
// ...
}
在建立SyncManager
的時候,又建立了Switch
:
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
中做的事情比較多,所以我們把它分成幾塊說:
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
方法,如下:
func (l *DefaultListener) OnStart() error {
l.BaseService.OnStart()
go l.listenRoutine()
return nil
}
其中的l.listenRoutine
,就是執行前面所說的向connections
channel裡放入連線的函式:
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
就是與對應節點進行互動的邏輯所在,但是這已經超出了本次問題的範疇,下次再講。
到此為止,本次的問題,應該已經講清楚了。
相關文章
- 剝開比原看程式碼11:比原是如何通過介面/create-account建立帳戶的
- 剝開比原看程式碼06:比原是如何把請求區塊資料的資訊發出去的
- 剝開比原看程式碼04:如何連上一個比原節點
- 剝開比原看程式碼05:如何從比原節點拿到區塊資料?
- 剝開比原看程式碼08:比原的Dashboard是怎麼做出來的?
- 剝開比原看程式碼14:比原的挖礦流程是什麼樣的?
- 剝開比原看程式碼07:比原節點收到“請求區塊資料”的資訊後如何應答?
- 剝開比原看程式碼02:比原啟動後去哪裡連線別的節點
- 剝開比原看程式碼01:初始化時生成的配置檔案在哪兒
- 如何更改oracle監聽器的埠Oracle
- 如何配置多個監聽器不同埠
- 使用java程式,監聽tcp協議埠JavaTCP協議
- UNIX環境設定監聽埠的指令碼指令碼
- aix用命令查監聽埠對應的程式AI
- 如何防止網路監聽與埠掃描
- Windows server 防火牆開放oracle監聽埠WindowsServer防火牆Oracle
- 初始化監聽埠
- 【Oracle】修改oracle監聽埠Oracle
- samba程序與監聽埠Samba
- Tomcat監聽443埠的方法Tomcat
- windows10怎麼檢視監聽埠_windows10檢視監聽埠的方法Windows
- iOS開發比較有用的程式碼段iOS
- Sqlserver重啟alwayson監聽埠SQLServer
- oracle 監聽配置多個埠Oracle
- memcached程式埠監控指令碼指令碼
- linux 子程式可以繼承父程式正在監聽的埠嗎? 如何子程式關閉了繼承的埠,父程式還能使用這個埠嗎?Linux繼承
- 12C RAC 修改監聽埠
- nginx 80埠監聽多個域名Nginx
- oracle rac scan監聽更改埠號Oracle
- Oracle 建立非1521埠監聽Oracle
- 【Oracle】修改scan監聽器埠號Oracle
- Oracle 11g 修改監聽埠Oracle
- Oracle資料庫修改LISTENER的監聽埠Oracle資料庫
- Derek解讀Bytom原始碼-protobuf生成比原核心程式碼原始碼
- 為什麼說無程式碼開發比低程式碼開發更好?
- 修改oracle監聽佔用的8080埠(轉)Oracle
- shell埠監聽異常郵箱告警
- Oracle 修改預設監聽埠故障分析Oracle