一、安装启动方式
Fabric的启动有几种方法,一种是使用Docker从官方下载相应的版本然后启动,一种是直接源码编译启动,这两种方式没有本质的不同。同时,最新的Fabric提供了Solo和Raft两种共识服务机制(不讨论旧版本的KAFKA等),在数据的保存机制上提供了LevelDB和CouchDB两种方式。
因此,有必要把这些环境都指定下来,才能更好的分析源码。这里主要以Raft共识为主,leveldb数据库为基础进行分析。安装启动的方式以源码编译为主。
之所以选取这种形式,主要是这种形式更容易理解,把相关复杂的与源码无关的环境配置选项从中摘除,以便清晰的展现代码运行的路径。
二、启动流程
重点是Peer和Orderer两个模块,先分析Peer:
1、入口函数:
//cobra是一个开源Golang命令包,可以快速创建命令行界面,在以太坊中用的CLI这个包
var mainCmd = &cobra.Command{
Use: "peer"}
func main() {
//下面这段代码,主要是用来处理环境变量的处理方式
viper.SetEnvPrefix(common.CmdRoot)
viper.AutomaticEnv()
replacer := strings.NewReplacer(".", "_")
viper.SetEnvKeyReplacer(replacer)
//处理命令格式,包括所有的peer相关的命令
mainFlags := mainCmd.PersistentFlags()
mainFlags.String("logging-level", "", "Legacy logging level flag")
viper.BindPFlag("logging_level", mainFlags.Lookup("logging-level"))
mainFlags.MarkHidden("logging-level")
//添加相关的命令,如版本、通道等
mainCmd.AddCommand(version.Cmd())
mainCmd.AddCommand(node.Cmd())
mainCmd.AddCommand(chaincode.Cmd(nil))
mainCmd.AddCommand(clilogging.Cmd(nil))
mainCmd.AddCommand(channel.Cmd(nil))
// 执行并处理异常,主要的任务由cobra完成,此处仅在错误时返回一个非零值
if mainCmd.Execute() != nil {
os.Exit(1)
}
}
看代码,这太简单了,简单的简直不像话。可是,一般来说,话少事大,同理,代码少,估计背后的服务啥的启动的少不了。挑一个常用的比如node.Cmd,进去看看:
func startCmd() *cobra.Command {
// Set the flags on the node start command.
flags := nodeStartCmd.Flags()
flags.BoolVarP(&chaincodeDevMode, "peer-chaincodedev", "", false,
"Whether peer in chaincode development mode")
return nodeStartCmd
}
//返回的是下面的类实例
var nodeStartCmd = &cobra.Command{
Use: "start",
Short: "Starts the node.",
Long: `Starts a node that interacts with the network.`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 0 {
return fmt.Errorf("trailing args detected")
}
// Parsing of the command line is done so silence cmd usage
cmd.SilenceUsage = true
return serve(args)
},
}
这个里头有一个Command结构体:
type Command struct {
// Use is the one-line usage message.
Use string
// Aliases is an array of aliases that can be used instead of the first word in Use.
Aliases []string
// SuggestFor is an array of command names for which this command will be suggested -
// similar to aliases but only suggests.
SuggestFor []string
// Short is the short description shown in the 'help' output.
Short string
// Long is the long message shown in the 'help <this-command>' output.
Long string
// Example is examples of how to use the command.
Example string
// ValidArgs is list of all valid non-flag arguments that are accepted in bash completions
ValidArgs []string
// Expected arguments
Args PositionalArgs
// ArgAliases is List of aliases for ValidArgs.
// These are not suggested to the user in the bash completion,
// but accepted if entered manually.
ArgAliases []string
// BashCompletionFunction is custom functions used by the bash autocompletion generator.
BashCompletionFunction string
// Deprecated defines, if this command is deprecated and should print this string when used.
Deprecated string
// Hidden defines, if this command is hidden and should NOT show up in the list of available commands.
Hidden bool
......
// commands is the list of commands supported by this program.
commands []*Command
// parent is a parent command for this command.
parent *Command
// Max lengths of commands' string lengths for use in padding.
commandsMaxUseLen int
commandsMaxCommandPathLen int
commandsMaxNameLen int
// commandsAreSorted defines, if command slice are sorted or not.
commandsAreSorted bool
// commandCalledAs is the name or alias value used to call this command.
commandCalledAs struct {
name string
called bool
}
// args is actual args parsed from flags.
args []string
// flagErrorBuf contains all error messages from pflag.
flagErrorBuf *bytes.Buffer
// flags is full set of flags.
flags *flag.FlagSet
// pflags contains persistent flags.
pflags *flag.FlagSet
// lflags contains local flags.
lflags *flag.FlagSet
// iflags contains inherited flags.
iflags *flag.FlagSet
// parentsPflags is all persistent flags of cmd's parents.
parentsPflags *flag.FlagSet
// globNormFunc is the global normalization function
// that we can use on every pflag set and children commands
globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName
// output is an output writer defined by user.
output io.Writer
// usageFunc is usage func defined by user.
usageFunc func(*Command) error
......
helpCommand *Command
// versionTemplate is the version template defined by user.
versionTemplate string
}
有一点吓人的长,不过有注释,再加上命名还是挺规范的,所以看看也没啥难度,就是有点多,估计经常得来回切换着看。现在只关心和Run有关系的Action,其它暂时可以放一下。
看一下命令的执行过程:
func main() {
......
//将相关加入的命令执行
if mainCmd.Execute() != nil {
os.Exit(1)
}
}
//调用
func (c *Command) Execute() error {
_, err := c.ExecuteC()
return err
}
func (c *Command) ExecuteC() (cmd *Command, err error) {
// 仅运行根命令
if c.HasParent() {
return c.Root().ExecuteC()
}
//预处理函数
if preExecHookFn != nil {
preExecHookFn(c)
}
// 默认初始化帮助
c.InitDefaultHelpCmd()
......
//关键是这里
err = cmd.execute(flags)
......
return cmd, err
}
func (c *Command) execute(a []string) (err error) {
......
//初始化默认的帮助和版本标记
c.InitDefaultHelpFlag()
c.InitDefaultVersionFlag()
//分析输入的标记
err = c.ParseFlags(a)
if err != nil {
return c.FlagErrorFunc()(c, err)
}
......
//判断执行函数是否存在
if !c.Runnable() {
return flag.ErrHelp
}
c.preRun()
//解析参数
argWoFlags := c.Flags().Args()
if c.DisableFlagParsing {
argWoFlags = a
}
if err := c.ValidateArgs(argWoFlags); err != nil {
return err
}
//下来就是一大票各种执行,包括父子命令和不同的命令格式情况下调用不同的命令形式运行
for p := c; p != nil; p = p.Parent() {
if p.PersistentPreRunE != nil {
if err := p.PersistentPreRunE(c, argWoFlags); err != nil {
return err
}
break
} else if p.PersistentPreRun != nil {
p.PersistentPreRun(c, argWoFlags)
break
}
}
if c.PreRunE != nil {
if err := c.PreRunE(c, argWoFlags); err != nil {
return err
}
} else if c.PreRun != nil {
c.PreRun(c, argWoFlags)
}
if err := c.validateRequiredFlags(); err != nil {
return err
}
if c.RunE != nil {
if err := c.RunE(c, argWoFlags); err != nil {
return err
}
} else {
c.Run(c, argWoFlags)
}
if c.PostRunE != nil {
if err := c.PostRunE(c, argWoFlags); err != nil {
return err
}
} else if c.PostRun != nil {
c.PostRun(c, argWoFlags)
}
for p := c; p != nil; p = p.Parent() {
if p.PersistentPostRunE != nil {
if err := p.PersistentPostRunE(c, argWoFlags); err != nil {
return err
}
break
} else if p.PersistentPostRun != nil {
p.PersistentPostRun(c, argWoFlags)
break
}
}
return nil
}
看到最后大片的if…else了吧,就是一开始命令中的设置。这样就明白了,一个命令执行,比如"peer node start -o 127.0.0.1:7091 --peer-chaincodedev=true",就可以从这里分析。
后面的AddCommand都是如此,包括日志,链码和通道相关,再看一下通道相关的命令:
func Cmd(cf *ChannelCmdFactory) *cobra.Command {
AddFlags(channelCmd)
channelCmd.AddCommand(createCmd(cf))
channelCmd.AddCommand(fetchCmd(cf))
channelCmd.AddCommand(joinCmd(cf))
channelCmd.AddCommand(listCmd(cf))
channelCmd.AddCommand(updateCmd(cf))
channelCmd.AddCommand(signconfigtxCmd(cf))
channelCmd.AddCommand(getinfoCmd(cf))
return channelCmd
}
func createCmd(cf *ChannelCmdFactory) *cobra.Command {
createCmd := &cobra.Command{
Use: "create",
Short: "Create a channel",
Long: "Create a channel and write the genesis block to a file.",
RunE: func(cmd *cobra.Command, args []string) error {
return create(cmd, args, cf)
},
}
flagList := []string{
"channelID",
"file",
"outputBlock",
"timeout",
}
attachFlags(createCmd, flagList)
return createCmd
}
好,流程明白了,回头看一下Start命令最后启动的server(args)。
2、服务启动
func serve(args []string) error {
// MSP的处理,只支持标准
mspType := mgmt.GetLocalMSP().GetType()
if mspType != msp.FABRIC {
panic("Unsupported msp type " + msp.ProviderTypeToString(mspType))
}
......
//使用ACL启动aclmgmt,创建ACL访问控制列表
aclProvider := aclmgmt.NewACLProvider(
aclmgmt.ResourceGetter(peer.GetStableChannelConfig),
)
//注册平台相关语言
pr := platforms.NewRegistry(
&golang.Platform{},
&node.Platform{},
&java.Platform{},
&car.Platform{},
)
//定义链码提供者实例
deployedCCInfoProvider := &lscc.DeployedCCInfoProvider{}
//获取通道管理者
identityDeserializerFactory := func(chainID string) msp.IdentityDeserializer {
return mgmt.GetManagerForChain(chainID)
}
//处理相关环境操作保存相关地址、端口、证书及TLS等。
opsSystem := newOperationsSystem()
err := opsSystem.Start()
if err != nil {
return errors.WithMessage(err, "failed to initialize operations subystems")
}
defer opsSystem.Stop()
//监控实例,监控相关节点信息并记录
metricsProvider := opsSystem.Provider
logObserver := floggingmetrics.NewObserver(metricsProvider)
flogging.Global.SetObserver(logObserver)
//保存连接成员信息,方便应用
membershipInfoProvider := privdata.NewMembershipInfoProvider(createSelfSignedData(), identityDeserializerFactory)
//initialize resource management exit
//此处是调用对帐本的处理也就是数据存储相关的服务
ledgermgmt.Initialize(
&ledgermgmt.Initializer{
//交易相关
CustomTxProcessors: peer.ConfigTxProcessors,
PlatformRegistry: pr,
//链码相关
DeployedChaincodeInfoProvider: deployedCCInfoProvider,
//节点间交互
MembershipInfoProvider: membershipInfoProvider,
MetricsProvider: metricsProvider,
//健康检查
HealthCheckRegistry: opsSystem,
},
)
......//处理参数部分省略
//下面拿到缓存的节点信息(主要是地址)后开始网络服务
peerEndpoint, err := peer.GetPeerEndpoint()
if err != nil {
err = fmt.Errorf("Failed to get Peer Endpoint: %s", err)
return err
}
peerHost, _, err := net.SplitHostPort(peerEndpoint.Address)
if err != nil {
return fmt.Errorf("peer address is not in the format of host:port: %v", err)
}
listenAddr := viper.GetString("peer.listenAddress")
serverConfig, err := peer.GetServerConfig()
if err != nil {
logger.Fatalf("Error loading secure config for peer (%s)", err)
}
//限流,最大2500
throttle := comm.NewThrottle(grpcMaxConcurrency)
serverConfig.Logger = flogging.MustGetLogger("core.comm").With("server", "PeerServer")
serverConfig.ServerStatsHandler = comm.NewServerStatsHandler(metricsProvider)
//设置拦截器,监控和RPC
serverConfig.UnaryInterceptors = append(
serverConfig.UnaryInterceptors,
grpcmetrics.UnaryServerInterceptor(grpcmetrics.NewUnaryMetrics(metricsProvider)),
grpclogging.UnaryServerInterceptor(flogging.MustGetLogger("comm.grpc.server").Zap()),
throttle.UnaryServerIntercptor,
)
serverConfig.StreamInterceptors = append(
serverConfig.StreamInterceptors,
grpcmetrics.StreamServerInterceptor(grpcmetrics.NewStreamMetrics(metricsProvider)),
grpclogging.StreamServerInterceptor(flogging.MustGetLogger("comm.grpc.server").Zap()),
throttle.StreamServerInterceptor,
)
//创建节点RPC服务
peerServer, err := peer.NewPeerServer(listenAddr, serverConfig)
if err != nil {
logger.Fatalf("Failed to create peer server (%s)", err)
}
//处理TLS
if serverConfig.SecOpts.UseTLS {
logger.Info("Starting peer with TLS enabled")
// set up credential support
cs := comm.GetCredentialSupport()
roots, err := peer.GetServerRootCAs()
if err != nil {
logger.Fatalf("Failed to set TLS server root CAs: %s", err)
}
cs.ServerRootCAs = roots
// set the cert to use if client auth is requested by remote endpoints
clientCert, err := peer.GetClientCertificate()
if err != nil {
logger.Fatalf("Failed to set TLS client certificate: %s", err)
}
comm.GetCredentialSupport().SetClientCertificate(clientCert)
}
mutualTLS := serverConfig.SecOpts.UseTLS && serverConfig.SecOpts.RequireClientCert
//策略柱果木榄果,对通道ID和变量等
policyCheckerProvider := func(resourceName string) deliver.PolicyCheckerFunc {
return func(env *cb.Envelope, channelID string) error {
return aclProvider.CheckACL(resourceName, channelID, env)
}
}
//创建分发和过滤区块事件服务
abServer := peer.NewDeliverEventsServer(mutualTLS, policyCheckerProvider, &peer.DeliverChainManager{}, metricsProvider)
pb.RegisterDeliverServer(peerServer.Server(), abServer)
// Initialize chaincode service
//设置本地链码安装位置;创建自签名CA,启动GRPC服务
chaincodeSupport, ccp, sccp, packageProvider := startChaincodeServer(peerHost, aclProvider, pr, opsSystem)
logger.Debugf("Running peer")
// Start the Admin server-这个是重点,管理员可以管理通道等
startAdminServer(listenAddr, peerServer.Server(), serverConfig)
privDataDist := func(channel string, txID string, privateData *transientstore.TxPvtReadWriteSetWithConfigInfo, blkHt uint64) error {
//分发私有数据
return service.GetGossipService().DistributePrivateData(channel, txID, privateData, blkHt)
}
//获得本地签名的身份信息,包括节点的功能,如背书和验证等
signingIdentity := mgmt.GetLocalSigningIdentityOrPanic()
serializedIdentity, err := signingIdentity.Serialize()
if err != nil {
logger.Panicf("Failed serializing self identity: %v", err)
}
//获得相关配置信息(上面的背书和签名)并初始化注册
libConf := library.Config{}
if err = viperutil.EnhancedExactUnmarshalKey("peer.handlers", &libConf); err != nil {
return errors.WithMessage(err, "could not load YAML config")
}
reg := library.InitRegistry(libConf)
authFilters := reg.Lookup(library.Auth).([]authHandler.Filter)
endorserSupport := &endorser.SupportImpl{
SignerSupport: signingIdentity,
Peer: peer.Default,
PeerSupport: peer.DefaultSupport,
ChaincodeSupport: chaincodeSupport,
SysCCProvider: sccp,
ACLProvider: aclProvider,
}
endorsementPluginsByName := reg.Lookup(library.Endorsement).(map[string]endorsement2.PluginFactory)
validationPluginsByName := reg.Lookup(library.Validation).(map[string]validation.PluginFactory)
signingIdentityFetcher := (endorsement3.SigningIdentityFetcher)(endorserSupport)
channelStateRetriever := endorser.ChannelStateRetriever(endorserSupport)
pluginMapper := endorser.MapBasedPluginMapper(endorsementPluginsByName)
pluginEndorser := endorser.NewPluginEndorser(&endorser.PluginSupport{
ChannelStateRetriever: channelStateRetriever,
TransientStoreRetriever: peer.TransientStoreFactory,
PluginMapper: pluginMapper,
SigningIdentityFetcher: signingIdentityFetcher,
})
endorserSupport.PluginEndorser = pluginEndorser
serverEndorser := endorser.NewEndorserServer(privDataDist, endorserSupport, pr, metricsProvider)
expirationLogger := flogging.MustGetLogger("certmonitor")
crypto.TrackExpiration(
serverConfig.SecOpts.UseTLS,
serverConfig.SecOpts.Certificate,
comm.GetCredentialSupport().GetClientCertificate().Certificate,
serializedIdentity,
expirationLogger.Warnf, // This can be used to piggyback a metric event in the future
time.Now(),
time.AfterFunc)
policyMgr := peer.NewChannelPolicyManagerGetter()
// Initialize gossip component--初始化网络间通信的Gossip协议服务
err = initGossipService(policyMgr, metricsProvider, peerServer, serializedIdentity, peerEndpoint.Address)
if err != nil {
return err
}
defer service.GetGossipService().Stop()
// register prover grpc service
// FAB-12971 disable prover service before v1.4 cut. Will uncomment after v1.4 cut
// err = registerProverService(peerServer, aclProvider, signingIdentity)
// if err != nil {
// return err
// }
// initialize system chaincodes
// deploy system chaincodes--部署系统链码
sccp.DeploySysCCs("", ccp)
logger.Infof("Deployed system chaincodes")
installedCCs := func() ([]ccdef.InstalledChaincode, error) {
//查看当前安装链码
return packageProvider.ListInstalledChaincodes()
}
//生命周期控制
lifecycle, err := cc.NewLifeCycle(cc.Enumerate(installedCCs))
if err != nil {
logger.Panicf("Failed creating lifecycle: +%v", err)
}
//链码的元数据更新
onUpdate := cc.HandleMetadataUpdate(func(channel string, chaincodes ccdef.MetadataSet) {
service.GetGossipService().UpdateChaincodes(chaincodes.AsChaincodes(), gossipcommon.ChainID(channel))
})
//添加对更新的监听
lifecycle.AddListener(onUpdate)
// this brings up all the channels--初始化通道相关信息
peer.Initialize(func(cid string) {
logger.Debugf("Deploying system CC, for channel <%s>", cid)
sccp.DeploySysCCs(cid, ccp)
sub, err := lifecycle.NewChannelSubscription(cid, cc.QueryCreatorFunc(func() (cc.Query, error) {
return peer.GetLedger(cid).NewQueryExecutor()
}))
if err != nil {
logger.Panicf("Failed subscribing to chaincode lifecycle updates")
}
//通道监听器注册
cceventmgmt.GetMgr().Register(cid, sub)
}, ccp, sccp, txvalidator.MapBasedPluginMapper(validationPluginsByName),
pr, deployedCCInfoProvider, membershipInfoProvider, metricsProvider)
if viper.GetBool("peer.discovery.enabled") {
registerDiscoveryService(peerServer, policyMgr, lifecycle)
}
networkID := viper.GetString("peer.networkId")
logger.Infof("Starting peer with ID=[%s], network ID=[%s], address=[%s]", peerEndpoint.Id, networkID, peerEndpoint.Address)
// Get configuration before starting go routines to avoid
// racing in tests是否定义配置文件
profileEnabled := viper.GetBool("peer.profile.enabled")
profileListenAddress := viper.GetString("peer.profile.listenAddress")
// 创建GRPC服务协程
serve := make(chan error)
// Start profiling http endpoint if enabled-如定义配置文件,启动RPC服务
if profileEnabled {
go func() {
logger.Infof("Starting profiling server with listenAddress = %s", profileListenAddress)
if profileErr := http.ListenAndServe(profileListenAddress, nil); profileErr != nil {
logger.Errorf("Error starting profiler: %s", profileErr)
}
}()
}
//处理事件消息
go handleSignals(addPlatformSignals(map[os.Signal]func(){
syscall.SIGINT: func() { serve <- nil },
syscall.SIGTERM: func() { serve <- nil },
}))
logger.Infof("Started peer with ID=[%s], network ID=[%s], address=[%s]", peerEndpoint.Id, networkID, peerEndpoint.Address)
// 检查帐本是否重置check to see if the peer ledgers have been reset
preResetHeights, err := kvledger.LoadPreResetHeight()
if err != nil {
return fmt.Errorf("error loading prereset height: %s", err)
}
for cid, height := range preResetHeights {
logger.Infof("Ledger rebuild: channel [%s]: preresetHeight: [%d]", cid, height)
}
......
// 下面启动节点服务
auth := authHandler.ChainFilters(serverEndorser, authFilters...)
// Register the Endorser server
pb.RegisterEndorserServer(peerServer.Server(), auth)
go func() {
var grpcErr error
if grpcErr = peerServer.Start(); grpcErr != nil {
grpcErr = fmt.Errorf("grpc server exited with error: %s", grpcErr)
}
serve <- grpcErr
}()
// Block until grpc server exits-//通道一直在此阻塞服务
return <-serve
}
又一大片,从刚开始接触Golang就发现这个语言的一个特点,稍微复杂的一点功能,就是一片代码,不像c++啥的拆来拆去。吐槽完毕,代码还得分析:
func (gServer *GRPCServer) Start() error {
return gServer.server.Serve(gServer.listener)
}
func (s *Server) Serve(lis net.Listener) error {
s.mu.Lock()
s.printf("serving")
s.serve = true
if s.lis == nil {
// Serve called after Stop or GracefulStop.
s.mu.Unlock()
lis.Close()
return ErrServerStopped
}
s.serveWG.Add(1)
defer func() {
s.serveWG.Done()
select {
// Stop or GracefulStop called; block until done and return nil.
case <-s.quit:
<-s.done
default:
}
}()
ls := &listenSocket{Listener: lis}
s.lis[ls] = true
if channelz.IsOn() {
ls.channelzID = channelz.RegisterListenSocket(ls, s.channelzID, "")
}
s.mu.Unlock()
defer func() {
s.mu.Lock()
if s.lis != nil && s.lis[ls] {
ls.Close()
delete(s.lis, ls)
}
s.mu.Unlock()
}()
var tempDelay time.Duration // how long to sleep on accept failure
for {
rawConn, err := lis.Accept()
if err != nil {
if ne, ok := err.(interface {
Temporary() bool
}); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.mu.Lock()
s.printf("Accept error: %v; retrying in %v", err, tempDelay)
s.mu.Unlock()
timer := time.NewTimer(tempDelay)
select {
case <-timer.C:
case <-s.quit:
timer.Stop()
return nil
}
continue
}
s.mu.Lock()
s.printf("done serving; Accept = %v", err)
s.mu.Unlock()
select {
case <-s.quit:
return nil
default:
}
return err
}
tempDelay = 0
// Start a new goroutine to deal with rawConn so we don't stall this Accept
// loop goroutine.
//
// Make sure we account for the goroutine so GracefulStop doesn't nil out
// s.conns before this conn can be added.
s.serveWG.Add(1)
go func() {
s.handleRawConn(rawConn)
s.serveWG.Done()
}()
}
}
看到这里,才看到了熟悉的套路,熟悉的配方,网线监听,网络服务的处理。到此处,基本一个Peer整体的流程的主干就清晰了。
3、说明
需要说明的是,在Server函数中,没有重点分析系统链码和帐本相关的处理,可以看一下相关的代码注释即可。没有什么太复杂的内容。
其实Peer节点中的代码还有很多,不过这里只是分析其启动的流程,相关的代码会在后面的相关部分进行展开。抓住重点,暂时扔掉细节,先从宏观上把握脉络。总结一下:
获取msp类型
设置资源访问策略 aclmgmt.NewACLProvider,测试可RegisterACLProvider()
初始化本地账本管理器 ledgermgmt.Initialize(peer.ConfigTxProcessors)
创建grpc服务1:PeerServer
创建grpc服务2:DeliverEvents(分发事件),注册到peerServer上
创建grpc服务3:链码管理服务
创建grpc服务4 : Admin注册到PeerServer上
隐私数据处理: 使用goosip协议进行分发
启动发现服务5:registerDiscoveryService
创建grpc服务6:背书服务
初始化goosip连接与加密组件
registerProverService:根据版本1.4前后不同的处理。
安装系统连码 initSysCCs并初始化peer.Initialize(func(cid string) {}
启动PeerServer Grpc服务
设置日志模块
和代码对比着流程,一一的看代码,就可以把整个的peer的内容看得清清楚楚。
三、总结
Peer的代码是整个Fabric中很重要的一个部分,可以说,他是基础,他提供了一系列相关的操作命令和通信的细节,这都需要后面不断的详细分析。