以太坊源码分析(3)——基本框架

一、 创建节点

在 App 一章我们说到,以太坊的程序从 main 函数进入,并执行全局 app 对象的 Run 方法,最终调用 app.Action 也就是 geth 主函数。这一章我们就进入正题,一起来看看以太坊的基本框架是怎样的。

1.1 app.Action(geth)

找到 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
}

以太坊代码项目是开源公链,一个比较好的地方就是注释很详细。我们看下 geth 的官方解释:

如果没有运行特殊的子命令,geth 是进入系统的主要入口点。它根据命令行参数创建一个默认节点,并以阻塞模式运行它,等待它关闭。

也就是说,geth 在官方的定义是,他是一个节点程序,而且是单进程的,这一点在实际应用中其实不是很友好,但暂时先不管,以后有时间我们说到公链和许可链的区别时再讨论。

先看看 geth 的逻辑:函数先从 cli.Context 结构中获取参数列表,如果参数个数大于0,报错返回,否则创建一个全节点对象,启动节点,以阻塞的方式等待节点退出。函数退出后,返回 nil。

1.2 node.Wait

看下节点(程序)等待退出的条件。

// Wait blocks the thread until the node is stopped. If the node is not running
// at the time of invocation, the method immediately returns.
func (n *Node) Wait() {
   n.lock.RLock()
   if n.server == nil {
      n.lock.RUnlock()
      return
   }
   stop := n.stop
   n.lock.RUnlock()

   <-stop
}

Wait 方法先检查节点的服务是否启动了,如果没有启动,立马返回。否则获取 stop 通道的拷贝,释放读锁,并阻塞等待通道中的消息,如果收到消息说明程序退出了,此时返回。

也就是说,阻塞等待节点退出的实现方式是是,判断 stop 通道中是否有值过来,我们可以将之视为一个信号,它是一个空结构体的通道,后面会有很多地方这样用到。

1.3 创建一个全节点

以太坊有全节点和轻节点之分,通常我们研究全节点就行了。这两种节点都是通过 makeFullNode 函数实现的,一起来看下。

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

   utils.RegisterEthService(stack, &cfg.Eth)

   if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
      utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
   }
   // Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
   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)
      }
      utils.RegisterShhService(stack, &cfg.Shh)
   }

   // Add the Ethereum Stats daemon if requested.
   if cfg.Ethstats.URL != "" {
      utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
   }
   return stack
}

我们看下函数返回值,*node.Node 是个 Node 类型的指针,返回的对象是 stack

函数逻辑如下:

  • 调用 makeConfigNode() 方法创建一个 Node 对象 stack 和相应的 cfg 配置对象
  • 调用 utils.RegisterEthService() 方法注册以太坊服务
  • 判断是否设置了 Dashboard 标志,如果设置了,调用 utils.RegisterDashboardService() 方法注册 Dashboard 服务
  • 判断 shhEnabledshhAutoEnabled 标志是否为真,如果是,则调用 utils.RegisterShhService() 注册 shh 服务
  • 判断 cfg 对象的 Ethstats 对象是否需要,如果是,调用 utils.RegisterEthStatsService() 方法注册 EthStats 对象
  • 返回 stack 对象。

注意,如果所有的标志都设置了,将注册 Eth 服务、 Dashboard 服务、Shh 服务、 EthStats 服务。

1.3.1 makeConfigNode

以太坊的 cfg 是个相较很重要的概念,它设置了节点所有服务的初始配置,并影响一些常用对象的创建。我们来看下细节。

func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
   // Load defaults.
   cfg := gethConfig{
      Eth:       eth.DefaultConfig,
      Shh:       whisper.DefaultConfig,
      Node:      defaultNodeConfig(),
      Dashboard: dashboard.DefaultConfig,
   }

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

   // Apply flags.
   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
}

函数首先使用默认的配置创建一个 cfg 对象,我们看下

type gethConfig struct {
   Eth       eth.Config
   Shh       whisper.Config
   Node      node.Config
   Ethstats  ethstatsConfig
   Dashboard dashboard.Config
}

是不是感觉有点熟悉,不错,gethConfig 这几个配置成员项对应着上面创建全节点时注册的几个服务,Node 是比较特殊的,后面讲。

获取完了默认配置后,判断是否设置了 configFile 标志,如果设置了,在配置文件名不为空的情况下,读取配置文件的内容,如果读取失败,直接报错退出。

接下来根据命令行参数和配置文件中配置项,调用 utils.SetNodeConfig() 方法设置 cfg.Node 成员,使用该配置创建一个 stack 对象。调用 utils.SetEthConfig() 方法设置 cfg.Eth 成员。如果设置了 EthStatsURL 标志,给 cfg.Ethstats.URL 赋值。最后分别调用 utils.SetShhConfig()utils.SetDashboardConfig() 方法设置 cfg.Shhcfg.Dashboard 成员。

