0%

概述

仓库

存放代码的目录,分为本地仓库和远程仓库,本地和远程可以通过 git remote add 建立关联。

本地仓库有三大分区:

  • 工作区 (Working Directory): 是我们编辑代码的地方
  • 暂存区 (Stage or Index): 数据暂时存放的区域,可以在工作区和版本库之间进行数据的交流
  • 版本库 (Commit History): 存放已经提交的数据,push 的时候就是把这个区域的数据推送到远程仓库

分支

git 中的分支本质上是一个指向某个 commit 的指针。

文件的生命周期

对于 git 仓库里的每一个文件(除了 .git 目录和被 .gitignore 忽略的文件),有如下几种状态:

  • Untracked 未跟踪
  • Unmodified 未修改
  • Modified 已修改
  • Staged 暂存区

常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
git init # 在当前目录下初始化一个本地 git 仓库
git status # 检查当前文件状态
git diff # 比较工作区和暂存区
git diff HEAD # 比较工作区和版本库
git diff --cached # 比较暂存区和版本库
git add <filename> # 将 <filename> 添加到暂存区
git add -A # 将所有文件添加到暂存区
git add . # 将当前目录下的所有文件添加到暂存区
git commit # 提交更改(将暂存区的文件提交到 Commit History)
git commit --amend # 修改 commit 信息
git push origin <branch> # 将本地分支推送到远程仓库
git pull origin <branch> # 将远程仓库的分支拉取到本地
git clone <repository> [<directory>] # 克隆远程分支到本地,repository 可以由 schema 为 http[s], ssh, git, ftp[s], file 等各种 uri 表示,也可以是 scp 风格的路径表示。directory 表示克隆下来的仓库目录名
git config [--global|--local] user.name "<username>" # 设置用户名,用户名会出现在 commit 信息里,加了--local参数只影响当前仓库的配置,加--global参数会影响本机上所有仓库的配置,默认是--local
git config [--global|--local] user.email "<email>" # 设置用户邮箱,邮箱会出现在 commit 信息里
git remote add origin <repository> # 添加一个远程分支,命名为 origin
git log [--oneline] # 查看 commit 信息
git log --oneline --graph # 查看 commit 节点树

高级操作

1
2
3
4
5
6
7
8
9
10
11
12
git branch # 查看本地分支
git branch -a # 查看所有分支,包括远程分支
git branch <name> # 创建一个和当前分支版本库一样的分支
git checkout <branch> # 切换分支
git checkout -b <branch> <start_point> # 创建一个新分支并切换,新分支的 HEAD 指针指向 start_point, start_point 可以是本地分支或远程分支的任意一个 commit
git reset --soft <commit> # 将版本库的 HEAD 指向 commit
git reset --mixed <commit> # 将版本库和暂存区的 HEAD 指向 commit
git reset --hard <commit> # 将版本库、暂存区和工作区的 HEAD 指向 commit,但不会影响未跟踪的文件
git rebase <branch> # 把当前分支变基到 branch 上
git stash # 将暂存区中的文件储藏起来
git stash apply # 恢复储藏的文件到暂存区
git revert <commit> # 撤销之前的某一个 commit

如下实例中,蓝色是版本库,绿色是暂存区,红色是工作区:

git reset有三个选项:

  • soft就是只动repo
  • mixed就是动repo还有staging(这个是默认参数)
  • hard就是动repo还有staging还有working
  1. soft:只修改版本库repo

    1
    git reset --soft <B commit>
  2. mixed:修改版本库和暂存区

    1
    git reset --mixed <B commit>
  3. hard:修改版本库、暂存区和工作区

    1
    git reset --hard <B commit>

冲突解决

当发生冲突的时候,冲突的文件中会出现以下格式的内容。

解决冲突的时候,可以选择保留当前修改,保留另一个分支的修改,或是两个都不保留,重新编辑。

解决冲突的时候既要解决文本冲突,也要注意逻辑冲突。当冲突解决完成后,应该将当前文件加入暂存区,并执行对应命令加上 –continue 参数。

1
2
3
4
5
<<<<<<< HEAD
modification belongs to current branch
=======
modification belongs to other branch
>>>>>>> commit 38a1cab...

.gitignore

文件 .gitignore 的格式规范如下:

  • 所有空行或者以 # 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配。
  • 匹配模式可以以(/)开头防止递归。
  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# no .a files
*.a

# but do track lib.a, even though you're ignoring .a files above
!lib.a

# only ignore the TODO file in the current directory, not subdir/TODO
/TODO

# ignore all files in the build/ directory
build/

# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt

# ignore all .pdf files in the doc/ directory
doc/**/*.pdf

Merge & Rebase

  • Merge: 合并两个分支
  • Rebase: 改变分支的 base commit

这两种操作都可以合并分支,rebase 的优势在于可以使 commit history 的线性程度更高。线性程度高的好处在于,浏览历史的时候更整洁,不容易碰到分叉,分叉部分的代码差异可能更大。

1
2
git merge <branch> # 把 branch 合并到当前分支
git rebase <commit> # 把当前分支 rebase 到 commit 上

rebase黄金准则: 永远不要 rebase 到一个共享的分支,只能 rebase 自己使用的私有分支。

假设我们有如下图一所示仓库,该仓库有master和develop两个分支,且develop是在(3.added merge.txt file)commit处从master拉出来的分支。

merge

假设现在HEAD在(6.added hello.txt file)处,也就是在master分支最近的一次提交处,此时执行git merge develop, 结果如下图所示。

工作原理就是:git 会自动根据两个分支的共同祖先即 (3.added merge.txt file)这个 commit 和两个分支的最新提交即 (6.added hello.txt file) 和 (5.added test.txt file) 进行一个三方合并,然后将合并中修改的内容生成一个新的 commit,即图二的(7.Merge branch ‘develop’)。
这是merge的效果,简单来说就合并两个分支并生成一个新的提交。

rebase

假设初始状态也是图一所显示的。两个分支一个master,一个develop,此时HEAD在(6.added hello.txt file)处,现在执行git rebase develop,结果如下图三所示。

在执行git rebase develop之前,HEAD在(6.added hello.txt file)处,当执行rebase操作时,git 会从两个分支的共同祖先 (3.added merge.txt file)开始提取 当前分支(此时是master分支)上的修改,即 (6.added hello.txt file)这个commit,再将 master 分支指向 目标分支的最新提交(此时是develop分支)即(5.added test.txt file) 处,然后将刚刚提取的修改应用到这个最新提交后面。如果提取的修改有多个,那git将依次应用到最新的提交后面,如下两图所示,图四为初始状态,图五为执行rebase后的状态。

初始状态如下图六所示,和之前一样的是,develop分支也是在 (3.added merge.txt file)处从master分支拉取develop分支。不一样的是两个分支各个commit的时间不同,之前develop分支的4和5commit在master分支3之后6之前,现在是develop分支的4提交早于master分支的5提交,develop分支的6提交晚于master的5提交早于master的7提交。

在上图情况下,在master分支的7commit处,执行git merge develop,结果如下图七所示:

执行git rebase develop,结果如下图八所示:

  1. 可以看出merge结果能够体现出时间线,但是rebase会打乱时间线。
  2. 而rebase看起来简洁,但是merge看起来不太简洁。
  3. 最终结果是都把代码合起来了,所以具体怎么使用这两个命令看项目需要。

还有一点说明的是,在项目中经常使用git pull来拉取代码,git pull相当于是git fetch + git merge,如果此时运行git pull -r,也就是git pull –rebase,相当于git fetch + git rebase

撤销rebase

当我们在本地的develop分支开发完成后,commit修改的内容,然后将origin/develop分支rebase到本地的develop分支,准备push代码的时候发现origin/develop分支的内容有问题,需要回退本地分支到rebase之前的状态(也可以撤销rebase其他本地分支的操作),首先执行命令:git reflog。输出如下:

1
2
3
4
5
$ git reflog
eec33cda (HEAD -> develop) HEAD@{0}: rebase finished: returning to refs/heads/develop
eec33cda (HEAD -> develop) HEAD@{1}: rebase: commit test
bf335175 (origin/develop) HEAD@{2}: rebase: checkout origin/develop
af69383a HEAD@{3}: commit: commit test

解释一下上述输出:

  • HEAD@{3}:提交本次内容
  • HEAD@{2}:开始rebase,首先checkout到origin/develop分支
  • HEAD@{1}:将origin/develop分支上的变动rebase到本地develop分支
  • HEAD@{0}:rebase结束

如果要撤销本地rebase操作,只需要执行:

1
git reset --hard HEAD@{3}

即可成功撤销rebase操作,回退到之前commit的状态。

detached HEAD

首先看看下面这种常规的commit情况:

1
2
3
4
5
6
7
        HEAD (refers to branch 'master')
|
v
a---b---c branch 'master' (refers to commit 'c')
^
|
tag 'v2.0' (refers to commit 'b')

此时再执行下述操作:

1
2
3
4
5
6
7
8
9
$ edit; git add; git commit

HEAD (refers to branch 'master')
|
v
a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

运行命令git checkout master后会checkout到master的最新commit上,接下来运行下述命令:

1
2
3
4
5
6
7
8
9
$ git checkout v2.0  # 或 git checkout master^^

HEAD (refers to commit 'b')
|
v
a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

现在head已经指向commit b,这就是所谓的dedatched head状态。从这里可以看出,head是当前index的状态,而不是当前分支(的最近commit节点)。这仅仅意味着head指向某个特定的commit点,而不是指向每一个特定的分支(的顶端节点)。如果我们此时提交一个commit:

1
2
3
4
5
6
7
8
9
10
11
12
$ edit; git add; git commit

HEAD (refers to commit 'e')
|
v
e
|
|
a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

注意,此时产生了一个新的提交点,但是它只能被head索引到,不属于任何一个分支。当然,我们还可以给在这个“无名分支”的基础上继续提交。实际上,我们可以进行任何git的常规操作。但是,当我们运行git checkout master后:

1
2
3
4
5
6
7
8
9
10
$ git checkout master

HEAD (refers to branch 'master')
e |
| |
| v
a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

此时,commit e已经处于无法被索引到的状态。最终e将被git的默认回收机制所回收,除非在它们被回收之前创建一个指向它们的索引。如果我们没有从commit e离开的话,可以这样创建一个指向e的索引:

1
$ git checkout -b develop  # 创建一个develop分支,指向e,接着更新head指向分支develop,此时不再处在detached head的状态

patch

概述

如果一个软件有了新版本,我们可以完整地下载新版本的代码进行编译安装。然而,像Linux Kernel这样的大型项目,代码即使压缩,也超过70MB,每次全新下载是有相当大的代价的。然而,每次更新变动的代码可能不超过1MB,因此,我们只要能够有两个版本代码的diff的数据,应该就可以以极低的代价更新程序了。

git提供了两种简单的patch方案。一是用git diff生成的标准patch,二是git format-patch生成的Git专用Patch。

git diff生成的标准patch

  • 最初在master(或别的分支上)
  • 新建分支修改bug: git checkout -b fix
  • 在fix分支上,提交修改内容: git commit -m “fix a bug”
  • 与master分支对比产生patch: git diff > patch
  • 使用git apply命令,在一条干净的分支上应用patch: git checkout master —> git checkout -b PATCH — > git apply –check patch —–> (补丁patch可以加入的话) git apply patch —> git commit -m “apply patch”
  • 切换到master上合并PATCH分支:git checkout master —-> git merge PATCH

注意:无论多少个文件、多少次commit的diff,都只会产生一个patch。

git format-patch生成的git专用补丁

  • 最初在master(或别的分支上)
  • 新建分支修改bug: git checkout -b fix
  • 在fix分支上,提交修改内容: git commit -m “fix a bug”
  • 将此次commit后的fix分支内容与master对比,根据commit msg生成一个patch: git format-patch -M master —->生成0001-fix-a-bug.patch
  • 使用git am命令,在一条干净的分支上应用patch: git checkout master —> git checkout -b PATCH —> git am 0001-fix-a-bug.patch —> git commit -m “apply patch”
  • 切换到master上合并PATCH分支:git checkout master —-> git merge PATCH

注意:每次commit,都只会产生对应的一个patch。

  • git am时出错:.git/rebase-apply still exists but mbox given
    git am –abort(该命令将git的状态恢复到之前状态就可以继续提交patch了)
  • 打补丁
    patch -p1 < my.patch
  • 取消补丁
    patch -R -p1 < my.patch

创建patch 文件的常用命令行

  • 某次提交(含)之前的几次提交:git format-patch 【commit sha1 id】-n
    n指从sha1 id对应的commit开始算起n个提交。
    eg:git format-patch 2a2fb4539925bfa4a141fe492d9828d030f7c8a8 -2
  • 某个提交的patch:git format-patch 【commit sha1 id】 -1
    eg:git format-patch 2a2fb4539925bfa4a141fe492d9828d030f7c8a8 -1
  • 某两次提交之间的所有patch:git format-patch 【commit sha1 id】..【commit sha1 id】
    eg:git format-patch 2a2fb4539925bfa4a141fe492d9828d030f7c8a8..89aebfcc73bdac8054be1a242598610d8ed5f3c8

创建diff文件的常用方法

1
2
3
git diff  【commit sha1 id】 【commit sha1 id】 >  【diff文件名】
# eg:
git diff 2a2fb4539925bfa4a141fe492d9828d030f7c8a8 89aebfcc73bdac8054be1a242598610d8ed5f3c8 > patch.diff

两种patch的比较

  • 兼容性:很明显,git diff生成的Patch兼容性强。如果你在修改的代码的官方版本库不是Git管理的版本库,那么你必须使用git diff生成的patch才能让你的代码被项目的维护人接受。
  • 除错功能:对于git diff生成的patch,你可以用git apply –check 查看补丁是否能够干净顺利地应用到当前分支中;如果git format-patch 生成的补丁不能打到当前分支,git am会给出提示,并协助你完成打补丁工作,你也可以使用git am -3进行三方合并。从这一点上看,两者除错功能都很强。
  • 版本库信息:由于git format-patch生成的补丁中含有这个补丁开发者的名字,因此在应用补丁时,这个名字会被记录进版本库,显然,这样做是恰当的。因此,目前使用Git的开源社区往往建议使用format-patch生成补丁。

submodule

  • 添加:git submodule add repo_address path,path可省略
  • 更新:git submodule update
  • 初始化/更新:git submodule update --init --recursive
  • 删除:首先,要在.gitmodules文件中删除相应配置信息。然后,执行git rm –cached命令将子模块所在的文件从git中删除
  • clone带有submodule的项目:git clone --recurse-submodules address

Git LFS

Git 在 clone 过程中会将仓库的整个历史记录传输到客户端,对于包涵大文件(尤其是经常被修改的大文件)的项目,初始克隆需要大量时间,因为客户端会下载每个文件的每个版本。

