go-ethereum中geth启动流程分析

geth是go-ethereum的一个重要工具,根据官方文档的说明,他是进入Ethereum网络的入口,也就是Ethereum的一个节点程序。如果直接输入geth运行,则会默认启动一个全节点,并以fast模式同步数据。我们从这个命令入手,学习一下geth的启动流程。

main.go

通过对geth命令的分析,我们了解到geth是一个CLI程序,程序的定义主要在go-ethereum\cmd\geth\main.go里面,在init函数中定义了大量子命令,用于实现下图所示的功能:

geth子命令

但是如果我们不指定任何子命令,直接输入geth并运行,则会执行app.Action指定的内容:

// go-ethereum\cmd\geth\main.go
app.Action = geth

func geth(ctx *cli.Context) error {
	if args := ctx.Args(); len(args) > 0 { 
		return fmt.Errorf("invalid command: %q", args[0])
	}
	node := makeFullNode(ctx)
	defer node.Close()
	startNode(ctx, node)
	node.Wait()
	return nil
}

geth的逻辑很简单,执行geth时不能有其他未解析的命令。之后调用makeFullNode创建一个全节点,然后启动节点,最后调用wait阻塞。

创建节点

// go-ethereum\cmd\geth\config.go
func makeFullNode(ctx *cli.Context) *node.Node {
	stack, cfg := makeConfigNode(ctx)
	if ctx.GlobalIsSet(utils.ConstantinopleOverrideFlag.Name) {
		cfg.Eth.ConstantinopleOverride = new(big.Int).SetUint64(ctx.GlobalUint64(utils.ConstantinopleOverrideFlag.Name))
	}
	
	utils.RegisterEthService(stack, &cfg.Eth) 
	
	if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
		utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
	}
	
	shhEnabled := enableWhisper(ctx)

	shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name)

	if shhEnabled || shhAutoEnabled {

		if ctx.GlobalIsSet(utils.WhisperMaxMessageSizeFlag.Name) {
			cfg.Shh.MaxMessageSize = uint32(ctx.Int(utils.WhisperMaxMessageSizeFlag.Name))
		}
		if ctx.GlobalIsSet(utils.WhisperMinPOWFlag.Name) {
			cfg.Shh.MinimumAcceptedPOW = ctx.Float64(utils.WhisperMinPOWFlag.Name)
		}
		if ctx.GlobalIsSet(utils.WhisperRestrictConnectionBetweenLightClientsFlag.Name) {
			cfg.Shh.RestrictConnectionBetweenLightClients = true
		}
		utils.RegisterShhService(stack, &cfg.Shh)
	}

	if ctx.GlobalIsSet(utils.GraphQLEnabledFlag.Name) {
		if err := graphql.RegisterGraphQLService(stack, cfg.Node.GraphQLEndpoint(), cfg.Node.GraphQLCors, cfg.Node.GraphQLVirtualHosts, cfg.Node.HTTPTimeouts); err != nil {
			utils.Fatalf("Failed to register the Ethereum service: %v", err)
		}
	}

	if cfg.Ethstats.URL != "" {
		utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
	}
	return stack
}

这里首先调用了makeConfigNode方法配置出一个节点,详见后文。节点对象生成后,利用RegisterEthService方法注册eth服务。接着根据需要注册dashboard服务,一般情况不开启此服务,需要额外启动。再往下判断是否启动whisper,条件时使用shh、shh.maxmessagesize、shh.pow或shh.restrict-light任意一个选项,或者没有显式设置shh但是启用了开发者模式(使用了–dev选项),此时配置shh然后注册shh服务。在往下如果使用了–graphql选项,则注册GraphQL服务,随后如果需要注册ethstats服务。

最后回顾可知makeFullNode主要做了这样几件事:加载配置,创建节点,注册服务。关于注册服务,默认只注册了eth服务。

