• 1. 创建Docker Client
    • 1.1. Docker命令flag参数解析
    • 1.2. 处理flag参数并收集Docker Client的配置信息
    • 1.3. 如何创建Docker Client
  • 2. Docke命令执行
    • 2.1. Docker Client解析请求命令
    • 2.2. Docker Client执行请求命令

    1. 创建Docker Client

    ​ Docker是一个client/server的架构,通过二进制文件docker创建Docker客户端将请求类型与参数发送给Docker Server,Docker Server具体执行命令调用。
    Docker Client运行流程图如下:
    Docker Client - 图1

    说明:本文分析的代码为Docker 1.2.0版本。

    1.1. Docker命令flag参数解析

    Docker Server与Docker Client由可执行文件docker命令创建并启动。

    • Docker Server的启动:docker -d或docker —daemon=true
    • Docker Client的启动:docker —daemon=false ps等

    docker参数分为两类:

    • 命令行参数(flag参数):—daemon=true,-d
    • 实际请求参数:ps ,images, pull, push等

    /docker/docker.go

    1. func main() {
    2. if reexec.Init() {
    3. return
    4. }
    5. flag.Parse()
    6. // FIXME: validate daemon flags here
    7. ......
    8. }

    reexec.Init()作用:协调execdriver与容器创建时dockerinit的关系。如果返回值为真则直接退出运行,否则继续执行。判断reexec.Init()之后,调用flag.Parse()解析命令行中的flag参数。

    /docker/flag.go

    1. var (
    2. flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit")
    3. flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode")
    4. flDebug = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode")
    5. flSocketGroup = flag.String([]string{"G", "-group"}, "docker", "Group to assign the unix socket specified by -H when running in daemon mode/nuse '' (the empty string) to disable setting of a group")
    6. flEnableCors = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API")
    7. flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by tls-verify flags")
    8. flTlsVerify = flag.Bool([]string{"-tlsverify"}, false, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)")
    9. // these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs
    10. flCa *string
    11. flCert *string
    12. flKey *string
    13. flHosts []string
    14. )
    15. func init() {
    16. flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust only remotes providing a certificate signed by the CA given here")
    17. flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file")
    18. flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file")
    19. opts.HostListVar(&flHosts, []string{"H", "-host"}, "The socket(s) to bind to in daemon mode/nspecified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.")
    20. }

    flag.go定义了flag参数,并执行了init的初始化。

    Go中的init函数

    1. 用于程序执行前包的初始化工作,比如初始化变量
    2. 每个包或源文件可以包含多个init函数
    3. init函数不能被调用,而是在mian函数调用前自动被调用
    4. 不同init函数的执行顺序,按照包导入的顺序执行

    当解析到第一个非flag参数时,flag解析工作就结束。例如docker —daemon=flase —version=false ps

    • 完成flag的解析,—daemon=false
    • 遇到第一个非flag参数ps,则将ps及其后的参数存入flag.Args(),以便执行之后的具体请求。

    1.2. 处理flag参数并收集Docker Client的配置信息

    处理的flag参数有flVersion,flDebug,flDaemon,flTlsVerify以及flTls。

    /docker/docker.go

    1. func main() {
    2. ......
    3. if len(flHosts) == 0 {
    4. defaultHost := os.Getenv("DOCKER_HOST")
    5. if defaultHost == "" || *flDaemon {
    6. // If we do not have a host, default to unix socket
    7. defaultHost = fmt.Sprintf("unix://%s", api.DEFAULTUNIXSOCKET)
    8. }
    9. if _, err := api.ValidateHost(defaultHost); err != nil {
    10. log.Fatal(err)
    11. }
    12. flHosts = append(flHosts, defaultHost)
    13. }
    14. ......
    15. }

    flHosts的作用是为Docker Client提供所要连接的host对象,也就是为Docker Server提供所要监听的对象。
    当flHosts为空,默认取环境变量DOCKER_HOST,若仍为空或flDaemon为真,则设置为unix socket,值为unix:///var/run/docker.sock。取自/api/common.go中的常量DEFAULTUNIXSOCKET。

    /docker/docker.go

    1. func main() {
    2. ...
    3. if *flDaemon {
    4. mainDaemon()
    5. return
    6. }
    7. ...
    8. }

    若flDaemon为真,表示启动Docker Daemon,调用/docker/daemon.go中的func mainDaemon()。

    /docker/docker.go

    1. if len(flHosts) > 1 {
    2. log.Fatal("Please specify only one -H")
    3. }
    4. protoAddrParts := strings.SplitN(flHosts[0], "://", 2)

    protoAddrParts的作用是解析出Docker Client 与Docker Server建立通信的协议与地址,通过strings.SplitN函数分割存储。flHosts[0]的值可以是tcp://0.0.0.0.2375或者unix:///var/run/docker.sock等。

    /docker/docker.go

    1. var (
    2. cli *client.DockerCli
    3. tlsConfig tls.Config
    4. )
    5. tlsConfig.InsecureSkipVerify = true

    tlsConfig对象的创建是为了保障cli在传输数据的时候遵循安全传输层协议(TLS)。flTlsVerity参数为真,则说明Docker Client 需Docker Server一起验证连接的安全性,如果flTls和flTlsVerity两个参数中有一个为真,则说明需要加载并发送客户端的证书。

    /docker/flags.go

    1. flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by tls-verify flags")
    2. flTlsVerify = flag.Bool([]string{"-tlsverify"}, false, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)")

    1.3. 如何创建Docker Client

    /docker/docker.go

    1. if *flTls || *flTlsVerify {
    2. cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
    3. } else {
    4. cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, protoAddrParts[0], protoAddrParts[1], nil)
    5. }

    在已有配置参数的情况下,通过/api/client/cli.go中的NewDockerCli方法创建Docker Client实例cli。

    /api/client/cli.go

    1. type DockerCli struct {
    2. proto string
    3. addr string
    4. configFile *registry.ConfigFile
    5. in io.ReadCloser
    6. out io.Writer
    7. err io.Writer
    8. isTerminal bool
    9. terminalFd uintptr
    10. tlsConfig *tls.Config
    11. scheme string
    12. }
    13. func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string, tlsConfig *tls.Config) *DockerCli {
    14. var (
    15. isTerminal = false
    16. terminalFd uintptr
    17. scheme = "http"
    18. )
    19. if tlsConfig != nil {
    20. scheme = "https"
    21. }
    22. if in != nil {
    23. if file, ok := out.(*os.File); ok {
    24. terminalFd = file.Fd()
    25. isTerminal = term.IsTerminal(terminalFd)
    26. }
    27. }
    28. if err == nil {
    29. err = out
    30. }
    31. return &DockerCli{
    32. proto: proto,
    33. addr: addr,
    34. in: in,
    35. out: out,
    36. err: err,
    37. isTerminal: isTerminal,
    38. terminalFd: terminalFd,
    39. tlsConfig: tlsConfig,
    40. scheme: scheme,
    41. }
    42. }

    2. Docke命令执行

    2.1. Docker Client解析请求命令

    创建Docker Client,docker命令中的请求参数(例如ps,经flag解析后放入flag.Args()),分析请求参数及请求的类型,转义为Docker Server可识别的请求后发给Docker Server。

    /docker/docker.go

    1. if err := cli.Cmd(flag.Args()...); err != nil {
    2. if sterr, ok := err.(*utils.StatusError); ok {
    3. if sterr.Status != "" {
    4. log.Println(sterr.Status)
    5. }
    6. os.Exit(sterr.StatusCode)
    7. }
    8. log.Fatal(err)
    9. }

    解析flag.Args()的具体请求参数,执行cli.Cmd函数。代码在/api/client/cli.go

    /api/client/cli.go

    1. // Cmd executes the specified command
    2. func (cli *DockerCli) Cmd(args ...string) error {
    3. if len(args) > 0 {
    4. method, exists := cli.getMethod(args[0])
    5. if !exists {
    6. fmt.Println("Error: Command not found:", args[0])
    7. return cli.CmdHelp(args[1:]...)
    8. }
    9. return method(args[1:]...)
    10. }
    11. return cli.CmdHelp(args...)
    12. }
    13. method, exists := cli.getMethod(args[0])获取请求参数,例如docker pull ImageNameargs[0]等于pull
    14. func (cli *DockerCli) getMethod(name string) (func(...string) error, bool) {
    15. if len(name) == 0 {
    16. return nil, false
    17. }
    18. methodName := "Cmd" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:])
    19. method := reflect.ValueOf(cli).MethodByName(methodName)
    20. if !method.IsValid() {
    21. return nil, false
    22. }
    23. return method.Interface().(func(...string) error), true
    24. }

    在getMethod中,返回method值为“CmdPull”。最后执行method(args[1:]…),即CmdPull(args[1:]…)。

    2.2. Docker Client执行请求命令

    docker pull ImageName中,即执行CmdPull(args[1:]…),args[1:]即为ImageName。命令代码在/api/client/command.go。

    /api/client/commands.go

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

    将args参数进行第二次flag参数解析,解析过程中先提取是否有符合tag这个flag的参数,若有赋值给tag参数,其余存入cmd.NArg(),若没有则所有的参数存入cmd.NArg()中。

    /api/client/commands.go

    1. var (
    2. v = url.Values{}
    3. remote = cmd.Arg(0)
    4. )
    5. v.Set("fromImage", remote)
    6. if *tag == "" {
    7. v.Set("tag", *tag)
    8. }
    9. remote, _ = parsers.ParseRepositoryTag(remote)
    10. // Resolve the Repository name from fqn to hostname + name
    11. hostname, _, err := registry.ResolveRepositoryName(remote)
    12. if err != nil {
    13. return err
    14. }

    通过remote变量先得到镜像的repository名称,并赋值给remote自身,随后解析改变后的remote,得出镜像所在的host地址,即Docker Registry的地址。若没有指定默认为Docker Hub地址https://index.docker.io/v1/。

    /api/client/commands.go

    1. cli.LoadConfigFile()
    2. // Resolve the Auth config relevant for this server
    3. authConfig := cli.configFile.ResolveAuthConfig(hostname)

    通过cli对象获取与Docker Server的认证配置信息。

    /api/client/commands.go

    1. pull := func(authConfig registry.AuthConfig) error {
    2. buf, err := json.Marshal(authConfig)
    3. if err != nil {
    4. return err
    5. }
    6. registryAuthHeader := []string{
    7. base64.URLEncoding.EncodeToString(buf),
    8. }
    9. return cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{
    10. "X-Registry-Auth": registryAuthHeader,
    11. })
    12. }

    定义pull函数:cli.stream(“POST”, “/images/create?”+v.Encode(),…)像Docker Server发送POST请求,请求url为“”/images/create?”+v.Encode()”,请求的认证信息为:map[string][]string{“X-Registry-Auth”: registryAuthHeader,}

    /api/client/commands.go

    1. if err := pull(authConfig); err != nil {
    2. if strings.Contains(err.Error(), "Status 401") {
    3. fmt.Fprintln(cli.out, "/nPlease login prior to pull:")
    4. if err := cli.CmdLogin(hostname); err != nil {
    5. return err
    6. }
    7. authConfig := cli.configFile.ResolveAuthConfig(hostname)
    8. return pull(authConfig)
    9. }
    10. return err
    11. }
    12. return nil

    调用pull函数,实现下载请求发送。后续有Docker Server接收到请求后具体实现。

    参考:

    • 《Docker源码分析》