《容器技术系列》一2.2 创建Docker Client

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 本节书摘来华章计算机《容器技术系列》一书中的第2章 ,第2.2节,孙宏亮 著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。 2.2 创建Docker Client 对于Docker这样一个Client/Server的架构,客户端的存在意味着Docker相应任务的发起。

本节书摘来华章计算机《容器技术系列》一书中的第2章 ,第2.2节,孙宏亮 著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.2 创建Docker Client

对于Docker这样一个Client/Server的架构,客户端的存在意味着Docker相应任务的发起。用户首先需要创建一个DockerClient,随后将特定的请求类型与参数传递至Docker Client,最终由Docker Client转义成Docker Server能识别的形式,并发送至Docker Server。
Docker Client的创建实质上是Docker用户通过二进制可执行文件docker,创建与Docker Server建立联系的客户端。以下分3个小节分别阐述Docker Client的创建流程。
Docker Client完整的运行流程如图2-1所示。

image


通过学习图2-1,我们可以更为清晰地了解Docker Client创建及执行请求的过程。其中涉及诸多Docker源码层次中的专有名词,本章后续会一一解释与分析。

2.2.1 Docker命令的flag参数解析

众所周知,在Docker的具体实现中,Docker Server与Docker Client均由可执行文件docker来完成创建并启动。那么,了解docker可执行文件通过何种方式来区分到底是Docker Server还是Docker Client,就显得尤为重要。
首先通过docker命令举例说明其中的区别。Docker Server的启动,命令为docker -d或docker --daemon=true;而Docker Client的启动则体现为docker --daemon=false ps、docker pull NAME等。
其实,对于Docker请求中的参数,我们可以将其分为两类:第一类为命令行参数,即docker程序运行时所需提供的参数,如: -D、--daemon=true、--daemon=false等;第二类为docker发送给Docker Server的实际请求参数,如:ps、pull NAME等。
对于第一类,我们习惯将其称为flag参数,在Go语言的标准库中,专门为该类参数提供了一个flag包,方便进行命令行参数的解析。
清楚docker二进制文件的使用以及基本的命令行flag参数之后,我们可以进入实现Docker Client创建的源码中,位于./docker/docker/docker.go。这个go文件包含了整个Docker的main函数,也就是整个Docker(不论Docker Daemon还是Docker Client)的运行入口。部分main函数代码如下:

func main() {
     if reexec.Init() {
          return
     }
     flag.Parse()
     // FIXME: validate daemon flags here
     ...
}

以上源码实现中,首先判断reexec.Init()方法的返回值,若为真,则直接退出运行,否则将继续执行。reexec.Init()函数的定义位于./docker/reexec/reexec.go,可以发现由于在docker运行之前没有任何Initializer注册,故该代码段执行的返回值为假。reexec存在的作用是:协调execdriver与容器创建时dockerinit这两者的关系。第13章在分析dockerinit的启动时,将详细讲解reexec的作用。
判断reexec.Init()之后,Docker的main函数通过调用flag.Parse()函数,解析命令行中的flag参数。如果熟悉Go语言中的flag参数,一定知道解析flag参数的值之前,程序必须先定义相应的flag参数。进一步查看Docker的源码,我们可以发现Docker在./docker/docker/flag.go中定义了多个flag参数,并通过init函数进行部分flag参数的初始化。代码如下:

var (
        flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit")
    flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode")
    flDebug = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode")
        flSocketGroup = flag.String([]string{"G", "-group"}, "docker", "Group to assign the unix socket specified by -H when running in daemon modeuse '' (the empty string) to disable setting of a group")
        flEnableCors = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API")
        flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by tls-verify flags")
        flTlsVerify = flag.Bool([]string{"-tlsverify"}, false, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)")

        // these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs
    flCa    *string
    flCert  *string
    flKey   *string
    flHosts []string
    )

    func init() {
        flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust only remotes providing a certificate signed by the CA given here")
        flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file")
        flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file")
        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.")
}

以上源码展示了Docker如何定义flag参数,以及在init函数中实现部分flag参数的初始化。Docker的main函数执行前,这些变量创建以及初始化工作已经全部完成。这里涉及了Go语言的一个特性,即init函数的执行。Go语言中引入其他包(import package)、变量的定义、init函数以及main函数这四者的执行顺序如图2-2所示。
关于Golang中的init函数,深入分析可以得出以下特性:
init函数用于程序执行前包的初始化工作,比如初始化变量等;
每个包可以有多个init函数;
包的每一个源文件也可以有多个init函数;
同一个包内的init函数的执行顺序没有明确的定义;
不同包的init函数按照包导入的依赖关系决定初始化的顺序;
init函数不能被调用,而是在main函数调用前自动被调用。

image


