From 9d48b6e5f04e7c8551b8a2d7b3f1acd659c8d6d7 Mon Sep 17 00:00:00 2001 From: thr Date: Sun, 10 Dec 2023 11:10:55 +0800 Subject: [PATCH] Site updated: 2023-12-10 11:10:54 --- 2023/11/18/Awk/index.html | 2 +- .../index.html" | 58 +++++++++---------- 2023/11/18/Jmeter/index.html | 2 +- .../Redis\345\237\272\347\241\200/index.html" | 2 +- 404.html | 2 +- about/index.html | 4 +- archives/2023/11/index.html | 6 +- archives/2023/index.html | 6 +- archives/index.html | 6 +- categories/Blockchain/index.html | 6 +- categories/DataBase/index.html | 6 +- categories/Jmeter/index.html | 6 +- categories/SSM/index.html | 6 +- categories/index.html | 4 +- categories/linux/index.html | 6 +- categories/python/index.html | 6 +- index.html | 50 ++++++++-------- search.xml | 36 ++++++------ tags/Jmeter/index.html | 6 +- tags/XShell/index.html | 6 +- tags/awk/index.html | 6 +- tags/fabric/index.html | 6 +- tags/index.html | 4 +- tags/mybatis/index.html | 6 +- tags/redis/index.html | 6 +- tags/spider/index.html | 6 +- tags/spring/index.html | 6 +- 27 files changed, 133 insertions(+), 133 deletions(-) diff --git a/2023/11/18/Awk/index.html b/2023/11/18/Awk/index.html index e277349..4622984 100644 --- a/2023/11/18/Awk/index.html +++ b/2023/11/18/Awk/index.html @@ -375,4 +375,4 @@

文章作者: thr
文章链接: https://tangsmallrong.github.io/2023/11/18/Awk/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog
\ No newline at end of file +
文章作者: thr
文章链接: https://tangsmallrong.github.io/2023/11/18/Awk/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog
\ No newline at end of file diff --git "a/2023/11/18/Fabric\345\205\245\351\227\250/index.html" "b/2023/11/18/Fabric\345\205\245\351\227\250/index.html" index 891c0d5..239174e 100644 --- "a/2023/11/18/Fabric\345\205\245\351\227\250/index.html" +++ "b/2023/11/18/Fabric\345\205\245\351\227\250/index.html" @@ -7,7 +7,7 @@ - + @@ -56,7 +56,7 @@ isHome: false, isHighlightShrink: false, isToc: true, - postUpdate: '2023-11-18 21:17:22' + postUpdate: '2023-12-10 11:07:53' }

Fabric 入门

Fabric

+ })(window)

Fabric 入门

Fabric

官方文档:关键概念 — hyperledger-fabricdocs master 文档

-

1. 关键概念

1.1 区块链网络

1681270713030

+

1. 关键概念

1.1 区块链网络

1681270713030

这个 Fabric 区块链网络包括了两个应用程序通道以及一个排序通道

组织 R1 和 R4 负责排序通道,R1 和 R2 负责蓝色的应用程序通道,R2 和 R3 负责红色的应用程序通道。

客户端应用程序 A1 是组织 R1 的元素,CA1 是它的证书颁发机构

@@ -168,8 +168,8 @@

-

1681270977420

+

1681271030975

+

1681270977420

1.2 身份

1.4 Peer 节点

区块链网络主要由 Peer 节点(或者简单称之为 Peer)组成

-

1681285693747区块链网络是由 Peer 节点组成的,每个节点都保存着账本和智能合约的副本。

+

1681285693747区块链网络是由 Peer 节点组成的,每个节点都保存着账本和智能合约的副本。

在这个例子中,网络 N 是由节点 P1、P2 和 P3 组成的,每个节点都维护这他们自己的分布式账本 L1。P1、P2 和 P3 使用相同的链码 S1 来访问他们的分布式账本的副本。

在 Fabric 中,链码等同于智能合约,因为它们是使用一个被称为链码的技术概念来实现智能合约

@@ -199,12 +199,12 @@

1.4.1 多账本

  • 一个 Peer 节点可以维护多个账本,并且每个账本具有零个或者多个链码使用账本
-

1681285960829

+

1681285960829

在这个例子中,我们能够看到 Peer 节点 P1 维护着账本 L1 和 L2。账本 L1 通过链码 S1 来访问。账本 2 通过链码 S1 和 S2 访问

1.4.2 多链码

  • 账本数量和访问账本的链码的数量之间没有固定的关系。一个 Peer 节点可能会有很多链码和账本
-

1.4.3 应用程序和 Peer 节点

1681287048342

+

1.4.3 应用程序和 Peer 节点

1681287048342

1.4.4 Peer 节点和排序节点

-

1681301669101

+

1681301669101

账本 L 由区块链 B 和世界状态 W 组成,其中世界状态 W 由区块链 B 决定。我们也可以说世界状态 W 是源自区块链 B

1.6.1 世界状态

  • 世界状态被作为数据库来实现
-

1681301818989

+

1681301818989

示例展示的是 CAR1 和 CAR2 这两辆车的账本状态,二者都各有一个值和一个键。应用程序可以调用智能合约,该合约使用简单的账本 API 来获取写入删除状态。注意状态值可以是简单值(Audi…),也可以是复合值(type:BMW…)。经常会通过查询世界状态来检索具有某些特定属性的对象,例如查找所有红色宝马汽车。

@@ -292,7 +292,7 @@

+

1681302297465

B0 是该区块链的第一个区块,也叫创世区块

它并不包含任何用户交易,但却是账本的起始点

@@ -306,7 +306,7 @@

+

1681302542402

区块头详情:区块 B2 的区块头 H2 包含了区块编号 2,当前区块数据 D2 的哈希值 CH2,以及前一个区块头 H1 的哈希值。

@@ -326,7 +326,7 @@

+

1681303391932

交易详情:交易 T4 位于区块 B1 的区块数据 D1 中,T4包括的内容如下:交易头 H4,一个交易签名 S4,一个交易提案 P4,一个交易响应 R4 和一系列背书 E4。

@@ -341,7 +341,7 @@

+

1681304156885

账本 L包含了一个世界状态 W 和一个区块链 B。其中 W 包含了四个状态,各状态的键分别是:CAR0,CAR1,CAR2 和 CAR3 。而 B 包含了两个区块 0和 1。区块1包含了四笔交易:T1,T2,T3,T4

@@ -365,14 +365,14 @@

+

1681360371573

排序节点的第一个角色是打包提案的账本更新。在本例中,应用程序 A1 向排序节点 O1 发送由 E1 和 E2 背书的交易 T1。同时,应用程序 A2 将 E1 背书的交易 T2 发送给排序节点 O1。O1 将来自应用程序 A1 的交易 T1 和来自应用程序 A2 的交易 T2 以及来自网络中其他应用程序的交易打包到区块 B2 中。我们可以看到,在 B2 中,交易顺序是 T1、T2、T3、T4、T6、T5,但这可能不是这些交易到达排序节点的顺序!(这个例子显示了一个非常简单的排序服务配置,只有一个排序节点。)

  • 验证和提交
-

1681360785332

+

1681360785332

排序节点的第二个角色是将区块分发给 Peer 节点。在本例中,排序节点 O1 将区块 B2 分配给节点 P1 和 P2。节点 P1 处理区块 B2,在 P1 上的账本 L1 中添加一个新区块。同时,节点 P2 处理区块 B2,从而将一个新区块添加到 P2 上的账本 L1中。一旦这个过程完成,节点 P1 和 P2 上的账本 L1 就会保持一致的更新,并且每个节点都可以通知与之连接的应用程序交易已经被处理。

@@ -390,7 +390,7 @@

2

2.1.1 ubuntu 联网

1
2
3
sudo systemctl restart NetworkManager.service
sudo apt-get install network-manager
sudo systemctl restart NetworkManager.service
-

1681387612411

+

1681387612411

2.1.2 apt 换源

1
2
3
https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/
sudo gedit /etc/apt/sources.list
sudo apt update

2.1.3 安装 docker

1
2
3
4
// 安装docker、docker-compose
sudo apt install docker docker-compose
sudo systemctl enable docker
sudo usermod -a -G docker <username>
@@ -447,7 +447,7 @@

+

1681607417562

商业票据的状态转移表。商业票据通过发行购买兑换交易在已发行交易中已兑换之间进行状态转移。

@@ -455,14 +455,14 @@

3.2.2 账本状态

文章作者: thr
文章链接: https://tangsmallrong.github.io/2023/11/18/Fabric%E5%85%A5%E9%97%A8/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog
\ No newline at end of file +

4. 工作流程

1681614315146

+
文章作者: thr
文章链接: https://tangsmallrong.github.io/2023/11/18/Fabric%E5%85%A5%E9%97%A8/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog
\ No newline at end of file diff --git a/2023/11/18/Jmeter/index.html b/2023/11/18/Jmeter/index.html index a8926bd..c8220d1 100644 --- a/2023/11/18/Jmeter/index.html +++ b/2023/11/18/Jmeter/index.html @@ -291,4 +291,4 @@

1690966360901

-
文章作者: thr
文章链接: https://tangsmallrong.github.io/2023/11/18/Jmeter/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog
\ No newline at end of file +
文章作者: thr
文章链接: https://tangsmallrong.github.io/2023/11/18/Jmeter/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog
\ No newline at end of file diff --git "a/2023/11/18/Redis\345\237\272\347\241\200/index.html" "b/2023/11/18/Redis\345\237\272\347\241\200/index.html" index 6f1424c..706a41e 100644 --- "a/2023/11/18/Redis\345\237\272\347\241\200/index.html" +++ "b/2023/11/18/Redis\345\237\272\347\241\200/index.html" @@ -958,4 +958,4 @@

1695449585760

-
文章作者: thr
文章链接: https://tangsmallrong.github.io/2023/11/18/Redis%E5%9F%BA%E7%A1%80/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog
\ No newline at end of file +
文章作者: thr
文章链接: https://tangsmallrong.github.io/2023/11/18/Redis%E5%9F%BA%E7%A1%80/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog
\ No newline at end of file diff --git a/404.html b/404.html index 20c8b24..7976e7e 100644 --- a/404.html +++ b/404.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:50' + postUpdate: '2023-12-10 11:10:16' }
\ No newline at end of file +
\ No newline at end of file diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index ec97b4f..b28309f 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
文章总览 - 8
2023
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
Spring
爬虫
关于 XShell 连不上远程 CentOS7 云服务器的问题
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
\ No newline at end of file diff --git a/archives/2023/index.html b/archives/2023/index.html index 00fc7a4..7908ceb 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
文章总览 - 8
2023
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
Spring
爬虫
关于 XShell 连不上远程 CentOS7 云服务器的问题
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
\ No newline at end of file diff --git a/archives/index.html b/archives/index.html index ee44bc5..98809f1 100644 --- a/archives/index.html +++ b/archives/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
文章总览 - 8
2023
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
Spring
爬虫
关于 XShell 连不上远程 CentOS7 云服务器的问题
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
\ No newline at end of file diff --git a/categories/Blockchain/index.html b/categories/Blockchain/index.html index 7da61dd..e9a4004 100644 --- a/categories/Blockchain/index.html +++ b/categories/Blockchain/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
分类 - Blockchain
2023
Fabric 入门
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/categories/DataBase/index.html b/categories/DataBase/index.html index 02e29ae..d892fed 100644 --- a/categories/DataBase/index.html +++ b/categories/DataBase/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
分类 - DataBase
2023
Redis 基础
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/categories/Jmeter/index.html b/categories/Jmeter/index.html index 84c18a8..d5cb959 100644 --- a/categories/Jmeter/index.html +++ b/categories/Jmeter/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
分类 - Jmeter
2023
Jmeter 压测
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/categories/SSM/index.html b/categories/SSM/index.html index 1f35636..0718ae4 100644 --- a/categories/SSM/index.html +++ b/categories/SSM/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
分类 - SSM
2023
MyBatis
Spring
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/categories/index.html b/categories/index.html index fedccc2..4b5d2b3 100644 --- a/categories/index.html +++ b/categories/index.html @@ -150,11 +150,11 @@ } } detectApple() - })(window)
\ No newline at end of file diff --git a/categories/linux/index.html b/categories/linux/index.html index 7ff7970..5c8031a 100644 --- a/categories/linux/index.html +++ b/categories/linux/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
\ No newline at end of file diff --git a/categories/python/index.html b/categories/python/index.html index 718feda..0009abc 100644 --- a/categories/python/index.html +++ b/categories/python/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
分类 - python
2023
爬虫
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/index.html b/index.html index cca414d..686bd9d 100644 --- a/index.html +++ b/index.html @@ -51,7 +51,7 @@ isHome: true, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
\ No newline at end of file diff --git a/search.xml b/search.xml index 3876ebf..80dfcf6 100644 --- a/search.xml +++ b/search.xml @@ -4,23 +4,23 @@ - Fabric 入门 - - /2023/11/18/Fabric%E5%85%A5%E9%97%A8/ + awk 编程 + + /2023/11/18/Awk/ - Fabric

