lotus是Lotus專案中的全節點命令列二進位制。啟動全節點的命令是:
lotus daemon
daemon程序的功能是全節點的完整功能,概述一下主要包括如下
透過P2P網路發現其他節點,並進行通訊
從其它全節點獲取區塊資料,並對每一次區塊的訊息及區塊打包資訊進行校驗並儲存到本地儲存
為其它全節點提供區塊同步的服務
錢包管理服務,包括轉帳、訊息簽名等
RPC API服務
本文就daemon程序的啟動過程進行分析
Command Action
lotus的命令列採用https://gopkg.in/urfave/cli.v2" target="_blank" rel="nofollow noreferrer noopener">https://gopkg.in/urfave/cli.v2,以下是daemon命令的Action程式碼邏輯:
var DaemonCmd = &cli.Command{ Name: "daemon", Usage: "Start a lotus daemon process", Flags: []cli.Flag{ ... }, Action: func(cctx *cli.Context) error { ... ... // 建立FsRepo,即lotus的Repo r, err := repo.NewFS(cctx.String("repo")) // 初始化Repo,例如建立初始檔案等 if err := r.Init(repo.FullNode); err != nil && err != repo.ErrRepoExists { return xerrors.Errorf("repo init error: %w", err) } // 獲取引數檔案 if err := paramfetch.GetParams(build.ParametersJson(), 0); err != nil { return xerrors.Errorf("fetching proof parameters: %w", err) } // 如果是Genesis啟動方式,則載入Genesis檔案 genesis := node.Options() if len(genBytes) > 0 { genesis = node.Override(new(modules.Genesis), modules.LoadGenesis(genBytes)) } ... var api api.FullNode // 建立全節點, 從第二個引數起都是Option stop, err := node.New(ctx, node.FullAPI(&api), // 上線, 多數初始化邏輯在此實現 node.Online(), // Repo相關的初始化 node.Repo(r), genesis, node.ApplyIf(func(s *node.Settings) bool { return cctx.IsSet("api") }, node.Override(node.SetApiEndpointKey, func(lr repo.LockedRepo) error { apima, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/" + cctx.String("api")) if err != nil { return err } return lr.SetAPIEndpoint(apima) })), node.ApplyIf(func(s *node.Settings) bool { return !cctx.Bool("bootstrap") }, node.Unset(node.RunPeerMgrKey), node.Unset(new(*peermgr.PeerMgr)), ), ) ... // 根據repo內的配置建立APIEndpoint, 即網路監聽物件 endpoint, err := r.APIEndpoint() ... // 啟動RPC服務 return serveRPC(api, stop, endpoint) },
啟動框架邏輯
上述程式碼雖省略了一些關鍵程式碼,但粗略一看程式碼邏輯很少啊,肯定是有大量的邏輯藏在哪些語句中!是的,就是node.Online這句!在進入node.Online之前,我們先看一下node.New是什麼東西:
func New(ctx context.Context, opts ...Option)(StopFunc, error){ settings := Settings{ modules: map[interface{}]fx.Option{}, invokes:make([]fx.Option, _nInvokes), nodeType: repo.FullNode,} // apply module options in the right orderif err :=Options(Options(defaults()...),Options(opts...))(&settings); err != nil {return nil, xerrors.Errorf("applying node options failed: %w", err)} // gather constructors for fx.Options ctors :=make([]fx.Option,0,len(settings.modules))for _, opt := range settings.modules { ctors =append(ctors, opt)} // fill holes in invokes for use in fx.Optionsfor i, opt := range settings.invokes {if opt == nil { settings.invokes[i]= fx.Options()}} app := fx.New( fx.Options(ctors...), fx.Options(settings.invokes...), fx.NopLogger,) // TODO: we probably should have a for Closing signal// on this context, and implement closing logic through lifecycles// correctlyif err := app.Start(ctx); err != nil {// comment fx.NopLogger few lines above for easier debuggingreturn nil, xerrors.Errorf("starting node: %w", err)} return app.Stop, nil }
估計多數人看到以上程式碼有點蒙了,又是Settings,又是Options(複數)和Option(單數),而且更費解的是有的Option/Options帶了fx包名,有的則沒有帶fx包名。好的,我們先看一眼Settings的完整程式碼:
type Settings struct { // modules is a map of constructors for DI // // In most cases the index will be a reflect. Type of element returned by // the constructor, but for some its // the return type should be (or the constructor returns fx group) modules map[interface{}]fx.Option // invokes are separate from modules as they canre unfamiliar with this style, see // https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html. type Option interface { apply(*App) } type optionFunc func(*App) func (f optionFunc) apply(app *App) { f(app) }
它是在第三方包中定義,程式碼檔案為"go.uber.org/[email protected]/app.go"。 Option定義是很超級簡單,但仍是不容易理解。原作者已經估計到多數人的感受,在註釋中加了一條連結:https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html" target="_blank" rel="nofollow noreferrer noopener">https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html,有興趣者自行去詳細瞭解一下。
但Option只是[go.uber.org\/fx]這個包中很小一部分,fx包是uber公司開源的Golang的依賴注入框架。lotus的程式碼也是採用該框架,所以對這個框架的理解是非常有必要的。
依賴注入(Dependency Injection)是一種設計模式,最出名的應用是Java的Spring框架,不瞭解的讀取自行搜尋一下。關於這個包的使用方法可以參https://pkg.go.dev/go.uber.org/fx" target="_blank" rel="nofollow noreferrer noopener">https://pkg.go.dev/go.uber.org/fx
這裡回頭繼續介紹Option, 這裡先做一個簡要的解釋:Option可以理解成是影響App行為的一個選項,如果想讓選項生效就執行apply方法,而App則可理解為整個程式程序,而Options也就是多個Option的組合,可以想象到Options裡所有Option應該是按順序執行的,否則可能會導致結果不是我們所預期的。
fx.Option是fx包內定義的Option,是一個介面(見以上程式碼),而Option(不帶fx.字首)是lotus內部定義的:
type Option func(*Settings) error
繼續看一下Option相關的幾個函式:
func ApplyIf(check func(s *Settings) bool, opts ...Option) Option { return func(s *Settings) error { if check(s) { return Options(opts...)(s) } return nil } } func If(b bool, opts ...Option) Option { return ApplyIf(func(s *Settings) bool { return b }, opts...) }
以上是根據判斷條件應用Option,回頭看一下前面daemon中的node.New語句的程式碼,多個option可組合成Options(實際是一個函式)的引數,返回型別又是Option,而且某些Option還可以用ApplyIf指定條件應用。感覺象是。。。另一種語言?這個大膽的猜想是正確的,這就是領域描述語言(DSL)的至簡版本,有興趣的讀者可以上網搜尋瞭解一下概念。
到此,為什麼lotus在fx.Option基礎上又定義了一個Option,原因就比較清楚了:fx.Option的功能比較簡單,缺少類似象ApplyIf這樣的邏輯,所以Option是lotus裡對fx.Option的功能增強。另外fx.Option對應是的App(即fx.App),但在lotus中使用的是Settings結構。
接著,再看一下方法:
// Override option changes constructor for a given type func Override(typ, constructor interface{}) Option { return func(s *Settings) error { if i, ok := typ.(invoke); ok { s.invokes[i] = fx.Invoke(constructor) return nil } if c, ok := typ.(special); ok { s.modules[c] = fx.Provide(constructor) return nil } ctor := as(constructor, typ) rt := reflect.TypeOf(typ).Elem() s.modules[rt] = fx.Provide(ctor) return nil } }
以上也有些費解,這裡解釋一下用途即可:Override函式輸入引數是兩個引數:typ, constructor,typ就是型別,constructor就是建構函式了。輸出是一個Option,本質就是一個func。func裡面做的事情是將typ和constructor的對應關係儲存起來。那為什麼叫Override呢,原因是typ型別的物件本身預設用New即可構造,現在預設的構造行為需要被過載。
至此,大家可以感覺到這些與DI有關,這裡我們大致這樣理解就可以了:如果需要定義一個新的型別物件(例如一個介面),我們就定義好建構函式(第一個輸出引數必須是該型別),然後將型別和建構函式作為輸入引數去呼叫Override即可,後續至於什麼時候去生成這個型別的物件,就是DI容器去操心的事情。
現在回過頭再去理解前面的node.New的程式碼邏輯,應該就比較好理解一些了,這裡我就僅大致介紹一下:開始是將所有的Option進行apply,然後將settings.modules裡儲存的構造方法傳遞給fx的DI框架,再呼叫fx裡的app.Start。
後續
前面講了lotus啟動過程的框架邏輯,後續將繼續講解lotus啟動過程中與業務邏輯相關的部分。