Docker源码分析(二):Docker Client创建与命令执行

时间:2022-09-06 20:33:29

1. 前言

如今,Docker作为业界领先的轻量级虚拟化容器管理引擎,给全球开发者提供了一种新颖、便捷的软件集成测试与部署之道。在团队开发软件时,Docker可以提供可复用的运行环境、灵活的资源配置、便捷的集成测试方法以及一键式的部署方式。可以说,Docker的优势在简化持续集成、运维部署方面体现得淋漓尽致,它完全让开发者从持续集成、运维部署方面中解放出来,把精力真正地倾注在开发上。

然而,把Docker的功能发挥到极致,并非一件易事。在深刻理解Docker架构的情况下,熟练掌握Docker Client的使用也非常有必要。前者可以参阅《Docker源码分析》系列之Docker架构篇,而本文主要针对后者,从源码的角度分析Docker Client,力求帮助开发者更深刻的理解Docker Client的具体实现,最终更好的掌握Docker Client的使用方法。即本文为《Docker源码分析》系列的第二篇——Docker Client篇。

2. Docker Client源码分析章节安排

本文从源码的角度,主要分析Docker Client的两个方面:创建与命令执行。整个分析过程可以分为两个部分:

第一部分分析Docker Client的创建。这部分的分析可分为以下三个步骤:

  • 分析如何通过docker命令,解析出命令行flag参数,以及docker命令中的请求参数;
  • 分析如何处理具体的flag参数信息,并收集Docker Client所需的配置信息;
  • 分析如何创建一个Docker Client。

第二部分在已有Docker Client的基础上,分析如何执行docker命令。这部分的分析又可分为以下两个步骤:

  • 分析如何解析docker命令中的请求参数,获取相应请求的类型;
  • 分析Docker Client如何执行具体的请求命令,最终将请求发送至Docker Server。

3. Docker Client的创建

Docker Client的创建,实质上是Docker用户通过可执行文件docker,与Docker Server建立联系的客户端。以下分三个小节分别阐述Docker Client的创建流程。

以下为整个docker源代码运行的流程图:

Docker源码分析(二):Docker Client创建与命令执行

上图通过流程图的方式,使得读者更为清晰的了解Docker Client创建及执行请求的过程。其中涉及了诸多源代码中的特有名词,在下文中会一一解释与分析。

3.1. Docker命令的flag参数解析

众所周知,在Docker的具体实现中,Docker Server与Docker Client均由可执行文件docker来完成创建并启动。那么,了解docker可执行文件通过何种方式区分两者,就显得尤为重要。

对于两者,首先举例说明其中的区别。Docker Server的启动,命令为docker -d或docker --daemon=true;而Docker Client的启动则体现为docker --daemon=false ps、docker pull NAME等。

可以把以上Docker请求中的参数分为两类:第一类为命令行参数,即docker程序运行时所需提供的参数,如: -D、--daemon=true、--daemon=false等;第二类为docker发送给Docker Server的实际请求参数,如:ps、pull NAME等。

对于第一类,我们习惯将其称为flag参数,在go语言的标准库中,同时还提供了一个flag包,方便进行命令行参数的解析。

交待以上背景之后,随即进入实现Docker Client创建的源码,位于./docker/docker/docker.go,该go文件包含了整个Docker的main函数,也就是整个Docker(不论Docker Daemon还是Docker Client)的运行入口。部分main函数代码如下:

func main() {
if reexec.Init() {
return
}
flag.Parse()
// FIXME: validate daemon flags here
……
}

在以上代码中,首先判断reexec.Init()方法的返回值,若为真,则直接退出运行,否则的话继续执行。查看位于./docker/reexec/reexec.go中reexec.Init()的定义,可以发现由于在docker运行之前没有任何的Initializer注册,故该代码段执行的返回值为假。

紧接着,main函数通过调用flag.Parse()解析命令行中的flag参数。查看源码可以发现Docker在./docker/docker/flag.go中定义了多个flag参数,并通过init函数进行初始化。代码如下:

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 use '' (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.")
}

这里涉及到了Golang的一个特性,即init函数的执行。在Golang中init函数的特性如下:

  • init函数用于程序执行前包的初始化工作,比如初始化变量等;
  • 每个包可以有多个init函数;
  • 包的每一个源文件也可以有多个init函数;
  • 同一个包内的init函数的执行顺序没有明确的定义;
  • 不同包的init函数按照包导入的依赖关系决定初始化的顺序;
  • init函数不能被调用,而是在main函数调用前自动被调用。

