最强以太坊源码解析-01-启动流程

一、main函数

文末有惊喜!

geth命令是整个以太坊的灵魂,这个命令的实现代码位置为:cmd/geth/main.go

func main() {
    	if err := app.Run(os.Args); err != nil {
    		fmt.Fprintln(os.Stderr, err)
    		os.Exit(1)
    	}
    }

在这段代码中,有一个app变量,这是一个全局的命令行变量,负责接收命令行参数,在init函数中赋值

在go语言中,每一个包可以有多个init,一般写一个就可以了,init会在这个包被调用时优先调用。

1. 第三方cli库(app变量)

geth命令解析命令行参数时,使用了一个第三方库,urfave/cli可以快速解析命令,实现复杂的功能

cli使用的是github上的开源库:https://github.com/urfave/cli

有demo,请自行查看

cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way.

2. app创建

定义全局变量

var (
	// Git SHA1 commit hash of the release (set via linker flags)
	gitCommit = ""
	// The app that holds all commands and flags.
	app = utils.NewApp(gitCommit, "the go-ethereum command line interface")
	// flags that configure the node
	nodeFlags = []cli.Flag{
		utils.IdentityFlag,
		utils.UnlockedAccountFlag,
		utils.PasswordFileFlag,
		utils.BootnodesFlag,
		utils.BootnodesV4Flag,
		//略...
	}

	rpcFlags = []cli.Flag{
		utils.RPCEnabledFlag,
		utils.RPCListenAddrFlag,
		utils.RPCPortFlag,
		utils.RPCApiFlag,
		utils.WSEnabledFlag,
		utils.WSListenAddrFlag,
		utils.WSPortFlag,
		utils.WSApiFlag,
		utils.WSAllowedOriginsFlag,
		utils.IPCDisabledFlag,
		utils.IPCPathFlag,
	}

	whisperFlags = []cli.Flag{
		utils.WhisperEnabledFlag,
		utils.WhisperMaxMessageSizeFlag,
		utils.WhisperMinPOWFlag,
		utils.WhisperRestrictConnectionBetweenLightClientsFlag,
	}

	metricsFlags = []cli.Flag{
		utils.MetricsEnableInfluxDBFlag,
		utils.MetricsInfluxDBEndpointFlag,
		utils.MetricsInfluxDBDatabaseFlag,
		utils.MetricsInfluxDBUsernameFlag,
		utils.MetricsInfluxDBPasswordFlag,
		utils.MetricsInfluxDBHostTagFlag,
	}
)

3. app初始化

在init函数中初始化,并指定执行geth函数

func init() {
	// Initialize the CLI app and start Geth
	app.Action = geth
	app.HideVersion = true // we have a command to print the version
	app.Copyright = "Copyright 2013-2018 The go-ethereum Authors"
	app.Commands = []cli.Command{
		// See chaincmd.go:
		initCommand,
		importCommand,
		exportCommand,
		importPreimagesCommand,
		exportPreimagesCommand,
		copydbCommand,
		removedbCommand,
		dumpCommand,
		// See monitorcmd.go:
		monitorCommand,
		// See accountcmd.go:
		accountCommand,
		walletCommand,
		// See consolecmd.go:
		consoleCommand,
		attachCommand,
		javascriptCommand,
		// See misccmd.go:
		makecacheCommand,
		makedagCommand,
		versionCommand,
		bugCommand,
		licenseCommand,
		// See config.go
		dumpConfigCommand,
	}
	sort.Sort(cli.CommandsByName(app.Commands))

	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...)
	app.Flags = append(app.Flags, metricsFlags...)

	app.Before = func(ctx *cli.Context) error {
		logdir := ""
		if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
			logdir = (&node.Config{DataDir: utils.MakeDataDir(ctx)}).ResolvePath("logs")
		}
		if err := debug.Setup(ctx, logdir); err != nil {
			return err
		}
		// Cap the cache allowance and tune the garbage collector
		var mem gosigar.Mem
		if err := mem.Get(); err == nil {
			allowance := int(mem.Total / 1024 / 1024 / 3)
			if cache := ctx.GlobalInt(utils.CacheFlag.Name); cache > allowance {
				log.Warn("Sanitizing cache to Go's GC limits", "provided", cache, "updated", allowance)
				ctx.GlobalSet(utils.CacheFlag.Name, strconv.Itoa(allowance))
			}
		}
		// Ensure Go's GC ignores the database cache for trigger percentage
		cache := ctx.GlobalInt(utils.CacheFlag.Name)
		gogc := math.Max(20, math.Min(100, 100/(float64(cache)/1024)))

		log.Debug("Sanitizing Go's GC trigger", "percent", int(gogc))
		godebug.SetGCPercent(int(gogc))

		// Start metrics export if enabled
		utils.SetupMetrics(ctx)

		// Start system runtime metrics collection
		go metrics.CollectProcessMetrics(3 * time.Second)

		return nil
	}

	app.After = func(ctx *cli.Context) error {
		debug.Exit()
		console.Stdin.Close() // Resets terminal mode.
		return nil
	}
}

