go-ethereum中geth启动流程分析
geth是go-ethereum的一个重要工具,根据官方文档的说明,他是进入Ethereum网络的入口,也就是Ethereum的一个节点程序。如果直接输入geth运行,则会默认启动一个全节点,并以fast模式同步数据。我们从这个命令入手,学习一下geth的启动流程。
main.go
通过对geth命令的分析,我们了解到geth是一个CLI程序,程序的定义主要在go-ethereum\cmd\geth\main.go里面,在init函数中定义了大量子命令,用于实现下图所示的功能:
但是如果我们不指定任何子命令,直接输入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(ðereum); 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