geth以太坊源码分析-启动服务1
最近区块链这么火,出于好奇想看看源码实现,比较著名的就是比特币和以太坊了,前者是始祖,后者因为有智能合约的存在,所以大量不会写区块链底层的人可以利用以太坊实现自己的链,实际上,就是ICO, token了。
找了两者的官方源码:比特币 和 以太坊, 由于之前对C,C++源码看的多一些,所以这回趁这机会再了解一下go怎么实现以太坊的。
一、编译运行
这里只简单举个例子,想编译运行的Google一下就可以了。
首先下载源码: https://github.com/kulv2012/go-ethereum
编译: make
初始化一下创世区块:build/bin/geth --datadir ./datadir/ init conf/genesis.json
然后就可以最简单的方式运行,会自动开始挖矿: build/bin/geth --datadir ./datadir --mine
下面开始从开头了解一下以太坊geth是怎么启动服务的。
二、初始化服务
初始化全局变量app
geth程序的入口函数在cmd/geth/main.go 里面,包括main,以及初始化等。函数开头初始化程序的全局变量app, 这里有点绕的是geth使用了gopkg.in/urfave/cli.v1 这个包,来管理程序的启动,以及命令行解析,因此读起来不那么顺。下面先放上创建全局变量app的部分:
var ( // The app that holds all commands and flags. //新建一个全局的app结构,用来管理程序启动,命令行配置等 app = utils.NewApp(gitCommit, "the go-ethereum command line interface") // flags that configure the node nodeFlags = []cli.Flag{ utils.IdentityFlag, //---- } //utils函数如下: func NewApp(gitCommit, usage string) *cli.App { //其实创建的是这个结构:vendor/gopkg.in/urfave/cli.v1/app.go app := cli.NewApp() app.Name = filepath.Base(os.Args[0]) app.Author = "" //app.Authors = nil app.Email = "" app.Version = params.Version if len(gitCommit) >= 8 { app.Version += "-" + gitCommit[:8] } app.Usage = usage return app }
主要就是新建了一个cli.NewApp结构,代码在vendor/gopkg.in/urfave/cli.v1/app.go 里面,这个包是用来管理程序启动的,App包括 几个基本的接口: Command(), Run(), Setup(), 这些接口将来会在main里面被调用到。
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, } }
初始化子命令,程序入口函数设置
go语言默认会先调用init()函数,如果有的话,然后再调用main,geth的init做了很重要的工作:设置程序的子命令集,以及程序入口函数,调用准备函数app.Before,以及负责扫尾的app.After().
对于各项Command, app会解析参数比如如果参数有console, 那么会由app类去调度,调用consoleCommand对应的处理函数。
func init() { //先调用初始化函数,设置app的各个参数 // Initialize the CLI app and start Geth //geth的处理函数会在app.HandleAction 里面会调用 app.Action = geth //默认的操作,就是启动一个节点 , 如果有其他命令行参数,会调用到下面的Commands 里面去 app.HideVersion = true // we have a command to print the version app.Copyright = "Copyright 2013-2017 The go-ethereum Authors" //设置各个命令的处理类/函数,比如consoleCommand 最后调用到 localConsole //如果命令行参数里面有下面的指令,就会直接调用下面的Command.Run方法,而不调用默认的app.Action方法 app.Commands = []cli.Command{ // See chaincmd.go: initCommand, //初始化创世块 importCommand, exportCommand, copydbCommand, removedbCommand, dumpCommand, // See monitorcmd.go: monitorCommand, // See accountcmd.go: accountCommand, walletCommand, // See consolecmd.go: consoleCommand, //js命令行终端 attachCommand, javascriptCommand, // See misccmd.go: makecacheCommand, makedagCommand, versionCommand, bugCommand, licenseCommand, // See config.go dumpConfigCommand, } sort.Sort(cli.CommandsByName(app.Commands))
再简单看一下命令处理函数的结构,其实挺简单,最重要的是Action成员了,这个函数最后会被调用,也就是对应的处理函数了,比如localConsole, initGenesis 等。
consoleCommand = cli.Command{ Action: utils.MigrateFlags(localConsole), //这个函数最后会被调用,也就是对应的处理函数了 Name: "console", Usage: "Start an interactive JavaScript environment", Flags: append(append(append(nodeFlags, rpcFlags...), consoleFlags...), whisperFlags...), Category: "CONSOLE COMMANDS", Description: `---`, }
接下来需要初始化各个命令行参数的解析方式,以及设置Before, After 函数, 这两函数简单可以理解为,在app.Run()里面,会先后调用他们,来初始化或者收尾程序的部分工作。具体代码后面在main之后分析。
app.Flags = append(app.Flags, nodeFlags...) app.Flags = append(app.Flags, rpcFlags...) app.Flags = append(app.Flags, consoleFlags...) app.Flags = append(app.Flags, debug.Flags...) app.Flags = append(app.Flags, whisperFlags...) //before函数在app.Run的开始会先调用,也就是gopkg.in/urfave/cli.v1/app.go Run函数的前面 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 } //after函数在最后调用,app.Run 里面会设置defer function app.After = func(ctx *cli.Context) error { debug.Exit() console.Stdin.Close() // Resets terminal mode. return nil } }
main入口
由于使用了gopkg.in/urfave/cli.v1 包,所以main函数非常简单,直接调用app.Run即可。实际上对于geth来说,app接下来实际上调用的函数有2种情况:
- 如果是geth命令行启动,不带子命令,那么直接调用app.Action = geth()函数;
- 如果带有子命令比如build/bin/geth console 启动,那么会调用对应命令的Command.Action, 对于console来说就是调用的 localConsole()函数;
func main() { if err := app.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }
Run的具体逻辑在 gopkg.in/urfave/cli.v1/app.go 里面,这里不多说了,反正就是上面2种情况。下面来看看如果不带任何其他子命令参数,那么会调用geth()函数。
geth()命令行启动服务
默认情况下,如果不带子命令参数,那么app.Action = geth, 也就会调用geth()函数 来启动以太坊节点。
以太坊大部分子命令包括geth的逻辑,启动的时候都是先用makeFullNode()创建一个全节点的node节点结构。然后 startNode() 启动服务。
func geth(ctx *cli.Context) error { //默认情况下,如果不带子命令参数,那么app.Action = geth, 也就会调用geth()函数 来启动以太坊 //新建一个全节点服务,根据参数来 node := makeFullNode(ctx) //启动节点 startNode(ctx, node) node.Wait() return nil }
创建以太坊节点makeFullNode()和启动以太坊节点startNode()这两部分放到后一篇(geth以太坊源码学习-启动服务2)继续 。
近期评论