makeConfigNode

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

	cfg := gethConfig{
		Eth: eth.DefaultConfig, 
		Shh:       whisper.DefaultConfig,   
		Node:      defaultNodeConfig(),    
		Dashboard: dashboard.DefaultConfig,
	}

	if file := ctx.GlobalString(configFileFlag.Name); file != "" {
		if err := loadConfig(file, &cfg); err != nil {
			utils.Fatalf("%v", err)
		}
	}

	utils.SetULC(ctx, &cfg.Eth)  go-ethereum\cmd\utils\flags.go
	utils.SetNodeConfig(ctx, &cfg.Node)
	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
}

第一步加载默认配置,创建了一个gethConfig对象,包含了eth、shh(即whisper)、node和dashboard的配置。如下:

//eth的默认配置如下
var DefaultConfig = Config{
	SyncMode: downloader.FastSync, 
	Ethash: ethash.Config{ 
		CacheDir:       "ethash",
		CachesInMem:    2,
		CachesOnDisk:   3,
		DatasetsInMem:  1,
		DatasetsOnDisk: 2,
	},
	NetworkId:      1,
	LightPeers:     100,
	DatabaseCache:  512,
	TrieCleanCache: 256,
	TrieDirtyCache: 256,
	TrieTimeout:    60 * time.Minute,
	MinerGasFloor:  8000000,
	MinerGasCeil:   8000000,
	MinerGasPrice:  big.NewInt(params.GWei),
	MinerRecommit:  3 * time.Second,

	TxPool: core.DefaultTxPoolConfig,
	GPO: gasprice.Config{
		Blocks:     20,
		Percentile: 60,
	},
}

//shh配置
var DefaultConfig = Config{
	MaxMessageSize:                        DefaultMaxMessageSize,
	MinimumAcceptedPOW:                    DefaultMinimumPoW,
	RestrictConnectionBetweenLightClients: true,
}

//node配置
func defaultNodeConfig() node.Config {
	cfg := node.DefaultConfig  //节点默认设置

	cfg.Name = clientIdentifier //节点名,"geth"
	cfg.Version = params.VersionWithCommit(gitCommit) //节点版本号
	cfg.HTTPModules = append(cfg.HTTPModules, "eth", "shh") //httprpc默认提供的接口
	cfg.WSModules = append(cfg.WSModules, "eth", "shh")     //websocketrpc默认提供的接口
	cfg.IPCPath = "geth.ipc" //ipcrpc文件名
	return cfg
}
var DefaultConfig = Config{
	DataDir:          DefaultDataDir(), //默认节点目录 /home/.ethereum
	HTTPPort:         DefaultHTTPPort, //httprpc默认端口 8545
	HTTPModules:      []string{"net", "web3"},  //httprpc默认提供的接口
	HTTPVirtualHosts: []string{"localhost"}, //httpcrpc默认服务地址
	HTTPTimeouts:     rpc.DefaultHTTPTimeouts, //httprpc默认超时时间,读超时30秒,写超时30秒,空闲超时120秒
	WSPort:           DefaultWSPort, //wsrpc默认端口,8546
	WSModules:        []string{"net", "web3"},//wscrpc默认服务地址
	P2P: p2p.Config{
		ListenAddr: ":30303", //p2p网络默认端口30303
		MaxPeers:   25, //默认最大连接数 25
		NAT:        nat.Any(), //默认端口映射
	},
}

//dashboard配置
var DefaultConfig = Config{
	Host:    "localhost",
	Port:    8080,
	Refresh: 5 * time.Second,
}

加载完默认配置后,再加载外部配置文件,就是–config指定的文件,toml格式。

接着就是架子我们给定配置。首先设置eth,之后设置node。这都是我们在执行geth命令时指定的选项,只与能设置那些值,详见这里

再往下使用node的new方法创建了一个节点对象。如果没有错的话,利用了几个set方法进一步配置了配置信息。

node.New

