以太坊源碼分析(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 的啓動就是圍繞這幾個功能模塊來展開的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章