前言
從這一節開始,我將開始以太坊代碼全覆蓋講解,講解的流程是:
- 以太坊程序入口
- 基本框架
- 以太坊協議
- 發送一筆交易後發生了什麼
- 啓動挖礦
- 以太坊共識
- p2p 網絡
閱讀本系列文章,將默認讀者具備一定的程序基礎,並對 Go 語言特性有一定的瞭解。如有需要,請自行翻閱 Go 語言相關文檔。go 語言中文網點擊這裏
話不多說,現在開始。
一、以太坊程序入口
1.1 main 函數
以太坊的主程是編譯出來的 geth
程序運行時,程序入口跟其他的高級語言一樣,都是從 main 函數進入。
進入 main 函數路徑:go-ethereum/cmd/geth/main.go
,找到 main 函數。
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
從這裏,我們就已經進入了閱讀以太坊源碼的主流程中。main 函數很簡單,執行 app.Run
函數,如果函數返回錯誤,輸出錯誤,並退出。
熟悉其他主流語言的你看到這裏就會想了,main 中沒有 app 這個變量,難道它是全局變量?你沒有猜錯,不過 go 語言更復雜一點,它類似 C++/ Java / Python 語言的集合體,這些先不管。找到 app
對象。
var (
...
app = utils.NewApp(gitCommit, "the go-ethereum command line interface")
)
點進去進去看 utils.NewApp()
方法是怎麼創建 app
對象的,
// NewApp creates an app with sane defaults.
func NewApp(gitCommit, usage string) *cli.App {
app := cli.NewApp()
app.Name = filepath.Base(os.Args[0])
app.Author = ""
//app.Authors = nil
app.Email = ""
app.Version = params.VersionWithMeta
if len(gitCommit) >= 8 {
app.Version += "-" + gitCommit[:8]
}
app.Usage = usage
return app
}
我們看到,NewApp
方法調用的是 cli.NewApp()
方法,先創建一個cli.App
類型的指針對象,完了給該對象成員賦值,例如:指定 app 的名字是函數執行時的第一個參數,即 geth
,還有指定程序的版本號等等,最後函數返回。
我們繼續點進去看 cli.NewApp()
是怎麼創建一個 app
對象的
// NewApp creates a new cli Application with some reasonable defaults for Name,
// Usage, Version and Action.
func NewApp() *App {
return &App{
Name: filepath.Base(os.Args[0]),
HelpName: filepath.Base(os.Args[0]),
Usage: "A new cli application",
UsageText: "",
Version: "0.0.0",
BashComplete: DefaultAppComplete,
Action: helpCommand.Action,
Compiled: compileTime(),
Writer: os.Stdout,
}
}
函數直接返回的就是一個 App
結構體類型的指針,而 App
結構體在 gopkg.in/urfave/cli.v1
包中定義,它是 app
應用程序的一系列封裝,具體的用法我們不展開,在這裏爲了深入瞭解 Ethereum 以太坊協議,我們稍微看下 App
結構體的幾個主要成員。
1.2 App 結構體對象
// App is the main structure of a cli application. It is recommended that
// an app be created with the cli.NewApp() function
type App struct {
Name string
HelpName string
...
BashComplete BashCompleteFunc
Before BeforeFunc
After AfterFunc
Action interface{}
...
Writer io.Writer
ErrWriter io.Writer
Metadata map[string]interface{}
ExtraInfo func() map[string]string
CustomAppHelpTemplate string
didSetup bool
}
App
結構體定義如上,我們關注的是其中的4個重要成員:
BashComplete
:匿名函數類型,可以稱之爲函數執行器,定義了 bash 執行的行爲Before
:匿名函數類型,前置函數執行器,定義了App
對象的Action
執行之前的行爲After
:匿名函數類型,後置函數執行器,定義了App
對象的Action
執行之後的行爲Action
:接口類型,App
對象運行的真正執行者
我們知道,以太坊編譯出來的二進制文件 geth
可以通過命令行向程序傳參,BashComplete
定義瞭解析命令行參數的行爲方式。有興趣的同學可以關注。
App
對象的執行體由 Action
成員指向的匿名函數定義, Before
成員定義了應用啓動之前的初始化操作,而 After
成員定義了應用程序執行完成後的一些行爲,值得注意的是,即使程序 panic 了,該成員指向的函數也會執行。
1.3 Before & After
app.Before = func(ctx *cli.Context) error {
runtime.GOMAXPROCS(runtime.NumCPU())
if err := debug.Setup(ctx); err != nil {
return err
}
// Start system runtime metrics collection
go metrics.CollectProcessMetrics(3 * time.Second)
utils.SetupNetwork(ctx)
return nil
}
匿名函數賦值給 app.Before
,函數先調用 runtime.GOMAXPROCS() 方法設置最大處理器的數量,然後啓動 debug
相關初始化設置,例如:初始化 logging 設置,檢查是否啓用 trace 和 cpuprofile 標識,用來判斷是否啓動 profiling,tracing,pprof 服務等,主要是用來 debug Ethereum
程序性能的。
我們回頭來看一下,App.Before
成員指向的函數,其實是跟以太坊協議無關的,主要是做了個全局的初始化設置,是爲了跟蹤日誌,分析內存使用情況和 CPU 使用情況等。接着往下看。
app.After = func(ctx *cli.Context) error {
debug.Exit()
console.Stdin.Close() // Resets terminal mode.
return nil
}
app.After
成員用來停止 debug 服務,重置終端模式。其他的就沒做什麼了。
1.4 App.Action 成員
前面說到,Action 成員是個接口類型的,它代表着 application 運行的真正實體,我們看下它的賦值
func init() {
// Initialize the CLI app and start Geth
app.Action = geth
...
}
也就是說,Action 成員被初始化 geth 命令行應用程序,它是以太坊主進程函數。
1.5 運行一個應用 app.Run
整個以太坊服務的啓動從 main 函數中 app.Run()
開始。點進去:
// Run is the entry point to the cli app. Parses the arguments slice and routes
// to the proper flag/args combination
func (a *App) Run(arguments []string) (err error) {
a.Setup()
......
if a.After != nil {
defer func() {
if afterErr := a.After(context); afterErr != nil {
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if a.Before != nil {
beforeErr := a.Before(context)
if beforeErr != nil {
ShowAppHelp(context)
HandleExitCoder(beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
if a.Action == nil {
a.Action = helpCommand.Action
}
// Run default Action
err = HandleAction(a.Action, context)
HandleExitCoder(err)
return err
}
函數先給 app
對象設置初始值,完了做一些其他的事情,我們暫時不用去關心。
緊接着,判斷 app.After
是否爲空,如果不空,函數結束後執行 After
方法,因爲這是整個程序的唯一主入口,所以,就像前面說的,即使這後面哪裏 panic 了,仍然會執行。
然後判斷 app.Before
是否爲空,如果不空,就執行 Before
方法。
最後判斷 a.Action
是否爲空,如果爲空的話,賦個默認的 helpCommond 值,調用 handleAction
方法來運行 Action
,前面也說了,這裏的 action
非空,值爲 geth
。最終 handleAction
將 contex 作爲 geth
的參數並運行該函數。函數執行的結果返回 error
被 HandleExitCoder 函數解析,如果返回錯誤將錯誤結果輸出到終端。Run
函數返回 error 結束。
1.5 總結
至此,我們簡單的看了下,以太坊服務 geth
是在哪裏啓動的,在它啓動之前和啓動之後都做了什麼,以及以太坊應用程序怎麼 Run 。
如果你對 Go 語言有一定了解,你就知道,它其實是一個很精簡而高效的語言:它不支持無謂的開銷,任何聲明而未使用的變量都會在編譯期報錯,有很多類似 Python 一樣的特性。
而我感覺以太坊將這些特性發揮的很好,定義一些全局變量,定義好命令行參數對應解析和取值,然後一個 app.run()
方法啓動進程,完了等待進程退出就行了,代碼簡潔而漂亮。