func New(conf *Config) (*Node, error) {
	confCopy := *conf
	conf = &confCopy
	if conf.DataDir != "" {
		absdatadir, err := filepath.Abs(conf.DataDir)
		if err != nil {
			return nil, err
		}
		conf.DataDir = absdatadir
	}

	if strings.ContainsAny(conf.Name, `/\`) {
		return nil, errors.New(`Config.Name must not contain '/' or '\'`)
	}
	if conf.Name == datadirDefaultKeyStore { //"keystore"
		return nil, errors.New(`Config.Name cannot be "` + datadirDefaultKeyStore + `"`)
	}
	if strings.HasSuffix(conf.Name, ".ipc") { //不含.ipc后缀
		return nil, errors.New(`Config.Name cannot end in ".ipc"`)
	}

	am, ephemeralKeystore, err := makeAccountManager(conf) 
	if err != nil {
		return nil, err
	}
	if conf.Logger == nil {
		conf.Logger = log.New()
	}

	return &Node{
		accman:            am,
		ephemeralKeystore: ephemeralKeystore,
		config:            conf,
		serviceFuncs:      []ServiceConstructor{},
		ipcEndpoint:       conf.IPCEndpoint(),
		httpEndpoint:      conf.HTTPEndpoint(),
		wsEndpoint:        conf.WSEndpoint(),
		eventmux:          new(event.TypeMux),
		log:               conf.Logger,
	}, nil
}

首先得到节点目录的绝对路径。然后检查了节点名,确保节点名中不包含“/\”这些特殊符号,以及不能等于“keystore”(密钥存储路径),还有不能有.ipc后缀。接着创建了账户管理者。makeAccountManager方法主要是创建了密钥存储路径以及创建了accountmanager对象。接着配置了log对象,然后创建了Node对象。

RegisterEthService

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) {
			return les.New(ctx, cfg)
		})
	} else {
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
			fullNode, err := eth.New(ctx, cfg)
			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)
	}
}

这里根据不同的同步模式创建不同的服务。如果是LightSync,创建Light Ethereum Subprotocol的服务;如果是其他模式则创建正常的eth服务。

服务注册的逻辑都是调用节点的Register方法

func (n *Node) Register(constructor ServiceConstructor) error {
	n.lock.Lock() 
	defer n.lock.Unlock()

	if n.server != nil {
		return ErrNodeRunning
	}
	n.serviceFuncs = append(n.serviceFuncs, constructor)
	return nil
}

type ServiceConstructor func(ctx *ServiceContext) (Service, error)

该方法需要提供一个ServiceConstructor对象,他实际是一个方法,执行后返回一个Service对象,Register方法就是将ServiceConstructor添加到node的serviceFuncs数组。

不管什么模式注册的服务方法都是简单的新建一个eth服务。

启动节点

创建完节点后就开始启动了。

func startNode(ctx *cli.Context, stack *node.Node) {
	debug.Memsize.Add("node", stack)

	utils.StartNode(stack)

	if keystores := stack.AccountManager().Backends(keystore.KeyStoreType); len(keystores) > 0 {
		ks := keystores[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)
			}
		}
	}

	events := make(chan accounts.WalletEvent, 16)
	stack.AccountManager().Subscribe(events)

	go func() {
		rpcClient, err := stack.Attach()
		if err != nil {
			utils.Fatalf("Failed to attach to self: %v", err)
		}
		stateReader := ethclient.NewClient(rpcClient)

		for _, wallet := range stack.AccountManager().Wallets() {
			if err := wallet.Open(""); err != nil {
				log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
			}
		}

		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()
			}
		}
	}()

	if ctx.GlobalBool(utils.ExitWhenSyncedFlag.Name) {
		go func() {
			sub := stack.EventMux().Subscribe(downloader.DoneEvent{})
			defer sub.Unsubscribe()
			for {
				event := <-sub.Chan()
				if event == nil {
					continue
				}
				done, ok := event.Data.(downloader.DoneEvent)
				if !ok {
					continue
				}
				if timestamp := time.Unix(done.Latest.Time.Int64(), 0); time.Since(timestamp) < 10*time.Minute {
					log.Info("Synchronisation completed", "latestnum", done.Latest.Number, "latesthash", done.Latest.Hash(),
						"age", common.PrettyAge(timestamp))
					stack.Stop()
				}

			}
		}()
	}

	if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
		if ctx.GlobalString(utils.SyncModeFlag.Name) == "light" {
			utils.Fatalf("Light clients do not support mining")
		}
		var ethereum *eth.Ethereum
		if err := stack.Service(&ethereum); err != nil {
			utils.Fatalf("Ethereum service not running: %v", err)
		}
		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)
		}
	}
}