在这段代码中,对geth的命令进行了设置,有一句代码非常重要

app.Action = geth

它表明app默认启动geth函数,也就是程序启动时执行的函数。

二、入口函数geth

cmd/geth/main.go->geth

1. 主要职责

  • 创建节点

  • 启动节点

  • 监听节点

2. geth代码

// geth is the main entry point into the system if no special subcommand is ran.
// It creates a default node based on the command line arguments and runs it in
// blocking mode, waiting for it to be shut down.
func geth(ctx *cli.Context) error {
	if args := ctx.Args(); len(args) > 0 {
		return fmt.Errorf("invalid command: %q", args[0])
	}
    //创建节点
	node := makeFullNode(ctx)
    //启动节点
	startNode(ctx, node)
    //监听终止节点
	node.Wait()
	return nil
}

三、创建节点

我们从创建节点开始说起,makeFullNode负责创建节点, 传入的ctx是命令行参数,解析参数时会用到。

这个函数会调用geth/config.go里面的同名函数:makeFullNode,该函数内部主要做两件事情:

  1. 创建配置节点
  2. 向Node中注册服务

代码为:

func makeFullNode(ctx *cli.Context) *node.Node {
    //创建Node
	stack, cfg := makeConfigNode(ctx)

    //注册服务
	utils.RegisterEthService(stack, &cfg.Eth)
    
	// 其他...
	return stack
}

1. 创建并配置节点

- 函数原型

func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) 

- 主要职责

依次加载三种配置文件:

  1. 默认数据
  2. 本地文件配置文件
  3. 命令行配置(传入的ctx)

- 加载默认配置

	// Load defaults.
	cfg := gethConfig{
		Eth:       eth.DefaultConfig, //挖矿相关
		Shh:       whisper.DefaultConfig, //p2p网络相关
		Node:      defaultNodeConfig(), //节点基本信息:服务端口8545,名字,安装目录等
		Dashboard: dashboard.DefaultConfig, //http显示??
	}

- 加载文件配置

	// Load config file.
	if file := ctx.GlobalString(configFileFlag.Name); file != "" {
		 //加载用户自己的配置文件,将file内容读取到cfg中
		if err := loadConfig(file, &cfg); err != nil {
			utils.Fatalf("%v", err)
		}
	}

- 加载命令行参数

	// Apply flags.命令行传入的参数,级别最高
	utils.SetNodeConfig(ctx, &cfg.Node)

	//New creates a new P2P node, ready for protocol registration. --duke
	//创建一个P2P节点
	stack, err := node.New(&cfg.Node)
	if err != nil {
		utils.Fatalf("Failed to create the protocol stack: %v", err)
	}
	utils.SetEthConfig(ctx, stack, &cfg.Eth)
	if ctx.GlobalIsSet(utils.EthStatsURLFlag.Name) {
		cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name)
	}

	utils.SetShhConfig(ctx, stack, &cfg.Shh)
	utils.SetDashboardConfig(ctx, &cfg.Dashboard)

最后返回节点实例和配置文件

	return stack, cfg

2. 向Node中注册服务

- 概述

创建好的Node并不完整,只是一个基本的网络节点实例,但是无法提供服务,所以需要注册服务环节,注册的服务类型有以下4种:

  1. RegisterEthService
  2. RegisterDashboardService
  3. RegisterShhService
  4. RegisterEthStatsService

- 代码

func makeFullNode(ctx *cli.Context) *node.Node {
	stack, cfg := makeConfigNode(ctx)

	utils.RegisterEthService(stack, &cfg.Eth)
	
    //其他三种注册代码省略...

	return stack
}

接下来详细说一下注册函数,我们只说第一种:RegisterEthService

- RegisterEthService

RegisterEthService adds an Ethereum client to the stack.

func RegisterEthService(stack *node.Node, cfg *eth.Config) 

- 主要职责

向stack中注册一个eth实例, 分为两种类型

  1. Ethereum: 支持FullSync模式, FastSync模式
  2. LightEthereum: 支持LightSync模式

以太坊有三种模式:
FullSync, FastSync LightSync, 位置:eth->downloader->modes.go:24行

在输入参数的cfg结构中有一个SyncMode字段,可以设置模式,默认是FullSync,用户可以从命令行指定

type Config struct {
	//...

	// Protocol options
	NetworkId uint64 // Network ID to use for selecting peers to connect to
	SyncMode  downloader.SyncMode  //<<<<------这里
	NoPruning bool

	// Whitelist of required block number -> hash values to accept
	Whitelist map[uint64]common.Hash `toml:"-"`
    //....
}

- 代码

请关注方法: stack.Register

