首页 > geth, GO > geth以太坊源码分析-启动服务1

geth以太坊源码分析-启动服务1

2018年6月23日 发表评论 阅读评论 29605次阅读    

最近区块链这么火,出于好奇想看看源码实现,比较著名的就是比特币和以太坊了,前者是始祖,后者因为有智能合约的存在,所以大量不会写区块链底层的人可以利用以太坊实现自己的链,实际上,就是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种情况:

  1. 如果是geth命令行启动,不带子命令,那么直接调用app.Action = geth()函数;
  2. 如果带有子命令比如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)继续 。

Share
分类: geth, GO 标签: , , ,

注意: 评论者允许使用'@user空格'的方式将自己的评论通知另外评论者。例如, ABC是本文的评论者之一,则使用'@ABC '(不包括单引号)将会自动将您的评论发送给ABC。使用'@all ',将会将评论发送给之前所有其它评论者。请务必注意user必须和评论者名相匹配(大小写一致)。