1. 程式人生 > >Docker原始碼分析(二)之Docker Client

Docker原始碼分析(二)之Docker Client

一、建立Docker Client

    Docker是一個client/server的架構,通過二進位制檔案docker建立Docker客戶端將請求型別與引數傳送給Docker Server,Docker Server具體執行命令呼叫。

Docker Client執行流程圖如下:

說明:本文分析的程式碼為Docker 1.2.0版本。

(一)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

func main() {

if reexec.Init() {

return

}

flag.Parse()

// FIXME: validate daemon flags here

......

}

reexec.Init()作用:協調execdriver與容器建立時dockerinit的關係。如果返回值為真則直接退出執行,否則繼續執行。判斷reexec.Init()之後,呼叫flag.Parse()解析命令列中的flag引數。

/docker/flag.go

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 mode\nuse '' (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.")

}

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(),以便執行之後的具體請求。

(二)處理flag引數並收集Docker Client的配置資訊

處理的flag引數有flVersion,flDebug,flDaemon,flTlsVerify以及flTls。
 

/docker/docker.go

func main() {

......

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的作用是為Docker Client提供所要連線的host物件,也就是為Docker Server提供所要監聽的物件。

當flHosts為空,預設取環境變數DOCKER_HOST,若仍為空或flDaemon為真,則設定為unix socket,值為unix:///var/run/docker.sock。取自/api/common.go中的常量DEFAULTUNIXSOCKET。

/docker/docker.go

func main() {  

...

if *flDaemon {

mainDaemon()

return

}

...

}


若flDaemon為真,表示啟動Docker Daemon,呼叫/docker/daemon.go中的func mainDaemon()。

/docker/docker.go

if len(flHosts) > 1 {

log.Fatal("Please specify only one -H")

}

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

var (

cli       *client.DockerCli

tlsConfig tls.Config

)

tlsConfig.InsecureSkipVerify = true


tlsConfig物件的建立是為了保障cli在傳輸資料的時候遵循安全傳輸層協議(TLS)。flTlsVerity引數為真,則說明Docker Client 需Docker Server一起驗證連線的安全性,如果flTls和flTlsVerity兩個引數中有一個為真,則說明需要載入併發送客戶端的證書。
 

/docker/flags.go

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)")

(三)如何建立Docker Client

/docker/docker.go

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)

}


在已有配置引數的情況下,通過/api/client/cli.go中的NewDockerCli方法建立Docker Client例項cli。
 

/api/client/cli.go

type DockerCli struct {

proto      string

addr       string

configFile *registry.ConfigFile

in         io.ReadCloser

out        io.Writer

err        io.Writer

isTerminal bool

terminalFd uintptr

tlsConfig  *tls.Config

scheme     string

}

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,

}

}

二、Docke命令執行

(一)Docker Client解析請求命令

建立Docker Client,docker命令中的請求引數(例如ps,經flag解析後放入flag.Args()),分析請求引數及請求的型別,轉義為Docker Server可識別的請求後發給Docker Server。
 

/docker/docker.go

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)

}


解析flag.Args()的具體請求引數,執行cli.Cmd函式。程式碼在/api/client/cli.go

/api/client/cli.go

// Cmd executes the specified command

func (cli *DockerCli) Cmd(args ...string) error {

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...)

}

method, exists := cli.getMethod(args[0])獲取請求引數,例如docker pull ImageName,args[0]等於pull。

func (cli *DockerCli) getMethod(name string) (func(...string) error, bool) {

if len(name) == 0 {

return nil, false

}

methodName := "Cmd" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:])

method := reflect.ValueOf(cli).MethodByName(methodName)

if !method.IsValid() {

return nil, false

}

return method.Interface().(func(...string) error), true

}


在getMethod中,返回method值為“CmdPull”。最後執行method(args[1:]...),即CmdPull(args[1:]...)。

(二)Docker Client執行請求命令

docker pull ImageName中,即執行CmdPull(args[1:]...),args[1:]即為ImageName。命令程式碼在/api/client/command.go。
 

/api/client/commands.go

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

}

...

}


將args引數進行第二次flag引數解析,解析過程中先提取是否有符合tag這個flag的引數,若有賦值給tag引數,其餘存入cmd.NArg(),若沒有則所有的引數存入cmd.NArg()中。
 

/api/client/commands.go

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

}

通過remote變數先得到映象的repository名稱,並賦值給remote自身,隨後解析改變後的remote,得出映象所在的host地址,即Docker Registry的地址。若沒有指定預設為Docker Hub地址https://index.docker.io/v1/。

/api/client/commands.go

cli.LoadConfigFile()

// Resolve the Auth config relevant for this server

authConfig := cli.configFile.ResolveAuthConfig(hostname)


通過cli物件獲取與Docker Server的認證配置資訊。
 

/api/client/commands.go

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函式: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

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

呼叫pull函式,實現下載請求傳送。後續有Docker Server接收到請求後具體實現。
 

 文章參考:

《Docker原始碼分析》