官方文档:关键概念 — hyperledger-fabricdocs master 文档

1. 关键概念

1.1 区块链网络

1681270713030

这个 Fabric 区块链网络包括了两个应用程序通道以及一个排序通道

组织 R1 和 R4 负责排序通道,R1 和 R2 负责蓝色的应用程序通道,R2 和 R3 负责红色的应用程序通道。

客户端应用程序 A1 是组织 R1 的元素,CA1 是它的证书颁发机构

组织 R2 的节点 P2 可以使用蓝色的通信设施,也可以使用红色的应用程序通道。

每个应用程序通道具有它自己的通道配置,这里是 CC1 和 CC2。

系统通道的通道配置是网络配置 NC4 的一部分。

一个有四个组织的网络,带有两个通道和三个 Peer 节点,两个智能合约和一个排序服务。

并由四个证书颁发机构来支撑。

它为三个客户端应用程序提供了账本及智能合约服务,这些应用程序可以通过两个通道与账本和智能合约进行交互。

1681271030975

1681270977420

1.2 身份

1.2.1 PKI

1.3 成员服务提供者 (MSP)

以太坊属于匿名网络,hpyerledger 是实名制的网络

1.4 Peer 节点

区块链网络主要由 Peer 节点(或者简单称之为 Peer)组成

1681285693747区块链网络是由 Peer 节点组成的,每个节点都保存着账本和智能合约的副本。

在这个例子中,网络 N 是由节点 P1、P2 和 P3 组成的,每个节点都维护这他们自己的分布式账本 L1。P1、P2 和 P3 使用相同的链码 S1 来访问他们的分布式账本的副本。

在 Fabric 中,链码等同于智能合约,因为它们是使用一个被称为链码的技术概念来实现智能合约

Peer 节点是账本及链码的宿主,应用程序及管理员如果想要访问这些资源,他们必须要和 Peer 节点进行交互

1.4.1 多账本

1681285960829

在这个例子中,我们能够看到 Peer 节点 P1 维护着账本 L1 和 L2。账本 L1 通过链码 S1 来访问。账本 2 通过链码 S1 和 S2 访问

1.4.2 多链码

1.4.3 应用程序和 Peer 节点

1681287048342

1.4.4 Peer 节点和排序节点

1.5 chaincode 智能合约

1681294843518

在上图中,我们可以看到组织 ORG1 和 ORG2 是如何通过定义一个 car 智能合约来实现 查询转移 和 更新 汽车的。来自这些组织的应用程序调用此智能合约执行业务流程中已商定的步骤,例如将特定汽车的所有权从 ORG1 转移到 ORG2

可以将智能合约看成交易的管理者,而链码则管理着如何将智能合约打包以便用于部署。

一个智能合约定义在一个链码中。而多个智能合约也可以定义在同一个链码中。当一个链码部署完毕,该链码中的所有智能合约都可供应用程序使用。

1
2
3
4
5
6
7
8
9
10
11
12
async createCar(ctx, carNumber, make, model, color, owner) {

const car = {
color,
docType: 'car',
make,
model,
owner,
};

await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car)));
}

1.5.1 背书

1.5.2 有效交易

注意,在执行智能合约时世界状态没有更新

1.5.3 通道

1681296849809

在上面的示例中,car 智能合约被定义在 VEHICLE 通道上,insurance 智能合约被定义在 INSURANCE 通道上。car 的链码定义明确了以下背书策略:任何交易在被认定为有效之前必须由 ORG1 和 ORG2 共同签名。insurance 智能合约的链码定义明确了只需要 ORG3 对交易进行背书即可。ORG1 参与了 VEHICLE 通道和 INSURANCE 通道这两个网络,并且能够跨网络协调与 ORG2 和 ORG3 的活动。

1.6 账本

1681301669101

账本 L 由区块链 B 和世界状态 W 组成,其中世界状态 W 由区块链 B 决定。我们也可以说世界状态 W 是源自区块链 B

1.6.1 世界状态

1681301818989

示例展示的是 CAR1 和 CAR2 这两辆车的账本状态,二者都各有一个值和一个键。应用程序可以调用智能合约,该合约使用简单的账本 API 来获取写入删除状态。注意状态值可以是简单值(Audi…),也可以是复合值(type:BMW…)。经常会通过查询世界状态来检索具有某些特定属性的对象,例如查找所有红色宝马汽车。

1.6.2 区块链

1681302297465

B0 是该区块链的第一个区块,也叫创世区块

它并不包含任何用户交易,但却是账本的起始点

相反的,创世区块包含了一个配置交易,该交易含有网络配置(未显示)的初始状态

1.6.3 区块

组成

1.6.4 交易

1681303391932

交易详情:交易 T4 位于区块 B1 的区块数据 D1 中,T4包括的内容如下:交易头 H4,一个交易签名 S4,一个交易提案 P4,一个交易响应 R4 和一系列背书 E4。

这部分用 H4 表示,它记录了关于交易的一些重要元数据,比如,相关链码的名字以及版本。

签名

这部分用 S4 表示,它包含了一个由客户端应用程序创建的加密签名。该字段是用来检查交易细节是否未经篡改,因为交易签名的生成需要用到应用程序的私钥。

提案

这部分用 P4 表示,它负责对应用程序供给智能合约的输入参数进行编码,随后该智能合约生成提案账本更新。在智能合约运行时,这个提案提供了一套输入参数,这些参数同当前的世界状态一起决定了新的账本世界状态。

响应

这部分用 R4 表示,它是以读写集 (RW-set)的形式记录下世界状态之前和之后的值。交易响应是智能合约的输出,如果交易验证成功,那么该交易会被应用到账本上,从而更新世界状态。

背书

1.7 排序服务

1.7.1 排序节点和交易流程

更新账本的应用程序涉及到三个阶段,该过程确保区块链网络中的所有节点保持它们的账本彼此一致。

在第一阶段,客户端应用程序将交易提案发送给一组节点,这些节点将调用智能合约来生成一个账本更新提案,然后背书该结果。背书节点此时不将提案中的更新应用于其账本副本。相反,背书节点将向客户端应用程序返回一个提案响应。

已背书的交易提案最终将在第二阶段经过排序生成区块,

然后在第三阶段分发给所有节点进行最终验证和提交。

1681360371573

排序节点的第一个角色是打包提案的账本更新。在本例中,应用程序 A1 向排序节点 O1 发送由 E1 和 E2 背书的交易 T1。同时,应用程序 A2 将 E1 背书的交易 T2 发送给排序节点 O1。O1 将来自应用程序 A1 的交易 T1 和来自应用程序 A2 的交易 T2 以及来自网络中其他应用程序的交易打包到区块 B2 中。我们可以看到,在 B2 中,交易顺序是 T1、T2、T3、T4、T6、T5,但这可能不是这些交易到达排序节点的顺序!(这个例子显示了一个非常简单的排序服务配置,只有一个排序节点。)

1681360785332

排序节点的第二个角色是将区块分发给 Peer 节点。在本例中,排序节点 O1 将区块 B2 分配给节点 P1 和 P2。节点 P1 处理区块 B2,在 P1 上的账本 L1 中添加一个新区块。同时,节点 P2 处理区块 B2,从而将一个新区块添加到 P2 上的账本 L1中。一旦这个过程完成,节点 P1 和 P2 上的账本 L1 就会保持一致的更新,并且每个节点都可以通知与之连接的应用程序交易已经被处理。

1.7.2 排序服务的实现

2. 入门

2.1 配置环境

此处参考b站视频,虽然可能不全,并且后半段实现不了

但是前面的跟下来都还不错!但是要注意自己的版本问题!

从0开始快速安装Hyperledger Fabric_哔哩哔哩_bilibili

2.1.1 ubuntu 联网

1
2
3
sudo systemctl restart NetworkManager.service
sudo apt-get install network-manager
sudo systemctl restart NetworkManager.service

1681387612411

2.1.2 apt 换源

1
2
3
https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/
sudo gedit /etc/apt/sources.list
sudo apt update

2.1.3 安装 docker

1
2
3
4
// 安装docker、docker-compose
sudo apt install docker docker-compose
sudo systemctl enable docker
sudo usermod -a -G docker <username>

2.1.4 安装 golang

1
2
3
4
5
6
7
8
// 安装golang
https://go.dev/doc/install
sudo su
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.6.linux-amd64.tar.gz
gedit /etc/profile
export PATH=$PATH:/usr/local/go/bin
gedit ~/.bashrc
source /etc/profile

2.1.5 docker 加速器

1
2
// docker加速器
https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

2.1.6 安装fabric-sample

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、手动创建脚本,安装samples、docker
https://github.com/hyperledger/fabric/blob/main/scripts/bootstrap.sh
修改binaries=false
sudo chmod u+x bootstrap.sh
./bootstrap.sh

2、安装binaries
https://github.com/hyperledger/fabric/releases/download/v2.4.1/hyperledger-fabric-linux-amd64-2.4.1.tar.gz
https://github.com/hyperledger/fabric-ca/releases/download/v1.5.2/hyperledger-fabric-ca-linux-amd64-1.5.2.tar.gz
tar -xzvf 压缩包名 -C 目的地

3、配置go代理
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

3. 开发

3.1 分析

3.1.1 商业票据

3.1.2 交易

3.2 流程和数据设计

3.2.1 生命周期

3.2.2 账本状态

3.2.3 状态键值

3.2.3 逻辑表示

为了满足不同类型的查询任务,把所有相关的商业票据按逻辑顺序排列在一起是很有帮助的。PaperNet 的设计包含了商业票据列表的思想——一个逻辑容器,每当商业票据发行或发生其他更改时,该容器都会更新

3.2.4 物理表现

3.3 智能合约处理

 连接到网络的所有应用程序必须使用相同版本的智能合约,以便它们共同实现相同的共享业务流程和数据。

在 Java 中,类必须使用 @Contract(...) 标注进行包装。它支持额外的智能合约信息,比如许可和作者。 @Default() 标注表明该智能合约是默认合约类。在智能合约中标记默认合约类在一些有多个合约类的智能合约中会很有用。