综上,makeConfigNode 函数先获取几个主要服务的默认配置,再读取配置文件里面的个性配置,接着使用这些配置为各个服务对象设置服务相关的参数,中间还使用 Node 服务的配置生成一个 Node 对象 stack

1.3.2 注册服务

上面我们说到,通过 makeConfigNode 函数创建了一个全局的节点 stack 对象以及存储了配置内容的 cfg 对象。这样是否就可以运行一个 p2p 节点开始挖矿了呢?当然不可能!

我们知道,以太坊服务节点仅仅只用一个进程就完成了挖矿,区块打包,广播区块的功能,它肯定不是简简单单的让 node start 一下就可以的了。那么?真相是什么?就是我们这里要说的“注册服务”了。

不得不说,以太坊区块链程序跟我们以往的后台服务器程序还是蛮像的,那就是:先读取配置文件,然后加载全局配置,将要做的事情抽象成服务注册到一个服务管理器中,使用管理器一键启动。这也是 Ethereum 程序基础架构,我们来看下:

  • 注册 Eth 服务
// RegisterEthService adds an Ethereum client to the stack.
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)
   }
}

Eth 服务是以太坊中的核心服务,使用 RegisterEthService() 方法注册,函数先判断当前的同步模式是否是轻节点模式,如果是,调用 stack.Register() 方法将函数注册进去,否则,还是调用 stack.Register() 将另一个函数注册进去。

  • 注册 DashBoard 服务
// RegisterDashboardService adds a dashboard to the stack.
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) {
	stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
		return dashboard.New(cfg, commit)
	})
}

DashBoard 服务是辅助服务,用来测试程序性能用的。

  • 注册 Shh 服务,即 Whisper 服务
// RegisterShhService configures Whisper and adds it to the given node.
func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
	if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) {
		return whisper.New(cfg), nil
	}); err != nil {
		Fatalf("Failed to register the Whisper service: %v", err)
	}
}

Whisper 服务用来在 Dapp 之间进行少量数据的通信服务。

  • 注册 EthStats 服务
// RegisterEthStatsService configures the Ethereum Stats daemon and adds it to
// th egiven node.
func RegisterEthStatsService(stack *node.Node, url string) {
	if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
		// Retrieve both eth and les services
		var ethServ *eth.Ethereum
		ctx.Service(&ethServ)

		var lesServ *les.LightEthereum
		ctx.Service(&lesServ)

		return ethstats.New(url, ethServ, lesServ)
	}); err != nil {
		Fatalf("Failed to register the Ethereum Stats service: %v", err)
	}
}

EthStats 是以太坊的监听服务,后面讲解代码时详细介绍。

我们看到,所有服务的注册方式都是通过 stack.Register 方法来注册的,注册的内容是一个函数。纳尼?注册一个函数进去?我们来看看这里面到底有什么阴谋。

// Register injects a new service into the node's stack. The service created by
// the passed constructor must be unique in its type with regard to sibling ones.
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
}

原来,这里的所谓注册服务,其实就是将一个函数构造器(ServiceConstructor) 添加到节点的 serviceFuncs 数组(切片)里,而所谓的函数构造器即 type ServiceConstructor func(ctx *ServiceContext) (Service, error) ,也就是个函数类型。这跟 C/C++ 语言中传递函数指针是一个道理。

需要注意的是,Register() 函数在注册服务时,需要在类型方面必须是唯一的,且对应服务真正的执行是通过反射在运行时动态完成的。

1.3.3 轻节点构造器 VS 全节点构造器

func(ctx *node.ServiceContext) (node.Service, error) {
    return les.New(ctx, cfg)
}

以上是轻节点构造器

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
}

全节点构造器比轻节点的要复杂一点,它先创建一个全节点的 Ethereum 对象,如果创建的对象不为空,并且,配置项 cfg.LightServ 大于0,通过当前的节点和配置创建一个轻节点,将这个轻节点服务加入到 fullNode 中,返回结果。

通过这里我们看到,如果是轻节点的服务的话,那么很简单,直接返回一个新对象就行;而如果是全节点的话,那就看启动节点时有没有附带要创建轻节点的需求,有就加进去,没有就算了。看起来全节点像是个大哥的样子。

二、启动节点

2.1 startNode 启动节点

我们创建好了节点对象和相关的配置对象,并把服务都注册到了节点对象之后,我们要启动节点,让 Node 对象来管理这些服务。

