前言

在上个实验 Hyperledger Fabric 多组织多排序节点部署在多个主机上 中,我们已经实现了多组织多排序节点部署在多个主机上,但到目前为止,我们所有的实验都只是研究了联盟链的网络配置方法(尽管这确实是重难点),而没有考虑具体的应用开发。本文将在前面实验的基础上,首先尝试使用 Go 语言开发了一个工作室联盟链的项目信息智能合约,并成功将其部署至联盟链上;然后依据官方示例,使用 fabric-gateway 模块实现了一个能够管理项目信息智能合约的客户端;之后对比了 fabric-gateway 模块和 fabric-sdk-* 模块各自的优缺点,分析官方示例源码实现了通过 fabric-sdk-* 模块管理整个联盟链网络。一般语境下,本文默认智能合约等于链码。

工作准备

本文工作

以三组织三排序节点的方式启动 Hyperledger Fabric 网络,实验共包含四个组织—— council 、 soft 、 web 、 hard , 其中 council 组织为网络提供 TLS-CA 服务,并且运行维护着三个 orderer 服务;其余每个组织都运行维护着一个 peer 节点、一个 admin 用户和一个 user 用户。网络结构为(实验代码已上传至:https://github.com/wefantasy/FabricLearn6_ContractGatewayAndSDK 下):

运行端口 说明
council.ifantasy.net 7050 council 组织的 CA 服务, 为联盟链网络提供 TLS-CA 服务
orderer1.council.ifantasy.net 7051 council 组织的 orderer1 服务
orderer1.council.ifantasy.net 7052 council 组织的 orderer1 服务的 admin 服务
orderer2.council.ifantasy.net 7054 council 组织的 orderer2 服务
orderer2.council.ifantasy.net 7055 council 组织的 orderer2 服务的 admin 服务
orderer3.council.ifantasy.net 7057 council 组织的 orderer3 服务
orderer3.council.ifantasy.net 7058 council 组织的 orderer3 服务的 admin 服务
soft.ifantasy.net 7250 soft 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.soft.ifantasy.net 7251 soft 组织的 peer1 成员节点
web.ifantasy.net 7350 web 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.web.ifantasy.net 7351 web 组织的 peer1 成员节点
hard.ifantasy.net 7450 hard 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.hard.ifantasy.net 7451 hard 组织的 peer1 成员节点

实验准备

本文网络结构直接将 Hyperledger Fabric无排序组织以Raft协议启动多个Orderer服务、TLS组织运行维护Orderer服务 中创建的 4-2_RunOrdererByCouncil 复制为 6_ContractGatewayAndSDK 并修改(建议直接将本案例仓库 FabricLearn 下的 6_ContractGatewayAndSDK 目录拷贝到本地运行),文中大部分命令在 Hyperledger Fabric定制联盟链网络工程实践 中已有介绍因此不会详细说明。默认情况下,所有命令皆在 6_ContractGatewayAndSDK 根目录下执行,在开始后面的实验前按照以下命令启动基础实验网络:

  1. 设置DNS(如果未设置): ./setDNS.sh
  2. 设置环境变量: source envpeer1soft
  3. 启动CA网络: ./0_Restart.sh

本实验初始 docker 网络为:

基础环境

注册用户

直接运行根目录下的 1_RegisterUser.sh 即可完成本实验所需用户的注册。以往我们每个组织只有一个 peer 节点和一个 admin 节点,但这些节点都不适合为客户端所用,因此基础环境的改变主要包含了为每个组织新增一个 client 类型的用户。以 soft 组织为例,其注册用户命令为:

echo "Working on soft"
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/ca/crypto/ca-cert.pem
export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/ca/admin
fabric-ca-client enroll -d -u https://ca-admin:ca-adminpw@soft.ifantasy.net:7250
# client 类型用户注册
fabric-ca-client register -d --id.name user1 --id.secret user1 --id.type client -u https://soft.ifantasy.net:7250
fabric-ca-client register -d --id.name peer1 --id.secret peer1 --id.type peer -u https://soft.ifantasy.net:7250
fabric-ca-client register -d --id.name admin1 --id.secret admin1 --id.type admin -u https://soft.ifantasy.net:7250

组织证书构建

直接运行根目录下的 2_EnrollUser.sh 即可完成本实验所需证书的构建,每个组织主要增加了 client 类型用户的证书构建每个注册用户单元配置文件 config.yaml ,以 soft 组织为例,其生成组织证书的命令为:

echo "Start Soft============================="
# 新增
echo "Enroll User1"
export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/user1
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem
export FABRIC_CA_CLIENT_MSPDIR=msp
fabric-ca-client enroll -d -u https://user1:user1@soft.ifantasy.net:7250 echo "Enroll Admin1"
export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem
export FABRIC_CA_CLIENT_MSPDIR=msp
fabric-ca-client enroll -d -u https://admin1:admin1@soft.ifantasy.net:7250
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/admincerts
cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/admincerts/cert.pem echo "Enroll Peer1"
export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem
export FABRIC_CA_CLIENT_MSPDIR=msp
fabric-ca-client enroll -d -u https://peer1:peer1@soft.ifantasy.net:7250
# for TLS
export FABRIC_CA_CLIENT_MSPDIR=tls-msp
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/tls-ca-cert.pem
fabric-ca-client enroll -d -u https://peer1soft:peer1soft@council.ifantasy.net:7050 --enrollment.profile tls --csr.hosts peer1.soft.ifantasy.net
cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/tls-msp/keystore/*_sk $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/tls-msp/keystore/key.pem
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/admincerts
cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/admincerts/cert.pem mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/admincerts
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/cacerts
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/tlscacerts
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/users
cp $LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/cacerts/
cp $LOCAL_CA_PATH/soft.ifantasy.net/assets/tls-ca-cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/tlscacerts/
cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/admincerts/cert.pem cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/msp/config.yaml
# 新增
cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/user1/msp/config.yaml
cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/config.yaml
cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/config.yaml
echo "End Soft============================="

为了配合使用每个用户的单元配置文件,需要将所有用户 msp 目录下的 cacerts/council-ifantasy-net-7050.pem 文件名修改为 cacerts/ca-cert.pem ,因此在 2_EnrollUser.sh 的末尾追加一行批量修改文件名的命令来实现此目的:

# 按正则匹配并批量修改符合要求的文件
find orgs/ -regex ".+cacerts.+.pem" -not -regex ".+tlscacerts.+" | rename 's/cacerts\/.+\.pem/cacerts\/ca-cert\.pem/'

配置通道

直接运行根目录下的 3_Configtxgen.sh 即可完成本实验所需通道配置,需要注意的是,为了使通道组织架构更加清晰,将通道配置文件 configtx.yaml 中各组织名称从 orgnameMSP 改为了 orgname ,以 soft 组织为例,其组织通道配置如下:

- &soft
Name: softMSP
ID: softMSP
MSPDir: ../orgs/soft.ifantasy.net/msp
Policies:
Readers:
Type: Signature
Rule: "OR('softMSP.admin', 'softMSP.peer', 'softMSP.client')"
Writers:
Type: Signature
Rule: "OR('softMSP.admin', 'softMSP.client')"
Admins:
Type: Signature
Rule: "OR('softMSP.admin')"
Endorsement:
Type: Signature
Rule: "OR('softMSP.peer')"
AnchorPeers:
- Host: peer1.soft.ifantasy.net
Port: 7251

智能合约开发

本节将参考官方示例智能合约 asset-transfer-basic 开发工作室联盟链的 项目资源管理智能合约 ,其在官方示例的基础上进行了依赖和结构上的简化。本示例是基于 Go 语言的智能合约,因此建议先学习 Go 语言基础概念和规范,不然自行定制可能会有一些 Bug 。

合约代码

  1. 初始化目录/文件

    在实验根目录 6_ContractGatewayAndSDK 下创建目录 contract 作为智能合约根目录,并在其下创建智能合约文件 project_contract.go ,后续代码皆在 project_contract.go 中。
  2. 智能合约结构体
    type ProjectContract struct {
    contractapi.Contract
    }

    智能合约结构体一般是固定写法,创建任意一个结构体然后继承 contractapi.Contract 即可,当部署至链上后利用其继承的 contractapi.Contract 的接口实现对合约操作。

  3. 项目信息结构体
    type Project struct {
    ID string `json:"ID"` // 项目唯一ID
    Name string `json:"Name"` // 项目名称
    Developer string `json:"Developer"` // 项目主要负责人
    Organization string `json:"Organization"` // 项目所属组织
    Category string `json:"Category"` // 项目所属类别
    Url string `json:"Url"` // 项目介绍地址
    Describes string `json:"Describes"` // 项目描述
    }

    项目信息结构体主要定义了单个项目的基本信息,类似于 Java 的 Entity 类、数据库的单个表。

  4. 初始化智能合约数据
    func (s *ProjectContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
    projects := []Project{
    {ID: "FA8B31A55CD59DB352BCBF4D2AE791AD", Name: "工作室联盟链管理系统", Developer: "Fantasy", Organization: "Web", Category: "blockchain", Url: "https://github.com/wefantasy/FabricLearn", Describes: "本项目虚拟了一个工作室联盟链需求并将逐步实现,致力于提供一个易理解、可复现的Fabric学习项目,其中项目部署步骤的各个环节都清晰可见,并且将所有实验打包为脚本使之能够被快速复现在任何一台主机上"},
    }
    for _, project := range projects {
    projectJSON, err := json.Marshal(project)
    if err != nil {
    return err
    }
    err = ctx.GetStub().PutState(project.ID, projectJSON)
    if err != nil {
    return fmt.Errorf("failed to put to world state. %v", err)
    }
    }
    return nil
    }

    在 Fabric 某个旧版本之前必须提供智能合约初始化函数,但在本实验所用的 Fabric 2.4 则是可选项,在此仅仅是为了写入预设实验数据。Fabric 底层使用默认键值对(key-value)状态数据库 LevelDB 储存数据,在操作体验上十分像 redis 数据库。

  5. 判断项目信息是否已存在
    func (s *ProjectContract) ProjectExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
    projectJSON, err := ctx.GetStub().GetState(id)
    if err != nil {
    return false, fmt.Errorf("failed to read from world state: %v", err)
    } return projectJSON != nil, nil
    }
  6. 写入新项目信息
    func (s *ProjectContract) CreateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {
    exists, err := s.ProjectExists(ctx, id)
    if err != nil {
    return err
    }
    if exists {
    return fmt.Errorf("the project %s already exists", id)
    }
    project := Project{
    ID: id,
    Name: name,
    Developer: developer,
    Organization: organization,
    Category: category,
    Url: url,
    Describes: describes,
    }
    projectJSON, err := json.Marshal(project)
    if err != nil {
    return err
    }
    return ctx.GetStub().PutState(id, projectJSON)
    }
  7. 删除指定项目信息
    func (s *ProjectContract) DeleteProject(ctx contractapi.TransactionContextInterface, id string) error {
    exists, err := s.ProjectExists(ctx, id)
    if err != nil {
    return err
    }
    if !exists {
    return fmt.Errorf("the project %s does not exist", id)
    } return ctx.GetStub().DelState(id)
    }

    Fabric 联盟链作为区块链的一种特殊形式,同样具有可追溯特性,因此任何对数据的增删改操作都是软操作——留下操作记录。

  8. 修改项目信息
    func (s *ProjectContract) UpdateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {
    exists, err := s.ProjectExists(ctx, id)
    if err != nil {
    return err
    }
    if !exists {
    return fmt.Errorf("the project %s does not exist", id)
    }
    project := Project{
    ID: id,
    Name: name,
    Developer: developer,
    Organization: organization,
    Category: category,
    Url: url,
    Describes: describes,
    }
    projectJSON, err := json.Marshal(project)
    if err != nil {
    return err
    }
    return ctx.GetStub().PutState(id, projectJSON)
    }
  9. 查询项目信息
    func (s *ProjectContract) ReadProject(ctx contractapi.TransactionContextInterface, id string) (*Project, error) {
    projectJSON, err := ctx.GetStub().GetState(id)
    if err != nil {
    return nil, fmt.Errorf("failed to read from world state: %v", err)
    }
    if projectJSON == nil {
    return nil, fmt.Errorf("the project %s does not exist", id)
    } var project Project
    err = json.Unmarshal(projectJSON, &project)
    if err != nil {
    return nil, err
    } return &project, nil
    }
  10. 查询链上所有项目信息
    func (s *ProjectContract) GetAllProjects(ctx contractapi.TransactionContextInterface) ([]*Project, error) {
    // GetStateByRange 查询参数为两个空字符串时即查询所有数据
    resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
    if err != nil {
    return nil, err
    }
    defer resultsIterator.Close() var projects []*Project
    for resultsIterator.HasNext() {
    queryResponse, err := resultsIterator.Next()
    if err != nil {
    return nil, err
    } var project Project
    err = json.Unmarshal(queryResponse.Value, &project)
    if err != nil {
    return nil, err
    }
    projects = append(projects, &project)
    } return projects, nil
    }
  11. 智能合约入口函数/主函数
    func main() {
    chaincode, err := contractapi.NewChaincode(&ProjectContract{})
    if err != nil {
    log.Panicf("Error creating project-manage chaincode: %v", err)
    } if err := chaincode.Start(); err != nil {
    log.Panicf("Error starting project-manage chaincode: %v", err)
    }
    }

至此,项目信息管理智能合约核心代码以编写完毕,完整 project_contract.go 文件内容如下(需要注意的是合约入口必须属于 main 包):

package main

import (
"encoding/json"
"fmt"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"log"
) type ProjectContract struct {
contractapi.Contract
} type Project struct {
ID string `json:"ID"` // 项目唯一ID
Name string `json:"Name"` // 项目名称
Developer string `json:"Developer"` // 项目主要负责人
Organization string `json:"Organization"` // 项目所属组织
Category string `json:"Category"` // 项目所属类别
Url string `json:"Url"` // 项目介绍地址
Describes string `json:"Describes"` // 项目描述
} // 初始化智能合约数据
func (s *ProjectContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
projects := []Project{
{ID: "FA8B31A55CD59DB352BCBF4D2AE791AD", Name: "工作室联盟链管理系统", Developer: "Fantasy", Organization: "Web", Category: "blockchain", Url: "https://github.com/wefantasy/FabricLearn", Describes: "本项目虚拟了一个工作室联盟链需求并将逐步实现,致力于提供一个易理解、可复现的Fabric学习项目,其中项目部署步骤的各个环节都清晰可见,并且将所有实验打包为脚本使之能够被快速复现在任何一台主机上"},
}
for _, project := range projects {
projectJSON, err := json.Marshal(project)
if err != nil {
return err
}
err = ctx.GetStub().PutState(project.ID, projectJSON)
if err != nil {
return fmt.Errorf("failed to put to world state. %v", err)
}
}
return nil
} // 写入新项目
func (s *ProjectContract) CreateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {
exists, err := s.ProjectExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the project %s already exists", id)
} project := Project{
ID: id,
Name: name,
Developer: developer,
Organization: organization,
Category: category,
Url: url,
Describes: describes,
}
projectJSON, err := json.Marshal(project)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, projectJSON)
} // 读取指定ID的项目信息
func (s *ProjectContract) ReadProject(ctx contractapi.TransactionContextInterface, id string) (*Project, error) {
projectJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if projectJSON == nil {
return nil, fmt.Errorf("the project %s does not exist", id)
} var project Project
err = json.Unmarshal(projectJSON, &project)
if err != nil {
return nil, err
} return &project, nil
} // 更新项目信息.
func (s *ProjectContract) UpdateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {
exists, err := s.ProjectExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the project %s does not exist", id)
} project := Project{
ID: id,
Name: name,
Developer: developer,
Organization: organization,
Category: category,
Url: url,
Describes: describes,
}
projectJSON, err := json.Marshal(project)
if err != nil {
return err
} return ctx.GetStub().PutState(id, projectJSON)
} // 删除指定ID的项目信息
func (s *ProjectContract) DeleteProject(ctx contractapi.TransactionContextInterface, id string) error {
exists, err := s.ProjectExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the project %s does not exist", id)
} return ctx.GetStub().DelState(id)
} // 判断某项目是否存在
func (s *ProjectContract) ProjectExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
projectJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
} return projectJSON != nil, nil
} // 读取所有项目信息
func (s *ProjectContract) GetAllProjects(ctx contractapi.TransactionContextInterface) ([]*Project, error) {
// GetStateByRange 查询参数为两个空字符串时即查询所有数据
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close() var projects []*Project
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
} var project Project
err = json.Unmarshal(queryResponse.Value, &project)
if err != nil {
return nil, err
}
projects = append(projects, &project)
} return projects, nil
} func main() {
chaincode, err := contractapi.NewChaincode(&ProjectContract{})
if err != nil {
log.Panicf("Error creating project-manage chaincode: %v", err)
} if err := chaincode.Start(); err != nil {
log.Panicf("Error starting project-manage chaincode: %v", err)
}
}

依赖下载

合约代码编写完成后并不能直接部署到联盟链上,需要将合约中 import 导入的包下载到本地以供后面一起打包,本小节所有命令默认运行于 6_ContractGatewayAndSDK/contract 下。

  1. 初始化模块
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract
  2. 将所有依赖下载到本地
    go mod vendor

以上命令运行成功后,智能合约开发工作基本结束,此时 contract 目录结构如下:

6_ContractGatewayAndSDK/contract
├── go.mod
├── go.sum
├── project_contract.go
└── vendor
├── github.com
├── golang.org
├── google.golang.org
├── gopkg.in
└── modules.tx

合约部署测试

如无特殊说明,以下命令默认运行于实验根目录 6_ContractGatewayAndSDK 下:

  1. 合约打包
     source envpeer1soft
    peer lifecycle chaincode package basic.tar.gz --path contract --lang golang --label basic_1
  2. 三组织安装
     source envpeer1soft
    peer lifecycle chaincode install basic.tar.gz
    peer lifecycle chaincode queryinstalled
    source envpeer1web
    peer lifecycle chaincode install basic.tar.gz
    peer lifecycle chaincode queryinstalled
    source envpeer1hard
    peer lifecycle chaincode install basic.tar.gz
    peer lifecycle chaincode queryinstalled
  3. 三组织批准
     export CHAINCODE_ID=basic_1:0f1f1ffc8e3865a9179e70a3c56237482b3eb4dcecd30ab51ab01a6f5d3daeff
    source envpeer1soft
    peer lifecycle chaincode approveformyorg -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID
    peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1
    source envpeer1web
    peer lifecycle chaincode approveformyorg -o orderer3.council.ifantasy.net:7057 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID
    peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1
    source envpeer1hard
    peer lifecycle chaincode approveformyorg -o orderer2.council.ifantasy.net:7054 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID
    peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1

    注意要将 CHAINCODE_ID 的值改为三组织安装时输出的连码包 ID

  4. 提交并测试
     source envpeer1soft
    peer lifecycle chaincode commit -o orderer2.council.ifantasy.net:7054 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --init-required --version 1.0 --sequence 1 --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE
    peer chaincode invoke --isInit -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["InitLedger"]}'
    sleep 5
    peer chaincode invoke -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["GetAllProjects"]}'

fabric-gateway 客户端示例

客户端代码

  1. 初始化目录/文件

    在实验根目录 6_ContractGatewayAndSDK 下创建目录 contract-gateway 作为 fabric-gateway 客户端的根目录,并在其下创建联盟链网络连接文件 connect.go 和 客户端主程序 app.go 。实验最终目录结构为:
    contract-gateway
    ├── app.go
    ├── connect.go
    ├── go.mod
    └── go.sum
  2. connect.go 写入以下内容
    package main
    
    import (
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "path"
    "github.com/hyperledger/fabric-gateway/pkg/identity"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    ) const (
    mspID = "softMSP" // 所属组织的MSPID
    cryptoPath = "/root/FabricLearn/6_ContractGatewayAndSDK/orgs/soft.ifantasy.net" // 中间变量
    certPath = cryptoPath + "/registers/user1/msp/signcerts/cert.pem" // client 用户的签名证书
    keyPath = cryptoPath + "/registers/user1/msp/keystore/" // client 用户的私钥路径
    tlsCertPath = cryptoPath + "/assets/tls-ca-cert.pem" // client 用户的 tls 通信证书
    peerEndpoint = "peer1.soft.ifantasy.net:7251" // 所连 peer 节点的地址
    gatewayPeer = "peer1.soft.ifantasy.net" // 网关 peer 节点名称
    ) // 创建指向联盟链网络的 gRPC 连接.
    func newGrpcConnection() *grpc.ClientConn {
    certificate, err := loadCertificate(tlsCertPath)
    if err != nil {
    panic(err)
    } certPool := x509.NewCertPool()
    certPool.AddCert(certificate)
    transportCredentials := credentials.NewClientTLSFromCert(certPool, gatewayPeer) connection, err := grpc.Dial(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))
    if err != nil {
    panic(fmt.Errorf("failed to create gRPC connection: %w", err))
    } return connection
    } // 根据用户指定的X.509证书为这个网关连接创建一个客户端标识。
    func newIdentity() *identity.X509Identity {
    certificate, err := loadCertificate(certPath)
    if err != nil {
    panic(err)
    } id, err := identity.NewX509Identity(mspID, certificate)
    if err != nil {
    panic(err)
    }
    return id
    } // 加载证书文件
    func loadCertificate(filename string) (*x509.Certificate, error) {
    certificatePEM, err := ioutil.ReadFile(filename)
    if err != nil {
    return nil, fmt.Errorf("failed to read certificate file: %w", err)
    }
    return identity.CertificateFromPEM(certificatePEM)
    } // 使用私钥从消息摘要生成数字签名
    func newSign() identity.Sign {
    files, err := ioutil.ReadDir(keyPath)
    if err != nil {
    panic(fmt.Errorf("failed to read private key directory: %w", err))
    }
    privateKeyPEM, err := ioutil.ReadFile(path.Join(keyPath, files[0].Name())) if err != nil {
    panic(fmt.Errorf("failed to read private key file: %w", err))
    } privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM)
    if err != nil {
    panic(err)
    } sign, err := identity.NewPrivateKeySign(privateKey)
    if err != nil {
    panic(err)
    } return sign
    }

    值得说明的是,不论是 gateway 客户端还是 fabric-sdk 客户端,一般都可以通过 client 、 admin 类型的用户连接联盟链网络,只是创建单独的 client 类型的专用用户连接网络更符合开发理念。

  3. app.go 写入以下内容
    package main
    
    import (
    "bytes"
    "encoding/json"
    "fmt"
    "time"
    "github.com/hyperledger/fabric-gateway/pkg/client"
    ) const (
    channelName = "testchannel" // 连接的通道
    chaincodeName = "basic" // 连接的链码
    ) func main() {
    clientConnection := newGrpcConnection()
    defer clientConnection.Close() id := newIdentity()
    sign := newSign() gateway, err := client.Connect(
    id,
    client.WithSign(sign),
    client.WithClientConnection(clientConnection),
    client.WithEvaluateTimeout(5*time.Second),
    client.WithEndorseTimeout(15*time.Second),
    client.WithSubmitTimeout(5*time.Second),
    client.WithCommitStatusTimeout(1*time.Minute),
    )
    if err != nil {
    panic(err)
    }
    defer gateway.Close() network := gateway.GetNetwork(channelName)
    contract := network.GetContract(chaincodeName) fmt.Println("getAllAssets:")
    getAllAssets(contract)
    }
    func getAllAssets(contract *client.Contract) {
    fmt.Println("Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger") evaluateResult, err := contract.EvaluateTransaction("GetAllProjects")
    if err != nil {
    panic(fmt.Errorf("failed to evaluate transaction: %w", err))
    }
    result := formatJSON(evaluateResult) fmt.Printf("*** Result:%s\n", result)
    } func formatJSON(data []byte) string {
    var prettyJSON bytes.Buffer
    if err := json.Indent(&prettyJSON, data, " ", ""); err != nil {
    panic(fmt.Errorf("failed to parse JSON: %w", err))
    }
    return prettyJSON.String()
    }

客户端演示

如无特殊说明,以下命令默认运行于实验根目录 contract-gateway 下:

  1. 初始化模块
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway
  2. 下载依赖
    go get

    此时实验目录结构为

  3. 运行客户端
    go run .

    因为本目录下同时有两个 packagemain 的 go 文件,所以要用 . 的方式运行,运行结果如下:

fabric-sdk-go 客户端示例

刚接触 Fabric 你可能会很疑惑,有些案例使用 fabric-gateway 连接联盟链、另一些案例通过 fabric-sdk-* 连接联盟链,并且似乎都可以操纵网络,那么有什么区别呢? fabric-sdk-* 被定义为 Fabric 的低级 SDK ,主要为开发者提供账本管理、通道管理、用户管理等联盟链管理的 API ,它的开发成本更高但功能丰富;而 fabric-gateway 被定义为 Fabric 的高级 SDK ,这里的高级主要体现在其抽象程度更高,主要为开发者提供账本管理的 API ,它的开发成本更低但功能较少。因此建议优先学习 fabric-sdk-* 的使用。

连接配置文件

就像刚才说的, fabric-sdk-* 开发成本比较高,我觉得高出来的开发成本有一半都在连接配置文件的配置上,它让我花费了至少半天的时间来排错,而网上几乎没有能把连接配置文件讲清楚的文章(也许是我没有找到),只能通过官方示例代码慢慢推导出正确的配置方法。

从 fabric-sdk-* 官方示例 assetTransfer.go 中引用的 connection-org1.yaml 连接配置文件出发,可以定位到生成它的相关文件为 ccp-generate.shccp-template.yaml ,后者为连接配置文件的基准模板,前者使用 bash 命令将基准模板替换为具体连接配置文件。连接配置文件有 json 和 yaml 两种格式,我觉得 yaml 语法更为简洁,后续实验以此为例。将 ccp-generate.sh 文件中的函数展开后,可以很容易的得生成连接配置文件的过程,本节所有命令默认运行于 6_ContractGatewayAndSDK 目录下,通过如下命令生成 soft 组织的连接配置文件:

  1. 创建模板文件

    将官方模板 ccp-template.yaml 复制一份至我们项目的 6_ContractGatewayAndSDK/config/ccp-template.yaml 中,由于我们的命名规范与官方不同,且该模板通用性不高,因此将其内容改为如下:
    ---
    name: test-network-${ORG}
    version: 1.0.0
    client:
    organization: ${ORG}
    connection:
    timeout:
    peer:
    endorser: '300'
    organizations:
    ${ORG}:
    mspid: ${ORG}MSP
    peers:
    - peer1.${ORG}.ifantasy.net
    certificateAuthorities:
    - ${ORG}.ifantasy.net
    peers:
    peer1.${ORG}.ifantasy.net:
    url: grpcs://peer1.${ORG}.ifantasy.net:${P0PORT}
    tlsCACerts:
    pem: |
    ${PEERPEM}
    grpcOptions:
    ssl-target-name-override: peer1.${ORG}.ifantasy.net
    hostnameOverride: peer1.${ORG}.ifantasy.net
    certificateAuthorities:
    ${ORG}.ifantasy.net:
    url: https://${ORG}.ifantasy.net:${CAPORT}
    caName: ${ORG}.ifantasy.net
    tlsCACerts:
    pem:
    - |
    ${CAPEM}
    httpOptions:
    verify: false

    这个模板可以跟我们项目很好的契合,需要特别注意的是其中组织名和组织ID必须与 configtx.yaml 文件中相匹配,这是前面修改 configtx.yaml 的原因,不然很容易出错,其中各个参数的含义可以对照下面的模板参数理解。

  2. 设置模板参数
    ORG=soft
    P0PORT=7251
    CAPORT=7250
    cryptoPath=$LOCAL_CA_PATH/soft.ifantasy.net
    PEERPEM=$cryptoPath/assets/tls-ca-cert.pem
    CAPEM=$cryptoPath/assets/ca-cert.pem
  3. 获取 tls 证书和 ca 证书
    PP="`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $PEERPEM`"
    CP="`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $CAPEM`"
  4. 生成模板文件
    sed -e "s/\${ORG}/$ORG/" \
    -e "s/\${P0PORT}/$P0PORT/" \
    -e "s/\${CAPORT}/$CAPORT/" \
    -e "s#\${PEERPEM}#$PP#" \
    -e "s#\${CAPEM}#$CP#" \
    config/ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g' > connection-soft.yaml

依次执行上述命令,最后会将连接配置文件 connection-soft.yaml 输出到实验根目录中,本例中其内容如下:

---
name: test-network-soft
version: 1.0.0
client:
organization: soft
connection:
timeout:
peer:
endorser: '300'
organizations:
soft:
mspid: softMSP
peers:
- peer1.soft.ifantasy.net
certificateAuthorities:
- soft.ifantasy.net
peers:
peer1.soft.ifantasy.net:
url: grpcs://peer1.soft.ifantasy.net:7251
tlsCACerts:
pem: |
-----BEGIN CERTIFICATE-----
MIICHzCCAcWgAwIBAgIUbO4XSCy2KbQQN/E63zvkhUJfMzwwCgYIKoZIzj0EAwIw
bDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK
EwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMR0wGwYDVQQDExRjb3VuY2ls
LmlmYW50YXN5Lm5ldDAeFw0yMjA2MTEwNTU3MDBaFw0zNzA2MDcwNTU3MDBaMGwx
CzAJBgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChML
SHlwZXJsZWRnZXIxDzANBgNVBAsTBkZhYnJpYzEdMBsGA1UEAxMUY291bmNpbC5p
ZmFudGFzeS5uZXQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQecDRTwml7bcaD
nZdPiEYiTxFwHa+g2nw+mq+6KeMPW98WT3BPNErb1gw9BQa6GRcTypJ7Ga1lSqLS
IFD+aypYo0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAd
BgNVHQ4EFgQUq3Q80AlYM9lGKHWVupCEjpyBb1kwCgYIKoZIzj0EAwIDSAAwRQIh
AJashZ+Sob7DoOpYII22wDOPSV8updo1W9LNEAaxzMyTAiAokfgCVjtlX3EJnV+m
qc5EBQCjA0AaX1HPNBTUII7T+Q==
-----END CERTIFICATE----- grpcOptions:
ssl-target-name-override: peer1.soft.ifantasy.net
hostnameOverride: peer1.soft.ifantasy.net
certificateAuthorities:
soft.ifantasy.net:
url: https://soft.ifantasy.net:7250
caName: soft.ifantasy.net
tlsCACerts:
pem:
- |
-----BEGIN CERTIFICATE-----
MIICGDCCAb+gAwIBAgIUXF3f1cgHiAMO03c/61iyFWAD/0AwCgYIKoZIzj0EAwIw
aTELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK
EwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRowGAYDVQQDExFzb2Z0Lmlm
YW50YXN5Lm5ldDAeFw0yMjA2MTEwNTU3MDBaFw0zNzA2MDcwNTU3MDBaMGkxCzAJ
BgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChMLSHlw
ZXJsZWRnZXIxDzANBgNVBAsTBkZhYnJpYzEaMBgGA1UEAxMRc29mdC5pZmFudGFz
eS5uZXQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASP0Vs5wUaRzIyiXx2ygH6A
IQyCLe6VhTxnNPmJhMUVOmO+iyLJqMUuQRRHIcCgiNGPR9cqd4ygcRJBvsG+sooY
o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4E
FgQUkPhZPSjyHVdL5NkQED1Rdif7GdowCgYIKoZIzj0EAwIDRwAwRAIgfOt69wD8
HEqroGm/zVFf/NiqivluaK5Yf3Ryn0C7p5ECID/KNGjbt5b53ivuL5slK5B+8eA2
KGUN7ysBzX8hTzPj
-----END CERTIFICATE----- httpOptions:
verify: false

上述操作已打包至 5_GenConnectYaml.sh 中,也可以直接在根目录下运行 5_GenConnectYaml.sh 来了生成连接配置文件。

客户端代码

  1. 初始化目录/文件

    在实验根目录 6_ContractGatewayAndSDK 下创建目录 contract-sdk 作为 fabric-sdk 客户端的根目录,并在其下创建主程序 app.go 。将上节生成的 connection-soft.yaml 复制到该目录下,最终目录结构为:
     contract-sdk
    ├── app.go
    ├── connection-soft.yaml
    ├── go.mod
    ├── go.sum
    ├── keystore
    └── wallet
    └── appUser.id
  2. 向 app.go 写入以下内容
     package main
    
     import (
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "path/filepath" "github.com/hyperledger/fabric-sdk-go/pkg/core/config"
    "github.com/hyperledger/fabric-sdk-go/pkg/gateway"
    ) func main() {
    log.Println("============ application-golang starts ============") err := os.Setenv("DISCOVERY_AS_LOCALHOST", "true")
    if err != nil {
    log.Fatalf("Error setting DISCOVERY_AS_LOCALHOST environemnt variable: %v", err)
    } wallet, err := gateway.NewFileSystemWallet("wallet")
    if err != nil {
    log.Fatalf("Failed to create wallet: %v", err)
    } err = populateWallet(wallet)
    // 调试建议注释这里
    // if !wallet.Exists("appUser") {
    // err = populateWallet(wallet)
    // if err != nil {
    // log.Fatalf("Failed to populate wallet contents: %v", err)
    // }
    // } ccpPath := filepath.Join(
    "connection-soft.yaml",
    ) gw, err := gateway.Connect(
    gateway.WithConfig(config.FromFile(filepath.Clean(ccpPath))),
    gateway.WithIdentity(wallet, "appUser"),
    )
    if err != nil {
    log.Fatalf("Failed to connect to gateway: %v", err)
    }
    defer gw.Close() network, err := gw.GetNetwork("testchannel")
    if err != nil {
    log.Fatalf("Failed to get network: %v", err)
    } contract := network.GetContract("basic") log.Println("--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger")
    result, err := contract.EvaluateTransaction("GetAllProjects")
    if err != nil {
    log.Fatalf("Failed to evaluate transaction: %v", err)
    }
    log.Println(string(result)) log.Println("--> Submit Transaction: DeleteProject, delete new project info with ID arguments")
    result, err = contract.SubmitTransaction("DeleteProject", "FA8B31A55CD59DB352BCBF4D2AE791AD")
    if err != nil {
    log.Fatalf("Failed to Submit transaction: %v", err)
    }
    log.Println(string(result))
    } func populateWallet(wallet *gateway.Wallet) error {
    log.Println("============ Populating wallet ============")
    credPath := filepath.Join(
    "..",
    "orgs",
    "soft.ifantasy.net",
    "registers",
    "user1",
    "msp",
    ) certPath := filepath.Join(credPath, "signcerts", "cert.pem")
    // read the certificate pem
    cert, err := ioutil.ReadFile(filepath.Clean(certPath))
    if err != nil {
    return err
    } keyDir := filepath.Join(credPath, "keystore")
    // there's a single file in this dir containing the private key
    files, err := ioutil.ReadDir(keyDir)
    if err != nil {
    return err
    }
    if len(files) != 1 {
    return fmt.Errorf("keystore folder should have contain one file")
    }
    keyPath := filepath.Join(keyDir, files[0].Name())
    key, err := ioutil.ReadFile(filepath.Clean(keyPath))
    if err != nil {
    return err
    } identity := gateway.NewX509Identity("softMSP", string(cert), string(key)) return wallet.Put("appUser", identity)
    }

客户端演示

如无特殊说明,以下命令默认运行于实验根目录 contract-sdk 下:

  1. 初始化模块
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway
  2. 下载依赖
    go get
  3. 运行客户端
    go run .

Q&A

遇到错误:

QueryBlockConfig failed: no channel peers configured for channel [testchannel]

解决方法: 大概率是连接配置文件组织名称啥的写错了,再次检查组织配置文件与configtx.yaml中声明的是否匹配。

遇到错误:

2022/06/10 15:55:44 Failed to get network: Failed to create new channel client: event service creation failed: could not get chConfig cache reference: QueryBlockConfig failed: QueryBlockConfig failed: target(s) required

解决方法: 可能是因为 wallet 目录下的身份与所申明的身份不匹配,建议每次启动前删除 wallet 目录让它重新生成。

遇到错误:

2022/06/10 16:08:13 Failed to Submit transaction: Failed to submit: error getting channel response for channel [testchannel]: no successful response received from any peer: access denied

解决方法: 此时检查对应的 peer 节点容器日志若有 implicit policy evaluation failed 错误,则说明当前使用的身份权限不足。在实验中使用 peer 类型的用户身份则会导致此问题,建议使用 client 身份的用户(admin 身份也行)。

遇到错误:

2022/06/10 16:08:13 Failed to Submit transaction: Failed to submit: error getting channel response for channel [testchannel]: no successful response received from any peer: access denied

解决方法: 此时检查对应的 peer 节点容器日志若有 implicit policy evaluation failed 错误,则说明当前使用的身份权限不足。在实验中使用 peer 类型的用户身份则会导致此问题,建议使用 client 身份的用户(admin 身份也行)。

参考

[1]: hyperledger-fabric. Fabric Contract APIs and Application APIs. readthedocs.io. [-]

[2]: barney2k7. What is the difference between fabric-chaincode-go and fabric-contract-api-go?. stackoverflow.com. [2020-05-08]

[3]: Nikos Karamolegkos. fabric-sdk-go vs fabric-gateway. When to use each one?. hyperledger.org. [2021-12-07]

[4]: kid1999 Karamolegkos. Fabric智能合约Go开发包简单理解. github.io. [2021-06-26]

Hyperledger Fabric 智能合约开发及 fabric-sdk-go/fabric-gateway 使用示例的更多相关文章

  1. hyperledger fabric 智能合约开发

    开发步奏: 1.创建教育联盟 2.区块链服务平台自动生成通道id 3.区块链网络服务人员通过命令行在区块链网络中创建对应通道 4.创建相关教育组织 5.邀请相关组织加入联盟 6.区块链网络管理人员通过 ...

  2. 使用IBM Blockchain Platform extension开发你的第一个fabric智能合约

    文章目录 安装IBM Blockchain Platform extension for VS Code 创建一个智能合约项目 理解智能合约 打包智能合约 Local Fabric Ops 安装智能合 ...

  3. 区块链入门到实战(27)之以太坊(Ethereum) – 智能合约开发

    智能合约的优点 与传统合同相比,智能合约有一些显著优点: 不需要中间人 费用低 代码就是规则 区块链网络中有多个备份,不用担心丢失 避免人工错误 无需信任,就可履行协议 匿名履行协议 以太坊(Ethe ...

  4. 智能合约开发环境搭建及Hello World合约

    如果你对于以太坊智能合约开发还没有概念(本文会假设你已经知道这些概念),建议先阅读入门篇. 就先学习任何编程语言一样,入门的第一个程序都是Hello World.今天我们来一步一步从搭建以太坊智能合约 ...

  5. 智能合约开发solidity编程语言开发一个以太坊应用区块链投票实例

    智能合约开发用solidity编程语言部署在以太坊这个区块链平台,本文提供一个官方实战示例快速入门,用例子深入浅出智能合约开发,体会以太坊构建去中心化可信交易技术魅力.智能合约其实是"执行合 ...

  6. NEO智能合约开发(二)再续不可能的任务

      NEO智能合约开发中,应用合约比较简单,是的你没看错,应用合约比较简单. 应用合约三部曲,发布.调用.看结果.除了看结果工具比较缺乏,发布调用neogui最起码可以支撑你测试.   鉴权合约比较麻 ...

  7. EOS智能合约开发(四):智能合约部署及调试(附编程示例)

    EOS智能合约开发(一):EOS环境搭建和创建节点 EOS智能合约开发(二):EOS创建和管理钱包 EOS智能合约开发(三):EOS创建和管理账号 部署智能合约的示例代码如下: $ cleos set ...

  8. EOS智能合约开发(三):EOS创建和管理账号

    没有看前面文章的小伙伴可以看一下 EOS智能合约开发(一):EOS环境搭建和启动节点 EOS智能合约开发(二):EOS创建和管理钱包 创建好钱包.密钥之后,接下来你就可以创建账号了,账号是什么?账号保 ...

  9. 以太坊智能合约开发,Web3.js API 中文文档 ethereum web3.js入门说明

    以太坊智能合约开发,Web3.js API 中文文档 ethereum web3.js入门说明 为了让你的Ðapp运行上以太坊,一种选择是使用web3.js library提供的web3.对象.底层实 ...

随机推荐

  1. 浅谈一下流式处理平台Flink

    浅谈一下流式处理平台(Flink) 大数据框架听过很多,比如 Hadoop,HDFS...不过自己的项目都没有上过 为什么突然提到 Flink,因为最近一个项目需要用到,所以学习最好的方式就是项目驱动 ...

  2. LC-203

    给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 . 示例 1: 输入:head = [1,2,6,3,4,5, ...

  3. Python-初见-高级篇

    目录 正则表达式 CGI MySQL PyMySQL 网络编程 多线程 线程同步 线程优先级 JSON 推荐阅读:https://www.cnblogs.com/zwtblog/tag/Python/ ...

  4. 前端CSS基础

    一:CSS 1.什么是CSS? CSS(Cascading Style Sheet,层叠样式表)定义如何显示HTML元素. 当浏览器读到一个样式表,它就会按照这个样式表来对文档进行格式化(渲染). C ...

  5. Linux内核--链表结构(一)

    一.前言 Linux内核链表结构是一种双向循环链表结构,与传统的链表结构不同,Linux内核链表结构仅包含前驱和后继指针,不包含数据域.使用链表结构,仅需在结构体成员中包含list_head*成员就行 ...

  6. Spring Cloud之负载均衡组件Ribbon原理分析

    目录 前言 一个问题引发的思考 Ribbon的简单使用 Ribbon 原理分析 @LoadBalanced 注解 @Qualifier注解 LoadBalancerAutoConfiguration ...

  7. css 动画 (2)

    1. html 结构 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...

  8. 一次不规范HTTP请求引发的nginx响应400问题分析与解决

    背景 最近分析数据偶然发现nginx log中有一批用户所有的HTTP POST log上报请求均返回400,没有任何200成功记录,由于只占整体请求的不到0.5%,所以之前也一直没有触发监控报警,而 ...

  9. 服务器安全加固 - Linux

    一.账号和口令 1.1 禁用或删除无用账号 查看 /etc/passwd 文件查看是否有无用的账号,如果存在则删除,降低安全风险. 操作步骤: 使用命令 userdel <用户名> 删除不 ...

  10. Spring 源码(8)Spring BeanPostProcessor的注册、国际化及事件发布机制

    上一篇文章https://www.cnblogs.com/redwinter/p/16198942.html介绍了Spring的注解的解析过程以及Spring Boot自动装配的原理,大概回顾下:Sp ...