Git LFS(Large File Storage) 是 Git 的一个扩展,用于实现 Git 对大文件的支持。它通过延迟下载大文件的相关版本来减少大文件在仓库中的影响,大文件是在 checkout 过程中下载的,而不在 clone 或 fetch 过程中下载, Git LFS 通过将仓库中的大文件替换为小指针文件来做到这一点。使用时用户不会注意到这些指针文件,因为它们是由 Git LFS 自动处理的:

  1. 当 add 一个文件到仓库时,Git LFS 用一个指针替换其内容,并将文件内容存储在本地 Git LFS 缓存中(缓存位于 .git/lfs/objects 目录中)。
  2. 当 push 新的提交到服务器时,新提交引用的所有 Git LFS 文件都会从本地 Git LFS 缓存传输到绑定到 Git 仓库的远程 Git LFS 存储(即 lfs 文件内容会直接从本地 Git LFS 缓存传输到远程 Git LFS 存储服务器)。
  3. 当 checkout 一个包含 Git LFS 指针的提交时,指针文件将替换为本地 Git LFS 缓存中的文件,或者从远端 Git LFS 存储区下载。

git clone 和 git pull 将明显更快,因为只下载实际 checkout 的提交所引用的大文件版本,而不是曾经存在过的文件的每一个版本。使用 Git LFS 需要一个支持 Git LFS 的托管服务器如 Bitbucket CloudBitbucket Server, 另外 GitHub 和 GitLab 也都支持 Git LFS。

一些用法

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
# 安装 git-lfs
$ brew install git-lfs

# 初始化 Git LFS, 只需运行一次,后面当 clone 包含 Git LFS 内容的仓库时,Git LFS 将自动启用
$ git lfs install

# 创建一个新的 Git LFS 仓库
$ git init
$ git lfs install
$ git lfs track ... # 指定要跟踪的文件

# 可以使用 git clone 命令来克隆 Git LFS 仓库,当克隆包含大量 lfs 文件的仓库时显式使用 git lfs clone 命令性能更好
# git lfs clone 命令不会一次下载一个 Git LFS 文件,而是等到 checkout 完成后再批量下载所有必需的 lfs 文件
# 利用并行下载的优势,并显著减少了产生的 HTTP 请求和进程的数量
$ git lfs clone git@xxx.git

# 可以使用常规的 git pull 命令拉取 Git LFS 仓库,拉取完成后需要的 Git LFS 文件都会作为自动检出过程的一部分而被下载
# 如果检出因为意外原因而失败则可通过 git lfs pull 命令来下载当前所有丢失的 Gi t 内容
$ git lfs pull

# 使用 git lfs track 命令指定一个模式告诉 Git LFS 对其进行跟踪
# 运行 git lfs track 后会在仓库中发现名为 .gitattributes 的新文件,Git LFS 自动创建或更新 .gitattributes 文件
# gitattributes 是一种 Git 机制,用于将特殊行为绑定到某些文件模式
$ git lfs track "*.ogg"

# 可以通过调用不带参数的 git lfs track 命令来显示 Git LFS 当前正在跟踪的所有模式的列表及 .gitattributes 文件
$ git lfs track
Listing tracked patterns
*.ogg (.gitattributes)
*.a (lib/.gitattributes)
Listing excluded patterns

# 可以从 .gitattributes 文件中删除相应的行或者运行 git lfs untrack 命令来停止使用 Git LFS 跟踪特定模式
$ git lfs untrack "*.ogg"

# 可以按常规方式 push 到包含 Git LFS 内容的仓库,如果传输 LFS 文件失败,推送将被终止,可以放心地重试
$ git push

# 可以使用 git lfs prune 命令从本地 Git LFS 缓存中删除文件,这将删除所有被认为是旧的本地 Git LFS 文件
# 可以使用 git lfs prune --dry-run 来测试修剪操作将产生什么效果
# 可以使用 git lfs prune --verbose --dry-run 命令精确查看哪些 Git LFS 对象将被修剪
# 可以使用 --verify-remote 选项在删除之前,检查远程 Git LFS 存储区是否具有你的 Git LFS 对象的副本,更安全
# 可以通过全局配置 lfs.pruneverifyremotealways 属性为系统永久启用 --verify-remote 选项
$ git lfs prune
✔ 4 local objects, 33 retained
Pruning 4 files, (2.1 MB)
✔ Deleted 4 files
$ git config --global lfs.pruneverifyremotealways true

数据结构

Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// /eos/libraries/chain/include/eosio/chain/block.hpp
struct signed_block : public signed_block_header {
private:
signed_block( const signed_block& ) = default;
public:
signed_block() = default;
explicit signed_block( const signed_block_header& h ):signed_block_header(h){}
signed_block( signed_block&& ) = default;
signed_block& operator=(const signed_block&) = delete;
signed_block clone() const { return *this; }

vector<transaction_receipt> transactions; /// new or generated transactions
extensions_type block_extensions;
};

// /eos/libraries/chain/include/eosio/chain/types.hpp
typedef vector<std::pair<uint16_t,vector<char>>> extensions_type;

signed_block结构体是从signed_block_header继承而来的,这个signed_block_header结构体只是包含了一条数据,那就是producer的签名。signed_block_header结构体又是从block_header继承而来的,这个结构体就包含了一个block中很多重要的数据,包括时间戳,producer的名字,所有交易的merkle root,所有action的root等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /eos/libraries/chain/include/eosio/chain/block_header.hpp
struct block_header
{
block_timestamp_type timestamp;
account_name producer;
uint16_t confirmed = 1;

block_id_type previous;

checksum256_type transaction_mroot;
checksum256_type action_mroot;

uint32_t schedule_version = 0;
optional<producer_schedule_type> new_producers;
extensions_type header_extensions;
};

struct signed_block_header : public block_header
{
signature_type producer_signature;
};

Transaction

signed_block中定义了transaction_receipt类型的vector,表示区块是由按顺序组织的交易来构成的集合。block_extensions则定义了一系列的扩展信息,这些信息都由一个整数类型的code来定义,需要的时候,都可以根据这个整数code来解析相应的信息。下面看看transaction_receipt的定义:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// /eos/libraries/chain/include/eosio/chain/block.hpp
struct transaction_receipt_header {
enum status_enum {
executed = 0, ///< succeed, no error handler executed
soft_fail = 1, ///< objectively failed (not executed), error handler executed
hard_fail = 2, ///< objectively failed and error handler objectively failed thus no state change
delayed = 3, ///< transaction delayed/deferred/scheduled for future execution
expired = 4 ///< transaction expired and storage space refuned to user
};

transaction_receipt_header():status(hard_fail){}
explicit transaction_receipt_header( status_enum s ):status(s){}

friend inline bool operator ==( const transaction_receipt_header& lhs, const transaction_receipt_header& rhs ) {
return std::tie(lhs.status, lhs.cpu_usage_us, lhs.net_usage_words) == std::tie(rhs.status, rhs.cpu_usage_us, rhs.net_usage_words);
}

fc::enum_type<uint8_t,status_enum> status;
uint32_t cpu_usage_us = 0; ///< total billed CPU usage (microseconds)
fc::unsigned_int net_usage_words; ///< total billed NET usage, so we can reconstruct resource state when skipping context free data... hard failures...
};

struct transaction_receipt : public transaction_receipt_header {

transaction_receipt():transaction_receipt_header(){}
explicit transaction_receipt( const transaction_id_type& tid ):transaction_receipt_header(executed),trx(tid){}
explicit transaction_receipt( const packed_transaction& ptrx ):transaction_receipt_header(executed),trx(ptrx){}

fc::static_variant<transaction_id_type, packed_transaction> trx;

digest_type digest()const {
digest_type::encoder enc;
fc::raw::pack( enc, status );
fc::raw::pack( enc, cpu_usage_us );
fc::raw::pack( enc, net_usage_words );
if( trx.contains<transaction_id_type>() )
fc::raw::pack( enc, trx.get<transaction_id_type>() );
else
fc::raw::pack( enc, trx.get<packed_transaction>().packed_digest() );
return enc.result();
}
};

// /eos/libraries/chain/include/eosio/chain/types.hpp
using checksum_type = fc::sha256;
using transaction_id_type = checksum_type;

// /eos/libraries/chain/include/eosio/chain/transaction.hpp
struct packed_transaction {
// 定义打包数据是否压缩的枚举类型
enum compression_type {
// 没有压缩
none = 0,
// 使用zlib压缩
zlib = 1,
};

// 签名信息
vector<signature_type> signatures;
// 是否压缩的标识信息
fc::enum_type<uint8_t,compression_type> compression;
// 上下文无关的信息
bytes packed_context_free_data;
// 打包后的交易数据
bytes packed_trx;
}

transaction_receipt_header主要是记录了这个交易的状态信息,以及CPU和网络的使用情况。当一笔交易被某个区块引用时,区块生产者针对这笔交易会做出相应的操作,而操作的不同结果会导致这笔交易的不同状态.而transaction_receipt结构体主要包含了一个打包过的交易以及其对应的交易类型。

packed_transaction中打包的数据来自于signed_transaction结构体,这个结构体的主要作用就是对交易做签名。signed_transaction又是从transaction结构体继承而来,一个transaction结构体的实例包含一系列的action,这些action要么全部成功,要么全部失败。

交易ID是通过对交易内容本身经过Hash运算得出,所以每个交易的ID是与其内容一一对应的。交易的主体是由操作构成的。一个交易在纳入区块之前必须含有签名,用以验证交易的合法性。

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
// /eos/libraries/chain/include/eosio/chain/transaction.hpp
struct signed_transaction : public transaction
{
// 签名信息
vector<signature_type> signatures;
// 上下文无关的数据
vector<bytes> context_free_data;
};

struct transaction : public transaction_header {
// 上下文无关的action
vector<action> context_free_actions;
// 交易操作
vector<action> actions;
// 交易扩展类型
extensions_type transaction_extensions;
}

// /eos/libraries/chain/include/eosio/chain/transaction.hpp
struct transaction_header {
// 这个交易的过期时间
time_point_sec expiration;
// 在最后的2^16 blocks中指定一个具体的block number
uint16_t ref_block_num = 0U;
// 在指定的get_ref_blocknum的blockid中取低位的32bit
uint32_t ref_block_prefix = 0UL;
// 最大的网络带块
fc::unsigned_int max_net_usage_words = 0UL;
// 最大的CPU使用
uint8_t max_cpu_usage_ms = 0;
// 这个交易的延期时间
fc::unsigned_int delay_sec = 0UL;
}

交易分为两种类型:一种是账户发起的普通交易,一种是由代码生成的自动交易,自动交易可以设置一个延迟时间,这样的交易叫延迟型交易,这种交易不会立即被执行,而是等到设定时间到时才会被执行。

transaction_header结构体包含了与每一个交易相关联的固定大小的数据,这些数据从具体的交易数据中分离出来,可以在需要的时候,帮助解析交易数据,而不再需要更多的动态内存分配。

所有的交易都有一个期限,这个期限限定了一个交易必须在规定时间内被纳入区块链,如果我们发现一个交易的时限已经过去,就可以放心的放弃这个交易,因为所有生产者都不会将它纳入任何区块。

Action

1
2
3
4
5
6
7
8
9
10
11
// /eos/contracts/eosiolib/action.hpp
struct action {
// 账户:操作的来源
account_name account;
// 名称:操作的标识
action_name name;
// 授权:执行操作的许可列表
vector<permission_level> authorization;
// 数据:执行操作需要用到的信息
bytes data;
}

EOS区块链中的交易是由一个个action组成的,操作可以理解成一个能够更改区块链全局状态的方法,操作的顺序是确定的,一个交易内的操作要么全部执行成功,要么都不执行,这与交易的本意是一致的。操作是区块链的最底层逻辑,相当于区块链这个大脑的神经元,区块链的智能最终也是通过一个个操作的组合来实现的。

操作的设计原则:

  • 独立原则 操作本身须包含足以解释操作含义的信息,而不需要依赖区块链提供的上下文信息来帮助解释。所以,即便一个操作的当前状态可以通过区块链上的数据推导得出,我们也需要将状态信息纳入操作数据中,以便每个操作是容易理解的。这个原则体现的是区块的可解释性,这一点非常重要,这个底层的设计原则将影响整个区块链的使用效率
  • 余额可计算原则 一个账户当前的余额计算,仅仅依赖于与这个账户相关的信息便可得出,而不需要解析整个区块链才能获得。这个原则针对的是比特币的设计,由于比特币的余额计算需要扫描区块链中的所有交易才能精准的计算出一个账户的余额,这使得一个非常基础的计算落地起来都变得相当繁琐,EOS的这个设计目的在于提升运算效率。
  • 明确费用原则 区块链的交易费用随时间变化而变化,所以,一个签名过的交易须明确的认同这个交易所需要支付的费用,这个费用是在交易形成之前就已经设定并且明确好了的,这一点也非常重要,因为明确的费用协议才能保证余额的正确计算。
  • 明确授权原则 每个操作须包含足够的授权信息以标明是哪一个账户拥有授权这个操作的权力,这种明确授权的设计思路带来几个好处:
  • 便于集中管理
  • 可以优化授权管理
  • 便于并行处理
  • 关联账户原则 每个操作须包含足够的关联账户信息,以保证这个操作能够遍历所有相关联的账户,也就是这个操作能够影响的所有账户,这个原则的目的同样是为了确保账户的余额能够得到及时和准确的运算

操作的来源:

  • 由一个账号产生,通过签名来授权,即显性方式。
  • 由代码生成,即隐形方式。

区块日志

区块日志是存储区块的二进制文件,区块日志的特性是只能从末尾追加(append only),区块日志包含两类文件:

区块文件,结构如下:

+———+—————-+———+—————-+—–+————+——————-+

| Block 1 | Pos of Block 1 | Block 2 | Pos of Block 2 | … | Head Block | Pos of Head Block |

+———+—————-+———+—————-+—–+————+——————-+

区块文件包含区块的内容以及每个区块的位置信息。区块位置信息是固定的8字节宽度,这样便于在连续读取区块的时候,按照读一个区块,向后跳跃8个字节,读一个区块,向后跳跃8个字节的模式快速加载区块内容。

索引文件,结构如下:

+—————-+—————-+—–+——————-+

| Pos of Block 1 | Pos of Block 2 | … | Pos of Head Block |

+—————-+—————-+—–+——————-+

区块索引的目的在于提供一个基于区块序号的快速随机搜索方法,使用索引文件可以快速定位目标区块在区块文件中的具体位置。索引文件不是必须的,没有索引文件区块链仍然可以运行,索引文件的主要作用是通过少量空间换取速度提升。索引文件可以通过顺序读取区块文件来重新构建。

简介

EOS: Enterprise Operation System 中文意思为:商业级区块链操作系统。EOS 项目的目标是建立可以承载商业级智能合约与应用的区块链基础设施,成为区块链世界的“底层操作系统”。

EOS通过石墨烯技术解决延迟和数据吞吐量问题,TPS可达到数千,交易的确认时间也只有数秒。同时声称未来使用并行链的方式,最高可以达到数百万TPS。此外,在 EOS 上转账交易及运行智能合约不需要消耗 EOS代币。而是EOS 系统当中,抵押代币获取对应的资源,来执行相应交易,在EOS运行程序完全免费的说是不准确的。

