搭建基于hyperledger fabric的联盟社区(六) --搭建node.js服务器
接下来我要做的是用fabric sdk来做出应用程序,代替CLI与整个区块链网络交互。并且实现一个http API,向社区提供一个简单的接口,使社区轻松的与区块链交互。
官方虽然提供了Node.JS,Java,Go(最近刚出了python)等多种语言的SDK,但是很多SDK还不成熟和完善,有的甚至文档都没有。我使用Node.js的原因有三。1.官方例子使用的是Node.js SDK 2.以前我做以太坊智能合约开发的时候也用过Node.js 3.最后是因为Node.js实现一个http服务器是非常简单的。
我们可以选择将node.js安装在本地(1.1介绍),或者将node应用部署在docker。需要注意的是fabric目前不支持node7.x版本,需要6.9.x或更高版本和NPM。
本地安装node的话我们只需要执行以下命令即可安装NodeJS的最新v6版本:
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - sudo apt-get install -y nodejs
国内使用NPM会遇到很多问题,可用淘宝NPM镜像:
npm install -g cnpm --registry=https://registry.npm.taobao.org
使用方法是用cnpm代替npm
而我这里主要介绍的方法是将node.js部署在docker。
一. 先拉取基础镜像
sudo docker pull node:6.11.3
再提醒一遍fabric目前不支持node7.x版本,需要6.9.x或更高版本。这一步,也可以省略,后面的Dockerfile文件,会自动拉取该镜像。
二.创建node.js程序
2.1创建 package.json,并写入相关信息和依赖
mkdir -p node/mynodeapp && cd node/mynodeapp
touch package.json
vi package.json
{ "name": "nodeTest", "version": "1.0.0", "description": "Hyperledger Fabric Node SDK Test Application", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { "fabric-ca-client": "^1.0.0", "fabric-client": "^1.0.0" }, "author": "Devin Zeng", "license": "Apache-2.0", "keywords": [ "Hyperledger", "Fabric", "Test", "Application" ] }
2.2 创建server.js
将与fabric交互的代码放在这里,监听8888端口。
touch server.js
vi server.js
由于我们这次是为peer0.org2开发API,所以本机IP地址为10.0.2.12,代码如下
1 var http = require('http'); 2 var url = require('url'); 3 4 http.createServer(function(req, res){ 5 var arg = url.parse(req.url, true).query; //方法二arg => { aa: '001', bb: '002' } 6 console.log(arg.func);//返回001 7 if (arg.func == "queryPost" || arg.func == "richQueryPosts" || arg.func == "getPostNum"){ 8 query(arg); 9 }else if (arg.func == "addPost" || arg.func == "updatePost"){ 10 invoke(arg); 11 } 12 }).listen(8888);//建立服务器并监听端口 13 14 console.log('Server running at http://127.0.0.1:8888/'); 15 16 17 18 19 20 function query(arg){ 21 'use strict'; 22 23 var hfc = require('fabric-client'); 24 var path = require('path'); 25 var sdkUtils = require('fabric-client/lib/utils') 26 var fs = require('fs'); 27 var options = { 28 user_id: 'Admin@org2.example.com', 29 msp_id:'Org2MSP', 30 channel_id: 'mychannel', 31 chaincode_id: 'mycc', 32 network_url: 'grpcs://10.0.2.12:7051',//因为启用了TLS,所以是grpcs,如果没有启用TLS,那么就是grpc 33 privateKeyFolder: path.join(__dirname,'./crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/keystore'), 34 signedCert: path.join(__dirname,'./crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/signcerts/Admin@org2.example.com-cert.pem'), 35 tls_cacerts:path.join(__dirname,'./crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt'), 36 server_hostname: "peer0.org2.example.com" 37 }; 38 39 var channel = {}; 40 var client = null; 41 const getKeyFilesInDir = (dir) => { 42 //该函数用于找到keystore目录下的私钥文件的路径 43 var files = fs.readdirSync(dir) 44 var keyFiles = [] 45 files.forEach((file_name) => { 46 let filePath = path.join(dir, file_name) 47 if (file_name.endsWith('_sk')) { 48 keyFiles.push(filePath) 49 } 50 }) 51 return keyFiles 52 } 53 Promise.resolve().then(() => { 54 console.log("Load privateKey and signedCert"); 55 client = new hfc(); 56 var createUserOpt = { 57 username: options.user_id, 58 mspid: options.msp_id, 59 cryptoContent: { privateKey: getKeyFilesInDir(options.privateKeyFolder)[0], 60 signedCert: options.signedCert } 61 } 62 //以上代码指定了当前用户的私钥,证书等基本信息 63 return sdkUtils.newKeyValueStore({ 64 path: "/tmp/fabric-client-stateStore/" 65 }).then((store) => { 66 client.setStateStore(store) 67 return client.createUser(createUserOpt) 68 }) 69 }).then((user) => { 70 channel = client.newChannel(options.channel_id); 71 72 let data = fs.readFileSync(options.tls_cacerts); 73 let peer = client.newPeer(options.network_url, 74 { 75 pem: Buffer.from(data).toString(), 76 'ssl-target-name-override': options.server_hostname 77 } 78 ); 79 peer.setName("peer0"); 80 //因为启用了TLS,所以上面的代码就是指定TLS的CA证书 81 channel.addPeer(peer); 82 return; 83 }).then(() => { 84 console.log("Make query"); 85 var transaction_id = client.newTransactionID(); 86 console.log("Assigning transaction_id: ", transaction_id._transaction_id); 87 //构造查询request参数 88 if(arg.func=="queryPost"){ 89 const request = { 90 chaincodeId: options.chaincode_id, 91 txId: transaction_id, 92 fcn: 'queryPost', 93 args: ["POST"+arg.id] 94 }; 95 }else if(arg.func=="richQueryPosts"){ 96 const request = { 97 chaincodeId: options.chaincode_id, 98 txId: transaction_id, 99 fcn: 'richQueryPosts', 100 args: [arg.attribute,arg.operator,arg.value] 101 }; 102 }else if(arg.func=="getPostNum"){ 103 const request = { 104 chaincodeId: options.chaincode_id, 105 txId: transaction_id, 106 fcn: 'getPostNum', 107 args: [arg.attribute,arg.operator,arg.value] 108 }; 109 } 110 return channel.queryByChaincode(request); 111 }).then((query_responses) => { 112 console.log("returned from query"); 113 if (!query_responses.length) { 114 console.log("No payloads were returned from query"); 115 } else { 116 console.log("Query result count = ", query_responses.length) 117 } 118 if (query_responses[0] instanceof Error) { 119 console.error("error from query = ", query_responses[0]); 120 } 121 res.writeHead(200, {'Content-Type': 'text/plain'}); 122 res.end(query_responses[0]); 123 console.log("Response is ", query_responses[0].toString());//打印返回的结果 124 }).catch((err) => { 125 console.error("Caught Error", err); 126 }); 127 } 128 129 130 131 132 133 134 function invoke(arg){ 135 'use strict'; 136 137 var hfc = require('fabric-client'); 138 var path = require('path'); 139 var util = require('util'); 140 var sdkUtils = require('fabric-client/lib/utils') 141 const fs = require('fs'); 142 var options = { 143 user_id: 'Admin@org2.example.com', 144 msp_id:'Org2MSP', 145 channel_id: 'mychannel', 146 chaincode_id: 'mycc', 147 peer_url: 'grpcs://10.0.2.12:7051',//因为启用了TLS,所以是grpcs,如果没有启用TLS,那么就是grpc 148 event_url: 'grpcs://10.0.2.12:7053',//因为启用了TLS,所以是grpcs,如果没有启用TLS,那么就是grpc 149 orderer_url: 'grpcs://10.0.2.10:7050',//因为启用了TLS,所以是grpcs,如果没有启用TLS,那么就是grpc 150 privateKeyFolder: path.join(__dirname,'./crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/keystore'), 151 signedCert:path.join(__dirname,'./crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/signcerts/Admin@org2.example.com-cert.pem'), 152 peer_tls_cacerts: path.join(__dirname,'./crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt'), 153 orderer_tls_cacerts:path.join(__dirname,'./crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt'), 154 server_hostname: "peer0.org2.example.com" 155 }; 156 157 var channel = {}; 158 var client = null; 159 var targets = []; 160 var tx_id = null; 161 const getKeyFilesInDir = (dir) => { 162 //该函数用于找到keystore目录下的私钥文件的路径 163 const files = fs.readdirSync(dir) 164 const keyFiles = [] 165 files.forEach((file_name) => { 166 let filePath = path.join(dir, file_name) 167 if (file_name.endsWith('_sk')) { 168 keyFiles.push(filePath) 169 } 170 }) 171 return keyFiles 172 } 173 Promise.resolve().then(() => { 174 console.log("Load privateKey and signedCert"); 175 client = new hfc(); 176 var createUserOpt = { 177 username: options.user_id, 178 mspid: options.msp_id, 179 cryptoContent: { privateKey: getKeyFilesInDir(options.privateKeyFolder)[0], 180 signedCert: options.signedCert } 181 } 182 //以上代码指定了当前用户的私钥,证书等基本信息 183 return sdkUtils.newKeyValueStore({ 184 path: "/tmp/fabric-client-stateStore/" 185 }).then((store) => { 186 client.setStateStore(store) 187 return client.createUser(createUserOpt) 188 }) 189 }).then((user) => { 190 channel = client.newChannel(options.channel_id); 191 let data = fs.readFileSync(options.peer_tls_cacerts); 192 let peer = client.newPeer(options.peer_url, 193 { 194 pem: Buffer.from(data).toString(), 195 'ssl-target-name-override': options.server_hostname 196 } 197 ); 198 //因为启用了TLS,所以上面的代码就是指定Peer的TLS的CA证书 199 channel.addPeer(peer); 200 //接下来连接Orderer的时候也启用了TLS,也是同样的处理方法 201 let odata = fs.readFileSync(options.orderer_tls_cacerts); 202 let caroots = Buffer.from(odata).toString(); 203 var orderer = client.newOrderer(options.orderer_url, { 204 'pem': caroots, 205 'ssl-target-name-override': "orderer.example.com" 206 }); 207 208 channel.addOrderer(orderer); 209 targets.push(peer); 210 return; 211 }).then(() => { 212 tx_id = client.newTransactionID(); 213 console.log("Assigning transaction_id: ", tx_id._transaction_id); 214 if(arg.func=="addPost"){ 215 var request = { 216 targets: targets, 217 chaincodeId: options.chaincode_id, 218 fcn: 'addPost', 219 args: [arg.originalwebsite,arg.originalid,arg.title,arg.content,arg.authorid,arg.publishtime,arg.updatetime,arg.category,arg.sourceid,arg.labels,arg.follower_num,arg.browse_num,arg.star_num], 220 chainId: options.channel_id, 221 txId: tx_id 222 }; 223 }else if(arg.func=="updatePost"){ 224 var request = { 225 targets: targets, 226 chaincodeId: options.chaincode_id, 227 fcn: 'updatePost', 228 args: [arg.id,arg.originalwebsite,arg.originalid,arg.title,arg.content,arg.authorid,arg.publishtime,arg.updatetime,arg.category,arg.sourceid,arg.labels,arg.follower_num,arg.browse_num,arg.star_num], 229 chainId: options.channel_id, 230 txId: tx_id 231 }; 232 } 233 return channel.sendTransactionProposal(request); 234 }).then((results) => { 235 var proposalResponses = results[0]; 236 var proposal = results[1]; 237 var header = results[2]; 238 let isProposalGood = false; 239 if (proposalResponses && proposalResponses[0].response && 240 proposalResponses[0].response.status === 200) { 241 isProposalGood = true; 242 console.log('transaction proposal was good'); 243 } else { 244 console.error('transaction proposal was bad'); 245 } 246 if (isProposalGood) { 247 console.log(util.format( 248 'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s', 249 proposalResponses[0].response.status, proposalResponses[0].response.message, 250 proposalResponses[0].response.payload, proposalResponses[0].endorsement.signature)); 251 console.log(proposalResponses[0].response.payload.toString()); 252 res.writeHead(200, {'Content-Type': 'text/plain'}); 253 res.end(proposalResponses[0].response.payload.toString()); 254 var request = { 255 proposalResponses: proposalResponses, 256 proposal: proposal, 257 header: header 258 }; 259 // set the transaction listener and set a timeout of 30sec 260 // if the transaction did not get committed within the timeout period, 261 // fail the test 262 var transactionID = tx_id.getTransactionID(); 263 var eventPromises = []; 264 let eh = client.newEventHub(); 265 //接下来设置EventHub,用于监听Transaction是否成功写入,这里也是启用了TLS 266 let data = fs.readFileSync(options.peer_tls_cacerts); 267 let grpcOpts = { 268 pem: Buffer.from(data).toString(), 269 'ssl-target-name-override': options.server_hostname 270 } 271 eh.setPeerAddr(options.event_url,grpcOpts); 272 eh.connect(); 273 274 let txPromise = new Promise((resolve, reject) => { 275 let handle = setTimeout(() => { 276 eh.disconnect(); 277 reject(); 278 }, 30000); 279 //向EventHub注册事件的处理办法 280 eh.registerTxEvent(transactionID, (tx, code) => { 281 clearTimeout(handle); 282 eh.unregisterTxEvent(transactionID); 283 eh.disconnect(); 284 285 if (code !== 'VALID') { 286 console.error( 287 'The transaction was invalid, code = ' + code); 288 reject(); 289 } else { 290 console.log( 291 'The transaction has been committed on peer ' + 292 eh._ep._endpoint.addr); 293 resolve(); 294 } 295 }); 296 }); 297 eventPromises.push(txPromise); 298 var sendPromise = channel.sendTransaction(request); 299 return Promise.all([sendPromise].concat(eventPromises)).then((results) => { 300 console.log(' event promise all complete and testing complete'); 301 return results[0]; // the first returned value is from the 'sendPromise' which is from the 'sendTransaction()' call 302 }).catch((err) => { 303 console.error( 304 'Failed to send transaction and get notifications within the timeout period.' 305 ); 306 return 'Failed to send transaction and get notifications within the timeout period.'; 307 }); 308 } else { 309 console.error( 310 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...' 311 ); 312 return 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'; 313 } 314 }, (err) => { 315 console.error('Failed to send proposal due to error: ' + err.stack ? err.stack : 316 err); 317 return 'Failed to send proposal due to error: ' + err.stack ? err.stack : 318 err; 319 }).then((response) => { 320 if (response.status === 'SUCCESS') { 321 console.log('Successfully sent transaction to the orderer.'); 322 return tx_id.getTransactionID(); 323 } else { 324 console.error('Failed to order the transaction. Error code: ' + response.status); 325 return 'Failed to order the transaction. Error code: ' + response.status; 326 } 327 }, (err) => { 328 console.error('Failed to send transaction due to error: ' + err.stack ? err 329 .stack : err); 330 return 'Failed to send transaction due to error: ' + err.stack ? err.stack : 331 err; 332 }); 333 }
三.创建Dockerfile
Docker会依照Dockerfile的内容来构建一个镜像。
cd ..
touch Dockerfile
vi Dockerfile
#设置基础镜像,如果本地没有该镜像,会从Docker.io服务器pull镜像
FROM node:6.11.3
#创建app目录,保存我们的代码
RUN mkdir -p /usr/src/node
#设置工作目录
WORKDIR /usr/src/node
#复制所有文件到 工作目录。
COPY . /usr/src/node
#编译运行node项目,使用npm安装程序的所有依赖,利用taobao的npm安装
WORKDIR /usr/src/node/mynodeapp
RUN npm install --registry=https://registry.npm.taobao.org
#暴露container的端口
EXPOSE 8888
#运行命令
CMD ["node", "server.js"]
四.构建image
在Dockerfile文件所在目录下,运行下面命令来构建一个Image
sudo docker build -t fabric/node .
最后提示
Removing intetmediate container <临时容器ID>
Successfully built <镜像ID>
就算构建成功了。(这一步时常失败,要看网络状况)
构建完后查看一下刚构建的镜像:
sudo docker images
多出来了一个fabric/node镜像。
五.运行镜像
sudo docker run -d --name nodeapp -p 8888:8888 fabric/node:latest
-d 表示容器在后台运行
--name 表示给容器别名 nodeapp
-p 表示端口映射。把本机的8888端口映射到容器的8888端口,这样外网就能通过本机的8888端口,访问我们的web了。
后面的 fabric/node 是image的REPOSITORY, latest的镜像的TAG
六.测试
我使用python写了一个简单的发送和接受http请求的程序,当然用插件发送请求来测试会更加便捷(推荐使用postman,十分便捷)。代码如下:
import urllib.request url = "http://localhost:8888/select?func=richQueryPosts&attribute=title&operator=0&value=d" req = urllib.request.Request(url) print(req) res_data = urllib.request.urlopen(req) res = res_data.read() print (res)
成功接收到包含对应帖子信息的response就说明node部署成功了。
之后对应的社区就可以调用HTTP API来完成社区和区块链的对接了!整个基于hyperledger fabric的社区联盟开发的内容就到这里了。