fabric-samples/CommercialPaperContract.java at master · hyperledger/fabric-samples (github.com)

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/*
SPDX-License-Identifier: Apache-2.0
*/
package org.example;

import java.util.logging.Logger;

import org.example.ledgerapi.State;
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.Contact;
import org.hyperledger.fabric.contract.annotation.Contract;
import org.hyperledger.fabric.contract.annotation.Default;
import org.hyperledger.fabric.contract.annotation.Info;
import org.hyperledger.fabric.contract.annotation.License;
import org.hyperledger.fabric.contract.annotation.Transaction;
import org.hyperledger.fabric.shim.ChaincodeStub;

/**
* A custom context provides easy access to list of all commercial papers
*/

/**
* Define commercial paper smart contract by extending Fabric Contract class
*
*/
@Contract(name = "org.papernet.commercialpaper", info = @Info(title = "MyAsset contract", description = "", version = "0.0.1", license = @License(name = "SPDX-License-Identifier: Apache-2.0", url = ""), contact = @Contact(email = "java-contract@example.com", name = "java-contract", url = "http://java-contract.me")))
@Default
public class CommercialPaperContract implements ContractInterface {

// use the classname for the logger, this way you can refactor
private final static Logger LOG = Logger.getLogger(CommercialPaperContract.class.getName());

@Override
public Context createContext(ChaincodeStub stub) {
return new CommercialPaperContext(stub);
}

public CommercialPaperContract() {
}

/**
* Define a custom context for commercial paper
*/

/**
* Instantiate to perform any setup of the ledger that might be required.
*
* @param {Context} ctx the transaction context
*/
@Transaction
public void instantiate(CommercialPaperContext ctx) {
// No implementation required with this example
// It could be where data migration is performed, if necessary
LOG.info("No data migration to perform");
}

/**
* Issue commercial paper
*
* @param {Context} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} issueDateTime paper issue date
* @param {String} maturityDateTime paper maturity date
* @param {Integer} faceValue face value of paper
*/
@Transaction
public CommercialPaper issue(CommercialPaperContext ctx, String issuer, String paperNumber, String issueDateTime,
String maturityDateTime, int faceValue) {

System.out.println(ctx);

// create an instance of the paper
CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime,
faceValue,issuer,"");

// Smart contract, rather than paper, moves paper into ISSUED state
paper.setIssued();

// Newly issued paper is owned by the issuer
paper.setOwner(issuer);

System.out.println(paper);
// Add the paper to the list of all similar commercial papers in the ledger
// world state
ctx.paperList.addPaper(paper);

// Must return a serialized paper to caller of smart contract
return paper;
}

/**
* Buy commercial paper
*
* @param {Context} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} currentOwner current owner of paper
* @param {String} newOwner new owner of paper
* @param {Integer} price price paid for this paper
* @param {String} purchaseDateTime time paper was purchased (i.e. traded)
*/
@Transaction
public CommercialPaper buy(CommercialPaperContext ctx, String issuer, String paperNumber, String currentOwner,
String newOwner, int price, String purchaseDateTime) {

// Retrieve the current paper using key fields provided
String paperKey = State.makeKey(new String[] { paperNumber });
CommercialPaper paper = ctx.paperList.getPaper(paperKey);

// Validate current owner
if (!paper.getOwner().equals(currentOwner)) {
throw new RuntimeException("Paper " + issuer + paperNumber + " is not owned by " + currentOwner);
}

// First buy moves state from ISSUED to TRADING
if (paper.isIssued()) {
paper.setTrading();
}

// Check paper is not already REDEEMED
if (paper.isTrading()) {
paper.setOwner(newOwner);
} else {
throw new RuntimeException(
"Paper " + issuer + paperNumber + " is not trading. Current state = " + paper.getState());
}

// Update the paper
ctx.paperList.updatePaper(paper);
return paper;
}

/**
* Redeem commercial paper
*
* @param {Context} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} redeemingOwner redeeming owner of paper
* @param {String} redeemDateTime time paper was redeemed
*/
@Transaction
public CommercialPaper redeem(CommercialPaperContext ctx, String issuer, String paperNumber, String redeemingOwner,
String redeemDateTime) {

String paperKey = CommercialPaper.makeKey(new String[] { paperNumber });

CommercialPaper paper = ctx.paperList.getPaper(paperKey);

// Check paper is not REDEEMED
if (paper.isRedeemed()) {
throw new RuntimeException("Paper " + issuer + paperNumber + " already redeemed");
}

// Verify that the redeemer owns the commercial paper before redeeming it
if (paper.getOwner().equals(redeemingOwner)) {
paper.setOwner(paper.getIssuer());
paper.setRedeemed();
} else {
throw new RuntimeException("Redeeming owner does not own paper" + issuer + paperNumber);
}

ctx.paperList.updatePaper(paper);
return paper;
}

}

3.4 应用

3.4.1 基本流程

4. 工作流程

1681614315146

]]> + 使用 awk 编写 shell 脚本

1. awk 基础

2. 文本格式化

2.1 awk 内置变量

内置变量解释
$n指定分隔符后,当前记录的第n个字段
$0完整的输入记录
FS字段分隔符,默认是空格
NF(Number of fields)分割后,当前行一共有多少个字段(几列?)
NR(Number of records)当前记录数,行数
FILENAME当前文件名
可用 man 手册查看man awk

2.2 自动定义输出内容

2.3 awk 参数

参数解释
-F指定分割字段符
-v定义或修改一个 awk 内部的变量
-f从脚本文件中读取 awk 命令

2.4 显示文件第5行

3. awk 模式 pattern

4. awk 与正则表达式

5. awk 案例

5.1 插入新字段和格式化空白

1
echo a b c d | awk '{$2=$2" e f g";print}'
1
awk 'BEGIN{OFS="\t"}{$1=$1;print}' a.txt

5.2 筛选 IPV4 地址

1
2
3
4
5
6
# 法一:正则匹配开头为inet 且第二列不以 127 开头
ifconfig | awk '/inet / && !($2 ~ /^127/){print $2}'

# 按段落读取
# 法二:一次性读取一段,可以修改输入的行分隔符为""
ifconfig | awk 'BEGIN{RS=""}!/lo/{print $6}'

5.3 读取 .ini 配置文件中的某段

读取 mysql 段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 匹配包含 [mysql] 的,但是可读性较差
/\[mysql\]/{} # 使用反斜线转义一下中括号

# 对于能够确定的字符串,可用index去搜索
# 搜索到的话,返回索引位;搜索不到返回0
index($0, "[mysql]") {
print # 输出符合条件的那一行
while( (getline var) > 0 ) {
if (var ~ /\[.*\]/) { # 如果匹配到格式形如 [..] 的,就停止
exit
}
print var
}
}

# getline 返回值:
# >0 表示已经读取到数据
# =0 表示遇到结尾 EOF,也就表示没有读取到东西
# <0 表示读取报错

5.4 根据某字段去重

对出现的次数进行判断?

1
2
3
4
5
6
7
8
9
# 以问号为分隔符
awk -F "?" '{
arr[$2]++;if(arr[$2]==1){print}
}' 1.txt

# 可以更加简短
awk -F "?" '{
!arr[$2]++{print} # 后置++返回加之前的数, 所以如果加之前为0, 就打印
}' 1.txt

5.5 次数统计

需要用到数组

1
awk '{arr[$0]++}END{for(i in arr){print arr[i], i}' 1.txt
1
netstat -tnap 2>/dev/null | awk '/^tcp/{arr[$6]++}END{for(i in arr){print arr[i], i}}' 
1
awk '$8!=200{arr[$1]++}END{for(i in arr){print arr[i], i}' access.log | sort -k1nr | head -n 10 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BEGIN {
FS="|" # 指定字段分隔符
}
# SEPSUB \034 真正保存时以 $1\034$2 保存
{
# 如果是第一次出现(类似联合主键)
if (!arr[$1, $2]++) { # awk 中的数组是关联数组, 它的索引全是字符串
arr1[$1]++
}
}

END {
for(i in arr1) {
print i, arr1[i] > (i".txt")
}
}

6. 课后作业

6.1 需求

使用 awk 编程编写 shell 脚本,需要有函数式、管道式、流水线编程的思想,具体要求如下:

6.2 实现思路

6.3 代码实现

1
2
3
4
5
# 给脚本赋予执行权限
chmod u+x count.sh

# 运行脚本
sudo ./count.sh
]]>
- Blockchain + Linux - fabric + awk @@ -29,23 +29,23 @@ - awk 编程 - - /2023/11/18/Awk/ + Jmeter 压测 + + /2023/11/18/Jmeter/ - 使用 awk 编写 shell 脚本
  • awk 是一个强大的 Linux 命令,有强大的文本格式化的能力
  • 三剑客:
    • grep:擅长单纯的查找或匹配文本内容
    • awk:更适合编辑、处理匹配到的文本内容
    • sed:更适合格式化文本内容,对文本进行复杂处理

1. awk 基础

  • awk 语法:

    1
    2
    awk [option] 'pattern[action]' file ...
    awk 参数 '条件动作' 文件
  • Action 指的是动作,awk 擅长文本格式化,且输出格式化后的结果,因此最常用的动作就是 print

    • 条件动作举例:
    1
    2
    3
    4
    {print $0}  # 把每行都打印出来, 等同于 {print}
    {print $1} # 把每行的第一列打印出来(默认以空格为列的分隔符)
    {print $NF} # 每行的最后一列[倒数第二列可以写成$(NF-1)]
    {print $1,$4,$5} # 打印多列(中间加上逗号是为了空格分割)
  • awk 是按行处理文件,一行处理完毕,处理下一行,根据用户指定的分隔符去工作,没有指定则默认空格

  • awk 分隔符有两种:

    • 输入分隔符,awk 默认是空格,空白字符,英文是 file separator,变量名是 FS
    • 输出分隔符,output field separator,简称 OFS,默认也是空格

2. 文本格式化

2.1 awk 内置变量

内置变量解释
$n指定分隔符后,当前记录的第n个字段
$0完整的输入记录
FS字段分隔符,默认是空格
NF(Number of fields)分割后,当前行一共有多少个字段(几列?)
NR(Number of records)当前记录数,行数
FILENAME当前文件名
可用 man 手册查看man awk

2.2 自动定义输出内容

  • awk,必须外层单引号,内存双引号
  • 内置变量 $1、$2 都不得添加双引号,否则会识别为文本,尽量别加引号
    • 如:awk '{print "第一列: "$1, "第二列: "$3}' hello.txt
    • 如(将 ipconfig 里的 eth0 段的ip输出):ipconfig eth0 | awk 'NR==2{print $0}'

2.3 awk 参数

参数解释
-F指定分割字段符
-v定义或修改一个 awk 内部的变量
-f从脚本文件中读取 awk 命令
  • 举例(指定冒号作为分隔符):
    • awk -F ":" '{print $1}' hello.txt
    • 也可以用 -v 修改 FS 参数:awk -v FS=":" '{print $1,$NF}' hello.txt
    • 还可以修改输出分隔符:awk -v OFS="\t" '{print $1,$NF}' hello.txt

2.4 显示文件第5行

  • NR 在 awk 中表示行号,NR==5 表示行号是 5 的那一行
    • 举例:找到第五行和第六行的内容并打印:awk 'NR==5, NR==6{print $0}' hello.txt
    • 看行号2-5的内容:awk 'NR==2, NR==5' hello.txt
    • 打印行号37-40的内容同时显示行号:awk 'NR==37, NR==40{print NR,$0}' hello.txt