在startNode首先调用了utils.StartNode(stack)区启动节点。调用完毕后一个节点正式启动。接着根据需求解锁我们指定的账户使用–unlock和–password指定要解锁的账户和密钥。接着注册了账户事件,是一个WalletEvent类型容量为16的channel。

接着启动了一个goroutine,在这个goroutine中先连接了节点使用Attach方法,实际上就是连接InProcRPC服务,然后获取了所有钱包,当接收到事件时触发对应逻辑。

再往下就是根据情况启动一些辅助逻辑,首先是同步完成监听,其次是根据需求启动挖矿。

utils.StartNode

// go-ethereum\cmd\utils\cmd.go
func StartNode(stack *node.Node) {
	if err := stack.Start(); err != nil {
		Fatalf("Error starting protocol stack: %v", err)
	}
	go func() {
		sigc := make(chan os.Signal, 1)
		signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
		defer signal.Stop(sigc)
		<-sigc
		log.Info("Got interrupt, shutting down...")
		go stack.Stop()
		for i := 10; i > 0; i-- {
			<-sigc
			if i > 1 {
				log.Warn("Already shutting down, interrupt more to panic.", "times", i-1)
			}
		}
		debug.Exit() // ensure trace and CPU profile data is flushed.
		debug.LoudPanic("boom")
	}()
}

这个方法中,先调用了stack.Start()去启动节点,之后启动了一个goroutine,这个goroutine的作用是接收到终端停止信息后,正确停止程序。利用的是Signal包,关于这个包的用法见这里。这个goroutine会一直阻塞直到收到退出信号,调用stop关闭节点。

Node.Start()

func (n *Node) Start() error {
	n.lock.Lock()
	defer n.lock.Unlock()

	if n.server != nil { 
		return ErrNodeRunning
	}
	if err := n.openDataDir(); err != nil {
		return err
	}

	n.serverConfig = n.config.P2P
	n.serverConfig.PrivateKey = n.config.NodeKey() 
	n.serverConfig.Name = n.config.NodeName() 
	n.serverConfig.Logger = n.log 
	if n.serverConfig.StaticNodes == nil { 
		n.serverConfig.StaticNodes = n.config.StaticNodes()
	}
	if n.serverConfig.TrustedNodes == nil {
		n.serverConfig.TrustedNodes = n.config.TrustedNodes()
	}
	if n.serverConfig.NodeDatabase == "" {
		n.serverConfig.NodeDatabase = n.config.NodeDB()
	}
	running := &p2p.Server{Config: n.serverConfig}
	n.log.Info("Starting peer-to-peer node", "instance", n.serverConfig.Name)

	services := make(map[reflect.Type]Service)
	for _, constructor := range n.serviceFuncs {
		ctx := &ServiceContext{
			config:         n.config,
			services:       make(map[reflect.Type]Service),
			EventMux:       n.eventmux,
			AccountManager: n.accman,
		}
		for kind, s := range services {
			ctx.services[kind] = s
		}

		service, err := constructor(ctx)
		if err != nil {
			return err
		}
		kind := reflect.TypeOf(service)
		if _, exists := services[kind]; exists {
			return &DuplicateServiceError{Kind: kind}
		}
		services[kind] = service 
	}
	for _, service := range services { 
		running.Protocols = append(running.Protocols, service.Protocols()...)
	}
	if err := running.Start(); err != nil {
		return convertFileLockError(err)
	}
	started := []reflect.Type{}
	for kind, service := range services {
		if err := service.Start(running); err != nil {
			for _, kind := range started {
				services[kind].Stop()
			}
			running.Stop()

			return err
		}
		started = append(started, kind)
	}
	if err := n.startRPC(services); err != nil {
		for _, service := range services {
			service.Stop()
		}
		running.Stop()
		return err
	}
	n.services = services
	n.server = running
	n.stop = make(chan struct{})

	return nil
}

