剥开比原看代码(八):比原的Dashboard是怎么做出来的?
作者:freewind
比原項目倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockchain/bytom
在前面的幾篇文章中,我們一直在研究如何與一個比原節點建立連接,并且從它那里請求區塊數據。然而我很快就遇到了瓶頸。
因為當我處理拿到的區塊數據時,發現我已經觸及到了比原鏈的核心,即區塊鏈的數據結構以及分叉的處理。如果不能完全理解這一塊,就沒有辦法正確的處理區塊數據。然而它涉及的內容太多了,在短時間之內把它理解透徹是一件非常困難的事情。
之前我的做法就好像我想了解一個城市,于是沿著一條路從外圍向市中心進發。前面一直很順利,但等到了市中心時,發現這里人多路雜,有點迷失了。在這種情況下,我覺得我應該暫停研究核心,而是從另外一條路開始,由外向內再來一遍。因為在行進的過程中,我可以慢慢的積累更多的知識,讓自己處于學習區而非恐慌區。這條路的終點也將是觸及到核心,但是不深入進去。這樣的話,等我多走了幾條路之后,積累的知識夠了,再研究核心就不會覺得迷茫了。
所以本文本來是想去研究一下,當別的節點把區塊數據發給我們之后,我們應該怎么處理,現在換成研究比原的Dashboard是怎么做出來的。為什么選擇這個呢?因為它非常以一種非常直觀的方式,展示了比原向我們提供的各種信息和功能。在本文中,我們并不過多的講解它上面的功能,而是把關注點放在比原到底是如何在代碼層面上實現了這樣的一個Dashboard。它上面的功能,將會在以后慢慢研究。
我們今天的問題是“比原的Dashboard是怎么做出來的”,但是這個問題有點大,并且不夠具體,所以我們還是跟以前一樣,先來把它細分一下:
我們下面開始一一探討。
我們怎樣在比原中啟用Dashboard功能?
當我們使用bytomd node啟動比原節點的時候,不需要任何配置,它就會自動啟用Dashboard功能,并且會在瀏覽器中打開頁面,非常方便。
如果是第一次運行,還沒有創建過帳戶,它會提示我們創建一個帳戶及相關的私鑰:
我們可以通過填寫帳戶別名、密鑰別名和相應的密碼來創建,或者點擊下面的"Restore wallet"來恢復之前的帳號(如果之前備份過的話):
點擊"Register"后,就會創建成功,并進入管理頁面:
注意它的地址是:http://127.0.0.1:9888/dashboard
如果我們查看配置文件config.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 = "" 復制代碼注意其中的api_addr,就是dashboard以及web-api的地址。比原在啟動之后,其BaseConfig.ApiAddress會從配置文件中取到相應的值:
config/config.go#L41-L85
type BaseConfig struct {// ...ApiAddress string `mapstructure:"api_addr"`// ... } 復制代碼然后在啟動時,比原的web api以及dashboard會使用該地址,并且在瀏覽器中打開dashboard。
然而此處有一個奇怪的問題,就是不論這里的值是什么,瀏覽器總是打開http://localhost:9888這個地址。為什么呢?因為它寫死在了代碼中。
在代碼中,http://localhost:9888一共出現在了三個地方,一個是用來表示dashboard的訪問地址,位于node/node.go中:
node/node.go#L33-L37
const (webAddress = "http://127.0.0.1:9888"expireReservationsPeriod = time.SecondmaxNewBlockChSize = 1024 ) 復制代碼這里的webAddress,只在從代碼中打開瀏覽器顯示dashboard時使用:
node/node.go#L153-L159
func lanchWebBroser() {log.Info("Launching System Browser with :", webAddress)if err := browser.Open(webAddress); err != nil {log.Error(err.Error())return} } 復制代碼比原通過"github.com/toqueteos/webbrowser"這個第三方的庫,可以在節點啟動的時候,調用系統默認的瀏覽器,并打開指定的網址,方便了用戶。(注意這段代碼中有不少錯別字,比如lanch、broser,已在后續版本中修正了)
另一個地方,是用于bytomcli這個命令行工具的,只是奇怪的是它放在了util/util.go下面:
util/util.go#L26-L28
var (coreURL = env.String("BYTOM_URL", "http://localhost:9888") ) 復制代碼為什么說它是屬于bytomcli的呢?因為這個coreURL最終被用在util包下的一個ClientCall(...)函數中,用于從代碼中向指定的web api發送請求,并使用其回復信息。但是這個方法在bytomcli所在的包使用。如果是這樣的話,coreURL及相關的函數,應該移到bytomcli包里才對。
第三個地方,跟第二個非常像,但是位于tools/sendbulktx/core/util.go中,它是用于另一個命令行工具sendbulktx的:
tools/sendbulktx/core/util.go#L26-L28
var (coreURL = env.String("BYTOM_URL", "http://localhost:9888") ) 復制代碼一模一樣,對吧。其實不光是這里,還有一堆相關的方法和函數,也是一模一樣的,一看就是跟第二處互相復制過來的。
關于這里的問題,我提了兩個issue:
-
dashboard和web api的地址寫在配置文件config.toml中,但是同時寫死在代碼中:這里在實現上的確是有一定難度的,原因是在配置文件中,寫的是0.0.0.0:9998,但是從瀏覽器或者命令行工具中去訪問時,需要使用一個具體的ip(而不是0.0.0.0),否則某些功能會不正常。另外,在后面的代碼分析處會看到,除了配置文件中的這個地址,比原還會優先從環境變量中取得LISTEN所對應的地址web api的地址。所以這里需要更多的研究才能正確修復。
-
與讀取webapi相關的代碼出現大量重復:官方解釋說sendbulktx這個工具在未來將從bytom項目中獨立出去,所以代碼是重復的,如果是這樣的話,可以接受。
Dashboard中提供了哪些信息和功能?
下面我們快速過一遍比原的Dashboard提供了哪些信息和功能。由于在本文中,我們關注的重點不是這些具體的功能,所以會不會細究。另外,前面剛創建好的帳號里,很多數據都是沒有的,為了展示方便,我事先做了一些數據。
首先是密鑰:
這里顯示了當前有幾個密鑰,其別名是什么,并且顯示出來了主公鑰。我們可以點擊右上角的“新建”按鈕創建多個密鑰,但是這里不再展示。
帳戶:
資產:默認只定義了BTM這一種資產,可以通過“新建”按鈕增加多種資產。
余額:
看起來我還是相當有錢的(可惜不能用)。
交易:
展示了多筆交易,實際上是在本機挖礦挖出來的。由于挖礦出來的BTM是由系統直接轉到我們的帳戶上的,所以也可以看作是一種交易。
創建交易:
我們也可以像這樣自己創建交易,把我們持有的某種資產(比如BTM)轉到另一個地址。
未花費輸出:
簡單的理解就是與我相關的每一筆交易都被記錄下來,有輸入和輸出部分,其中的輸出可能又是另一個交易的輸入。這里顯示的是還沒有花費掉的輸出(可以根據它來計算我當前到底還剩下多少余額)
查看核心狀態:
定義訪問控制:
備份和還原操作:

另外每個頁面左側欄的下面,還有關于連接的鏈的類型(此處為solonet),以及同步情況和與當前節點連接的其它節點數。
這里展示的信息和功能我們還不需要細究,但是這里出現的名詞卻是要留意的,因為它們都是比原的核心概念。等我們以后研究比原內部區塊鏈核心功能的時候,實際上都是圍繞著它們來的。這里的每一個概念,可能都需要一到多篇文章專門討論。
我們在今天關注的是技術實現層面,下面我們要開始進入代碼時間了。
比原是如何實現了http服務器?
首先讓我們從比原節點啟動開始,一直找到啟動http服務的地方:
cmd/bytomd/main.go#L54-L57
func main() {cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir()))cmd.Execute() } 復制代碼cmd/bytomd/commands/run_node.go#L41-L54
func runNode(cmd *cobra.Command, args []string) error {// Create & start noden := node.NewNode(config)if _, err := n.Start(); err != nil {// .. } 復制代碼node/node.go#L169-L180
func (n *Node) OnStart() error {// ...n.initAndstartApiServer()// ... } 復制代碼很快找到了,initAndstartApiServer:
node/node.go#L161-L167
func (n *Node) initAndstartApiServer() {// 1.n.api = api.NewAPI(n.syncManager, n.wallet, n.txfeed, n.cpuMiner, n.miningPool, n.chain, n.config, n.accessTokens)// 2. listenAddr := env.String("LISTEN", n.config.ApiAddress)env.Parse()// 3.n.api.StartServer(*listenAddr) } 復制代碼可以看到,該方法分成了三部分:
由于2比較簡單,所以我們下面將仔細分析1和3.
先找到1處所對應的api.NewAPI方法:
api/api.go#L143-L157
func NewAPI(sync *netsync.SyncManager, wallet *wallet.Wallet, txfeeds *txfeed.Tracker, cpuMiner *cpuminer.CPUMiner, miningPool *miningpool.MiningPool, chain *protocol.Chain, config *cfg.Config, token *accesstoken.CredentialStore) *API {api := &API{sync: sync,wallet: wallet,chain: chain,accessTokens: token,txFeedTracker: txfeeds,cpuMiner: cpuMiner,miningPool: miningPool,}api.buildHandler()api.initServer(config)return api } 復制代碼它主要就是把傳進來的各參數拿住,供后面使用。然后就是api.buildHandler來配置各個功能點的路徑和處理函數,以及用api.initServer來初始化服務。
進入api.buildHandler()。這個方法有點長,把它分成幾部分來講解:
api/api.go#L164-L244
func (a *API) buildHandler() {walletEnable := falsem := http.NewServeMux() 復制代碼看來http服務使用的是Go自帶的http包。
向下是,當用戶的錢包功能沒有禁用的話,就會配置與錢包相關的各功能點(比如帳號、交易、密鑰等):
if a.wallet != nil {walletEnable = truem.Handle("/create-account", jsonHandler(a.createAccount))m.Handle("/list-accounts", jsonHandler(a.listAccounts))m.Handle("/delete-account", jsonHandler(a.deleteAccount))m.Handle("/create-account-receiver", jsonHandler(a.createAccountReceiver))m.Handle("/list-addresses", jsonHandler(a.listAddresses))m.Handle("/validate-address", jsonHandler(a.validateAddress))m.Handle("/create-asset", jsonHandler(a.createAsset))m.Handle("/update-asset-alias", jsonHandler(a.updateAssetAlias))m.Handle("/get-asset", jsonHandler(a.getAsset))m.Handle("/list-assets", jsonHandler(a.listAssets))m.Handle("/create-key", jsonHandler(a.pseudohsmCreateKey))m.Handle("/list-keys", jsonHandler(a.pseudohsmListKeys))m.Handle("/delete-key", jsonHandler(a.pseudohsmDeleteKey))m.Handle("/reset-key-password", jsonHandler(a.pseudohsmResetPassword))m.Handle("/build-transaction", jsonHandler(a.build))m.Handle("/sign-transaction", jsonHandler(a.pseudohsmSignTemplates))m.Handle("/submit-transaction", jsonHandler(a.submit))m.Handle("/estimate-transaction-gas", jsonHandler(a.estimateTxGas))m.Handle("/get-transaction", jsonHandler(a.getTransaction))m.Handle("/list-transactions", jsonHandler(a.listTransactions))m.Handle("/list-balances", jsonHandler(a.listBalances))m.Handle("/list-unspent-outputs", jsonHandler(a.listUnspentOutputs))m.Handle("/backup-wallet", jsonHandler(a.backupWalletImage))m.Handle("/restore-wallet", jsonHandler(a.restoreWalletImage))} else {log.Warn("Please enable wallet")} 復制代碼錢包功能默認是啟用的,用戶如何才能禁用它呢?方法是在配置文件config.toml中,加上這一節代碼:
[wallet] disable = true 復制代碼在前面的代碼中,在配置功能點時,使用了大量的m.Handle("/create-account", jsonHandler(a.createAccount))這樣的代碼,它是什么意思呢?
這里先看第3步中的jsonHandler的代碼:
api/api.go#L259-L265
func jsonHandler(f interface{}) http.Handler {h, err := httpjson.Handler(f, errorFormatter.Write)if err != nil {panic(err)}return h } 復制代碼它里面用到了httpjson,它是比原代碼中提供的一個包,位于net/http/httpjson 。它的功能主要是為了在http訪問與Go的函數之間增加了一層轉換。通常用戶通過http與api交互的時候,發送和接收的都是JSON數據,而我們在第2步的handler中定義的是Go函數,通過httpjson,可以在兩者之間自動轉換,使得我們在寫Go代碼的時候,不需要考慮JSON以及http協議相關的問題。相應的,為了與jsonhttp配合使用,第2步中的handler在格式上也會有一些要求,詳情可參見這里的詳細注釋:net/http/httpjson/doc.go#L3-L40 。由于httpjson所涉及的代碼還比較多,這里就不詳述,以后有機會專開一篇。
然后我們再看第2步的a.createAccount的代碼:
api/accounts.go#L16-L30
func (a *API) createAccount(ctx context.Context, ins struct {RootXPubs []chainkd.XPub `json:"root_xpubs"`Quorum int `json:"quorum"`Alias string `json:"alias"` }) Response {acc, err := a.wallet.AccountMgr.Create(ctx, ins.RootXPubs, ins.Quorum, ins.Alias)if err != nil {return NewErrorResponse(err)}annotatedAccount := account.Annotated(acc)log.WithField("account ID", annotatedAccount.ID).Info("Created account")return NewSuccessResponse(annotatedAccount) } 復制代碼這個函數的內容我們在這里不細究,需要注意的反而是它的格式,因為前面說了,它需要跟jsonHandler配合使用。格式的要求大概就是,第一個參數是Context,第二個參數是可以從JSON數據轉換過來的參數,返回值是一個Response以及一個Error,但是這四個又全部是可選的。
讓我們回到api.buildHandler(),繼續往下:
m.Handle("/", alwaysError(errors.New("not Found")))m.Handle("/error", jsonHandler(a.walletError))m.Handle("/create-access-token", jsonHandler(a.createAccessToken))m.Handle("/list-access-tokens", jsonHandler(a.listAccessTokens))m.Handle("/delete-access-token", jsonHandler(a.deleteAccessToken))m.Handle("/check-access-token", jsonHandler(a.checkAccessToken))m.Handle("/create-transaction-feed", jsonHandler(a.createTxFeed))m.Handle("/get-transaction-feed", jsonHandler(a.getTxFeed))m.Handle("/update-transaction-feed", jsonHandler(a.updateTxFeed))m.Handle("/delete-transaction-feed", jsonHandler(a.deleteTxFeed))m.Handle("/list-transaction-feeds", jsonHandler(a.listTxFeeds))m.Handle("/get-unconfirmed-transaction", jsonHandler(a.getUnconfirmedTx))m.Handle("/list-unconfirmed-transactions", jsonHandler(a.listUnconfirmedTxs))m.Handle("/get-block-hash", jsonHandler(a.getBestBlockHash))m.Handle("/get-block-header", jsonHandler(a.getBlockHeader))m.Handle("/get-block", jsonHandler(a.getBlock))m.Handle("/get-block-count", jsonHandler(a.getBlockCount))m.Handle("/get-difficulty", jsonHandler(a.getDifficulty))m.Handle("/get-hash-rate", jsonHandler(a.getHashRate))m.Handle("/is-mining", jsonHandler(a.isMining))m.Handle("/set-mining", jsonHandler(a.setMining))m.Handle("/get-work", jsonHandler(a.getWork))m.Handle("/submit-work", jsonHandler(a.submitWork))m.Handle("/gas-rate", jsonHandler(a.gasRate))m.Handle("/net-info", jsonHandler(a.getNetInfo)) 復制代碼可以看到還是各種功能的定義,主要是跟區塊數據、挖礦、訪問控制等相關的功能,這里就不詳述了。
再繼續:
handler := latencyHandler(m, walletEnable)handler = maxBytesHandler(handler)handler = webAssetsHandler(handler)handler = gzip.Handler{Handler: handler}a.handler = handler } 復制代碼這里是把前面定義的功能點配置包成了一個handler,然后在它外面包了一層又一層,添加上了更多的功能:
然后讓我們回到主線,看看前面的NewAPI中最后調用的api.initServer(config):
api/api.go#L89-L122
func (a *API) initServer(config *cfg.Config) {// The waitHandler accepts incoming requests, but blocks until its underlying// handler is set, when the second phase is complete.var coreHandler waitHandlervar handler http.HandlercoreHandler.wg.Add(1)mux := http.NewServeMux()mux.Handle("/", &coreHandler)handler = muxif config.Auth.Disable == false {handler = AuthHandler(handler, a.accessTokens)}handler = RedirectHandler(handler)secureheader.DefaultConfig.PermitClearLoopback = truesecureheader.DefaultConfig.HTTPSRedirect = falsesecureheader.DefaultConfig.Next = handlera.server = &http.Server{// Note: we should not set TLSConfig here;// we took care of TLS with the listener in maybeUseTLS.Handler: secureheader.DefaultConfig,ReadTimeout: httpReadTimeout,WriteTimeout: httpWriteTimeout,// Disable HTTP/2 for now until the Go implementation is more stable.// https://github.com/golang/go/issues/16450// https://github.com/golang/go/issues/17071TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},}coreHandler.Set(a) } 復制代碼這個方法在本文不適合細講,因為它更多的是涉及到http層面的一些東西,不是本文的重點。值得關注的地方是,方法創建了一個Go提供的http.Server,把前面我們辛苦配置好的handler塞進去,萬事俱備,只欠啟動。
下面就是啟動啦。我們終于可以回到最新的initAndstartApiServer方法了,還記得它的第3塊內容嗎?主要就是調用了n.api.StartServer(*listenAddr):
api/api.go#L125-L140
func (a *API) StartServer(address string) {// ...listener, err := net.Listen("tcp", address)// ...go func() {if err := a.server.Serve(listener); err != nil {log.WithField("error", errors.Wrap(err, "Serve")).Error("Rpc server")}}() } 復制代碼這塊比較簡單,就是使用Go的net.Listen來監聽傳入的web api地址,得到相應的listener之后,把它傳給我們在前面創建的http.Server的Serve方法,就大功告成了。
這一塊代碼分析寫得十分痛苦,主要原因是它的web api這里幾乎涉及到了所有比原提供的功能,很龐雜。還有不少跟http協議相關的東西。同時,因為暴露出了接口,這里就容易出現安全風險,所以代碼里面還有不少涉及到用戶輸入、安全檢查等。這些東西當然是非常重要的,但是從代碼閱讀的角度上來講又難免枯燥,除非我們就是為了研究安全性。
本文的任務主要是研究比原是如何提供http服務的,關于比原在安全性方面做了哪些事情,以后會有專門的分析。
Dashboard使用了什么樣的前端框架?
比原的前端代碼是在另一個獨立的項目中:https://github.com/Bytom/dashboard
本文我們并不去探討代碼細節,而僅僅去看一下它使用了哪些前端框架,有個大概印象即可。
通過github.com/Bytom/dashb…我們就可以大概了解到,比原前端使用了:
Dashboard上面的數據,是以什么樣的方式從后臺拿到的?
以Account相關的代碼為例:
src/sdk/api/accounts.js#L16
const accountsAPI = (client) => {return {create: (params, cb) => shared.create(client, '/create-account', params, {cb, skipArray: true}),createBatch: (params, cb) => shared.createBatch(client, '/create-account', params, {cb}),// ...listAddresses: (accountId) => shared.query(client, 'accounts', '/list-addresses', {account_id: accountId}),} } 復制代碼這些函數主要是通過fetch-ponyfill庫中提供的方法,向向前面使用go創建的web api接口發送http請求,并且拿到相應的回復數據。而它們又將在React組件中被調用,拿回來的數據用于填充頁面。
同樣,更細節的內容在本文就不講啦。
終于,經過這一大篇的分析,我覺得我對于比原的Dashboard是怎么做出來的,有了一些基本的印象。剩下的,就是在以后,針對其中的功能進行細致的研究。
總結
以上是生活随笔為你收集整理的剥开比原看代码(八):比原的Dashboard是怎么做出来的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 正则一文通
- 下一篇: 刚刚在用的React Native的照相