Youmai の Blog


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

golang数据库相关的几个package的关系

发表于 2017-11-15 | 分类于 go

挑orm的过程中不知不觉被搞晕了,怎么那么多相关的package,下面我们来理一理这些package的关系

  • database/sql
  • https://github.com/go-sql-driver/mysql/
  • https://github.com/jmoiron/sqlx

database/sql

database/sql是go的一个标准库,提供了一些sql数据库的通用接口。它需要和数据库驱动一起使用,这里是一个数据库驱动的列表。

对于mysql而言,我们最常用的是https://github.com/go-sql-driver/mysql/

driver数据库驱动

数据库驱动具体是什么呢?

每一个数据库都有一套客户端-服务端的通信协议。作为客户端,如果要从数据库中查询/插入数据,就要遵循这套协议,按照协议格式发送请求。显然,让每个用户实现协议非常浪费而不切实际。所以数据库作者提供了一个软件(或者说package),这个软件实现了协议,并且暴露出了一些API接口供客户端与数据库交互,这个软件就是数据库驱动。

驱动有标准,比如ODBC和JDBC,符合标准的驱动会实现一套相同的API接口,这样即使更换了数据库,客户端也不需要变更。

另外显而易见的是,驱动是语言相关的,不同的编程语言操作数据库需要对应语言实现的数据库驱动。所以驱动主要就是实现了与数据库交互的协议,当我们使用时,一般会这么写:

1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// mysql是驱动的名字
db, err := sql.Open("mysql", "username:passwd@/test?charset=utf8")
}

上面的

1
2
3
import (
_ "github.com/go-sql-driver/mysql"
)

会执行init函数,将mysql驱动注册到database/sql中的drivers中去,drivers是一个map:

1
drivers = make(map[string]driver.Driver)

显然,我们可以注册多个驱动,如果我们后端使用了不同的数据库的话,比如同时使用了mysql和postgresql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
)
func main() {
// mysql和postgres是驱动的名字
my_db, err := sql.Open("mysql", "username:passwd@/test?charset=utf8")
postgre_db, err := sql.Open("postgres", "username:passwd@/test?charset=utf8")
}

sqlx

sql是标准库database/sql的一个扩展,提供了一些更友好的接口,但是并没有过度封装,很多人放弃使用各种orm转而使用了sqlx。

go语言作用域踩坑

发表于 2017-11-03 | 分类于 go

今天饭饭给我出了个题目,下面这段代码为什么报错,怎么改?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
import (
"fmt"
)
type A struct {
s string
}
func main() {
var a *A
if check(a) {
a, err := generate()
fmt.Println(a.s, err)
}
fmt.Println(a.s)
}
func generate() (*A, error) {
return &A{s: "b"}, nil
}
func check(a *A) bool {
return true
}

运行一下,发现报错如下:

1
2
3
4
5
6
7
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1095520]
goroutine 1 [running]:
main.main()
/go_code/src/go_examples/go_scope.go:18 +0x280
exit status 2

18行,也就是

1
fmt.Println(a.s)

报错了,报错没法提供更多信息(go新手,老鸟可能能看出端倪),我们加一些打印

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var a *A
fmt.Printf("%p\n", &a) // 1
if check(a) {
a, err := generate()
fmt.Printf("%p\n", &a) // 2
fmt.Println(a.s, err) // 3
}
fmt.Printf("%p\n", &a) // 4
fmt.Println(a.s)
}

结果如下:

1
2
3
4
0xc42000c028 // 1
0xc42000c038 // 2
b <nil> // 3
0xc42000c028 // 4

我们发现,2处的a竟然不是我们定义的(1处)a,发生了什么!

其实看到这里很多人可能都明白了,其实是a, err := generate()里面:=的问题,我们最初的设想是golang会定义新变量err,而a为初始定义的那个变量(1处)。但实际情况是,对于使用:=定义的变量,如果新变量与那个同名已定义变量 (这里就是1处的变量a)不在一个作用域中时,那么golang会重新定义这个变量,这就是导致这个问题的真凶。

怎么改呢,我们重写一下main函数:

1
2
3
4
5
6
7
8
9
10
func main() {
var a *A
var err error
if check(a) {
a, err = generate()
fmt.Println(a.s, err)
}
fmt.Println(a.s)
}

如此即可~

这个坑真的非常容易踩,而且不太好发现,感谢饭饭🙏

Docker数据管理-Volume, bind mount和tmpfs mount

发表于 2017-09-17 | 分类于 Docker

我们可以将数据写到容器的可写入层,但是这种写入是有缺点的:

  • 当容器停止运行时,写入的数据会丢失。你也很难将这些数据从容器中取出来给另外的应用程序使用。
  • 容器的可写入层与宿主机是紧密耦合的。这些写入的数据在可以轻易地被删掉。
  • 写入容器的可写入层需要一个存储驱动(storage driver)来管理文件系统。这个存储驱动通过linux内核提供了一个union filesystem。相比于数据卷(data volume),这种额外的抽象会降低性能。

Docker提供了3种方法将数据从Docker宿主机挂载(mount)到容器:volumes,bind mounts和tmpfs mounts。一般来说,volumes总是最好的选择。

##选择合适的挂载方式

不管你选择哪种挂载方式,从容器中看都是一样的。数据在容器的文件系统中被展示为一个目录或者一个单独的文件。

一个简单区分volumes,bind mounts和tmpfs mounts不同点的方法是:思考数据在宿主机上是如何存在的。

different mount

  • Volumes由Docker管理,存储在宿主机的某个地方(在linux上是/var/lib/docker/volumes/)。非Docker应用程序不能改动这一位置的数据。Volumes是Docker最好的数据持久化方法。
  • Bind mounts的数据可以存放在宿主机的任何地方。数据甚至可以是重要的系统文件或目录。非Docker应用程序可以改变这些数据。
  • tmpfs mounts的数据只存储在宿主机的内存中,不会写入到宿主机的文件系统。

更详细的Diff

  • Volumes:由Docker创建和管理。你可以通过docker volume create命令显式地创建volume,Docker也可以在创建容器或服务是自己创建volume。

    当你创建了一个volume,它会被存放在宿主机的一个目录下。当你将这个volume挂载到某个容器时,这个目录就是挂载到容器的东西。这一点和bind mounts类似,除了volumes是由Docker创建的,和宿主机的核心(core functionality)隔离。

    一个volume可以同时被挂载到几个容器中。即使没有正在运行的容器使用这个volume,volume依然存在,不会被自动清除。可以通过docker volume prune清除不再使用的volumes。

    volumes也支持volume driver,可以将数据存放在另外的机器或者云上。

  • Bind mounts:Docker早期就支持这个特性。与volumes相比,Bind mounts支持的功能有限。使用bind mounts时,宿主机上的一个文件或目录被挂载到容器上。

    警告:使用Bind mounts的一个副作用是,容器中运行的程序可以修改宿主机的文件系统,包括创建,修改,删除重要的系统文件或目录。这个功能可能会有安全问题。

  • tmpfs mounts:tmpfs mounts的数据不会落盘。在容器的生命周期内,它可以被用来存储一些不需要持久化的状态或敏感数据。例如,swarm服务通过tmpfs mounts来将secrets挂载到一个服务的容器中去。