首先检查是否启动过,之后调用openDataDir尝试打开节点目录

func (n *Node) openDataDir() error {
	if n.config.DataDir == "" {
		return nil 
	}

	instdir := filepath.Join(n.config.DataDir, n.config.name())
	if err := os.MkdirAll(instdir, 0700); err != nil {
		return err
	}

	release, _, err := flock.New(filepath.Join(instdir, "LOCK"))
	if err != nil {
		return convertFileLockError(err)
	}
	n.instanceDirLock = release
	return nil
}

openDataDir比较简单,首先判断是否设置了节点目录,没有的话直接返回,有的话创建一个以节点名为名字的子目录,默认为geth。之后在子目录geth中创建一个文件锁,名字是LOCK,为的是线程安全,之后用instanceDirLock变量记录文件锁引用,以便后续释放。

回到start中,此时文件目录及文件锁都创建完毕。然后初始化p2p服务,首先配置私钥

func (c *Config) NodeKey() *ecdsa.PrivateKey {
	if c.P2P.PrivateKey != nil {
		return c.P2P.PrivateKey
	}

	if c.DataDir == "" {
		key, err := crypto.GenerateKey()
		if err != nil {
			log.Crit(fmt.Sprintf("Failed to generate ephemeral node key: %v", err))
		}
		return key
	}

	keyfile := c.ResolvePath(datadirPrivateKey)            
	if key, err := crypto.LoadECDSA(keyfile); err == nil { 
		return key
	}

	key, err := crypto.GenerateKey() 
	if err != nil {
		log.Crit(fmt.Sprintf("Failed to generate node key: %v", err))
	}
	instanceDir := filepath.Join(c.DataDir, c.name())
	if err := os.MkdirAll(instanceDir, 0700); err != nil {
		log.Error(fmt.Sprintf("Failed to persist node key: %v", err))
		return key
	}
	keyfile = filepath.Join(instanceDir, datadirPrivateKey) 
	if err := crypto.SaveECDSA(keyfile, key); err != nil {  
		log.Error(fmt.Sprintf("Failed to persist node key: %v", err))
	}
	return key
}

第一步检查是否加载过私钥。没有的话,如果节点目录为空,直接生成一个,是椭圆曲线加密的秘钥。如果配置节点目录的话,则先尝试从目录中加载秘钥文件,之后读取私钥,秘钥文件是在geth子目录下的nodekey文件。若没有秘钥文件,则生成一个秘钥,之后存储到文件中,还是geth目录下的nodekey文件。

配置为秘钥后,又配置了服务名,使用NodeName方法,这个方法是一系列字符串组合,不在叙述。之后配置了静态节点

func (c *Config) StaticNodes() []*enode.Node {
	return c.parsePersistentNodes(&c.staticNodesWarning, c.ResolvePath(datadirStaticNodes))
}

func (c *Config) parsePersistentNodes(w *bool, path string) []*enode.Node {
	if c.DataDir == "" {
		return nil
	}
	if _, err := os.Stat(path); err != nil { 
		return nil
	}
	c.warnOnce(w, "Found deprecated node list file %s, please use the TOML config file instead.", path)

	var nodelist []string
	if err := common.LoadJSON(path, &nodelist); err != nil { 
		log.Error(fmt.Sprintf("Can't load node list file: %v", err))
		return nil
	}

	var nodes []*enode.Node
	for _, url := range nodelist {
		if url == "" {
			continue
		}
		node, err := enode.ParseV4(url) 
		if err != nil {
			log.Error(fmt.Sprintf("Node URL %s: %v\n", url, err))
			continue
		}
		nodes = append(nodes, node) 
	}
	return nodes
}

这里直接调用了parsePersistentNodes方法,包括后面加载可信节点也是这个方法,只是传入的参数不同。以静态节点为例,首先通过ResolvePath方法构造出静态节点文件的路径,对于静态节点是datadir/static-nodes.json,对于可信节点是datadir/trusted-nodes.json。之后在parsePersistentNodes中,如果没有配置节点路径,则返回空,否则判断文件是否存在,存在的话解析json文件,实际就是一个字符串数据。每个字符串代表一个节点,之后对于每个字符串解析为一个node节点,最后返回所有节点。

