衆所周知,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的筆記
路漫漫,其修遠兮