3. awk 模式 pattern

  • 特殊的 pattern:BEGINEND
    • BEGIN 模式是处理文本之前需要执行的操作
    • END 模式是处理完所有行之后执行的操作
  • 比如:
    • awk BEGIN{print "hhh"} hello.txt
    • awk 'BEGIN{print "处理文本之前"}{print $0}END{print "所有文本处理完毕"}' hello.txt

4. awk 与正则表达式

  • 主要与 pattern模式(条件)结合使用
    • 不指定模式,awk每一行都会执行对应的动作
    • 指定了模式,只有被模式匹配到的、符合条件的行才会执行动作
  • awk 使用正则语法:
    • awk '/正则表达式/动作' /etc/passwd
    • awk 命令使用正则表达式,必须把正则放入 // 双斜杠中,匹配到结果后执行动作 {print $0},打印整行信息
  • 比如:
    • 输出以 games 开头的行:awk '/^games/{print $0}' hello.txt
    • 输出符合上面条件的行的第一列和最后一列(分隔符为冒号):awk -F ":" '/^games/{print $0, $NF}' hello.txt

5. awk 案例

5.1 插入新字段和格式化空白

  • a b c db 后面插入 3 个字段 e f g
1
echo a b c d | awk '{$2=$2" e f g";print}'
  • 移除每行的前缀、后缀空白,并将各部分左对齐
1
awk 'BEGIN{OFS="\t"}{$1=$1;print}' a.txt

5.2 筛选 IPV4 地址

  • 从 ifconfig 命令的结果中筛选出除了 lo 网卡外的所有 IPv4 地址
1
2
3
4
5
6
# 法一:正则匹配开头为inet 且第二列不以 127 开头
ifconfig | awk '/inet / && !($2 ~ /^127/){print $2}'

# 按段落读取
# 法二:一次性读取一段,可以修改输入的行分隔符为""
ifconfig | awk 'BEGIN{RS=""}!/lo/{print $6}'

5.3 读取 .ini 配置文件中的某段

读取 mysql 段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 匹配包含 [mysql] 的,但是可读性较差
/\[mysql\]/{} # 使用反斜线转义一下中括号

# 对于能够确定的字符串,可用index去搜索
# 搜索到的话,返回索引位;搜索不到返回0
index($0, "[mysql]") {
print # 输出符合条件的那一行
while( (getline var) > 0 ) {
if (var ~ /\[.*\]/) { # 如果匹配到格式形如 [..] 的,就停止
exit
}
print var
}
}

# getline 返回值:
# >0 表示已经读取到数据
# =0 表示遇到结尾 EOF,也就表示没有读取到东西
# <0 表示读取报错

5.4 根据某字段去重

对出现的次数进行判断?

1
2
3
4
5
6
7
8
9
# 以问号为分隔符
awk -F "?" '{
arr[$2]++;if(arr[$2]==1){print}
}' 1.txt

# 可以更加简短
awk -F "?" '{
!arr[$2]++{print} # 后置++返回加之前的数, 所以如果加之前为0, 就打印
}' 1.txt

5.5 次数统计

需要用到数组

  • 统计单词出现次数
1
awk '{arr[$0]++}END{for(i in arr){print arr[i], i}' 1.txt
  • 统计 TCP 链接状态数量
1
netstat -tnap 2>/dev/null | awk '/^tcp/{arr[$6]++}END{for(i in arr){print arr[i], i}}' 
  • 统计日志中各IP访问非200状态码的次数,结合 sort 和 head 命令显示前十个最多的
1
awk '$8!=200{arr[$1]++}END{for(i in arr){print arr[i], i}' access.log | sort -k1nr | head -n 10 
  • 统计每个 URL 的独立访问 IP 有多少个 (去重),并且要为每个 URL 保存一个对应的文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BEGIN {
FS="|" # 指定字段分隔符
}
# SEPSUB \034 真正保存时以 $1\034$2 保存
{
# 如果是第一次出现(类似联合主键)
if (!arr[$1, $2]++) { # awk 中的数组是关联数组, 它的索引全是字符串
arr1[$1]++
}
}

END {
for(i in arr1) {
print i, arr1[i] > (i".txt")
}
}

6. 课后作业

6.1 需求

使用 awk 编程编写 shell 脚本,需要有函数式、管道式、流水线编程的思想,具体要求如下:

  • 现有 auth.log 日志文件,日志文件格式如下,要求统计登录出错的记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Sep 20 06:25:01 iZbp1amwnz2vqj9ehdtgjvZ CRON[6905]: pam_unix(cron:session): session closed for user root
    Sep 20 06:35:01 iZbp1amwnz2vqj9ehdtgjvZ CRON[7029]: pam_unix(cron:session): session opened for user root by (uid=0)
    Sep 20 06:35:01 iZbp1amwnz2vqj9ehdtgjvZ CRON[7029]: pam_unix(cron:session): session closed for user root
    Sep 20 06:45:01 iZbp1amwnz2vqj9ehdtgjvZ CRON[7034]: pam_unix(cron:session): session opened for user root by (uid=0)
    ...
    ...
    Sep 26 19:07:15 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: error: Could not load host key: /etc/ssh/ssh_host_ed25519_key
    Sep 26 19:07:28 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: Invalid user administrator from 95.10.179.8 port 28508
    Sep 26 19:07:29 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: pam_unix(sshd:auth): check pass; user unknown
    Sep 26 19:07:29 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=95.10.179.8
    Sep 26 19:07:31 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: Failed password for invalid user administrator from 95.10.179.8 port 28508 ssh2
    Sep 26 19:07:32 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: Connection closed by invalid user administrator 95.10.179.8 port 28508 [preauth]
    Sep 26 19:15:02 iZbp1amwnz2vqj9ehdtgjvZ CRON[18764]: pam_unix(cron:session): session opened for user root by (uid=0)
    Sep 26 19:15:02 iZbp1amwnz2vqj9ehdtgjvZ CRON[18764]: pam_unix(cron:session): session closed for user root
    ...
  • 对上述 log 文件进行分析,然后编写 shell 脚本,使用 awk 编程,先读取日志文件,然后将出错的记录以下面的格式写入/usr/local/log/error.md 文件中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ...
    ### 2021-9-26 19:07:15 [18760]
    #### sshd
    **error:** Could not load host key: /etc/ssh/ssh_host_ed25519_key
    **pam_unix(sshd:auth):** check pass; user user unknown
    **pam_unix(sshd:auth):** authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=95.10.179.8 port 28508

    Invalid user administrator from 95.10.1
    Failed password for invalid user administrator from 95.10.179.8 port 28508 ssh2
    Connection closed by invalid user administrator 95.10.179.8 port 28508 [preauth]





    ### 2021-9-26 19:15:02 [18764]
    #### CRON
    **pam_unix(cron:session):** session opened for user root by (uid=0)
    **pam_unix(cron:session):** session closed for user root

6.2 实现思路

  • 准备工作
    • 定义日志文件路径,确保日志目录存在
  • 处理日志文件
    • 使用 awk 处理 auth.log 文件,按进程 ID 分类记录错误信息
    • BEGIN 块中,初始化全局变量、获取当前年份、以及初始化一些变量
    • 在每一行的处理中,解析进程信息、过滤不需要的行、按照进程 ID 分类记录错误信息
    • END 块中,处理最后一个进程 ID 的错误信息
  • 输出错误信息
    • 错误信息会按照进程 ID 分类记录在 /usr/local/log/error.md 文件中

6.3 代码实现

  • count.sh

    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
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    #!/bin/bash

    # LOG_FILE="/usr/local/log/error.md" # 日志文件名作为常量

    # # 检查日志目录是否存在,不存在则创建
    # mkdir -p "$(dirname "$LOG_FILE")"

    # # 检查日志文件是否存在,存在则清空,不存在则创建
    # if [ -e "$LOG_FILE" ]; then
    # true > "$LOG_FILE"
    # else
    # touch "$LOG_FILE"
    # fi

    LOG_FILE="/usr/local/log/error.md"

    # 确保日志目录存在,不存在则创建
    mkdir -p "$(dirname "$LOG_FILE")"

    # 清空日志文件,如果不存在则创建
    > "$LOG_FILE"


    # 使用 awk 处理 auth.log 文件
    awk -v log_file="$LOG_FILE" '
    BEGIN {
    # 全局变量的定义
    prev_process_id = ""
    has_colon = 0 # 初始化 has_colon

    # 获取系统当前年份
    cmd = "date +%Y"
    cmd | getline year
    close(cmd)

    # 初始化 error_data 为空字符串
    error_data = ""
    }

    {
    # 获取当前行的进程id号
    parseProcessInfo($5)
    process_name = process_info[1]
    process_id = process_info[2]

    # 过滤掉不是错误信息的行
    if (process_name ~ /(vsftpd|systemd-logind)/) {
    next
    }

    # 如果当前行的进程id号不等于上一行的进程id号或上一行的进程号为空
    if (process_id != prev_process_id || prev_process_id == "") {
    # 打印上一进程号的错误信息
    printErrorData(prev_process_id)
    # 清空数组
    delete error_array

    if (prev_process_id != "") {
    print "\n\n" >> log_file
    }

    # 记录当前的进程id号为 prev_process_id
    prev_process_id = process_id

    # 获取时间并打印, 同时打印进程id, 格式为 ### 2021-9-26 19:07:15 [process_id]
    formatted_datetime = getFormattedDateTime($1, $2, $3)
    print "###", formatted_datetime, "[" process_id "]" >> log_file
    print "####", process_name >> log_file
    }

    # 解析当前行的错误信息并输出到缓存
    error_data = parseErrorInfo($6)
    for (i = 7; i <= NF; i++) {
    error_data = error_data " " $i
    }
    # 存储到数组
    error_array[has_colon] = error_array[has_colon] error_data "\n"
    }

    END {
    # 打印最后一个进程号的错误信息
    printErrorData(prev_process_id)
    }

    # 获取格式化的日期和时间
    function getFormattedDateTime(month, day, time) {
    month_number = (index("JanFebMarAprMayJunJulAugSepOctNovDec", month) + 2) / 3
    return year"-"month_number"-"day" "time
    }

    # 获取并解析进程名和进程id
    function parseProcessInfo(process_data, arr) {
    split(process_data, arr, "[")
    if (length(arr) > 1) {
    gsub("]:", "", arr[2])
    process_info[1] = arr[1]
    process_info[2] = arr[2]
    } else {
    process_info[1] = process_data
    process_info[2] = ""
    }
    }

    # 解析错误信息
    function parseErrorInfo(error_column) {
    has_colon = (substr(error_column, length(error_column), 1) == ":")
    return has_colon ? "**" error_column "**" : error_column
    }

    # 打印错误信息
    function printErrorData(process_id) {
    if (process_id != "") {
    # 输出标识为1的错误信息
    print error_array[1] >> log_file

    # 检查是否有标识为0的错误信息
    if (error_array[0] != "") {
    # 输出空行
    print "" >> log_file
    # 输出标识为0的错误信息
    print error_array[0] >> log_file
    }
    }
    }

    ' auth.log
  • 运行命令如下:

1
2
3
4
5
# 给脚本赋予执行权限
chmod u+x count.sh

# 运行脚本
sudo ./count.sh
]]>
+ JMeter

参考:JMeter 快速上手 | 白月黑羽 (byhy.net)

JMeter 这个软件工具 主要用于服务端系统的性能测试

比如 测试 web网站,API服务器 的性能,如下图所示