EOS底层使用的是石墨烯技术,石墨烯是一个开源的区块链底层库,采用的是 DPOS(Delegated Proof-of-Stake 股份授权证明机制 )的共识机制。DPOS为了提高出块速度TPS,限制了参与记账了人数,在DPOS中,记账者不称为矿工,而是改称为见证人Witness,现在EOS中,又有一个新词:Block Producer,简称BP,翻译为超级节点。

DPOS下节点需要参与见证人选举,只有赢得选举的节点才能负责出块,在EOS中,赢得选举的21个节点见证人轮流出块。另外还有100个备用见证人(候选节点),在21个见证人出现问题后做替补。EOS的发行总量是10亿,见证人在完成打包交易区块后,可以领取到区块的奖励,区块的奖励来自对发行量的通胀增发,通胀率每年接近5%。

钱包和账户

钱包

EOS钱包功能官方上定义很单一,仅仅存私钥公钥,提供私钥公钥生成和导入,其他的功能由EOS账户统一管理,比如代币查询、交易、合约功能都属于账户关联下的功能。

账户

EOS账号由一串自定义字符组成,与以太坊中不同。账户名下有EOS系统的代币和用户调用eosio.token合约创建的代币、合约和公钥等

钱包和账户关系

EOS里面的key是公私钥对成对出现的,key和password不是同一个概念,虽然看起来都是一串很长的字符,但是password是针对钱包wallet的,而key则是针对账户account的。

假如创建了钱包hearing,不等于已经在链上有了hearing这个账户,实际上wallet和account两者之间完全是没有关系的,只有当你将account的key放在wallet里的时候,它们两者之间才有了联系。EOS账户创建需要关联钱包公钥,账户创建可以使用同一个钱包公钥创建多个账户,但各个账户是独立的。可以理解成使用同一个密码创建了不同的账户,账户数据相互独立。

每一个account都会有两组公私钥对,对应该账户的两个角色——owner和active。所以我们每次创建账户的时候,都需要给新账户导入两个key进去。官方推荐用户平时使用 Active 私钥,把Owner 私钥离线保存。只有重大安全问题时需要用到 Owner 私钥。

权限

-p

创建钱包和账户

  1. 开启keosd

    1
    $ keosd &
    1
    $ pkill keosd
  2. 开启nodeos

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $ nodeos -e -p eosio \
    --plugin eosio::producer_plugin \
    --plugin eosio::chain_api_plugin \
    --plugin eosio::http_plugin \
    --plugin eosio::history_plugin \
    --plugin eosio::history_api_plugin \
    --access-control-allow-origin='*' \
    --contracts-console \
    --http-validate-host=false \
    --verbose-http-errors \
    --filter-on='*' >> nodeos.log 2>&1 &
  3. 钱包操作

    1
    2
    3
    4
    5
    6
    7
    $ cleos wallet list   # 查看钱包
    $ cleos wallet create --to-console/--file -n hearing # 创建钱包,生成私钥,-n指定钱包名,默认名为default
    $ cleos wallet open # 打开钱包
    $ cleos wallet unlock # 解锁钱包,需要提供钱包私钥
    $ cleos wallet create_key # 在钱包中创建私钥
    Created new private key with a public key of: "EOS7qyuXyBtqMYLYBveB3APTiWeyu1d6Z4mTLX1mMP5ZU3kWUqXcJ"
    $ cleos wallet keys # 查看钱包的keys
  4. 导入开发者Key

    每个新的EOSIO链都有一个名为“eosio”的默认“系统”账户。此帐户用于通过加载系统合同来设置链,该合同规定了EOSIO链的治理和共识。每个新的EOSIO链都带有一个开发密钥,这个密钥是相同的(5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3)。这个账户在我看来就是一个使用5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3秘钥对创建的,所以开发者需要把它导入到某个钱包,并得到对应的公钥。

    1
    $ cleos wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3   # 将开发者私钥导入到default钱包,会输出对应的公钥,可通过-n指定钱包
  5. 创建秘钥

    1
    2
    3
    $ cleos create key --to-console
    Private key: 5KRuAjE9xf6zK3AeMTZa9yLCZMQmhaBxuZePRtdUnWqB5nkd5Jk
    Public key: EOS8XaSdS9EieYyWuAfUma7cmB4TEuPWjcU8mpMRR2jztuwd1Cs2a
    1
    2
    3
    $ cleos create key --to-console
    Private key: 5J3BgEZ168EEP9shdnWBhZsPZRAfjjpUyKVxx1qjezXzJW9bEZv
    Public key: EOS81sw5LyVND1KjYoGA7iYNSo9ezgyucEjNzVAP1bPBHRdgkJo29
  6. 向wallet导入秘钥

    1
    2
    $ cleos wallet import -n hearing --private-key 5J3BgEZ168EEP9shdnWBhZsPZRAfjjpUyKVxx1qjezXzJW9bEZv
    imported private key for: EOS81sw5LyVND1KjYoGA7iYNSo9ezgyucEjNzVAP1bPBHRdgkJo29
    1
    2
    $ cleos wallet import -n hearing --private-key 5KRuAjE9xf6zK3AeMTZa9yLCZMQmhaBxuZePRtdUnWqB5nkd5Jk
    imported private key for: EOS8XaSdS9EieYyWuAfUma7cmB4TEuPWjcU8mpMRR2jztuwd1Cs2a
  7. 创建账户

    1
    $ cleos create account [OPTIONS] creator name OwnerKey [ActiveKey]  # 命令中的key为public-key
    1
    2
    $ cleos create account eosio hearing EOS81sw5LyVND1KjYoGA7iYNSo9ezgyucEjNzVAP1bPBHRdgkJo29 EOS8XaSdS9EieYyWuAfUma7cmB4TEuPWjcU8mpMRR2jztuwd1Cs2a
    $ cleos create account hearing hearing1 EOS81sw5LyVND1KjYoGA7iYNSo9ezgyucEjNzVAP1bPBHRdgkJo29 EOS8XaSdS9EieYyWuAfUma7cmB4TEuPWjcU8mpMRR2jztuwd1Cs2a

智能合约

在eos私有节点操作中,我们通常是一个合约对应一个合约账户,并且一个账户中只能部署一个智能合约。如果在同一个账户部署多个合约,那么最后部署的合约会覆盖掉之前的合约。

HelloWorld

  1. 编写合约

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <eosiolib/eosio.hpp>

    using namespace eosio;

    class [[eosio::contract("hello")]] hello : public contract {
    public:
    using contract::contract;

    [[eosio::action]]
    void hi( name user ) {
    print( "Hello, ", user);
    }
    };

    EOSIO_DISPATCH( hello, (hi))
  2. 编译合约

    1
    $ eosio-cpp -I include -o hello.wasm hello.cpp --abigen
  3. 创建账户

    1
    $ cleos create account eosio hello EOS7qyuXyBtqMYLYBveB3APTiWeyu1d6Z4mTLX1mMP5ZU3kWUqXcJ -p eosio@active    # -p指定账户的权限
  4. 部署合约

    1
    $ cleos set contract hello CONTRACTS_DIR/hello -p hello@active
  5. 调用合约

    1
    $ cleos push action hello hi '["bob"]' -p alice@active

eosio.token合约

在eos目录中自带的合约中,有一个eosio.token智能合约,这个智能合约的功能是为账户发放token,token可以用来转账操作。

  1. 创建账户

    1
    $ cleos create account eosio eosio.token EOS81sw5LyVND1KjYoGA7iYNSo9ezgyucEjNzVAP1bPBHRdgkJo29 EOS8XaSdS9EieYyWuAfUma7cmB4TEuPWjcU8mpMRR2jztuwd1Cs2a
  2. 把eosio.token合约部署到eosio.token账户上

    1
    $ cleos set contract eosio.token ./eosio.token
  3. 创建代币

    1
    $ cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token@active
  4. 为账户发放token

    1
    $ cleos push action eosio.token issue '[ "lilei", "1000.0000 EOS", "" ]' -p eosio@active
  5. 交易token

    1
    $ cleos push action eosio.token transfer '[ "eosio", "hearing", "25.0000 EOS", "m" ]' -p eosio@active
  6. 查询余额

    1
    $ cleos get currency balance eosio.token hearing EOS

单主机多节点

关于配置

比如机器10.186.11.211上的部分配置:

1
2
3
4
5
6
7
8
9
10
bnet-endpoint = 10.186.11.211:4321    

//for communicatin with cleos
http-server-address = 10.186.11.211:8888

//for sync block
p2p-listen-endpoint = 10.186.11.211:9876
p2p-peer-address = 10.186.11.223:9876
p2p-peer-address = 10.186.11.220:9876
p2p-peer-address = 10.186.11.141:9876
  • bnet-endpoint: 所监听的传入链接的端点。 默认:0.0.0.0:4321
  • http-server-address: 本地的http服务地址 默认: 127.0.0.1:8888
  • p2p-listen-endpoint: 所监听的p2p传入链接的端点。 默认:0.0.0.0:9876
  • p2p-peer-address: 公共的p2p对等节点地址。

启动keosd

1
$ keosd --http-server-address 127.0.0.1:8899

创建钱包

1
2
3
4
5
$ cleos --wallet-url http://127.0.0.1:8899  wallet create --to-console
Creating wallet: default
Save password to use in the future to unlock this wallet.
Without password imported keys will not be retrievable.
"PW5J2VNR7bJpNtuJXwaEy2LNug5BNbBRRZUR8DcMPd7CrqMVtvnVn"

加载eosio秘钥

1
2
$ cleos --wallet-url http://127.0.0.1:8899 wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
imported private key for: EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

启动第一个生产节点

1
$ nodeos --enable-stale-production --producer-name eosio --plugin eosio::chain_api_plugin --plugin eosio::net_api_plugin --max-transaction-time=1000 --hard-replay-blockchain

启动第二个生产节点

加载eosio.bios合约

要启动其他节点,必须先加载eosio.bios合同。通过此合同,可以直接控制其他帐户的资源分配并访问其他特权API调用。(如果第一次已经加载了合约,那么这一步可以跳过)

1
$ cleos --wallet-url http://127.0.0.1:8899 set contract eosio ./eosio.bios

创建账户(inita)

1
2
3
4
5
6
7
8
$ cleos create key --to-console
Private key: 5JcjxL4XqNvR85aQ9hAqworWJuoJfDoGo7wUtZLDbVePRXWCxUf
Public key: EOS58iXdanxG4nwok3AVSrDGu4Fj2dTB9HqeLUdbQu6wRRgyq18CS

$ cleos --wallet-url http://127.0.0.1:8899 wallet import --private-key 5JcjxL4XqNvR85aQ9hAqworWJuoJfDoGo7wUtZLDbVePRXWCxUf
imported private key for: EOS58iXdanxG4nwok3AVSrDGu4Fj2dTB9HqeLUdbQu6wRRgyq18CS

$ cleos --wallet-url http://127.0.0.1:8899 create account eosio inita EOS58iXdanxG4nwok3AVSrDGu4Fj2dTB9HqeLUdbQu6wRRgyq18CS EOS58iXdanxG4nwok3AVSrDGu4Fj2dTB9HqeLUdbQu6wRRgyq18CS

命令行启动节点

1
$ nodeos --producer-name inita --plugin eosio::chain_api_plugin --plugin eosio::net_api_plugin --http-server-address 127.0.0.1:8889 --p2p-listen-endpoint 127.0.0.1:9877 --p2p-peer-address 127.0.0.1:9876 --config-dir node2 --data-dir node2 --private-key 5JcjxL4XqNvR85aQ9hAqworWJuoJfDoGo7wUtZLDbVePRXWCxUf

将inita注册为具有bios节点的生产者,并且bios节点需要执行动作来更新生产者计划(测试失败):

1
$ cleos --wallet-url http://127.0.0.1:8899 push action eosio setprods "{ \"schedule\": [{\"producer_name\": \"inita\",\"block_signing_key\": \"EOS58iXdanxG4nwok3AVSrDGu4Fj2dTB9HqeLUdbQu6wRRgyq18CS\"}]}" -p eosio@active

配置文件启动节点

也可以通过conf文件启动多个节点,方式如下:

node2.ini:

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
# for communicatin with cleos
http-server-address = 127.0.0.1:8889

# for sync block
p2p-listen-endpoint = 127.0.0.1:9877
p2p-peer-address = 127.0.0.1:9876
p2p-peer-address = 127.0.0.1:9878

# agent-name = "EOS Test Agent"

# if eosio, this flag must be true, else must be set false, it decide whether or not
# product block
enable-stale-production = true

# producer name
producer-name = inita

# producer key,get by use"cleos ceate key"
# private-key =["EOS8Znrtgwt8TfpmbVpTKvA2oB8Nqey625CLN8bCN3TEbgx86Dsvr", "5K463ynhZoCDDa4RDcr63cUwWLTnKqmdcoTKTHBjqoKfv4u5V7p"]

# load plugin
plugin = eosio::chain_api_plugin
plugin = eosio::history_api_plugin
plugin = eosio::chain_plugin
plugin = eosio::history_plugin
plugin = eosio::net_plugin
plugin = eosio::net_api_plugin

node3.ini:

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
# for communicatin with cleos
http-server-address = 127.0.0.1:8890

# for sync block
p2p-listen-endpoint = 127.0.0.1:9878
p2p-peer-address = 127.0.0.1:9876
p2p-peer-address = 127.0.0.1:9877

# agent-name = "EOS Test Agent"

# if eosio, this flag must be true, else must be set false, it decide whether or not
# product block
enable-stale-production = true

# producer name
producer-name = initb

# producer key,get by use"cleos ceate key"
# private-key =["EOS8Znrtgwt8TfpmbVpTKvA2oB8Nqey625CLN8bCN3TEbgx86Dsvr", "5K463ynhZoCDDa4RDcr63cUwWLTnKqmdcoTKTHBjqoKfv4u5V7p"]

# load plugin
plugin = eosio::chain_api_plugin
plugin = eosio::history_api_plugin
plugin = eosio::chain_plugin
plugin = eosio::history_plugin
plugin = eosio::net_plugin
plugin = eosio::net_api_plugin

启动node2:

1
nodeos --config node2.ini --config-dir node2 --data-dir node2 --private-key 5JcjxL4XqNvR85aQ9hAqworWJuoJfDoGo7wUtZLDbVePRXWCxUf

启动node3:

1
nodeos --config node3.ini --config-dir node3 --data-dir node3 --private-key 5JcjxL4XqNvR85aQ9hAqworWJuoJfDoGo7wUtZLDbVePRXWCxUf

搭建测试网络的自动化脚本

为方便搭建测试环境,我将一些相关的操作都封装到了一个shell脚本中,在此附上脚本的github地址:https://github.com/ljd1996/eos_script/tree/master/mult_node.

该脚本的目录结构如下:

├── conf
│   ├── node10.ini
│   ├── node11.ini
│   ├── node12.ini
│   ├── node1.ini
│   ├── node2.ini
│   ├── node3.ini
│   ├── node4.ini
│   ├── node5.ini
│   ├── node6.ini
│   ├── node7.ini
│   ├── node8.ini
│   └── node9.ini
├── data
├── key
├── log
└── wpk
├── start.sh