加载完静态节点和可信节点后,有配置了数据库路径,默认是datadir/geth/nodes,这里用了NodeDB方法

func (c *Config) NodeDB() string {
	if c.DataDir == "" {
		return "" // ephemeral
	}
	return c.ResolvePath(datadirNodeDatabase)
}

关键是ResolvePath,这个方法被多次用到,这里分析一下

var isOldGethResource = map[string]bool{
	"chaindata":          true,
	"nodes":              true,
	"nodekey":            true,
	"static-nodes.json":  false, 
	"trusted-nodes.json": false, 
}

func (c *Config) ResolvePath(path string) string {
	if filepath.IsAbs(path) {
		return path
	}
	if c.DataDir == "" {
		return ""
	}

	if warn, isOld := isOldGethResource[path]; isOld { 
		oldpath := ""
		if c.name() == "geth" {
			oldpath = filepath.Join(c.DataDir, path)
		}
		if oldpath != "" && common.FileExist(oldpath) {
			if warn {
				c.warnOnce(&c.oldGethResourceWarning, "Using deprecated resource file %s, please move this file to the 'geth' subdirectory of datadir.", oldpath)
			}
			return oldpath
		}
	}
	return filepath.Join(c.instanceDir(), path)
}

这里传入一个路径,一般是文件名,然后先判断是否是绝对路径,是的话直接返回,不是的话再判断datadir是否被设置,没有的话返回空。接着从isOldGethResource中查找以path为键的值,warn表示对应的值,isold表示是否有该键。如果有该键且节点名为geth时,设置目录为datadir/path。接着如果该文件已经存在表示有旧的数据,如果warn位true的话打印log并返回路径。如果节点名不是geth或者没有该键,或者文件不存在,则返回新的路径:datadir/节点名/path

回到start中,最后新建一个p2p服务。接着构建服务集合,遍历我们再注册服务时创建的serviceFuncs对象(默认时只有eth服务),每次遍历是都先创建一个ServiceContext,然后利用我们注册服务时提供的公共构造服务,然后反射服务的类型放到services中,但是不能重复。按照服务创建的先后顺序,后创建的服务在其ServiceContext中都持有先前服务的引用。

接着变量已创建的服务,将其协议添加到p2p服务的协议字段。最后先启动p2p服务,然后再启动之前注册的各个服务,注意在某个服务启动失败后,将终止所有服务,包括p2p服务。最后返回错误。

紧接着,调用startRPC方法启动rpc服务

func (n *Node) startRPC(services map[reflect.Type]Service) error {
	apis := n.apis()
	for _, service := range services {
		apis = append(apis, service.APIs()...)
	}

	if err := n.startInProc(apis); err != nil {
		return err
	}
	if err := n.startIPC(apis); err != nil {
		n.stopInProc()
		return err
	}
	if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors, n.config.HTTPVirtualHosts, n.config.HTTPTimeouts); err != nil {
		n.stopIPC()
		n.stopInProc()
		return err
	}
	if err := n.startWS(n.wsEndpoint, apis, n.config.WSModules, n.config.WSOrigins, n.config.WSExposeAll); err != nil {
		n.stopHTTP()
		n.stopIPC()
		n.stopInProc()
		return err
	}

	n.rpcAPIs = apis
	return nil
}

首先收集所有可以提供的api,然后依次调用startInProc、startIPC、startHTTP和startWS来根据需求启动各种rpc服务。注意其中任何一个rpc开启失败的话,都会关闭之前开启的rpc服务,并返回错误。

总结

geth的启动流程主要是就是先构造一个节点,然后启动各种服务。最先被启动的是p2p服务,然后在启动我们之前注册的各种服务,默认情况下只有Ethereum服务,最后启动各种RPC服务。

题图来自unsplash:https://unsplash.com/photos/DuBNA1QMpPA