IPFS原理及实现-入口

  对于想深入了解IPFS原理的人来说,在当前IPFS技术资料并不够多的情况下,阅读IPFS源码更是一种好的选择。但是IPFS代码较多,而且设计的模块也比较多,很多人对此往往不知道该从何下手。针对这一问题,社区有两个建议:一是从github上头寻找带有help_wanted标签的issue,并以此为切入口开始学习IPFS代码,这样带有目标性的学习就不会枯燥;另一种方式是可以从比较熟悉的命令行入手,一步一步理解IPFS的原理,这样有一个由浅入深的过程。本文就从命令行出发,让大家对IPFS实现有一个初步的感觉。

说明:本文涉及到代码版本为v0.4.7

总体思路

  IPFS当中,不同的功能作为单独的模块存放,各个模块之间来互相调用。涉及到本文档的主要有两个模块:go-ipfs,go-ipfs-cmds。设计思路如下:

a, go-ipfs/core/commands/ 目录下定义好各个子命令(如add.go,cat.go)。包括子命令的描述信息、option内容、和将要运行的函数。

b, 在go-ipfs/cmd/ipfs/main.go中定义好两个函数用于后续的构造(buildEnv和makeExecutor)。

c, 使用go-ipfs-cmds模块,将命令行工具和环境配置等需要的信息转换成request结构体。

d, 根据request内容,跳到相应的已定义好的待执行函数并执行。

关键数据结构

go-ipfs-cmds/command.go中的Command类型

type Command struct {
        Options   []cmdkit.Option     
        Arguments []cmdkit.Argument   
        PreRun    func(req *Request, env Environment) error   //此处定义的函数会在Run之前运行

        Run      Function   //子命令要执行的函数
        PostRun  PostRunMap
        Encoders EncoderMap
        Helptext cmdkit.HelpText  //help时候的输出

        External bool 

        
        // type返回的类型
        Type        interface{}
        //子命令
        Subcommands map[string]*Command
}

go-ipfs-cmds/request.go中的request类型

type Request struct {
        Context       context.Context
        Root, Command *Command    //Root是根命令,Command是子命令解析

        Path      []string
        Arguments []string
        Options   cmdkit.OptMap

        Files files.File

        bodyArgs *arguments
}

源码详解

当我们在命令行输入: ipfs refs local 会发生什么?

首先,当命令运行时候会先执行go-ipfs/cmd/ipfs/ipfs.go中的init()函数,此函数完成子命令的注册。

var Root = &cmds.Command{
        Options:  commands.Root.Options,
        Helptext: commands.Root.Helptext,
}


var localCommands = map[string]*cmds.Command{
        "daemon":   daemonCmd,
        "init":     initCmd,
        "commands": commandsClientCmd,
}

func init() {
        Root.Subcommands = localCommands

        for k, v := range commands.Root.Subcommands {
                if _, found := Root.Subcommands[k]; !found {
                        Root.Subcommands[k] = v
                }
        }
}

然后从main()函数这个入口开始看:
打开go-ipfs/cmd/ipfs/main.go文件

func main() {
        os.Exit(mainRet())   //转到下头的mainRet函数
}

func mainRet() int {
        rand.Seed(time.Now().UnixNano())  //设置随机数种子
        ctx := logging.ContextWithLoggable(context.Background(), loggables.Uuid("session"))  //生成一个带有ID的context
        var err error

        //定义一个报错函数,方便后面调用
        printErr := func(err error) {
                fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
        }

        stopFunc, err := profileIfEnabled() //设置pprof用于性能分析
        if err != nil {
                printErr(err)
                return 1
        }
        defer stopFunc() //函数执行完之后结束性能分析 

        intrh, ctx := setupInterruptHandler(ctx) //设置处理信号函数
        defer intrh.Close()

        // 微调命令行参数
        if len(os.Args) == 2 {
                if os.Args[1] == "help" {
                        os.Args[1] = "-h"
                } else if os.Args[1] == "--version" {
                        os.Args[1] = "version"
                }
        }

        // 程序名固定下来,这样输出会稳定
        os.Args[0] = "ipfs"

        //构造环境的函数,后续会调用
        buildEnv := func(ctx context.Context, req *cmds.Request) (cmds.Environment, error) {
                checkDebug(req)
                repoPath, err := getRepoPath(req)
                if err != nil {
                        return nil, err
                }
                log.Debugf("config path is %s", repoPath)


                return &oldcmds.Context{
                        ConfigRoot: repoPath,
                        LoadConfig: loadConfig,
                        ReqLog:     &oldcmds.ReqLog{},
                        ConstructNode: func() (n *core.IpfsNode, err error) {
                                if req == nil {
                                        return nil, errors.New("constructing node without a request")
                                }

                                r, err := fsrepo.Open(repoPath)
                                if err != nil {
                                        return nil, err
                                }

                                n, err = core.NewNode(ctx, &core.BuildCfg{
                                        Repo: r,
                                })
                                if err != nil {
                                        return nil, err
                                }

                                n.SetLocal(true)
                                return n, nil
                        },
                }, nil
        }
        //调用go-ipfs-cmds下的cli包继续
        err = cli.Run(ctx, Root, os.Args, os.Stdin, os.Stdout, os.Stderr, buildEnv, makeExecutor)
        if err != nil {
                return 1
        }

        return 0
}

然后进入go-ipfs-cmds/cli/run.go文件:

