众所周知,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的笔记
路漫漫,其修远兮