1690955200926

1. 安装 JMeter

前提要有 JDK8/JDK11

官网:Apache JMeter - Download Apache JMeter

1690955571823

  • 下载后解压放在 D盘即可

1690955752866

2. 打开

  • 进入 bin 目录下,双击 jmeter.bat 文件,就会出现如下界面:

1690955889212

  • 修改系统背景和字体大小还有语言,可以点击 option

1690956774162

3. 界面简介

性能测试是:验证在各种性能负载场景下 ,系统的表现是否符合预期

模拟大量用户访问被测系统?

一个性能测试场景, JMeter称之为 test plan ,中文翻译为 测试计划 。实际上,一个 JMeter的测试计划对应性能测试的一个场景,其实也就是一个测试用例应该先准备好性能测试用例, 然后再用JMeter对应配置好一个测试计划来对应它。每个测试计划都可以保存在一个单独的文件中。

JMeter的测试计划,具体里面的内容(怎么测),根据你设计好的测试用例,在这个下面添加相应子节点定义。JMeter 把每个节点称之为 element ,翻译为中文叫 元件 ,或者 元素

首先,必须定义用户访问的行为。 就是用户 先访问哪个网页,发出哪些HTTP请求,再访问哪个网页,发出哪些HTTP请求。 根据用例,往往需要模拟 多种 用户行为, 每种行为的用户有多少个。JMeter 把一个用户的行为 用一个线程来执行。JMeter 把同一种用户的行为和数量, 用线程组来定义,代表有一批用户,都是这样的行为

4. 测试计划和线程组

例:

比如,现在我们要做的一个性能测试用例 需要模拟的行为 如下:

第1种行为(用户数量 5个):

先访问首页,再访问 单课页 ,再访问 新闻页

第2种行为(用户数量 10个):

先访问首页,再访问 专题页 ,再访问 新闻页

我们就要定义2个线程组,对应这两种行为的用户。

1690957715900

  • 线程组中:

    • Ramp-Up 时间 意思是 所有用户上线的总时间,以秒为单位。
    • 比如5,就表示总共耗时5秒,如果是 5个用户,那就意味着 每隔1.25秒上线一个: 5秒/(5-1) = 1.25
    • 循环次数 意思是 每个用户 做线程组里面定义的动作行为 多少轮 。 缺省就做一轮。

    1690958054931

  • 接下来 这个线程组里面 的 行为定义,也就是这类用户 先访问哪个网页,发出哪些HTTP请求,再访问哪个网页,发出哪些HTTP请求, 这个行为怎么定义呢?

    • 一个具体的请求,是通过 Sampler 取样器 来 定义的。
    • 通常最常用的就是这个 HTTP请求取样器。

    1690958170476

  • 定义设置好以后,下面就是要运行看看。

    • 运行前,通常我们要保存这个测试计划
    • 先选择计划节点,再选择工具栏的 保存 按钮,自己选择合适的保存位置

    1690958691364

5. 调试运行

接下来就可以运行一下看看。

注意,运行JMeter 有2种运行模式: GUI 图形界面模式CLI 命令行模式,前者是开发调试用的,后者才是真正执行压力测试时用的,现在就是开发阶段,当然先使用 图形界面模式,等调试没有问题,再使用命令行模式

所以图形界面模式,我们要查看结果,就是加 Listener 监听器

最常见的就是 ViewResultTree查看结果树 ,里面会显示HTTP请求具体的细节的信息。

注意右键根节点

1690959259494

结果树 的 Sample result 页 参数的意义: (若要删除记录,左侧栏右键查看结果树,点击清除即可)

1690959893695

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Connect Time : 
jmeter 和 被测系统 建立 TCP 连接的时间,包括3路握手时间,
如果连接复用, 值为0

lantency:
从发出请求前 到 接收完第一个响应的时间

loadtime(以前叫Elapsed time):
从发出请求前 到 接收完所有响应的时间
如果是长消息, 往往时长 >= lantency,因为有多个响应

Size in bytes : 整个消息消息大小 = Headers size in bytes + Body size in bytes
Headers size in bytes : 响应消息头大小
Body size in bytes : 响应消息体大小

6. HTTP 请求默认值配置

测试过程中,被测系统换了, 就要换配置的地址, 要手动修改 请求参数,请求取样器多了, 就非常麻烦了。可以使用HTTP请求默认值 解决这个问题。

那么如果具体在写测试行为时,如果没写,就默认使用http请求默认值里配置的信息,比如请求的ip啥的

1690960289999


这边自己试了下,直接测匹配的接口,除了 post 发送时需要在请求头加上点东西,其他没啥,亲测可用,所以接下来看下报告如何打印应该就行,,

1690962876252


7. 模拟间隔时间

补充一下,点右键可以禁用某个用户行为

场景1中两组请求中间没有间隔,不符合实际情况。

怎么让它们有间隔呢?

  • 可以使用 JMeter的 定时器 Timer,有很多种,这里选择 固定的试下

    • 但是注意:定时器 执行优先级高于 取样器, 会先暂停, 可以放在下一个消息的前面
    • 也就是这个定时器你放一个,就会对它同目录下的所有都起效果,所以慎重使用

    1690963423636

  • 也可以使用 取样器 里面的 测试活动 flow control action 取样器

    1690963791158

    这个就是正常等

    1690963852512

8. 执行压力测试-命令行模式

真正实施性能测试应该在命令行模式下运行(而不是我们看到的 GUI 界面),

在 .jmx 文件所在位置打开 cmd 命令行,输入命令格式如下:

1
D:\JMeter\apache-jmeter-5.6.2\bin\jmeter -n -t parktest-case1.jmx -l log.jtl

-n 表示命令行

-t 指定执行的测试计划 保存的 .jmx 文件

-l 最终生成的测试日志

注意JMeter的路径替换为你的安装路径

假设 我们有如下的性能测试用例,如下是简化的测试用例,实际工作的会很复杂

注意,一个线程代表一个用户

如果要测试系统的并发连接数应该如何测试?

1
2
3
4
5
6
只有一种用户行为:

无需登录,先访问首页,再访问 单课页 ,再访问 新闻页
访问页面间隔 10 秒

用户数量 1200 个,在10分钟依次上线
  • 这就已经开始进行压测了,由于设置的是 600 秒,等十分钟之后就行

    1690965288695

  • 结束

    1690965713070

9. dashboard 产生图表

1
D:\JMeter\apache-jmeter-5.6.2\bin\jmeter -g log.jtl -o report1

就会产生report1目录,里面的index.html 打开就是报告

注意 -o 后面的目录 一定要不存在,或者内容为空,否则会报错。

执行命令后进入 report1 目录下,点击里面的 index.html 文件,就可以看到如下页面:

1690965891290

1690966360901

]]>
- Linux + Jmeter - awk + Jmeter @@ -54,23 +54,23 @@ - Jmeter 压测 - - /2023/11/18/Jmeter/ + Fabric 入门 + + /2023/11/18/Fabric%E5%85%A5%E9%97%A8/ - JMeter

参考:JMeter 快速上手 | 白月黑羽 (byhy.net)

JMeter 这个软件工具 主要用于服务端系统的性能测试

比如 测试 web网站,API服务器 的性能,如下图所示

1690955200926

1. 安装 JMeter

前提要有 JDK8/JDK11

官网:Apache JMeter - Download Apache JMeter

1690955571823

  • 下载后解压放在 D盘即可

1690955752866

2. 打开

  • 进入 bin 目录下,双击 jmeter.bat 文件,就会出现如下界面:

1690955889212

  • 修改系统背景和字体大小还有语言,可以点击 option

1690956774162

3. 界面简介

性能测试是:验证在各种性能负载场景下 ,系统的表现是否符合预期

模拟大量用户访问被测系统?

一个性能测试场景, JMeter称之为 test plan ,中文翻译为 测试计划 。实际上,一个 JMeter的测试计划对应性能测试的一个场景,其实也就是一个测试用例应该先准备好性能测试用例, 然后再用JMeter对应配置好一个测试计划来对应它。每个测试计划都可以保存在一个单独的文件中。

JMeter的测试计划,具体里面的内容(怎么测),根据你设计好的测试用例,在这个下面添加相应子节点定义。JMeter 把每个节点称之为 element ,翻译为中文叫 元件 ,或者 元素

首先,必须定义用户访问的行为。 就是用户 先访问哪个网页,发出哪些HTTP请求,再访问哪个网页,发出哪些HTTP请求。 根据用例,往往需要模拟 多种 用户行为, 每种行为的用户有多少个。JMeter 把一个用户的行为 用一个线程来执行。JMeter 把同一种用户的行为和数量, 用线程组来定义,代表有一批用户,都是这样的行为

4. 测试计划和线程组

例:

比如,现在我们要做的一个性能测试用例 需要模拟的行为 如下:

第1种行为(用户数量 5个):

先访问首页,再访问 单课页 ,再访问 新闻页

第2种行为(用户数量 10个):

先访问首页,再访问 专题页 ,再访问 新闻页

我们就要定义2个线程组,对应这两种行为的用户。

1690957715900

  • 线程组中:

    • Ramp-Up 时间 意思是 所有用户上线的总时间,以秒为单位。
    • 比如5,就表示总共耗时5秒,如果是 5个用户,那就意味着 每隔1.25秒上线一个: 5秒/(5-1) = 1.25
    • 循环次数 意思是 每个用户 做线程组里面定义的动作行为 多少轮 。 缺省就做一轮。

    1690958054931

  • 接下来 这个线程组里面 的 行为定义,也就是这类用户 先访问哪个网页,发出哪些HTTP请求,再访问哪个网页,发出哪些HTTP请求, 这个行为怎么定义呢?

    • 一个具体的请求,是通过 Sampler 取样器 来 定义的。
    • 通常最常用的就是这个 HTTP请求取样器。

    1690958170476

  • 定义设置好以后,下面就是要运行看看。

    • 运行前,通常我们要保存这个测试计划
    • 先选择计划节点,再选择工具栏的 保存 按钮,自己选择合适的保存位置

    1690958691364

5. 调试运行

接下来就可以运行一下看看。

注意,运行JMeter 有2种运行模式: GUI 图形界面模式CLI 命令行模式,前者是开发调试用的,后者才是真正执行压力测试时用的,现在就是开发阶段,当然先使用 图形界面模式,等调试没有问题,再使用命令行模式

所以图形界面模式,我们要查看结果,就是加 Listener 监听器

最常见的就是 ViewResultTree查看结果树 ,里面会显示HTTP请求具体的细节的信息。

注意右键根节点

1690959259494

结果树 的 Sample result 页 参数的意义: (若要删除记录,左侧栏右键查看结果树,点击清除即可)

1690959893695

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Connect Time : 
jmeter 和 被测系统 建立 TCP 连接的时间,包括3路握手时间,
如果连接复用, 值为0

lantency:
从发出请求前 到 接收完第一个响应的时间

loadtime(以前叫Elapsed time):
从发出请求前 到 接收完所有响应的时间
如果是长消息, 往往时长 >= lantency,因为有多个响应

Size in bytes : 整个消息消息大小 = Headers size in bytes + Body size in bytes
Headers size in bytes : 响应消息头大小
Body size in bytes : 响应消息体大小

6. HTTP 请求默认值配置

测试过程中,被测系统换了, 就要换配置的地址, 要手动修改 请求参数,请求取样器多了, 就非常麻烦了。可以使用HTTP请求默认值 解决这个问题。

那么如果具体在写测试行为时,如果没写,就默认使用http请求默认值里配置的信息,比如请求的ip啥的