清楚Go语言一些基本的特性之后,回到Docker中来。Docker的main函数执行之前,Docker已经定义了诸多flag参数,并对很多flag参数进行初始化。定义并初始化的命令行flag参数有:flVersion、flDaemon、flDebug、flSocketGroup、flEnableCors、flTls、flTlsVerify、flCa、flCert、flKey、flHosts等。
以下具体分析flDaemon:

  • 定义:flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode");
  • flDaemon的类型为Bool类型;
  • flDaemon名称为"d"或者"-daemon",该名称会出现在docker命令中,如docker –d;
  • flDaemon的默认值为false;
  • flDaemon的用途信息为"Enable daemon mode";
  • 访问flDaemon的值时,使用指针*flDaemon解引用访问。
  • 在解析命令行flag参数时,以下语句为合法的(以flDaemon为例):
  • -d, --daemon
  • -d=true, --daemon=true
  • -d="true", --daemon="true"
  • -d='true', --daemon='true'

当解析到第一个非定义的flag参数时,命令行flag参数解析工作结束。举例说明,当执行docker命令docker --daemon=false --version=false ps时,flag参数解析主要完成两个工作:
完成命令行flag参数的解析,根据flag的名称-daemon和-version,得知具体的flag参数为flDaemon和flVersion,并获得相应的值,均为false。
遇到第一个非定义的flag参数ps时,flag包会将ps及其之后所有的参数存入flag.Args(),以便之后执行Docker Client具体的请求时使用。
如需深入学习flag的实现,可以参见Docker源码./docker/pkg/mflag/flag.go。

2.2.2 处理flag信息并收集Docker Client的配置信息

理解Go语言解析flag参数的相关知识,可以很大程度上帮助理解Docker的main函数的执行流程。通过总结,首先列出源码中处理的flag信息以及收集Docker Client的配置信息,然后再一一进行分析:
处理的flag参数有:flVersion、flDebug、flDaemon、flTlsVerify以及flTls。
为Docker Client收集的配置信息有:protoAddrParts(通过flHosts参数获得,作用是提供Docker Client与Docker Server的通信协议以及通信地址)、tlsConfig(通过一系列flag参数获得,如flTls、flTlsVerify,作用是提供安全传输层协议的保障)。
清楚flag参数以及Docker Client的配置信息之后,我们进入main函数的源码,具体分析如下。
在flag.Parse()之后的源码如下:

if *flVersion {
     showVersion()
     return
}

以上代码很好理解,解析flag参数后,若Docker发现flag参数flVersion为真,则说明Docker用户希望查看Docker的版本信息。此时,Docker调用showVersion()显示版本信息,并从main函数退出;否则的话,继续往下执行。

if *flDebug {
     os.Setenv("DEBUG", "1")
}

若flDebug参数为真的话,Docker通过os包中的Setenv函数创建一个名为DEBUG的环境变量,并将其值设为"1";继续往下执行。

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)
      }
      if _, err := api.ValidateHost(defaultHost); err != nil {
           log.Fatal(err)
      }
      flHosts = append(flHosts, defaultHost)
}

以上的源码主要分析内部变量flHosts。flHosts的作用是为Docker Client提供所要连接的host对象,也就是为Docker Server提供所要监听的对象。
在分析过程中,首先判断flHosts变量是否长度为0。若是的话,则说明用户并没有显性传入地址,此时Docker的策略为选用默认值。Docker通过os包获取名为DOCKER_HOST环境变量的值,将其赋值于defaultHost。若defaultHost为空或者flDaemon为真,说明目前还没有一个定义的host对象,则将其默认设置为unix socket,值为api.DEFAULTUNIXSOCKET,该常量位于./docker/api/common.go,值为"/var/run/docker.sock",故defaultHost为"unix:///var/run/docker.sock"。验证该defaultHost的合法性之后,将defaultHost的值追加至flHost的末尾,继续往下执行。当然若flHost的长度不为0,则说明用户已经指定地址,同样继续往下执行。

if *flDaemon {
     mainDaemon()
     return
}

若flDaemon参数为真,则说明用户的需求是启动Docker Daemon。Docker随即执行mainDaemon函数,实现Docker Daemon的启动;若mainDaemon函数执行完毕,则退出main函数。一般mainDaemon函数不会主动终结,Docker Daemon将作为一个常驻进程运行在宿主机上。本章着重介绍Docker Client的启动,故假设flDaemon参数为假,不执行以上代码块。继续往下执行。

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

由于不执行Docker Daemon的启动流程,故属于Docker Client的执行逻辑。首先,判断flHosts的长度是否大于1。若flHosts的长度大于1,则说明需要新创建的Docker Client访问不止1个Docker Daemon地址,显然逻辑上行不通,故抛出错误日志,提醒用户只能指定一个Docker Daemon地址。接着,Docker将flHosts这个string数组中的第一个元素进行分割,通过"://"来分割,分割出的两个部分放入变量protoAddrParts数组中。protoAddrParts的作用是:解析出Docker Client与Docker Server建立通信的协议与地址,为Docker Client创建过程中不可或缺的配置信息之一。一般情况下,flHosts[0]的值可以是tcp://0.0.0.0:2375或者unix:///var/run/docker.sock等。