适合Volumes的场景

  • 在不同的容器中共享数据。If you don’t explicitly create it, a volume is created the first time it is mounted into a container. When that container stops or is removed, the volume still exists. Multiple containers can mount the same volume simultaneously, either read-write or read-only. Volumes are only removed when you explicitly remove them.
  • When the Docker host is not guaranteed to have a given directory or file structure. Volumes help you decouple the configuration of the Docker host from the container runtime.
  • When you want to store your container’s data on a remote host or a cloud provider, rather than locally.
  • 当你需要备份或迁移数据的时候,When you need to be able to back up, restore, or migrate data from one Docker host to another, volumes are a better choice. You can stop containers using the volume, then back up the volume’s directory (such as /var/lib/docker/volumes/).

适合bind mounts的场景

  • 宿主机和容器共享配置文件。Docker提供的DNS解决方案就是如此,将宿主机的/etc/resolv.conf挂载到每个容器中。
  • 开发环境需要在宿主机和容器中共享代码。docker的开发就是如此,毕竟容器中一般是没有编辑器的
  • When the file or directory structure of the Docker host is guaranteed to be consistent with the bind mounts the containers require.

适合tmpfs mounts的场景

tmpfs mounts主要用在你既不想在容器内,又不想在宿主机文件系统保存数据的时候。这可能是出于安全原因,也可能是你的应用需要写非常多的非持久化数据,tmpfs mounts这时候可以保证容器性能。

docker swarm部署应用

发表于 2017-09-16 | 分类于 Docker

预备知识

什么是swarm

一个swarm就是一组运行Docker并组成集群的机器。之后,你继续运行以前使用的Docker命令,但现在它们是由swarm manager(也是一台机器,执行docker swarm init的就是manager)在集群上执行。集群中的机器可以是物理机或虚拟机。加入集群后,它们被称为节点(node)。

swarm manager可以使用几种策略来运行容器,例如“最空的节点”(empties node) - 将容器放到利用率最低的机器上。或者“全局”(global)模式,它确保每台机器只能获得指定容器的一个实例。你通过在compose文件来指示swarm manager使用这些策略。

swarm manager是集群中唯一可以执行命令的机器,也是唯一可以授权其他机器作为worker加入集群的机器。worker只提供capacity,它无法告诉任何其他机器它可以做什么和不能做什么。

什么是slack

stack是一组相互关联的服务,可以被一起编排。单个stack能够定义整个应用程序的功能(尽管非常复杂的应用程序可能希望使用多个stack)。

Set up your swarm

一个swarm由很多node组成,node可以是物理机或虚拟机。基本概念很简单:运行docker swarm init启用集群模式,使当前的机器成为manager,然后在其他机器上运行docker swarm join,使他们以worker身份加入集群。

创建集群

我们可以在本机创建几台虚拟机来创建我们的集群(需要安装VirtualBox)

首先,使用docker-machine命令,通过VirtualBox驱动创建两台虚拟机,myvm1和myvm2:

1
2
$ docker-machine create --driver virtualbox myvm1
$ docker-machine create --driver virtualbox myvm2

我们将myvm1用作manager,它可以执行docker命令和授权别的worker加入swarm,myvm2将作为worker。

登录docker-machine创建的虚拟机可以使用docker-machine ssh命令,下面我们设置myvm1为manager:

1
2
3
4
5
6
7
8
9
10
11
12
13
➜ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
myvm1 - virtualbox Running tcp://192.168.99.101:2376 v17.06.2-ce
myvm2 - virtualbox Running tcp://192.168.99.102:2376 v17.06.2-ce
➜ docker-machine ssh myvm1 "docker swarm init --advertise-addr 192.168.99.101:2377"
Swarm initialized: current node (u5e02o4thu4uurxm9w71kxtu5) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-02sp63h6alkj7sd57dkv6omkyvkb60y05delt5zvkspe0293ao-dtmm6kqjzbbe33jmo03yrml8t 192.168.99.101:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

可以看到,返回值中已经有了一个配置好的docker swarm join命令,复制这个命令,在myvm2上执行,使myvm2加入你的swarm作为worker:

1
2
➜ docker-machine ssh myvm2 "docker swarm join --token SWMTKN-1-02sp63h6alkj7sd57dkv6omkyvkb60y05delt5zvkspe0293ao-dtmm6kqjzbbe33jmo03yrml8t 192.168.99.101:2377"
This node joined a swarm as a worker.

🎉,我们的第一个swarm创建成功了。

