Docker源码学习笔记二、Docker Client
作为一种轻量级虚拟化容器管理引擎,docker给开发者提供了一种新颖、便捷的软件集成测试和部署的方式。Docker采用C/S架构进行设计,用户与Docker交互时,通过Docker Client发送一系列命令,因此使用Docker时最先感受到的存在就是Docker Client。
1. 创建Docker Client
对于C/S架构的应用,Client一般扮演着命令发送者的角色,Docker Client也是一样。用户需要先创建一个Docker Client,随后将特定的请求类型与参数传递至Docker Client,最终由Docker Client转义成Docker Server能识别的形式,并发送至Docker Server。另外,DockerClient的创建需要依据docker二进制文件。
1.1. Docker命令的flag参数解析
Docker Server和Dcker Client都是由可执行文件docker来创建并启动,docker -d
命令是启动Docker Server,docker ps
和docker pull
等命令是启动Docker Client。
Docker请求中的参数可以归为两类:第一类为命令行参数,如docker -d
;第二类为docker发送给Docker Server的实际请求参数,如docker ps
和docker pull
。对于第一类参数,通常称为flag参数,利用Golang提供的专门的flag包进行解析。
源码中./docker/docker/docker.go
,包含了整个Docker的main函数,即整个Docker(不论是Docker Client还是Docker Daemon)的运行入口。事实上,fdocker定义的flag参数并不多(11个,4个在main函数启动前自动初始化),下面列出5个常用的。
var (
...
// 定义多个flag参数,在main函数执行之前,这些参数未被初始化
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 mode\nuse '' (the empty string) to disable setting of a group")
flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by tls-verify flags")
...
)
假设我现在要执行docker -d -v ps
命令,flag参数解析会完成两个工作:首先完成命令行flag参数的解析,拿到flDaemon和flVersion;其次,遇到第一个非定义的flag参数ps时,flag包会将ps及其之后所有参数放入flag.Args(),为之后执行Docker Client具体请求使用。
1.2. 处理flag信息并收集Docker Client的配置信息
1.3. 创建DockerClient
Docker Client的创建其实就是在已有配置参数信息的情况下,通过Client包中的NewDockerCli方法创建一个Docker Client示例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)
}
2. Docker命令执行
创建完Docker Client,现在拿到了一个DockerCli实例以及docker中请求参数(flag.Args())。接下来程序需要使用Docker Client解析出Docker命令中的请求参数,转义为Docker Server可以识别的请求。
2.1. Docker Client解析请求
Docker Client首先解析存放于flag.Args()中的具体请求参数。
// 解析存放于flag.Args()中的具体请求参数
if err := cli.Cmd(flag.Args()...); err != nil {
if sterr, ok := err.(*utils.StatusError); ok {
if sterr.Status != "" {
log.Println(sterr.Status)
}
os.Exit(sterr.StatusCode)
}
log.Fatal(err)
}
cli.Cmd(flag.Args()...)
函数执行具体的指令。如docker -d -v pull nginx
命令执行步骤如下:
- 解析flag参数,Docker将请求参数
pull
和nginx
放在flag.Args()
。 - 创建好的Docker Client为cli,cli执行cli.Cmd(flag.Args()…)。
- Cmd函数中,通过args[0]即
pull
,执行cli.getMethod(args[0])
,获取method的名称。 - 在getMethod()方法中,Docker通过处理最终返回的method值为
CmdPull
。 - 最终执行
method(args[1:]...)
也就是CmdPull(args[1:]...)
。
2.2. Docker Client执行请求命令
上一小节中docker pull
的例子通过反射将命令的最终执行任务放到CmdPull
方法中。其中的pull函数实现实际意义上的下载请求发送。总之,请求执行过程中,大多是命令行中关于请求的参数进行初步处理,最终根据约定好的API,通过指定协议向Docker Server发送请求。
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.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 对象获取与Docker Server 通信所需的认证配置信息
cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(hostname)
// 定义一个名为pull的函数,,传人的参数类型为registry.AuthConfig ,返回类型为error。
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,
})
}
// 这一步骤具体调用执行pull 函数,实现实际意义上的下载请求发送
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
}
return nil
}