深藍前幾篇博客講了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】你們一塊兒討論。