下面我将分别介绍每个文件和目录的作用:

  • data目录主要存储节点启动后的区块等相关的数据,均由EOS自动生成,不需要自己做相关处理;
  • key目录用来存储节点构建过程中生成的账户的秘钥对;
  • log用来存项keos和nodeos运行过程中的日志输出;
  • wpk目录用来存储账户数据;
  • start.sh是脚本的源码。

其中,在运行脚本时我们唯一需要修改的是conf下面的配置文件,下面是其中一个配置文件的内容:

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
# for communicatin with cleos
http-server-address = 192.168.11.103:8888

# for sync block
p2p-listen-endpoint = 192.168.11.103:9876
p2p-peer-address = 192.168.11.103:9877
p2p-peer-address = 192.168.11.103:9878
p2p-peer-address = 192.168.11.103:9879
p2p-peer-address = 192.168.11.135:9880
p2p-peer-address = 192.168.11.135:9881
p2p-peer-address = 192.168.11.135:9882
p2p-peer-address = 192.168.11.135:9883
p2p-peer-address = 192.168.11.135:9884
p2p-peer-address = 192.168.11.135:9885
p2p-peer-address = 192.168.11.135:9886
p2p-peer-address = 192.168.11.135:9887
p2p-peer-address = 192.168.11.135:9888
p2p-peer-address = 192.168.11.135:9889
p2p-peer-address = 192.168.11.103:9890
p2p-peer-address = 192.168.11.103:9891
p2p-peer-address = 192.168.11.103:9892
p2p-peer-address = 192.168.11.103:9893
p2p-peer-address = 192.168.11.103:9894
p2p-peer-address = 192.168.11.103:9895
p2p-peer-address = 192.168.11.103:9896
p2p-peer-address = 192.168.11.103:9897
p2p-peer-address = 192.168.11.135:9877

# if eosio, this flag must be true, else must be set false, it decide whether or not product block
enable-stale-production = true

# load plugin
plugin = eosio::chain_api_plugin
plugin = eosio::history_api_plugin
plugin = eosio::chain_plugin
plugin = eosio::history_plugin
plugin = eosio::net_plugin
plugin = eosio::net_api_plugin
plugin = eosio::txn_test_gen_plugin
  • http-server-address: 开启的节点之中,这个值不能冲突,ip为本机的ip(ip:port)
  • p2p-listen-endpoint: 在节点的端到端连接中表示自己这个节点的位置,也需保持唯一,ip为本机的ip(ip:port)
  • p2p-peer-address: 端到端连接中其他节点的位置
  • enable-stale-production: 出块节点需要设置为true
  • plugin: 插件

在每次启动节点前,确定一下自己需要启动的节点数以及各自节点的网络地址,然后把conf下的配置文件按照上述原则进行修改,每个node.imi对应一个节点的配置文件,且命名规则和原来一样递增,否则可能会造成节点的开启失败。

脚本使用方法:

1
2
# 清除相关数据
$ ./start.sh clean
1
2
3
4
5
6
# 运行脚本启动节点
$ ./start.sh run wallet_dir contracts_dir node_num

# wallet_dir:本地钱包所在目录
# contracts_dir:EOS中系统智能合约所在目录
# node_num:在本机要开启的节点数,视conf中的配置文件数而定

多主机多节点测试网搭建

本节主要介绍如何通过自动化脚本在两台物理机上分别开启多个节点,并测试TPS的过程。

主机A的配置

在主机A:192.168.11.103中开启12个节点,其中节点1–eosio节点作为出块节点。下面给出几个配置文件的内容:

node1.imi:

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
# for communicatin with cleos
http-server-address = 192.168.11.103:8888

# for sync block
p2p-listen-endpoint = 192.168.11.103:9876
p2p-peer-address = 192.168.11.103:9877
p2p-peer-address = 192.168.11.103:9878
p2p-peer-address = 192.168.11.103:9879
p2p-peer-address = 192.168.11.135:9880
p2p-peer-address = 192.168.11.135:9881
p2p-peer-address = 192.168.11.135:9882
p2p-peer-address = 192.168.11.135:9883
p2p-peer-address = 192.168.11.135:9884
p2p-peer-address = 192.168.11.135:9885
p2p-peer-address = 192.168.11.135:9886
p2p-peer-address = 192.168.11.135:9887
p2p-peer-address = 192.168.11.135:9888
p2p-peer-address = 192.168.11.135:9889
p2p-peer-address = 192.168.11.103:9890
p2p-peer-address = 192.168.11.103:9891
p2p-peer-address = 192.168.11.103:9892
p2p-peer-address = 192.168.11.103:9893
p2p-peer-address = 192.168.11.103:9894
p2p-peer-address = 192.168.11.103:9895
p2p-peer-address = 192.168.11.103:9896
p2p-peer-address = 192.168.11.103:9897
p2p-peer-address = 192.168.11.135:9877

# if eosio, this flag must be true, else must be set false, it decide whether or not product block
enable-stale-production = true

# load plugin
plugin = eosio::chain_api_plugin
plugin = eosio::history_api_plugin
plugin = eosio::chain_plugin
plugin = eosio::history_plugin
plugin = eosio::net_plugin
plugin = eosio::net_api_plugin
plugin = eosio::txn_test_gen_plugin

node12.imi:

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
# for communicatin with cleos
http-server-address = 192.168.11.103:8909

# for sync block
p2p-listen-endpoint = 192.168.11.103:9897
p2p-peer-address = 192.168.11.103:9876
p2p-peer-address = 192.168.11.103:9877
p2p-peer-address = 192.168.11.103:9878
p2p-peer-address = 192.168.11.103:9879
p2p-peer-address = 192.168.11.135:9881
p2p-peer-address = 192.168.11.135:9882
p2p-peer-address = 192.168.11.135:9883
p2p-peer-address = 192.168.11.135:9884
p2p-peer-address = 192.168.11.135:9885
p2p-peer-address = 192.168.11.135:9886
p2p-peer-address = 192.168.11.135:9887
p2p-peer-address = 192.168.11.135:9888
p2p-peer-address = 192.168.11.135:9889
p2p-peer-address = 192.168.11.103:9890
p2p-peer-address = 192.168.11.103:9891
p2p-peer-address = 192.168.11.103:9892
p2p-peer-address = 192.168.11.103:9893
p2p-peer-address = 192.168.11.103:9894
p2p-peer-address = 192.168.11.103:9895
p2p-peer-address = 192.168.11.103:9896
p2p-peer-address = 192.168.11.135:9880
p2p-peer-address = 192.168.11.135:9877

# if eosio, this flag must be true, else must be set false, it decide whether or not product block
# enable-stale-production = true

# load plugin
plugin = eosio::chain_api_plugin
plugin = eosio::history_api_plugin
plugin = eosio::chain_plugin
plugin = eosio::history_plugin
plugin = eosio::net_plugin
plugin = eosio::net_api_plugin
plugin = eosio::txn_test_gen_plugin

开启节点:

1
$ ./start.sh run /home/hearing/eosio-wallet ~/Downloads/eosio.contracts 12

主机B的配置

在主机A:192.168.11.135中开启10个节点,所有节点都不出块。下面给出一个配置文件的内容:

node1.imi:

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
# for communicatin with cleos
http-server-address = 192.168.11.135:8892

# for sync block
p2p-listen-endpoint = 192.168.11.135:9880
p2p-peer-address = 192.168.11.103:9876
p2p-peer-address = 192.168.11.103:9877
p2p-peer-address = 192.168.11.103:9878
p2p-peer-address = 192.168.11.103:9879
p2p-peer-address = 192.168.11.135:9881
p2p-peer-address = 192.168.11.135:9882
p2p-peer-address = 192.168.11.135:9883
p2p-peer-address = 192.168.11.135:9884
p2p-peer-address = 192.168.11.135:9885
p2p-peer-address = 192.168.11.135:9886
p2p-peer-address = 192.168.11.135:9887
p2p-peer-address = 192.168.11.135:9888
p2p-peer-address = 192.168.11.135:9889
p2p-peer-address = 192.168.11.103:9890
p2p-peer-address = 192.168.11.103:9891
p2p-peer-address = 192.168.11.103:9892
p2p-peer-address = 192.168.11.103:9893
p2p-peer-address = 192.168.11.103:9894
p2p-peer-address = 192.168.11.103:9895
p2p-peer-address = 192.168.11.103:9896
p2p-peer-address = 192.168.11.103:9897
p2p-peer-address = 192.168.11.135:9877

开启节点:

1
$ ./start.sh run /home/hearing/eosio-wallet ~/Downloads/eosio.contracts 10

测试tps

使用EOS官方推荐的txn_test_gen_plugin插件作为tps的测试工具,其原理如下:

  1. 在一个指定的节点上新建测试账户

    1
    $ curl --data-binary '["eosio", "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"]' http://192.168.11.103:8888/v1/txn_test_gen/create_test_accounts
  2. 在测试账户间进行自动交易(每10秒产生20个交易)

    1
    $ curl --data-binary '["", 10, 20]' http://192.168.11.103:8888/v1/txn_test_gen/start_generation
  3. 上述产生交易的步骤也可在其他节点上运行

  4. 查看节点的输出日志,计算tps

    • 出块节点的日志:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      info  2019-03-13T07:56:24.050 thread-0  producer_plugin.cpp:1584      produce_block        ] Produced block 00000519d3484607... #1305 @ 2019-03-13T07:56:24.000 signed by eosio [trxs: 371, lib: 1304, confirmed: 0]
      info 2019-03-13T07:56:24.539 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 0000051aaa2f783e... #1306 @ 2019-03-13T07:56:24.500 signed by eosio [trxs: 358, lib: 1305, confirmed: 0]
      info 2019-03-13T07:56:25.017 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 0000051beb1d3276... #1307 @ 2019-03-13T07:56:25.000 signed by eosio [trxs: 257, lib: 1306, confirmed: 0]
      info 2019-03-13T07:56:25.549 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 0000051c6c7937c6... #1308 @ 2019-03-13T07:56:25.500 signed by eosio [trxs: 155, lib: 1307, confirmed: 0]
      info 2019-03-13T07:56:26.028 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 0000051d9113d888... #1309 @ 2019-03-13T07:56:26.000 signed by eosio [trxs: 154, lib: 1308, confirmed: 0]
      info 2019-03-13T07:56:26.559 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 0000051ed4d34911... #1310 @ 2019-03-13T07:56:26.500 signed by eosio [trxs: 276, lib: 1309, confirmed: 0]
      info 2019-03-13T07:56:27.023 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 0000051f69ccd987... #1311 @ 2019-03-13T07:56:27.000 signed by eosio [trxs: 163, lib: 1310, confirmed: 0]
      info 2019-03-13T07:56:27.544 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 000005208f4e00f9... #1312 @ 2019-03-13T07:56:27.500 signed by eosio [trxs: 249, lib: 1311, confirmed: 0]
      info 2019-03-13T07:56:28.033 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 000005210110210b... #1313 @ 2019-03-13T07:56:28.000 signed by eosio [trxs: 214, lib: 1312, confirmed: 0]
      info 2019-03-13T07:56:28.526 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 00000522ea3e4544... #1314 @ 2019-03-13T07:56:28.500 signed by eosio [trxs: 314, lib: 1313, confirmed: 0]
      info 2019-03-13T07:56:29.017 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 000005234705e92a... #1315 @ 2019-03-13T07:56:29.000 signed by eosio [trxs: 259, lib: 1314, confirmed: 0]
      info 2019-03-13T07:56:29.527 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 00000524841be821... #1316 @ 2019-03-13T07:56:29.500 signed by eosio [trxs: 502, lib: 1315, confirmed: 0]
      info 2019-03-13T07:56:30.014 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 000005257f6d4e2e... #1317 @ 2019-03-13T07:56:30.000 signed by eosio [trxs: 897, lib: 1316, confirmed: 0]
      info 2019-03-13T07:56:30.521 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 00000526b3aa10f1... #1318 @ 2019-03-13T07:56:30.500 signed by eosio [trxs: 526, lib: 1317, confirmed: 0]
      info 2019-03-13T07:56:31.023 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 0000052756e32d73... #1319 @ 2019-03-13T07:56:31.000 signed by eosio [trxs: 652, lib: 1318, confirmed: 0]
      info 2019-03-13T07:56:31.514 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 000005288100c034... #1320 @ 2019-03-13T07:56:31.500 signed by eosio [trxs: 616, lib: 1319, confirmed: 0]
      info 2019-03-13T07:56:32.048 thread-0 producer_plugin.cpp:1584 produce_block ] Produced block 0000052914f4a78b... #1321 @ 2019-03-13T07:56:32.000 signed by eosio [trxs: 662, lib: 1320, confirmed: 0]
    • 其他节点的日志:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      info  2019-03-13T07:55:53.164 thread-0  producer_plugin.cpp:345       on_incoming_block    ] Received block 9f6a6c4c171e385a... #1243 @ 2019-03-13T07:55:53.000 signed by eosio [trxs: 128, lib: 1242, conf: 0, latency: 164 ms]
      info 2019-03-13T07:55:53.600 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block faff04083aafe1e8... #1244 @ 2019-03-13T07:55:53.500 signed by eosio [trxs: 79, lib: 1243, conf: 0, latency: 100 ms]
      info 2019-03-13T07:55:54.139 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block bf606f4ee1ee1498... #1245 @ 2019-03-13T07:55:54.000 signed by eosio [trxs: 113, lib: 1244, conf: 0, latency: 139 ms]
      info 2019-03-13T07:55:54.806 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block e7ba16b5cda9ae0e... #1246 @ 2019-03-13T07:55:54.500 signed by eosio [trxs: 224, lib: 1245, conf: 0, latency: 306 ms]
      info 2019-03-13T07:55:55.151 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 149c694678a74f0e... #1247 @ 2019-03-13T07:55:55.000 signed by eosio [trxs: 144, lib: 1246, conf: 0, latency: 151 ms]
      info 2019-03-13T07:55:55.634 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 06efdba68e432e11... #1248 @ 2019-03-13T07:55:55.500 signed by eosio [trxs: 112, lib: 1247, conf: 0, latency: 134 ms]
      info 2019-03-13T07:55:56.100 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 1722e7db91fbc95b... #1249 @ 2019-03-13T07:55:56.000 signed by eosio [trxs: 112, lib: 1248, conf: 0, latency: 100 ms]
      info 2019-03-13T07:55:56.615 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block a1ea6b148a9669a8... #1250 @ 2019-03-13T07:55:56.500 signed by eosio [trxs: 112, lib: 1249, conf: 0, latency: 115 ms]
      info 2019-03-13T07:55:57.131 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block ea93f8c7878d058b... #1251 @ 2019-03-13T07:55:57.000 signed by eosio [trxs: 112, lib: 1250, conf: 0, latency: 131 ms]
      info 2019-03-13T07:55:57.744 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block a0f4be3b1b68b79c... #1252 @ 2019-03-13T07:55:57.500 signed by eosio [trxs: 169, lib: 1251, conf: 0, latency: 244 ms]
      info 2019-03-13T07:55:58.173 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 7334cd134b162612... #1253 @ 2019-03-13T07:55:58.000 signed by eosio [trxs: 126, lib: 1252, conf: 0, latency: 173 ms]
      info 2019-03-13T07:55:58.730 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 0c563bf6bad3095a... #1254 @ 2019-03-13T07:55:58.500 signed by eosio [trxs: 169, lib: 1253, conf: 0, latency: 230 ms]
      info 2019-03-13T07:55:59.086 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 6d0901b8f1b15739... #1255 @ 2019-03-13T07:55:59.000 signed by eosio [trxs: 64, lib: 1254, conf: 0, latency: 86 ms]
      info 2019-03-13T07:55:59.778 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 01a641dfcb07dbe5... #1256 @ 2019-03-13T07:55:59.500 signed by eosio [trxs: 251, lib: 1255, conf: 0, latency: 278 ms]
      info 2019-03-13T07:56:00.131 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 308907ac4cf4bc98... #1257 @ 2019-03-13T07:56:00.000 signed by eosio [trxs: 85, lib: 1256, conf: 0, latency: 131 ms]
      info 2019-03-13T07:56:00.688 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 3834f77aa9b994c9... #1258 @ 2019-03-13T07:56:00.500 signed by eosio [trxs: 128, lib: 1257, conf: 0, latency: 188 ms]
      info 2019-03-13T07:56:01.359 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 4ad642cb075d8d2a... #1259 @ 2019-03-13T07:56:01.000 signed by eosio [trxs: 206, lib: 1258, conf: 0, latency: 359 ms]
      info 2019-03-13T07:56:01.630 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 9ed257c131cffcee... #1260 @ 2019-03-13T07:56:01.500 signed by eosio [trxs: 114, lib: 1259, conf: 0, latency: 130 ms]
      info 2019-03-13T07:56:02.114 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block be8627adc345d6a4... #1261 @ 2019-03-13T07:56:02.000 signed by eosio [trxs: 96, lib: 1260, conf: 0, latency: 114 ms]
      info 2019-03-13T07:56:02.732 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 6d609f6c67134258... #1262 @ 2019-03-13T07:56:02.500 signed by eosio [trxs: 211, lib: 1261, conf: 0, latency: 232 ms]
      info 2019-03-13T07:56:03.152 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block d884495e2f09d7ff... #1263 @ 2019-03-13T07:56:03.000 signed by eosio [trxs: 116, lib: 1262, conf: 0, latency: 152 ms]
      info 2019-03-13T07:56:03.741 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 2735616823a12a4b... #1264 @ 2019-03-13T07:56:03.500 signed by eosio [trxs: 133, lib: 1263, conf: 0, latency: 241 ms]
      info 2019-03-13T07:56:04.321 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 55d4565cc4094b30... #1265 @ 2019-03-13T07:56:04.000 signed by eosio [trxs: 192, lib: 1264, conf: 0, latency: 321 ms]
      info 2019-03-13T07:56:04.744 thread-0 producer_plugin.cpp:345 on_incoming_block ] Received block 1e4ba2d9bee5b06f... #1266 @ 2019-03-13T07:56:04.500 signed by eosio [trxs: 228, lib: 1265, conf: 0, latency: 244 ms]

      tps的计算方式为日志中的trxs数值乘以2,因为EOS的出块时间是每500毫秒出一个区块。