我们可以登上myvm1去看看现在的节点情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜ docker-machine ssh myvm1
## .
## ## ## ==
## ## ## ## ## ===
/"""""""""""""""""\___/ ===
~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ / ===- ~~~
\______ o __/
\ \ __/
\____\_______/
_ _ ____ _ _
| |__ ___ ___ | |_|___ \ __| | ___ ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__| < __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker version 17.06.2-ce, build HEAD : ff16afa - Wed Sep 6 00:17:25 UTC 2017
Docker version 17.06.2-ce, build cec0b72
docker@myvm1:~$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
ggr9eji9z868qrorwerufrr45 myvm2 Ready Active
u5e02o4thu4uurxm9w71kxtu5 * myvm1 Ready Active Leader

一目了然,现在有两个node,myvm1是作为leader存在

部署应用

我们使用这个简单的python应用来作为部署对象,你完全可以使用自己的应用程序(但还是建议用我使用的这个程序,这个应用打印了container的hostname,这是下面负载均衡的一个测试)。

首先将docker-compose.yml拷贝到myvm1:

1
docker-machine scp docker-compose.yml myvm1:~

然后就是部署了:

1
2
3
4
5
docker-machine ssh myvm1 "docker stack deploy -c docker-compose.yml firstswarmapp"
Creating service firstswarmapp_visualizer
Creating service firstswarmapp_redis
Creating service firstswarmapp_web

这时候部署已经完成了,我们来看看成果:

1
2
3
4
5
6
7
8
9
10
11
12
13
➜ docker-machine ssh myvm1 "docker stack ls"
NAME SERVICES
firstswarmapp 1
➜ docker-machine ssh myvm1 "docker stack ps firstswarmapp"
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
6lfkdnzq27yf firstswarmapp_redis.1 redis:latest myvm1 Running Running 43 seconds ago
ui2tdail0fxv firstswarmapp_visualizer.1 dockersamples/visualizer:stable myvm1 Running Running 2 minutes ago
901cprboy06k firstswarmapp_web.1 michaelyou/friendlyhello:v1 myvm2 Running Running 33 minutes ago
uvju8t48sc2s firstswarmapp_web.2 michaelyou/friendlyhello:v1 myvm2 Running Running 33 minutes ago
sk2z91e4yn9x firstswarmapp_web.3 michaelyou/friendlyhello:v1 myvm1 Running Running 34 minutes ago
tuzdxnfyofyc firstswarmapp_web.4 michaelyou/friendlyhello:v1 myvm2 Running Running 33 minutes ago
715at7211p8k firstswarmapp_web.5 michaelyou/friendlyhello:v1 myvm1 Running Running 34 minutes ago

可以看到,我们一共有7个container。web5个,分布在两台机器上,redis和visualizer各一个,在manager节点上。这和我们在docker-compose.yml中定义的一样。

myvm1和myvm2的地址都可以访问我们的应用。你创建的网络在它们之间是共享,并且负载平衡的。运行docker-machine ls来获取你的虚拟机的IP地址,并在浏览器上访问它们,并刷新。你会看到五个可能的容器ID,它们随机循环,显示了负载平衡的存在。

两个IP地址都能工作的原因是群集中的节点加入了一个入口路由网格(routing mesh)。这样可以确保在集群中某个端口部署的服务始终将该端口保留给其自己,无论实际运行容器的是哪个节点。以下是在一个三节点集群的8080端口上部署的名为my-web的服务的路由网格示意图:

ingress-routing-mesh

如果想要增加容器个数,只要修改docker-compose.yml文件中的replicas的数量,然后重新执行deploy就可以了。

如果要加入新的node,只要在新的机器上执行我们在myvm2上执行的docker swarm join命令就可以了,加入新的节点后,重新执行deploy,我们就能用上新的机器了(注意要重新执行deploy,不会自动deploy,这一点和elasticsearch等软件不一样)。

因为我们部署了visualizer,可以通过网页来看看我们集群现在的情况:

my swarm

跟docker stack ps输出是一致的。

清理

清理stack

1
docker-machine ssh myvm1 "docker stack rm firstswarmapp"

这时候swarm还是在的,清理swarm:

1
docker-machine ssh myvm1 "docker swarm leave --force"

docker源码分析(一)-cli

发表于 2017-09-12 | 分类于 Docker

Docker代码更新很快,网上各位大神的源码解析很多已经是几年前的版本了。实现上有了很大改变,加之Docker项目更名为moby,其中很多组件又从moby中拆分了出来,一开始看简直是一脸懵逼啊。在这里跟大家分享了一下最近看的docker/cli的源码,抛砖引玉,欢迎大家批评指正。

分析的docker client版本是17.06.2-ce。

docker client是Docker的客户端程序,也就是我们敲的docker * * 命令,我们通过他与docker deamon程序进行交互。可以将他看成一个普通的客户端程序,docker的核心namespace和cgroup等技术都不在这里。

搭建docker-client开发环境

docker_client的开发环境也是在容器中,项目已经给我们做好了封装,具体可以参考项目Readme的Development章节。

问几个问题,大家可以思考一下:

1. 开发是在容器中,可是容器里面连编辑器都没有,怎么开发?

答: 准确来说应该是在容器中调试,开发还是在本地开发,容器中看到的目录是我们本地目录mount进去的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# docker.Makefile文件
......
MOUNTS = -v "$(CURDIR)":/go/src/github.com/docker/cli
......
# start container in interactive mode for in-container development
.PHONY: dev
dev: build_docker_image
docker run -ti --security-opt=seccomp:unconfined $(ENVVARS) $(MOUNTS) \
-v /var/run/docker.sock:/var/run/docker.sock \
$(DEV_DOCKER_IMAGE_NAME) ash
shell: dev
......

从shell里面还是很容器看出来的,run的时候mount了$(CURDIR)

2. 容器里面是不是还装了一个docker啊,为什么我在里面敲命令有响应?

答:不是的,里面只有你编译生成的docker client程序,是没有docker deamon程序,至于为什么能响应,还是看上面的shell脚本,-v /var/run/docker.sock:/var/run/docker.sock这一行将docker.sock文件也mount了进去,docker.sock是docker deamon默认监听的Unix套接字(Unix domain socket),容器中的进程可以通过他与docker deamon进行通信。所以这里docker client通信的是你本机的docker daemon。

docker_deamon_socket

Note: 关于/var/run/docker.sock更多内容可以看这里

所以我们的开发流程是:本地改代码–>到container中编译–>运行docker client看效果

源码

是时候表演真正的技术了!

😆,开个玩笑,下面的分析如果有问题,还请大家不吝赐教!

我尽量把文件路径列出来,会贴一些代码,但主要还是路径,建议大家把代码clone下来照着看。

docker—client是基于cobra写的,建议大家先看一下cobra,至少写个hello world熟悉一下基本用法。

我们开始了!

入口文件在cmd/docker/docker.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
// 日志输出采用了第三方库logrus
// Set terminal emulation based on platform as required.
stdin, stdout, stderr := term.StdStreams()
logrus.SetOutput(stderr)
dockerCli := command.NewDockerCli(stdin, stdout, stderr)
// root命令,子命令都在这里面,重点看
cmd := newDockerCommand(dockerCli)
// 命令执行
if err := cmd.Execute(); err != nil {
if sterr, ok := err.(cli.StatusError); ok {
if sterr.Status != "" {
fmt.Fprintln(stderr, sterr.Status)
}
// StatusError should only be used for errors, and all errors should
// have a non-zero exit status, so never exit with 0
if sterr.StatusCode == 0 {
os.Exit(1)
}
os.Exit(sterr.StatusCode)
}
fmt.Fprintln(stderr, err)
os.Exit(1)
}
}

还是这个文件,newDockerCommand函数调用了commands.AddCommands(cmd, dockerCli)来添加命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := cliflags.NewClientOptions()
var flags *pflag.FlagSet
cmd := &cobra.Command{
Use: "docker [OPTIONS] COMMAND [ARG...]",
Short: "A self-sufficient runtime for containers",
SilenceUsage: true,
SilenceErrors: true,
TraverseChildren: true,
Args: noArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if opts.Version {
showVersion()
return nil
}
return command.ShowHelp(dockerCli.Err())(cmd, args)
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// daemon command is special, we redirect directly to another binary
if cmd.Name() == "daemon" {
return nil
}
// flags must be the top-level command flags, not cmd.Flags()
opts.Common.SetDefaultOptions(flags)
dockerPreRun(opts)
if err := dockerCli.Initialize(opts); err != nil {
return err
}
return isSupported(cmd, dockerCli)
},
}
cli.SetupRootCommand(cmd)
flags = cmd.Flags()
flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit")
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
opts.Common.InstallFlags(flags)
setFlagErrorFunc(dockerCli, cmd, flags, opts)
setHelpFunc(dockerCli, cmd, flags, opts)
cmd.SetOutput(dockerCli.Out())
cmd.AddCommand(newDaemonCommand())
# 添加子命令,cmd里现在是root命令
commands.AddCommands(cmd, dockerCli)
setValidateArgs(dockerCli, cmd, flags, opts)
return cmd
}

这行上面的cmd.AddCommand(newDaemonCommand())是为docker daemon命令进行的输出,newDaemonCommand定义在cmd/docker/daemon_none.go中,从他的RunE方法可以看出来,是输出了不能运行的提示,runtime.GOOS是输出当前系统的名称,比如mac是Darwin, ubuntu是linux,完整看来就是:

`docker daemon` is not supported on Darwin. Please run `dockerd` directly

让我们来到commands.AddCommands定义的地方,cli/command/commands/commands.go文件,可以看到cmd.AddCommand的调用,这里就是在添加我们看到的二级命令,也就是紧跟着docker后面的命令。

1
2
3
4
5
6
7
8
9
10
11
12
func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
cmd.AddCommand(
......
// container
container.NewContainerCommand(dockerCli),
container.NewRunCommand(dockerCli),
......
hide(container.NewPsCommand(dockerCli)),
hide(container.NewRenameCommand(dockerCli)),
......

我们从上看到下,整齐划一中发现最后好多命令被用hide包裹了。这些命令是一些旧命令,在新的版本中可以使用新的命令来代替,如果设置了环境变量DOCKER_HIDE_LEGACY_COMMANDS不为空,那么docker的提示将不会输出这些。

这里的命令太多了,我们通过docker ps命令来了解一下大致的执行流程。

docker ps命令也是旧版的命令,hide(container.NewPsCommand(dockerCli))就是在处理他。在新版本中对应的是docker container ls命令。实现代码在cli/command/container/list.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// NewPsCommand creates a new cobra.Command for `docker ps`
func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command {
options := psOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
Use: "ps [OPTIONS]",
Short: "List containers",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runPs(dockerCli, &options)
},
}
flags := cmd.Flags()
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display numeric IDs")
flags.BoolVarP(&options.size, "size", "s", false, "Display total file sizes")
flags.BoolVarP(&options.all, "all", "a", false, "Show all containers (default shows just running)")
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
flags.BoolVarP(&options.nLatest, "latest", "l", false, "Show the latest created container (includes all states)")
flags.IntVarP(&options.last, "last", "n", -1, "Show n last created containers (includes all states)")
flags.StringVarP(&options.format, "format", "", "", "Pretty-print containers using a Go template")
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
return cmd
}
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := *NewPsCommand(dockerCli)
cmd.Aliases = []string{"ps", "list"}
cmd.Use = "ls [OPTIONS]"
return &cmd
}

NewPsCommand对应的是docker ps, newListCommand对应的是docker container ls,可以看出来, newListCommand里的command就是NewPsCommand返回的,只是加了一下说明而已。

这里还漏了一点,docker container ls是3个命令,我们知道docker是root命令,按照cobra的写法,container和ls应该是分开的。他们的关系是这样的,在cli/command/commands/commands.go文件中container.NewContainerCommand(dockerCli)加入了container命令,NewContainerCommand的实现在cli/command/container/cmd.go中,在NewContainerCommand函数中通过newListCommand(dockerCli)加入了ls命令,可以看到还添加了很多别的方法,那些都是container支持的子命令。这样就回到了我们上面提到的内容了,没有魔法

我们继续看NewPsCommand:

里面定义了docker ps(下面所有的内容对docker container ls都适用)支持的一些选项(flag),选项被放在了psOptions这个结构体中,最终传给runPs函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func runPs(dockerCli *command.DockerCli, options *psOptions) error {
ctx := context.Background()
listOptions, err := buildContainerListOptions(options)
if err != nil {
return err
}
containers, err := dockerCli.Client().ContainerList(ctx, *listOptions)
if err != nil {
return err
}
format := options.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().PsFormat) > 0 && !options.quiet {
format = dockerCli.ConfigFile().PsFormat
} else {
format = formatter.TableFormatKey
}
}
containerCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewContainerFormat(format, options.quiet, listOptions.Size),
Trunc: !options.noTrunc,
}
return formatter.ContainerWrite(containerCtx, containers)
}

listOptions, err := buildContainerListOptions(options)对选项进行了处理,重点在dockerCli.Client().ContainerList(ctx, *listOptions),ContainerList是一个interface。

实现在vendor/github.com/docker/docker/client/container_list.go(vendor在当前目录下,我是go新手,所以担心大家不知道,高手忽略)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func (cli *Client) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
query := url.Values{}
if options.All {
query.Set("all", "1")
}
if options.Limit != -1 {
query.Set("limit", strconv.Itoa(options.Limit))
}
if options.Since != "" {
query.Set("since", options.Since)
}
if options.Before != "" {
query.Set("before", options.Before)
}
if options.Size {
query.Set("size", "1")
}
if options.Filters.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
if err != nil {
return nil, err
}
query.Set("filters", filterJSON)
}
resp, err := cli.get(ctx, "/containers/json", query, nil)
if err != nil {
return nil, err
}
var containers []types.Container
err = json.NewDecoder(resp.body).Decode(&containers)
ensureReaderClosed(resp)
return containers, err
}

前面是在拼请求的参数,resp, err := cli.get(ctx, "/containers/json", query, nil)这里发起了请求,返回了container的列表。我们可以通过curl或nc来获得原始的数据:

1
2
3
4
5
➜ curl -XGET --unix-socket /var/run/docker.sock http://localhost/containers/json
或者
➜ echo 'GET /containers/json HTTP/1.0\r\n\r\n' | nc -U /var/run/docker.sock
[{"Id":"c64ea306f74980555521ad2fcbb4c54e1b05e7ca0d597838557d4d613ed12039","Names":["/brave_swirles"],"Image":"docker-cli-dev","ImageID":"sha256:b123a157eaf7e4d468647b42be4c8e339e9e4b9e84058a56575dbfa7249161c9","Command":"ash","Created":1505218585,"Ports":[],"Labels":{},"State":"running","Status":"Up 25 hours","HostConfig":{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"4b149c263b9a12caef2b16482db33fded74eac5396a57da1724428948464e00f","EndpointID":"6a3fb06309f2142b23db84303cbdb5d55b3a41e91cf2468649bd5f2adee11c66","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:02","DriverOpts":null}}},"Mounts":[{"Type":"bind","Source":"/Users/youwangqiu/go_code/src/github.com/docker/cli","Destination":"/go/src/github.com/docker/cli","Mode":"","RW":true,"Propagation":"rprivate"},{"Type":"bind","Source":"/var/run/docker.sock","Destination":"/var/run/docker.sock","Mode":"","RW":true,"Propagation":"rprivate"}]}]

可以看到数据是以json格式返回的,是一个列表。

回到cli/command/container/list.go,接收到返回值之后,这时候还是上面看到的json,下面进行了格式化输出,大家应该都有做打印9*9乘法表的经历,这里的格式化是同样的道理,不明白的同学可以继续往挖,我偷个懒,就不继续了。

至此docker ps的流程就走完了,大家可以对源码做一些修改,然后进到容器中执行

1
make binary

就可以生产新的docker client,执行一下就能得到想要的输出啦。

谢谢大家,欢迎大家指正🙂

Moby & LinuxKit 初体验

发表于 2017-09-07 | 分类于 Docker

Docker的开源部分被改名成了Moby,同时Docker公司还开源了一个LinuxKit的项目,大家可以先去看一下readme。

Moby是一个组装容器系统的框架。它包含一个容器化组件库和一个将这些组件组装成独立的容器系统的框架。目前,Docker正在被拆分成模块化的组件,将来,Docker会从这些被Moby打包的组件中被组装出来。

LinuxKit是一个用来构造最小Linux发行版的工具集。它通过Moby来创建镜像(image),使用LinuxKit工具来运行这些镜像。

下面我们来体验一下,首先确保你已经安装了Docker和Golang。

构建Moby

1
2
3
4
5
6
➜ git clone https://github.com/linuxkit/linuxkit.git
➜ cd linuxkit
➜ make && sudo make install
➜ moby version
moby version 0.0
commit: 1ff0e3beeeb1e741b9c5a54574f01ac5eee525a5

构建linux镜像

构建镜像是通过yaml文件(格式可以参考官方文档 LinuxKit YAML),linuxkit目录下有一个linuxkit.yml可以用,examples目录下也有很多可用的yaml文件,我们可以任选一个来构建,这里直接使用linuxkit.yml文件。

1
2
3
4
➜ linuxkit git:(master) moby build linuxkit.yml
最后在当前目录下输出了3个文件
Create outputs:
linuxkit-kernel linuxkit-initrd.img linuxkit-cmdline

运行LinuxKit镜像

运行通过linuxkit自带的linuxkit命令,命令在linuxkit/bin目录下,我们只是体验,也没必要写PATH,就直接写全路径吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
➜ linuxkit git:(master) bin/linuxkit run linuxkit
...满屏满屏的输出,最后是熟悉的🐳
Welcome to LinuxKit
## .
## ## ## ==
## ## ## ## ## ===
/"""""""""""""""""\___/ ===
{ / ===-
\______ O __/
\ \ __/
\____\_______/
...

现在我们就是在我们自己构建的linux最小发行版里面了,敲几个命令试试吧,退出是halt。

这个镜像只有54M,可以说是非常小了。

1
2
3
4
5
6
➜ linuxkit git:(master) ls -alh | grep linuxkit
-rw-r--r-- 1 vagrant vagrant 42 Sep 7 11:52 linuxkit-cmdline
-rw-r--r-- 1 vagrant vagrant 54M Sep 7 11:52 linuxkit-initrd.img
-rw-r--r-- 1 vagrant vagrant 6.3M Sep 7 11:52 linuxkit-kernel
drwxr-xr-x 2 vagrant vagrant 4.0K Sep 7 12:11 linuxkit-state
-rw-rw-r-- 1 vagrant vagrant 1.7K Sep 7 10:33 linuxkit.yml

Moby和LinuxKit是非常不错的工具,但一般来说我们是用不上的。如果你需要构建定制的linux发行版,那他们可以节约很多时间。但如果你只是想将应用放在容器里跑起来,那还是直接用Docker吧!

另外,Moby和LinuxKit应该还没有成熟,命令一天一个变化(夸张了),如果你发现上面的命令失效了,建议google一下,或者联系我(知乎 or whaterver)

浙江大学SEL实验室Docker系列文章目录

发表于 2017-09-07 | 分类于 docker

DOCKER源码分析(一):DOCKER架构-2014.12.02

DOCKER源码分析(二):DOCKER CLIENT创建与命令执行-2014.12.02

DOCKER源码分析(三):DOCKER DAEMON启动-2014.12.02

DOCKER源码分析(四):DOCKER DAEMON之NEWDAEMON实现-2014.12.02

DOCKER源码分析(五):DOCKER SERVER的创建-2014.12.09

DOCKER源码分析(六):DOCKER DAEMON网络-2015.01.05

DOCKER源码分析(七):DOCKER CONTAINER网络 (上)-2015.01.26

DOCKER源码分析(八):DOCKER CONTAINER网络(下)-2015.03.12

DOCKER源码分析(九):DOCKER镜像-2015.03.12

DOCKER网络详解及PIPEWORK源码解读与实践-2015.01.16

DOCKER背后的内核知识——NAMESPACE资源隔离-2015.03.13

DOCKER背后的内核知识——CGROUPS资源限制-2015.04.22

DOCKER背后的容器管理——LIBCONTAINER深度解析-2015.06.03

玩转DOCKER镜像-2014.12.16

深入理解ETCD技术分享PPT-2015.01.20

ETCD:从应用场景到实现原理的全方位解读-2015.02.01

GOOGLE KUBERNETES设计文档之安全篇-2014.12.11

GOOGLE KUBERNETES设计文档之POD篇-2014.12.19

GOOGLE KUBERNETES设计文档之服务篇-2014.12.23

GOOGLE KUBERNETES设计文档之网络篇-2014.12.29

GOOGLE KUBERNETES设计文档之VOLUMES-2015.01.02

KUBERNETES MINION NODE 组件 之 KUBELET-2015.01.13

KUBERNETES代码走读之MINION NODE 组件 KUBE-PROXY-2015.01.22

4S: SERVICES ACCOUNT, SECRET, SECURITY CONTEXT AND SECURITY IN KUBERNETES-2015.07.30

KUBERNETES NODE COMPONENTS – KUBELET-2015.08.07

KUBERNETES APISERVER源码分析——API请求的认证过程-2015.08.09

python字符编码和字符串

发表于 2017-09-02 | 分类于 python

背景:我司代码都是基于python2

今天接到一个需求,用户提交的评论字数如果大于20个字要给用户发红包。毫不迟疑,我写下了下面的代码:

1
2
if len(content) > 20:
# 发红包

我转念一想,事情不大对,我这算的是字符数(这个表述是正确的,是字符)啊

1
2
len("abcd") # 等于4
len("I love python") # 等于13

上面的例子显而易见,小学生水平,再看下面的

1
2
len("我爱北京天安门") # 这个该是多少呢?
len(u"我爱北京天安门") # 这个又该是多少呢?

大家可以本地打开python试一下,结果是:

1
2
len("我爱北京天安门") # 等于21
len(u"我爱北京天安门") # 等于7

其实我想要的是下面这个,但是我在代码里直接那么写,我操作的字符串前面到底有没有u呢,这个u我知道是unicode字符串,它和没有u的字符串到底区别在哪里?

几年前我曾经下决心弄懂这个问题,可能当时也确实明白了,但是现在差不多已经全忘了。我自己总结了一下,一是之前一直是用python3,python3做了一些修改。二是这个问题确实不应该交给程序员来解决(我承认我的记性确实不好)。

然而,旧的系统那么多,难保你下次跳槽的公司没有一些上了年纪的代码,我们还是来梳理一下吧。

字符与字节

一个字符不等价于一个字节,字符是人类能够识别的符号,而这些符号要保存到计算机的存储中就需要用计算机能够识别的字节来表示。一个字符往往有多种表示方法,不同的表示方法会使用不同的字节数。这里所说的不同的表示方法就是指字符编码,比如字母A-Z都可以用ASCII码表示(占用一个字节),也可以用UNICODE表示(占两个字节),还可以用UTF-8表示(占用一个字节)。字符编码的作用就是将人类可识别的字符转换为机器可识别的字节码,以及反向过程。

UNICDOE才是真正的字符串,而用ASCII、UTF-8、GBK等字符编码表示的是字节串。关于这点,我们可以在Python的官方文档中经常可以看到这样的描述“Unicode string” , “translating a Unicode string into a sequence of bytes”。(注意:英文中string是我们说的字符串,bytes是字节串)

我们写代码是写在文件中的,而字符是以字节形式保存在文件中的,因此当我们在文件中定义个字符串时被当做字节串也是可以理解的。但是,我们需要的是字符串,而不是字节串(我们写代码处理的是我们能想象的数据,也就是字符串,应该不会有人想象字节串吧)。一个优秀的编程语言,应该严格区分两者的关系并提供巧妙的完美的支持。JAVA语言就很好,我认识的JAVA程序员从来没有考虑过这些不应该由程序员来处理的问题(我一直这么认为)。遗憾的是,很多编程语言试图混淆“字符串”和“字节串”,他们把字节串当做字符串来使用,PHP和Python2都属于这种编程语言。最能说明这个问题的操作就是取一个包含中文字符的字符串的长度:

  • 对字符串取长度,结果应该是所有字符的个数,无论中文还是英文
  • 对字符串对应的字节串取长度,就跟编码(encode)过程使用的字符编码有关了(比如:UTF-8编码,一个中文字符需要用3个字节来表示;GBK编码,一个中文字符需要2个字节来表示)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [7]: a = u"中国"
In [9]: print len(a), type(a)
2 <type 'unicode'>
In [10]: b = a.encode('utf-8')
In [11]: print len(b), type(b)
6 <type 'str'>
In [12]: c = a.encode('gbk')
In [13]: print len(c), type(c)
4 <type 'str'>

编码与解码

UNICODE字符编码,也是一张字符与数字的映射,但是这里的数字被称为代码点(code point), 实际上就是十六进制的数字。

Python官方文档中对Unicode字符串、字节串与编码之间的关系有这样一段描述:

a Unicode string is a sequence of code points, which are numbers from 0 to 0x10ffff.
This sequence needs to be represented as a set of bytes (meaning, values from 0–255) in memory.
The rules for translating a Unicode string into a sequence of bytes are called an encoding.

Unicode字符串是一个代码点(code point)序列,代码点取值范围为0到0x10FFFF(对应的十进制为1114111)。
这个代码点序列在存储(包括内存和物理磁盘)中需要被表示为一组字节(0到255之间的值),
而将Unicode字符串转换为字节序列的规则称为编码。

这里说的编码不是指字符编码,而是指编码的过程以及这个过程中所使用到的Unicode字符的代码点与字节的映射规则。这个映射不必是简单的一对一映射,因此编码过程也不必处理每个可能的Unicode字符,例如:

将Unicode字符串转换为ASCII编码的规则很简单–对于每个代码点:

  • 如果代码点数值<128,则每个字节与代码点的值相同
  • 如果代码点数值>=128,则Unicode字符无法在此编码中进行表示(这种情况下,Python会引发一个UnicodeEncodeError异常)

将Unicode字符串转换为UTF-8编码使用以下规则:

  • 如果代码点数值<128,则由相应的字节值表示(与Unicode转ASCII字节一样)
  • 如果代码点数值>=128,则将其转换为一个2个字节,3个字节或4个字节的序列,该序列中的每个字节都在128到255之间。

简单总结:

  • 编码(encode):将Unicode字符串(中的代码点)转换特定字符编码对应的字节串的过程和规则
  • 解码(decode):将特定字符编码的字节串转换为对应的Unicode字符串(中的代码点)的过程和规则

可见,无论是编码还是解码,都需要一个重要因素,就是特定的字符编码。因为一个字符用不同的字符编码进行编码后的字节值以及字节个数大部分情况下是不同的,反之亦然。

而且很容易看到,编码和解码都是基于Unicode字符串,没有第二种字符串掺和,其他都是编码方式,一定要深刻地认识到这一点。

Python编码

Python源代码文件的执行过程

我们都知道,磁盘上的文件都是以二进制格式存放的,其中文本文件都是以某种特定编码的字节形式存放的。对于程序源代码文件的字符编码是由编辑器指定的,比如我们使用vim来编写Python程序时会指定文件编码为UTF-8,那么Python代码被保存到磁盘时就会被转换为UTF-8编码对应的字节(encode过程)后写入磁盘。当执行Python代码文件中的代码时,Python解释器在读取Python代码文件中的字节串之后,需要将其转换为UNICODE字符串(decode过程)之后才执行后续操作。

上面已经解释过,这个转换过程(decode,解码)需要我们指定文件中保存的字节使用的字符编码是什么,才能知道这些字节在UNICODE这张万国码和统一码中找到其对应的代码点是什么。这里指定字符编码的方式大家都很熟悉,一图胜千言:

1
# -*- coding:utf-8 -*-

python源文件文件的执行过程

默认编码

那么,如果我们没有在代码文件开始的部分指定字符编码,Python解释器会使用哪种字符编码把从代码文件中读取到的字节转换为UNICODE代码点呢?就像我们配置某些软件时,有很多默认选项一样,Python解释器内部设置了默认的字符编码。因此大家所说的Python中文字符问题就可以总结为一句话:当无法通过默认的字符编码对字节进行转换时,就会出现解码错误(UnicodeEncodeError)。

Python2和Python3的解释器使用的默认编码是不一样的,我们可以通过sys.getdefaultencoding()来获取默认编码:

1
2
3
4
5
6
7
8
9
10
11
# python2
In [1]: import sys
In [2]: sys.getdefaultencoding()
Out[3]: 'ascii'
# python3
In [1]: import sys
In [2]: sys.getdefaultencoding()
Out[3]: 'utf-8'

因此,对于Python2来讲,Python解释器在读取到中文字符的字节码尝试解码操作时,会先查看当前代码文件头部是否有指明当前代码文件中保存的字节码对应的字符编码是什么。如果没有指定则使用默认字符编码”ASCII”进行解码导致解码失败,导致如下错误:

1
SyntaxError: Non-ASCII character '\xc4' in file xxx.py on line 11, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

对于Python3来讲,执行过程是一样的,只是Python3的解释器以”UTF-8”作为默认编码,但是这并不表示可以完全兼容中文问题。比如我们在Windows上进行开发时,Python工程及代码文件都使用的是默认的GBK编码,也就是说Python代码文件是被转换成GBK格式的字节码保存到磁盘中的。Python3的解释器执行该代码文件时,试图用UTF-8进行解码操作时,同样会解码失败,导致如下错误:

1
SyntaxError: Non-UTF-8 code starting with '\xc4' in file xxx.py on line 11, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

再谈python2和python3中的字符串

其实Python3中对字符串支持的改进,不仅仅是更改了默认编码,而是重新进行了字符串的实现,而且它已经实现了对UNICODE的内置支持,从这方面来讲Python已经和JAVA等语言一样优秀。下面我们来看下Python2与Python3中对字符串的支持有什么区别:

python

Python2中对字符串的支持由以下三个类提供

1
2
3
class basestring(object)
class str(basestring)
class unicode(basestring)

执行help(str)和help(bytes)会发现结果都是str类的定义,这也说明Python2中str就是字节串,而后来的unicode对象对应才是真正的字符串。

1
2
3
4
5
6
7
8
9
In [1]: a = "中国"
In [2]: b = u"中国"
In [3]: print type(a), len(a)
<type 'str'> 6
In [4]: print type(b), len(b)
<type 'unicode'> 2

python3

Python3中对字符串的支持进行了实现类层次的上简化,去掉了unicode类,添加了一个bytes类。从表面上来看,可以认为Python3中的str和unicode合二为一了。

1
2
class bytes(object)
class str(object)

实际上,Python3中已经意识到之前的错误,开始明确的区分字符串与字节。因此Python3中的str已经是真正的字符串,而字节是用单独的bytes类来表示。也就是说,Python3默认定义的就是字符串,实现了对UNICODE的内置支持,减轻了程序员对字符串处理的负担。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [1]: a = "中国"
In [2]: b = u"中国"
In [3]: c = a.encode("gbk")
In [4]: print(type(a), len(a))
<class 'str'> 2
In [5]: print(type(b), len(b))
<class 'str'> 2
In [6]: print(type(c), len(c))
<class 'bytes'> 4

字符编码转换

上面提到,UNICODE字符串可以与任意字符编码的字节进行相互转换,如图:

unicode 字符串

那么大家很容易想到一个问题,就是不同的字符编码的字节可以通过Unicode相互转换吗?答案是肯定的。

Python2中的字符串进行字符编码转换过程是:

字节串–>decode(‘原来的字符编码’)–>Unicode字符串–>encode(‘新的字符编码’)–>字节串

1
2
3
4
5
6
In [1]: a = "中国" # utf-8编码的字节串
In [2]: b = a.decode('utf-8').encode('gbk') # 先转换成unicode字符串,再进行gbk编码
In [3]: print b.decode('gbk') # 解码成unicode字符串
中国

Python3中定义的字符串默认就是unicode,因此不需要先解码,可以直接编码成新的字符编码:

字符串–>encode(‘新的字符编码’)–>字节串

1
2
3
4
5
6
In [1]: a = "中国"
In [2]: b = a.encode("gbk")
In [3]: print(b.decode("gbk"))
中国

结语

python字符串编码差不多就是这些内容了,搞清楚原理其实记忆也不难。祝大家不会再掉到python字符编码的坑中,祝我司能尽快迁移到python3!

go和python变量赋值的一个小问题

发表于 2017-08-30 | 分类于 go

平时写得多的是python,最近看了一点go,今天碰到了一个问题,和大家分享一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
import "fmt"
type student struct {
Name string
Age int
}
func pase_student() {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for _, stu := range stus {
m[stu.Name] = &stu
}
fmt.Println(m["zhou"].Name)
}
func main() {
pase_student()
}

代码很简单,大家可以思考一下会打印出什么。

1
time.sleep(60) # 思考

结果是wang!,惊喜不惊喜!遍历赋值啊同学们,这么简单的操作都能出幺蛾子,WTF!

为什么是wang呢?你tm给我解释解释什么是惊喜:

for循环的时候,变量stu的指针是不变的,每次循环仅仅是对student结构体的值拷贝,上面的for循环和下面是一样的:

1
2
3
4
var stu student
for _, stu = range stus {
m[stu.Name] = &stu
}

所以&stu自始至终都是一个地址,变化的是这个地址上存储的值。&stu最终存储的值是student{Name: "wang", Age: 22}结构体,所以拿出来的是wang。

可以将m打出来看一下:

1
map[zhou:0xc42000a260 li:0xc42000a260 wang:0xc42000a260]

验证了我们上面的想法,大家的value都是同一个地址。

看到这里,如果是一个日常写c,c++等强类型语言的同学可能会说,神经病啊!这有什么好说的!不就是这样的吗!请原谅我,我日常写python的 [捂脸]。

从上面的例子可以看出来,在go中,变量名是存储地址的名字。它在编译时绑定已经完成,运行时是不可以改变的,你只能改变地址中存储的值。

而在python中,变量是对象的名字,运行时变量可以绑定到任意的对象上。如下所示:

1
2
3
4
5
6
7
8
9
In [4]: a = 123456
In [5]: id(a)
Out[5]: 4426596208
In [6]: a = 1234567
In [7]: id(a)
Out[7]: 4426592592

注意:由于python对int类型实现了小整数对象池,不要用0-255的整数做实验,不然你得到id会是一样的。

也就是说,当你循环一个list的时候,每次得到的是不同对象,变量指向了不同的地址:

1
2
3
4
5
6
In [9]: for i in [2222, 2223, 2224]:
...: print(id(i))
...:
4426596208
4426592336
4426596080

上面这段代码,python为我们创建了3个PyIntObject,i只是他们的名字。而在go中,可以认为只有一个object,值变化了3次。

python中说的赋值就是建立一个对象的引用,是实话。

SSL/TLS 会话流量分析

发表于 2017-08-13 | 分类于 网络

原文:http://blog.fourthbit.com/2014/12/23/traffic-analysis-of-an-ssl-slash-tls-session

在这篇文章中,我将会给大家展示当我们使用SSL/TLS的时候,在协议层面上发生了什么。为了便于分析,我将会使用一个基于OpenSSL实现的非阻塞TCP客户端和服务端。

我们要记住,SSL/TLS能让应用像底层基础设施(网络和主机)一样安全,确保通信安全是它唯一做的事。SSL/TLS是一个独立的协议,它处于应用层(一般来说是HTTP,其他同样也可以)和传输层(TCP)之间。如此,TLS不需要上下层协议有大的改动,对用户而言,它几乎是透明的,也就是说用户根本不需要知道这个协议的存在。当然,这也带来了一些弊端,它在一些基础方面对协议做了限制(比如不支持UDP)。

自SSL/TLS诞生以来,它已经有了相当大的改进。现在,SSL 2.0和3.0被认为是不安全的,他们已经被TLS 1.0/1.1/1.2 替换。这些协议的历史是一个很有趣的话题。

协议描述

就像上面提到的,TLS协议在应用层和传输层中间。它被分割成两个主要的子层。下面是这个协议的主要结构和它在网络协议栈中的位置。

tls structure

下面的子层在TCP之上,因为TCP是一个面向连接的可靠的协议。这一层主要包括TLS记录协议。简而言之,记录协议首先将上层协议的数据变成2^14bytes或更少的块,然后对数据进行压缩(可选),加上一个消息认证码(Message Authentication Code),最后,根据协商的加密细则对数据进行加密并添加一个SSL记录头。值得注意的是,每一个块都会被打包进一个结构体,这个结构体并不保存应用层消息的边界,这就意味着多条相同类型的消息可能会被合并到一个结构体中。

下面的图描述了建立一个SSL Record的过程

ssl record

上面的子层在SSL记录协议(SSL Record Protocol)之上,它由四个子协议组成。每个子协议都有非常明确的目的,并在通信的不同阶段被应用:

  1. 握手协议(HandShake Protocol):它使双方可以相互认证,以及为本次连接协商出一个加密套件(cipher suite)和其他参数。SSL握手协议涉及到客户端和服务端之间4组消息交换。每一组都是在独立的TCP段(segment)中传输。下面的图为这个过程做了总结,它包含好几个步骤,一些步骤是可选的。注意,ChangeCipherSpec不属于这个协议,他们是独立的协议,下面会讲到。
    handshake

  2. ChangeCipherSpec协议:它使之前双方协商得到的参数起作用,通信变成加密的。

  3. Alert协议:用于通信异常,并预警可能降低通信安全性的问题
  4. 应用数据协议(Application Data Protocol):它接受任意大小的数据(通常是应用层数据),将数据喂给安全通道(feeds it through the secure channel)。

多条消息可以被串联成一条记录层(Record Layer)消息,但是这些消息必须归属于相同的子协议。结果是,这4个协议的每个都必须是自定界的(比如,都需要包含自己的长度字段)。

记录协议格式(Record Protocol format)

TLS 记录头(Record header)包含3个字段,高层协议建立在它之上:

  1. Byte 0:TLS记录类型(TLS recode type)
  2. Bytes 1-2: TLS version (major/minor)
  3. Bytes 3-4: Length of data in the record (excluding the header itself). The maximum supported is 16384 (16K)

record protocol format

握手协议格式(Handshake Protocol format)

这是TLS种最复杂的子协议。TLS标准主要也在这一部分,因为它为建立一个安全的连接处理了所有细节。下面的图展示了握手协议消息的主体结构。TLS标准存在10种(不包括扩展)握手消息类型,下面一一做了展示。

handshake message general structure

  • HelloRequest:允许服务器重启握手协商。不常用。如果一个连接被挂起了足够长时间只是安全性下降,服务器可以使用此消息迫使客户端重新协商新的会话秘钥(keys)。

hello request

  • ClientHello:这条消息代表了一个TLS握手协商的开始。发送它的同时会带上客户端支持的加密套件列表,服务端会挑选最合适的那个(安全性最强的那个),此外还有一个压缩方法列表,一个扩展列表。通过包含SessionId字段,它也赋予了客户端重新开始之前的会话的能力。

client hello

  • ServerHello:ServerHello消息和ClientHello消息很类似,除了它只包含一个密码套件和一个加密方法。如果他包含了一个SessionId(例如:SessionId长度大于0),就是通知客户端在以后重用这个会话。

server hello

  • Certificate:这个消息的消息体包含了一个公钥证书链。证书链使TLS支持证书分层和公钥基础设施(PKI:Public Key Infrastructures)

certificate

  • ServerKeyExchange:该消息携带客户端需要从服务器获得的密钥交换算法参数,以便之后能使用对称加密。这是可选的,因为并非所有密钥交换都要求服务器明确地发送此消息。实际上,在大多数情况下,Certificate消息足以使客户端与服务器安全地通信一个预先密钥。这些参数的格式完全取决于所选择的密钥套件(CipherSuite),它先前由服务器通过ServerHello消息设置。

server key exchange

  • CertificateRequest:当服务器需要客户端身份验证时使用它。在Web服务器中不常用,但在某些情况下非常重要。该消息不仅向客户端询问证书,还会告知哪些证书类型是可接受的。此外,它还指出那些证书机构是可信赖的。

certificate request

  • ServerHelloDone:该消息完成了握手协商的服务器部分。它没有附加信息。

server hello done

  • ClientKeyExchange:它为服务器提供必要的数据,以生成对称加密的密钥。消息格式与ServerKeyExchange非常相似,因为它主要取决于服务器选择的密钥交换算法。

client key exchange

  • CertificateVerify:客户端使用该消息来证明服务器拥有与其公钥证书相对应的私钥。该消息保存有客户端数字签名的散​​列信息。如果服务器向客户端发出CertificateRequest,则需要发送需要验证的证书。同样的,信息的确切尺寸和结构取决于商定的算法。在所有情况下,输入到哈希函数的信息是相同的。

certificate verify

  • Finished:此消息表示TLS协商已完成,密码套件(CipherSuite)已激活。应该发送已经加密,因为协商成功完成,所以必须在此之前发送一个ChangeCipherSpec协议消息来激活加密。Finish消息包含一个用所有先前的握手消息组合的哈希,其后是识别服务器/客户端角色的特殊号码,主密钥和填充。所产生的哈希与CertificateVerify哈希不同,因为存在更多的握手消息。

finish

ChangeCipherSpec协议格式

这是最简单的协议:它只有一条消息。该消息必须是单独的协议而不是握手协议的一部分的原因是由于记录层封装(Record Layer encapsulation)。 TLS协议同时对整个记录层消息进行加密。 ChangeCipherSpec消息表示加密的激活,并且由于加密不能应用于消息的一部分,因此任何其他消息都不可能遵循ChangeCipherSpec。避免这些组合的最佳方法是将此消息升级为协议状态。

下面展示ChangeCipherSpec消息是如何构造的:

change cipher spec

Alert协议格式

警报协议也很简单。它定义了两个字段:严重性级别和警报描述。第一个字段表示警报的严重性(警告为1,致命的2),而第二个字段则表示准确的条件。支持的警报描述取决于SSL/TLS版本。

alert protocol format

ApplicationData协议格式

该协议的任务是正确地封装来自网络堆栈的应用层的数据,从而可以通过底层协议(TCP)无缝地处理数据,而不会强制更改任何这些层。此协议中消息的格式遵循与以前协议相同的结构。

applicationdata protocol format

分析SSL/TLS流量

我将使用Wireshark进行抓包。客户端基于Scheme的方式支持SSL/TLS,服务端是OpenSSL分发的一部分(具有证书)。服务器监听在443端口,所有通信将通过环回设备进行。在Wireshark中将视图限制为TLS数据包的最简单方法是使用协议过滤器“ssl”。

第一个包(客户端->服务端)

一旦服务器运行并等待连接,客户端就可以启动它。这是客户端发送的第一个数据包

first flight

第二个包(服务端->客户端)

第一个包只包含一个从客户端发送到服务器的TLS握手消息(ClientHello)。然而,服务器发送给客户端的作为ClientHello响应的下一个TCP数据包携带3个握手消息。这些消息是ServerHello,Certificate和ServerHelloDone(没有发送ServerKeyExchange或CertificateRequest)。在下一个数据包之后,我将省略较低的堆栈协议(TCP/IP)。

second flight

第三个包(客户端->服务端)

目前看上去都是正确的,与上述的协议一致。客户端和服务器现在已经同意使用的算法(密钥交换的RSA,对称加密的AES-256-CBC和消息散列的SHA),压缩(无压缩)和使用的TLS扩展(SessionTicket TLS,重新谈判信息)。此外,客户端现在拥有服务器的证书,因此可以决定是否信任服务器。下一个数据包由客户端发送,并携带以下消息:ClientKeyExchange,ChangeCipherSpec,Finished(已经加密)。

third flight

第四个包(服务端->客户端)

在客户端发送ChangeCipherSpec和Finished之后,服务器预期执行相同的操作,以便双向启动采用对称密钥和所有协商的密码套件参数的加密通信。服务器必须发送自己的ChangeCipherSpec和Finished的消息,以使握手过程可以被认为是成功的。在这个消息中发生的一件非常有趣的事情是,我们看到其中一个扩展,扩展名称为Transport Layer Security(传输层安全TLS)会话恢复,而无需服务器端状态,它明确说明了它的作用。你应该记得,它被要求作为我们的ClientHello的一部分,并由服务器在其ServerHello中实现。有关与此扩展相关的消息的信息,我们需要查找RFC5077规范。如文档中所述,此扩展握手消息已被分配号码4。

fourth flight

应用层数据(客户端<->服务端)

此时,客户端和服务器完成握手。加密通信就位,应用数据可以安全传输。这是一个示例记录,单个TCP数据包可以携带几个消息:

application data

关闭连接

由于我们的连接是加密的,所以想知道在数据包中发送什么的唯一方法是使Wireshark或类似的工具知道传输中使用的密钥。尽管这是可能的,我认为为了分析的目的,当客户端或服务器主动关闭连接时,知道客户端会发出一个警告消息就足够了。此警报消息的类型应为CloseNotify(类型0),但是我们将无法从原始数据中看到它。在这种情况下,客户端是以下警报消息的发件人:

closing message

结语

从流量分析可以看出,标准库和客户端的实现遵循了TLS 1.0规范。希望这对您了解SSL/TLS协议的内部机制是有用的。我发现检查十六进制原始数据(特别是像Wireshark这样强大的工具)是一种非常有意义和有趣的工作方式。

123…18
You Wangqiu

You Wangqiu

世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也

171 日志
21 分类
24 标签
GitHub 知乎 E-Mail
© 2018 You Wangqiu
由 Hexo 强力驱动
主题 - NexT.Muse