// startNode boots up the system node and all registered protocols, after which
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
// miner.
func startNode(ctx *cli.Context, stack *node.Node) {
   // Start up the node itself
   utils.StartNode(stack)

   // 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)
      }
   }
   // Register wallet event handlers to open and auto-derive wallets
   events := make(chan accounts.WalletEvent, 16)
   stack.AccountManager().Subscribe(events)

   go func() {
      // Create an 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)

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

         case accounts.WalletDropped:
            log.Info("Old wallet dropped", "url", event.Wallet.URL())
            event.Wallet.Close()
         }
      }
   }()
   // 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.GlobalBool(utils.LightModeFlag.Name) || 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)
      }
      // Use a reduced number of threads if requested
      if threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name); threads > 0 {
         type threaded interface {
            SetThreads(threads int)
         }
         if th, ok := ethereum.Engine().(threaded); ok {
            th.SetThreads(threads)
         }
      }
      // Set the gas price to the limits from the CLI and start mining
      ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
      if err := ethereum.StartMining(true); err != nil {
         utils.Fatalf("Failed to start mining: %v", err)
      }
   }
}

这部分就是启动节点 Node 的逻辑,我们看下函数实现:

  • 调用 utils.StartNode 启动 Node 自身
  • 解锁在命令行中指定请求的账户
  • 使用账户管理器订阅钱包事件
  • 使用一个协程创建 rpcClient 并处理钱包事件
  • 判断命令行是否启用了挖矿功能,后者是否启用了开发者模式,如果启用了任何一个标志,将启动挖矿功能。其中,轻节点同步模式和轻节点服务模式不支持挖矿功能。

在查看节点怎么真正启动之前,我们稍微看下 Node 是怎么定义的:

// Node is a container on which services can be registered.
type Node struct {
   eventmux *event.TypeMux 
   config   *Config                         // 节点配置
   accman   *accounts.Manager               // 账户管理器

   ephemeralKeystore string                 // 密码
   instanceDirLock   flock.Releaser         // keystore 的目录锁

   serverConfig p2p.Config                  // p2p 服务配置
   server       *p2p.Server                 // p2p 服务

   serviceFuncs []ServiceConstructor        // 函数构造器列表
   services     map[reflect.Type]Service    // 服务映射表

   rpcAPIs       []rpc.API          // rpc 服务的API
   inprocHandler *rpc.Server        // rpc 服务处理器

   ipcEndpoint string               // RPC-IPC 服务端点信息
   ipcListener net.Listener         // RPC-IPC 服务监听器
   ipcHandler  *rpc.Server          // RPC-IPC 服务

   httpEndpoint  string             // RPC-HTTP 服务端点信息
   httpWhitelist []string           // RPC-HTTP 服务白名单
   httpListener  net.Listener       // RPC-HTTP 服务监听器
   httpHandler   *rpc.Server        // RPC-HTTP 服务

   wsEndpoint string                // RPC-WS 服务端点信息
   wsListener net.Listener          // RPC-WS 服务监听器
   wsHandler  *rpc.Server           // RPC-WS 服务 

   stop chan struct{}               // 节点停止通道
   lock sync.RWMutex                // 通道读写锁

   log log.Logger                   // 日志 
}

到这里已经进入了以太坊架构的核心部分。以太坊说到底还是个由多个 p2p 节点连接起来的分布式网络,Node 在底层服务中承担着重要作用。从 Node 的定义中我们可以看出来,以太坊中的节点,即 Node 承担着以下几重角色:

  • 一个存放数据的服务器
  • 一个负责跟其他 peer 进行 p2p 通信的网络节点
  • 一个管理多个服务的”容器“
  • 支持多种 rpc 通信功能的服务

也就是说,在以太坊设计框架中,一个节点,它既是一台拥有账户管理功能的机器,还能支持 RPC 访问,同时,它还是 p2p 网络中的一个 peer。除此之外,节点还是一个可以挖矿的计算机,当然,这是通过 Node 的 ethereum 服务实现的。

可以说,Ethereum 使用一些相对较杂凑的功能模块完成了一个比较庞大的功能,这真的好吗?其实这是公链中没办法的事,公链的特点是适用人群五花八门,而其中更多的人群是单一开发人员,因而为了操作上的方便,尽量的让功能更紧凑,操作起来也就更方便。

对于上面这一点,在许可链里面则没有这样的情况。或者在设计上直接避过这一环。对于更多的许可链内容不在这里展开,如果大家有兴趣可以评论区留言。

2.2 StartNode 启动节点自身

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, os.Interrupt)
		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")
	}()
}

函数先调用 node.Start() 函数,然后使用一个协程捕获 Interrupt 信号,如果捕获到了,就执行 node.Stop() 函数停止节点。

2.3 node.Start()