因此,在main函数执行之前,Docker已经定义了诸多flag参数,并对很多flag参数进行初始化。定义的命令行flag参数有:flVersion、flDaemon、flDebug、flSocketGroup、flEnableCors、flTls、flTlsVerify、flCa、flCert、flKey等。

以下具体分析flDaemon:

  • 定义:flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode")
  • flDaemon的类型为Bool类型
  • flDaemon名称为”d”或者”-daemon”,该名称会出现在docker命令中
  • flDaemon的默认值为false
  • flDaemon的帮助信息为”Enable daemon mode”
  • 访问flDaemon的值时,使用指针* flDaemon解引用访问

在解析命令行flag参数时,以下的语言为合法的:

  • -d, --daemon
  • -d=true, --daemon=true
  • -d=”true”, --daemon=”true”
  • -d=’true’, --daemon=’true’

当解析到第一个非定义的flag参数时,命令行flag参数解析工作结束。举例说明,当执行docker命令docker --daemon=false --version=false ps时,flag参数解析主要完成两个工作:

  • 完成命令行flag参数的解析,名为-daemon和-version的flag参数flDaemon和flVersion分别获得相应的值,均为false;
  • 遇到第一个非flag参数的参数ps时,将ps及其之后所有的参数存入flag.Args(),以便之后执行Docker Client具体的请求时使用。

如需深入学习flag的解析,可以参见源码命令行参数flag的解析

3.2. 处理flag信息并收集Docker Client的配置信息

有了以上flag参数解析的相关知识,分析Docker的main函数就变得简单易懂很多。通过总结,首先列出源代码中处理的flag信息以及收集Docker Client的配置信息,然后再一一对此分析:

  • 处理的flag参数有:flVersion、flDebug、flDaemon、flTlsVerify以及flTls;
  • 为Docker Client收集的配置信息有:protoAddrParts(通过flHosts参数获得,作用为提供Docker Client与Server的通信协议以及通信地址)、tlsConfig(通过一系列flag参数获得,如*flTls、*flTlsVerify,作用为提供安全传输层协议的保障)。

随即分析处理这些flag参数信息,以及配置信息。

在flag.Parse()之后的代码如下:

  if *flVersion {
showVersion()
return
}

不难理解的是,当经过解析flag参数后,若flVersion参数为真时,调用showVersion()显示版本信息,并从main函数退出;否则的话,继续往下执行。

  if *flDebug {
os.Setenv("DEBUG", "1")
}

若flDebug参数为真的话,通过os包的中Setenv函数创建一个名为DEBUG的系统环境变量,并将其值设为”1”。继续往下执行。

  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。flHosts的作用是为Docker Client提供所要连接的host对象,也为Docker Server提供所要监听的对象。

分析过程中,首先判断flHosts变量是否长度为0,若是的话,通过os包获取名为DOCKER_HOST环境变量的值,将其赋值于defaultHost。若defaultHost为空或者flDaemon为真的话,说明目前还没有一个定义的host对象,则将其默认设置为unix socket,值为api.DEFAULTUNIXSOCKET,该常量位于./docker/api/common.go,值为"/var/run/docker.sock",故defaultHost为”unix:///var/run/docker.sock”。验证该defaultHost的合法性之后,将defaultHost的值追加至flHost的末尾。继续往下执行。

  if *flDaemon {
mainDaemon()
return
}

若flDaemon参数为真的话,则执行mainDaemon函数,实现Docker Daemon的启动,若mainDaemon函数执行完毕,则退出main函数,一般mainDaemon函数不会主动终结。由于本章节介绍Docker Client的启动,故假设flDaemon参数为假,不执行以上代码块。继续往下执行。

  if len(flHosts) > 1 {
log.Fatal("Please specify only one -H")
}
protoAddrParts := strings.SplitN(flHosts[0], "://", 2)

以上,若flHosts的长度大于1的话,则抛出错误日志。接着将flHosts这个string数组中的第一个元素,进行分割,通过”://”来分割,分割出的两个部分放入变量protoAddrParts数组中。protoAddrParts的作用为解析出与Docker Server建立通信的协议与地址,为Docker Client创建过程中不可或缺的配置信息之一。

  var (
cli *client.DockerCli
tlsConfig tls.Config
)
tlsConfig.InsecureSkipVerify = true