1690960289999


这边自己试了下,直接测匹配的接口,除了 post 发送时需要在请求头加上点东西,其他没啥,亲测可用,所以接下来看下报告如何打印应该就行,,

1690962876252


7. 模拟间隔时间

补充一下,点右键可以禁用某个用户行为

场景1中两组请求中间没有间隔,不符合实际情况。

怎么让它们有间隔呢?

  • 可以使用 JMeter的 定时器 Timer,有很多种,这里选择 固定的试下

    • 但是注意:定时器 执行优先级高于 取样器, 会先暂停, 可以放在下一个消息的前面
    • 也就是这个定时器你放一个,就会对它同目录下的所有都起效果,所以慎重使用

    1690963423636

  • 也可以使用 取样器 里面的 测试活动 flow control action 取样器

    1690963791158

    这个就是正常等

    1690963852512

8. 执行压力测试-命令行模式

真正实施性能测试应该在命令行模式下运行(而不是我们看到的 GUI 界面),

在 .jmx 文件所在位置打开 cmd 命令行,输入命令格式如下:

1
D:\JMeter\apache-jmeter-5.6.2\bin\jmeter -n -t parktest-case1.jmx -l log.jtl

-n 表示命令行

-t 指定执行的测试计划 保存的 .jmx 文件

-l 最终生成的测试日志

注意JMeter的路径替换为你的安装路径

假设 我们有如下的性能测试用例,如下是简化的测试用例,实际工作的会很复杂

注意,一个线程代表一个用户

如果要测试系统的并发连接数应该如何测试?

1
2
3
4
5
6
只有一种用户行为:

无需登录,先访问首页,再访问 单课页 ,再访问 新闻页
访问页面间隔 10 秒

用户数量 1200 个,在10分钟依次上线
  • 这就已经开始进行压测了,由于设置的是 600 秒,等十分钟之后就行

    1690965288695

  • 结束

    1690965713070

9. dashboard 产生图表

1
D:\JMeter\apache-jmeter-5.6.2\bin\jmeter -g log.jtl -o report1

就会产生report1目录,里面的index.html 打开就是报告

注意 -o 后面的目录 一定要不存在,或者内容为空,否则会报错。

执行命令后进入 report1 目录下,点击里面的 index.html 文件,就可以看到如下页面:

1690965891290

1690966360901

]]>
+ Fabric

官方文档:关键概念 — hyperledger-fabricdocs master 文档

1. 关键概念

1.1 区块链网络

1681270713030

这个 Fabric 区块链网络包括了两个应用程序通道以及一个排序通道

组织 R1 和 R4 负责排序通道,R1 和 R2 负责蓝色的应用程序通道,R2 和 R3 负责红色的应用程序通道。

客户端应用程序 A1 是组织 R1 的元素,CA1 是它的证书颁发机构

组织 R2 的节点 P2 可以使用蓝色的通信设施,也可以使用红色的应用程序通道。

每个应用程序通道具有它自己的通道配置,这里是 CC1 和 CC2。

系统通道的通道配置是网络配置 NC4 的一部分。

一个有四个组织的网络,带有两个通道和三个 Peer 节点,两个智能合约和一个排序服务。

并由四个证书颁发机构来支撑。

它为三个客户端应用程序提供了账本及智能合约服务,这些应用程序可以通过两个通道与账本和智能合约进行交互。

1681271030975

1681270977420

1.2 身份

  • 确定了对资源的确切权限以及对参与者在区块链网络中拥有的信息的访问权限
  • 要使身份可以被验证,它必须来自可信任的权威机构
  • 成员服务提供者(Membership Service Provider,MSP)是 Fabirc 中可以信任的权威机构
  • MSP 将可验证的身份转变为区块链网络的成员

1.2.1 PKI

  • 公钥基础结构(PKI)是一组互联网技术,可在网络中提供安全通信
  • 关键要素
    • 数字证书
    • 公钥和私钥
    • 证书授权中心
    • 证书撤销列表

1.3 成员服务提供者 (MSP)

以太坊属于匿名网络,hpyerledger 是实名制的网络

1.4 Peer 节点

区块链网络主要由 Peer 节点(或者简单称之为 Peer)组成

1681285693747区块链网络是由 Peer 节点组成的,每个节点都保存着账本和智能合约的副本。

在这个例子中,网络 N 是由节点 P1、P2 和 P3 组成的,每个节点都维护这他们自己的分布式账本 L1。P1、P2 和 P3 使用相同的链码 S1 来访问他们的分布式账本的副本。

在 Fabric 中,链码等同于智能合约,因为它们是使用一个被称为链码的技术概念来实现智能合约

Peer 节点是账本及链码的宿主,应用程序及管理员如果想要访问这些资源,他们必须要和 Peer 节点进行交互

1.4.1 多账本

  • 一个 Peer 节点可以维护多个账本,并且每个账本具有零个或者多个链码使用账本

1681285960829

在这个例子中,我们能够看到 Peer 节点 P1 维护着账本 L1 和 L2。账本 L1 通过链码 S1 来访问。账本 2 通过链码 S1 和 S2 访问

1.4.2 多链码

  • 账本数量和访问账本的链码的数量之间没有固定的关系。一个 Peer 节点可能会有很多链码和账本

1.4.3 应用程序和 Peer 节点

1681287048342

1.4.4 Peer 节点和排序节点

  • 一个更新的交易和一个查询的交易区别很大,因为一个单独的 Peer 节点不能够由它自己来更新账本——更新需要网络中其他节点的同意

  • 在一个账本的更新被应用到 Peer 节点的本地账本之前, Peer 节点会请求网络中的其他 Peer 节点来批准这次更新。这个过程被称为共识,这会比一个简单的查询花费更长的时间来完成

  • 想要更新账本的应用程序会被引入到一个三阶段的流程,这确保了在一个区块链网络中所有的 Peer 节点都彼此保持着一致的账本。

    • 在第一个阶段,应用程序会跟背书节点的子集一起工作,其中的每个节点都会向应用程序为提案的账本更新提供背书,但是不会将提案的更新应用到他们的账本副本上。
    • 在第二个阶段,这些分散的背书会被搜集到一起当做交易被打包进区块中。
    • 在最后一个阶段,这些区块会被分发回每个 Peer 节点,在这些 Peer 节点上每笔交易在被应用到 Peer 节点的账本副本之前会被验证。

    排序节点在这个流程中处于中心地位

1.5 chaincode 智能合约

  • 智能合约用可执行的代码定义了不同组织之间的规则。应用程序调用智能合约来生成被记录到账本上的交易。

1681294843518

在上图中,我们可以看到组织 ORG1 和 ORG2 是如何通过定义一个 car 智能合约来实现 查询转移 和 更新 汽车的。来自这些组织的应用程序调用此智能合约执行业务流程中已商定的步骤,例如将特定汽车的所有权从 ORG1 转移到 ORG2

可以将智能合约看成交易的管理者,而链码则管理着如何将智能合约打包以便用于部署。

一个智能合约定义在一个链码中。而多个智能合约也可以定义在同一个链码中。当一个链码部署完毕,该链码中的所有智能合约都可供应用程序使用。

  • 智能合约的核心是一组 交易 定义。例如,在 fabcar.js中,你可以看到一个创建了一辆新车的智能合约交易:
1
2
3
4
5
6
7
8
9
10
11
12
async createCar(ctx, carNumber, make, model, color, owner) {

const car = {
color,
docType: 'car',
make,
model,
owner,
};

await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car)));
}

1.5.1 背书

  • 每个链码都有一个背书策略与之相关联,该背书策略适用于此链码中定义的所有智能合约。背书策略非常重要,它指明了区块链网络中哪些组织必须对一个给定的智能合约所生成的交易进行签名,以此来宣布该交易有效
    • 一个示例背书策略可能这样定义:参与区块链网络的四个组织中有三个必须在交易被认为有效之前签署该交易。所有的交易,无论是有效的还是无效的,都会被添加到分布式账本中,但只有有效交易会更新世界状态。
    • 背书策略是 Hyperledger Fabric 与以太坊(Ethereum)或比特币(Bitcoin)等其他区块链的区别所在

1.5.2 有效交易

注意,在执行智能合约时世界状态没有更新

  • 所有的交易都有一个识别符、一个提案和一个被一群组织签名的响应。所有交易,无论是否有效,都会被记录在区块链上,但仅有效交易会更新世界状态

  • 有效交易举例:

    1681296263304

    检查 车辆转移 交易。您可以看到 ORG1 和 ORG2 之间为转移一辆车而进行的交易 t3。看一下交易是如何通过输入 {CAR1,ORG1,ORG2} 和输出 {CAR1.owner=ORG1,CAR1.owner=ORG2} 来表示汽车的所有者从 ORG1 变为了 ORG2。注意输入是如何由应用程序的组织 ORG1 签名的,输出是如何由背书策略标识的两个组织( ORG1 和 ORG2 )签名的。这些签名是使用每个参与者的私钥生成的,这意味着网络中的任何人都可以验证网络中的所有参与者是否在交易细节上达成了一致

  • 一项交易被分发给网络中的所有节点,各节点通过两个阶段对其进行验证。首先,根据背书策略检查交易,确保该交易已被足够的组织签署。其次,继续检查交易,以确保当该交易在受到背书节点签名时它的交易读集与世界状态的当前值匹配,并且中间过程中没有被更新。如果一个交易通过了这两个测试,它就被标记为有效。所有交易,不管是有效的还是无效的,都会被添加到区块链历史中,但是仅有效的交易才会更新世界状态。

1.5.3 通道

  • Hyperledger Fabric 允许一个组织利用通道同时参与多个、彼此独立的区块链网络。通过加入多个通道,一个组织可以参与一个所谓的网络的网络
  • 通道在一群组织之间提供了一种完全独立的通信机制。当链码定义被提交到通道上时,该通道上所有的应用程序都可以使用此链码中的智能合约。

1681296849809

在上面的示例中,car 智能合约被定义在 VEHICLE 通道上,insurance 智能合约被定义在 INSURANCE 通道上。car 的链码定义明确了以下背书策略:任何交易在被认定为有效之前必须由 ORG1 和 ORG2 共同签名。insurance 智能合约的链码定义明确了只需要 ORG3 对交易进行背书即可。ORG1 参与了 VEHICLE 通道和 INSURANCE 通道这两个网络,并且能够跨网络协调与 ORG2 和 ORG3 的活动。

1.6 账本

  • 由“世界状态“和”区块链“这两部分组成
    • 世界状态是一个数据库,它存储了一组账本状态的当前值,通过世界状态,程序可以直接访问一个账本状态的当前值,不需要遍历整个交易日志来计算当前值
    • 区块链是交易日志,它记录了促成当前世界状态的所有改变

1681301669101

账本 L 由区块链 B 和世界状态 W 组成,其中世界状态 W 由区块链 B 决定。我们也可以说世界状态 W 是源自区块链 B

1.6.1 世界状态

  • 世界状态被作为数据库来实现

1681301818989