func Run(ctx context.Context, root *cmds.Command,
        cmdline []string, stdin, stdout, stderr *os.File,
        buildEnv cmds.MakeEnvironment, makeExecutor cmds.MakeExecutor) error {
        //报错函数供后续调用
        printErr := func(err error) {
                fmt.Fprintf(stderr, "Error: %s\n", err)
        }
        //将ctx,命令行参数和root(包含所有subcommand)转化为request结构体
        req, errParse := Parse(ctx, cmdline[1:], stdin, root)

        var cancel func()
        //设置超时时间
        if timeoutStr, ok := req.Options[cmds.TimeoutOpt]; ok {
                timeout, err := time.ParseDuration(timeoutStr.(string))
                if err != nil {
                        return err
                }
                req.Context, cancel = context.WithTimeout(req.Context, timeout)
        } else {
                req.Context, cancel = context.WithCancel(req.Context)
        }
        defer cancel()

        // 定义打印函数
        printMetaHelp := func(w io.Writer) {
                cmdPath := strings.Join(req.Path, " ")
                fmt.Fprintf(w, "Use '%s %s --help' for information about this command\n", cmdline[0], cmdPath)
        }
        //定义打印函数
        printHelp := func(long bool, w io.Writer) {
                helpFunc := ShortHelp
                if long {
                        helpFunc = LongHelp
                }

                var path []string
                if req != nil {
                        path = req.Path
                }

                if err := helpFunc(cmdline[0], root, path, w); err != nil {
                        panic(err)
                }
        }

        // 如果命令行是help,则打印返回,否则err=ErrNoHelpRequested,并继续
        err := HandleHelp(cmdline[0], req, stdout)
        if err == nil {
                return nil
        } else if err != ErrNoHelpRequested {
                return err
        }

     
        // 现在处理上头的参数解析错误
        if errParse != nil {
                printErr(errParse)

                // 用户使用错误,直接报错退出
                if req != nil && req.Command != nil {
                        fmt.Fprintln(stderr) 
                        printHelp(false, stderr)
                }

                return err
        }


        // 以下是代码错误:代码未实现相应的子命令函数,打印到标准输出
        if req == nil || req.Command == nil || req.Command.Run == nil {
                printHelp(false, stdout)
                return nil
        }

        // 此时的cmd已经是子命令对用的command结构体
        cmd := req.Command
        // 构建环境信息,这个函数会返回包含配置和IpfsNode构造函数的结构体
        env, err := buildEnv(req.Context, req)
        if err != nil {
                printErr(err)
                return err
        }
        if c, ok := env.(Closer); ok {
                defer c.Close()
        }
        // 这个函数经过一些步骤之后,找到最终要执行的函数
        exctr, err := makeExecutor(req, env)
        if err != nil {
                printErr(err)
                return err
        }

        var (
                re     cmds.ResponseEmitter
                exitCh <-chan int
        )
        //encoding这些可以先不理会
        encTypeStr, _ := req.Options[cmds.EncLong].(string)
        encType := cmds.EncodingType(encTypeStr)

        // 如果子命令没有实现对应文本解析器,使用json
        if _, ok := cmd.Encoders[encType]; encType == cmds.Text && !ok {
                req.Options[cmds.EncLong] = cmds.JSON
        }

        // 生成responseEmitter
        if enc, ok := cmd.Encoders[encType]; ok {
                re, exitCh = NewResponseEmitter(stdout, stderr, enc, req)
        } else if enc, ok := cmds.Encoders[encType]; ok {
                re, exitCh = NewResponseEmitter(stdout, stderr, enc, req)
        } else {
                return fmt.Errorf("could not find matching encoder for enctype %#v", encType)
        }

        errCh := make(chan error, 1)
        //执行子命令的Run函数,跳到真正执行函数的地方
        go func() {
                err := exctr.Execute(req, re, env)
                if err != nil {
                        errCh <- err
                }
        }()

        select {
        case err := <-errCh:
                printErr(err)

                if kiterr, ok := err.(*cmdkit.Error); ok {
                        err = *kiterr
                }
                if kiterr, ok := err.(cmdkit.Error); ok && kiterr.Code == cmdkit.ErrClient {
                        printMetaHelp(stderr)
                }

                return err

        case code := <-exitCh:
                if code != 0 {
                        return ExitError(code)
                }
        }

        return nil
}

现在真正执行子命令的Run函数,假如命令行是ipfs refs local,那么会执行go-ipfs/core/commands/refs.go内的RefsLocalCmd的Run函数

 Run: func(req cmds.Request, res cmds.Response) {
                ctx := req.Context()
                //这里最终会调用buildEnv里头的ConstructNode方法,得到关键性的IpfsNode结构。
                n, err := req.InvocContext().GetNode()
                if err != nil {
                        res.SetError(err, cmdkit.ErrNormal)
                        return
                }

                // 获取本节点所有的keys
                allKeys, err := n.Blockstore.AllKeysChan(ctx)
                if err != nil {
                        res.SetError(err, cmdkit.ErrNormal)
                        return
                }

                out := make(chan interface{})
                res.SetOutput((<-chan interface{})(out))

                go func() {
                        defer close(out)
                // 输出结果
                        for k := range allKeys {
                                select {
                                case out <- &RefWrapper{Ref: k.String()}:
                                case <-req.Context().Done():
                                        return
                                }
                        }
                }()
        },

这样,就会输出对应的结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容