由于之前已经假设过flDaemon为假,则可以认定main函数的运行是为了Docker Client的创建与执行。在这里创建两个变量:一个为类型是client.DockerCli指针的对象cli,另一个为类型是tls.Config的对象tlsConfig。并将tlsConfig的InsecureSkipVerify属性设置为真。TlsConfig对象的创建是为了保障cli在传输数据的时候,遵循安全传输层协议(TLS)。安全传输层协议(TLS) 用于两个通信应用程序之间保密性与数据完整性。tlsConfig是Docker Client创建过程中可选的配置信息。

  // If we should verify the server, we need to load a trusted 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
}

若flTlsVerify这个flag参数为真的话,则说明需要验证server端的安全性,tlsConfig对象需要加载一个受信的ca文件。该ca文件的路径为*flCA参数的值,最终完成tlsConfig对象中RootCAs属性的赋值,并将InsecureSkipVerify属性置为假。

// 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}
}
}

如果flTls和flTlsVerify两个flag参数中有一个为真,则说明需要加载以及发送client端的证书。最终将证书内容交给tlsConfig的Certificates属性。

至此,flag参数已经全部处理,并已经收集完毕Docker Client所需的配置信息。之后的内容为Docker Client如何实现创建并执行。

3.3. Docker Client的创建

Docker Client的创建其实就是在已有配置参数信息的情况,通过Client包中的NewDockerCli方法创建一个实例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)
}

如果flag参数flTls为真或者flTlsVerify为真的话,则说明需要使用TLS协议来保障传输的安全性,故创建Docker Client的时候,将TlsConfig参数传入;否则的话,同样创建Docker Client,只不过TlsConfig为nil。

关于Client包中的NewDockerCli函数的实现,可以具体参见./docker/api/client/cli.go

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,
}
}

总体而言,创建DockerCli对象较为简单,较为重要的DockerCli的属性有proto:传输协议;addr:host的目标地址,tlsConfig:安全传输层协议的配置。若tlsConfig为不为空,则说明需要使用安全传输层协议,DockerCli对象的scheme设置为“https”,另外还有关于输入,输出以及错误显示的配置,最终返回该对象。

通过调用NewDockerCli函数,程序最终完成了创建Docker Client,并返回main函数继续执行。

4. Docker命令执行

main函数执行到目前为止,有以下内容需要为Docker命令的执行服务:创建完毕的Docker Client,docker命令中的请求参数(经flag解析后存放于flag.Arg())。也就是说,需要使用Docker Client来分析docker 命令中的请求参数,并最终发送相应请求给Docker Server。

4.1. Docker Client解析请求命令

Docker Client解析请求命令的工作,在Docker命令执行部分第一个完成,直接进入main函数之后的源码部分

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函数。进入./docker/api/client/cli.go的Cmd函数

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

由代码注释可知,Cmd函数执行具体的指令。源码实现中,首先判断请求参数列表的长度是否大于0,若不是的话,说明没有请求信息,返回docker命令的Help信息;若长度大于0的话,说明有请求信息,则首先通过请求参数列表中的第一个元素args[0]来获取具体的method的方法。如果上述method方法不存在,则返回docker命令的Help信息,若存在的话,调用具体的method方法,参数为args[1]及其之后所有的请求参数。

还是以一个具体的docker命令为例,docker –daemon=false –version=false pull Name。通过以上的分析,可以总结出以下操作流程:

(1) 解析flag参数之后,将docker请求参数”pull”和“Name”存放于flag.Args();

(2) 创建好的Docker Client为cli,cli执行cli.Cmd(flag.Args()…);

在Cmd函数中,通过args[0]也就是”pull”,执行cli.getMethod(args[0]),获取method的名称;

(3) 在getMothod方法中,通过处理最终返回method的值为”CmdPull”;

(4) 最终执行method(args[1:]…)也就是CmdPull(args[1:]…)。

4.2. Docker Client执行请求命令

上一节通过一系列的命令解析,最终找到了具体的命令的执行方法,本节内容主要介绍Docker Client如何通过该执行方法处理并发送请求。

