docker源碼學習-main

衆所周知,docker client和docker server共用一個可執行文件,通過命令行參數來區分是client還是server。
喵一眼main函數源碼:
docker/docker.go


func main() {
    // 爲了exedriver
    if reexec.Init() {
        return
    }
    // 命令行參數解析
    flag.Parse()
    // FIXME: validate daemon flags here

    // 在docker編譯時實現
    if *flVersion {
        showVersion()
        return
    }
    if *flDebug {
        os.Setenv("DEBUG", "1")
    }

    // flHosts是docker server監聽,docker連接的地址
    if len(flHosts) == 0 {
        defaultHost := os.Getenv("DOCKER_HOST")
        if defaultHost == "" || *flDaemon {
            // If we do not have a host, default to unix socket
            defaultHost = fmt.Sprintf("unix://%s", api.DEFAULTUNIXSOCKET)
        }
        defaultHost, err := api.ValidateHost(defaultHost)
        if err != nil {
            log.Fatal(err)
        }
        flHosts = append(flHosts, defaultHost)
    }

以上代碼四個要點:
0.reexec.Init是爲了exedriver,分析到driver部分再說;
1.命令行參數用到flag表解析,不贅述;
2.flVersion代碼編譯時實現的版本控制;
3.flHosts是docker server監聽,docker連接的地址。

好繼續往下看:
docker/docker.go:

    // 說明啓動的是docer daemon,也就是docker server,也就是說後邊代碼不在執行
    if *flDaemon {
        mainDaemon()
        return
    }


    if len(flHosts) > 1 {
        log.Fatal("Please specify only one -H")
    }
    // protoAddrParts解析出 Docker Client 與 Docker Server建立通信的協議與地址
    protoAddrParts := strings.SplitN(flHosts[0], "://", 2)

    var (
        cli       *client.DockerCli // client對象
        tlsConfig tls.Config        // tls協議配置
    )
    tlsConfig.InsecureSkipVerify = true//默認不啓用

    // If we should verify the server, we need to load a trusted ca
    // 如果啓用TLS,則讀ca文件
    if *flTlsVerify {
        *flTls = true
        certPool := x509.NewCertPool()
        file, err := ioutil.ReadFile(*flCa)
        if err != nil {
            log.Fatalf("Couldn't read ca cert %s: %s", *flCa, err)
        }
        certPool.AppendCertsFromPEM(file)
        tlsConfig.RootCAs = certPool
        tlsConfig.InsecureSkipVerify = false
    }

        // If tls is enabled, try to load and send client certificates
    if *flTls || *flTlsVerify {
        _, errCert := os.Stat(*flCert)
        _, errKey := os.Stat(*flKey)
        if errCert == nil && errKey == nil {
            *flTls = true
            cert, err := tls.LoadX509KeyPair(*flCert, *flKey)
            if err != nil {
                log.Fatalf("Couldn't load X509 key pair: %s. Key encrypted?", err)
            }
            tlsConfig.Certificates = []tls.Certificate{cert}
        }
        // Avoid fallback to SSL protocols < TLS1.0
        tlsConfig.MinVersion = tls.VersionTLS10
    }

以上代碼可以看出:
1.docker中如何實現客戶端服務端同文件;
2.docker可選支持TLS安全傳輸協議。

okay看到這裏以及看完了docker命令行解析,接下來看客戶端啓動和服務端啓動吧。
客戶端啓動:

    // 啓動客戶端
    if *flTls || *flTlsVerify {
        cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
    } else {
        cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], nil)
    }

    // 把命令行參數傳過去
    if err := cli.Cmd(flag.Args()...); err != nil {
        if sterr, ok := err.(*utils.StatusError); ok {
            if sterr.Status != "" {
                log.Infof("%s", sterr.Status)
            }
            os.Exit(sterr.StatusCode)
        }
        log.Fatal(err)
    }

再看看client包中的NewDockerCli代碼:

func NewDockerCli(in io.ReadCloser, out, err io.Writer, key libtrust.PrivateKey, proto, addr string, tlsConfig *tls.Config) *DockerCli {
    var (
        inFd          uintptr
        outFd         uintptr
        isTerminalIn  = false
        isTerminalOut = false
        scheme        = "http"
    )
    // 這樣就走https了
    if tlsConfig != nil {
        scheme = "https"
    }

    if in != nil {
        if file, ok := in.(*os.File); ok {
            inFd = file.Fd()
            isTerminalIn = term.IsTerminal(inFd)
        }
    }

    if out != nil {
        if file, ok := out.(*os.File); ok {
            outFd = file.Fd()
            isTerminalOut = term.IsTerminal(outFd)
        }
    }

    if err == nil {
        err = out
    }

    // The transport is created here for reuse during the client session
    tr := &http.Transport{
        TLSClientConfig: tlsConfig,
        Dial: func(dial_network, dial_addr string) (net.Conn, error) {
            // Why 32? See issue 8035
            return net.DialTimeout(proto, addr, 32*time.Second)
        },
    }
    if proto == "unix" {
        // no need in compressing for local communications
        tr.DisableCompression = true
    }

    return &DockerCli{
        proto:         proto,
        addr:          addr,
        in:            in,
        out:           out,
        err:           err,
        key:           key,
        inFd:          inFd,
        outFd:         outFd,
        isTerminalIn:  isTerminalIn,
        isTerminalOut: isTerminalOut,
        tlsConfig:     tlsConfig,
        scheme:        scheme,
        transport:     tr,
    }
}

