docker源码阅读笔记三

"docker客户端与服务器端通信模块"

Posted by Xu on April 20, 2017

docker源码阅读笔记三-docker客户端与服务器端通信模块

今天我们需要了解的内容主要有如下几个部分:
1.客户端发送请求
2.服务器端接收请求
//下一节对第三块内容进行展开分析
3.服务器解析请求并实现路由(下一节)

1.客户端发送请求

在docker源码阅读笔记二中我们分析到当客户端发送checkpoint create命令请求时使用了cli.post(ctx, “/containers/”+container+”/checkpoints”, nil, options, nil)方法,我们从这个方法入手,分析docker客户端发送请求的流程

func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) {
    //将请求数据编码成http请求的body(json)及header,header为空设置为默认header   "Content-Type" : "application/json"
	body, headers, err := encodeBody(obj, headers)
	if err != nil {
		return serverResponse{}, err
	}
	return cli.sendRequest(ctx, "POST", path, query, body, headers)//根据编码的http body及header发送http请求
}

http发送请求的实现:

func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) {
	req, err := cli.buildRequest(method, cli.getAPIPath(path, query), body, headers)//根据参数信息构建http请求体对象
	if err != nil {
		return serverResponse{}, err
	}
	return cli.doRequest(ctx, req)//客户端执行该请求体对象,发送请求
}

doRequest(ctx,req)函数内只做了两件事:

1.发送请求resp, err := ctxhttp.Do(ctx, cli.client, req),接受响应,ctxhttp.Do函数实现http架包DefaultClient.do(req.WithContext)//这部分的实现是http jar包中的实现内容

2.请求失败处理,及当1中的err不为空时根据错误信息做出相应的处理

2.服务器端接收请求数据

2.1 服务器端命令行接口的创建

在分析docker daemon接受请求数据之前,我们先看看docker daemon的启动过程: docker daemon启动涉及到的最重要的函数为newDaemonCommand()(定义在cmd/dockerd/docker.go文件),它主要在两个地方被调用,一个是cmd/dockerd/docker.go文件中main()调用启动daemon,另一个地方就是cmd/docker/docker.go文件中的newDockerCommand()中调用该函数启动daemon。

接下来从newDaemonCommand()展开分析,研究Docker daemon启动过程: 该函数的目的与newDockerCommand()目的一样,是为了构建一个docker服务器命令行接口对象,命令行接口包含了docker服务器所有可以执行的命令,并通过每一个命令结构体对象中的Run等成员函数来具体执行,同时该docker服务器命令行接口也为一个Command结构体对象。

func newDaemonCommand() *cobra.Command {
	opts := daemonOptions{
		daemonConfig: config.New(),
		common:       cliflags.NewCommonOptions(),
	}
//构建docker服务器端命令行接口对象cmd
	cmd := &cobra.Command{
		Use:           "dockerd [OPTIONS]",
		Short:         "A self-sufficient runtime for containers.",
		SilenceUsage:  true,
		SilenceErrors: true,
		Args:          cli.NoArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			opts.flags = cmd.Flags()
			return runDaemon(opts)//当该docker服务器命令行接口Command结构体执行时,则执行该runDaemon(opts)函数
		},
	}
	
	cli.SetupRootCommand(cmd)
    
    //设置一些默认的服务器命令行接口的参数
	flags := cmd.Flags()
	flags.BoolVarP(&opts.version, "version", "v", false, "Print version information and quit")
	flags.StringVar(&opts.configFile, flagDaemonConfigFile, defaultDaemonConfigFile, "Daemon configuration file")
	opts.common.InstallFlags(flags)
	installConfigFlags(opts.daemonConfig, flags)
	installServiceFlags(flags)

	return cmd
}

从newDaemonCommand()代码分析中可以知道该函数主要分为两个步骤: 1.根据可选项参数opts创建docker DaemonCommand结构体对象,并定义该Command执行函数RunE,当RunE执行时,主要在于runDaemon(opts)的执行步骤,该函数将在接下来进行分析。

2.对一些Docker daemon参数flag进行赋值和默认配置的设置。

2.2 服务器端命令行接口的执行

当daemon服务器命令行接口定义好并执行时,runDaemon开始运作,通过阅读该函数代码发现这个函数主要部分可以用两行代码概括:

   daemonCli:=NewDaemonCli()//创建daemon客户端对象
   daemonCli.start(opts)//启动daemonCli