启动测试网

用户可以主观地给候选节点投票,当所有代币的15%进行投票后,会由投票数靠前的节点(默认21个)出块,出块的节点票数必须大于0,且超级节点数默认不大于21个。

可以通过上述给出的链接中的脚本进行自动化启动,具体使用如下(我这里一共开启了包括创世节点在内共6个节点,主网启动后,由其中的三个节点轮流出块):

1
2
3
4
5
6
7
8
9
# 初始化
$ ./start.sh clean

# 开启节点并注册候选人
$ ./start.sh run_vote /home/hearing/eosio-wallet ~/Downloads/eosio.contracts/ 6

# 投票
# 数字4表示user2投票给prod2,user3投票给prod3,user4投票给user4
$ ./start.sh vote 4

此时会由prod2,3,4作为BP轮流出块(候选账户名需要跟节点名相同)。

注意:由于txn_test_gen_plugin的限制,当启动测试网后,貌似不能直接通过它去测试TPS(create_test_accounts会报错),故我这里加上了手动创建测试账户的过程(秘钥不能更改):

1
2
3
$ cleos system newaccount --transfer eosio txn.test.a EOS5hBSuDvcU2hHZJLAWCzBCCS7pV6SeQ4FuMhLPQPYqu9hcFakhy --stake-net "100000000.0000 EOS" --stake-cpu "100000000.0000 EOS" --buy-ram "20000.0000 EOS"
$ cleos system newaccount --transfer eosio txn.test.b EOS5gUGqvjsoAmqEJvSBAygi7XF75CaCDfpysZRRVPRBdAzcirTWG --stake-net "100000000.0000 EOS" --stake-cpu "100000000.0000 EOS" --buy-ram "20000.0000 EOS"
$ cleos system newaccount --transfer eosio txn.test.t EOS5gUGqvjsoAmqEJvSBAygi7XF75CaCDfpysZRRVPRBdAzcirTWG --stake-net "100000000.0000 EOS" --stake-cpu "100000000.0000 EOS" --buy-ram "20000.0000 EOS"

限制端口带宽的相关命令:

1
2
3
4
5
6
7
8
$ sudo iptables -t mangle -F
$ sudo tc qdisc del dev lo root
$ sudo tc qdisc add dev lo root handle 1: htb default 1
$ sudo tc class add dev lo parent 1: classid 1:1 htb rate 100mbps
$ sudo tc class add dev lo parent 1:1 classid 1:5 htb rate 256Kbit ceil 512Kbit prio 1
$ sudo tc filter add dev lo parent 1:0 prio 1 protocol ip handle 5 fw flowid 1:5
$ sudo iptables -A OUTPUT -t mangle -p tcp --sport 9877:9879 -j MARK --set-mark 5
$ sudo iptables -A INPUT -t mangle -p tcp --sport 9877:9879 -j MARK --set-mark 5

然后通过txn_test_gen_plugin提供的start_generation接口进行模拟交易,并测试TPS。

1
$ curl --data-binary '["", 20, 20]' http://127.0.0.1:8888/v1/txn_test_gen/start_generation

简介

现有网络中,对流量的控制和转发都依赖于网络设备实现,且设备中集成了与业务特性紧耦合的操作系统和专用硬件,这些操作系统和专用硬件都是各个厂家自己开发和设计的。

SDN是一种新型的网络架构,它的设计理念是将网络的控制平面与数据转发平面进行分离,从而通过集中的控制器中的软件平台去实现可编程化控制底层硬件,实现对网络资源灵活的按需调配。在SDN网络中,网络设备只负责单纯的数据转发,可以采用通用的硬件;而原来负责控制的操作系统将提炼为独立的网络操作系统,负责对不同业务特性进行适配,而且网络操作系统和业务特性以及硬件设备之间的通信都可以通过编程实现。

与传统网络相比,SDN的基本特征有3点:

  1. 控制与转发分离。转发平面由受控转发的设备组成,转发方式以及业务逻辑由运行在分离出去的控制面上的控制应用所控制。
  2. 控制平面与转发平面之间的开放接口。SDN 为控制平面提供开放可编程接口。通过这种方式,控制应用只需要关注自身逻辑,而不需要关注底层更多的实现细节。
  3. 逻辑上的集中控制。逻辑上集中的控制平面可以控制多个转发面设备,也就是控制整个物理网络,因而可以获得全局的网络状态视图,并根据该全局网络状态视图实现对网络的优化控制。

概述

ADB,即 Android Debug Bridge.

命令语法

adb 命令的基本语法如下:

1
adb [-d|-e|-s <serialNumber>] <command>

如果只有一个设备/模拟器连接时,可以省略掉 [-d|-e|-s <serialNumber>] 这一部分,直接使用 adb <command>。

指定目标设备

参数 含义
-d 指定当前唯一通过 USB 连接的 Android 设备为命令目标
-e 指定当前唯一运行的模拟器为命令目标
-s <serialNumber> 指定相应 serialNumber 号的设备/模拟器为命令目标

在多个设备/模拟器连接的情况下较常用的是 -s <serialNumber> 参数,serialNumber 可以通过 adb devices 命令获取。

启动/停止

启动 adb server 命令:

1
adb start-server

一般无需手动执行此命令,在运行 adb 命令时若发现 adb server 没有启动会自动调起。

停止 adb server 命令:

1
adb kill-server

无线连接

  1. 将 Android 设备与将运行 adb 的电脑连接到同一个局域网。
  2. 将设备与电脑通过 USB 线连接。
  3. 让设备在 5555 端口监听 TCP/IP 连接:adb tcpip 5555
  4. 断开 USB 连接。
  5. 找到设备的 IP 地址。
  6. 通过 IP 地址连接设备:adb connect <device-ip-address>
  7. 确认连接状态:adb devices
  8. 断开无线连接:adb disconnect <device-ip-address>

应用管理

查看应用列表

1
adb shell pm list packages [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER]
参数 显示列表
所有应用
-f 显示应用关联的 apk 文件
-d 只显示 disabled 的应用
-e 只显示 enabled 的应用
-s 只显示系统应用
-3 只显示第三方应用
-i 显示应用的 installer
-u 包含已卸载应用
<FILTER> 包名包含 <FILTER> 字符串

安装 APK

1
adb install <apk file>
参数 含义
-r 允许覆盖安装。
-s 将应用安装到 sdcard。
-d 允许降级覆盖安装。

卸载应用

1
adb uninstall [-k] <packagename>

-k 参数可选,表示卸载应用但保留数据和缓存目录。

清除应用数据与缓存

1
adb shell pm clear <packagename>

这条命令的效果相当于在设置里的应用信息界面点击了「清除缓存」和「清除数据」。

与应用交互

主要是使用 am <command> 命令,常用的 command 如下:

command 用途
start [options] <INTENT> 启动 <INTENT> 指定的 Activity
startservice [options] <INTENT> 启动 <INTENT> 指定的 Service
broadcast [options] <INTENT> 发送 <INTENT> 指定的广播
force-stop <packagename> 停止 <packagename> 相关的进程

<INTENT> 参数很灵活,和写 Android 程序时代码里的 Intent 相对应。用于决定 intent 对象的选项如下:

参数 含义
-a <ACTION> 指定 action,比如 android.intent.action.VIEW
-c <CATEGORY> 指定 category,比如 android.intent.category.APP_CONTACTS
-n <COMPONENT> 指定完整 component 名,用于明确指定启动哪个 Activity,如 com.example.app/.ExampleActivity

<INTENT> 里还能带数据,就像写代码时的 Bundle 一样:

参数 含义
–esn <EXTRA_KEY> null 值(只有 key 名)
–es <EXTRA_KEY> <EXTRA_STRING_VALUE> String 值
–ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> boolean 值
–ei <EXTRA_KEY> <EXTRA_INT_VALUE> integer 值
–el <EXTRA_KEY> <EXTRA_LONG_VALUE> long 值
–ef <EXTRA_KEY> <EXTRA_FLOAT_VALUE> float 值
–eu <EXTRA_KEY> <EXTRA_URI_VALUE> URI
–ecn <EXTRA_KEY> <EXTRA_COMPONENT_NAME_VALUE> component name
–eia <EXTRA_KEY> <EXTRA_INT_VALUE>[,<EXTRA_INT_VALUE…] integer 数组
–ela <EXTRA_KEY> <EXTRA_LONG_VALUE>[,<EXTRA_LONG_VALUE…] long 数组

调起 Activity

1
adb shell am start [options] <INTENT>

例如:

1
adb shell am start -n com.tencent.mm/.ui.LauncherUI

表示调起微信主界面。

1
adb shell am start -n org.mazhuang.boottimemeasure/.MainActivity --es "toast" "hello, world"

表示调起 org.mazhuang.boottimemeasure/.MainActivity 并传给它 string 数据键值对 toast - hello, world。

调起 Service

1
adb shell am startservice [options] <INTENT>

例如:

1
adb shell am startservice -n com.tencent.mm/.plugin.accountsync.model.AccountAuthenticatorService

表示调起微信的某 Service。

发送广播

1
adb shell am broadcast [options] <INTENT>

例如:

1
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n org.mazhuang.boottimemeasure/.BootCompletedReceiver

表示向 org.mazhuang.boottimemeasure/.BootCompletedReceiver 发送一个 BOOT_COMPLETED 广播,这类用法在测试的时候很实用,比如某个广播的场景很难制造,可以考虑通过这种方式来发送广播。

强制停止应用

1
adb shell am force-stop <packagename>

命令示例:

1
adb shell am force-stop com.qihoo360.mobilesafe

设备/进程信息

Linux

原生Linux的查看方式见Linux笔记

procrank

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
rocrank
PID Vss Rss Pss Uss cmdline
1078 59840K 59708K 42125K 39344K com.csr.BTApp
2683 59124K 59040K 37960K 33032K com.android.launcher
1042 51572K 51488K 35686K 33604K android.process.acore
782 32808K 32748K 16775K 14716K system_server
667 20560K 17560K 12739K 8940K /system/bin/surfaceflinger
851 30124K 30036K 12085K 7996K com.android.systemui
2999 27680K 27596K 9929K 7040K com.baidu.input
959 20764K 20676K 5522K 3788K com.android.phone
3468 21892K 21800K 4591K 1920K com.apical.dreamthemetime
982 19880K 19792K 4438K 2644K com.csr.csrservices
668 19592K 19480K 3525K 1360K zygote
670 2960K 2960K 2407K 2356K /system/bin/mediaserver
663 1784K 1784K 1209K 1116K /system/bin/synergy_service
756 3404K 1348K 1133K 1124K /usr/bin/gpsexe
669 1468K 1468K 959K 928K /system/bin/drmserver
675 692K 692K 692K 692K /bin/sh
3482 656K 652K 456K 444K procrank
1 164K 164K 144K 144K /init
------ ------ ------
195031K 163724K TOTAL

RAM: 480380K total, 3624K free, 732K buffers, 299788K cached, 264844K shmem, 7632K slab

dumpsys

1
2
3
4
5
6
7
dumpsys [options]
meminfo 显示内存信息
cpuinfo 显示CPU信息
account 显示accounts信息
activity 显示所有的activities的信息
window 显示键盘,窗口和它们的关系
wifi 显示wifi信息

可以在其后通过包名或者进程pid展示指定进程的信息。

型号

1
adb shell getprop ro.product.model

电池状况

1
adb shell dumpsys battery

屏幕分辨率

1
adb shell wm size

屏幕密度

1
adb shell wm density

android_id

1
adb shell settings get secure android_id

IMEI

在 Android 4.4 及以下版本可通过如下命令获取 IMEI:

1
adb shell dumpsys iphonesubinfo

而在 Android 5.0 及以上版本里这个命令输出为空,得通过其它方式获取了(需要 root 权限):

