以太坊源码分析(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() 方法启动进程,完了等待进程退出就行了,代码简洁而漂亮。

你可能感兴趣的:(以太坊)