深蓝前几篇博客讲了Fabric的环境搭建,在环境搭建好后,咱们就能够进行Fabric的开发工做了。Fabric的开发主要分红2部分,ChainCode链上代码开发和基于SDK的Application开发。咱们这里先讲ChainCode的开发。Fabric的链上代码支持Java或者Go语言进行开发,由于Fabric自己是Go开发的,因此深蓝建议仍是用Go进行ChainCode的开发。git
ChainCode的Go代码须要定义一个SimpleChaincode这样一个struct,而后在该struct上定义Init和Invoke两个函数,而后还要定义一个main函数,做为ChainCode的启动入口。如下是ChainCode的模板:github
package main import ( "github.com/hyperledger/fabric/core/chaincode/shim" pb "github.com/hyperledger/fabric/protos/peer" "fmt" ) type SimpleChaincode struct { } func main() { err := shim.Start(new(SimpleChaincode)) if err != nil { fmt.Printf("Error starting Simple chaincode: %s", err) } } func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response { return shim.Success(nil) } func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response { function, args := stub.GetFunctionAndParameters() fmt.Println("invoke is running " + function) if function == "test1" {//自定义函数名称 return t.test1(stub, args)//定义调用的函数 } return shim.Error("Received unknown function invocation") } func (t *SimpleChaincode) test1(stub shim.ChaincodeStubInterface, args []string) pb.Response{ return shim.Success([]byte("Called test1")) }
这里咱们能够看到,在Init和Invoke的时候,都会传入参数stub shim.ChaincodeStubInterface,这个参数提供的接口为咱们编写ChainCode的业务逻辑提供了大量实用的方法。下面一一讲解:
前面给出的ChainCode的模板中,咱们已经能够看到,在Invoke的时候,由传入的参数来决定咱们具体调用了哪一个方法,因此须要先使用GetFunctionAndParameters解析调用的时候传入的参数。除了这个方法之外,接口还提供了另外几个方法,不过其本质都是同样的。
对于ChainCode来讲,核心的操做就是对State Database的增删改查,对此Fabric接口提供了3个对State DB的操做方法。数据库
对于State DB来讲,增长和修改数据是统一的操做,由于State DB是一个Key Value数据库,若是咱们指定的Key在数据库中已经存在,那么就是修改操做,若是Key不存在,那么就是插入操做。对于实际的系统来讲,咱们的Key多是单据编号,或者系统分配的自增ID+实体类型做为前缀,而Value则是一个对象通过JSON序列号后的字符串。好比说咱们定义一个Student的Struct,而后插入一个学生数据,对于的代码应该是这样的:json
type Student struct { Id int Name string } func (t *SimpleChaincode) testStateOp(stub shim.ChaincodeStubInterface, args []string) pb.Response{ student1:=Student{1,"Devin Zeng"} key:="Student:"+strconv.Itoa(student1.Id)//Key格式为 Student:{Id} studentJsonBytes, err := json.Marshal(student1)//Json序列号 if err != nil { return shim.Error(err.Error()) } err= stub.PutState(key,studentJsonBytes) if(err!=nil){ return shim.Error(err.Error()) } return shim.Success([]byte("Saved Student!")) }
这个也很好理解,根据Key删除State DB的数据。若是根据Key找不到对于的数据,删除失败。数组
err= stub.DelState(key) if err != nil { return shim.Error("Failed to delete Student from DB, key is: "+key) }
由于咱们是Key Value数据库,因此根据Key来对数据库进行查询,是一件很常见,很高效的操做。返回的数据是byte数组,咱们须要转换为string,而后再Json反序列化,能够获得咱们想要的对象。
dbStudentBytes,err:= stub.GetState(key) var dbStudent Student; err=json.Unmarshal(dbStudentBytes,&dbStudent)//反序列化 if err != nil { return shim.Error("{\"Error\":\"Failed to decode JSON of: " + string(dbStudentBytes)+ "\" to Student}") } fmt.Println("Read Student from DB, name:"+dbStudent.Name)
【注意:不能在一个ChainCode函数中PutState后又立刻GetState,这个时候GetState是没有最新值的,由于在这时Transaction并无完成,尚未提交到StateDB里面】函数
前面在进行数据库的增删改查的时候,都须要用到Key,而咱们使用的是咱们本身定义的Key格式:{StructName}:{Id},这是有单主键Id还比较简单,若是咱们有多个列作联合主键怎么办?实际上,ChainCode也为咱们提供了生成Key的方法CreateCompositeKey,经过这个方法,咱们能够将联合主键涉及到的属性都传进去,并声明了对象的类型便可。
以选课表为例,里面包含了如下属性:
type ChooseCourse struct { CourseNumber string //开课编号 StudentId int //学生ID Confirm bool //是否确认 }
其中CourseNumber+StudentId构成了这个对象的联合主键,咱们要得到生成的复核主键,那么可写为:
cc:=ChooseCourse{"CS101",123,true} var key1,_= stub.CreateCompositeKey("ChooseCourse",[]string{cc.CourseNumber,strconv.Itoa(cc.StudentId)}) fmt.Println(key1)
【注:其实Fabric就是用U+0000来把各个字段分割开的,由于这个字符太特殊,因此很适合作分割】
既然有组合那么就有拆分,当咱们从数据库中得到了一个复合键的Key以后,怎么知道其具体是由哪些字段组成的呢。其实就是用U+0000把这个复合键再Split开,获得结果中第一个是objectType,剩下的就是复合键用到的列的值。区块链
objType,attrArray,_:= stub.SplitCompositeKey(key1) fmt.Println("Object:"+objType+" ,Attributes:"+strings.Join(attrArray,"|"))
这里实际上是一种对Key进行前缀匹配的查询,也就是说,咱们虽然是部分复合键的查询,可是不容许拿后面部分的复合键进行匹配,必须是前面部分。spa
这个方法能够得到调用这个ChainCode的客户端的用户的证书,这里虽然返回的是byte数组,可是实际上是一个字符串,内容格式以下:
-----BEGIN CERTIFICATE-----
MIICGjCCAcCgAwIBAgIRAMVe0+QZL+67Q+R2RmqsD90wCgYIKoZIzj0EAwIwczEL
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
cmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh
Lm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwODEyMTYyNTU1WhcNMjcwODEwMTYyNTU1
WjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN
U2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWVXNlcjFAb3JnMS5leGFtcGxlLmNvbTBZ
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABN7WqfFwWWKynl9SI87byp0SZO6QU1hT
JRatYysXX5MJJRzvvVsSTsUzQh5jmgwkPbFcvk/x4W8lj5d2Tohff+WjTTBLMA4G
A1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIO2os1zK9BKe
Lb4P8lZOFU+3c0S5+jHnEILFWx2gNoLkMAoGCCqGSM49BAMCA0gAMEUCIQDAIDHK
gPZsgZjzNTkJgglZ7VgJLVFOuHgKWT9GbzhwBgIgE2YWoDpG0HuhB66UzlA+6QzJ
+jvM0tOVZuWyUIVmwBM=
-----END CERTIFICATE-----code
咱们常见的需求是在ChainCode中得到当前用户的信息,方便进行权限管理。那么咱们怎么得到当前用户呢?咱们能够把这个证书的字符串转换为Certificate对象。一旦转换成这个对象,咱们就能够经过Subject得到当前用户的名字。对象
func (t *SimpleChaincode) testCertificate(stub shim.ChaincodeStubInterface, args []string) pb.Response{ creatorByte,_:= stub.GetCreator() certStart := bytes.IndexAny(creatorByte, "-----BEGIN") if certStart == -1 { fmt.Errorf("No certificate found") } certText := creatorByte[certStart:] bl, _ := pem.Decode(certText) if bl == nil { fmt.Errorf("Could not decode the PEM structure") } cert, err := x509.ParseCertificate(bl.Bytes) if err != nil { fmt.Errorf("ParseCertificate failed") } uname:=cert.Subject.CommonName fmt.Println("Name:"+uname) return shim.Success([]byte("Called testCertificate "+uname)) }
前面提到的GetState只是最基本的根据Key查询值的操做,可是对于不少时候,咱们须要查询返回的是一个集合,好比我要知道某个区间的Key对于全部对象,或者咱们须要对Value对象内部的属性进行查询。
提供了对某个区间的Key进行查询的接口,适用于任何State DB。因为返回的是一个StateQueryIteratorInterface接口,咱们须要经过这个接口再作一个for循环,才能读取返回的信息,全部咱们能够独立出一个方法,专门将该接口返回的数据以string的byte数组形式返回。这是咱们的转换方法:
func getListResult(resultsIterator shim.StateQueryIteratorInterface) ([]byte,error){ defer resultsIterator.Close() // buffer is a JSON array containing QueryRecords var buffer bytes.Buffer buffer.WriteString("[") bArrayMemberAlreadyWritten := false for resultsIterator.HasNext() { queryResponse, err := resultsIterator.Next() if err != nil { return nil, err } // Add a comma before array members, suppress it for the first array member if bArrayMemberAlreadyWritten == true { buffer.WriteString(",") } buffer.WriteString("{\"Key\":") buffer.WriteString("\"") buffer.WriteString(queryResponse.Key) buffer.WriteString("\"") buffer.WriteString(", \"Record\":") // Record is a JSON object, so we write as-is buffer.WriteString(string(queryResponse.Value)) buffer.WriteString("}") bArrayMemberAlreadyWritten = true } buffer.WriteString("]") fmt.Printf("queryResult:\n%s\n", buffer.String()) return buffer.Bytes(), nil }
好比咱们要查询编号从1号到3号的全部学生,那么咱们的查询代码能够这么写:
func (t *SimpleChaincode) testRangeQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{ resultsIterator,err:= stub.GetStateByRange("Student:1","Student:3") if err!=nil{ return shim.Error("Query by Range failed") } students,err:=getListResult(resultsIterator) if err!=nil{ return shim.Error("getListResult failed") } return shim.Success(students) }
这是一个“富查询”,是对Value的内容进行查询,若是是LevelDB,那么是不支持,只有CouchDB时才能用这个方法。
关于传入的query这个字符串,实际上是CouchDB所使用的Mango查询,咱们能够在官方博客了解到一些信息:https://blog.couchdb.org/2016/08/03/feature-mango-query/ 其基本语法能够在https://github.com/cloudant/mango 这里看到。
好比咱们仍然之前面的Student为例,咱们要按Name来进行查询,那么咱们的代码能够写为:
func (t *SimpleChaincode) testRichQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{ name:="Devin Zeng"//这里按理来讲应该是参数传入 queryString := fmt.Sprintf("{\"selector\":{\"Name\":\"%s\"}}", name) resultsIterator,err:= stub.GetQueryResult(queryString)//必须是CouchDB才行 if err!=nil{ return shim.Error("Rich query failed") } students,err:=getListResult(resultsIterator) if err!=nil{ return shim.Error("Rich query failed") } return shim.Success(students) }
对同一个数据(也就是Key相同)的更改,会记录到区块链中,咱们能够经过GetHistoryForKey方法得到这个对象在区块链中记录的更改历史,包括是在哪一个TxId,修改的数据,修改的时间戳,以及是不是删除等。好比以前的Student:1这个对象,咱们更改和删除过数据,如今要查询这个对象的更改记录,那么对应代码为:
func (t *SimpleChaincode) testHistoryQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{ student1:=Student{1,"Devin Zeng"} key:="Student:"+strconv.Itoa(student1.Id) it,err:= stub.GetHistoryForKey(key) if err!=nil{ return shim.Error(err.Error()) } var result,_= getHistoryListResult(it) return shim.Success(result) } func getHistoryListResult(resultsIterator shim.HistoryQueryIteratorInterface) ([]byte,error){ defer resultsIterator.Close() // buffer is a JSON array containing QueryRecords var buffer bytes.Buffer buffer.WriteString("[") bArrayMemberAlreadyWritten := false for resultsIterator.HasNext() { queryResponse, err := resultsIterator.Next() if err != nil { return nil, err } // Add a comma before array members, suppress it for the first array member if bArrayMemberAlreadyWritten == true { buffer.WriteString(",") } item,_:= json.Marshal( queryResponse) buffer.Write(item) bArrayMemberAlreadyWritten = true } buffer.WriteString("]") fmt.Printf("queryResult:\n%s\n", buffer.String()) return buffer.Bytes(), nil }
这个我在前面3.3已经说过了,只是由于那个函数便是复合键的,也是高级查询的,因此我在这里给这个函数留了一个位置。
这个比较好理解,就是在咱们的链上代码中调用别人已经部署好的链上代码。好比官方提供的example02,咱们要在代码中去实现a->b的转帐,那么咱们的代码应该以下:
func (t *SimpleChaincode) testInvokeChainCode(stub shim.ChaincodeStubInterface, args []string) pb.Response{ trans:=[][]byte{[]byte("invoke"),[]byte("a"),[]byte("b"),[]byte("11")} response:= stub.InvokeChaincode("mycc",trans,"mychannel") fmt.Println(response.Message) return shim.Success([]byte( response.Message)) }
这里须要注意,咱们使用的是example02的链上代码的实例名mycc,而不是代码的名字example02.
从客户端发现背书节点的Transaction或者Query都是一个提案,GetSignedProposal得到当前的提案对象包括客户端对这个提案的签名。提案的内容若是直接打印出来感受就像是乱码,其内包含了提案Header,Payload和Extension,里面更包含了复杂的结构,这里不讲,之后能够写一篇博客专门研究提案对象。
Transient是在提案中Payload对象中的一个属性,也就是ChaincodeProposalPayload.TransientMap
交易时间戳也是在提案对象中获取的,提案对象的Header部分,也就是proposal.Header.ChannelHeader.Timestamp
这个Binding对象也是从提案对象中提取并组合出来的,其中包含proposal.Header中的SignatureHeader.Nonce,SignatureHeader.Creator和ChannelHeader.Epoch。关于Proposal对象确实很8复杂,我目前了解的并不对,接下来得详细研究。
当ChainCode提交完毕,会经过Event的方式通知Client。而通知的内容能够经过SetEvent设置。
func (t *SimpleChaincode) testEvent(stub shim.ChaincodeStubInterface, args []string) pb.Response{ tosend := "Event send data is here!" err := stub.SetEvent("evtsender", []byte(tosend)) if err != nil { return shim.Error(err.Error()) } return shim.Success(nil) }
事件设置完毕后,须要在客户端也作相应的修改。因为我如今尚未作Application的开发,因此了解的还不够。之后也须要写一篇博客探讨这个话题。
最后,你们若是想进一步探讨Fabric或者使用中遇到什么问题能够加入QQ群【494085548】你们一块儿讨论。