做者:freewind前端
比原項目倉庫:node
Github地址:https://github.com/Bytom/bytomwebpack
Gitee地址:https://gitee.com/BytomBlockc...git
在前面的幾篇文章中,咱們一直在研究如何與一個比原節點創建鏈接,而且從它那裏請求區塊數據。然而我很快就遇到了瓶頸。github
由於當我處理拿到的區塊數據時,發現我已經觸及到了比原鏈的核心,即區塊鏈的數據結構以及分叉的處理。若是不能徹底理解這一塊,就沒有辦法正確的處理區塊數據。然而它涉及的內容太多了,在短期以內把它理解透徹是一件很是困難的事情。golang
以前個人作法就好像我想了解一個城市,因而沿着一條路從外圍向市中心進發。前面一直很順利,但等到了市中心時,發現這裏人多路雜,有點迷失了。在這種狀況下,我以爲我應該暫停研究核心,而是從另一條路開始,由外向內再來一遍。由於在行進的過程當中,我能夠慢慢的積累更多的知識,讓本身處於學習區而非恐慌區。這條路的終點也將是觸及到核心,可是不深刻進去。這樣的話,等我多走了幾條路以後,積累的知識夠了,再研究核心就不會以爲迷茫了。web
因此本文原本是想去研究一下,當別的節點把區塊數據發給咱們以後,咱們應該怎麼處理,如今換成研究比原的Dashboard是怎麼作出來的。爲何選擇這個呢?由於它很是以一種很是直觀的方式,展現了比原向咱們提供的各類信息和功能。在本文中,咱們並不過多的講解它上面的功能,而是把關注點放在比原究竟是如何在代碼層面上實現了這樣的一個Dashboard。它上面的功能,將會在之後慢慢研究。npm
咱們今天的問題是「比原的Dashboard是怎麼作出來的」,可是這個問題有點大,而且不夠具體,因此咱們仍是跟之前同樣,先來把它細分一下:json
咱們下面開始一一探討。bootstrap
當咱們使用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
會從配置文件中取到相應的值:
type BaseConfig struct { // ... ApiAddress string `mapstructure:"api_addr"` // ... }
而後在啓動時,比原的web api以及dashboard會使用該地址,而且在瀏覽器中打開dashboard。
然而此處有一個奇怪的問題,就是不論這裏的值是什麼,瀏覽器老是打開http://localhost:9888
這個地址。爲何呢?由於它寫死在了代碼中。
在代碼中,http://localhost:9888
一共出如今了三個地方,一個是用來表示dashboard的訪問地址,位於node/node.go
中:
const ( webAddress = "http://127.0.0.1:9888" expireReservationsPeriod = time.Second maxNewBlockChSize = 1024 )
這裏的webAddress
,只在從代碼中打開瀏覽器顯示dashboard時使用:
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
下面:
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:
0.0.0.0:9998
,可是從瀏覽器或者命令行工具中去訪問時,須要使用一個具體的ip(而不是0.0.0.0
),不然某些功能會不正常。另外,在後面的代碼分析處會看到,除了配置文件中的這個地址,比原還會優先從環境變量中取得LISTEN
所對應的地址web api的地址。因此這裏須要更多的研究才能正確修復。sendbulktx
這個工具在將來將從bytom項目中獨立出去,因此代碼是重複的,若是是這樣的話,能夠接受。下面咱們快速過一遍比原的Dashboard提供了哪些信息和功能。因爲在本文中,咱們關注的重點不是這些具體的功能,因此會不會細究。另外,前面剛建立好的賬號裏,不少數據都是沒有的,爲了展現方便,我事先作了一些數據。
首先是密鑰:
這裏顯示了當前有幾個密鑰,其別名是什麼,而且顯示出來了主公鑰。咱們能夠點擊右上角的「新建」按鈕建立多個密鑰,可是這裏再也不展現。
賬戶:
資產:
默認只定義了BTM
這一種資產,能夠經過「新建」按鈕增長多種資產。
餘額:
看起來我仍是至關有錢的(惋惜不能用)。
交易:
展現了多筆交易,其實是在本機挖礦挖出來的。因爲挖礦出來的BTM是由系統直接轉到咱們的賬戶上的,因此也能夠看做是一種交易。
建立交易:
咱們也能夠像這樣本身建立交易,把咱們持有的某種資產(好比BTM)轉到另外一個地址。
未花費輸出:
簡單的理解就是與我相關的每一筆交易都被記錄下來,有輸入和輸出部分,其中的輸出可能又是另外一個交易的輸入。這裏顯示的是尚未花費掉的輸出(能夠根據它來計算我當前到底還剩下多少餘額)
查看核心狀態:
定義訪問控制:
備份和還原操做:
另外每一個頁面左側欄的下面,還有關於鏈接的鏈的類型(此處爲solonet
),以及同步狀況和與當前節點鏈接的其它節點數。
這裏展現的信息和功能咱們還不須要細究,可是這裏出現的名詞倒是要留意的,由於它們都是比原的核心概念。等咱們之後研究比原內部區塊鏈核心功能的時候,實際上都是圍繞着它們來的。這裏的每個概念,可能都須要一到多篇文章專門討論。
咱們在今天關注的是技術實現層面,下面咱們要開始進入代碼時間了。
首先讓咱們從比原節點啓動開始,一直找到啓動http服務的地方:
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 node n := node.NewNode(config) if _, err := n.Start(); err != nil { // .. }
func (n *Node) OnStart() error { // ... n.initAndstartApiServer() // ... }
很快找到了,initAndstartApiServer
:
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) }
能夠看到,該方法分紅了三部分:
API
對象。進去後會看到大量的與url相關的配置。LISTEN
對應的值,若是沒有的話,再使用config.toml
中指定的api_addr
值,做爲api服務的入口地址因爲2比較簡單,因此咱們下面將仔細分析1和3.
先找到1處所對應的api.NewAPI
方法:
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()
。這個方法有點長,把它分紅幾部分來說解:
func (a *API) buildHandler() { walletEnable := false m := http.NewServeMux()
看來http服務使用的是Go自帶的http
包。
向下是,當用戶的錢包功能沒有禁用的話,就會配置與錢包相關的各功能點(好比賬號、交易、密鑰等):
if a.wallet != nil { walletEnable = true m.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))
這樣的代碼,它是什麼意思呢?
/create-account
:該功能的路徑,好比對於這個,用戶須要在瀏覽器或者命令行中,使用地址http://localhost:9888/create-account
來訪問a.createAccount
:用於處理用戶的訪問,好比拿到用戶提供的數據,處理完後再返回某個數據給用戶,會在下面詳解jsonHandler
:是一箇中間層,把用戶發送的JSON數據轉成第2步handler須要的Go類型參數,或者把2返回的Go數據轉成JSON給用戶m.Handle(path, handler)
:用來把功能點路徑和相應的處理函數對應起來這裏先看第3步中的jsonHandler
的代碼:
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
的代碼:
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,而後在它外面包了一層又一層,添加上了更多的功能:
latencyHandler
:我目前還不能準確說出它的做用,留待之後補充maxBytesHandler
:防止用戶提交的數據過大,目前值約爲10MB
。對於除signer/sign-block
之外的url有效webAssetsHandler
:向用戶提供dashboard相關的前端頁面資源(好比網頁、圖片等等)。多是爲了性能和方便性方面的考慮,前端文件都通過混淆後,以字符串形式嵌入在dashboard/dashboard.go中,真正的代碼在另外一個項目中 https://github.com/Bytom/dashboard,咱們在後面會看一下gzip.Handler
:對http客戶端進行是否支持gzip
的檢測,而且在支持的狀況下,傳輸數據時使用gzip壓縮而後讓咱們回到主線,看看前面的NewAPI
中最後調用的api.initServer(config)
:
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 waitHandler var handler http.Handler coreHandler.wg.Add(1) mux := http.NewServeMux() mux.Handle("/", &coreHandler) handler = mux if config.Auth.Disable == false { handler = AuthHandler(handler, a.accessTokens) } handler = RedirectHandler(handler) secureheader.DefaultConfig.PermitClearLoopback = true secureheader.DefaultConfig.HTTPSRedirect = false secureheader.DefaultConfig.Next = handler a.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/17071 TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, } coreHandler.Set(a) }
這個方法在本文不適合細講,由於它更多的是涉及到http層面的一些東西,不是本文的重點。值得關注的地方是,方法建立了一個Go提供的http.Server
,把前面咱們辛苦配置好的handler塞進去,萬事俱備,只欠啓動。
下面就是啓動啦。咱們終於能夠回到最新的initAndstartApiServer
方法了,還記得它的第3塊內容嗎?主要就是調用了n.api.StartServer(*listenAddr)
:
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服務的,關於比原在安全性方面作了哪些事情,之後會有專門的分析。
比原的前端代碼是在另外一個獨立的項目中:https://github.com/Bytom/dash...
本文咱們並不去探討代碼細節,而僅僅去看一下它使用了哪些前端框架,有個大概印象便可。
經過https://github.com/Bytom/dashboard/blob/master/package.json咱們就能夠大概瞭解到,比原前端使用了:
npm
的Scripts
React
+ Redux
bootstrap
fetch-ponyfill
webpack
mocha
以Account相關的代碼爲例:
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是怎麼作出來的,有了一些基本的印象。剩下的,就是在之後,針對其中的功能進行細緻的研究。