// Start create a live P2P node and starts running it.
func (n *Node) Start() error {
	n.lock.Lock()
	defer n.lock.Unlock()

	// Short circuit if the node's already running
	if n.server != nil {
		return ErrNodeRunning
	}
	if err := n.openDataDir(); err != nil {
		return err
	}

	// Initialize the p2p server. This creates the node key and
	// discovery databases.
	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)

	// Otherwise copy and specialize the P2P configuration
	services := make(map[reflect.Type]Service)
	for _, constructor := range n.serviceFuncs {
		// Create a new context for the particular service
		ctx := &ServiceContext{
			config:         n.config,
			services:       make(map[reflect.Type]Service),
			EventMux:       n.eventmux,
			AccountManager: n.accman,
		}
		for kind, s := range services { // copy needed for threaded access
			ctx.services[kind] = s
		}
		// Construct and save the service
		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
	}
	// Gather the protocols and start the freshly assembled P2P server
	for _, service := range services {
		running.Protocols = append(running.Protocols, service.Protocols()...)
	}
	if err := running.Start(); err != nil {
		return convertFileLockError(err)
	}
	// Start each of the services
	started := []reflect.Type{}
	for kind, service := range services {
		// Start the next service, stopping all previous upon failure
		if err := service.Start(running); err != nil {
			for _, kind := range started {
				services[kind].Stop()
			}
			running.Stop()

			return err
		}
		// Mark the service started for potential cleanup
		started = append(started, kind)
	}
	// Lastly start the configured RPC interfaces
	if err := n.startRPC(services); err != nil {
		for _, service := range services {
			service.Stop()
		}
		running.Stop()
		return err
	}
	// Finish initializing the startup
	n.services = services
	n.server = running
	n.stop = make(chan struct{})

	return nil
}

函数主要做了以下几件事:

  • 加锁,打开 data 目录
  • 给节点的 p2p 服务配置 serverConfig 赋值
  • 使用 serverConfig 新建 p2p server 对象
  • 遍历 serviceFuncs 构造出服务实例
  • 遍历服务,将服务的协议添加到 p2p 实例 running 的协议集中
  • 启动 p2p 服务
  • 启动各服务,如果有一个服务启动失败,停止所有已启动服务并退出
  • 启动 RPC 服务
  • 给 Node 成员赋值,函数返回 nil

2.4 node.Stop()

// Stop terminates a running node along with all it's services. In the node was
// not started, an error is returned.
func (n *Node) Stop() error {
	n.lock.Lock()
	defer n.lock.Unlock()

	// Short circuit if the node's not running
	if n.server == nil {
		return ErrNodeStopped
	}

	// Terminate the API, services and the p2p server.
	n.stopWS()
	n.stopHTTP()
	n.stopIPC()
	n.rpcAPIs = nil
	failure := &StopError{
		Services: make(map[reflect.Type]error),
	}
	for kind, service := range n.services {
		if err := service.Stop(); err != nil {
			failure.Services[kind] = err
		}
	}
	n.server.Stop()
	n.services = nil
	n.server = nil

	// Release instance directory lock.
	if n.instanceDirLock != nil {
		if err := n.instanceDirLock.Release(); err != nil {
			n.log.Error("Can't release datadir lock", "err", err)
		}
		n.instanceDirLock = nil
	}

	// unblock n.Wait
	close(n.stop)

	// Remove the keystore if it was created ephemerally.
	var keystoreErr error
	if n.ephemeralKeystore != "" {
		keystoreErr = os.RemoveAll(n.ephemeralKeystore)
	}

	if len(failure.Services) > 0 {
		return failure
	}
	if keystoreErr != nil {
		return keystoreErr
	}
	return nil
}

stop 函数用来退出一个正在运行的节点,并停止所有的服务,它的过程如下:

  • 加锁,判断节点是否在运行,如果节点已停止,报错退出
  • 停止 RPC 服务
  • 停止所有的服务
  • 解锁目录锁
  • 向 node.stop 通道发送节点停止信号
  • 移除掉所暂时生成的 keystore

节点整个停止动作基本是启动的逆过程。

三、总结

这里回顾一下,我们对以太坊程序的简单认识:

1)以太坊的服务端程序是一个 App 应用程序,它是单进程的,通过 gopkg.in/urfave/cli.v1 包中的 app 应用来实现
2)我们学习了 以太坊应用程序 app 是怎么启动的,他真正运行的是哪个函数 —— geth
3)接着我们简单走读了一下 geth 函数的执行过程,并分析了 Node 对象是个什么玩意儿
4)Node 对象是以太坊底层最重要的几个概念之一,它是几个功能模块的组合:账号管理器,服务管理者,p2p 服务,rpc 服务端。
5)Node 的启动就是围绕这几个功能模块来展开的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值