示例展示的是 CAR1 和 CAR2 这两辆车的账本状态,二者都各有一个值和一个键。应用程序可以调用智能合约,该合约使用简单的账本 API 来获取写入删除状态。注意状态值可以是简单值(Audi…),也可以是复合值(type:BMW…)。经常会通过查询世界状态来检索具有某些特定属性的对象,例如查找所有红色宝马汽车。

  • 应用程序提交那些会更改世界状态的交易,这些交易最终被提交到账本区块链上。
  • 应用程序无法看到 Hyperledger Fabric SDK(软件开发工具包)设定的共识机制的细节内容,它们能做的只是调用智能合约以及在交易被收进区块链时收到通知(所有被提交的交易,无论有效与否,都会被收进区块链),但是只有那些受到相关背书组织签名的交易才会更新世界状态。如果一个交易没有得到足够背书节点的签名,那么它不会更新世界状态
  • 每个状态都有一个版本号,版本号是供 Hyperledger Fabric 内部使用的,并且每次状态更改时版本号会发生递增。每当更新状态时,都会检查该状态的版本,以确保当前状态与背书时的版本相匹配。

1.6.2 区块链

  • 世界状态存储了与业务对象当前状态相关的事实信息
  • 区块链是一种历史记录,它记录了这些业务对象是如何到达各自当前状态的相关事实。区块链记录了每个账本状态之前的所有版本以及状态是如何被更改的
  • 区块链的结构是一群相互链接的区块的序列化日志,其中每个区块都包含一系列交易,各项交易代表了一个对世界状态进行的查询或更新操作
  • ==区块链总是以文件实现,而与之相反的是,世界状态以数据库实现==

1681302297465

B0 是该区块链的第一个区块,也叫创世区块

它并不包含任何用户交易,但却是账本的起始点

相反的,创世区块包含了一个配置交易,该交易含有网络配置(未显示)的初始状态

1.6.3 区块

组成

  • 区块头

    • 区块编号:编号从0(初始区块)开始,每在区块链上增加一个新区块,编号的数字都会加1。
    • 当前区块的哈希值:当前区块中包含的所有交易的哈希值。
    • 前一个区块头的哈希值:区块链中前一个区块头的哈希值。

    1681302542402

    区块头详情:区块 B2 的区块头 H2 包含了区块编号 2,当前区块数据 D2 的哈希值 CH2,以及前一个区块头 H1 的哈希值。

  • 区块数据

    • 这部分包含了一个有序的交易列表。区块数据是在排序服务创建区块时被写入的
  • 区块元数据

    • 这个部分包含了区块被写入的时间,还有区块写入者的证书、公钥以及签名。随后,区块的提交者也会为每一笔交易添加一个有效或无效的标记,但由于这一信息与区块同时产生,所以它不会被包含在哈希中。

1.6.4 交易

  • 交易记录了世界状态发生的更新
  • 把交易包含在区块中的区块数据结构

1681303391932

交易详情:交易 T4 位于区块 B1 的区块数据 D1 中,T4包括的内容如下:交易头 H4,一个交易签名 S4,一个交易提案 P4,一个交易响应 R4 和一系列背书 E4。

这部分用 H4 表示,它记录了关于交易的一些重要元数据,比如,相关链码的名字以及版本。

签名

这部分用 S4 表示,它包含了一个由客户端应用程序创建的加密签名。该字段是用来检查交易细节是否未经篡改,因为交易签名的生成需要用到应用程序的私钥。

提案

这部分用 P4 表示,它负责对应用程序供给智能合约的输入参数进行编码,随后该智能合约生成提案账本更新。在智能合约运行时,这个提案提供了一套输入参数,这些参数同当前的世界状态一起决定了新的账本世界状态。

响应

这部分用 R4 表示,它是以读写集 (RW-set)的形式记录下世界状态之前和之后的值。交易响应是智能合约的输出,如果交易验证成功,那么该交易会被应用到账本上,从而更新世界状态。

背书

  • 示例账本

    1681304156885

    账本 L包含了一个世界状态 W 和一个区块链 B。其中 W 包含了四个状态,各状态的键分别是:CAR0,CAR1,CAR2 和 CAR3 。而 B 包含了两个区块 0和 1。区块1包含了四笔交易:T1,T2,T3,T4

1.7 排序服务

  • Hyperledger Fabric 的工作方式不同。它有一种称为排序节点的节点使交易有序,并与其他排序节点一起形成一个排序服务。因为 Fabric 的设计依赖于确定性的共识算法,所以 Peer 节点所验证的区块都是最终的和正确的。账本不会像其他分布式的以及无需许可的区块链中那样产生分叉
  • 排序节点还维护着允许创建通道的组织列表。此组织列表称为“联盟”
  • 排序节点还对通道执行基本访问控制,限制谁可以读写数据,以及谁可以配置数据

1.7.1 排序节点和交易流程

更新账本的应用程序涉及到三个阶段,该过程确保区块链网络中的所有节点保持它们的账本彼此一致。

在第一阶段,客户端应用程序将交易提案发送给一组节点,这些节点将调用智能合约来生成一个账本更新提案,然后背书该结果。背书节点此时不将提案中的更新应用于其账本副本。相反,背书节点将向客户端应用程序返回一个提案响应。

已背书的交易提案最终将在第二阶段经过排序生成区块,

然后在第三阶段分发给所有节点进行最终验证和提交。

  • 将交易排序并打包到区块中
    • 在此阶段,应用程序客户端把包含已背书交易提案响应的交易提交到排序服务节点。排序服务创建交易区块,这些交易区块最终将分发给通道上的所有 Peer 节点,以便在第三阶段进行最终验证和提交。
    • 排序服务节点的工作是将提交的交易按定义好的顺序安排成批次,并将它们打包成区块。这些区块将成为区块链的区块

1681360371573

排序节点的第一个角色是打包提案的账本更新。在本例中,应用程序 A1 向排序节点 O1 发送由 E1 和 E2 背书的交易 T1。同时,应用程序 A2 将 E1 背书的交易 T2 发送给排序节点 O1。O1 将来自应用程序 A1 的交易 T1 和来自应用程序 A2 的交易 T2 以及来自网络中其他应用程序的交易打包到区块 B2 中。我们可以看到,在 B2 中,交易顺序是 T1、T2、T3、T4、T6、T5,但这可能不是这些交易到达排序节点的顺序!(这个例子显示了一个非常简单的排序服务配置,只有一个排序节点。)

  • 验证和提交

1681360785332

排序节点的第二个角色是将区块分发给 Peer 节点。在本例中,排序节点 O1 将区块 B2 分配给节点 P1 和 P2。节点 P1 处理区块 B2,在 P1 上的账本 L1 中添加一个新区块。同时,节点 P2 处理区块 B2,从而将一个新区块添加到 P2 上的账本 L1中。一旦这个过程完成,节点 P1 和 P2 上的账本 L1 就会保持一致的更新,并且每个节点都可以通知与之连接的应用程序交易已经被处理。

1.7.2 排序服务的实现

  • 推荐 Raft
    • Raft 是一种基于 etcd中 Raft 协议实现的崩溃容错(Crash Fault Tolerant,CFT)排序服务。Raft 遵循“领导者跟随者”模型,这个模型中,在每个通道上选举领导者节点,其决策被跟随者复制。Raft 排序服务会比基于 Kafka 的排序服务更容易设置和管理,它的设计允许不同的组织为分布式排序服务贡献节点。
    • 节点总是处于以下三种状态之一:跟随者、候选人或领导者。所有节点最初都是作为跟随者开始的。在这种状态下,他们可以接受来自领导者的日志条目(如果其中一个已经当选),或者为领导者投票。如果在一段时间内没有接收到日志条目或心跳(例如,5秒),节点将自己提升到候选状态。在候选状态中,节点从其他节点请求选票。如果候选人获得法定人数的选票,那么他就被提升为领导者。领导者必须接受新的日志条目并将其复制到跟随者。

2. 入门

2.1 配置环境

此处参考b站视频,虽然可能不全,并且后半段实现不了

但是前面的跟下来都还不错!但是要注意自己的版本问题!

从0开始快速安装Hyperledger Fabric_哔哩哔哩_bilibili

2.1.1 ubuntu 联网

1
2
3
sudo systemctl restart NetworkManager.service
sudo apt-get install network-manager
sudo systemctl restart NetworkManager.service

1681387612411

2.1.2 apt 换源

1
2
3
https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/
sudo gedit /etc/apt/sources.list
sudo apt update

2.1.3 安装 docker

1
2
3
4
// 安装docker、docker-compose
sudo apt install docker docker-compose
sudo systemctl enable docker
sudo usermod -a -G docker <username>

2.1.4 安装 golang

1
2
3
4
5
6
7
8
// 安装golang
https://go.dev/doc/install
sudo su
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.6.linux-amd64.tar.gz
gedit /etc/profile
export PATH=$PATH:/usr/local/go/bin
gedit ~/.bashrc
source /etc/profile

2.1.5 docker 加速器

1
2
// docker加速器
https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

2.1.6 安装fabric-sample

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、手动创建脚本,安装samples、docker
https://github.com/hyperledger/fabric/blob/main/scripts/bootstrap.sh
修改binaries=false
sudo chmod u+x bootstrap.sh
./bootstrap.sh

2、安装binaries
https://github.com/hyperledger/fabric/releases/download/v2.4.1/hyperledger-fabric-linux-amd64-2.4.1.tar.gz
https://github.com/hyperledger/fabric-ca/releases/download/v1.5.2/hyperledger-fabric-ca-linux-amd64-1.5.2.tar.gz
tar -xzvf 压缩包名 -C 目的地

3、配置go代理
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

3. 开发

3.1 分析

3.1.1 商业票据

  • 票据 00001 是 5 月 31 号由 MagnetoCorp 发行的。该票据的第一个状态,它具有不同的属性和值:

    1
    2
    3
    4
    5
    6
    7
    Issuer = MagnetoCorp
    Paper = 00001
    Owner = MagnetoCorp
    Issue date = 31 May 2020
    Maturity = 30 November 2020
    Face value = 5M USD
    Current state = issued

    票据的状态是 发行 交易的结果,它使得 MagnetoCorp 公司的第一张商业票据面世!注意该票据在今年晚些时候如何兑换面值 500 万美元。当票据 00001 发行后 Issuer 和 Owner 具有相同的值。该票据有唯一标识 MagnetoCorp00001——它是 Issuer 属性和 Paper 属性的组合。最后,属性 Current state = issued 快速识别了 MagnetoCorp 票据 00001 在它生命周期中的阶段。

  • 发行后不久,该票据被 DigiBank 购买。由于购买交易,同一个商业票据如何发生变化:

    1
    2
    3
    4
    5
    6
    7
    Issuer = MagnetoCorp
    Paper = 00001
    Owner = DigiBank
    Issue date = 31 May 2020
    Maturity date = 30 November 2020
    Face value = 5M USD
    Current state = trading

    最重要的变化是 Owner 的改变——票据初始拥有者是 MagnetoCorp 而现在是 DigiBank。我们可以想象该票据后来如何被出售给 BrokerHouse 或 HedgeMatic,以及相应的变更为相应的 Owner。注意 Current state 允许我们轻松的识别该票据目前状态是 trading

  • 6 个月后,如果 DigiBank 仍然持有商业票据,它就可以从 MagnetoCorp 那里兑换:

    1
    2
    3
    4
    5
    6
    7
    Issuer = MagnetoCorp
    Paper = 00001
    Owner = MagnetoCorp
    Issue date = 31 May 2020
    Maturity date = 30 November 2020
    Face value = 5M USD
    Current state = redeemed

    最终的兑换交易结束了这个商业票据的生命周期——它可以被认为票据已经终止。通常必须保留已兑换的商业票据的记录,并且 redeemed 状态允许我们快速识别这些。通过将 Owner 跟交易创建者的身份进行比较,一个票据的 Owner 值可以被用来在兑换交易上进行访问控制

