对于想深入了解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
}
}
}()
},
这样,就会输出对应的结果。