从这一节开始,我将开始以太坊代码全覆盖讲解,讲解的流程是:
阅读本系列文章,将默认读者具备一定的程序基础,并对 Go 语言特性有一定的了解。如有需要,请自行翻阅 Go 语言相关文档。go 语言中文网点击这里
话不多说,现在开始。
以太坊的主程是编译出来的 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
结构体的几个主要成员。
// 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 了,该成员指向的函数也会执行。
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 服务,重置终端模式。其他的就没做什么了。
前面说到,Action 成员是个接口类型的,它代表着 application 运行的真正实体,我们看下它的赋值
func init() {
// Initialize the CLI app and start Geth
app.Action = geth
...
}
也就是说,Action 成员被初始化 geth 命令行应用程序,它是以太坊主进程函数。
整个以太坊服务的启动从 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 结束。
至此,我们简单的看了下,以太坊服务 geth
是在哪里启动的,在它启动之前和启动之后都做了什么,以及以太坊应用程序怎么 Run 。
如果你对 Go 语言有一定了解,你就知道,它其实是一个很精简而高效的语言:它不支持无谓的开销,任何声明而未使用的变量都会在编译期报错,有很多类似 Python 一样的特性。
而我感觉以太坊将这些特性发挥的很好,定义一些全局变量,定义好命令行参数对应解析和取值,然后一个 app.run()
方法启动进程,完了等待进程退出就行了,代码简洁而漂亮。