3.1.2 交易

  • 我们已经看到票据 00001 的生命周期相对简单——由于发行购买兑换交易,它在 issuedtrading 和 redeemed 状态之间转移

  • 注意交易和票据不同!!

    • 发行交易
    1
    2
    3
    4
    5
    6
    Txn = issue
    Issuer = MagnetoCorp
    Paper = 00001
    Issue time = 31 May 2020 09:00:00 EST
    Maturity date = 30 November 2020
    Face value = 5M USD
    • 购买交易
    1
    2
    3
    4
    5
    6
    7
    Txn = buy
    Issuer = MagnetoCorp
    Paper = 00001
    Current owner = MagnetoCorp
    New owner = DigiBank
    Purchase time = 31 May 2020 10:00:00 EST
    Price = 4.94M USD
    • 兑换交易
    1
    2
    3
    4
    5
    Txn = redeem
    Issuer = MagnetoCorp
    Paper = 00001
    Current owner = HedgeMatic
    Redeem time = 30 Nov 2020 12:00:00 EST

3.2 流程和数据设计

3.2.1 生命周期

  • 在处理商业票据时有两个重要的概念:状态交易

  • 对状态和交易的有效分析是成功实施的重要起点,可以用状态转移表来表示商业票据的生命周期:

    1681607417562

    商业票据的状态转移表。商业票据通过发行购买兑换交易在已发行交易中已兑换之间进行状态转移。

3.2.2 账本状态

  • 商业票据的结构:

    1681607716814

    商业票据可以被表示为属性集,每个属性都对应一个值。通常,这些属性的组合会为每个票据提供一个唯一键

    结合来看,属性的完整集合构成了商业票据的状态。此外,这些商业票据的全部集合构成了账本的世界状态。

  • 查看 MagnetoCorp 的票据 00001 如何表示为一个状态向量,根据不同的交易刺激进行转换:

    1681607900927

    注意每个独立的票据都起于空状态,技术上被称作 nil,来表示票据不存在!通过发行交易,票据 00001 问世,然后由于购买兑换交易而更新状态

3.2.3 状态键值

  • 大多数的实际应用中,状态会有一个属性组合在给定的上下文中唯一识别它——它就是主键
  • PaperNet 商业票据的主键是通过 Issuer 属性和 paper 属性拼接得到的,所以 MagnetoCorp 的第一个票据的主键就是 MagnetoCorp00001
  • Fabric 需要账本中的每个状态都有唯一的主键
  • 当唯一主键在可用的属性集中不能获得,应用决定的唯一键会被指定为交易的输入来创建状态。这个唯一键的形式一般是 UUID

3.2.3 逻辑表示

为了满足不同类型的查询任务,把所有相关的商业票据按逻辑顺序排列在一起是很有帮助的。PaperNet 的设计包含了商业票据列表的思想——一个逻辑容器,每当商业票据发行或发生其他更改时,该容器都会更新

  • 把所有的 PaperNet 商业票据放在一个商业票据列表中是有帮助的:

    1681608349698

    新票据由于发行交易被加入到列表中,然后列表中已存在的票据因为购买交易和兑换交易可以被更新状态。列表有一个描述性的名称:org.papernet.papers;使用这种DNS 名真的是一个好主意,因为适当的名称会让你的区块链设计对其他人来说是直观的。这种想法同样也适用于智能合约的名字。

3.2.4 物理表现

  • 我们可以正确地想到 PaperNet 中的单个票据列表—— org.papernet.papers ——列表最好作为一组单独的 Fabric 状态来实现,其复合键将状态与其列表关联起来。这样,每个状态的复合键都是惟一的,并支持有效的列表查询

    1681608524162

  • 状态向量的物理设计对于优化性能和行为非常重要。保持状态的独立!

3.3 智能合约处理

 连接到网络的所有应用程序必须使用相同版本的智能合约,以便它们共同实现相同的共享业务流程和数据。

在 Java 中,类必须使用 @Contract(...) 标注进行包装。它支持额外的智能合约信息,比如许可和作者。 @Default() 标注表明该智能合约是默认合约类。在智能合约中标记默认合约类在一些有多个合约类的智能合约中会很有用。

fabric-samples/CommercialPaperContract.java at master · hyperledger/fabric-samples (github.com)

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/*
SPDX-License-Identifier: Apache-2.0
*/
package org.example;

import java.util.logging.Logger;

import org.example.ledgerapi.State;
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.Contact;
import org.hyperledger.fabric.contract.annotation.Contract;
import org.hyperledger.fabric.contract.annotation.Default;
import org.hyperledger.fabric.contract.annotation.Info;
import org.hyperledger.fabric.contract.annotation.License;
import org.hyperledger.fabric.contract.annotation.Transaction;
import org.hyperledger.fabric.shim.ChaincodeStub;

/**
* A custom context provides easy access to list of all commercial papers
*/

/**
* Define commercial paper smart contract by extending Fabric Contract class
*
*/
@Contract(name = "org.papernet.commercialpaper", info = @Info(title = "MyAsset contract", description = "", version = "0.0.1", license = @License(name = "SPDX-License-Identifier: Apache-2.0", url = ""), contact = @Contact(email = "java-contract@example.com", name = "java-contract", url = "http://java-contract.me")))
@Default
public class CommercialPaperContract implements ContractInterface {

// use the classname for the logger, this way you can refactor
private final static Logger LOG = Logger.getLogger(CommercialPaperContract.class.getName());

@Override
public Context createContext(ChaincodeStub stub) {
return new CommercialPaperContext(stub);
}

public CommercialPaperContract() {
}

/**
* Define a custom context for commercial paper
*/

/**
* Instantiate to perform any setup of the ledger that might be required.
*
* @param {Context} ctx the transaction context
*/
@Transaction
public void instantiate(CommercialPaperContext ctx) {
// No implementation required with this example
// It could be where data migration is performed, if necessary
LOG.info("No data migration to perform");
}

/**
* Issue commercial paper
*
* @param {Context} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} issueDateTime paper issue date
* @param {String} maturityDateTime paper maturity date
* @param {Integer} faceValue face value of paper
*/
@Transaction
public CommercialPaper issue(CommercialPaperContext ctx, String issuer, String paperNumber, String issueDateTime,
String maturityDateTime, int faceValue) {

System.out.println(ctx);

// create an instance of the paper
CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime,
faceValue,issuer,"");

// Smart contract, rather than paper, moves paper into ISSUED state
paper.setIssued();

// Newly issued paper is owned by the issuer
paper.setOwner(issuer);

System.out.println(paper);
// Add the paper to the list of all similar commercial papers in the ledger
// world state
ctx.paperList.addPaper(paper);

// Must return a serialized paper to caller of smart contract
return paper;
}

/**
* Buy commercial paper
*
* @param {Context} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} currentOwner current owner of paper
* @param {String} newOwner new owner of paper
* @param {Integer} price price paid for this paper
* @param {String} purchaseDateTime time paper was purchased (i.e. traded)
*/
@Transaction
public CommercialPaper buy(CommercialPaperContext ctx, String issuer, String paperNumber, String currentOwner,
String newOwner, int price, String purchaseDateTime) {

// Retrieve the current paper using key fields provided
String paperKey = State.makeKey(new String[] { paperNumber });
CommercialPaper paper = ctx.paperList.getPaper(paperKey);

// Validate current owner
if (!paper.getOwner().equals(currentOwner)) {
throw new RuntimeException("Paper " + issuer + paperNumber + " is not owned by " + currentOwner);
}

// First buy moves state from ISSUED to TRADING
if (paper.isIssued()) {
paper.setTrading();
}

// Check paper is not already REDEEMED
if (paper.isTrading()) {
paper.setOwner(newOwner);
} else {
throw new RuntimeException(
"Paper " + issuer + paperNumber + " is not trading. Current state = " + paper.getState());
}

// Update the paper
ctx.paperList.updatePaper(paper);
return paper;
}

/**
* Redeem commercial paper
*
* @param {Context} ctx the transaction context
* @param {String} issuer commercial paper issuer
* @param {Integer} paperNumber paper number for this issuer
* @param {String} redeemingOwner redeeming owner of paper
* @param {String} redeemDateTime time paper was redeemed
*/
@Transaction
public CommercialPaper redeem(CommercialPaperContext ctx, String issuer, String paperNumber, String redeemingOwner,
String redeemDateTime) {

String paperKey = CommercialPaper.makeKey(new String[] { paperNumber });

CommercialPaper paper = ctx.paperList.getPaper(paperKey);

// Check paper is not REDEEMED
if (paper.isRedeemed()) {
throw new RuntimeException("Paper " + issuer + paperNumber + " already redeemed");
}

// Verify that the redeemer owns the commercial paper before redeeming it
if (paper.getOwner().equals(redeemingOwner)) {
paper.setOwner(paper.getIssuer());
paper.setRedeemed();
} else {
throw new RuntimeException("Redeeming owner does not own paper" + issuer + paperNumber);
}

ctx.paperList.updatePaper(paper);
return paper;
}

}

3.4 应用

3.4.1 基本流程

  • 应用程序如何调用商业票据智能合约的简化图表:

    1681609886914

  • 应用程序必须遵循六个基本步骤来提交交易:

    • 从钱包中选择一个身份
    • 连接到网关
    • 访问所需的网络
    • 构建智能合约的交易请求
    • 将交易提交到网络
    • 处理响应

4. 工作流程

1681614315146

]]>
- Jmeter + Blockchain - Jmeter + fabric diff --git a/tags/Jmeter/index.html b/tags/Jmeter/index.html index 3795545..9689b17 100644 --- a/tags/Jmeter/index.html +++ b/tags/Jmeter/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
标签 - Jmeter
2023
Jmeter 压测
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/tags/XShell/index.html b/tags/XShell/index.html index 9f6a0a5..c392624 100644 --- a/tags/XShell/index.html +++ b/tags/XShell/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
\ No newline at end of file diff --git a/tags/awk/index.html b/tags/awk/index.html index 54b9fd4..461237e 100644 --- a/tags/awk/index.html +++ b/tags/awk/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
标签 - awk
2023
awk 编程
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/tags/fabric/index.html b/tags/fabric/index.html index 895c435..23c7dd4 100644 --- a/tags/fabric/index.html +++ b/tags/fabric/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
标签 - fabric
2023
Fabric 入门
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/tags/index.html b/tags/index.html index 57c27af..e8c61ee 100644 --- a/tags/index.html +++ b/tags/index.html @@ -150,11 +150,11 @@ } } detectApple() - })(window)
\ No newline at end of file diff --git a/tags/mybatis/index.html b/tags/mybatis/index.html index 0b47f6d..484c2a8 100644 --- a/tags/mybatis/index.html +++ b/tags/mybatis/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
标签 - mybatis
2023
MyBatis
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/tags/redis/index.html b/tags/redis/index.html index 504a0e8..fc7ee75 100644 --- a/tags/redis/index.html +++ b/tags/redis/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
标签 - redis
2023
Redis 基础
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/tags/spider/index.html b/tags/spider/index.html index 6aa53bf..6c57aa3 100644 --- a/tags/spider/index.html +++ b/tags/spider/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
标签 - spider
2023
爬虫
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file diff --git a/tags/spring/index.html b/tags/spring/index.html index 42350c2..3222d4c 100644 --- a/tags/spring/index.html +++ b/tags/spring/index.html @@ -51,7 +51,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2023-11-29 08:11:51' + postUpdate: '2023-12-10 11:10:16' }
标签 - spring
2023
Spring
最新文章
Fabric 入门
awk 编程
Jmeter 压测
Redis 基础
MyBatis
+ })(window)
\ No newline at end of file +
\ No newline at end of file