func RegisterEthService(stack *node.Node, cfg *eth.Config) {
	var err error
	if cfg.SyncMode == downloader.LightSync {
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
            
			//Package les implements the Light Ethereum Subprotocol. --duke
			return les.New(ctx, cfg) <<---这个函数会返回一个LightEthereum
		})
	} else {
		//Register的参数是一个函数,返回一个Ethereum实例 --duke
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
            
			//fullNode 是Ethereum类型的,把Ethereum实例注册到stack中 --duke
			fullNode, err := eth.New(ctx, cfg) //<<---这个函数会返回一个 Ethereum
			if fullNode != nil && cfg.LightServ > 0 {
				ls, _ := les.NewLesServer(fullNode, cfg)
				fullNode.AddLesServer(ls)
			}
			return fullNode, err
		})
	}
	if err != nil {
		Fatalf("Failed to register the Ethereum service: %v", err)
	}
}

stack.Register的参数时一个函数类型,返回值类型为Service,原型如下:

type Service interface {
	// Protocols retrieves the P2P protocols the service wishes to start.
	Protocols() []p2p.Protocol

	// APIs retrieves the list of RPC descriptors the service provides
	APIs() []rpc.API

	// Start is called after all services have been constructed and the networking
	// layer was also initialized to spawn any goroutines required by the service.
	Start(server *p2p.Server) error

	// Stop terminates all goroutines belonging to the service, blocking until they
	// are all terminated.
	Stop() error
}

- 创建ETH实例

创建eth实例的过程非常复杂,而且极其重要,我们稍后再详细解读,先把整体流程梳理完毕。

至此,创建Node完毕,可以正常启动了!

四、启动节点

分为四步启动

1. 启动节点自身

	// Start up the node itself
	utils.StartNode(stack)

2. 解锁账户

	// Unlock any account specifically requested
	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)

	passwords := utils.MakePasswordList(ctx)
	unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
	for i, account := range unlocks {
		if trimmed := strings.TrimSpace(account); trimmed != "" {
			unlockAccount(ctx, ks, trimmed, i, passwords)
		}
	}

3. 订阅钱包监听事件

	// Register wallet event handlers to open and auto-derive wallets
	events := make(chan accounts.WalletEvent, 16)
	stack.AccountManager().Subscribe(events)

启动一个go程来进行监听处理

	go func() {
		// Create a chain state reader for self-derivation
		rpcClient, err := stack.Attach()
		if err != nil {
			utils.Fatalf("Failed to attach to self: %v", err)
		}
		stateReader := ethclient.NewClient(rpcClient)

		// Open any wallets already attached
		for _, wallet := range stack.AccountManager().Wallets() {
			if err := wallet.Open(""); err != nil {
				log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
			}
		}
		// Listen for wallet event till termination
		for event := range events {
			switch event.Kind {
			case accounts.WalletArrived:
				if err := event.Wallet.Open(""); err != nil {
					log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
				}
			case accounts.WalletOpened:
				status, _ := event.Wallet.Status()
				log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)

				derivationPath := accounts.DefaultBaseDerivationPath
				if event.Wallet.URL().Scheme == "ledger" {
					derivationPath = accounts.DefaultLedgerBaseDerivationPath
				}
				event.Wallet.SelfDerive(derivationPath, stateReader)

			case accounts.WalletDropped:
				log.Info("Old wallet dropped", "url", event.Wallet.URL())
				event.Wallet.Close()
			}
		}
	}()

4. 启动辅助服务-挖矿

	// Start auxiliary services if enabled
	if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
		// Mining only makes sense if a full Ethereum node is running
		if ctx.GlobalString(utils.SyncModeFlag.Name) == "light" {
			utils.Fatalf("Light clients do not support mining")
		}
		var ethereum *eth.Ethereum
		if err := stack.Service(ðereum); err != nil {
			utils.Fatalf("Ethereum service not running: %v", err)
		}
		// Set the gas price to the limits from the CLI and start mining
		gasprice := utils.GlobalBig(ctx, utils.MinerLegacyGasPriceFlag.Name)
		if ctx.IsSet(utils.MinerGasPriceFlag.Name) {
			gasprice = utils.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)
		}
		ethereum.TxPool().SetGasPrice(gasprice)

		threads := ctx.GlobalInt(utils.MinerLegacyThreadsFlag.Name)
		if ctx.GlobalIsSet(utils.MinerThreadsFlag.Name) {
			threads = ctx.GlobalInt(utils.MinerThreadsFlag.Name)
		}
		if err := ethereum.StartMining(threads); err != nil {   //<<<<---这里
			utils.Fatalf("Failed to start mining: %v", err)
		}
	}

至此,整个启动流程结束,为了能更好的梳理各个模块间的关系,我这里画了一个思维导图,供大家参考(空白部分是为了日后扩展):
欢迎转载,需要高清图片的同学请在评论区留言~
最强以太坊源码解析-01-启动流程_第1张图片

你可能感兴趣的:(以太坊源码分析,区块链源码,智能合约,区块链,Dapp,以太坊)