1
service call iphonesubinfo 1

Android 系统版本

1
adb shell getprop ro.build.version.release

Mac 地址

1
adb shell cat /sys/class/net/wlan0/address

CPU 信息

1
adb shell cat /proc/cpuinfo

更多硬件与系统属性

设备的更多硬件与系统属性可以通过如下命令查看:

1
adb shell cat /system/build.prop

这会输出很多信息,包括前面几个小节提到的「型号」和「Android 系统版本」等。输出里还包括一些其它有用的信息,它们也可通过 adb shell getprop <属性名> 命令单独查看,列举一部分属性如下:

模拟按键/输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Usage: input [<source>] <command> [<arg>...]

The sources are:
mouse
keyboard
joystick
touchnavigation
touchpad
trackball
stylus
dpad
gesture
touchscreen
gamepad

The commands and default sources are:
text <string> (Default: touchscreen)
keyevent [--longpress] <key code number or name> ... (Default: keyboard)
tap <x> <y> (Default: touchscreen)
swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
press (Default: trackball)
roll <dx> <dy> (Default: trackball)

比如使用 adb shell input keyevent <keycode> 命令,部分keycode如下:

比如可以使用 adb shell input text hello 命令来输入文本。

屏幕截图

-p 指定保存为png.

1
adb shell screencap -p /sdcard/sc.png

录制屏幕

1
adb shell screenrecord /sdcard/filename.mp4

由包名获取apk路径

1
adb shell pm path pkg

获取当前APP包名

1
adb shell dumpsys window | findstr mCurrentFocus

TCP/IP的问题

互联网上的数据传递都是封装在“包裹”里的,将信息传递到终点的程序,以及这些“包裹”的格式,通常用 TCP/IP 协议来描述。为了让 TCP 数据传输成功,接收数据的人需要按照当时发出时的顺序,准确的来接收这些“数字包裹”。如果其中有一个数据包,因为某种原因给丢失了,那么这种互联网协议就会将其看作是网络拥堵的一个信号,数据传输速度立刻下降一半,之后它速度回升起来的也非常缓慢。该处理机制在某些状况下也许很理想,但是在另外一些状况下就会很糟糕。其根本的原因就在于:这套互联网协议本身并没有足够的智能,来分别接下来做什么事才是最正确的选择。同时,尽管从理论上来说,数字包可以从 A 点到 B 点以无限条路径进行传说,但事实上,在一个 TCP 连接中,数据传输一般都走的是相同的路径,这就给了数字黑客以机会,方便他们侵入到你的通信交流中。

网络编码

网络编码是一种通过中继节点对接收到的信息进行编码来达到提高多播网络容量的技术。在传统的数据传输技术中,中继节点只负责数据的存储转发,而基于网络编码技术的网络的中继节点在具备传统中继功能的基础上,会根据网络编码规则将接收到的信息进行线性或非线性处理再进行传播,这种做法最直观的优势是减少了传输次数。

“网络编码”能够让网络中的每一个节点都变得比现在更加智能。在 TCP/IP 协议中,网络节点只是一些简单的转换节点,只负责存储“数字包裹”,并且按照之前预设的路径转发到下一节点,而相比之下,在“网络编码”中,每一个节点都可以对“数字包裹”进行再加工,比如重新编制路径,或者重新编码。将智能赋予到网络的每个节点,是该技术称得上“破坏性创新”的理由。因为这将赋予信息处理技术以史无前例的灵活性。例如,它可以利用多路径 TCP,另外,应用了再编码机制,可以进一步的提升安全性和数据传输速度,甚至能够在网络的每个节点内部存储数据信息。

在”网络编码”中,“数字包裹”中的内容被看作是一个真实的数字,“数字包裹”以“批”为单位进行处理。每一个节点都构建了一套线性方程,利用的是从“数字包裹”中提取出来的数字,以及随机生成的一组系数。每一个线性方程都能生成一个已编码的包裹,其系数存储在编码包裹的头部,未知的变量是每一个包裹的实际信息,当作一个数字。换句话说,每一个已编码的包裹中,都一次性的在几个“标准”的包裹上含有部分的信息,但同时还乘以不同的系数。如果你还没有忘记高中数学的话,你知道需要 N 个线性方程来解决 N 个未知变量。因为每一个以编码的数字包裹都包含一个单独的方程,这意味着接收信息者如果想要解码这段信息,就需要 N 个这样的数字包裹(当然乘以不同的系数才可以)。

这样做使得接收内容彻底与数字包裹接收的顺序撇开了关系,接收信息者得到了 N 个已编码的包裹,每个都配有不同的系数,所以它能够解开所有的方程,还原最原始的数据。这种打破固有顺序所带来的灵活性,意味着整个信息系统将更加高效。也意味着曾经在 TCP/IP 中发生的严重的数据传递延迟甚至数据包丢失的情况一去不复返。因为顺序不再重要,数字包裹可以在网络中以各种不同的路径进行传递,这样会提升安全性。也就没有人能够切入到私人的通信网络中。

线性网络编码

线性网络编码

假设网络是有向的,执行线性网络编码时每个节点收到所有连入线路的数据后,再执行编码,然后把数据从连出线路发出。新的数据包括执行线性编码所用的系数以及合成后的数据。

随机线性网络编码

随机线性网络编码可以取得更好的组播传输速率,较为实用。在实际网络中,节点会将来自连入线路的封包缓存起来,当节点需要发送封包时再将缓存的封包执行网络编码,然后发出。

单例模式

饿汉模式

线程安全的饿汉模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingleTon {
//1、构造方法私有化
private SingleTon() {
}

//2、创建类的唯一实例
private static SingleTon instance = new SingleTon();

//提供一个获取唯一实例的方法
public static SingleTon getInstance() {
return instance;
}
}

饿汉模式在类加载(包括初始化)后便会实例化 SingleTon 对象,但是在类加载(不初始化),SingleTon 不会被实例化,这时相当于懒汉模式。关于类加载的过程,可参考: JVM类加载流程

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
public class Test {
public static void main(String[] args) throws Exception {
Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});
m.setAccessible(true);
ClassLoader cl = ClassLoader.getSystemClassLoader();
Object test1 = m.invoke(cl, "SingleTon");
System.out.println(test1 != null);
System.out.println(SingleTon.class.getName());
Object test2 = m.invoke(cl, "SingleTon");
System.out.println(test2 != null);
SingleTon.getInstance();
Object test3 = m.invoke(cl, "SingleTon");
System.out.println(test3 != null);
}
}

class SingleTon {
private SingleTon() {
System.out.println("constructor");
}

private static SingleTon instance = new SingleTon();

public static SingleTon getInstance() {
return instance;
}
}

// 输出
false
SingleTon
true
constructor
true

可以看到 SingleTon 类已经被加载了,但是并没有执行初始化过程,也就没有实例化 SingleTon 对象。直到调用 getInstance 方法时(也可以是调用SingleTon中的静态变量)才会实例化。

懒汉模式

线程不安全的懒汉模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingleTon {
//1、构造方法私有化
private SingleTon() {
}

//2、创建类的唯一实例
private static SingleTon instance;

//提供一个获取唯一实例的方法
public static SingleTon getInstance() {
if (instance == null) {
instance = new SingleTon();
}
return instance;
}
}

线程安全的懒汉模式,双重检查锁定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SingleTon {
//1、构造方法私有化
private SingleTon() {
}

//2、创建类的唯一实例
private static volatile SingleTon instance;

//提供一个获取唯一实例的方法
public static SingleTon getInstance() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}

这里之所以要加 volatile 标识,是因为 synchronized 无法禁止指令重排序,而 instance 的赋值过程可以拆分为三个步骤:

  1. 申请一个内存区域(空白内存);
  2. 调用构造方法等进行初始化(写内存);
  3. 将对象引用赋值给变量;

而 2 和 3 可能发生重排序,当线程 T1 拿到锁开始创建实例,如果发生了重排序,此时 instance 已经被赋值,但是对象没有初始化,如果此时 instance 被同步到了主内存,当线程 T2 调用 getInstance 方法时,发现 instance 不为空,则直接返回使用,进而出错。

线程安全的懒汉模式,静态内部类:

1
2
3
4
5
6
7
8
9
10
11
12
public class SingleTon {
private SingleTon() {
}

private static class LazyHolder {
private static SingleTon instance = new SingleTon();
}

public static SingleTon getInstance() {
return LazyHolder.instance;
}
}

枚举单例

enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。枚举会被编译成如下形式:

1
2
3
public final class T extends Enum{

}

其中,Enum是Java提供给编译器的一个用于继承的类。枚举量的实现其实是public static final T 类型的未初始化变量,之后,会在静态代码中对枚举量进行初始化。所以,如果用枚举去实现一个单例,这样的加载时间其实有点类似于饿汉模式,并没有起到lazy-loading的作用。

对于序列化和反序列化,因为每一个枚举类型和枚举变量在JVM中都是唯一的,即Java在序列化和反序列化枚举时做了特殊的规定,枚举的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject会破坏单例的问题。

对于线程安全方面,类似于普通的饿汉模式,通过在第一次调用时的静态初始化创建的对象是线程安全的。

因此,选择枚举作为Singleton的实现方式,相对于其他方式尤其是类似的饿汉模式主要有以下优点:

  1. 代码简单
  2. 自由序列化

单例的枚举实现在《Effective Java》中有提到,因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被作者认为是实现Singleton的最佳方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingleTon {
private SingleTon() { }

public static SingleTon getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}

private static enum SingletonEnum{
INSTANCE;

private SingleTon singleton;

//JVM会保证此方法绝对只调用一次
private SingletonEnum(){
singleton = new SingleTon();
}

public SingleTon getInstance(){
return singleton;
}
}
}

更简单:

1
2
3
public enum EasySingleton {
INSTANCE;
}

单例模板

不像C++,C++可以通过直接new T()实现泛型单例,但Java不可以.

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
public class SingleTonBase {
private static final ConcurrentMap<Class, Object> map = new ConcurrentHashMap<>();

public static <T> T getInstance(Class<T> type) {
Object ob = map.get(type);
try {
if (ob == null) {
synchronized (map) {
if (map.get(type) == null) {
ob = type.newInstance();
map.put(type, ob);
}
}
}
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return (T) ob;
}

public static <T> void remove(Class<T> type) {
map.remove(type);

}
}

反射攻击

反射可以获取到单例的私有构造函数,从而破坏单例。如果要抵御这种攻击,就要修改构造器,让他在被要求创建第二个实例的时候抛出异常。可以通过一个boolean类型的静态变量来标识。但是依然可以通过反射的方式来修改标识的值,从而破坏单例。

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
public class Main {
public static void main(String[] args) {
System.out.println("the program is ready to start...");

try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

SingTo singTo = SingTo.getInstance();

Class<SingTo> cls = SingTo.class;
try {
Field flag = cls.getDeclaredField("flag");
flag.setAccessible(true);
flag.set(null, false);
Constructor<SingTo> constructor = cls.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingTo singTo1 = constructor.newInstance();

System.out.println(singTo == singTo1);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException
| NoSuchFieldException e) {
e.printStackTrace();
}
}
}

枚举单例可以避免反射攻击,如下调用newInstance会报错:

1
2
3
4
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
enumSingleton.test();
Class<EnumSingleton> cls = EnumSingleton.class;
System.out.println(enumSingleton == cls.newInstance());

因为类的cls.newInstance()方法最后调用了Constructor的newInstance方法:

1
2
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");

序列化与单例

为了防止反射破坏单例,在私有构造方法里面加入了一个同步变量的判断,确保构造方法只调用一次。但是仍然无法阻止序列化破坏单例,而枚举类可以防止。

Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。 也就是说,以下面枚举为例,序列化的时候只将 DATASOURCE 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

在readObj底层实现上,是通过反射调用私有构造方法来返回实例的,但是在这段代码后面,还会检测该类是否有一个readResolve()方法,如果有,就返回这个方法返回值。这个方法必然是为了解决序列化破坏单例的,我们可以添加这个方法,让其返回单例。

对于枚举单例之外的其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。

所以在单例类中添加如下方法:

1
2
3
public Object readResolve(){
return instance();
}

工厂模式

概述