由于不同的请求内容不同,执行流程大致相同,本节依旧以一个例子来阐述其中的流程,例子为:docker pull NAME。

Docker Client在执行以上请求命令的时候,会执行CmdPull函数,传入参数为args[1:]...。源码具体为./docker/api/client/command.go中的CmdPull函数

以下逐一分析CmdPull的源码实现。

(1) 通过cli包中的Subcmd方法定义一个类型为Flagset的对象cmd。

cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry")

(2) 给cmd对象定义一个类型为String的flag,名为”#t”或”#-tag”,初始值为空。

tag := cmd.String([]string{"#t", "#-tag"}, "", "Download tagged image in a repository")

(3) 将args参数进行解析,解析过程中,先提取出是否有符合tag这个flag的参数,若有,将其给赋值给tag参数,其余的参数存入cmd.NArg();若无的话,所有的参数存入cmd.NArg()中。

if err := cmd.Parse(args); err != nil {
return nil }

(4) 判断经过flag解析后的参数列表,若参数列表中参数的个数不为1,则说明需要pull多个image,pull命令不支持,则调用错误处理方法cmd.Usage(),并返回nil。

if cmd.NArg() != 1 {
cmd.Usage()
return nil
}

(5) 创建一个map类型的变量v,该变量用于存放pull镜像时所需的url参数;随后将参数列表的第一个值赋给remote变量,并将remote作为键为fromImage的值添加至v;最后若有tag信息的话,将tag信息作为键为”tag”的值添加至v。

var (
v = url.Values{}
remote = cmd.Arg(0)
)
v.Set("fromImage", remote)
if *tag == "" {
v.Set("tag", *tag)
}

(6) 通过remote变量解析出镜像所在的host地址,以及镜像的名称。

  remote, _ = parsers.ParseRepositoryTag(remote)
// Resolve the Repository name from fqn to hostname + name
hostname, _, err := registry.ResolveRepositoryName(remote)
if err != nil {
return err
}

(7) 通过cli对象获取与Docker Server通信所需要的认证配置信息。

cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(hostname)

(8) 定义一个名为pull的函数,传入的参数类型为registry.AuthConfig,返回类型为error。函数执行块中最主要的内容为:cli.stream(……)部分。该部分具体发起了一个给Docker Server的POST请求,请求的url为"/images/create?"+v.Encode(),请求的认证信息为: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,
})
}

(9) 由于上一个步骤只是定义pull函数,这一步骤具体调用执行pull函数,若成功则最终返回,若返回错误,则做相应的错误处理。若返回错误为401,则需要先登录,转至登录环节,完成之后,继续执行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
}

以上便是pull请求的全部执行过程,其他请求的执行在流程上也是大同小异。总之,请求执行过程中,大多都是将命令行中关于请求的参数进行初步处理,并添加相应的辅助信息,最终通过指定的协议给Docker Server发送Docker Client和Docker Server约定好的API请求。

5. 总结

本文从源码的角度分析了从docker可执行文件开始,到创建Docker Client,最终发送给Docker Server请求的完整过程。

笔者认为,学习与理解Docker Client相关的源码实现,不仅可以让用户熟练掌握Docker命令的使用,还可以使得用户在特殊情况下有能力修改Docker Client的源码,使其满足自身系统的某些特殊需求,以达到定制Docker Client的目的,最大发挥Docker开放思想的价值

6. 参考文献

  1. http://www.infoq.com/cn/articles/docker-command-line-quest
  2. http://docs.studygolang.com/pkg/
  3. http://blog.studygolang.com/2013/02/%E6%A0%87%E5%87%86%E5%BA%93-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90flag/
  4. https://docs.docker.com/reference/commandline/cli/