第一行代码很好理解,创建一个DaemonCli结构体对象,该结构体包含配置信息,配置文件,参数信息,APIServer,Daemon对象,authzMiddleware(认证插件)

   type DaemonCli struct {
	*config.Config  //配置信息
	configFile *string //配置文件
	flags      *pflag.FlagSet  //flag参数信息
	api             *apiserver.Server //APIServer:提供api服务,定义在docker/api/server/server.go
	d               *daemon.Daemon  //Daemon对象,结构体定义在daemon/daemon.go文件中
	authzMiddleware *authorization.Middleware // authzMiddleware enables to dynamically reload the authorization plugins
}

由于Daemon结构体过于复杂,在这里先不做具体描述,而APIServer在接下来的daemonCli.start()实现过程中具有非常重要的作用,所以先对APIServer结构体进行分析:

 // Server contains instance details for the server
type Server struct {
	cfg           *Config//apiserver的配置信息
	servers       []*HTTPServer//httpServer结构体对象,包括http.Server和net.Listener监听器。
	routers       []router.Router//路由表对象Route,包括Handler,Method, Path
	routerSwapper *routerSwapper//路由交换器对象,使用新的路由交换旧的路由器
	middlewares   []middleware.Middleware//中间件
}

服务器daemonCli 启动核心函数daemonCli. start()分析

由于该函数实现代码复杂,所以在这里我只给出我对该函数实现步骤的分析和理解,daemonCli.start(opts)实现步骤主要分为如下18个步骤:

(1).opts.common.setDefaultOption()设置默认可选项参数

(2).loadDaemonCliConfig(opts)根据opts对象信息来加载DaemonCli的配置信息config对象,并将该config对象配置到DaemonCli结构体对象中去。

(3).对DaemonCli结构体中的其它成员根据opts进行配置。

(4).根据DaemonCli结构体对象中的信息定义APIServer配置信息结构体对象&apiserver.Config(包括tls传输层协议信息)

(5).根据定义好的&apiserver.Config新建APIServer对象:
api := apiserver.New(serverConfig)

(6).解析host文件及传输协议(tcp)等内容

(7).根据host解析内容初始化监听器listener.Init()。

(8).api.Accept(addr, ls…),为建立好的APIServer设置我们初始化的监听器listener,可以监听该地址的连接。

(9).根据DaemonCli.Config.ServiceOptions来注册一个新的服务对象
registryService := registry.NewService(cli.Config.ServiceOptions)

(10).根据DaemonCli中的相关信息来新建libcontainerd对象
containerdRemote, err := libcontainerd.New(cli.getLibcontainerdRoot(), cli.getPlatformRemoteOptions()…)

(11).设置信号捕获
signals := []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGPIPE}包括如下四种信号,传入一个cleanup函数作为参数,捕获到了这几种信号,可以利用该函数进行shutdown善后处理:

signal.Trap(func() { cli.stop()//关闭apiserver <-stopc // wait for daemonCli.start() to return,处于阻塞状态,等待stopc通道返回数据。 })

(12).preNotifySystem()提前通知系统api可以工作了,但是要在daemon安装成功之后。

(13).根据DaemonCli的配置信息,注册的服务对象及libcontainerd对象来构建Daemon对象。
d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote)

(14).新建cluster对象:

   c, err := cluster.New(cluster.Config{
		Root:                   cli.Config.Root,
		Name:                   name,
		Backend:                d,
		NetworkSubnetsProvider: d,
		DefaultAdvertiseAddr:   cli.Config.SwarmDefaultAdvertiseAddr,
		RuntimeRoot:            cli.getSwarmRunRoot(),
	})
   d.SetCluster(c)
   



(15).重启Swarm容器:d.RestartSwarmContainers()

(16).将新建的Daemon对象与DaemonCli相关联:cli.d = d

(17).初始化路由器:initRouter(api, d, c)

(18).

   go api.Wait(serveAPIWait)//新建goroutine来监听apiserver执行情况,当执行报错时通道serverAPIWait就会传出错误信息
   // after the daemon is done setting up we can notify systemd api
	notifySystem()//通知系统Daemon已经安装完成,可以提供api服务了
	// Daemon is fully initialized and handling API traffic
	// Wait for serve API to complete
	errAPI := <-serveAPIWait//等待apiserver执行出现错误,没有错误则会阻塞到该语句
	c.Cleanup()//执行到这一步说明,serverAPIWait有错误信息传出,所以对cluster进行清理操作
	shutdownDaemon(d)//同上,关闭Daemon
	containerdRemote.Cleanup()//同上关闭libcontainerd
	if errAPI != nil {
		return fmt.Errorf("Shutting down due to ServeAPI error: %v", errAPI)
	}