  1. 抽象工厂角色:这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在java中它由抽象类或者接口来实现。
  2. 具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。
  3. 抽象产品角色:它是具体产品继承的父类或者是实现的接口。在java中一般有抽象类或者接口来实现。
  4. 具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在java中由具体的类来实现。

可以使用反射方法替代多个具体工厂角色,避免新建过多的具体工厂类。

实例

抽象工厂接口:

1
2
3
public interface HairFactory {
Hair create();
}

具体工厂类:

1
2
3
4
5
6
7
8
9
10
11
12
public class LeftHairFactory implements HairFactory {

public Hair create() {
return new LeftHair();
}
}

public class RightHairFactory implements HairFactory {
public Hair create() {
return new RightHair();
}
}

抽象产品接口:

1
2
3
public abstract class Hair {
public Hair(){}
}

具体产品类:

1
2
3
4
5
6
7
8
9
10
11
public class LeftHair extends Hair {
public LeftHair() {
System.out.println("LeftHair...");
}
}

public class RightHair extends Hair {
public RightHair() {
System.out.println("RightHair...");
}
}

使用反射的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HairFactoryByReflect {

public Hair getHairByClass(Class c){
try {
return (Hair) Class.forName(c.getName()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}

观察者模式

概述

观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

观察者模式所涉及的角色有:

  • 抽象主题(Subject)角色:抽象主题角色把所有对观察者对象的引用保存在一个集合里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,抽象主题角色又叫做抽象被观察者(Observable)角色。
  • 具体主题(ConcreteSubject)角色:将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者(Concrete Observable)角色。
  • 抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。
  • 具体观察者(ConcreteObserver)角色:存储与主题的状态自恰的状态。具体观察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态 像协调。如果需要,具体观察者角色可以保持一个指向具体主题对象的引用。

实例

抽象主题角色类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Subject {
// 用来保存注册的观察者对象
private List<Observer> list = new ArrayList<Observer>();

// 注册观察者对象
public void attach(Observer observer){
list.add(observer);
System.out.println("Attached an observer");
}

// 删除观察者对象
public void detach(Observer observer){
list.remove(observer);
}

// 通知所有注册的观察者对象
public void nodifyObservers(String newState){
for(Observer observer : list){
observer.update(newState);
}
}
}

具体主题角色类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcreteSubject extends Subject{

private String state;

public String getState() {
return state;
}

public void change(String newState){
state = newState;
System.out.println("主题状态为:" + state);
// 状态发生改变,通知各个观察者
this.nodifyObservers(state);
}
}

抽象观察者角色类:

1
2
3
public interface Observer {
public void update(String state);
}

具体观察者角色类:

1
2
3
4
5
6
7
8
9
10
11
public class ConcreteObserver implements Observer {
// 观察者的状态
private String observerState;

@Override
public void update(String state) {
// 更新观察者的状态,使其与目标的状态保持一致
observerState = state;
System.out.println("状态为:" + observerState);
}
}

客户端类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {

public static void main(String[] args) {
// 创建主题对象
ConcreteSubject subject = new ConcreteSubject();
// 创建观察者对象
Observer observer = new ConcreteObserver();
// 将观察者对象登记到主题对象上
subject.attach(observer);
// 改变主题对象的状态
subject.change("new state");
}
}

代理/委托模式

概述

当不能直接访问一个对象时,可以使用代理来间接访问,比如对象在另外一台机器上,或者对象被持久化了,对象是受保护的。代理模式侧重于控制对对象的访问,代理类可以对它的客户隐藏一个对象的具体信息。

静态代理模式的代理类只是实现了特定类的代理,如果代理类对象的方法越多,你就得写越多的重复的代码,使用动态代理可以比较好地解决这个问题,动态代理可以动态的生成代理类,实现对不同类下的不同方法的代理。

静态代理和动态代理的区别:

  1. 静态代理在代理前就知道要代理的是哪个对象,而动态代理是运行时才知道;
  2. 静态代理一般只能代理一个类,而动态代理能代理实现了接口的多个类;

静态代理

概述

当使用静态代理模式的时候,我们常常在一个代理类中创建一个对象的实例。

参与者:

  • 被代理对象基类(Subject):定义一个接口。
  • 具体被代理对象类(ConcreteSubject):实现接口的方法。
  • 代理类(Proxy):内部有被代理对象的成员变量,且直接在内部实例化它,代理类也实现了被代理对象基类的方法。

实例

被代理对象基类:

1
2
3
public interface Subject {
void operate();
}

具体被代理对象类:

1
2
3
4
5
6
7
public class ConcreteSubject implements Subject {

@Override
public void operate() {
System.out.println("do something...");
}
}

代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Proxy implements Subject {

private Subject subject = new ConcreteSubject();

@Override
public void operate() {
// 调用目标之前可以做相关操作
System.out.println("before....");
subject.operate();
// 调用目标之后可以做相关操作
System.out.println("after....");
}
}

客户端类:

1
2
3
4
5
6
7
public class Client {

public static void main(String[] args) {
Subject subject = new Proxy();
subject.operate();
}
}

Jdk动态代理

概述

jdk动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用业务方法前调用InvocationHandler处理。代理类必须实现InvocationHandler接口,并且,JDK动态代理只能代理实现了接口的类,没有实现接口的类是不能实现JDK动态代理。

使用JDK动态代理类基本步骤:

  1. 编写需要被代理的类和接口;
  2. 编写代理类,需要实现InvocationHandler接口,重写invoke方法;
  3. 使用Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)动态创建代理类对象,通过代理类对象调用业务方法。

实例

被代理对象基类:

1
2
3
public interface Subject {
void operate();
}

具体被代理对象类:

1
2
3
4
5
6
7
public class ConcreteSubject implements Subject {

@Override
public void operate() {
System.out.println("do something...");
}
}

代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Proxy implements InvocationHandler {

private Object target;

public Proxy(Object target) {
this.target = target;
}

@Override
public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
System.out.println("before....");
// 使用方法的反射
Object invoke = method.invoke(target, args);
System.out.println("after....");
return invoke;
}
}

客户端类:

1
2
3
4
5
6
7
8
9
10
public class Client {

public static void main(String[] args) {
Subject subject = new ConcreteSubject();
Class<?> clazz = subject.getClass();
Proxy proxy = new Proxy(subject);
Subject subjectProxy = (Subject) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), proxy);
subjectProxy.operate();
}
}

Cglib动态代理

概述

Cglib是针对类来实现代理的,类不用实现接口,Cglib会对目标类产生一个代理子类,通过方法拦截技术对过滤父类的方法调用,因此不能代理声明为final类型的类和方法。代理子类需要实现MethodInterceptor接口。

Cglib实现的MethodInterceptor接口在spring-core包。

实例

被代理对象类:

1
2
3
4
5
6
public class Subject {

public void operate() {
System.out.println("do something...");
}
}

代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Proxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();

public Object getProxyObj(Class clazz) {
//设置父类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
enhancer.setUseCache(false);
return enhancer.create();
}

@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("before....");
// 代理类对象实例调用父类方法
methodProxy.invokeSuper(o, args);
System.out.println("after....");
return null;
}
}

客户端类:

1
2
3
4
5
6
7
8
public class Client {

public static void main(String[] args) {
Proxy proxy = new Proxy();
Subject subjectProxy = (Subject) proxy.getProxyObj(Subject.class);
subjectProxy.operate();
}
}

装饰模式

概述

装饰模式(Decorator),动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。装饰器模式关注于在一个对象上动态的添加方法,因此,当使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。

参与者:

  • 被装饰对象基类(Subject):定义一个接口。
  • 具体被装饰对象类(ConcreteSubject):实现接口的方法。
  • 装饰者类(Decorator):内部有被装饰对象的成员变量,其实例由外部传入,装饰者类也实现了被装饰对象基类的方法。

实例

被装饰对象基类:

1
2
3
public interface Subject {
void operate();
}

具体被装饰对象类:

1
2
3
4
5
6
7
public class ConcreteSubject implements Subject {

@Override
public void operate() {
System.out.println("do something...");
}
}

装饰者类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Decorator implements Subject {

private Subject subject;

public Decorator(Subject subject) {
this.subject = subject;
}

@Override
public void operate() {
// 调用目标之前可以做相关操作
System.out.println("before....");
subject.operate();
// 调用目标之后可以做相关操作
System.out.println("after....");
}
}

客户端类:

1
2
3
4
5
6
7
8
public class Client {

public static void main(String[] args) {
Subject subject = new ConcreteSubject();
Subject decorator = new Decorator(subject);
decorator.operate();
}
}

外观/门面(Facade)模式

概述

隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口。这种类型的设计模式属于结构性模式。为子系统中的一组接口提供了一个统一的访问接口,这个接口使得子系统更容易被访问或者使用。

  1. 门面(Facede)角色:外观模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求预定了几种功能的组合。
  2. 子系统角色:实现了子系统的功能。它对客户角色和Facade是未知的。它内部可以有系统内的相互交互,也可以由供外界调用的接口。
  3. 客户角色:通过调用Facede来完成要实现的功能。

实例

每个Computer都有CPU、Memory、Disk。在Computer开启和关闭的时候,相应的部件也会开启和关闭,所以,使用了该外观模式后,会使用户和部件之间解耦。

子系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CPU {
public void start() {
System.out.println("CPU start...");
}

public void shutdown() {
System.out.println("CPU shutdown...");
}
}

public class Memory {
public void start() {
System.out.println("Memory start...");
}

public void shutdown() {
System.out.println("Memory shutdown...");
}
}

门面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Computer {
private CPU cpu;
private Memory memory;

public Computer() {
cpu = new CPU();
memory = new Memory();
}

public void start() {
System.out.println("Computer start begin...");
cpu.start();
memory.start();
System.out.println("Computer start end...");
}

public void shutdown() {
System.out.println("Computer shutdown begin...");
cpu.shutdown();
memory.shutdown();
System.out.println("Computer shutdown end...");
}
}

客户:

1
2
3
4
5
6
public static void main(String[] args) {
Computer computer = new Computer();
computer.start();
System.out.println("=========");
computer.shutdown();
}

命令模式

概述

命令模式是对命令的封装。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。

每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。命令允许请求的一方和接收请求的一方能够独立演化,从而具有以下的优点:

  • 命令模式使新的命令很容易地被加入到系统里。
  • 允许接收请求的一方决定是否要否决请求。
  • 能较容易地设计一个命令队列。
  • 可以容易地实现对请求的撤销和恢复。
  • 在需要的情况下,可以较容易地将命令记入日志。

命令模式涉及到五个角色,它们分别是:

  • 客户端(Client)角色:创建一个具体命令(ConcreteCommand)对象并确定其接收者。
  • 命令(Command)角色:声明了一个给所有具体命令类的抽象接口。
  • 具体命令(ConcreteCommand)角色:定义一个接收者和行为之间的弱耦合;实现execute()方法,负责调用接收者的相应操作。execute()方法通常叫做执行方法。
  • 请求者(Invoker)角色:负责调用命令对象执行请求,相关的方法叫做行动方法。
  • 接收者(Receiver)角色:负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法叫做行动方法。

实例

接收者角色类:

1
2
3
4
5
6
7
8
public class Receiver {
/**
* 真正执行命令相应的操作
*/
public void action(){
System.out.println("执行操作");
}
}

抽象命令角色类:

1
2
3
4
5
6
public interface Command {
/**
* 执行方法
*/
void execute();
}

具体命令角色类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConcreteCommand implements Command {
// 持有相应的接收者对象
private Receiver receiver = null;

public ConcreteCommand(Receiver receiver){
this.receiver = receiver;
}

@Override
public void execute() {
// 通常会转调接收者对象的相应方法,让接收者来真正执行功能
receiver.action();
}
}

请求者角色类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Invoker {
// 持有命令对象
private Command command = null;

public Invoker(Command command){
this.command = command;
}

public void action(){
command.execute();
}
}

客户端角色类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {

public static void main(String[] args) {
// 创建接收者
Receiver receiver = new Receiver();
// 创建命令对象,设定它的接收者
Command command = new ConcreteCommand(receiver);
// 创建请求者,把命令对象设置进去
Invoker invoker = new Invoker(command);
// 执行方法
invoker.action();
}
}

责任链模式

概述

责任链模式是一种对象的行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任,如Java语言的try catch。

责任链模式涉及到的角色如下所示:

  • 抽象处理者(Handler)角色:定义出一个处理请求的接口。如果需要,接口可以定义出一个方法以设定和返回对下家的引用。这个角色通常由一个Java抽象类或者Java接口实现。
  • 具体处理者(ConcreteHandler)角色:具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家。

实例

抽象处理者角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Handler {

// 持有后继的责任对象
protected Handler nextHandler;

public abstract void handleRequest();

public Handler getNextHandler() {
return nextHandler;
}

public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
}

具体处理者角色:

1
2
3
4
5
6
7
8
9
10
11
12
public class ConcreteHandler extends Handler {

@Override
public void handleRequest() {
if(getNextHandler() != null) {
System.out.println("放过请求");
getNextHandler().handleRequest();
} else {
System.out.println("处理请求");
}
}
}

客户端类:

1
2
3
4
5
6
7
8
9
10
11
public class Client {

public static void main(String[] args) {
// 组装责任链
Handler handler1 = new ConcreteHandler();
Handler handler2 = new ConcreteHandler();
handler1.setSuccessor(handler2);
// 提交请求
handler1.handleRequest();
}
}

享元(Flyweight)模式

概述

享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象d结构模式。

Java中字符串常量,Android中的Message,线程池等都运用了享元模式,单纯享元模式所涉及到的角色如下:

  • 抽象享元(Flyweight)角色 :给出一个抽象接口,以规定出所有具体享元角色需要实现的方法。
  • 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定出的接口。
  • 享元工厂(FlyweightFactory)角色 :本角色负责创建和管理享元角色。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。

实例

抽象享元角色类:

1
2
3
public interface Flyweight {
public void operation(String state);
}

具体享元角色类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConcreteFlyweight implements Flyweight {
private Character intrinsicState = null;

public ConcreteFlyweight(Character state){
this.intrinsicState = state;
}

@Override
public void operation(String state) {
System.out.println("Intrinsic State = " + this.intrinsicState);
System.out.println("Extrinsic State = " + state);
}
}

享元工厂角色类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FlyweightFactory {
private Map<Character, Flyweight> files = new HashMap<Character, Flyweight>();

public Flyweight factory(Character state){
// 先从缓存中查找对象
Flyweight fly = files.get(state);
if (fly == null) {
// 如果对象不存在则创建一个新的Flyweight对象
fly = new ConcreteFlyweight(state);
// 把这个新的Flyweight对象添加到缓存中
files.put(state, fly);
}
return fly;
}
}

客户端类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {

public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight fly = factory.factory(new Character('a'));
fly.operation("First Call");

fly = factory.factory(new Character('b'));
fly.operation("Second Call");

fly = factory.factory(new Character('a'));
fly.operation("Third Call");
}
}

模板模式

概述

准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法的角色:

  • 抽象类(AbstractClass):实现了模板方法,定义了算法的骨架。
  • 具体类(ConcreteClass):实现抽象类中的抽象方法,已完成完整的算法。

实例

抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class AbstractImageLoader {

// 抽象类定义整个流程骨架
public final void downloadImage() {
// 先获取最终的数据源URL
String finalImageUrl = getUrl();
// 然后开始执行下载
// ...
}

//以下是不同子类根据自身特性完成的具体步骤
protected abstract String getUrl();
}

具体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WebpImageLoader extends AbstractImageLoader {
@Override
protected String getUrl() {
return "...";
}
}

public class JpgImageLoader extends AbstractImageLoader {
@Override
protected String getUrl() {
return ".....";
}
}

客户端类:

1
2
3
4
5
6
7
8
9
public class Client {

public static void main(String[] args) {
AbstractImageLoader loader = new WebpImageLoader();
loader.downloadImage();
loader = new JpgImageLoader();
loader.downloadImage();
}
}

策略模式

概述

策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换,策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模式是对算法的包装,是把使用算法的责任和算法本身分割开来,委派给不同的对象管理。策略模式通常把一个系列的算法包装到一系列的策略类里面,作为一个抽象策略类的子类。这个模式涉及到三个角色:

  • 环境(Context)角色:持有一个Strategy的引用。
  • 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  • 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

实例

环境角色类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Context {
// 持有一个具体策略的对象
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public void contextInterface() {
strategy.strategyInterface();
}
}

抽象策略角色类:

1
2
3
public interface Strategy {
public void strategyInterface();
}

具体策略角色类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConcreteStrategyA implements Strategy {

@Override
public void strategyInterface() {
// 相关的业务
}
}

public class ConcreteStrategyB implements Strategy {

@Override
public void strategyInterface() {
// 相关的业务
}
}

public class ConcreteStrategyC implements Strategy {

@Override
public void strategyInterface() {
// 相关的业务
}
}

客户端类:

1
2
3
4
5
6
7
8
9
public class Client {

public static void main(String[] args) {
Strategy strategy = new ConcreteStrategyB();
Context context = new Context(strategy);
// 通过不同的Strategy得到不同的结果
context.contextInterface();
}
}

镜像

获取镜像

Docker 运行容器前需要本地存在对应的镜像,如果镜像不存在本地,Docker 会从镜像仓库下载(默认是 Docker Hub 公共注册服务器中的仓库)。

下载镜像:

1
2
3
docker pull image:tag
# 相当于
docker pull registry.hub.docker.com/image:tag

列出镜像:

1
docker images

创建镜像

修改已有镜像

  1. 使用本地镜像启动容器: docker run -i -t ubuntu:12.04 /bin/bash
  2. 修改
  3. 提交: docker commit -m=”msg” -a=”author” container_id rep/name:tag

提交参数:

  • -m:提交的描述信息
  • -a:指定镜像作者
  • e218edb10161:容器ID
  • runoob/ubuntu:v2:指定要创建的目标镜像名

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM    centos:6.7
MAINTAINER Fisher "fisher@sudops.com"

