以太坊源碼分析(2)——以太坊APP對象

前言

從這一節開始,我將開始以太坊代碼全覆蓋講解,講解的流程是:

  • 以太坊程序入口
  • 基本框架
  • 以太坊協議
  • 發送一筆交易後發生了什麼
  • 啓動挖礦
  • 以太坊共識
  • 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() 方法啓動進程,完了等待進程退出就行了,代碼簡潔而漂亮。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章