var (
     cli       *client.DockerCli
     tlsConfig tls.Config
)
tlsConfig.InsecureSkipVerify = true

由于之前已经假设过flDaemon为假,可以认定main函数的运行是为了Docker Client的创建与执行。Docker在这里创建了两个变量:一个为类型是*client.DockerCli的对象cli,另一个为类型是tls.Config的对象tlsConfig。定义完变量之后,Docker将tlsConfig的InsecureSkipVerify属性置为真。tlsConfig对象的创建是为了保障cli在传输数据的时候遵循安全传输层协议(TLS)。安全传输层协议(TLS)用于确保两个通信应用程序之间的保密性与数据完整性。tlsConfig是Docker Client创建过程中可选的配置信息。

// If we should verify the server, we need to load a trusted 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
}

若flTlsVerify这个flag参数为真,则说明Docker Client需Docker Server一起验证连接的安全性。此时,tlsConfig对象需要加载一个受信的ca文件。该ca文件的路径为*flCA参数的值,最终完成tlsConfig对象中RootCAs属性的赋值,并将InsecureSkipVerify属性置为假。

// 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}
     }
}

如果flTls和flTlsVerify两个flag参数中有一个为真,则说明需要加载并发送客户端的证书。最终将证书内容交给tlsConfig的Certificates属性。
至此,flag参数已经全部处理完毕,DockerClient也已经收集到所需的配置信息。下一节将主要分析如何创建Docker Client。

2.2.3 如何创建Docker Client

Docker Client的创建其实就是在已有配置参数信息的情况下,通过Client包中的NewDockerCli方法创建一个Docker Clinet实例cli。具体源码实现如下:

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

若flag参数flTls为真或者flTlsVerify为真,则说明需要使用TLS协议来保障传输的安全性,故创建Docker Client的时候,将tlsConfig参数传入;否则,同样创建Docker Client,只不过tlsConfig为nil。
关于Client包中的NewDockerCli函数的实现,可以具体参见./docker/api/clie`javascript
nt/cli.go。
func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string, tlsConfig tls.Config) DockerCli {
var (

 isTerminal = false
 terminalFd uintptr
 scheme     = "http"

)

if tlsConfig != nil {

 scheme = "https"

}

if in != nil {

 if file, ok := out.(*os.File); ok {
      terminalFd = file.Fd()
      isTerminal = term.IsTerminal(terminalFd)
 }

}

if err == nil {

 err = out

}
return &DockerCli{

 proto:      proto,
 addr:       addr,
 in:         in,
 out:        out,
 err:        err,
 isTerminal: isTerminal,
 terminalFd: terminalFd,
 tlsConfig:  tlsConfig,
 scheme:     scheme,
 }

}

总体而言,创建DockerCli对象的过程比较简单。较为重要的DockerCli的属性有:proto,DockerClient与Docker Server的传输协议;addr,Docker Client需要访问的host目标地址;tlsConfig,安全传输层协议的配置。若tlsConfig不为空,则说明需要使用安全传输层协议,DockerCli对象的scheme设置为“https”,另外还有关于输入、输出以及错误显示的配置等。最终函数返回DockerCli对象。
相关文章
|
15小时前
|
Docker 容器
如何修改docker容器的端口映射
如何修改docker容器的端口映射
|
3天前
|
监控 数据可视化 Linux
如何使用可视化管理工具DockerUI远程管理docker容器
如何使用可视化管理工具DockerUI远程管理docker容器
|
3天前
|
NoSQL 应用服务中间件 nginx
【Docker】3、Docker 基本操作【容器操作】
【Docker】3、Docker 基本操作【容器操作】
16 0
|
4天前
|
消息中间件 数据安全/隐私保护 Docker
Docker容器常用命令
Docker容器常用命令
|
4天前
|
关系型数据库 MySQL Linux
docker镜像与容器的迁移
docker迁移镜像步骤 docker迁移容器步骤 docker迁移mysql容器步骤
|
5天前
|
Kubernetes 测试技术 Linux
【Docker管理工具】安装Docker容器自动更新工具Watchtower
【2月更文挑战第8天】安装Docker容器自动更新工具Watchtower
35 0
|
11天前
|
IDE 数据中心 Docker
使用PyCharm与Docker容器进行开发:从入门到精通
使用PyCharm与Docker容器进行开发:从入门到精通
|
2月前
|
Docker 容器
Docker学习笔记三:如何运行一个容器?
Docker学习笔记三:如何运行一个容器?
Docker学习笔记三:如何运行一个容器?
|
2月前
|
Java 持续交付 Docker
微服务框架(二十一)Piplin 持续部署 Docker 容器
此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。 本文为使用Piplin 持续部署 Docker 容器
|
9天前
|
存储 监控 测试技术
【Docker管理工具】安装Oxker容器管理工具
【2月更文挑战第4天】安装Oxker容器管理工具
47 2

相关产品

  • 容器镜像服务
  • 容器服务Kubernetes版