# go语言fabric_sdk技术总结 **Repository Path**: LeeMH647/go-sdk-backend-conclude ## Basic Information - **Project Name**: go语言fabric_sdk技术总结 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-12-22 - **Last Updated**: 2021-12-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # go语言fabirc_sdk后端技术总结 ## *** ## 1.sdk配置 * 用GoLand打开该sdk后,默认使用该module :github.com/BSNDA/PCNGateway-Go-SDK 需要注意的是 如果想要更换module名,则重新配置起来比较复杂,并且在每个页面的import中,例如main.go有: ``` import ( "fmt" "github.com/BSNDA/PCNGateway-Go-SDK/pkg/client/fabric" "github.com/BSNDA/PCNGateway-Go-SDK/pkg/core/config" "github.com/BSNDA/PCNGateway-Go-SDK/router" ) ``` ​ 则需要将 github.com/BSNDA/PCNGateway-Go-SDK 这前半部分改为自己的module名,如:“loveserver/router” ​ 还需要注意的是,在pkg目录和third_part目录下有三种联盟链:fabric、fisco-bcos(金融区块链合作联盟)和xuperchain(百度超级链),使用fabric则将其他两个的文件夹 删除即可。 * 下载依赖: 使用go mod tidy命令将所有依赖下载至mod中,直到所有依赖变绿即可,此时运行该项目,它会报错,提示你还有一些依赖没有下载,此时按照提示,在终端中输入go get “ “命令一个一个将依赖下载完成即可正常运行。 *** ## ## 2.sdk初始化 ### 需要使用以下函数方法来实现客户端的初始化 * 初始化config :config.NewConfig(api, userCode, appCode, puk, prk, mspDir, cert) 该函数有6个入参,分别是api、userCode(用户码)、appCode、puk(公钥)、prk(私钥)、mspDir(证书存储目录)、 cert(网络证书),需要注意的是最后两个证书只针对非托管应用,而本次项目都使用的是托管应用,所以无需填写,其他参数则是在链码部署上BSN后即可获取到 ```go api := viper.GetString("bc.api") //PCN gateway address userCode := viper.GetString("bc.userCode") //user code appCode := viper.GetString("bc.appCode") //DApp code puk := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV5+GtbQNzwfPsMfVIFNBg3xgRuwV\n2RDS+Rjd72F6qXGwrb8QLSDUitNinENWuowAEsu/U9GsfSn0A48xlRTfSg==\n-----END PUBLIC KEY-----" //public key prk := "-----BEGIN PRIVATE KEY-----\\n-----END PRIVATE KEY-----" //private key mspDir := "" //cert storage directory cert := "" //cert config, err := config.NewConfig(api, userCode, appCode, puk, prk, mspDir, cert) if err != nil { log.Fatal(err) } ``` 需要注意的是,config.NewConfig的返回值是Config结构体,需要用该结构体来初始化客户端 * 初始化client : client,err:=fabric.InitFabricClient(config) 该函数的输入是config 输出是Client 结构体 需要注意的是,client对象可以使用所有的方法,所以要想对区块链进行操作,必须得有前两步生成client对象 * 封装初始化方法: 由于初始化的最终目的是使用client对象,所以需要进行必要的封装: ```go //初始化客户端 func BCinit() *fabric.FabricClient { api := viper.GetString("bc.api") //PCN gateway address userCode := viper.GetString("bc.userCode") //user code appCode := viper.GetString("bc.appCode") //DApp code puk := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV5+GtbQNzwfPsMfVIFNBg3xgRuwV\n2RDS+Rjd72F6qXGwrb8QLSDUitNinENWuowAEsu/U9GsfSn0A48xlRTfSg==\n-----END PUBLIC KEY-----" //public key prk := "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVbS8svrC3sdFgIFW\nUoLfczfjCjq0z7fGTSFfqMkjssChRANCAARXn4a1tA3PB8+wx9UgU0GDfGBG7BXZ\nENL5GN3vYXqpcbCtvxAtINSK02KcQ1a6jAASy79T0ax9KfQDjzGVFN9K\n-----END PRIVATE KEY-----" //private key mspDir := "" //cert storage directory cert := "" //cert config, err := config.NewConfig(api, userCode, appCode, puk, prk, mspDir, cert) if err != nil { log.Fatal(err) } client,err:=fabric.InitFabricClient(config) return client } ``` *** ## 3.后端操作区块链 * 使用 client.ReqChainCode(node.TransReqDataBody(trans))方法来实现对区块链的操作 首先需要配置 trans对象:trans.Args(上链入参,切片类型,具体细节写在后面文档)、trans.ChainCode(链码名)、trans.FuncName(链码的功能名,链码部署后会给每个方法设置funcname,如地址上链操作就可以设置为”SetAdress“)、trans.Nonce(对随机数进行base64加密编码生成Nonce,具体细节写在后面文档) 配置完成后调用client.ReqChainCode方法,该方法会返回 tranResData结构体,块中的数据是由tranResData.body也就是TranResDataBody结构体实现,该结构体分别封装了两个结构体BlockInfo、CCRes ```go type TranResData struct { base.BaseResModel Body *TranResDataBody `json:"body"` } type TranResDataBody struct { BlockInfo BlockInfo `json:"blockInfo"` CCRes CCRes `json:"ccRes"` } type BlockInfo struct { TxId string `json:"txId"` //交易ID BlockHash string `json:"blockHash"` //区块哈希 Status int `json:"status"` } type CCRes struct { CCCode int `json:"ccCode"` //操作成功则返回200 CCData string `json:"ccData"` } ``` 需要注意的是CCData这个参数,该参数是由链码定义的返回值,如地址上链成功链码可以定义CCdata:”地址上链成功“,查询用户的收获地址则 CCdata:用户的收货地址(链码实现查询功能,将返回值传到CCdata中) * 用户地址上链操作示例(MVC): ```go //model层: //用户住址上链 func SetLocation(message string ) (string, error) { //随机数nonce生成 rand.Seed(time.Now().UnixNano())//放在最外层 bytes:=RandomByte(24) nonce:=Base64Encrypt(bytes) //传入string var orderdata = []string {message} var trans node.TransReqDataBody trans.Args=orderdata trans.ChainCode=viper.GetString("bc.chaincode") trans.FuncName="set" //链码功能名 trans.Nonce=nonce client:=BCinit() //初始化客户端 res,err:=client.ReqChainCode(node.TransReqDataBody(trans)) if err != nil { log.Fatal(err) } var messages BCMessage messages.Body=res.Body.CCRes.CCData return messages.Body,nil } //handler层: //用户地址上链 func SetUserLocation(c *gin.Context) { buf := &bytes.Buffer{} tea := io.TeeReader(c.Request.Body, buf) body, err := ioutil.ReadAll(tea) if err!=nil{ log.Fatal(err.Error())} data:=string(body) fmt.Println(body) //上链函数 message,err:=model.SetLocation(data) if err != nil { SendResponse(c, errno.ErrDatabase, nil) return } if (message=="SUCCESS"){ var suc string="用户地址上链成功!" SendResponse(c, nil, suc)} else { var faild string="上链失败!" SendResponse(c, nil, faild)} } 路由层: func loadUserLocation(v1 *gin.RouterGroup){ v1.Use() { v1.POST("/getuserlocation", handler.GetUserLocation) v1.POST("/setuserlocation", handler.SetUserLocation) } } ``` *** ## 4.传参和结构体接收参数问题 * 后端需要传入什么样的参数给链码函数,则是由链码定义的,所以有以下参数输入路径: ​ 前端 —> 后端—>链码 所以有两种参数拼接方式(根据链码的需求): 1.前端(参数拼接完成)—>后端直接解析参数转化为string,原封不动传给trans.Args—>链码接收 2.前端(json格式参数传入)—>后端结构体接收参数,对每个参数进行拼接,拼接为一个string类型的整体参数, ​ 传给trans.Args—>链码接收 在我看来方法一是最直截了当的,链码需要什么参数,前端根据输入直接完成拼接,后端接收参数直接返回给链码,过程比较简洁, 而方法二则需要根据前端的入参来设置结构体,将前端传入的json参数解析给每个结构体,再将结构体的参数拼接为一个string传给链码(可能是自身水平原因,拼接操作做的不好,到底是使用方法一还是方法二,则需要前后端沟通达成共识) * 快递地址上链操作示例: 根据链码的需求,快递地址上链需要的参数为: ```json { "BaseKey":"sf00027", "BaseValue": [ {"Node":"北京","NextAdr":"武汉"}, {"Node":"武汉","NextAdr":"郑州"}, {"Node":"郑州","NextAdr":"安徽"} ] } ``` * 此时我们选择方法一,也就是前端直接拼接成这种类型,后端直接接收传参即可 前端根据上述参数需求拼接完成后,后端接收代码 ```go //在handler中: buf := &bytes.Buffer{} tea := io.TeeReader(c.Request.Body, buf) body, err := ioutil.ReadAll(tea) //读取前端的输入,并生成为body对象,此时body为 []byte类型 if err!=nil{ log.Fatal(err.Error())} data:=string(body) //将[]byte类型转换为string类型(因为trans.Args是string类型的切片) ``` * 方法二(未完全实现) ```go //地址结构体 type Adresses struct { Node string `json:"node"` NextAdr string `json:"nextadr"` } //订单结构体 type OrderInfo struct{ OrderId string `json:"orderid"` Adresses []Adresses `json:"adresses"` //切片类型 } //前端字符串json解析操作 func Decode (body []byte ) OrderInfo { var oderinfo OrderInfo err1 := json.Unmarshal(body,&oderinfo) //json.Unmarshal该方法将[]byte类型的入参解析绑定到结构体中 if err1 != nil { fmt.Println(err1) } return oderinfo } //将参数解析到结构体中 handler buf := &bytes.Buffer{} tea := io.TeeReader(c.Request.Body, buf) body, err := ioutil.ReadAll(tea) if err!=nil{ log.Fatal(err.Error())} oderinfo :=model.Decode(body) c.Writer.WriteString(oderinfo.OrderId+"/ "+oderinfo.Adresses[1].Node+"/ "+oderinfo.Adresses[1].NextAdr) //试着输出下参数绑定成功的结构体 //最后将参数拼接(未完成) ``` 关于复杂json解析请参考这篇文章:https://blog.csdn.net/kevin_tech/article/details/105213359 *** ## ## 5.生成随机数和base64加密 * 由于操作区块链需要配置随机数参数trans.Nonce,所以需要在go语言中实现 ```go //需要的包 import ( "encoding/base64" "math/rand" "time" ) //生成24位伪随机数int // Returns an int >= min, < max func randomInt(min, max int) int { return min + rand.Intn(max-min) } // Generate a random string of A-Z chars with len = l //因为对随机数进行base64加密需要byte类型 所以还需要转化 func RandomByte(len int) []byte { //将int转化为byte类型 //这两步比较经典 bytes := make([]byte, len) for i := 0; i < len; i++ { bytes[i] = byte(randomInt(65, 90)) } // ms:= string(bytes) return bytes } //对随机数进行base64加密编码生成Nonce func Base64Encrypt(body []byte) string{ encodeString := base64.StdEncoding.EncodeToString(body) return encodeString } //主函数: rand.Seed(time.Now().UnixNano())//放在最外层,通过时间来生成随机数 bytes:=RandomByte(24)//生成 byte类型的随机数 nonce:=Base64Encrypt(bytes)//base64加密 ``` *** ## 6.一些未尽事宜 * 每次使用链操作方法都要初始化客户端,原因在于对client对象进行操作,是否可以在一个地方初始化好后生成了client对象,直接调用该对象来操作区块链方法(go语言能力需要进一步加强) * 后端参数拼接问题还是需要加强,不可能每个人前端都有能力拼接好了供后端使用,打铁还需自身硬 * viper读取配置信息的问题,对于公钥、私钥的读取还存在一些问题,可能是格式上存在一些不同 * 查询或者上链失败的时候,ccdata参数会出现问题,导致系统飘红,所以响应前端的时候,只能做判断 ccdata是否等于查询成功返回的参数,其实可能返回值中还有其他的参数来对应查询或者上链失败,这是很重要的一点来完善前后端的逻辑,但是我因为时间的问题还没做到 * 还有很多小细节也可以优化 对于这次区块链竞赛的后端,我觉得在短期的时间内做成这样已经算是不错了,因为一切都是未知的,我没有相应的经验,遇到的问题都是网上搜索队友相互讨论来解决的,从0到1还是实现了一些技术上的问题,下次做区块链后端时,有了这次的经验,再做一些改进,相信会越来越好的!