以上代碼有一個要點:proto是DockerClient 與 Docker Server 的傳輸協議
具體的DockerCli類實現,稍後繼續

下面看看 cli.Cmd(flag.Args()…),究竟是咋個情況。擼一下

// Cmd executes the specified command
func (cli *DockerCli) Cmd(args ...string) error {
    if len(args) > 1 {
        method, exists := cli.getMethod(args[:2]...)
        if exists {
            return method(args[2:]...)
        }
    }
    if len(args) > 0 {
        method, exists := cli.getMethod(args[0])
        if !exists {
            fmt.Println("Error: Command not found:", args[0])
            return cli.CmdHelp(args[1:]...)
        }
        return method(args[1:]...)
    }
    // 說明沒傳入參數,那就返回其幫助信息
    return cli.CmdHelp(args...)
}

其實就是用命令行參數拼出方法名,然後反射把方法返回,然後傳參給返回的方法來執行。很優雅,以後做命令行的程序可以借鑑這種方法。爲方便理解我把這段代碼拉出來改了一下,如下所示:

package main

import (
    "fmt"
    "reflect"
    "strings"
)

type Client struct {
    name string
}

func (cli *Client) CmdHelp(args ...string) error {
    fmt.Println("func CmdHelp")
    return nil
}

func (cli *Client) CmdPull(args ...string) error {
    fmt.Println("func CmdPull")
    fmt.Println(args)
    return nil
}

func (cli *Client) getMethod(args ...string) (func(...string) error, bool) {
    camelArgs := make([]string, len(args))
    for i, s := range args {
        if len(s) == 0 {
            return nil, false
        }
        camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
    }
    methodName := "Cmd" + strings.Join(camelArgs, "")
    method := reflect.ValueOf(cli).MethodByName(methodName)
    if !method.IsValid() {
        return nil, false
    }
    return method.Interface().(func(...string) error), true
}

func (cli *Client) Cmd(args ...string) error {
    if len(args) > 1 {
        method, exists := cli.getMethod(args[:2]...)
        if exists {
            return method(args[2:]...)
        }
    }
    if len(args) > 0 {
        method, exists := cli.getMethod(args[0])
        if !exists {
            fmt.Println("Error: Command not found:", args[0])
            return cli.CmdHelp(args[1:]...)
        }
        return method(args[1:]...)
    }
    return cli.CmdHelp(args...)
}

func main() {
    var cli *Client
    cli.Cmd("pull", "1", "2", "3")
}

以上代碼打印:

func CmdPull
[1 2 3]

可以看到調用了CmdPull,並且參數成功傳入了函數CmdPull。docker源碼中也是一樣,看下CmdPull源碼:

func (cli *DockerCli) CmdPull(args ...string) error {
    cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry")
    tag := cmd.String([]string{"#t", "#-tag"}, "", "Download tagged image in a repository")
    if err := cmd.Parse(args); err != nil {
        return nil
    }

    if cmd.NArg() != 1 {
        cmd.Usage()
        return nil
    }
    var (
        v      = url.Values{}
        remote = cmd.Arg(0)
    )
    // 把要拉取的鏡像存到了v中
    v.Set("fromImage", remote)

    if *tag == "" {
        v.Set("tag", *tag)
    }

    remote, _ = parsers.ParseRepositoryTag(remote)
    // Resolve the Repository name from fqn to hostname + name
    hostname, _, err := registry.ResolveRepositoryName(remote)
    if err != nil {
        return err
    }

    cli.LoadConfigFile()

    // Resolve the Auth config relevant for this server
    authConfig := cli.configFile.ResolveAuthConfig(hostname)
    // 創建pull函數,作用是向docker server 發送Post命令,其中/images/create?"+v.Encode()爲url部分,map[string][]string{
            "X-Registry-Auth": registryAuthHeader,爲認證信息
    pull := func(authConfig registry.AuthConfig) error {
        buf, err := json.Marshal(authConfig)
        if err != nil {
            return err
        }
        registryAuthHeader := []string{
            base64.URLEncoding.EncodeToString(buf),
        }

        return cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{
            "X-Registry-Auth": registryAuthHeader,
        })
    }

    if err := pull(authConfig); err != nil {
        if strings.Contains(err.Error(), "Status 401") {
            fmt.Fprintln(cli.out, "\nPlease login prior to pull:")
            if err := cli.CmdLogin(hostname); err != nil {
                return err
            }
            authConfig := cli.configFile.ResolveAuthConfig(hostname)
            return pull(authConfig)
        }
        return err
    }

ok,至此docker clinet已經啓動成功,下一篇我寫一下學習docker deamon的筆記

路漫漫,其修遠兮

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章