RUN /bin/echo 'root:123456' |chpasswd
RUN useradd runoob
RUN /bin/echo 'runoob:123456' |chpasswd
RUN /bin/echo -e "LANG=\"en_US.UTF-8\"" >/etc/default/local
EXPOSE 22
EXPOSE 80
ADD myApp /var/www
EXPOSE 80
CMD /usr/sbin/sshd -D
  • 使用#注释
  • 每一个指令都会在镜像上创建一个新的层,每一个指令的前缀必须大写
  • 一个镜像不能超过 127 层
  • 第一条FROM指令指定使用哪个镜像源
  • MAINTAINER 表示维护者的信息
  • RUN 指令表示在镜像内执行的命令
  • ADD 命令复制本地文件到镜像
  • EXPOSE 命令向外部开放端口
  • CMD 命令描述容器启动后运行的程序
  • 最后通过docker build命令来构建一个镜像
1
2
3
# -t 添加 tag
# dir 是 Dockerfile 所在的路径
docker build -t runoob/centos:6.7 dir

本地文件系统导入

1
cat ubuntu-14.04-x86_64-minimal.tar.gz  |docker import - ubuntu:14.04

上传镜像

  • 登录: docker login -u user -p passwd -e email hub.c.163.com(网易蜂巢)
  • 标签: docker tag image user_name/repo_name[:tag]
  • 上传: docker push hub.c.163.com/user_name/repo_name[:tag]

导出与导入

1
2
3
4
5
6
# 导出镜像到本地文件
docker save -o file_name image:tag
# 载入镜像
docker load --input file_name
#
docker load < file_name

移除

1
docker rmi name/id

容器

基本操作

1
2
3
4
5
6
7
8
# 运行容器
docker run image:tag cmd
# 交互式运行容器
docker run -i -t image:tag cmd
# 停止容器
docker stop id/name
# 重启容器
docket stop id/name
  • -t: 在新容器内指定一个伪终端或终端
  • -i: 允许对容器内的标准输入 (STDIN) 进行交互
  • -d: 后台启动一个容器
  • -p local_port:container_port: 端口映射

进入容器

attach

1
docker attach name/id

nsenter

删除

1
docker rm name/id

数据管理

数据卷

数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:

  • 数据卷可以在容器之间共享和重用
  • 对数据卷的修改会立马生效
  • 对数据卷的更新,不会影响镜像
  • 卷会一直存在,直到没有容器使用
  • 数据卷的使用,类似于 Linux 下对目录或文件进行 mount。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 加载一个数据卷到容器的 /webapp 目录
# 也可以在 Dockerfile 中使用 VOLUME 来添加一个或者多个新的卷到由该镜像创建的任意容器
docker run -d -P --name web -v /webapp training/webapp python app.py

# 指定挂载一个本地主机目录到容器中
# 加载主机的 /src/webapp 目录到容器的 /opt/webapp 目录, 如果目录不存在会自动创建
# Dockerfile 中不支持这种用法,因为不同操作系统的路径格式不一样
docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py

# 挂载数据卷的默认权限是读写,可以通过 :ro 指定为只读
docker run -d -P --name web -v /src/webapp:/opt/webapp:ro training/webapp python app.py

# 挂载一个本地主机文件作为数据卷
docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

数据卷容器

如果有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器。数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。

  1. 创建一个命名的数据卷容器dbdata:
1
docker run -d -v /dbdata --name dbdata training/postgres echo Data-only container for postgres
  1. 在其他容器中使用 –volumes-from 来挂载 dbdata 容器中的数据卷
1
2
3
4
5
docker run -d --volumes-from dbdata --name db1 training/postgres
docker run -d --volumes-from dbdata --name db2 training/postgres
# 可以使用多个 --volumes-from 参数来从多个容器挂载多个数据卷,也可以从其他已经挂载了数据卷的容器来挂载数据卷。
# 使用 --volumes-from 参数所挂载数据卷的容器自己并不需要保持在运行状态
docker run -d --name db3 --volumes-from db1 training/postgres

如果删除了挂载的容器(包括 dbdata、db1 和 db2),数据卷并不会被自动删除。如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时使用 docker rm -v 命令来指定同时删除关联的容器。

备份、恢复、迁移数据卷

备份

首先使用 –volumes-from 标记来创建一个加载 dbdata 容器卷的容器,并从本地主机挂载当前到容器的 /backup 目录。命令如下:

1
docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

容器启动后,使用了 tar 命令来将 dbdata 卷备份为本地的 /backup/backup.tar。

恢复

如果要恢复数据到一个容器,首先创建一个带有数据卷的容器 dbdata2。

1
docker run -v /dbdata --name dbdata2 ubuntu /bin/bash

然后创建另一个容器,挂载 dbdata2 的容器,并使用 untar 解压备份文件到挂载的容器卷中。

1
docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf /backup/backup.tar

网络

外部访问容器

  • 当使用 -P 标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。
  • -p 则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort。

映射所有接口地址:

  • docker run -d -p 5000:5000 training/webapp python app.py: 默认会绑定本地所有接口上的所有地址。

映射到指定地址的指定端口:

  • 使用 ip:hostPort:containerPort 格式指定映射使用一个特定地址

映射到指定地址的任意端口:

  • 使用 ip::containerPort 绑定 localhost 的任意端口到容器的 5000 端口,本地主机会自动分配一个端口。

还可以使用 udp 标记来指定 udp 端口:

  • docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

查看映射端口配置:

  • 使用 docker port 来查看当前映射的端口配置,也可以查看到绑定的地址:port nostalgic_morse 5000

容器互联

容器的连接(linking)系统是除了端口映射外,另一种跟容器中应用交互的方式。该系统会在源和接收容器之间创建一个隧道,接收容器可以看到源容器指定的信息。

自定义容器命名

连接系统依据容器的名称来执行。因此,首先需要自定义一个好记的容器命名。使用 –name 标记可以为容器自定义命名。

1
docker run -d -P --name web training/webapp python app.py

在执行 docker run 的时候如果添加 –rm 标记,则容器在终止后会立刻删除。注意,–rm 和 -d 参数不能同时使用。

容器互联

使用 –link 参数可以让容器之间安全的进行交互。–link 参数的格式为 –link name:alias,其中 name 是要链接的容器的名称,alias 是这个连接的别名。

1
2
3
4
5
6
7
8
9
10
# 创建一个新的数据库容器。
docker run -d --name db training/postgres
# 创建一个新的 web 容器,并将它连接到 db 容器
docker run -d -P --name web --link db:db training/webapp python app.py
# docker ps 查看容器的连接
docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
349169744e49 training/postgres:latest su postgres -c '/usr About a minute ago Up About a minute 5432/tcp db, web/db
aed84ee21bde training/webapp:latest python app.py 16 hours ago Up 2 minutes 0.0.0.0:49154->5000/tcp web

可以看到自定义命名的容器,db 和 web,db 容器的 names 列有 db 也有 web/db。这表示 web 容器链接到 db 容器,web 容器将被允许访问 db 容器的信息。Docker 在两个互联的容器之间创建了一个安全隧道,而且不用映射它们的端口到宿主主机上。在启动 db 容器的时候并没有使用 -p 和 -P 标记,从而避免了暴露数据库端口到外部网络上。

Docker 通过 2 种方式为容器公开连接信息:

  1. 环境变量
  2. 更新 /etc/hosts 文件

使用 env 命令来查看 web 容器的环境变量:

1
2
3
4
5
6
7
8
9
$ sudo docker run --rm --name web2 --link db:db training/webapp env

DB_NAME=/web2/db
DB_PORT=tcp://172.17.0.5:5432
DB_PORT_5000_TCP=tcp://172.17.0.5:5432
DB_PORT_5000_TCP_PROTO=tcp
DB_PORT_5000_TCP_PORT=5432
DB_PORT_5000_TCP_ADDR=172.17.0.5
# 其中 DB_ 开头的环境变量是供 web 容器连接 db 容器使用,前缀采用大写的连接别名。

除了环境变量,Docker 还添加 host 信息到父容器的 /etc/hosts 的文件。下面是父容器 web 的 hosts 文件

1
2
3
4
5
docker run -t -i --rm --link db:db training/webapp /bin/bash
root@aed84ee21bde:/opt/webapp# cat /etc/hosts
172.17.0.7 aed84ee21bde
. . .
172.17.0.5 db

这里有两个 hosts,第一个是 web 容器,web 容器用 id 作为他的主机名,第二个是 db 容器的 ip 和主机名。可以在 web 容器中安装 ping 命令来测试跟db容器的连通。

区别

传输协议:

  • RPC,可以基于TCP协议,也可以基于HTTP协议
  • HTTP,基于HTTP协议

传输效率:

  • RPC,使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率
  • HTTP,如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装一下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理

性能消耗:主要在于序列化和反序列化的耗时

  • RPC,可以基于thrift实现高效的二进制传输
  • HTTP,大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能

负载均衡:

  • RPC,基本都自带了负载均衡策略
  • HTTP,需要配置Nginx,HAProxy来实现

服务治理(下游服务新增,重启,下线时如何不影响上游调用者):

  • RPC,能做到自动通知,不影响上游
  • HTTP,需要事先通知,修改Nginx/HAProxy配置

总结:
  RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。

RPC

RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

  • 客户端(Client),服务的调用方。
  • 服务端(Server),真正的服务提供者。
  • 客户端存根(Client Stub),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • 服务端存根(Server Stub),接收客户端发送过来的消息,将消息解包,并调用本地的方法。

RPC主要是为了解决的两个问题:

  • 解决分布式系统中,服务之间的调用问题。
  • 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。

假设Service A(Client)要调用Service B(Server)中Util类的handle方法:

  1. Service A的应用层代码中,调用了handle方法;
  2. 这个Util实现类,内部并不是直接实现handle逻辑,而是通过远程调用Service B的RPC接口,来获取运算结果,因此称之为Stub;
  3. Stub和Service B建立远程通讯需要用到远程通讯工具,也就是图中的Run-time Library,这个工具将实现远程通讯的功能,比如Java的Socket,就是这样一个库,当然,也可以用基于Http协议的HttpClient,或者其他通讯工具类都可以,RPC并没有规定要用何种协议进行通讯;
  4. Stub通过调用通讯工具提供的方法,和Service B建立起了通讯,然后将请求数据发给Service B。需要注意的是,由于底层的网络通讯是基于二进制格式的,因此这里Stub传给通讯工具类的数据也必须是二进制,然后传给通讯工具类;
  5. 二进制的数据传到Service B后,Service B使用自己的通讯工具接收二进制的请求;
  6. 然后将二进制的数据反序列化为请求对象,再将这个请求对象交给Service B的Stub处理;
  7. Stub负责解析请求对象,知道调用方要调的是哪个RPC接口,传进来的参数又是什么,然后再把这些参数传给对应的RPC接口,也就是Util的实际实现类去执行;
  8. RPC接口执行完毕,返回执行结果,二者交互角色,重复以上过程。

概述

分布式系统(Distributed System)是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据

为了性能的提升,当单个节点的处理能力无法满足日益增长的计算、存储任务,且硬件的提升高昂到得不偿失的时候,我们才考虑用分布式系统。因为分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。

Partition和Replication是解决分布式系统问题的一个组合,很多具体的问题都可以用这个思路去解决。当然也会引入更多的问题,比如为了可用性与可靠性保证,引用了冗余(复制集)。有了冗余,又需要考虑各个副本间的一致性问题。正如CAP、FLP等理论,在分布式系统中,没有最佳的选择,只有最合适的选择。

分布式系统的挑战:

  1. 异构的机器与网络:分布式系统中的机器配置不一样,其上运行的服务也可能由不同的语言、架构实现,因此处理能力也不一样;节点间通过网络连接,而不同网络运营商提供的网络的带宽、延时、丢包率又不一样。
  2. 节点故障:当节点数目达到一定规模时,出故障的概率就变高了。分布式系统需要保证故障发生的时候,系统仍然是可用的。
  3. 不可靠的网络:节点间通过网络通信,而网络是不可靠的。

分布式系统特性与衡量标准:

  1. 透明性:分布式系统的最高境界是用户根本感知不到这是一个分布式系统。
  2. 可扩展性
  3. 可用性与可靠性
  4. 高性能
  5. 一致性

一个大型网站包含诸多组件,每一个组件都是一个分布式系统,比如分布式存储就是一个分布式系统,消息队列就是一个分布式系统。

一些概念:

  • 负载均衡:

    • Nginx
    • LVS
  • webserver:

    • Java:Tomcat,Apache,Jboss
    • Python:gunicorn、uwsgi、twisted、webpy、tornado
  • service:  

    • SOA、微服务、spring boot,django
  • 容器:

    • docker,kubernetes
  • cache:

    • memcache、redis等
  • 协调中心:

    • zookeeper、etcd等
  • rpc框架:

    • grpc、dubbo、brpc
  • 消息队列:

    • kafka、rabbitMQ、rocketMQ、QSP
  • 实时数据平台:

    • storm、akka
  • 离线数据平台:

    • hadoop、spark
  • dbproxy:

    • cobar也是阿里开源的,在阿里系中使用也非常广泛,是关系型数据库的sharding + replica 代理
  • db:

    • mysql、oracle、MongoDB、HBase
  • 搜索:

    • elasticsearch、solr
  • 日志:

    • rsyslog、elk、flume

请求过程

用户使用Web、APP、SDK等通过HTTP、TCP连接到系统。在分布式系统中,为了高并发、高可用,一般都是多个节点提供相同的服务,所以需要考虑负载均衡。

通过负载均衡找到一个节点后,接下来就是真正处理用户的请求了,请求有可能简单,也有可能很复杂。如果有缓存,则需要考虑分布式缓存,如果缓存没有命中,那么需要去数据库拉取数据。

假设服务A需要调用服务B的服务,首先两个节点需要通信,网络通信都是建立在TCP/IP协议的基础上,但是,每个应用都手写socket是一件冗杂、低效的事情,因此需要应用层的封装,因此有了HTTP、FTP等各种应用层协议。当系统愈加复杂,提供大量的http接口也是一件困难的事情。因此,有了更进一步的抽象,那就是RPC。

一个请求可能包含诸多操作,即在服务A上做一些操作,然后在服务B上做另一些操作,这就涉及到分布式事务的问题。

分布式系统中有大量的服务,每个服务又是多个节点组成,一个服务要想找到另一个服务的某个节点就需要服务注册与发现了。

用户请求操作会产生一些数据、日志等信息,其他一些系统可能会对这些消息感兴趣,这里就抽象出了两个概念,消息的生产者与消费者。那么生产者怎么讲消息发送给消费者呢,RPC并不是一个很好的选择,因为RPC肯定得指定消息发给谁,但实际的情况是生产者并不清楚、也不关心谁会消费这个消息,这个时候就需要消息队列了。

上面提到,用户操作会产生一些数据,这就催生了分布式计算平台用来处理这些海量的数据。

最后,用户的操作完成之后,用户的数据需要持久化,但数据量很大,大到按个节点无法存储,这时就需要分布式存储。