一、 創建節點
在 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
服務 - 判斷
shhEnabled
或shhAutoEnabled
標誌是否爲真,如果是,則調用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.Shh
和 cfg.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(ðServ)
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(ðereum); 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 的啓動就是圍繞這幾個功能模塊來展開的。