当前位置:网站首页>Fabric 账本数据块结构解析(一):如何解析账本中的智能合约交易数据
Fabric 账本数据块结构解析(一):如何解析账本中的智能合约交易数据
2022-06-24 18:42:00 【51CTO】
id:BSN_2021
公众号:BSN研习社 作者:红枣科技高晨曦
背景:BSN公网Fabric联盟链的出现降低了使用区块链的难度,在通过BSN城市节点网关发起交易时,只能获取最基本交易信息,想要展示更多区块链特性的数据就需要从账本数据中获取,而解析账本数据有一定的难度。
目标:了解账本数据结构,更好的设计开发自己的项目
对象: 使用BSN联盟链Fabric的开发人员
前言
开始之前先看一个简单的合约代码
import (
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"github.com/hyperledger/fabric-contract-api-go/metadata"
"math/big"
)
type DemoChaincode struct {
contractapi . Contract
}
func ( d * DemoChaincode) Set( ctx contractapi . TransactionContextInterface, key string, value int64) ( string, error) {
bigValue : = new( big . Int) . SetInt64( value)
keyValue , err : = ctx . GetStub() . GetState( key)
var resultValue string
if err != nil || len( keyValue) == 0 {
keyValue = bigValue . Bytes()
resultValue = bigValue . String()
} else {
bigKeyValue : = new( big . Int) . SetBytes( keyValue)
result : = new( big . Int) . Add( bigKeyValue, bigValue)
keyValue = result . Bytes()
resultValue = result . String()
}
err = ctx . GetStub() . PutState( key, keyValue)
if err != nil {
return "", err
} else {
ctx . GetStub() . SetEvent( "set_key", bigValue . Bytes())
return resultValue, nil
}
}
func ( d * DemoChaincode) Query( ctx contractapi . TransactionContextInterface, key string) ( string, error) {
valueBytes, err : = ctx . GetStub() . GetState( key)
if err != nil || len( valueBytes) == 0 {
return "0", nil
}
bigKeyValue : = new( big . Int) . SetBytes( valueBytes)
return bigKeyValue . String(), nil
}
- 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.
这是一个通过Go语言开发的Fabric智能合约,合约提供了两个合约方法Set
、Query
。
Set
方法的主要功能就是根据输入的key
读取账本数据并且累加value
的值,并输出结果。
Query
方法主要就是查询当前key
的值。
那么我们在调用这个合约的Set
方法时,会在链上留下什么数据,都包含那些有用的信息呢?
接下来我们通过BSN城市节点网关查询交易所在的块信息来查看一个账本的块数据都有那些信息。
通过BSN城市节点网关查询块数据
在此之前,我们先通过接口调用一下Set
方法,拿到合约的执行结果和交易Id。
cli :=getTestNetFabricClient(t)
nonce,_ :=common.GetRandomNonce()
reqData :=node.TransReqDataBody{
Nonce: base64.StdEncoding.EncodeToString(nonce),
ChainCode: "cc_f73a60f601654467b71bdc28b8f16033",
FuncName: "Set",
Args: []string{"abc","76"},
}
res,err :=cli.ReqChainCode(reqData)
if err !=nil {
t.Fatal(err)
}
resBytes,_ :=json.Marshal(res)
fmt.Println(string(resBytes))
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
getTestNetFabricClient 方法主要是根据网关地址、用户信息等参数创建了一个FabricClient对象
响应结果为:
{
"header":{
"code":0,
"msg":"success"
},
"mac":"MEUCIQCLnU5gTu6NvM0zn4HH1lDSEef5i6HgNjKS2YRirDfYVgIgJaN+BQRUulS6jtqePAvb/Z3E9U0W5Go4aV7ffrkMbBc=",
"body":{
"blockInfo":{
"txId":"32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999",
"blockHash":"",
"status":0
},
"ccRes":{
"ccCode":200,
"ccData":"276"
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
在返回的结果中我们可以看到本次交易的Id为32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999
,合约的返回结果为276
,状态为200
表示合约执行成功。
由于目前BSN网关的ReqChainCode
接口不会等待交易落块后再返回,所以接口不会返回当前交易的块信息,也无法确认交易的最终状态,所以需要我们再次调用接口查询交易的信息。
调用接口查询块信息
BSN城市节点网关提供了两个接口查询块信息 getBlockInfo
以及getBlockData
,这两个接口的参数相同,返回又有哪些不同呢?
getBlockInfo
接口返回了解析之后的块信息以及交易的简略信息,只包含了交易Id、状态、提交者、交易时间等。
getBlockData
接口则返回了完成的块数据,块数据是以base64
编码后的块数据,示例如下:
响应结果为:
{
"header":{
"code":0,
"msg":"success"
},
"mac":"MEQCIDIg/lhMy2yK1oaK/7naISwmL9gEYUtVHsgYykUYr73jAiALQcEsIBfmeFZvdgq4gEBNY/thLO/ZJUb/tbPl9ql9WA==",
"body":{
"blockHash":"66e7d01b102a0bbd2ebe55fff608d46512c3d243dde0d1305fec44a31a800932",
"blockNumber":384187,
"preBlockHash":"0ce4a7200bb67aea157e92f8dfea5c40bd1a6390d28cc70bad91e9af79098df4",
"blockData":"Ckg ... muf1s="
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
在网关返回的参数中blockData
即完整的块数据。
转换块数据
网关响应的块数据即github.com\hyperledger\fabric-protos-go\common\common.pb.go
中Block
经过proto.Marshal
序列化后的数据进行base64
编码后的。
解析代码如下
func ConvertToBlock(blockData string) (*common.Block, error) {
blockBytes, err := base64.StdEncoding.DecodeString(blockData)
if err != nil {
return nil, errors.WithMessage(err, "convert block data has error")
}
block := &common.Block{}
err = proto.Unmarshal(blockBytes, block)
if err != nil {
return nil, errors.WithMessage(err, "convert block bytes has error")
}
return block, nil
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
同时在github.com/hyperledger/fabric-config/protolator
包中也提供了转换为json
格式的方法:
Fabric 块内数据包含哪些内容
我们先来看common.Block
对象:
// This is finalized block structure to be shared among the orderer and peer
// Note that the BlockHeader chains to the previous BlockHeader, and the BlockData hash is embedded
// in the BlockHeader. This makes it natural and obvious that the Data is included in the hash, but
// the Metadata is not.
type Block struct {
Header *BlockHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
Data *BlockData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
Metadata *BlockMetadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
下面我们来详细的解释每一部分的内容
Header
// BlockHeader is the element of the block which forms the block chain
// The block header is hashed using the configured chain hashing algorithm
// over the ASN.1 encoding of the BlockHeader
type BlockHeader struct {
Number uint64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"`
PreviousHash []byte `protobuf:"bytes,2,opt,name=previous_hash,json=previousHash,proto3" json:"previous_hash,omitempty"`
DataHash []byte `protobuf:"bytes,3,opt,name=data_hash,json=dataHash,proto3" json:"data_hash,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
BlockHeader
中包含块号、上一个块哈希、当前块的数据哈希。由于不包含当前块的块哈希,所以我们如果想获取当前块的哈希,需要自己手动计算。
即计算asn1
编码后的块号、上一个块哈希、数据哈希的哈希即可。
Data
BlockData
中包含的即为当前块内的每一条交易,是common.Envelope
对象的序列化结果的合集。即每一个交易。
// Envelope wraps a Payload with a signature so that the message may be authenticated
type Envelope struct {
// A marshaled Payload
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
// A signature by the creator specified in the Payload header
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
common.Envelope
对象即客户端收集到足够的背书结果后向orderer提交的交易内容,包含详细交易以及客户端签名,将Payload
序列化为common.Payload
对象后我么可以得到向orderer提交的channel信息,提交者信息。他们在common.Payload.Header
中。
type Header struct {
ChannelHeader []byte `protobuf:"bytes,1,opt,name=channel_header,json=channelHeader,proto3" json:"channel_header,omitempty"`
SignatureHeader []byte `protobuf:"bytes,2,opt,name=signature_header,json=signatureHeader,proto3" json:"signature_header,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
他们分别是common.ChannelHeader
以及common.SignatureHeader
对象,这些信息中包含了channelId,交易Id,交易时间,提交者,MSPId,等信息。
而对common.Payload.Data
解析后我们可以得到peer.Transaction
对象。这里就是我们向各个节点发起的交易提案以及节点的背书结果。每一个peer.TransactionAction
对象中包含两部分数据,
// TransactionAction binds a proposal to its action. The type field in the
// header dictates the type of action to be applied to the ledger.
type TransactionAction struct {
// The header of the proposal action, which is the proposal header
Header []byte `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
// The payload of the action as defined by the type in the header For
// chaincode, it's the bytes of ChaincodeActionPayload
Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
Header
是交易提案提交者身份信息,一般和common.Payload.Header
中的SignatureHeader
一致。
Payload
是peer.ChaincodeActionPayload
对象,包含交易提案的详细信息以及节点的模拟执行结果和背书信息。
// ChaincodeActionPayload is the message to be used for the TransactionAction's
// payload when the Header's type is set to CHAINCODE. It carries the
// chaincodeProposalPayload and an endorsed action to apply to the ledger.
type ChaincodeActionPayload struct {
// This field contains the bytes of the ChaincodeProposalPayload message from
// the original invocation (essentially the arguments) after the application
// of the visibility function. The main visibility modes are "full" (the
// entire ChaincodeProposalPayload message is included here), "hash" (only
// the hash of the ChaincodeProposalPayload message is included) or
// "nothing". This field will be used to check the consistency of
// ProposalResponsePayload.proposalHash. For the CHAINCODE type,
// ProposalResponsePayload.proposalHash is supposed to be H(ProposalHeader ||
// f(ChaincodeProposalPayload)) where f is the visibility function.
ChaincodeProposalPayload []byte `protobuf:"bytes,1,opt,name=chaincode_proposal_payload,json=chaincodeProposalPayload,proto3" json:"chaincode_proposal_payload,omitempty"`
// The list of actions to apply to the ledger
Action *ChaincodeEndorsedAction `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
ChaincodeProposalPayload
存储了peer.ChaincodeProposalPayload
对象的序列化数据,其中包含了向节点发起的交易提案内容,它包含了我们调用的合约信息、合约方以及输入参数等信息。
ChaincodeEndorsedAction
对象中包含了两部分数据:
// ChaincodeEndorsedAction carries information about the endorsement of a
// specific proposal
type ChaincodeEndorsedAction struct {
// This is the bytes of the ProposalResponsePayload message signed by the
// endorsers. Recall that for the CHAINCODE type, the
// ProposalResponsePayload's extenstion field carries a ChaincodeAction
ProposalResponsePayload []byte `protobuf:"bytes,1,opt,name=proposal_response_payload,json=proposalResponsePayload,proto3" json:"proposal_response_payload,omitempty"`
// The endorsement of the proposal, basically the endorser's signature over
// proposalResponsePayload
Endorsements []*Endorsement `protobuf:"bytes,2,rep,name=endorsements,proto3" json:"endorsements,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
Endorsements
是节点的背书信息,包含节点证书以及节点签名。
ProposalResponsePayload
存储了peer.ProposalResponsePayload
对象的序列化数据,它包含了ProposalHash
以及Extension
,
其中Extension
部分的数据在合约调用中为peer.ChaincodeAction
对象的序列化数据,包含了执行的合约名称、合约中对账本的读写集合,合约的返回结果,以及合约事件等。
// ChaincodeAction contains the actions the events generated by the execution
// of the chaincode.
type ChaincodeAction struct {
// This field contains the read set and the write set produced by the
// chaincode executing this invocation.
Results []byte `protobuf:"bytes,1,opt,name=results,proto3" json:"results,omitempty"`
// This field contains the events generated by the chaincode executing this
// invocation.
Events []byte `protobuf:"bytes,2,opt,name=events,proto3" json:"events,omitempty"`
// This field contains the result of executing this invocation.
Response *Response `protobuf:"bytes,3,opt,name=response,proto3" json:"response,omitempty"`
// This field contains the ChaincodeID of executing this invocation. Endorser
// will set it with the ChaincodeID called by endorser while simulating proposal.
// Committer will validate the version matching with latest chaincode version.
// Adding ChaincodeID to keep version opens up the possibility of multiple
// ChaincodeAction per transaction.
ChaincodeId *ChaincodeID `protobuf:"bytes,4,opt,name=chaincode_id,json=chaincodeId,proto3" json:"chaincode_id,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
Results
为合约执行过程中对不同合约的读写信息,由rwset.TxReadWriteSet
对象序列化。
Events
为合约执行中的事件信息,有peer.ChaincodeEvent
对象序列化。
Response
为合约的返回信息,包含Status
、Message
、Payload
。
ChaincodeId
为执行的合约信息,包含合约名,版本等。
Metadata
Metadata
目前包含5个Byte数组,他们分别对应,Orderer签名信息,最后一个配置块号,交易过滤器,Orderer信息,提交哈希。其中 配置块号和Orderer信息在目前的版本中未使用。
其中交易过滤器中的每一个Bytes表示对应交易的状态信息,转换为peer.TxValidationCode
对象即可。
结语
通过对块数据的学习我们可以查到交易在提案、背书、提交各个阶段的信息,帮助我们更好的理解交易的流程以及每一个参与交易的合约和账本的数据修改情况。对在排查异常交易的过程中提供帮助。 最后用一个块的json
格式数据来帮助大家更好的理解Fabric账本的块数据
{
"data": {
"data": [
{
"payload": {
"data": {
"actions": [
{
"header": {
"creator": {
"id_bytes": "LS0 ... LQo=",
"mspid": "ECDSARTestNodeMSP"
},
"nonce": "rG24c6sj28YGtCo8PBeQMTJsgPusft6m"
},
"payload": {
"action": {
"endorsements": [
{
"endorser": "ChF ... LQo=",
"signature": "MEQCIDr+a5HiELJq1M2vZWc2NqNxDRnCEck7EtErgbvfe+mOAiAx9XKRmCcM2xyEyYoz5l6wMuYE4zDIR5GVvLnz0MAmXg=="
}
],
"proposal_response_payload": {
"extension": {
"chaincode_id": {
"name": "cc_f73a60f601654467b71bdc28b8f16033",
"path": "",
"version": "1.0.0.1"
},
"events": {
"chaincode_id": "cc_f73a60f601654467b71bdc28b8f16033",
"event_name": "set_key",
"payload": "TA==",
"tx_id": "32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999"
},
"response": {
"message": "",
"payload": "Mjc2",
"status": 200
},
"results": {
"data_model": "KV",
"ns_rwset": [
{
"collection_hashed_rwset": [],
"namespace": "_lifecycle",
"rwset": {
"metadata_writes": [],
"range_queries_info": [],
"reads": [
{
"key": "namespaces/fields/cc_f73a60f601654467b71bdc28b8f16033/Sequence",
"version": {
"block_num": "384158",
"tx_num": "0"
}
}
],
"writes": []
}
},
{
"collection_hashed_rwset": [],
"namespace": "cc_f73a60f601654467b71bdc28b8f16033",
"rwset": {
"metadata_writes": [],
"range_queries_info": [],
"reads": [
{
"key": "abc",
"version": {
"block_num": "384179",
"tx_num": "0"
}
}
],
"writes": [
{
"is_delete": false,
"key": "abc",
"value": "ARQ="
}
]
}
}
]
}
},
"proposal_hash": "3jOb59oJFGtq2NM4loU4cwmHSqp//YV7EwA+qNKV4fo="
}
},
"chaincode_proposal_payload": {
"TransientMap": {},
"input": {
"chaincode_spec": {
"chaincode_id": {
"name": "cc_f73a60f601654467b71bdc28b8f16033",
"path": "",
"version": ""
},
"input": {
"args": [
"U2V0",
"YWJj",
"NzY="
],
"decorations": {},
"is_init": false
},
"timeout": 0,
"type": "GOLANG"
}
}
}
}
}
]
},
"header": {
"channel_header": {
"channel_id": "channel202010310000001",
"epoch": "0",
"extension": {
"chaincode_id": {
"name": "cc_f73a60f601654467b71bdc28b8f16033",
"path": "",
"version": ""
}
},
"timestamp": "2022-06-09T03:29:47.851381445Z",
"tls_cert_hash": null,
"tx_id": "32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999",
"type": 3,
"version": 0
},
"signature_header": {
"creator": {
"id_bytes": "LS0 ... LQo=",
"mspid": "ECDSARTestNodeMSP"
},
"nonce": "rG24c6sj28YGtCo8PBeQMTJsgPusft6m"
}
}
},
"signature": "MEQCIEIoQw4ZlOB6qc42oQ9L85I4Chs3lKPYgXEbDUEiQUPBAiAf/OQj21xhinlmI6ef7Ufv04KoeIuLwrFlS9lAfltXpw=="
},
]
},
"header": {
"data_hash": "u/d0Jx1D5tEv4WZIpqkjw17J/89klX27L+ukmhNdTQU=",
"number": "384187",
"previous_hash": "DOSnIAu2euoVfpL43+pcQL0aY5DSjMcLrZHpr3kJjfQ="
},
"metadata": {
"metadata": [
"CgI ... cDB",
"",
"AAA=",
"",
"CiCROQhM45JkjcmvLOVSVLqEoS1artoPCQdcipWPGa5/Ww=="
]
}
}
- 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.
由于数据过大该块内只保留了一个交易,以及去掉了证书部分
以上的数据都可以在BSN测试网服务中查询操作
边栏推荐
- SAP license: SAP s/4 Hana module function introduction
- SAP license: ERP for supply chain management and Implementation
- Road vector data download tutorial
- Selection (030) - what is the output of the following code?
- 建立自己的网站(8)
- TKDE2022:基于知识增强采样的对话推荐系统
- This is not safe
- Usage of typedef enum (enumeration)
- 面试算法 - 字符串问题总结
- R language Quantitative Ecology redundancy analysis RDA analysis plant diversity species data visualization
猜你喜欢
电源噪声分析
微服务系统设计——数据模型与系统架构设计
JS deep understanding of scope
Mqtt protocol usage of LabVIEW
干货 | 新手经常忽略的嵌入式基础知识点,你都掌握了吗?
SDL: cannot play audio after upgrading openaudio to openaudiodevice
使用阿里云RDS for SQL Server性能洞察优化数据库负载-初识性能洞察
FROM_ GLC introduction and data download tutorial
R language Quantitative Ecology redundancy analysis RDA analysis plant diversity species data visualization
LabView之MQTT协议使用
随机推荐
Mqtt protocol usage of LabVIEW
【Leetcode】旋转系列(数组、矩阵、链表、函数、字符串)
微服务系统设计——数据模型与系统架构设计
【leetcode】838. Push domino (Analog)
Microservice system design -- data model and system architecture design
Introduction to smart contract security audit delegatecall (2)
论文解读(SR-GNN)《Shift-Robust GNNs: Overcoming the Limitations of Localized Graph Training Data》
Make track map
Get the actual name of the method parameter through the parameter
华为机器学习服务语音识别功能,让应用绘“声”绘色
电源噪声分析
微服務系統設計——子服務項目構建
ArrayList源码解析
Introduction, download and use of global meteorological data CRU ts from 1901 to 2020
面试算法 - 字符串问题总结
干货 | 新手经常忽略的嵌入式基础知识点,你都掌握了吗?
Using to release resources
The sharp sword of API management -- eolink
okcc呼叫中心数据操作的效率问题
API管理之利剑 -- Eolink