Docker源码分析(二):Docker Client创建与命令执行的更多相关文章

  1. docker 源码分析 二(基于1.8.2版本),docker client与daemon交互

    (2) 那我们通过docker客户端发送一个命令,docker是怎样接收到并处理的呢,我们就举个例子来看一下,比如docker pull 命令: 我们回到 docker/docker.go 中,在上一 ...

  2. Docker源码分析(五):Docker Server的创建

    1.Docker Server简介 Docker架构中,Docker Server是Docker Daemon的重要组成部分.Docker Server最主要的功能是:接受用户通过Docker Cli ...

  3. docker 源码分析 一(基于1.8.2版本),docker daemon启动过程;

    最近在研究golang,也学习一下比较火的开源项目docker的源代码,国内比较出名的docker源码分析是孙宏亮大牛写的一系列文章,但是基于的docker版本有点老:索性自己就git 了一下最新的代 ...

  4. docker 源码分析 四(基于1.8.2版本),Docker镜像的获取和存储

    前段时间一直忙些其他事情,docker源码分析的事情耽搁了,今天接着写,上一章了解了docker client 和 docker daemon(会启动一个http server)是C/S的结构,cli ...

  5. Docker源码分析(八):Docker Container网络(下)

    1.Docker Client配置容器网络模式 Docker目前支持4种网络模式,分别是bridge.host.container.none,Docker开发者可以根据自己的需求来确定最适合自己应用场 ...

  6. Docker源码分析(七):Docker Container网络 (上)

    1.前言(什么是Docker Container) 如今,Docker技术大行其道,大家在尝试以及玩转Docker的同时,肯定离不开一个概念,那就是“容器”或者“Docker Container”.那 ...

  7. Docker源码分析(四):Docker Daemon之NewDaemon实现

    1. 前言 Docker的生态系统日趋完善,开发者群体也在日趋庞大,这让业界对Docker持续抱有极其乐观的态度.如今,对于广大开发者而言,使用Docker这项技术已然不是门槛,享受Docker带来的 ...

  8. Docker源码分析(三):Docker Daemon启动

    1 前言 Docker诞生以来,便引领了轻量级虚拟化容器领域的技术热潮.在这一潮流下,Google.IBM.Redhat等业界翘楚纷纷加入Docker阵营.虽然目前Docker仍然主要基于Linux平 ...

  9. Docker源码分析(一):Docker架构

    1 背景 1.1 Docker简介 Docker是Docker公司开源的一个基于轻量级虚拟化技术的容器引擎项目,整个项目基于Go语言开发,并遵从Apache 2.0协议.目前,Docker可以在容器内 ...

随机推荐

  1. JavaScript动画-拖拽改变元素大小

    ▓▓▓▓▓▓ 大致介绍 拖拽改变元素大小是在模拟拖拽上增加了一些功能 效果:拖拽改变元素大小 ▓▓▓▓▓▓ 拖拽改变元素大小原理 首先这个方块得知道我们想要改变这个它的大小,所以我给它设定一个范围,当 ...

  2. Winserver2012下mysql 5.7解压版(zip)配置安装

    一.安装 下载mysqlzip版本mysql不需要运行可执行文件,解压即可,下载zip版本mysqlmsi版本mysql双击文件即可安装,相对简单,本文不介绍此版本安装 配置环境变量打开环境变量配置页 ...

  3. C语言初学者代码中的常见错误与瑕疵(19)

    见:C语言初学者代码中的常见错误与瑕疵(19)

  4. How to say "no"?

    How to say "no"?7招教你如何拒绝别人      Do you have a hard time saying no to others? Do you say “y ...

  5. lintcode:子数组之和为0

    题目: 子数组之和 给定一个整数数组,找到和为零的子数组.你的代码应该返回满足要求的子数组的起始位置和结束位置 样例 给出[-3, 1, 2, -3, 4],返回[0, 2] 或者 [1, 3]. 解 ...

  6. jQuery制作go to top按钮

    转自:http://www.w3cplus.com/jquery/scrolling-to-the-top-with-jquery 每每看到网友Blog的页面底部或中间有一个按钮回到页面顶部,羡慕死人 ...

  7. Python之作用域

    作用域测试例子: >>> a = 10 >>> def test(): ... a = 20 ... print a ... >>> a 10 & ...

  8. python 中的__new__与__init__

    在Python中的class中有两个方法__new__与__init__,有什么区别呢? class TestCls(): """docstring for TestCl ...

  9. 【Java并发编程六】线程池

    一.概述 在执行并发任务时,我们可以把任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程,只要池里有空闲的线程,任务就会分配一个线程执行.在线程池的内部,任务被插入一个阻塞队列(Blo ...

  10. CF 643 E. Bear and Destroying Subtrees

    E. Bear and Destroying Subtrees http://codeforces.com/problemset/problem/643/E 题意: Q个操作. 加点,在原来的树上加一 ...