
关注于区块链技术、跨链、密码学、通证经济、智能合约
暂时未有相关通用技术能力~
阿里云技能认证
详细说明最近在和同事交流我们PalletOne中对UTXO和签名的处理,有些心得,写下此博文。对比特币有点基本概念的都知道,比特币是通过ECDSA数字签名来解锁UTXO中的未花费余额。 关于UTXO我不需要做太多介绍,毕竟介绍这个概念的文章已经很多了。我主要是谈谈已经有UTXO了,该怎么花掉。 交易的结构 我们先来看看在比特币中,一个交易的结构是什么样的? type MsgTx struct { Version int32 TxIn []*TxIn TxOut []*TxOut LockTime uint32 } type TxOut struct { Value int64 PkScript []byte } type TxIn struct { PreviousOutPoint OutPoint SignatureScript []byte Sequence uint32 } type OutPoint struct { Hash chainhash.Hash Index uint32 } 我们可以看到,一个交易(MsgTx)是由多个Input和多个Output组成的,而在Input中是由指向UTXO的OutPoint,解锁脚本SignatureScript和序列Sequence组成。 UTXO我们可以认为是一个KeyValue的大表,在该表中,交易的Hash和该交易中Output所在的位置索引Index就构成了UTXO的Key,而Value就是比特币Amount、锁定脚本等信息,所以在UTXO数据库中,我们通过OutPoint能够很快的找到对应的Amount和锁定脚本。 在比特币中,要做一笔交易分为三个步骤: 构建原始交易RawTransaction,该交易包含了输入指向的OutPoint,也包含了完整的Output,但是没有签名,也就是没有设置SignatureScript的内容。 用私钥对签名构建的RawTransaction进行签名,并将签名构建成完整的解锁脚本,填入对应的Input的SignatureScript字段中。 将签名后的Transaction发送到P2P网络中。 构建原始交易RawTransaction 现在假设我有一个地址mx3KrUjRzzqYTcsyyvWBiHBncLrrTPXnkV(这是一个测试网地址),该地址收到了两笔转账,一笔0.4BTC(https://testnet.blockchain.info/tx-index/239152566/1),另一笔1.1BTC(https://testnet.blockchain.info/tx-index/239157459/1),这两笔收入都是在其交易Output的第二条,也就是Index=1(Index从0开始算)。现在我们想要做一笔1.2BTC的转账,然后给一定的手续费后,找零到原地址,所以我们会构建一笔交易,该交易有2Input和2Output。 以下是我用Go基于btcd写的示例代码,这里我们就构建好了一个RawTransaction。 func buildRawTx() *wire.MsgTx { //https://testnet.blockchain.info/tx/f0d9d482eb122535e32a3ae92809dd87839e63410d5fd52816fc9fc6215018cc?show_adv=true tx := wire.NewMsgTx(wire.TxVersion) //https://testnet.blockchain.info/tx-index/239152566/1 0.4BTC utxoHash, _ := chainhash.NewHashFromStr("1dda832890f85288fec616ef1f4113c0c86b7bf36b560ea244fd8a6ed12ada52") point := wire.OutPoint{Hash: *utxoHash, Index: 1} //构建第一个Input,指向一个0.4BTC的UTXO,第二个参数是解锁脚本,现在是nil tx.AddTxIn(wire.NewTxIn(&point, nil, nil)) //https://testnet.blockchain.info/tx-index/239157459/1 1.1BTC utxoHash2, _ := chainhash.NewHashFromStr("24f284aed2b9dbc19f0d435b1fe1ee3b3ddc763f28ca28bad798d22b6bea0c66") point2 := wire.OutPoint{Hash: *utxoHash2, Index: 1} //构建第二个Input,指向一个1.1BTC的UTXO,第二个参数是解锁脚本,现在是nil tx.AddTxIn(wire.NewTxIn(&point2, nil, nil)) //找零的地址(这里是16进制形式,变成Base58格式就是mx3KrUjRzzqYTcsyyvWBiHBncLrrTPXnkV) pubKeyHash, _ := hex.DecodeString("b5407cec767317d41442aab35bad2712626e17ca") lock, _ := txscript.NewScriptBuilder().AddOp(txscript.OP_DUP).AddOp(txscript.OP_HASH160). AddData(pubKeyHash).AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG). Script() //构建第一个Output,是找零0.2991024 BTC tx.AddTxOut(wire.NewTxOut(29910240, lock)) //支付给了某个地址,仍然是16进制形式,Base58形式是:mxqnGTekzKqnMqNFHKYi8FhV99WcvQGhfH。 pubKeyHash2, _ := hex.DecodeString("be09abcbfda1f2c26899f062979ab0708731235a") lock2, _ := txscript.NewScriptBuilder().AddOp(txscript.OP_DUP).AddOp(txscript.OP_HASH160). AddData(pubKeyHash2).AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG). Script() //构建第二个Output,支付1.2 BTC出去 tx.AddTxOut(wire.NewTxOut(120000000, lock2)) return tx } 交易的签名过程 现在我们知道私钥,需要对该交易进行签名,因为有2个Input,所以我们要签名2次,每个签名的原理是一样的,我就以第一个Input为例来说明吧。 在比特币中,对一笔交易的签名流程是这样的: 1.查找该笔交易对应的UTXO 2.获得该UTXO对应的锁定脚本 3.复制该交易对象,并在复制副本中将该Input的解锁脚本字段的值设置为对应的锁定脚本 4.清除其他Input的解锁脚本字段 5.对这个改造后的交易对象计算Hash 6.使用私钥对Hash进行签名。 用表格的形式可以更容易表达: 这是原始未签名的交易RawTransaction,主要是第二列和第三列: UTXO Input Output TxHash:1dda832890f85288fec616ef1f4113c0c86b7bf36b560ea244fd8a6ed12ada52, OutIndex:1, Amount:0.4BTC,PkScript: OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG PreviousOutPoint={ TxHash:1dda832890f85288fec616ef1f4113c0c86b7bf36b560ea244fd8a6ed12ada52, OutIndex:1} SignatureScript =NULL,Sequence =0xFFFFFFFF Value=29910240 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG TxHash:24f284aed2b9dbc19f0d435b1fe1ee3b3ddc763f28ca28bad798d22b6bea0c66, OutIndex:1, Amount:1.1BTC,PkScript: OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG PreviousOutPoint={ TxHash:24f284aed2b9dbc19f0d435b1fe1ee3b3ddc763f28ca28bad798d22b6bea0c66, OutIndex:1} SignatureScript =NULL,Sequence =0xFFFFFFFF Value=120000000 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG 接下来我们要对第一个Input签名,于是我们需要将交易复制一个副本,并改为: Input Output PreviousOutPoint={ TxHash:1dda832890f85288fec616ef1f4113c0c86b7bf36b560ea244fd8a6ed12ada52, OutIndex:1} SignatureScript = OP_DUP OP_HASH160 PUSHDATA(20) b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG ,Sequence =0xFFFFFFFF Value=29910240 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG PreviousOutPoint={ TxHash:24f284aed2b9dbc19f0d435b1fe1ee3b3ddc763f28ca28bad798d22b6bea0c66, OutIndex:1} SignatureScript =NULL,Sequence =0xFFFFFFFF Value=120000000 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG 接下来对这个交易计算Hash,然后进行签名。得到签名结果:3045022100c435eb458b295381d6e1f489b8683d1b10ecad0a7691949a4ae7ffee74bd22ae022031e47b9ebed5b90f6d51cd05e6f53bdc59f5d6d754aff14a88a6e8659b5fdad501 而我们知道这个地址的公钥是:038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb 所以签完名后,我们的交易变成: Input Output PreviousOutPoint={ TxHash:1dda832890f85288fec616ef1f4113c0c86b7bf36b560ea244fd8a6ed12ada52, OutIndex:1} SignatureScript = PUSHDATA(72)[3045022100c435eb458b295381d6e1f489b8683d1b10ecad0a7691949a4ae7ffee74bd2 2ae022031e47b9ebed5b90f6d51cd05e6f53bdc59f5d6d754aff14a88a6e8659b5fdad501] PUSHDATA(33)[038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb] ,Sequence =0xFFFFFFFF Value=29910240 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG PreviousOutPoint={ TxHash:24f284aed2b9dbc19f0d435b1fe1ee3b3ddc763f28ca28bad798d22b6bea0c66, OutIndex:1} SignatureScript =NULL,Sequence =0xFFFFFFFF Value=120000000 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG 这才只是完成了第一个Input的签名,接下来我们再对第二个Input进行签名,同样的道理,我们需要制造一个交易的副本,然后把第一个Input的SignatureScript清空,然后给第二个Input的SignatureScript赋值: Input Output PreviousOutPoint={ TxHash:1dda832890f85288fec616ef1f4113c0c86b7bf36b560ea244fd8a6ed12ada52, OutIndex:1} SignatureScript =NULL,Sequence =0xFFFFFFFF Value=29910240 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG PreviousOutPoint={ TxHash:24f284aed2b9dbc19f0d435b1fe1ee3b3ddc763f28ca28bad798d22b6bea0c66, OutIndex:1} SignatureScript =OP_DUP OP_HASH160 PUSHDATA(20) b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG ,Sequence =0xFFFFFFFF Value=120000000 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG 显然这个副本与第一个签名时的数据是不一样的,所以签名结果也不一样,最终签名结果为:30440220196bce75f0a25ac8afa7218aefc86cba3924845450f3d311c89e9c2a3438a99c0220230bed598a610be971ca49690f4b42ac2acfa80c09d4cbabd278b03c824af14501,当然我们因为是同一个地址,所以公钥是一样的:038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb 我们把这个签名和公钥再放回原始交易中,就变成我们需要的完整签名的交易: Input Output PreviousOutPoint={ TxHash:1dda832890f85288fec616ef1f4113c0c86b7bf36b560ea244fd8a6ed12ada52, OutIndex:1} SignatureScript =PUSHDATA(72)[3045022100c435eb458b295381d6e1f489b8683d1b10ecad0a7691949a4ae7ffee74bd22ae022031e47b9ebed5b90f6d51cd05e6f53bdc59f5d6d754aff14a88a6e8659b5fdad501] PUSHDATA(33)[038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb] ,Sequence =0xFFFFFFFF Value=29910240 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG PreviousOutPoint={ TxHash:24f284aed2b9dbc19f0d435b1fe1ee3b3ddc763f28ca28bad798d22b6bea0c66, OutIndex:1} SignatureScript =PUSHDATA(71)[30440220196bce75f0a25ac8afa7218aefc86cba3924845450f3d311c89e9c2a3438a99c0220230bed598a610be971ca49690f4b42ac2acfa80c09d4cbabd278b03c824af14501] PUSHDATA(33)[038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb] ,Sequence =0xFFFFFFFF Value=120000000 PkScript= OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG 这就是一个真实完整的交易了,接下来就可以通过P2P网络发送该交易,并最终被矿工打包确认。 总结 实际上在比特币的源码中比我上面说的还要复杂一些,还涉及到这个hash是对整个交易进行SigHashAll还是SigHashSingle或者SigHashNone,这些都是很特殊的情况,一般的比特币钱包也不支持,具体可以参加精通比特币书中的介绍:6.5.3签名哈希类型( SIGHASH) 普通来说,我们要对一笔交易进行签名或者验签,就是把当前Input中的解锁脚本替换成锁定脚本,而其他Input的解锁脚本情况,然后计算Hash和签名! 其实我还是有点不明白,为什么比特币中不直接对没有任何解锁脚本的RawTransaction进行签名呢?而是非要加上锁定脚本来签名?不知道这里面有什么更深的考虑。 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
最近在研究区块链的时候关注了一下加密技术,小有心得,于是设计了一款数据加密共享与签名的方案,希望能够为做电子合同,数据存证,数据共享的朋友有所帮助吧。 业务场景 一、电子合同 Alice和Bob需要签订一个合同,而Charlie是中介,也需要在看到该合同上并签字,而Dave是外人,不参与这个合同的签订,所以不允许看到合同的内容。 二、数据存证 Alice和Bob在网上玩剪刀石头布的游戏,由于没有第三方的参与,所以Alice必须告诉Bob她已经做出了选择,但是同时又不能告诉Bob她具体出的是什么,Bob同样也是要告诉Alice他已经做出了选择,而又告诉她具体选择是什么。在双方都确认对方做出了选择后,然后各自公布自己的选择,并且可以验证对方公布的结果是不是跟之前告之的一致。 三、数据共享 Alice、Bob和Charlie是同事,他们在使用同一个公司网盘共享文件。现在有一个机密项目只有Alice和Bob在参与,他们希望继续通过公司网盘共享文件,但是同时也不希望Charlie能够看到他们共享的文件中的内容。后来新同事Dave加入到了该机密项目中,又希望Dave能够看到共享的文件内容。 涉及的密码学 1.对称加密 也就是说用明文通过密钥的加密后得到密文,使用同样的密钥就可以把密文解密为明文。常用的对称加密算法是3DES。 2.非对称加密 密钥是有一对(2个,1个叫公钥,1个叫私钥),使用公钥加密的信息,只有对应的私钥才能解密;使用私钥加密的信息,只有对应的公钥才能解密。公钥是可以公开出来的,私钥需要自己保存,不能让其他人知道。目前主要的非对称加密算法有RSA和椭圆曲线加密算法ECC。 3.哈希 哈希算法是一种摘要算法,对于任意长度的输入,都输出相同长度的结果,并且输出结果对输入具有敏感性,也就是说输入只是一个小小的变化,就会引起输出巨大的不同。另外哈希算法还需要扛对撞,也就是说我们不能轻易找到两个不同的输入,使得他们的哈希输出相同。常用的哈希算法有MD5, SHA256 。 4.数字签名 数字签名就是将哈希算法与非对称加密算法结合的一个最好应用。对于一条明文消息M,我们需要对其进行签名,那么首先就是计算该消息的摘要,也就是哈希值,得到H(M),然后再用我们的私钥对这个哈希值进行加密,结果就是数字签名。任何人拿到消息M和数字签名后,都可以用我们的公钥对数字签名进行解密,将解密结果与消息M的哈希值进行对比,如果相同,就说明M没有被更改,同时该签名也是我们签署的,而不可能是别人伪造的签名。 电子合同的签名方案 1.准备 每个用户都有自己的私钥和公钥,私钥由于私密性,所以需要加密保存,用户只有输入自己设置的口令后才能解密出自己的私钥。而用户的公钥则公开在系统上,所有用户都可访问。 2.加密合同 Alice现在准备好合同文件M,由于隐私的考虑,所以需要对合同文件加密,而这里加密采用的是对称加密算法,密钥是随机生成的,加密后的合同文件为密文M。Alice希望合同的乙方Bob还有就是中介Charlie能够看到这个合同(当然Alice本人也需要能看到合同),所以她将这个随机密钥用各自的公钥进行加密,加密后生成了3个密文:密文A、密文B、密文C。现在Alice就可以将加密后的合同密文M以及密文A、密文B、密文C放到网上。 3.签名合同 Alice不需要对明文的合同M进行签名,她需要的是在密文M上进行签名,也就是说先计算出密文M的哈希值“H(密文M)”,然后用自己的私钥A对该哈希值进行加密,这样就能得到签名A。现在Alice把签名A也放在网上,因为Alice的公钥是公开的,所以任何人都可以用公钥A来解密签名A,从而验证密文M的哈希值是否和解密相同。 4.解密合同 Bob作为合同的乙方,在收到Alice上传并签名的合同后,他首先需要验证合同的签名A是否正确,如果正确则说明该合同确实是Alice签名的,而且没有被篡改过。但是网上存在的合同是密文M,那么Bob该怎么查看合同内容呢?因为Bob有自己的私钥,他可以解锁出解密文件的随机密钥,用这个密钥再去解密加密文件,就能够看到原始文件了。 5.加签合同 接下来Bob需要加签合同,那么他的做法和步骤3签名合同是类似的,只需要用自己的私钥对密文进行签名,然后把签名放到网络上即可。 【上面的文章是我在整理电脑时发现当年写了,没有发布的,现在稍作修改发布出来,希望对大家有所帮助。】 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
我在之前的一篇文章中介绍了怎么通过C#将一句话写入到比特币的区块链网络中,最近花了好几天的时间,我终于把比特币的区块链数据载入到了SQLServer(具体做法参加我的这篇博客:http://www.cnblogs.com/studyzy/p/export-bitcoin-blockchain-to-database.html) 由于数据量特别特别的大,而我只是在自己的PC上做的,没有服务器,所以除了主键查询以外,其他查询都会很慢很慢。我花了一些时间,把区块链Output部分的OP_RETURN脚本区域给查询出来了,然后再做了个解码,看看有什么好玩的东西没有。 在OP_RETURN上大家主要干了这么几件事情: 1.文档存在性证明 2.不知道是什么资源的URI 3.各国语言的文字内容 4.各种Hash值 有表白、求婚的。 这里比特币就见证了一场成功的求婚: https://blockchain.info/tx/b17a027a8f7ae0db4ddbaa58927d0f254e97fce63b7e57e8e50957d3dad2e66e https://blockchain.info/tx/e89e09ac184e1a175ce748775b3e63686cb1e5fe948365236aac3b3aef3fedd0 2014-09-07 Tetsu向其女朋友Yuki求婚,而其女友很爽快的答应了。真是虐狗虐到了比特币上! 有日本人发布自己的愿望的,我怀疑应该是日本那边有人开发了一个软件,帮忙大家把自己的愿望写到区块链上去,因为我看到了大量的日文,而且比较集中,而且基本上都是写的一个心愿。 有的想去夏威夷举行婚礼,有的想和孩子去一趟迪士尼乐园 转账备注 99100146 2 杨靖玉转3.95BTC至张益兵 99100147 2 马祥珍转2.694BTC至张益兵 我也做了一个转账,就是把我所知道的4代人的家谱写到区块链上,我这笔记录已经被永久记录! 以上是我在几个月前写的博客,不知道当时什么原因,没有写完,就保留成草稿了。今天翻了出来,就发表了吧。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
前一篇博客讲到了如何编译本地的Fabric Code成镜像文件,那么如果我们想改Fabric源代码,实现一些Fabric官方并没有提供的功能,该怎么办呢?这时我们除了改源码,增加需要的功能外,还需要能够跑通Fabric的测试。Fabric的测试主要包括单元测试和行为测试,下面分别介绍。 一、单元测试 单元测试是通过testenv这个镜像来完成的,而这个镜像的容器在启动后实际执行的就是unit-test文件夹下面的run.sh脚本。我们使用make unit-test命令即可对整个Fabric的所有单元测试进行运行。 1.单元测试 因为Fabric是用Go写的,所以Fabric的单元测试也是用Go的单元测试命令来完成,也就是go test命令。在Fabric的源代码中,我们看到的所有*_test.go这些就是单元测试的代码。这些代码在正式编译的时候是会被自动忽略的,只有在go test命令的时候才会去运行。 以bccsp为例,这是提供密码学相关方法的接口文件,在bccsp文件夹下的bccsp.go文件,而他的单元测试文件自然就是bccsp_test.go文件。在该单元测试文件中,以Test开头的函数,就是具体的测试用例。我们要跑具体的某一个测试用例,比如其中的TestKeyGenOpts,那么我们的可以使用命令: go.exe test github.com\hyperledger\fabric\bccsp -run ^TestKeyGenOpts$ 这里我们可以看到-run后面跟的是一个正则表达式,我们可以写其他正则表达式来表示一批方法。当然我们也可以不加^$,直接写方法名。 如果我们不指定具体的测试用例,而只指定包,那么就是测试整个包下面的所有用例。 go test -v -timeout 30s github.com\hyperledger\fabric\bccsp 这里我加了2个参数,这两个参数都是在go test的时候很常用的,-v是输出详细信息。-timeout是设置跑完整个测试的时间限制,如果里面有死循环之类的就会超时而退出。 如果我们要测试不是某个包,而是整个文件夹下面的所有包,那么我们可以使用“…”来表示。比如: go test -v -timeout 60s github.com\hyperledger\fabric\bccsp\… 2.性能测试 go test除了提供单元测试外,还有性能测试的功能。前面说到_test.go文件里面Test开头的是单元测试的测试用例入口函数,而性能测试则是以Benchmark开头。 Fabric本身并没有写什么性能测试的代码,但是我们可以从vendor代码中找到例子。比如: github.com\hyperledger\fabric\vendor\github.com\docker\docker\pkg\stdcopy 这里有个BenchmarkWrite函数,用于测试NewStdWriter.Write的性能,我们使用go test命令带上-bench参数就可以执行性能测试。性能测试不仅仅关心执行的时间,也关系内存的分配情况。再加上-benchmem参数,可以查看内存性能测试结果。 go test -benchmem github.com\hyperledger\fabric\vendor\github.com\docker\docker\pkg\stdcopy -bench ^BenchmarkWrite$ 以下是我在本机执行的结果: BenchmarkWrite-4 5000000 283 ns/op 15507.52 MB/s 0 B/op 0 allocs/op PASS ok github.com/hyperledger/fabric/vendor/github.com/docker/docker/pkg/stdcopy 2.406s Success: Benchmarks passed. 3.代码覆盖率 代码覆盖率是度量测试自身完整和有消息的一种手段。通过覆盖率值,我们可以分析测试代码的编写质量。 在go test命令后跟上-cover参数,就可以提供代码覆盖率百分比的结果。 go test -cover github.com\hyperledger\fabric\bccsp 返回结果: ok github.com/hyperledger/fabric/bccsp 2.828s coverage: 100.0% of statements 但是这里返回的结果太少了,我们如果希望得到更详细的覆盖率信息,可以指定covermode和converprofile参数。 go test -cover -covermode count -coverprofile c:\Temp\cover.out github.com\hyperledger\fabric\bccsp 这里是将覆盖率的结果输出到C:\Temp\cover.out这个文件中。同时使用count可以对函数的执行次数进行计数。执行完毕后,我们可以使用以下命令将cover.out转换为html在浏览器中查看: go tool cover -html=C:\Temp\cover.out 在浏览器中,用绿色表示覆盖,而执行次数,是需要把鼠标放上去才会显示。这是我浏览器显示的覆盖率结果: 二、行为测试 我这里翻译成行为测试可能不一定很可取,英文是BDDTests,BDD是敏捷开发中的一个概念,英文是Behavior Driven Development,可以认为是TDD的升级版吧。所有行为测试的代码都在Fabric文件夹下面的bddtests文件夹中。 要进行Fabric的行为测试,需要安装相关的环境,Fabric主要用到的是Behave这个工具,https://github.com/behave/behave 官方给我们提供了安装脚本,直接运行: sudo ./scripts/install_behave.sh 这里需要安装的包比较多,安装完成后我们就可以进行BDD的测试了。 官方的make命令下就为我们提供了执行全部行为测试的命令: make behave 系统就会按照配置的场景,启动对应的Docker容器,进行行为测试。 如果我们想跑某一个行为测试,而不是全部,那么就需要进一步的设置,具体参考:https://github.com/hyperledger/fabric/tree/release/bddtests 依次执行以下代码: sudo pip install virtualenv sudo pip install virtualenvwrapper export WORKON_HOME=~/Envs source /usr/local/bin/virtualenvwrapper.sh mkvirtualenv -p /usr/bin/python2.7 behave_venv 执行完上面命令后,我们可以看到我们的命令行变成了: (behave_venv) studyzy@ubuntu1:~/go/src/github.com/hyperledger/fabric/bddtests$ 接下来再安装以下工具: pip install behave pip install grpcio-tools pip install "pysha3==1.0b1" pip install b3j0f.aop pip install jinja2 pip install pyopenssl pip install ecdsa pip install python-slugify pip install pyyaml 总的来说就是给behave的执行设置了一个虚拟环境,所有代码的执行是在这个虚拟环境中执行,不会影响真实环境。 安装完毕后,我们想要测试某一个BDDTest,那么可以执行: cd bddtests behave -k -D cache-deployment-spec features/bootstrap.feature 这里测试的就是在bddtests\features\bootstrap.feature的这个例子。 测试完成后,使用 deactivate 命令即可退出虚拟环境,回到我们传统的命令行下。 三、总结 如果我们要动Fabric的源码,那么首先保证能够跑通Fabric的单元测试和行为测试,然后再改。如果是新功能模块,那么也需要写自己模块的单元测试代码。写完之后用go test来测试,保证我们的代码能够通过单元测试,而且要注意代码覆盖率,保持较高的覆盖率能够发现很多代码中隐藏的问题。 如果我们的功能涉及到一系列的步骤操作,那么就一定要写行为测试了。行为测试可以保证整个功能串起来运行是正常的。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
前面的几篇博客,我们已经把Fabric环境搭建好了,也可以使用Go开发ChainCode了,那么我们在ChainCode开发完毕后,可以通过CLI来测试ChainCode的正确性,ChainCode开发后,接下来就是关于Application的编写了。 Application分为2部分,一部分是关于后来业务逻辑的,也就是Web API,一般是通过RESTful的形式提供,另外一部分就是UI,当然大多数情况下都是GUI,也就是网站前端,Windows程序,APP之类的。关于前端,我由于没啥艺术细胞,做出来的界面很丑,所以也就扬长避短,很少做前端开发,专注于后台业务逻辑的实现。 一 简介 在Web API的开发中,业内最知名的工具就是Swagger了,这简直就是一件神器啊!我之前在C#开发的时候就使用ABP框架,用到了Swagger,在试着使用Go的Web开发框架Beego的时候也看到了Swagger,现在使用Node开发,想不到又用到Swagger,只能说明Swagger的跨平台跨语言的能力太强了。Swagger可以帮助我们把API文档化,方便进行测试。 Swagger的开发方式有2种: 使用Web开发框架中迁移过来的Swagger库,也就是先代码,后生成API文档的模式。比如ABP框架中就是,我们只需要在ApiController中定义好接口和注释好,其框架就可以帮我们生成Swagger界面。 使用Swagger的yaml文件定义API接口,定义好后,再使用Swagger官方提供的CodeGen生成对应语言的代码。 第一种开发方式要看你使用的Web框架的支持情况,接下来我们要讲的就是第二种开发方式。 二 编写Swagger YAML 官方已经给我提供一个宠物商店的示例,并提供了强大的语法检查和预览功能,那就是Swagger Editor,我们直接访问http://editor.swagger.io/ 就可以看到。如果由于某些神秘的力量,造成访问特别缓慢或者无法访问,我们也可以下载Swagger Editor的Image到本地来运行。 直接在安装了Docker的环境中运行如下命令: docker pull swaggerapi/swagger-editor docker run -p 80:8080 swaggerapi/swagger-editor 然后就可以访问http://{Your_Server_IP}。 不管是在线的Editor或者是本地部署的Docker,我们最终看到是这样一个界面: 左边窗口就是我们要编辑的YAML文件的内容,右边窗口就是预览的API文档的效果。 关于YAML文件,其实可读性还是很强的,大部分都不需要解释就知道是什么意思,下面我来着重介绍以下几个比较重要的元素: 1. host&basePath host是指定了我们的API服务器的地址,也就是我们部署了Web API时,是部署在哪个Server上。如果我们是本地开发,而且使用了自定义端口,比如8080,那么需要改成localhost:8080。 basePath是指定API的虚拟目录,比如我们有个获得所有用户列表的API是:GET /User,如果我们设定了basePath是“/api”,那么我们要访问的路径应该是: GET http://localhost:8080/api/User 当然,如果我们要更规范,比如把API版本也放进去,那么我们可以设置basePath为”/api/v1”,于是我们的访问路径就是: GET http://localhost:8080/api/v1/User 这个basePath参数涉及到服务器端api路由的生成,而host涉及到各个API测试时候的调用地址。 2. tags Tags是用于我们对大量的API进行分区用的,说简单点就是为了大量的API能够更好看,更容易查找。我们可以为tag添加注释,使得API文档更容易读懂。 Tags不涉及到后台的改变,每一个具体的API都可以指定属于哪个(或者哪几个tag),然后在Swagger显示的时候,会将这些API归到所属的Tag下面去。 【注意:YAML文件格式严格要求缩进,就像Python一样,所以如果我们在添加元素的时候一定要注意缩进是否正确。】 比如我们新建一个Tag叫Bank,然后增加一点对这个Tag的描述,接下来我们再到/pet post下面,可以把tags增加一行,写为银行,然后就可以看到右边的预览窗口更新了,显示了银行这个Tag相关的API: 如果没有刷新,我们可以点击上面菜单的Edit->Convert to YAML可以看到效果。 3. paths 这是最主要的配置元素。主要的API配置都在这个环节。下面一级一级的讲解。 第一级是URL,以/开头,URL中可以指定参数。比如我们要获得某个bankId对应的银行信息,那么URL就是 /bank/{bankId} 第二级是HTTP方法,我们在WebAPI中主要用到的方法有:查询get,创建post,修改put和删除delete。因为我们是要查询某个银行ID对应的银行信息,所以我们在这一级输入get 第三级有多个元素,分别是: tags,说明这个API是属于哪个Tag的。 summary,对该接口的简单描述,一句话即可。 description,顾名思义,是接口的介绍,可以写的详细一点。 operationId,这是对应的后台的方法名,Swagger的路由就可以根据URL和这里的operationId找到对应的Action方法。 consumes,是客户端往服务器传的时候,支持什么类型,一般我们只需要保留json即可,可以把xml删除。如果是get方法,不需要该元素。 produces,就是服务器在返回给客户端数据的时候,是什么样式的数据,我们仍然保留json即可。 parameters就是具体的参数,这里的设置比较复杂,包括指定参数是在URL中还是在Body中,传入的参数是什么类型的,是否必须有该参数,对该参数的描述等。如果参数是一个对象,那么需要添加对该对象类型的引用,而对象的定义在后面definitions节点中。 responses是服务器返回的HTTP Code有哪些。每一种状态码表示什么意思。 security是指定该接口的安全检查方式,如果没有设置,那么就是匿名访问。其引用的是securityDefinitions中的定义。 x-swagger-router-controller,这是一个扩展元素,用来指定该URL对应的后台的Controller名。 结合上面介绍的,我们自定义一个根据ID获取Bank对象的YAML内容如下: /bank/{bankId} : get: tags: - Bank summary: 根据银行ID获得银行基本信息 description: 详细描述 operationId: getBankById produces: - application/json parameters: - name: bankId in: path description: 银行对象的主键ID required: true type: integer format: int64 responses: '200': description: 找到银行 schema: $ref: '#/definitions/Bank' '400': description: 无效的ID '404': description: ID对应的银行未找到 4. securityDefinitions 这是安全定义模块,在这里可以定义我们WebAPI的安全认证方式,比如: Basic Authentication API Keys Bearer Authentication OAuth 2.0 OpenID Connect Discovery Cookie Authentication 这里面这么多种认证方式,很多我也没用过,了解不深,我主要用的是Bearer和OAuth 2.0,具体设置大家可以参考文档: https://swagger.io/docs/specification/authentication/ 5. definitions 这里是定义我们在API中会涉及到哪些JSON对象的地方。也就是说我们在API中要POST上去的JSON或者通过GET由服务器返回的JSON,其对象都在这里定义,上面的步骤直接引用这里的定义即可。 比如我们上面需要引用到Bank对象,那么我们在这里定义如下: Bank: type: object properties: id: type: integer format: int32 name: type: string 如果是对象嵌套引用了其他对象,也可以通过$ref的方式引用过去,我们可以参考官方示例中的Pet对象,就引用了Category。 以上各个元素我只是简单的讲解,对于各种深入的用法,大家可以参考官方文档:https://swagger.io/docs/ 三 生成后台代码 只要我们预览右边的代码没有报任何错误,那么我们就可以生成对于的后台代码了。这里因为Fabric SDK是Node的,所以我们的Web API也是使用Node来开发。我们点击Generate Server菜单下的nodejs-server选项: 系统会下载一个压缩包,该压缩包解压后就是我们的Web API Node项目。在安装了Node的机器上,我们使用以下命令,安装项目所依赖的包: npm install --registry=https://registry.npm.taobao.org 安装完毕后,运行以下命令: npm start 我们可以看到网站地址是:http://localhost:8080/docs 打开浏览器,访问这个网站,就可以看到Swagger生成的UI,并看到我们自定义的获取银行对象的方法。 下面,我们来试一试传入参数1,并调用该API,可以看到这样的结果: 这里返回的是Swagger给我们Mock的一个假结果,如果我们要返回真实的结果,那么需要在Controllers文件夹中找到BankService.js,看到如下的内容: 'use strict'; exports.getBankById = function(args, res, next) { /** * 根据银行ID获得银行基本信息 * 详细描述 * * bankId Integer 银行对象的主键ID * returns Bank **/ var examples = {}; examples['application/json'] = { "name" : "aeiou", "id" : 0 }; if (Object.keys(examples).length > 0) { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2)); } else { res.end(); } } 将Mock代码部分删除,将我们真实的业务逻辑写进去即可完成我们的WebAPI的开发工作。 四 总结 Swagger真的不愧是Web API开发的神器,太好用了。另外官方还有SwaggerHub,支持多人协作编写YAML文档,不过是收费的。我们在项目中其实可以通过Git来管理yaml文件,因为该文件存在于WebAPI项目的api文件夹中,所以其实大家可以共同编辑,然后使用Git来合并冲突。另外Swagger还有Client,看了一些支持各种语言,各种框架,各种APP开发,真是太强大了,我由于不开发GUI,所以没怎么接触,需要你去研究了。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
我在很久之前就有个想法,(参见:http://www.cnblogs.com/studyzy/p/4118528.html)就是做一个帮助英文学习的软件,其实当时也做了,但是由于各方面的问题,加上软件本身并不完善,所以我也就搁置了,并没有发布。最近心血来潮,加上收到了一个网友的来信,询问这款软件,所以我就把他正式发布出来吧。 一、简介 深蓝英文字幕助手是一款对英文字幕的生词进行注释,让用户能够在一边看英文电影/电视剧,一边学习英语的小软件。使用深蓝英文字幕助手后,用户可以彻底告别中午字幕,听着正宗地道的发音,看着英文字幕,遇到不认识的单词会给出注释,让用户能够顺畅的看英文字幕电影/电视剧。下载地址: https://github.com/studyzy/LearnEnglishBySubtitle/releases 二、使用方法 由于本软件使用C# 写成,需要.Net Framework 4.0的运行环境,如果使用XP或者Vista,那么在运行的时候可能直接报错,只需要下载安装.Net Framework 4.0即可。 1.设置个人的词汇量 本软件纯绿色,无需安装,解压后双击“深蓝英文字幕助手.exe”,即可打开本软件。第一次打开本软件时,需要设置用户的词汇量: 用户可以根据自己英文水平进行设置,我是按柯林斯词典的词频对英文单词进行了归类分级,如果用户是个高中英文水平或者刚过4级,可以设置4,如果达到6级水平可以设置3,更高的英文词汇量可以设置2或者1. 我们这里就以4级词频为例,点击确定按钮,需要等待一会儿。系统会将这些常用的词写入本地数据库。 2.下载英文影片和对应的英文字幕 我们去找到想看的英文影片,下载下来,然后找到对应的包含英文字幕的字幕文件(可以是纯英文字幕,也可以是英文中文双语字幕),字幕文件一般是srt或者是ass文件。网上有很多提供字幕搜索下载的网站,如果要找冷门资源的英文字幕,可能就得去国外的字幕搜索网站。另外我使用迅雷影音也可以搜索下载字幕文件。只需要打开视频文件,然后右键选择字幕,在线搜索即可。之前是可以的,最近我试了试总是提示搜索字幕失败,不知道是不是迅雷关闭这个服务了。总之我们把字幕文件准备好,存在在磁盘上。 3.载入并注释字幕 选择软件的“载入字幕”按钮,选中我们下载好的字幕文件,点击确定,系统就可以将字幕载入进来: 然后点击“注释字幕”按钮,系统会根据用户词汇量,将认为可能是生词的词语显示出来。 下面解释一下每一列的意思: 忽略,就是说可能是生词,但是我并不想记住的词语,比如人名地名之类的,记住了也没太大意义。忽略后再也不会当生词出现。 是否生词,如果这个词确实不认识,那么就不要点记住,如果不是生词,早就记住了,那么可以点“记住”。比如第一个pig,我是认识的,所以可以点“记住”,将这个词加入到已记住的列表中。 关注,是需要特别注意的,想接下来重点背的单词。比如mud这个词我要关注,那么就点mud行的关注星星。 单词,就是系统解析出来的可能是不认识的单词。 词频,就是该单词在本字幕文件中,出现了多少次。 原文,就是单词所在的句子。 解释,就是根据单词,在字典中查到的汉语解释,如果一个词多个意思,那么就是一个下拉框,我们需要去选择该单词在句子中应该是哪个意思。 自定义解释,就是说这些字典给出的解释都不对,我们自己来注解一下。 我们主要是把认识的单词给剔除出这个列表,标记完成后的列表如下: 4.保存字幕 标识完生词后,我们点击“确定”按钮,回到主界面,我们可以看到,字幕文件中对于生词部分,已经给出了对于的简短中文注释: 单击右下角的“保存注释”按钮,系统会将注释后的字幕保存到原字幕文件夹,并加上_new以示区分。 5.使用新字幕播放影片 现在基本上所有的播放器都支持手动选择字幕载入,不过好像大部分不支持在srt中设置格式,我试了一下KMPlayer是支持的。我可以看到,对于普通的生词,旁边给出了注释,方便我们在看影片时理解当前对话的意思。 而对于我们之前特别关注的生词(打了星星的),那么就会以红色显示出来,方便我们记忆: 6.管理我的词汇 在“设置”菜单的“用户词汇管理”选项下,我们可以看到哪些单词是我们认识的,哪些是不认识的。因为我在前面设置了自己的用户词汇等级,所以现在可以看到大量的单词我是认识的。 如果有些词,其实你不认识,那么可以在上面右击,在弹出的菜单中选择“不记得了,加入生词本”,那么就会把这个词放到生词列表中。由于熟悉的词和生词会越来越多,所以我增加了一个“单词记录”的查询页面,可以查询某个词是否记住,是在哪里出现的。 另外我们还可以把熟悉的单词或者生词本导出,方便其他系统导入。也可以批量的导入熟悉的单词或者生词。 7.其他功能 7.1真人发音 本软件提供了真人发音功能,在单词上双击或者右击选择“真人发音”即可听到单词的发音。不过这个功能需要联网,所以可能因为网络的原因,并不那么实时。发音也可以选择是英式发音还是美式发音,并提供离线存储发音的功能,可以选择“设置”菜单的“真人发音设置”。 7.2整句翻译 如果整句都不是很好理解,我们可以使用整句翻译服务,在设置中可以选择使用哪种服务进行整句翻译。所有整句翻译服务都是网络服务,所有必须联网,并保证能够访问对应的网站。 在载入字幕后,选中某句字幕,然后右击,选择整句翻译服务,即可将这句字幕翻译成中文。 7.3生词预习 如果觉得一个字幕一个字幕的学习,比较慢,那么我们可以批量下载好整季美剧的字幕,然后批量预习。在“工具”菜单的“生词预习”选项,可以打开生词预习窗口,然后选择字幕文件夹。 系统会将所有字幕进行分析,找出其中的生词和词频,并给出原文和解释。 三、原理 该软件分析字幕文件,提取其中的单词,基于斯坦福的自然语言处理库,找到词语的原型和最匹配的解释,然后将用户的选择结果记录到本地Sqlite数据库中,并基于用户的选择,替换掉原文中文本,把解释放在单词旁边。本软件默认的单词解释来自于灵格斯字典中的Vicon English-Chinese(S) Dictionary,本来我是提供了多种字典可供选择的,但是由于其他字典的解释太多了,反而不是很好在字幕上显示,所以我暂时去掉了其他字典的选择。 希望我这个小软件能够帮助到想通过看美剧、英剧,看好莱坞大片的方式学英语的同学。以后可以自豪的说,我看原声电影根本不需要中文字幕! 最后,经常有用户反映GitHub上的文件无法下载,我于是上传了一份到百度网盘,这样应该就能下载了吧:http://pan.baidu.com/s/1bpEWqo3 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
前面的文章都是在讲解Fabric网络的搭建和ChainCode的开发,那么在ChainCode开发完毕后,我们就需要使用Fabric SDK做应用程序的开发了。官方虽然提供了Node.JS,Java,Go,Python等多种语言的SDK,但是由于整个Fabric太新了,很多SDK还不成熟和完善,所以我采用Node JS的SDK,毕竟这个是功能毕竟齐全,而且也是官方示例的时候使用的SDK。由于我从来没有接触过Node.JS的开发,对这个语言理解不深,所以讲的比较肤浅,希望大家见谅。 1.环境准备 Node.js是一个跨平台的语言,可以在Linux,Window和Mac上安装,我们在开发的时候可以在Windows下开发,最后生产环境一般都是Linux,所以我们这里就以Ubuntu为例。Fabric Node SDK支持的Node版本是v6,不支持最新的v8版本。NodeJS官方给我们提供了很方便的安装方法,具体文档在:https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions 我们只需要执行以下命令即可安装NodeJS的最新v6版本: curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - sudo apt-get install -y nodejs 安装完成后我们可以使用以下两个命令来查看安装的Node版本和npm版本。 node –v npm -v 关于NPM,这个是一个包管理器,我觉得很像VS里面的NuGet,关于NPM的基础知识,我们可以参考这篇博客:http://www.ruanyifeng.com/blog/2016/01/npm-install.html 只要安装好node和npm,接下来我们就可以进行Fabric Node SDK Application的开发了。 由于我们想基于官方Example的e2e_cli里面的Fabric网络来写程序,关于Fabric网络的搭建我就不多说,大家可以参考我之前的博客。总之结果就是我们现在已经成功运行了e2e_cli这个网络,也就是说Example02这个ChainCode已经安装部署,并且测试通过了,我们接下来只是换用Node SDK的方式进行查询和调用。 2.编写package.json并下载依赖模块 我们首先在当前用户的根目录建立一个nodeTest的文件夹,用于存放我们关于node的相关项目文件,然后在其中新建一个包配置文件,package.json mkdir ~/nodeTest cd ~/nodeTest vi package.json 在这个文件中,我们可以定义很多项目相关的属性,这篇博客详细的介绍了每个属性有什么用,大家可以参考:http://www.cnblogs.com/tzyy/p/5193811.html 总之,最后我们在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" ] } 最主要的就是dependencies,这里我们放了Fabric CA Client和Fabric Node SDK的Client,虽然本示例中没用到CA Client,但是以后会用到,所以先放在这里了。 编辑保存好该文件后,我们就可以运行npm install命令来下载所有相关的依赖模块,但是由于npm服务器在国外,所以下载可能会很慢,感谢淘宝为我们提供了国内的npm镜像,使得安装npm模块快很多。运行的命令是: npm install --registry=https://registry.npm.taobao.org 运行完毕后我们查看一下nodeTest目录,可以看到多了一个node_modules文件夹。这里就是使用刚才的命令下载下来的所有依赖包。 2.编写对Fabric的Query方法 下面我们新建一个query.js文件,开始我们的Fabric Node SDK编码工作。由于代码比较长,所以我就不分步讲了,直接在代码中增加注释,将完整代码贴出来: 'use strict'; var hfc = require('fabric-client'); var path = require('path'); var sdkUtils = require('fabric-client/lib/utils') var fs = require('fs'); var options = { user_id: 'Admin@org1.example.com', msp_id:'Org1MSP', channel_id: 'mychannel', chaincode_id: 'mycc', network_url: 'grpcs://localhost:7051',//因为启用了TLS,所以是grpcs,如果没有启用TLS,那么就是grpc privateKeyFolder:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore', signedCert:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem', tls_cacerts:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt', server_hostname: "peer0.org1.example.com" }; var channel = {}; var client = null; const getKeyFilesInDir = (dir) => { //该函数用于找到keystore目录下的私钥文件的路径 var files = fs.readdirSync(dir) var keyFiles = [] files.forEach((file_name) => { let filePath = path.join(dir, file_name) if (file_name.endsWith('_sk')) { keyFiles.push(filePath) } }) return keyFiles } Promise.resolve().then(() => { console.log("Load privateKey and signedCert"); client = new hfc(); var createUserOpt = { username: options.user_id, mspid: options.msp_id, cryptoContent: { privateKey: getKeyFilesInDir(options.privateKeyFolder)[0], signedCert: options.signedCert } } //以上代码指定了当前用户的私钥,证书等基本信息 return sdkUtils.newKeyValueStore({ path: "/tmp/fabric-client-stateStore/" }).then((store) => { client.setStateStore(store) return client.createUser(createUserOpt) }) }).then((user) => { channel = client.newChannel(options.channel_id); let data = fs.readFileSync(options.tls_cacerts); let peer = client.newPeer(options.network_url, { pem: Buffer.from(data).toString(), 'ssl-target-name-override': options.server_hostname } ); peer.setName("peer0"); //因为启用了TLS,所以上面的代码就是指定TLS的CA证书 channel.addPeer(peer); return; }).then(() => { console.log("Make query"); var transaction_id = client.newTransactionID(); console.log("Assigning transaction_id: ", transaction_id._transaction_id); //构造查询request参数 const request = { chaincodeId: options.chaincode_id, txId: transaction_id, fcn: 'query', args: ['a'] }; return channel.queryByChaincode(request); }).then((query_responses) => { console.log("returned from query"); if (!query_responses.length) { console.log("No payloads were returned from query"); } else { console.log("Query result count = ", query_responses.length) } if (query_responses[0] instanceof Error) { console.error("error from query = ", query_responses[0]); } console.log("Response is ", query_responses[0].toString());//打印返回的结果 }).catch((err) => { console.error("Caught Error", err); }); 编写完代码,我们想要测试一下我们的代码是否靠谱,直接运行 node query.js 即可,我们可以看到,a账户的余额是90元。 studyzy@ubuntu1:~/nodeTest$ node query.js Load privateKey and signedCert Make query Assigning transaction_id: ee3ac35d40d8510813546a2216ad9c0d91213b8e1bba9b7fe19cfeff3014e38a returned from query Query result count = 1 Response is 90 为什么a账户是90?因为我们跑e2e_cli的Fabric网络时,系统会自动安装Example02的ChainCode,然后自动跑查询,转账等操作。 3.编写对Fabric的Invoke方法 相比较于Query方法,Invoke方法要复杂的多,主要是因为Invoke需要和Orderer通信,而且发起了Transaction之后,还要设置EventHub来接收消息。下面贴出invoke.js的全部内容,对于比较重要的部分我进行了注释: 'use strict'; var hfc = require('fabric-client'); var path = require('path'); var util = require('util'); var sdkUtils = require('fabric-client/lib/utils') const fs = require('fs'); var options = { user_id: 'Admin@org1.example.com', msp_id:'Org1MSP', channel_id: 'mychannel', chaincode_id: 'mycc', peer_url: 'grpcs://localhost:7051',//因为启用了TLS,所以是grpcs,如果没有启用TLS,那么就是grpc event_url: 'grpcs://localhost:7053',//因为启用了TLS,所以是grpcs,如果没有启用TLS,那么就是grpc orderer_url: 'grpcs://localhost:7050',//因为启用了TLS,所以是grpcs,如果没有启用TLS,那么就是grpc privateKeyFolder:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore', signedCert:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem', peer_tls_cacerts:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt', orderer_tls_cacerts:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt', server_hostname: "peer0.org1.example.com" }; var channel = {}; var client = null; var targets = []; var tx_id = null; const getKeyFilesInDir = (dir) => { //该函数用于找到keystore目录下的私钥文件的路径 const files = fs.readdirSync(dir) const keyFiles = [] files.forEach((file_name) => { let filePath = path.join(dir, file_name) if (file_name.endsWith('_sk')) { keyFiles.push(filePath) } }) return keyFiles } Promise.resolve().then(() => { console.log("Load privateKey and signedCert"); client = new hfc(); var createUserOpt = { username: options.user_id, mspid: options.msp_id, cryptoContent: { privateKey: getKeyFilesInDir(options.privateKeyFolder)[0], signedCert: options.signedCert } } //以上代码指定了当前用户的私钥,证书等基本信息 return sdkUtils.newKeyValueStore({ path: "/tmp/fabric-client-stateStore/" }).then((store) => { client.setStateStore(store) return client.createUser(createUserOpt) }) }).then((user) => { channel = client.newChannel(options.channel_id); let data = fs.readFileSync(options.peer_tls_cacerts); let peer = client.newPeer(options.peer_url, { pem: Buffer.from(data).toString(), 'ssl-target-name-override': options.server_hostname } ); //因为启用了TLS,所以上面的代码就是指定Peer的TLS的CA证书 channel.addPeer(peer); //接下来连接Orderer的时候也启用了TLS,也是同样的处理方法 let odata = fs.readFileSync(options.orderer_tls_cacerts); let caroots = Buffer.from(odata).toString(); var orderer = client.newOrderer(options.orderer_url, { 'pem': caroots, 'ssl-target-name-override': "orderer.example.com" }); channel.addOrderer(orderer); targets.push(peer); return; }).then(() => { tx_id = client.newTransactionID(); console.log("Assigning transaction_id: ", tx_id._transaction_id); //发起转账行为,将a->b 10元 var request = { targets: targets, chaincodeId: options.chaincode_id, fcn: 'invoke', args: ['a', 'b', '10'], chainId: options.channel_id, txId: tx_id }; return channel.sendTransactionProposal(request); }).then((results) => { var proposalResponses = results[0]; var proposal = results[1]; var header = results[2]; let isProposalGood = false; if (proposalResponses && proposalResponses[0].response && proposalResponses[0].response.status === 200) { isProposalGood = true; console.log('transaction proposal was good'); } else { console.error('transaction proposal was bad'); } if (isProposalGood) { console.log(util.format( 'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s', proposalResponses[0].response.status, proposalResponses[0].response.message, proposalResponses[0].response.payload, proposalResponses[0].endorsement.signature)); var request = { proposalResponses: proposalResponses, proposal: proposal, header: header }; // set the transaction listener and set a timeout of 30sec // if the transaction did not get committed within the timeout period, // fail the test var transactionID = tx_id.getTransactionID(); var eventPromises = []; let eh = client.newEventHub(); //接下来设置EventHub,用于监听Transaction是否成功写入,这里也是启用了TLS let data = fs.readFileSync(options.peer_tls_cacerts); let grpcOpts = { pem: Buffer.from(data).toString(), 'ssl-target-name-override': options.server_hostname } eh.setPeerAddr(options.event_url,grpcOpts); eh.connect(); let txPromise = new Promise((resolve, reject) => { let handle = setTimeout(() => { eh.disconnect(); reject(); }, 30000); //向EventHub注册事件的处理办法 eh.registerTxEvent(transactionID, (tx, code) => { clearTimeout(handle); eh.unregisterTxEvent(transactionID); eh.disconnect(); if (code !== 'VALID') { console.error( 'The transaction was invalid, code = ' + code); reject(); } else { console.log( 'The transaction has been committed on peer ' + eh._ep._endpoint.addr); resolve(); } }); }); eventPromises.push(txPromise); var sendPromise = channel.sendTransaction(request); return Promise.all([sendPromise].concat(eventPromises)).then((results) => { console.log(' event promise all complete and testing complete'); return results[0]; // the first returned value is from the 'sendPromise' which is from the 'sendTransaction()' call }).catch((err) => { console.error( 'Failed to send transaction and get notifications within the timeout period.' ); return 'Failed to send transaction and get notifications within the timeout period.'; }); } else { console.error( 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...' ); return 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'; } }, (err) => { console.error('Failed to send proposal due to error: ' + err.stack ? err.stack : err); return 'Failed to send proposal due to error: ' + err.stack ? err.stack : err; }).then((response) => { if (response.status === 'SUCCESS') { console.log('Successfully sent transaction to the orderer.'); return tx_id.getTransactionID(); } else { console.error('Failed to order the transaction. Error code: ' + response.status); return 'Failed to order the transaction. Error code: ' + response.status; } }, (err) => { console.error('Failed to send transaction due to error: ' + err.stack ? err .stack : err); return 'Failed to send transaction due to error: ' + err.stack ? err.stack : err; }); 保存文件并退出,接下来测试一下我们的代码,运行: node invoke.js 我们可以看到系统返回如下结果: Load privateKey and signedCert Assigning transaction_id: 1adbf20ace0d1601b00cc2b9dfdd4a431cfff9a13f6a6f5e5e4a80c897e0f7a8 transaction proposal was good Successfully sent Proposal and received ProposalResponse: Status - 200, message - "OK", metadata - "", endorsement signature: 0D x��N��n�#���/�G���QD�w�����As� \]��FfWҡ�+������=m9I���� 6�i info: [EventHub.js]: _connect - options {"grpc.ssl_target_name_override":"peer0.org1.example.com","grpc.default_authority":"peer0.org1.example.com"} The transaction has been committed on peer localhost:7053 event promise all complete and testing complete Successfully sent transaction to the orderer. 从打印出的结果看,我们的转账已经成功了,我们可以重新调用之前写的query.js重新查询,可以看到a账户的余额已经变少了10元。 4.总结 我们以上的query和Invoke都是参照了官方的fabcar示例,该示例在https://github.com/hyperledger/fabric-samples/tree/release/fabcar 这只是简单的测试Node SDK是否可用,如果我们要做项目,那么就会复杂很多,可以参考官方的两个项目: https://github.com/hyperledger/fabric-samples/tree/release/balance-transfer https://github.com/IBM-Blockchain/marbles 我之前一直卡在怎么基于某个用户的私钥和证书来设置当前的Context,后来感谢neswater的帮助,终于才解决了这个问题。还有就是TLS的问题,官方给出的fabcar是没有TLS的,我搞了半天才搞定,原来除了制定TLS证书之外,我们访问Peer的URL也是不一样的。 最后,大家如果想进一步探讨Fabric或者使用中遇到什么问题可以加入QQ群【494085548】大家一起讨论。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
在企业级应用开发中,经常会涉及到流程和状态,而有限状态机(FSM)则是对应的一种简单实现,如果复杂化,就上升到Workflow和BPM了。我们在Fabric ChainCode的开发过程中,也很可能涉及到状态机,这里我们就举一个例子,用FSM实现一个二级审批的状态转移。 我们有一个表单,员工填写表单是可以保存为Draft状态,提交后变成Submitted状态,然后在一级审批的时候,可以Approve或者Reject,同意了改为L1Approved,进入下一级审批,拒绝了那么就以Reject状态打回给起草人,二级审批人员也是有Approve和Reject两个操作,同意了状态就改为Complete,拒绝了就改为Reject。这是一个很常见的审批例子。 我们使用Go来开发ChainCode,那么可以采用https://github.com/looplab/fsm 这个FSM库。这个库也是Fabric官方采用的状态机库。下面是我的操作过程: 1.新建ChainCode项目并引入fsm库 我们新建一个项目fsmtest,并在其中建立住ChainCode文件:main.go,然后新建vendor文件夹,将https://github.com/looplab/fsm从GitHub clone下来,并放在vendor/github.com/looplab/fsm文件夹中,最终项目个文件结构如下: 2.定义FSM初始化函数 接下来打开main.go文件,除了编写ChainCode所必须使用的函数外,最主要的就是编写定义状态机转移的初始化函数了,我们根据前面流程图中的流程状态定义,我们可以写出如下的FSM初始化函数: func InitFSM(initStatus string) *fsm.FSM{ f := fsm.NewFSM( initStatus, fsm.Events{ {Name: "Submit", Src: []string{"Draft"}, Dst: "Submited"}, {Name: "Approve", Src: []string{"Submited"}, Dst: "L1Approved"}, {Name: "Reject", Src: []string{"Submited"}, Dst: "Reject"}, {Name: "Approve", Src: []string{"L1Approved"}, Dst: "Complete"}, {Name: "Reject", Src: []string{"L1Approved"}, Dst: "Reject"}, }, fsm.Callbacks{}, ) return f; } 3.在ChainCode中调用FSM Event 接下来我们在ChainCode重定义了4个函数, Draft Submit Approve Reject 于是我们可以在Invoke函数中定义4中情况: func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response { function, args := stub.GetFunctionAndParameters() fmt.Println("invoke is running " + function) if function == "Draft" { //自定义函数名称 return t.Draft(stub, args) //定义调用的函数 } else if function == "Submit" { return FsmEvent(stub,args,"Submit") } else if function == "Approve" { return FsmEvent(stub,args,"Approve") } else if function == "Reject" { return FsmEvent(stub,args,"Reject") } return shim.Error("Received unknown function invocation") } 其中Draft函数就是把表单状态初始化为Draft并保存到数据库,并不涉及状态的修改: func (t *SimpleChaincode) Draft(stub shim.ChaincodeStubInterface, args []string) pb.Response{ formNumber:=args[0] status:="Draft" stub.PutState(formNumber,[]byte(status))//初始化Draft状态的表单保存到StateDB return shim.Success([]byte(status)) } 而其他操作都涉及状态的修改,由于我们引入了状态机,所以我们只需要初始化状态机,并发送对应的Event即可,而最新的状态是由状态机根据我们的定义而获得的。所以我们虽然有3个操作,去只需要一个函数就能完成,并没有冗余的if else判断,这就是状态机的优势! func FsmEvent(stub shim.ChaincodeStubInterface, args []string,event string) pb.Response{ formNumber:=args[0] bstatus,err:=stub.GetState(formNumber)//从StateDB中读取对应表单的状态 if err!=nil{ return shim.Error("Query form status fail, form number:"+formNumber) } status:=string(bstatus) fmt.Println("Form["+formNumber+"] status:"+status) f:=InitFSM(status)//初始化状态机,并设置当前状态为表单的状态 err=f.Event(event)//触发状态机的事件 if err!=nil{ return shim.Error("Current status is "+status+" does not support envent:"+event) } status=f.Current() fmt.Println("New status:"+status) stub.PutState(formNumber,[]byte(status))//更新表单的状态 return shim.Success([]byte(status));//返回新状态 } 4.部署并测试ChainCode 现在状态写完了,我们需要进行测试,我们可以git push到GitHub,然后到Ubuntu中git clone下来,也可以通过rz命令,把Windows中开发好的ChainCode上传到Ubuntu中,不管什么方法,最终我们整个ChainCode项目放在了~/go/src/github.com/hyperledger/fabric/examples/chaincode/go/fsmtest这个文件夹下。 然后使用e2e_cli下面的network_setup.sh up命令启动整个Fabric网络。启动Fabric网络后,我们需要进入CLI进行部署和合适fsmtest: docker exec -it cli bash 然后安装并初始化我们的ChainCode: peer chaincode install -n fsmtest -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/fsmtest ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem peer chaincode instantiate -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA -C mychannel -n fsmtest -v 1.0 -c '{"Args":[]}' 现在安装完毕后,我们可以起草一个报销单EXP1: peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA -C mychannel -n fsmtest -c '{"Args":["Draft","EXP1"]}' 我们可以看到系统返回的结果: 现在状态是Draft,然后我们试一试提交报销单EXP1: peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA -C mychannel -n fsmtest -c '{"Args":["Submit","EXP1"]}' 我们看到状态已经改为Submitted了。接下来我们进一步一级审批通过,二级审批通过,都是执行相同的命令: peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA -C mychannel -n fsmtest -c '{"Args":["Approve","EXP1"]}' 这个时候,状态已经是Complete了,如果我们再次调用Approve函数会怎么样?因为我们在状态机中并没有定义这么一个流转事件,所以肯定是报错,无法正常执行的: 大家如果也在做这个实验,也可以去测试Reject函数,会得到想要的结果的。 5.总结 总的来说,在Fabric的ChainCode开发中,引入第三方的库可以方便我们编写更强大的链上代码。而这个FSM虽然简单,但是也可以很好的将状态流转的逻辑进行集中,避免了在状态流转时编写大量的Ugly的代码,让我们在每个函数中更专注于业务逻辑,而不是麻烦的状态转移。最后直接粘贴出我的完整ChainCode 源码,方便大家直接使用。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
我们前面关于Fabric的所有文章中用到的例子都没有CA Server,都是由cryptogen这个工具根据crypto-config.yaml而生成的。但是在实际生产环境中,我们肯定不能这么做,我们应该为每个Org建立一个CA,由CA来管理其中的用户。下面我们就试着讲Fabric CA集成到整个Fabric网络中,并用CA Client生成新用户,最终使用新用户调用ChainCode,验证新用户的合法性。我们仍然以官方的e2e_cli为例,关于这个例子的环境搭建,可以参考我的上一篇博客:http://www.cnblogs.com/studyzy/p/7437157.html 1.修改docker-compose文件,增加CA容器 我们就以给org1这个组织增加CA容器为例,打开e2e_cli文件夹中的docker-compose-cli.yaml ,增加以下内容: ca0: image: hyperledger/fabric-ca environment: - FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server - FABRIC_CA_SERVER_CA_NAME=ca0 - FABRIC_CA_SERVER_TLS_ENABLED=false ports: - "7054:7054" command: sh -c 'fabric-ca-server start --ca.certfile /etc/hyperledger/fabric-ca-server-config/ca.org1.example.com-cert.pem --ca.keyfile /etc/hyperledger/fabric-ca-server-config/${PRIVATE_KEY} -b admin:adminpw -d' volumes: - ./crypto-config/peerOrganizations/org1.example.com/ca/:/etc/hyperledger/fabric-ca-server-config container_name: ca0 这里我们注意到,Fabric CA Server启动的时候,带了3个重要的参数:ca.certfile 指定了CA的根证书,ca.keyfile 指定了接下来给新用户签发证书时的私钥,这里我们使用变量${PRIVATE_KEY}代替,这是因为每次network_setup的时候,私钥的名字是不一样的,所以需要从启动脚本中传入。另外就是-b参数,指定了CA Client连接CA Server时使用的用户名密码。 2.修改network_setup.sh启动脚本,将CA容器启动的参数带入 接下来我们需要修改network_setup.sh文件,因为前面我们使用了变量${PRIVATE_KEY},所以这里我们需要读取变量并带入docker-compose 启动的时候。具体修改如下: function networkUp () { if [ -f "./crypto-config" ]; then echo "crypto-config directory already exists." else #Generate all the artifacts that includes org certs, orderer genesis block, # channel configuration transaction source generateArtifacts.sh $CH_NAME fi folder="crypto-config/peerOrganizations/org1.example.com/ca" privName="" for file_a in ${folder}/* do temp_file=`basename $file_a` if [ ${temp_file##*.} != "pem" ];then privName=$temp_file fi done echo $privName if [ "${IF_COUCHDB}" == "couchdb" ]; then CHANNEL_NAME=$CH_NAME TIMEOUT=$CLI_TIMEOUT docker-compose -f $COMPOSE_FILE -f $COMPOSE_FILE_COUCH up -d 2>&1 else CHANNEL_NAME=$CH_NAME TIMEOUT=$CLI_TIMEOUT PRIVATE_KEY=$privName docker-compose -f $COMPOSE_FILE up -d 2>&1 fi if [ $? -ne 0 ]; then echo "ERROR !!!! Unable to pull the images " exit 1 fi docker logs -f cli } 这里脚本的逻辑很简单,就是去crypto-config/peerOrganizations/org1.example.com/ca这个文件夹中去遍历文件,找到私钥文件的文件名,并把文件名赋值给privName,然后在docker-compse的启动时,指定到PRIVATE_KEY即可。 3.使用CA Client生成新用户 只需要经过前面2步,我们给Org1设置的CA Server就算完成了。 3.1启动Fabric网络 运行 ./network_setup.sh up 启动整个Fabric网络。接下来需要使用CA Client来生成新用户。我们需要以下几步: 3.2下载并安装Fabric CA Client 官方提供的CA Client需要依赖于libtool这个库,所以需要先安装这个库,运行命令: sudo apt install libtool libltdl-dev 然后执行以下命令安装Fabric CA Client: go get -u github.com/hyperledger/fabric-ca/cmd/... 该命令执行完毕后,我们应该在~/go/bin下面看到生成的2个文件: fabric-ca-client fabric-ca-server 3.3注册认证管理员 我们首先需要以管理员身份使用CA Client连接到CA Server,并生成相应的文件。 export FABRIC_CA_CLIENT_HOME=$HOME/ca fabric-ca-client enroll -u http://admin:adminpw@localhost:7054 这个时候我们可以去$HOME/ca目录,看到CA Client创建了一个fabric-ca-client-config.yaml文件和一个msp文件夹。config可以去修改一些组织信息之类的。 3.4注册新用户 接下来我们想新建一个叫devin的用户,那么需要先执行这个命令: fabric-ca-client register --id.name devin --id.type user --id.affiliation org1.department1 --id.attrs 'hf.Revoker=true,foo=bar' 系统会返回一个该用户的密码: 2017/09/05 22:20:41 [INFO] User provided config file: /home/studyzy/ca/fabric-ca-client-config.yaml 2017/09/05 22:20:41 [INFO] Configuration file location: /home/studyzy/ca/fabric-ca-client-config.yaml Password: GOuMzkcGgGzq 我们拿到这个密码以后就可以再次使用enroll命令,给devin这个用户生成msp的私钥和证书: fabric-ca-client enroll -u http://devin:GOuMzkcGgGzq@localhost:7054 -M $FABRIC_CA_CLIENT_HOME/devinmsp 现在新用户devin的私钥和证书就在$HOME/ca/devinmsp目录下,我们可以使用tree命令查看一下: devinmsp/ ├── cacerts │ └── localhost-7054.pem ├── keystore │ └── a044e43ad1fd7cdfd1fd995abaef53895534bd70e8cdfdb665430d12665f2041_sk └── signcerts └── cert.pem 4.编写ChainCode验证当前用户 由于官方提供的example02并没有关于当前用户的信息的代码,所以我们需要编写自己的ChainCode。 这里我们主要是用到ChainCode接口提供的GetCreator方法,具体完整的ChainCode如下: package main import ( "github.com/hyperledger/fabric/core/chaincode/shim" pb "github.com/hyperledger/fabric/protos/peer" "fmt" "encoding/pem" "crypto/x509" "bytes" ) 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 == "cert" {//自定义函数名称 return t.testCertificate(stub, args)//定义调用的函数 } return shim.Error("Received unknown function invocation") } func (t *SimpleChaincode) testCertificate(stub shim.ChaincodeStubInterface, args []string) pb.Response{ creatorByte,_:= stub.GetCreator() certStart := bytes.IndexAny(creatorByte, "-----")// Devin:I don't know why sometimes -----BEGIN is invalid, so I use ----- 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") } fmt.Println(string(certText)) cert, err := x509.ParseCertificate(bl.Bytes) if err != nil { fmt.Errorf("ParseCertificate failed") } fmt.Println(cert) uname:=cert.Subject.CommonName fmt.Println("Name:"+uname) return shim.Success([]byte("Called testCertificate "+uname)) } 我们只需要在~/go/src/github.com/hyperledger/fabric/examples/chaincode/go目录下新建一个文件夹,比如test1,然后新建一个文件test1.go并粘贴上面的代码进去即可。 现在ChainCode已经开发完成,我们需要部署并测试该ChainCode的正确性,下面是部署步骤: 首先登陆到cli中: docker exec -it cli bash 然后在cli下面执行以下命令: peer chaincode install -n test1 -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/test1 ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem peer chaincode instantiate -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA -C mychannel -n test1 -v 1.0 -c '{"Args":[]}' peer chaincode query -C mychannel -n test1 -c '{"Args":["cert"]}' 系统返回结果,说明我们当前的用户是Admin@org1.example.com root@2d1735e72642:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -C mychannel -n test1 -c '{"Args":["cert"]}' 2017-09-05 14:40:23.175 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP 2017-09-05 14:40:23.176 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity 2017-09-05 14:40:23.176 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc 2017-09-05 14:40:23.176 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc 2017-09-05 14:40:23.176 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0A95070A6708031A0B08D7EEBACD0510...07120574657374311A060A0463657274 2017-09-05 14:40:23.176 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: B4BB1EE5E6EBA63E50C85831C8820FB0B4490C55F23C247EDE5529DDAB23C273 Query Result: Called testCertificate Admin@org1.example.com 2017-09-05 14:40:23.195 UTC [main] main -> INFO 007 Exiting..... 5.设置新用户的证书和私钥文件夹,验证新用户的可用性 因为我们是给org1设置的CA,用户devin也是在org1下,所以需要把~/ca/devinmsp下面的文件转移到org1下面。org1的用户证书和私钥文件夹在: ~/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users 我们需要新建文件夹devin用于保存新用户的证书和私钥,我们新建一个Ubuntu的命令行窗口,前面已经登录您的cli的窗口保留,我们接下来还会用。 cd ~/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users mkdir devin cp ~/ca/devinmsp/ devin/msp –R 不知道什么原因,Fabric在使用的时候需要用到msp文件夹下的admincerts文件夹,但是CA Client在生成的时候并没有这个文件夹,所以我们需要从signcerts这个文件夹中拷贝一个过来,运行以下命令: mkdir devin/msp/admincerts cp devin/msp/signcerts/cert.pem devin/msp/admincerts/ 好现在我们的新用户的所有证书准备完毕,tree devin看看结果: devin/ └── msp ├── admincerts │ └── cert.pem ├── cacerts │ └── localhost-7054.pem ├── keystore │ └── a044e43ad1fd7cdfd1fd995abaef53895534bd70e8cdfdb665430d12665f2041_sk └── signcerts └── cert.pem 接下来切换到cli窗口,我们把当前cli的用户msp文件夹切换成devin的文件夹,具体命令是: CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/devin/msp 现在我们再来运行一下ChainCode: peer chaincode query -C mychannel -n test1 -c '{"Args":["cert"]}' 我们可以看到结果已经变化了,用户已经由Admin变成了devin: root@2d1735e72642:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -C mychannel -n test1 -c '{"Args":["cert"]}' 2017-09-05 14:53:17.497 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP 2017-09-05 14:53:17.497 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity 2017-09-05 14:53:17.497 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc 2017-09-05 14:53:17.497 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc 2017-09-05 14:53:17.497 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0AE3070A6808031A0C08DDF4BACD0510...07120574657374311A060A0463657274 2017-09-05 14:53:17.497 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: 21EE9B77A231E22ACB5FDD59C668C1A6E300833A820901DF9E896E22C00FC00F Query Result: Called testCertificate devin 2017-09-05 14:53:17.508 UTC [main] main -> INFO 007 Exiting..... 以上就是关于Fabric CA环境集成的简单测试。关于CA Server有配置文件在CA Server容器内部,可以针对不同的org信息进行修改。而CA Client也有配置文件,也可以在enroll之前进行修改。关于具体的修改方法,参考官方文档:http://hyperledger-fabric-ca.readthedocs.io/en/latest/【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
Fabric在启动之前需要生成Orderer的创世区块和channel的配置区块。也就是说在Fabric网络启动之前我们就必须定好了有哪些Org,而当Fabric已经跑起来之后,想要增加Org却是很麻烦的事情。 官方给出的解决方案是:使用configtxlator,可以将配置区块二进制转换为JSON,然后修改JSON,增加Org,再使用这个工具生成一个增量区块。最后再配合peer channel update命令,实现对原有配置的更新。 整个过程操作起来非常复杂。官方文档在这里: http://hyperledger-fabric.readthedocs.io/en/latest/configtxlator.html 汉化版是: https://github.com/qiushaoxi/doc_translation/blob/master/Reconfiguring%20with%20configtxlator.rst Yeasy的GitHub上也有更清晰的一篇介绍: https://github.com/yeasy/docker-compose-files/blob/master/hyperledger/docs/configtxlator.md 大壮应该是根据Yeasy的文章,进行了扩充,写了一篇中文的: http://www.jianshu.com/p/eb8fe7cb6f5a 真的是太麻烦了,希望以后的版本能够有所改进。博主本人并没有实测过这些步骤,不过据网友反应, 这样操作是能够成功添加Org的。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
博主之前的文章都是教大家怎么快速的搭建一个Fabric的环境,但是其中大量的工作都隐藏到了官方的脚本中,并不方便大家深入理解其中的过程,所以博主这里就将其中的过程一步步分解,方便大家! 前面的准备工作我就不用多说了,也就是各种软件和开发环境的安装,安装好以后,我们git clone下来最新的代码,并切换到v1.0.0,并且下载好我们需要使用的docker镜像,也就是到步骤6,接下来我们要解析的是之后的步骤,也就是真正的搭建Fabric的过程。 1.生成公私钥和证书 Fabric中有两种类型的公私钥和证书,一种是给节点之前通讯安全而准备的TLS证书,另一种是用户登录和权限控制的用户证书。这些证书本来应该是由CA来颁发,但是我们这里是测试环境,并没有启用CA节点,所以Fabric帮我们提供了一个工具:cryptogen。 1.1编译生成cryptogen 我们既然获得了Fabric的源代码,那么就可以轻易的使用make命令编译需要的程序。Fabric官方提供了专门编译cryptogen的入口,我们只需要运行以下命令即可: cd ~/go/src/github.com/hyperledger/fabric make cryptogen 运行后系统返回结果: build/bin/cryptogen CGO_CFLAGS=" " GOBIN=/home/studyzy/go/src/github.com/hyperledger/fabric/build/bin go install -tags "" -ldflags "-X github.com/hyperledger/fabric/common/tools/cryptogen/metadata.Version=1.0.0" github.com/hyperledger/fabric/common/tools/cryptogen Binary available as build/bin/cryptogen 也就是说我们在build/bin文件夹下可以看到编译出来的cryptogen程序。 1.2配置crypto-config.yaml examples/e2e_cli/crypto-config.yaml已经提供了一个Orderer Org和两个Peer Org的配置,该模板中也对字段进行了注释。我们可以把Org2拿来分析一下: - Name: Org2 Domain: org2.example.com Template: Count: 2 Users: Count: 1 Name和Domain就是关于这个组织的名字和域名,这主要是用于生成证书的时候,证书内会包含该信息。而Template Count=2是说我们要生成2套公私钥和证书,一套是peer0.org2的,还有一套是peer1.org2的。最后Users. Count=1是说每个Template下面会有几个普通User(注意,Admin是Admin,不包含在这个计数中),这里配置了1,也就是说我们只需要一个普通用户User1@org2.example.com 我们可以根据实际需要调整这个配置文件,增删Org Users等。 1.3生成公私钥和证书 我们配置好crypto-config.yaml文件后,就可以用cryptogen去读取该文件,并生成对应的公私钥和证书了: cd examples/e2e_cli/ ../../build/bin/cryptogen generate --config=./crypto-config.yaml 生成的文件都保存到crypto-config文件夹,我们可以进入该文件夹查看生成了哪些文件: tree crypto-config 2.生成创世区块和Channel配置区块 2.1编译生成configtxgen 与前面1.1说到的类似,我们可以通过make命令生成configtxgen程序: cd ~/go/src/github.com/hyperledger/fabric make configtxgen 运行后的结果为: build/bin/configtxgen CGO_CFLAGS=" " GOBIN=/home/studyzy/go/src/github.com/hyperledger/fabric/build/bin go install -tags "nopkcs11" -ldflags "-X github.com/hyperledger/fabric/common/configtx/tool/configtxgen/metadata.Version=1.0.0" github.com/hyperledger/fabric/common/configtx/tool/configtxgen Binary available as build/bin/configtxgen 2.2配置configtx.yaml 官方提供的examples/e2e_cli/configtx.yaml这个文件里面配置了由2个Org参与的Orderer共识配置TwoOrgsOrdererGenesis,以及由2个Org参与的Channel配置:TwoOrgsChannel。Orderer可以设置共识的算法是Solo还是Kafka,以及共识时区块大小,超时时间等,我们使用默认值即可,不用更改。而Peer节点的配置包含了MSP的配置,锚节点的配置。如果我们有更多的Org,或者有更多的Channel,那么就可以根据模板进行对应的修改。 2.3生成创世区块 配置修改好后,我们就用configtxgen 生成创世区块。并把这个区块保存到本地channel-artifacts文件夹中: cd examples/e2e_cli/ ../../build/bin/configtxgen -profile TwoOrgsOrdererGenesis -outputBlock ./channel-artifacts/genesis.block 2.4生成Channel配置区块 ../../build/bin/configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/channel.tx -channelID mychannel 另外关于锚节点的更新,我们也需要使用这个程序来生成文件: ../../build/bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org1MSPanchors.tx -channelID mychannel -asOrg Org1MSP ../../build/bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org2MSPanchors.tx -channelID mychannel -asOrg Org2MSP 最终,我们在channel-artifacts文件夹中,应该是能够看到4个文件。 channel-artifacts/ ├── channel.tx ├── genesis.block ├── Org1MSPanchors.tx └── Org2MSPanchors.tx 3.配置Fabric环境的docker-compose文件 前面对节点和用户的公私钥以及证书,还有创世区块都生成完毕,接下来我们就可以配置docker-compose的yaml文件,启动Fabric的Docker环境了。 3.1配置Orderer Orderer的配置是在base/docker-compose-base.yaml里面,我们看看其中的内容: orderer.example.com: container_name: orderer.example.com image: hyperledger/fabric-orderer environment: - ORDERER_GENERAL_LOGLEVEL=debug - ORDERER_GENERAL_LISTENADDRESS=0.0.0.0 - ORDERER_GENERAL_GENESISMETHOD=file - ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block - ORDERER_GENERAL_LOCALMSPID=OrdererMSP - ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp # enabled TLS - ORDERER_GENERAL_TLS_ENABLED=true - ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key - ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt - ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt] working_dir: /opt/gopath/src/github.com/hyperledger/fabric command: orderer volumes: - ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block - ../crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/msp:/var/hyperledger/orderer/msp - ../crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/:/var/hyperledger/orderer/tls ports: - 7050:7050 这里主要关心的是,ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block,而这个创世区块就是我们之前创建的创世区块,这里就是Host到Docker的映射: - ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block 另外的配置主要是TL,Log等,最后暴露出服务端口7050。 3.2配置Peer Peer的配置是在base/docker-compose-base.yaml和peer-base.yaml里面,我们摘取其中的peer0.org1看看其中的内容: peer-base: image: hyperledger/fabric-peer environment: - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock # the following setting starts chaincode containers on the same # bridge network as the peers # https://docs.docker.com/compose/networking/ - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=e2ecli_default #- CORE_LOGGING_LEVEL=ERROR - CORE_LOGGING_LEVEL=DEBUG - CORE_PEER_TLS_ENABLED=true - CORE_PEER_GOSSIP_USELEADERELECTION=true - CORE_PEER_GOSSIP_ORGLEADER=false - CORE_PEER_PROFILE_ENABLED=true - CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt - CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key - CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer command: peer node start peer0.org1.example.com: container_name: peer0.org1.example.com extends: file: peer-base.yaml service: peer-base environment: - CORE_PEER_ID=peer0.org1.example.com - CORE_PEER_ADDRESS=peer0.org1.example.com:7051 - CORE_PEER_CHAINCODELISTENADDRESS=peer0.org1.example.com:7052 - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.example.com:7051 - CORE_PEER_LOCALMSPID=Org1MSP volumes: - /var/run/:/host/var/run/ - ../crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp:/etc/hyperledger/fabric/msp - ../crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls:/etc/hyperledger/fabric/tls ports: - 7051:7051 - 7052:7052 - 7053:7053 在Peer的配置中,主要是给Peer分配好各种服务的地址,以及TLS和MSP信息。 3.3配置CLI CLI在整个Fabric网络中扮演客户端的角色,我们在开发测试的时候可以用CLI来代替SDK,执行各种SDK能执行的操作。CLI会和Peer相连,把指令发送给对应的Peer执行。CLI的配置在docker-compose-cli.yaml中,我们看看其中的内容: cli: container_name: cli image: hyperledger/fabric-tools tty: true environment: - GOPATH=/opt/gopath - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock - CORE_LOGGING_LEVEL=DEBUG - CORE_PEER_ID=cli - CORE_PEER_ADDRESS=peer0.org1.example.com:7051 - CORE_PEER_LOCALMSPID=Org1MSP - CORE_PEER_TLS_ENABLED=true - CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt - CORE_PEER_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key - CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt - CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer command: /bin/bash -c './scripts/script.sh ${CHANNEL_NAME}; sleep $TIMEOUT' volumes: - /var/run/:/host/var/run/ - ../chaincode/go/:/opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/go - ./crypto-config:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ - ./scripts:/opt/gopath/src/github.com/hyperledger/fabric/peer/scripts/ - ./channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts depends_on: - orderer.example.com - peer0.org1.example.com - peer1.org1.example.com - peer0.org2.example.com - peer1.org2.example.com 从这里我们可以看到,CLI启动的时候默认连接的是peer0.org1.example.com,并且启用了TLS。默认是以Admin@org1.example.com这个身份连接到Peer的。CLI启动的时候,会去执行./scripts/script.sh 脚本,这个脚本也就是fabric/examples/e2e_cli/scripts/script.sh 这个脚本,这个脚本完成了Fabric环境的初始化和ChainCode的安装及运行,也就是接下来要讲的步骤4和5.在文件映射配置上,我们注意到../chaincode/go/:/opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/go,也就是说我们要安装的ChainCode都是在fabric/examples/chaincode/go目录下,以后我们要开发自己的ChainCode,只需要把我们的代码复制到该目录即可。 【注意:请注释掉cli中command这一行,我们不需要CLI启动的时候自动执行脚本,我们在步骤4,5要一步步的手动执行!】 4.初始化Fabric环境 4.1启动Fabric环境的容器 我们将整个Fabric Docker环境的配置放在docker-compose-cli.yaml后,只需要使用以下命令即可: docker-compose -f docker-compose-cli.yaml up -d 最后这个-d参数如果不加,那么当前终端就会一直附加在docker-compose上,而如果加上的话,那么docker容器就在后台运行。运行docker ps命令可以看启动的结果: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 6f98f57714b5 hyperledger/fabric-tools "/bin/bash" 8 seconds ago Up 7 seconds cli 6e7b3fd0e803 hyperledger/fabric-peer "peer node start" 11 seconds ago Up 8 seconds 0.0.0.0:10051->7051/tcp, 0.0.0.0:10052->7052/tcp, 0.0.0.0:10053->7053/tcp peer1.org2.example.com 9e67abfb982f hyperledger/fabric-orderer "orderer" 11 seconds ago Up 8 seconds 0.0.0.0:7050->7050/tcp orderer.example.com 908d7fe2a4c7 hyperledger/fabric-peer "peer node start" 11 seconds ago Up 9 seconds 0.0.0.0:7051-7053->7051-7053/tcp peer0.org1.example.com 6bb187ac10ec hyperledger/fabric-peer "peer node start" 11 seconds ago Up 10 seconds 0.0.0.0:9051->7051/tcp, 0.0.0.0:9052->7052/tcp, 0.0.0.0:9053->7053/tcp peer0.org2.example.com 150baa520ed0 hyperledger/fabric-peer "peer node start" 12 seconds ago Up 9 seconds 0.0.0.0:8051->7051/tcp, 0.0.0.0:8052->7052/tcp, 0.0.0.0:8053->7053/tcp peer1.org1.example.com 可以看到1Orderer+4Peer+1CLI都启动了。 4.2创建Channel 现在我们要进入cli容器内部,在里面创建Channel。先用以下命令进入CLI内部Bash: docker exec -it cli bash 创建Channel的命令是peer channel create,我们前面创建2.4创建Channel的配置区块时,指定了Channel的名字是mychannel,那么这里我们必须创建同样名字的Channel。 ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem peer channel create -o orderer.example.com:7050 -c mychannel -f ./channel-artifacts/channel.tx --tls true --cafile $ORDERER_CA 执行该命令后,系统会提示: 2017-08-29 20:36:47.486 UTC [channelCmd] readBlock -> DEBU 020 Received block:0 系统会在cli内部的当前目录创建一个mychannel.block文件,这个文件非常重要,接下来其他节点要加入这个Channel就必须使用这个文件。 4.3各个Peer加入Channel 前面说过,我们CLI默认连接的是peer0.org1,那么我们要将这个Peer加入mychannel就很简单,只需要运行如下命令: peer channel join -b mychannel.block 系统返回消息: 2017-08-29 20:40:27.053 UTC [channelCmd] executeJoin -> INFO 006 Peer joined the channel! 那么其他几个Peer又该怎么加入Channel呢?这里就需要修改CLI的环境变量,使其指向另外的Peer。比如我们要把peer1.org1加入mychannel,那么命令是: CORE_PEER_LOCALMSPID="Org1MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp CORE_PEER_ADDRESS=peer1.org1.example.com:7051 peer channel join -b mychannel.block 系统会返回成功加入Channel的消息。 同样的方法,将peer0.org2加入mychannel: CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer0.org2.example.com:7051 peer channel join -b mychannel.block 最后把peer1.org2加入mychannel: CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/tls/ca.crt CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer1.org2.example.com:7051 peer channel join -b mychannel.block 4.4更新锚节点 关于AnchorPeer,我理解的不够深刻,经过我的测试,即使没有设置锚节点的情况下,整个Fabric网络仍然是能正常运行的。 对于Org1来说,peer0.org1是锚节点,我们需要连接上它并更新锚节点: CORE_PEER_LOCALMSPID="Org1MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp CORE_PEER_ADDRESS=peer0.org1.example.com:7051 peer channel update -o orderer.example.com:7050 -c mychannel -f ./channel-artifacts/Org1MSPanchors.tx --tls true --cafile $ORDERER_CA 另外对于Org2,peer0.org2是锚节点,对应的更新代码是: CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer0.org2.example.com:7051 peer channel update -o orderer.example.com:7050 -c mychannel -f ./channel-artifacts/Org2MSPanchors.tx --tls true --cafile $ORDERER_CA 5.链上代码的安装与运行 以上,整个Fabric网络和Channel都准备完毕,接下来我们来安装和运行ChainCode。这里仍然以最出名的Example02为例。这个例子实现了a,b两个账户,相互之间可以转账。 5.1Install ChainCode安装链上代码 链上代码的安装需要在各个相关的Peer上进行,对于我们现在这种Fabric网络,如果4个Peer都想对Example02进行操作,那么就需要安装4次。 仍然是保持在CLI的命令行下,我们先切换到peer0.org1这个节点: CORE_PEER_LOCALMSPID="Org1MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp CORE_PEER_ADDRESS=peer0.org1.example.com:7051 使用peer chaincode install命令可以安装指定的ChainCode并对其命名: peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 安装的过程其实就是对CLI中指定的代码进行编译打包,并把打包好的文件发送到Peer,等待接下来的实例化。 其他节点由于暂时还没使用到,我们可以先不安装,等到了步骤5.4的时候再安装。 5.2Instantiate ChainCode实例化链上代码 实例化链上代码主要是在Peer所在的机器上对前面安装好的链上代码进行包装,生成对应Channel的Docker镜像和Docker容器。并且在实例化时我们可以指定背书策略。我们运行以下命令完成实例化: peer chaincode instantiate -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA -C mychannel -n mycc -v 1.0 -c '{"Args":["init","a","100","b","200"]}' -P "OR ('Org1MSP.member','Org2MSP.member')" 如果我们新开一个Ubuntu终端,去查看peer0.org1上的日志,那么就可以知道整个实例化的过程到底干了什么: docker logs -f peer0.org1.example.com 主要几行重要的日志: 2017-08-29 21:14:12.290 UTC [chaincode-platform] generateDockerfile -> DEBU 3fd FROM hyperledger/fabric-baseos:x86_64-0.3.1 ADD binpackage.tar /usr/local/bin LABEL org.hyperledger.fabric.chaincode.id.name="mycc" \ org.hyperledger.fabric.chaincode.id.version="1.0" \ org.hyperledger.fabric.chaincode.type="GOLANG" \ org.hyperledger.fabric.version="1.0.0" \ org.hyperledger.fabric.base.version="0.3.1" ENV CORE_CHAINCODE_BUILDLEVEL=1.0.0 ENV CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/peer.crt COPY peer.crt /etc/hyperledger/fabric/peer.crt 2017-08-29 21:14:12.297 UTC [util] DockerBuild -> DEBU 3fe Attempting build with image hyperledger/fabric-ccenv:x86_64-1.0.0 2017-08-29 21:14:48.907 UTC [dockercontroller] deployImage -> DEBU 3ff Created image: dev-peer0.org1.example.com-mycc-1.0 2017-08-29 21:14:48.908 UTC [dockercontroller] Start -> DEBU 400 start-recreated image successfully 2017-08-29 21:14:48.908 UTC [dockercontroller] createContainer -> DEBU 401 Create container: dev-peer0.org1.example.com-mycc-1.0 接下来的日志就是各种初始化,验证,写账本之类的。总之完毕后,我们回到Ubuntu终端,使用docker ps可以看到有新的容器正在运行: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 07791d4a99b7 dev-peer0.org1.example.com-mycc-1.0 "chaincode -peer.a..." About a minute ago Up About a minute dev-peer0.org1.example.com-mycc-1.0 6f98f57714b5 hyperledger/fabric-tools "/bin/bash" About an hour ago Up About an hour cli 6e7b3fd0e803 hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:10051->7051/tcp, 0.0.0.0:10052->7052/tcp, 0.0.0.0:10053->7053/tcp peer1.org2.example.com 9e67abfb982f hyperledger/fabric-orderer "orderer" About an hour ago Up About an hour 0.0.0.0:7050->7050/tcp orderer.example.com 908d7fe2a4c7 hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:7051-7053->7051-7053/tcp peer0.org1.example.com 6bb187ac10ec hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:9051->7051/tcp, 0.0.0.0:9052->7052/tcp, 0.0.0.0:9053->7053/tcp peer0.org2.example.com 150baa520ed0 hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:8051->7051/tcp, 0.0.0.0:8052->7052/tcp, 0.0.0.0:8053->7053/tcp peer1.org1.example.com 5.3在一个Peer上查询并发起交易 现在链上代码的实例也有了,并且在实例化的时候指定了a账户100,b账户200,我们可以试着调用ChainCode的查询代码,验证一下,在cli容器内执行: peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}' 返回结果:Query Result: 100 接下来我们可以试着把a账户的10元转给b。对应的代码: peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA -C mychannel -n mycc -c '{"Args":["invoke","a","b","10"]}' 5.4在另一个节点上查询交易 前面的操作都是在org1下面做的,那么处于同一个区块链(同一个Channel下)的org2,是否会看org1的更改呢?我们试着给peer0.org2安装链上代码: CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer0.org2.example.com:7051 peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 由于mycc已经在前面org1的时候实例化了,也就是说对应的区块已经生成了,所以在org2不能再次初始化。我们直接运行查询命令: peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}' 这个时候我们发现运行该命令后要等很久(我这里花了40秒)才返回结果: Query Result: 90 这是因为peer0.org2也需要生成Docker镜像,创建对应的容器,才能通过容器返回结果。我们回到Ubuntu终端,执行docker ps,可以看到又多了一个容器: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3e37aba50189 dev-peer0.org2.example.com-mycc-1.0 "chaincode -peer.a..." 2 minutes ago Up 2 minutes dev-peer0.org2.example.com-mycc-1.0 07791d4a99b7 dev-peer0.org1.example.com-mycc-1.0 "chaincode -peer.a..." 21 minutes ago Up 21 minutes dev-peer0.org1.example.com-mycc-1.0 6f98f57714b5 hyperledger/fabric-tools "/bin/bash" About an hour ago Up About an hour cli 6e7b3fd0e803 hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:10051->7051/tcp, 0.0.0.0:10052->7052/tcp, 0.0.0.0:10053->7053/tcp peer1.org2.example.com 9e67abfb982f hyperledger/fabric-orderer "orderer" About an hour ago Up About an hour 0.0.0.0:7050->7050/tcp orderer.example.com 908d7fe2a4c7 hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:7051-7053->7051-7053/tcp peer0.org1.example.com 6bb187ac10ec hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:9051->7051/tcp, 0.0.0.0:9052->7052/tcp, 0.0.0.0:9053->7053/tcp peer0.org2.example.com 150baa520ed0 hyperledger/fabric-peer "peer node start" About an hour ago Up About an hour 0.0.0.0:8051->7051/tcp, 0.0.0.0:8052->7052/tcp, 0.0.0.0:8053->7053/tcp peer1.org1.example.com 总结 通过以上的分解,希望大家对Fabric环境的创建有了更深入的理解。我这里的示例仍然是官方的示例,并没有什么太新的东西。只要把这每一步搞清楚,那么接下来我们在生产环境创建更多的Org,创建大量的Channel,执行各种ChainCode都是如出一辙。 最后,大家如果想进一步探讨Fabric或者使用中遇到什么问题可以加入QQ群【494085548】大家一起讨论。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
之前笔者写了一篇Fabric1.0 Beta的部署和Fabric 1.0的多机部署。但是很多人在部署Fabric的时候还是很容易出问题,所以我就再把Fabric 1.0的单机环境搭建讲一下。其实很多内容和前面博客相同。下面开始我们的环境搭建工作: 1. 使用VirtualBox并在其中安装好Ubuntu 这一步其实没啥好说的,下载好最新版的VirtualBox,下载Ubuntu Server,我用的是Ubuntu16.04.2 X64 Server。在安装完Ubuntu后,需要保证apt source是国内的,不然如果是国外的话会很慢很慢的。具体做法是 sudo vi /etc/apt/sources.list 打开这个apt源列表,如果其中看到是http://us.xxxxxx之类的,那么就是外国的,如果看到是http://cn.xxxxx之类的,那么就不用换的。我的是美国的源,所以需要做一下批量的替换。在命令模式下,输入: :%s/us./cn./g 就可以把所有的us.改为cn.了。然后输入:wq即可保存退出。 sudo apt-get update 更新一下源。 然后安装ssh,这样接下来就可以用putty或者SecureCRT之类的客户端远程连接Ubuntu了。 sudo apt-get install ssh 2. Go的安装 Ubuntu的apt-get虽然提供了Go的安装,但是版本比较旧,最好的方法还是参考官方网站 https://golang.org/dl/ ,下载最新版的Go。具体涉及到的命令包括: wget https://storage.googleapis.com/golang/go1.9.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.9.linux-amd64.tar.gz 【注意:不要使用apt方式安装go,apt的go版本太低了!】 接下来编辑当前用户的环境变量: vi ~/.profile 添加以下内容: export PATH=$PATH:/usr/local/go/bin export GOROOT=/usr/local/go export GOPATH=$HOME/go export PATH=$PATH:$HOME/go/bin 编辑保存并退出vi后,记得把这些环境载入: source ~/.profile 我们把go的目录GOPATH设置为当前用户的文件夹下,所以记得创建go文件夹 cd ~ mkdir go 3. Docker安装 我们可以使用阿里提供的镜像,安装也非常方便。通过以下命令来安装Docker curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh - 安装完成后需要修改当前用户(我使用的用户叫fabric)权限: sudo usermod -aG docker fabric 注销并重新登录,然后添加阿里云的Docker Hub镜像: sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://obou6wyb.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker 不同的版本添加方法是不一样的,官方的文档如下: https://cr.console.aliyun.com/#/accelerator 当然觉得阿里云镜像不好用,喜欢用DaoClound的也可以用DaoClound的镜像。DaoCloud的镜像设置文档为:https://www.daocloud.io/mirror#accelerator-doc 4. Docker-Compose的安装 Docker-compose是支持通过模板脚本批量创建Docker容器的一个组件。在安装Docker-Compose之前,需要安装Python-pip,运行脚本: sudo apt-get install python-pip 然后是安装docker-compose,我们从官方网站(https://github.com/docker/compose/releases)下载也可以从国内的进行DaoClound下载,为了速度快接下来从DaoClound安装Docker-compose,运行脚本: curl -L https://get.daocloud.io/docker/compose/releases/download/1.12.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose sudo mv ~/docker-compose /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose 5. Fabric源码下载 我们可以使用Git命令下载源码,首先需要建立对应的目录,然后进入该目录,Git下载源码: mkdir -p ~/go/src/github.com/hyperledger cd ~/go/src/github.com/hyperledger git clone https://github.com/hyperledger/fabric.git 由于Fabric一直在更新,所有我们并不需要最新最新的源码,需要切换到v1.0.0版本的源码即可: cd ~/go/src/github.com/hyperledger/fabric git checkout v1.0.0 6. Fabric Docker镜像的下载 这个其实很简单,因为我们已经设置了Docker Hub镜像地址,所以下载也会很快。官方文件也提供了批量下载的脚本。我们直接运行: cd ~/go/src/github.com/hyperledger/fabric/examples/e2e_cli/ source download-dockerimages.sh -c x86_64-1.0.0 -f x86_64-1.0.0 这样就可以下载所有需要的Fabric Docker镜像了。由于我们设置了国内的镜像,所以下载应该是比较快的。 下载完毕后,我们运行以下命令检查下载的镜像列表: docker images 得到的结果如下: 7.启动Fabric网络并完成ChainCode的测试 我们仍然停留在e2e_cli文件夹,这里提供了启动、关闭Fabric网络的自动化脚本。我们要启动Fabric网络,并自动运行Example02 ChainCode的测试,执行一个命令: ./network_setup.sh up 这个做了以下操作: 7.1编译生成Fabric公私钥、证书的程序,程序在目录:fabric/release/linux-amd64/bin 7.2基于configtx.yaml生成创世区块和通道相关信息,并保存在channel-artifacts文件夹。 7.3基于crypto-config.yaml生成公私钥和证书信息,并保存在crypto-config文件夹中。 7.4基于docker-compose-cli.yaml启动1Orderer+4Peer+1CLI的Fabric容器。 7.5在CLI启动的时候,会运行scripts/script.sh文件,这个脚本文件包含了创建Channel,加入Channel,安装Example02,运行Example02等功能。 最后运行完毕,我们可以看到这样的界面: 如果您看到这个界面,这说明我们整个Fabric网络已经通了。 8.手动测试一下Fabric网络 我们仍然是以现在安装好的Example02为例,在官方例子中,channel名字是mychannel,链码的名字是mycc。我们首先进入CLI,我们重新打开一个命令行窗口,输入: docker exec -it cli bash 运行以下命令可以查询a账户的余额: peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}' 可以看到余额是90: 然后,我们试一试把a账户的余额再转20元给b账户,运行命令: peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n mycc -c '{"Args":["invoke","a","b","20"]}' 运行结果为: 现在转账完毕, 我们试一试再查询一下a账户的余额,没问题的话,应该是只剩下70了。我们看看实际情况: 果然,一切正常。最后我们要关闭Fabric网络,首先需要运行exit命令退出cli容器。关闭Fabric的命令与启动类似,命令为: cd ~/go/src/github.com/hyperledger/fabric/examples/e2e_cli ./network_setup.sh down 现在我们整个Fabric的环境已经测试完毕,恭喜,一切正常,接下来我们就是去做自己的区块链的开发。希望我的文章对大家有所帮助。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
深蓝前几篇博客讲了Fabric的环境搭建,在环境搭建好后,我们就可以进行Fabric的开发工作了。Fabric的开发主要分成2部分,ChainCode链上代码开发和基于SDK的Application开发。我们这里先讲ChainCode的开发。Fabric的链上代码支持Java或者Go语言进行开发,因为Fabric本身是Go开发的,所以深蓝建议还是用Go进行ChainCode的开发。 ChainCode的Go代码需要定义一个SimpleChaincode这样一个struct,然后在该struct上定义Init和Invoke两个函数,然后还要定义一个main函数,作为ChainCode的启动入口。以下是ChainCode的模板: 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的业务逻辑提供了大量实用的方法。下面一一讲解: 1.获得调用的参数 前面给出的ChainCode的模板中,我们已经可以看到,在Invoke的时候,由传入的参数来决定我们具体调用了哪个方法,所以需要先使用GetFunctionAndParameters解析调用的时候传入的参数。除了这个方法以外,接口还提供了另外几个方法,不过其本质都是一样的。 GetArgs() [][]byte 以byte数组的数组的形式获得传入的参数列表 GetStringArgs() []string 以字符串数组的形式获得传入的参数列表 GetFunctionAndParameters() (string, []string) 将字符串数组的参数分为两部分,数组第一个字是Function,剩下的都是Parameter GetArgsSlice() ([]byte, error) 以byte切片的形式获得参数列表 2. 增删改查State DB 对于ChainCode来说,核心的操作就是对State Database的增删改查,对此Fabric接口提供了3个对State DB的操作方法。 2.1 增改数据PutState(key string, value []byte) error 对于State DB来说,增加和修改数据是统一的操作,因为State DB是一个Key Value数据库,如果我们指定的Key在数据库中已经存在,那么就是修改操作,如果Key不存在,那么就是插入操作。对于实际的系统来说,我们的Key可能是单据编号,或者系统分配的自增ID+实体类型作为前缀,而Value则是一个对象经过JSON序列号后的字符串。比如说我们定义一个Student的Struct,然后插入一个学生数据,对于的代码应该是这样的: 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!")) } 2.2 删除数据DelState(key string) error 这个也很好理解,根据Key删除State DB的数据。如果根据Key找不到对于的数据,删除失败。 err= stub.DelState(key) if err != nil { return shim.Error("Failed to delete Student from DB, key is: "+key) } 2.3 查询数据GetState(key string) ([]byte, error) 因为我们是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里面】 3. 复合键的处理 3.1 生成复合键CreateCompositeKey(objectType string, attributes []string) (string, error) 前面在进行数据库的增删改查的时候,都需要用到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来把各个字段分割开的,因为这个字符太特殊,所以很适合做分割】 3.2 拆分复合键SplitCompositeKey(compositeKey string) (string, []string, error) 既然有组合那么就有拆分,当我们从数据库中获得了一个复合键的Key之后,怎么知道其具体是由哪些字段组成的呢。其实就是用U+0000把这个复合键再Split开,得到结果中第一个是objectType,剩下的就是复合键用到的列的值。 objType,attrArray,_:= stub.SplitCompositeKey(key1) fmt.Println("Object:"+objType+" ,Attributes:"+strings.Join(attrArray,"|")) 3.3 部分复合键的查询GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error) 这里其实是一种对Key进行前缀匹配的查询,也就是说,我们虽然是部分复合键的查询,但是不允许拿后面部分的复合键进行匹配,必须是前面部分。 4. 获得当前用户GetCreator() ([]byte, error) 这个方法可以获得调用这个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----- 我们常见的需求是在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)) } 5.高级查询 前面提到的GetState只是最基本的根据Key查询值的操作,但是对于很多时候,我们需要查询返回的是一个集合,比如我要知道某个区间的Key对于所有对象,或者我们需要对Value对象内部的属性进行查询。 5.1 Key区间查询GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) 提供了对某个区间的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) } 5.2 富查询GetQueryResult(query string) (StateQueryIteratorInterface, error) 这是一个“富查询”,是对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) } 5.3历史数据查询GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error) 对同一个数据(也就是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 } 5.4部分复合键查询GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error) 这个我在前面3.3已经说过了,只是因为那个函数即是复合键的,也是高级查询的,所以我在这里给这个函数留了一个位置。 6.调用另外的链上代码 InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response 这个比较好理解,就是在我们的链上代码中调用别人已经部署好的链上代码。比如官方提供的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. 7.获得提案对象Proposal属性 7.1 获得签名的提案GetSignedProposal() (*pb.SignedProposal, error) 从客户端发现背书节点的Transaction或者Query都是一个提案,GetSignedProposal获得当前的提案对象包括客户端对这个提案的签名。提案的内容如果直接打印出来感觉就像是乱码,其内包含了提案Header,Payload和Extension,里面更包含了复杂的结构,这里不讲,以后可以写一篇博客专门研究提案对象。 7.2获得Transient对象 GetTransient() (map[string][]byte, error) Transient是在提案中Payload对象中的一个属性,也就是ChaincodeProposalPayload.TransientMap 7.3获得交易时间戳GetTxTimestamp() (*timestamp.Timestamp, error) 交易时间戳也是在提案对象中获取的,提案对象的Header部分,也就是proposal.Header.ChannelHeader.Timestamp 7.4 获得Binding对象 GetBinding() ([]byte, error) 这个Binding对象也是从提案对象中提取并组合出来的,其中包含proposal.Header中的SignatureHeader.Nonce,SignatureHeader.Creator和ChannelHeader.Epoch。关于Proposal对象确实很8复杂,我目前了解的并不对,接下来得详细研究。 8.事件设置SetEvent(name string, payload []byte) error 当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】大家一起讨论。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
Fabric1.0已经正式发布一段时间了,官方给出的单机部署的脚本也很完备,基本上傻瓜式的一键部署,直接运行官方的network_setup.sh up即可。但是在实际生产环境,我们不可能把所有的节点都放在一台机器中,所以必然会遇到多级部署的问题。下面我们就来讲讲怎么实现多机部署和测试官方的ChainCode。 1.环境准备 我们要部署的是4Peer+1Orderer的架构,也就是官方的e2c_cli架构。为此我们需要准备5台机器。我们可以开5台虚拟机,也可以购买5台云服务器,不管怎么样,我们需要这5台机器网络能够互通,而且安装相同的系统,我们用的是Ubuntu 16.04版。为了方便,我建议先启用1台虚拟机,在其中把准备工作做完,然后基于这台虚拟机,再复制出4台即可。这里是我用到5台Server的主机名(角色)和IP: orderer.example.com 10.174.13.185 peer0.org1.example.com 10.51.120.220 peer1.org1.example.com 10.51.126.19 peer0.org2.example.com 10.51.116.133 peer1.org2.example.com 10.51.126.5 接下来我们需要准备软件环境,包括Go、Docker、Docker Compose,我在之前的单机部署的博客中也讲到过具体的方法,这里再复述一下: 1.1 Go的安装 Ubuntu的apt-get虽然提供了Go的安装,但是版本比较旧,最好的方法还是参考官方网站,下载最新版的Go。具体涉及到的命令包括: wget https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz 接下来编辑当前用户的环境变量: vi ~/.profile 添加以下内容: export PATH=$PATH:/usr/local/go/bin export GOROOT=/usr/local/go export GOPATH=$HOME/go export PATH=$PATH:$HOME/go/bin 编辑保存并退出vi后,记得把这些环境载入: source ~/.profile 我们把go的目录GOPATH设置为当前用户的文件夹下,所以记得创建go文件夹 cd ~ mkdir go 1.2 Docker安装 我们可以使用阿里提供的镜像,安装也非常方便。通过以下命令来安装Docker curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh - 安装完成后需要修改当前用户(我使用的用户叫fabric)权限: sudo usermod -aG docker fabric 注销并重新登录,然后添加阿里云的Docker Hub镜像: 不同的版本添加方法是不一样的,官方的文档如下: https://cr.console.aliyun.com/#/accelerator 当然觉得阿里云镜像不好用,喜欢用DaoClound的也可以用DaoClound的镜像。 1.3 Docker-Compose的安装 Docker-compose是支持通过模板脚本批量创建Docker容器的一个组件。在安装Docker-Compose之前,需要安装Python-pip,运行脚本: sudo apt-get install python-pip 安装完成后,接下来从DaoClound安装Docker-compose,运行脚本: curl -L https://get.daocloud.io/docker/compose/releases/download/1.10.1/docker-compose-`uname -s`-`uname -m` > ~/docker-compose sudo mv ~/docker-compose /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose 1.4 Fabric源码下载 我们可以使用Git命令下载源码,也可以使用go get命令,偷懒一点,我们直接用go get命令获取最新的Fabric源码: go get github.com/hyperledger/fabric 这个可能等的时间比较久,等完成后,我们可以在~/go/src/github.com/hyperledger/fabric中找到所有的最新的源代码。 由于Fabric一直在更新,所有我们并不需要最新最新的源码,需要切换到v1.0.0版本的源码即可: cd ~/go/src/github.com/hyperledger/fabric git checkout v1.0.0 1.5 Fabric Docker镜像的下载 这个其实很简单,因为我们已经设置了Docker Hub镜像地址,所以下载也会很快。官方文件也提供了批量下载的脚本。我们直接运行: cd ~/go/src/github.com/hyperledger/fabric/examples/e2e_cli/ source download-dockerimages.sh -c x86_64-1.0.0 -f x86_64-1.0.0 这样就可以下载所有需要的Fabric Docker镜像了。 2.docker-compose 配置文件准备 在Fabric的源码中,提供了单机部署4Peer+1Orderer的示例,在Example/e2e_cli文件夹中。我们可以在其中一台机器上运行单机的Fabric实例,确认无误后,在该机器上,生成公私钥,修改该机器中的Docker-compose配置文件,然后把这些文件分发给另外4台机器。我们就以orderer.example.com这台机器为例 2.1单机运行4+1 Fabric实例,确保脚本和镜像正常 我们先进入这个文件夹,然后直接运行 ./network_setup.sh up 这个命令可以在本机启动4+1的Fabric网络并且进行测试,跑Example02这个ChainCode。我们可以看到每一步的操作,最后确认单机没有问题。确认我们的镜像和脚本都是正常的,我们就可以关闭Fabric网络,继续我们的多机Fabric网络设置工作。关闭Fabric命令: ./network_setup.sh down 2.2生成公私钥、证书、创世区块等 公私钥和证书是用于Server和Server之间的安全通信,另外要创建Channel并让其他节点加入Channel就需要创世区块,这些必备文件都可以一个命令生成,官方已经给出了脚本: ./generateArtifacts.sh mychannel 运行这个命令后,系统会创建channel-artifacts文件夹,里面包含了mychannel这个通道相关的文件,另外还有一个crypto-config文件夹,里面包含了各个节点的公私钥和证书的信息。 2.3设置peer节点的docker-compose文件 e2e_cli中提供了多个yaml文件,我们可以基于docker-compose-cli.yaml文件创建: cp docker-compose-cli.yaml docker-compose-peer.yaml 然后修改docker-compose-peer.yaml,去掉orderer的配置,只保留一个peer和cli,因为我们要多级部署,节点与节点之前又是通过主机名通讯,所以需要修改容器中的host文件,也就是extra_hosts设置,修改后的peer配置如下: peer0.org1.example.com: container_name: peer0.org1.example.com extends: file: base/docker-compose-base.yaml service: peer0.org1.example.com extra_hosts: - "orderer.example.com:10.174.13.185" 同样,cli也需要能够和各个节点通讯,所以cli下面也需要添加extra_hosts设置,去掉无效的依赖,并且去掉command这一行,因为我们是每个peer都会有个对应的客户端,也就是cli,所以我只需要去手动执行一次命令,而不是自动运行。修改后的cli配置如下: cli: container_name: cli image: hyperledger/fabric-tools tty: true environment: - GOPATH=/opt/gopath - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock - CORE_LOGGING_LEVEL=DEBUG - CORE_PEER_ID=cli - CORE_PEER_ADDRESS=peer0.org1.example.com:7051 - CORE_PEER_LOCALMSPID=Org1MSP - CORE_PEER_TLS_ENABLED=true - CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt - CORE_PEER_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key - CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt - CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer volumes: - /var/run/:/host/var/run/ - ../chaincode/go/:/opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/go - ./crypto-config:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ - ./scripts:/opt/gopath/src/github.com/hyperledger/fabric/peer/scripts/ - ./channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts depends_on: - peer0.org1.example.com extra_hosts: - "orderer.example.com:10.174.13.185" - "peer0.org1.example.com:10.51.120.220" - "peer1.org1.example.com:10.51.126.19" - "peer0.org2.example.com:10.51.116.133" - "peer1.org2.example.com:10.51.126.5" 在单击模式下,4个peer会映射主机不同的端口,但是我们在多机部署的时候是不需要映射不同端口的,所以需要修改base/docker-compose-base.yaml文件,将所有peer的端口映射都改为相同的: ports: - 7051:7051 - 7052:7052 - 7053:7053 2.4设置orderer节点的docker-compose文件 与创建peer的配置文件类似,我们也复制一个yaml文件出来进行修改: cp docker-compose-cli.yaml docker-compose-orderer.yaml orderer服务器上我们只需要保留order设置,其他peer和cli设置都可以删除。orderer可以不设置extra_hosts。 2.5分发配置文件 前面4步的操作,我们都是在orderer.example.com上完成的,接下来我们需要将这些文件分发到另外4台服务器上。Linux之间的文件传输,我们可以使用scp命令。 我先登录peer0.org1.example.com,将本地的e2e_cli文件夹删除: rm e2e_cli –R 然后再登录到orderer服务器上,退回到examples文件夹,因为这样可以方便的把其下的e2e_cli文件夹整个传到peer0服务器上。 scp -r e2e_cli fabric@10.51.120.220:/home/fabric/go/src/github.com/hyperledger/fabric/examples/ 我们在前面配置的就是peer0.org1.example.com上的节点,所以复制过来后不需要做任何修改。 再次运行scp命令,复制到peer1.org1.example.com上,然后我们需要对docker-compose-peer.yaml做一个小小的修改,将启动的容器改为peer1.org1.example.com,并且添加peer0.org1.example.com的IP映射,对应的cli中也改成对peer1.org1.example.com的依赖。这是修改后的peer1.org1.example.com上的配置文件: version: '2' services: peer1.org1.example.com: container_name: peer1.org1.example.com extends: file: base/docker-compose-base.yaml service: peer1.org1.example.com extra_hosts: - "orderer.example.com:10.174.13.185" - "peer0.org1.example.com:10.51.120.220" cli: container_name: cli image: hyperledger/fabric-tools tty: true environment: - GOPATH=/opt/gopath - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock - CORE_LOGGING_LEVEL=DEBUG - CORE_PEER_ID=cli - CORE_PEER_ADDRESS=peer1.org1.example.com:7051 - CORE_PEER_LOCALMSPID=Org1MSP - CORE_PEER_TLS_ENABLED=true - CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/tls/server.crt - CORE_PEER_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/tls/server.key - CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/tls/ca.crt - CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer volumes: - /var/run/:/host/var/run/ - ../chaincode/go/:/opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/go - ./crypto-config:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ - ./scripts:/opt/gopath/src/github.com/hyperledger/fabric/peer/scripts/ - ./channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts depends_on: - peer1.org1.example.com extra_hosts: - "orderer.example.com:10.174.13.185" - "peer0.org1.example.com:10.51.120.220" - "peer1.org1.example.com:10.51.126.19" - "peer0.org2.example.com:10.51.116.133" - "peer1.org2.example.com:10.51.126.5" 接下来继续使用scp命令将orderer上的文件夹传送给peer0.org2.example.com和peer1.org2.example.com,然后也是修改一下docker-compose-peer.yaml文件,使得其启动对应的peer节点。 3.启动Fabric 现在所有文件都已经准备完毕,我们可以启动我们的Fabric网络了。 3.1启动orderer 让我们首先来启动orderer节点,在orderer服务器上运行: docker-compose -f docker-compose-orderer.yaml up –d 运行完毕后我们可以使用docker ps看到运行了一个名字为orderer.example.com的节点。 3.2启动peer 然后我们切换到peer0.org1.example.com服务器,启动本服务器的peer节点和cli,命令为: docker-compose -f docker-compose-peer.yaml up –d 运行完毕后我们使用docker ps应该可以看到2个正在运行的容器。 接下来依次在另外3台服务器运行启动peer节点容器的命令: docker-compose -f docker-compose-peer.yaml up –d 现在我们整个Fabric4+1服务器网络已经成型,接下来是创建channel和运行ChainCode。 3.3创建Channel测试ChainCode 我们切换到peer0.org1.example.com服务器上,使用该服务器上的cli来运行创建Channel和运行ChainCode的操作。首先进入cli容器: docker exec -it cli bash 进入容器后我们可以看到命令提示变为: root@b41e67d40583:/opt/gopath/src/github.com/hyperledger/fabric/peer# 说明我们已经以root的身份进入到cli容器内部。官方已经提供了完整的创建Channel和测试ChainCode的脚本,并且已经映射到cli容器内部,所以我们只需要在cli内运行如下命令: ./scripts/script.sh mychannel 那么该脚本就可以一步一步的完成创建通道,将其他节点加入通道,更新锚节点,创建ChainCode,初始化账户,查询,转账,再次查询等链上代码的各个操作都可以自动化实现。直到最后,系统提示: ===================== All GOOD, End-2-End execution completed ===================== 说明我们的4+1的Fabric多级部署成功了。我们现在是在peer0.org1.example.com的cli容器内,我们也可以切换到peer0.org2.example.com服务器,运行docker ps命令,可以看到本来是2个容器的,现在已经变成了3个容器,因为ChainCode会创建一个容器: docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES add457f79d57 dev-peer0.org2.example.com-mycc-1.0 "chaincode -peer.a..." 11 minutes ago Up 11 minutes dev-peer0.org2.example.com-mycc-1.0 0c06fb8e8f20 hyperledger/fabric-tools "/bin/bash" 13 minutes ago Up 13 minutes cli 632c3e5d3a5e hyperledger/fabric-peer "peer node start" 13 minutes ago Up 13 minutes 0.0.0.0:7051-7053->7051-7053/tcp peer0.org2.example.com 4.总结 我在Fabric多机部署的过程中还是遇到了不少坑,前前后后花了2天的时间才趟坑完毕,实现了最终的4+1多机部署。其中与单机部署最大的不同的地方就是在单机部署的时候,我们是在同一个docker网络中,所以相互之间通过主机名通讯很容易,而在多机环境中,就需要额外设置DNS或者就是通过extra_hosts参数,设置容器中的hosts文件。而且不能一股脑的就跟cli一样把5台机器的域名IP配置到peer中,那样会报错的,所以只需要设置需要的即可。 官方给的脚本已经替我们做了很多工作,同时也隐藏了很多细节,所以我们并没有真正了解其内部的实现过程,我以后会再写一篇博客详细介绍Fabric多机部署的详细过程。为了方便,我把设计到的几个docker-compose文件打包了一份放出来,如果大家想进行同样的部署,只需要修改一下IP即可复用。 Docker-compose下载 最后,大家如果想进一步探讨Fabric或者使用中遇到什么问题可以加入QQ群【494085548】大家一起讨论。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
回顾一下我之前的一篇博客,在Fabric 1.0中,我们存在3种类型的数据存储,一种是基于文件系统的区块链数据,这个跟比特币很像,比特币也是文件形式存储的。Fabric1.0中的区块链存储了Transaction订单读写集。而读写集到底是读什么?写什么?其实就是我们的State Database,也叫做World State,里面以键值对的方式存储了我们在ChainCode中操作的业务数据。另外还有就是对历史数据和区块链索引的数据库。 区块链是文件系统,这个目前不支持更改,历史数据和区块链的索引是LevelDB,这个也不能更改。而对于State Database,由于和业务相关,所以提供了替换数据库,目前支持默认的LevelDB和用户可选择的CouchDB。这里要说到2点,一个是在0.6的时候其实用的RockDB,但是由于License的考虑,所以在1.0改成了LevelDB。另外就是CouchDB也不一定是最优的,很多人还考虑到MongoDB或者MySQL等,但是由于现在Fabric那边开发资源有限,所以在1.0还不会做,以后可能会实现。 CouchDB安装 下面我们来说一说这个CouchDB。 CouchDB是一个完全局域RESTful API的键值数据库,也就是说我们不需要任何客户端,只需要通过HTTP请求就可以操作数据库了。LevelDB是Peer的本地数据库,那么肯定是和Peer一对一的关系,那么CouchDB是个网络数据库,应该和Peer是什么样一个关系呢?在生产环境中,我们会为每个组织部署节点,而且为了高可用,可能会在一个组织中部署多个Peer。同样我们在一个组织中也部署多个CouchDB,每个Peer对应一个CouchDB。 HyperLedger在Docker Hub上也发布了CouchDB的镜像,为了能够深入研究CouchDB和Fabric的集成,我们就采用官方发布的CouchDB来做。 docker pull klaemo/couchdb 【注意,如果我们是docker pull couchdb,那么只能获得1.6版本的CouchDB,而要获得最新的2.0版,就需要用上面这个镜像。】 可以获得官方的CouchDB镜像。CouchDB在启动的时候需要指定一个本地文件夹映射成CouchDB的数据存储文件夹,所以我们可以在当前用户的目录下创建一个文件夹用于存放数据。 mkdir couchdb 下载完成后,我们只需要执行以下命令即可启用一个CouchDB的实例: docker run -p 5984:5984 -d --name my-couchdb -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v ~/couchdb:/opt/couchdb/data klaemo/couchdb 启动后我们打开浏览器,访问Linux的IP的5984端口的URL,比如我的Linux是192.168.100.129,那么URL是: http://192.168.100.129:5984/_utils 这个时候我们就可以看到CouchDB的Web管理界面了。输入用户名admin密码password即可进入。 现在是一个空数据库,我们将CouchDB和Peer结合起来后再看会是什么样的效果。 配置CouchDB+Fabric环境 先删除刚才创建的CouchDB容器: docker rm -f my-couchdb 首先我们是4个Peer+1Orderer的模式,所以我们先创建4个CouchDB数据库: cd ~ mkdir couchdb0 mkdir couchdb1 mkdir couchdb2 mkdir couchdb3 docker run -p 5984:5984 -d --name couchdb0 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v ~/couchdb0:/opt/couchdb/data klaemo/couchdb docker run -p 6984:5984 -d --name couchdb1 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v ~/couchdb1:/opt/couchdb/data klaemo/couchdb docker run -p 7984:5984 -d --name couchdb2 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v ~/couchdb2:/opt/couchdb/data klaemo/couchdb docker run -p 8984:5984 -d --name couchdb3 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v ~/couchdb3:/opt/couchdb/data klaemo/couchdb 然后我们需要启动Fabric了。Fabric的准备环境,可以参见我们这篇博客:http://www.cnblogs.com/studyzy/p/6973334.html 官方已经提供了多个Docker-compose文件,如果我们使用的是./network_setup.sh up命令,那么启用的就是docker-compose-cli.yaml这个文件。如果要基于这个yaml文件启用CouchDB的Peer,则打开该文件,并编辑其中的Peer节点,改为如下的形式: peer0.org1.example.com: container_name: peer0.org1.example.com environment: - CORE_LEDGER_STATE_STATEDATABASE=CouchDB - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=192.168.100.129:5984 - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=password extends: file: base/docker-compose-base.yaml service: peer0.org1.example.com 这里的192.168.100.129:5984是我映射CouchDB后的Linux的IP地址和IP。然后是设置用户名和密码。把4个Peer的配置都改好后,保存,我们试着启用Fabric: ./network_setup.sh up 等Fabric启动完成并运行了ChainCode测试后,我们刷新http://192.168.100.129:5984/_utils ,可以看到以Channel名字创建的Database,另外还有几个是系统数据库。 点进mychannel数据库,我们可以看到其中的数据内容。点击“Mango Query”可以编写查询,默认提供的查询可以点击Run Query按钮查询所有的数据结果: CouchDB的直接查询 接下来我们使用Linux的curl来查询CouchDB数据库。 比如我们要看看mychannel数据库下有哪些数据: curl http://192.168.100.129:5984/mychannel/_all_docs 可以看到我运行了一些ChainCode后的State DATABASE结果: {"total_rows":7,"offset":0,"rows":[ {"id":"devincc\u0000a","key":"devincc\u0000a","value":{"rev":"2-a979bf6c2716ecae6d106999f833a59c"}}, {"id":"devincc\u0000b","key":"devincc\u0000b","value":{"rev":"2-ad1c549305fd277097180405f96bdcd8"}}, {"id":"lscc\u0000devincc","key":"lscc\u0000devincc","value":{"rev":"1-05d2cd0b344c4dd8a8d1a3ffd7332544"}}, {"id":"lscc\u0000mycc","key":"lscc\u0000mycc","value":{"rev":"1-2cba0344b1610b9d9254bbafbda5e9b1"}}, {"id":"mycc\u0000a","key":"mycc\u0000a","value":{"rev":"2-588a45b289359afa9dc6e5e7866eaf97"}}, {"id":"mycc\u0000b","key":"mycc\u0000b","value":{"rev":"2-54e6639a858b0f91298c9a354484513a"}}, {"id":"statedb_savepoint","key":"statedb_savepoint","value":{"rev":"10-6ccde2a55c71d7d6a70d9333d119fc8e"}} ]} 如果我们要查询其中的一条数据,只需要用/ChannelId/id来查询,比如查询:statedb_savepoint curl http://192.168.100.129:5984/mychannel/statedb_savepoint 返回的结果: {"_id":"statedb_savepoint","_rev":"10-6ccde2a55c71d7d6a70d9333d119fc8e","BlockNum":4,"TxNum":0,"UpdateSeq":"19-g1AAAAEzeJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjMlMiTJ____PyuRAYeCJAUgmWQPVsOCS40DSE08WA0jLjUJIDX1eO3KYwGSDA1ACqhsPiF1CyDq9mclsuJVdwCi7j4h8x5A1AHdx5kFAI6sYwk"} 麻烦的是业务数据是“ChainCodeName\u0000数据”这样的格式的ID,而如果我们要通过这个ID查询,那么就根本找不到啊! curl http://192.168.100.129:5984/mychannel/mycc\u0000a {"error":"not_found","reason":"missing"} 正确的做法是把\u0000替换为%00,也就是说我们的查询应该是: curl http://192.168.100.129:5984/mychannel/mycc%00a 正确返回结果: {"_id":"mycc\u0000a","_rev":"2-588a45b289359afa9dc6e5e7866eaf97","chaincodeid":"mycc","version":"4:0","_attachments":{"valueBytes":{"content_type":"application/octet-stream","revpos":2,"digest":"md5-hhOYXsSeuPdXrmQ56Hm7Kg==","length":2,"stub":true}}} Fabric可能会遇到的问题 虽然区块链是一个只能插入和查询的数据库,但是我们的业务数据是存放在State Database中的,如果我们直接修改了CouchDB的数据,那么接下来的查询和事务是直接基于修改后的CouchDB的,并不会去检查区块链中的记录,所以理论上是可以通过直接改CouchDB来实现业务数据的修改。 我们以官方的Marble为例,看看修改CouchDB会怎么样? 具体操作步骤如下: 1.Install,instantiate和初始化数据: peer chaincode install -n marbles02 -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/marbles02 peer chaincode instantiate -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n marbles02 -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/marbles02 -c '{"Args":["init"]}' -P "OR ('Org1MSP.member','Org2MSP.member')" peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n marbles02 -c '{"Args":["initMarble","marble2","red","50","tom"]}' peer chaincode query -C mychannel -n marbles02 -c '{"Args":["readMarble","marble2"]}' 我们可以看到通过curl直接查询CouchDB中的数据: curl http://192.168.100.129:5984/mychannel/marbles02%00marble2 {"_id":"marbles02\u0000marble2","_rev":"1-a1844f47b9ed94294b430c9a9a6f543b","chaincodeid":"marbles02","data":{"docType":"marble","name":"marble2","color":"red","size":50,"owner":"tom"},"version":"6:0"} 如果我们要修改其中的数据,把颜色改成green,大小改成10,那么我们可以运行: curl -X PUT http://192.168.100.129:5984/mychannel/marbles02%00marble2 -d '{"_id":"marbles02\u0000marble2","_rev":"1-a1844f47b9ed94294b430c9a9a6f543b","chaincodeid":"marbles02","data":{"docType":"marble","name":"marble2","color":"green","size":10,"owner":"tom"},"version":"6:0"}' 系统返回结果: {"ok":true,"id":"marbles02\u0000marble2","rev":"2-6ffc6652cfc707f8352a5f06c3ce1ce6"} 我们在4个CouchDB中都运行这个命令,把4个数据库的数据都改了。 接下来我们通过ChainCode来查询,看看会怎么样。 peer chaincode query -C mychannel -n marbles02 -c '{"Args":["readMarble","marble2"]}' 返回结果: Query Result: {"color":"green","docType":"marble","name":"marble2","owner":"tom","size":10} 可以看到数据已经变成新的值,那么接下来运行其他的Transaction会怎么样?我们试一试转账操作: peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n marbles02 -c '{"Args":["transferMarble","marble2","jerry"]}' 系统返回成功,我们再查一下呢 peer chaincode query -C mychannel -n marbles02 -c '{"Args":["readMarble","marble2"]}' Query Result: {"color":"green","docType":"marble","name":"marble2","owner":"jerry","size":10} 所以我们对CouchDB数据库的更改都是有效的,在Fabric看来似乎并不知道我们改了CouchDB的内容。 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
如果把区块链比作一个只能读写,不能删改的分布式数据库的话,那么事务和查询就是对这个数据库进行的最重要的操作。以比特币来说,我们通过钱包或者Blockchain.info进行区块链的查询操作,而转账行为就是Transaction的处理。而HyperLedger Fabric在1.0对系统架构进行了升级,使得事务的处理更加复杂。 一、架构 让我们来看看Fabric 0.6到1.0的架构图: 这个图来自IBM微课堂第三讲,我们可以看到原来单一的peer节点在1.0中进行了拆分,分为peer(背书节点和提交节点)和orderer(排序节点)。membership也就是我们在1.0中说的CA节点,其中也涉及到很多密码学和安全相关的知识,我们暂且按住不表,只说SDK、Peer和Orderer之间的关系。 二、账本 要了解Fabric对事务的处理,首先我们需要了解Fabric中的账本,也就是实际存储和查询数据的地方。这是IBM微讲堂中对Fabric账本的示意图: Fabric 1.0中的账本分为3种: 区块链数据,这是用文件系统存储在Committer节点上的。区块链中存储了Transaction的读写集。 为了检索区块链的方便,所以用LevelDB对其中的Transaction进行了索引。 ChainCode操作的实际数据存储在State Database中,这是一个Key Value的数据库,默认采用的LevelDB,现在1.0也支持使用CouchDB作为State Database。 三、事务提交过程 了解了Fabric中的账本,接下来我们来了解一下对这些账本的操作涉及到的Transaction。 我们仍然以Example02为例,具体准备过程可参看我之前的博客:http://www.cnblogs.com/studyzy/p/6973334.html 当执行a向b转账10元,我们在cli中执行的命令为: peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n devincc -c '{"Args":["invoke","a","b","10"]}' 当CLI中运行该命令时,发生了什么呢?我们来看看IBM微讲堂中PPT关于事务生命周期和相关账本的示例图: 其中peer chaincode invoke表明这是一个Transaction调用。-c '{"Args":["invoke","a","b","10"]}'中的”invoke”说明调用的是example02.go中的invoke函数,具体函数我们可以看看到底实现了什么功能: // Transaction makes payment of X units from A to B func (t *SimpleChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response { var A, B string // Entities var Aval, Bval int // Asset holdings var X int // Transaction value var err error if len(args) != 3 { return shim.Error("Incorrect number of arguments. Expecting 3") } A = args[0] B = args[1] // Get the state from the ledger // TODO: will be nice to have a GetAllState call to ledger Avalbytes, err := stub.GetState(A) if err != nil { return shim.Error("Failed to get state") } if Avalbytes == nil { return shim.Error("Entity not found") } Aval, _ = strconv.Atoi(string(Avalbytes)) Bvalbytes, err := stub.GetState(B) if err != nil { return shim.Error("Failed to get state") } if Bvalbytes == nil { return shim.Error("Entity not found") } Bval, _ = strconv.Atoi(string(Bvalbytes)) // Perform the execution X, err = strconv.Atoi(args[2]) if err != nil { return shim.Error("Invalid transaction amount, expecting a integer value") } Aval = Aval - X Bval = Bval + X fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval) // Write the state back to the ledger err = stub.PutState(A, []byte(strconv.Itoa(Aval))) if err != nil { return shim.Error(err.Error()) } err = stub.PutState(B, []byte(strconv.Itoa(Bval))) if err != nil { return shim.Error(err.Error()) } return shim.Success(nil) } 其中主要的4个关于StateDatabase调用是: Avalbytes, err := stub.GetState(A) Bvalbytes, err := stub.GetState(B) err = stub.PutState(A, []byte(strconv.Itoa(Aval))) err = stub.PutState(B, []byte(strconv.Itoa(Bval))) 1.客户端SDK把'{"Args":["invoke","a","b","10"]}'这些参数发送到endorser peer节点,2.endorser peer会与ChainCode的docker实例通信,并为其提供模拟的State Database的读写集,也就是说ChainCode会执行完逻辑,但是并不会在stub.PutState的时候写数据库。3.endorser把这些读写集连同签名返回给Client SDK。4.SDK再把读写集发送给Orderer节点,Orderer节点是进行共识的排序节点,在测试的情况下,只启动一个orderer节点,没有容错。在生产环境,要进行Crash容错,需要启用Zookeeper和Kafka。在1.0中移除了拜占庭容错,没有0.6的PBFT,也没有传说中的SBFT,不得不说是一个遗憾。5.Orderer节点只是负责排序和打包工作,处理的结果是一个Batch的Transactions,也就是一个Block,这个Block的产生有两种情况,一种情况是Transaction很多,Block的大小达到了设定的大小,而另一种情况是Transaction很少,没有达到设定的大小,那么Orderer就会等,等到大小足够大或者超时时间。这些设置是在configtx.yaml中设定的。6.打包好的一堆Transactions会发送给Committer Peer提交节点,7.提交节点收到Orderer节点的数据后,会先进行VSCC校验,检查Block的数据是否正确。接下来是对每个Transaction的验证,主要是验证Transaction中的读写数据集是否与State Database的数据版本一致。验证完Block中的所有Transactions后,提交节点会把吧Block写入区块链。然后把所有验证通过的Transaction的读写集中的写的部分写入State Database。另外对于区块链,本身是文件系统,不是数据库,所有也会有把区块中的数据在LevelDB中建立索引。 四、查询 如果我们只是通过ChainCode查询数据,而存在写入数据,那么会有什么区别呢?在CLI中peer命令提供了query子命令,比如Example02中,查询a账户的余额是: peer chaincode query -C mychannel -n devincc -c '{"Args":["query","a"]}' 这样系统会调用ChainCode中的invoke函数,但是传入的function name是query。也就是会执行如下代码: } else if function == "query" { // the old "Query" is now implemtned in invoke return t.query(stub, args) } // query callback representing the query of a chaincode func (t *SimpleChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response { var A string // Entities var err error if len(args) != 1 { return shim.Error("Incorrect number of arguments. Expecting name of the person to query") } A = args[0] // Get the state from the ledger Avalbytes, err := stub.GetState(A) if err != nil { jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}" return shim.Error(jsonResp) } if Avalbytes == nil { jsonResp := "{\"Error\":\"Nil amount for " + A + "\"}" return shim.Error(jsonResp) } jsonResp := "{\"Name\":\"" + A + "\",\"Amount\":\"" + string(Avalbytes) + "\"}" fmt.Printf("Query Response:%s\n", jsonResp) return shim.Success(Avalbytes) } 我们可以看到,我们只是调用了stub.GetState(A),并没有写操作,那么会像前面说的Transaction一样那么复杂吗?答案是不会。 因为调用调用的是peer query,在代码中,只有invoke的时候才会执行Transaction步骤中的4、5、6、7. 但是如果我们使用peer invoke,那么会怎么样呢?比如如下的命令: peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n mycc -c '{"Args":["query","a"]}' 那么从代码上来看,虽然我们是一个查询,却会以Transaction的生命周期来处理。 五、小结 通过对这个Transaction过程的分析,我们可以得出以下结论: Fabric不支持对同一个数据的并发事务处理,也就是说,如果我们同时运行了a->b 10元,b->a 10元,那么只会第一条Transaction成功,而第二条失败。因为在Committer节点进行读写集版本验证的时候,第二条Transaction会验证失败。这是我完全无法接受的一点! Fabric是异步的系统,在Endorser的时候a->b 10元,b->a 10元都会返回给SDK成功,而第二条Transaction在Committer验证失败后不进行State Database的写入,但是并不会通知Client SDK,所以必须使用EventHub通知Client或者Client重新查询才能知道是否写入成功。 不管在提交节点对事务的读写数据版本验证是否通过,因为Block已经在Orderer节点生成了,所以Block是被整块写入区块链的,而在State Database不会写入,所以会在Transaction之外的地方标识该Transaction是无效的。 query没有独立的函数出来,并不是根据只有读集没有写集而判断是query还是Transaction。 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
在接触了比特币和区块链后,我一直有一个想法,就是把所有比特币的区块链数据放入到关系数据库(比如SQL Server)中,然后当成一个数据仓库,做做比特币交易数据的各种分析。想法已经很久了,但是一直没有实施。最近正好有点时间,于是写了一个比特币区块链的导出导入程序。 之前我的一篇博客:在区块链上表白——使用C#将一句话放入比特币的区块链上 介绍了怎么发起一笔比特币的交易,今天我们仍然是使用C#+NBitcoin,读取比特币钱包Bitcoin Core下载到本地的全量区块链数据,并将这些数据写入数据库。如果有和我一样想法的朋友,可以参考下面是我的操作过程: 一、准备 我们要解析的是存储在本地硬盘上的Bitcoin Core钱包的全量比特币数据,那么首先就是要下载并安装好Bitcoin Core,下载地址:https://bitcoin.org/en/download 然后就等着这个软件同步区块链数据吧。目前比特币的区块链数据大概130G,所以可能需要好几天,甚至一个星期才能将所有区块链数据同步到本地。当然如果你很早就安装了这个软件,那么就太好了,毕竟要等好几天甚至一个星期,真的很痛苦。 二、建立比特币区块链数据模型 要进行区块链数据的分析,那么必须得对区块链的数据模型了解才行。我大概研究了一下,可以总结出4个实体:区块、交易、输入、输出。而其中的关系是,一个区块对应多个交易,一个交易对应多个输入和多个输出。除了Coinbase的输入外,一笔输入对应另一笔交易中的输出。于是我们可以得出这样的数据模型: 需要特别说明几点的是: 1.TxId是自增的int,我没有用TxHash做Transaction的PK,那是因为TxHash根本就不唯一啊!有好几个不同区块里面的第一笔交易,也就是Coinbase交易是相同的。这其实应该是异常数据,因为相同的TxHash将导致只能花费一次,所以这个矿工杯具了。 2.对于一笔Coinbase 的Transaction,其输入的PreOutTxId是0000000000000000000000000000000000000000000000000000000000000000,而其PreOutIndex是-1,这是一条不存在的TxOutput,所以我并没有建立TXInput和TxOutput的外键关联。 3.对于Block,PreId就是上一个Block的ID,而创世区块的PreId是0000000000000000000000000000000000000000000000000000000000000000,也是一个不存在的BlockId,所以我没有建立Block的自引用外键。 4.有很多字段其实并不是区块链数据结构中的,这些字段是我添加为了接下来方便分析用的。在导入的时候并没有值,需要经过一定的SQL运算才能得到。比如Trans里面的TotalInAmount,TransFee等。 我用的是PowerDesigner,建模完成后,生成SQL语句,即可。这是我的建表SQL: create table Block ( Height int not null, BlkId char(64) not null, TxCount int not null, Size int not null, PreId char(64) not null, Timestamp datetime not null, Nonce bigint not null, Difficulty double precision not null, Bits char(64) not null, Version int not null, TxMerkleRoot char(64) not null, constraint PK_BLOCK primary key nonclustered (BlkId) ) go /*==============================================================*/ /* Index: Block_Height */ /*==============================================================*/ create unique clustered index Block_Height on Block ( Height ASC ) go /*==============================================================*/ /* Table: Trans */ /*==============================================================*/ create table Trans ( TxId int not null, BlkId char(64) not null, TxHash char(64) not null, Version int not null, InputCount int not null, OutputCount int not null, TotalOutAmount bigint not null, TotalInAmount bigint not null, TransFee bigint not null, IsCoinbase bit not null, IsHeightLock bit not null, IsTimeLock bit not null, LockTimeValue int not null, Size int not null, TransTime datetime not null, constraint PK_TRANS primary key (TxId) ) go /*==============================================================*/ /* Index: Relationship_1_FK */ /*==============================================================*/ create index Relationship_1_FK on Trans ( BlkId ASC ) go /*==============================================================*/ /* Index: Trans_Hash */ /*==============================================================*/ create index Trans_Hash on Trans ( TxHash ASC ) go /*==============================================================*/ /* Table: TxInput */ /*==============================================================*/ create table TxInput ( TxId int not null, Idx int not null, Amount bigint not null, PrevOutTxId char(64) not null, PrevOutIndex int not null, PaymentScriptLen int not null, PaymentScript varchar(8000) not null, Address char(58) null, constraint PK_TXINPUT primary key (TxId, Idx) ) go /*==============================================================*/ /* Index: Relationship_2_FK */ /*==============================================================*/ create index Relationship_2_FK on TxInput ( TxId ASC ) go /*==============================================================*/ /* Table: TxOutput */ /*==============================================================*/ create table TxOutput ( TxId int not null, Idx int not null, Amount bigint not null, ScriptPubKeyLen int not null, ScriptPubKey varchar(8000) not null, Address char(58) null, IsUnspendable bit not null, IsPayToScriptHash bit not null, IsValid bit not null, IsSpent bit not null, constraint PK_TXOUTPUT primary key (TxId, Idx) ) go /*==============================================================*/ /* Index: Relationship_3_FK */ /*==============================================================*/ create index Relationship_3_FK on TxOutput ( TxId ASC ) go alter table Trans add constraint FK_TRANS_RELATIONS_BLOCK foreign key (BlkId) references Block (BlkId) go alter table TxInput add constraint FK_TXINPUT_RELATIONS_TRANS foreign key (TxId) references Trans (TxId) go alter table TxOutput add constraint FK_TXOUTPUT_RELATIONS_TRANS foreign key (TxId) references Trans (TxId) go View Code 三、导出区块链数据为CSV 数据模型有了,接下来我们就是建立对应的表,然后写程序将比特币的Block写入到数据库中。我本来用的是EntityFramework来实现插入数据库的操作。但是后来发现实在太慢,插入一个Block甚至要等10多20秒,这要等到何年何月才能插入完啊!我试了各种方案,比如写原生的SQL,用事务,用LINQToSQL等,性能都很不理想。最后终于找到了一个好办法,那就是直接导出为文本文件(比如CSV格式),然后用SQL Server的Bulk Insert命令来实现批量导入,这是我已知的最快的写入数据库的方法。 解析Bitcoin Core下载下来的所有比特币区块链数据用的还是NBitcoin这个开源库。只需要用到其中的BlockStore 类,即可轻松实现区块链数据的解析。 以下是我将区块链数据解析为我们的Block对象的代码: private static void LoadBlock2DB(string localPath, int start) { var store = new BlockStore(localPath, Network.Main); int i = -1; BlockToCsvHelper helper = new BlockToCsvHelper(height); foreach (var block in store.Enumerate(false)) { i++; if (i < start) { continue; } try { log.Debug("Start load Block " + i + ": " + block.Item.Header + " from file:" + block.BlockPosition.ToString()); var blk = LoadBlock(block, i);//将NBitcoin的Block转换为我们建模的Block对象 helper.WriteBitcoin2Csv(blk);//将我们的Block对象转换为CSV保存 } catch (Exception ex) { log.Error("保存Block到数据库时异常,请手动载入,i=" + i, ex); } } Console.WriteLine("--------End-----------"); Console.ReadLine(); } private static Block LoadBlock(StoredBlock block, int i) { var blk = new Block() { BlkId = block.Item.Header.ToString(), Difficulty = block.Item.Header.Bits.Difficulty, Bits = block.Item.Header.Bits.ToString(), Height = i, Nonce = block.Item.Header.Nonce, PreId = block.Item.Header.HashPrevBlock.ToString(), TxMerkleRoot = block.Item.GetMerkleRoot().ToString(), Size = block.Item.GetSerializedSize(), Version = block.Item.Header.Version, Timestamp = block.Item.Header.BlockTime.UtcDateTime, TxCount = block.Item.Transactions.Count }; log.Debug("Transaction Count=" + block.Item.Transactions.Count); foreach (var transaction in block.Item.Transactions) { var tx = new Trans() { BlkId = blk.BlkId, TxHash = transaction.GetHash().ToString(), Version = (int)transaction.Version, InputCount = transaction.Inputs.Count, OutputCount = transaction.Outputs.Count, TotalOutAmount = transaction.TotalOut.Satoshi, TransTime = blk.Timestamp, IsCoinbase = transaction.IsCoinBase, IsHeightLock = transaction.LockTime.IsHeightLock, IsTimeLock = transaction.LockTime.IsTimeLock, LockTimeValue = (int)transaction.LockTime.Value, Size = transaction.GetSerializedSize() }; blk.Trans.Add(tx); for (var idx = 0; idx < transaction.Inputs.Count; idx++) { var input = transaction.Inputs[idx]; var txInput = new TxInput() { PaymentScript = input.ScriptSig.ToString(), PaymentScriptLen = input.ScriptSig.Length, PrevOutTxId = input.PrevOut.Hash.ToString(), PrevOutIndex = (int)input.PrevOut.N, Trans = tx, Idx = idx }; if (!tx.IsCoinbase) { var addr = input.ScriptSig.GetSignerAddress(Network.Main); if (addr != null) { txInput.Address = addr.ToString(); } } if (txInput.PaymentScript.Length > 8000) { log.Error("Transaction Input PaymentScript异常,将被截断,TxHash: " + tx.TxHash); txInput.PaymentScript = txInput.PaymentScript.Substring(0, 7999); } tx.TxInput.Add(txInput); } for (var idx = 0; idx < transaction.Outputs.Count; idx++) { var output = transaction.Outputs[idx]; var txOutput = new TxOutput() { Amount = output.Value.Satoshi, ScriptPubKey = output.ScriptPubKey.ToString(), ScriptPubKeyLen = output.ScriptPubKey.Length, Trans = tx, IsUnspendable = output.ScriptPubKey.IsUnspendable, IsPayToScriptHash = output.ScriptPubKey.IsPayToScriptHash, IsValid = output.ScriptPubKey.IsValid, Idx = idx }; if (txOutput.ScriptPubKey.Length > 8000) { log.Error("Transaction Output ScriptPubKey异常,将被截断,TxHash: " + tx.TxHash); txOutput.ScriptPubKey = txOutput.ScriptPubKey.Substring(0, 7999); } if (!output.ScriptPubKey.IsUnspendable) { if (output.ScriptPubKey.IsPayToScriptHash) { txOutput.Address = output.ScriptPubKey.GetScriptAddress(Network.Main).ToString(); } else { var addr = output.ScriptPubKey.GetDestinationAddress(Network.Main); if (addr == null) { var keys = output.ScriptPubKey.GetDestinationPublicKeys(); if (keys.Length == 0) { //异常 log.Warn("Transaction Output异常,TxHash: " + tx.TxHash); } else { addr = keys[0].GetAddress(Network.Main); } } if (addr != null) { txOutput.Address = addr.ToString(); } } } tx.TxOutput.Add(txOutput); } } return blk; } View Code 至于WriteBitcoin2Csv方法,就是以一定的格式,把Block、Trans、TxInput、TxOutput这4个对象分别写入4个文本文件中即可。 四、将CSV导入SQL Server 在完成了CSV文件的导出后,接下来就是怎么将CSV文件导入到SQL Server中。这个很简单,只需要执行BULK INSERT命令。比如这是我在测试的时候用到的SQL语句: bulk insert [Block] from 'F:\temp\blk205867.csv'; bulk insert Trans from 'F:\temp\trans205867.csv'; bulk insert TxInput from 'F:\temp\input205867.csv'; bulk insert TxOutput from 'F:\temp\output205867.csv'; 当然在实际的情况中,我并不是这么做的。我是每1000个Block就生成4个csv文件,然后使用C#连接到数据库,执行bulk insert命令。执行完成后再把这生成的4个csv文件删除,然后再循环继续导出下一批1000个Block。因为比特币的区块链数据实在太大了,如果我不分批,那么我的PC机硬盘就不够用了,而且在导入SQL Server的时候我也怀疑能不能导入那么大批量的数据。 最后,附上一张我正在导入中的进程图,已经导了一天了,还没有完成,估计还得再花一、两天时间吧。 所有区块链数据都进入数据库以后,就要发挥一下我的想象力,看能够分析出什么有意思的结果了。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
【更新:1.0Beta已经是过去式了,现在出了1.0.0的正式版,请大家参照 http://www.cnblogs.com/studyzy/p/7437157.html 安装Fabric 1.0.0】 今天HyperLedger Fabric放出了1.0 Beta版的镜像,按照命名上来说,这应该是一个基本可用的版本了,所以我赶紧第一时间下载下来,把玩把玩。以下是在Ubuntu中安装并测试Fabric 1.0 Beta的步骤: 一、环境准备 1.1 安装VirtualBox并在其中安装好Ubuntu 这一步其实没啥好说的,下载好最新版的VirtualBox,下载Ubuntu Server,我用的是16.10 X64。在安装完Ubuntu后,需要保证apt source是国内的,不然如果是国外的话会很慢很慢的。具体做法是 sudo vi /etc/apt/sources.list 打开这个apt源列表,如果其中看到是http://us.xxxxxx之类的,那么就是外国的,如果看到是http://cn.xxxxx之类的,那么就不用换的。我的是美国的源,所以需要做一下批量的替换。在命令模式下,输入: :%s/us./cn./g 就可以把所有的us.改为cn.了。然后输入:wq即可保存退出。 sudo apt-get update 更新一下源。 然后安装ssh,这样接下来就可以用putty或者SecureCRT之类的客户端远程连接Ubuntu了。 sudo apt-get install ssh 1.2 安装Docker 安装Docker也会遇到外国网络慢的问题,幸好国内有很好的镜像,推荐DaoClound,安装Docker的命令是: curl -sSL https://get.daocloud.io/docker | sh 安装完成后,运行以下脚本将当前用户添加到Docker的组中 sudo usermod -aG docker studyzy 重新登录当前用户,接下来修改 Docker 服务配置(/etc/default/docker 文件)。 sudo vi /etc/default/docker 添加以下内容: DOCKER_OPTS="$DOCKER_OPTS -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --api-cors-header='*'" 接下来就需要设置国内的Docker镜像地址,需要注册一个账号,然后在加速器页面提供了设置Docker镜像的脚本,加速器页面是: https://www.daocloud.io/mirror 我提供的脚本是: curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://d4cc5789.m.daocloud.io 运行完脚本后,重启Docker服务 sudo service docker restart 1.3 安装docker-compose Docker-compose是支持通过模板脚本批量创建Docker容器的一个组件。在安装Docker-Compose之前,需要安装Python-pip,运行脚本: sudo apt-get install python-pip 安装完成后,接下来从DaoClound安装Docker-compose,运行脚本: curl -L https://get.daocloud.io/docker/compose/releases/download/1.10.1/docker-compose-`uname -s`-`uname -m` > ~/docker-compose sudo mv ~/docker-compose /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose 二、部署Fabric 1.0 Beta 2.1下载官方自动化部署脚本 我们首先创建一个文件夹,用于存放自动化部署的脚本。 mkdir fabric-sample cd fabric-sample 然后就可以使用curl命令下载并运行自动化部署脚本了:1.0 beta的命令是: curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/master/scripts/bootstrap-1.0.0-beta.sh | bash 【2017/6/24更新: 1.0 rc1 那么获取的命令是:】 curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/master/scripts/bootstrap-1.0.0-rc1.sh | bash 这个过程会比较漫长,会下载大量的x86_64-1.0.0-beta或者x86_64-1.0.0-rc1的docker image。下载完所有镜像后会再做一次rename,把x86_64-1.0.0-beta改为latest,这样才方便使用。 当所有下载完毕后,我们运行 docker images 可以看到有如下的镜像: REPOSITORY TAG IMAGE ID CREATED SIZE hyperledger/fabric-tools latest ae6b0f53cb70 14 hours ago 1.32 GB hyperledger/fabric-tools x86_64-1.0.0-beta ae6b0f53cb70 14 hours ago 1.32 GB hyperledger/fabric-couchdb latest 31bbbec3d853 14 hours ago 1.48 GB hyperledger/fabric-couchdb x86_64-1.0.0-beta 31bbbec3d853 14 hours ago 1.48 GB hyperledger/fabric-kafka latest c4ac1c9a4797 14 hours ago 1.3 GB hyperledger/fabric-kafka x86_64-1.0.0-beta c4ac1c9a4797 14 hours ago 1.3 GB hyperledger/fabric-zookeeper latest 2c4ebacb6f00 14 hours ago 1.31 GB hyperledger/fabric-zookeeper x86_64-1.0.0-beta 2c4ebacb6f00 14 hours ago 1.31 GB hyperledger/fabric-orderer latest 11ff350dd297 14 hours ago 179 MB hyperledger/fabric-orderer x86_64-1.0.0-beta 11ff350dd297 14 hours ago 179 MB hyperledger/fabric-peer latest e01c2b645f11 14 hours ago 182 MB hyperledger/fabric-peer x86_64-1.0.0-beta e01c2b645f11 14 hours ago 182 MB hyperledger/fabric-javaenv latest 61c188dca542 14 hours ago 1.42 GB hyperledger/fabric-javaenv x86_64-1.0.0-beta 61c188dca542 14 hours ago 1.42 GB hyperledger/fabric-ccenv latest 7034cca1918d 14 hours ago 1.29 GB hyperledger/fabric-ccenv x86_64-1.0.0-beta 7034cca1918d 14 hours ago 1.29 GB hyperledger/fabric-ca latest e549e8c53c2e 15 hours ago 238 MB hyperledger/fabric-ca x86_64-1.0.0-beta e549e8c53c2e 15 hours ago 238 MB 2.2启动Fabric实例 在前面下载的官方提供的自动化部署脚本中,已经包含了启动Fabric实例的脚本。直接运行: cd ~/fabric-sample/release/linux-amd64 ./network_setup.sh up 系统运行完毕后会看到这样的界面: 系统就会创建1个客户端实例cli,1个orderer节点,还有4个peer节点。另外,当前的脚本包含了我们接下来要测试的mycc的实例,所以可能还会看到3个链上代码的实例在运行。 这是命令运行完毕后,使用docker ps命令看到的实例: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0bfa0ff5e77c dev-peer1.org2.example.com-mycc-1.0 "chaincode -peer.a..." 3 minutes ago Up 3 minutes dev-peer1.org2.example.com-mycc-1.0 05751dedc36a dev-peer0.org1.example.com-mycc-1.0 "chaincode -peer.a..." 4 minutes ago Up 4 minutes dev-peer0.org1.example.com-mycc-1.0 7006bcd8e671 dev-peer0.org2.example.com-mycc-1.0 "chaincode -peer.a..." 4 minutes ago Up 4 minutes dev-peer0.org2.example.com-mycc-1.0 fd52ef8e4be8 hyperledger/fabric-tools "/bin/bash -c './s..." 5 minutes ago Up 5 minutes cli 11e34078645f hyperledger/fabric-peer "peer node start" 5 minutes ago Up 5 minutes 0.0.0.0:10051->7051/tcp, 0.0.0.0:10053->7053/tcp peer1.org2.example.com af042ab813ed hyperledger/fabric-peer "peer node start" 5 minutes ago Up 5 minutes 0.0.0.0:8051->7051/tcp, 0.0.0.0:8053->7053/tcp peer1.org1.example.com 08723b2ec1ec hyperledger/fabric-peer "peer node start" 5 minutes ago Up 5 minutes 0.0.0.0:7051->7051/tcp, 0.0.0.0:7053->7053/tcp peer0.org1.example.com e84bc309e09e hyperledger/fabric-orderer "orderer" 5 minutes ago Up 5 minutes 0.0.0.0:7050->7050/tcp orderer.example.com 3ec6e7cf006b hyperledger/fabric-peer "peer node start" 5 minutes ago Up 5 minutes 0.0.0.0:9051->7051/tcp, 0.0.0.0:9053->7053/tcp peer0.org2.example.com 三、测试Fabric 其实我们在前面运行./network_setup.sh up的时候系统已经运行了一个Example02的ChainCode测试,部署上去的ChainCodeName是mycc,所以接下来我们要测试的话不能再初始化并部署同样名字的ChainCode了,我们可以使用自己另外命名的名字,比如devincc。 3.1在CLI中测试Example02 首先我们需要登录到CLI这个容器中,才能执行Fabric的CLI命令。 docker exec -it cli bash 如果成功进入,我们会切换到该容器的root用户下,得到如下的命令行目录: root@12f2eb6d9fa6:/opt/gopath/src/github.com/hyperledger/fabric/peer# 与0.6Fabric不同的是,在1.0中,链上代码是需要经过Install和Instantiate两步的。下面我们首先安装Example02,并指定一个名字,比如我们这里就用devincc: peer chaincode install -n devincc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 运行后可以看到提示运行成功,返回200状态: 接下来是Instantiate,也就是初始化实例,设置a账户有100元,b账户有200元。 peer chaincode instantiate -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n devincc -v 1.0 -c '{"Args":["init","a", "100", "b","200"]}' -P "OR ('Org1MSP.member','Org2MSP.member')" 运行成功后可以看到如下的结果: 接下来我们用Query命令来看一看a账户的余额: peer chaincode query -C mychannel -n devincc -c '{"Args":["query","a"]}' 返回的结果是: 好接下来我们需要把a账户的10元转给b账户,需要调用invoke命令: peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n devincc -c '{"Args":["invoke","a","b","10"]}' 运行返回的结果为: 最后我们再调用query命令来查一下b账户的余额,如果没有计算错,应该是210元。 peer chaincode query -C mychannel -n devincc -c '{"Args":["query","b"]}' 看来我们的Fabric 1.0 Beta已经部署成功并测试通过了。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
最近在研究Go,打算基于Go做点Web API,于是经过初步调研,打算用Beego这个框架,然后再结合其中提供的ORM以及Swagger的集成,可以快速搭建一个RESTful API的网站。 下面是具体做法: 1. 在Ubuntu中安装Go 1.8 默认Ubuntu apt-get提供的是Go 1.6,而我们要用最新的Go 1.8需要执行以下操作: 1.1 添加apt-get源并刷新 $ sudo add-apt-repository ppa:gophers/archive $ sudo apt-get update 1.2 安装Go 1.8 $ sudo apt-get install golang-1.8 1.3 设置环境变量 等安装完毕后,Go会被安装到/usr/lib/go-1.8目录。我们要执行go命令和建立自己项目的话,需要增加一些环境变量。 我们以后代码要放在当前用户下的Go目录下,需要先创建2个目录: $ mkdir -p ~/go/bin $ mkdir -p ~/go/src 然后设置当前用户的环境变量: vi ~/.profile 在结尾增加以下内容: export GOROOT=/usr/lib/go-1.8 export PATH="$PATH:$GOROOT/bin" export GOPATH=$HOME/go export PATH="$PATH:$GOPATH/bin" 保存后,重新刷新环境变量 source ~/.profile 接下来我们验证一下我们的Go版本,输入 go version 我当前返回的是go version go1.8.1 linux/amd64说明我们的Go 1.8已经安装成功 2. 下载Beego、Bee工具和MySQL驱动 Beego是一个非常适合Go初学者的Web框架,提供了很多的功能,有些人说他臃肿,不过对于我这个Go初学者来说,不在乎是否臃肿,而在乎是否快速解决问题,是否简单。下面我们来安装Beego,这个很简单,只需要执行以下命令: $ go get -u github.com/astaxie/beego $ go get -u github.com/beego/bee 其中beego是框架的源代码,而bee是一个快速创建运行Beego项目的工具。 我们的目标是要实现ORMapping,那么连接数据库是必不可少的,需要另外下载Go版的MySQL驱动: $ go get github.com/go-sql-driver/mysql 这些通过go get下载下来的文件都在~/go/src中,而bee工具是在~/go/bin中。 3. 创建api项目并运行 直接使用bee工具创建一个简单的RESTful API项目是个不二的选择,假设我们的项目名字叫testApi,那么只需要执行: bee api testApi 那么程序就会创建对应的文件在目录~/go/src/testApi 接下来我们需要运行这个项目。首先切换到到项目文件夹,然后运行bee run命令: cd ~/go/src/testApi bee run -gendoc=true -downdoc=true 这个时候我们可以看到系统已经运行在8080端口,我们切换到浏览器,访问这个网站的Swagger地址: http://192.168.100.129:8080/swagger/ 就可以看到我们熟悉的Swagger界面了: 4. 修改代码,实现ORMapping 如果我们来到testApi项目文件夹,会看到类似MVC的结构,不过由于Web API不需要真正的View, 所有view文件夹被Swagger替换。下面我们要新建一个Student对象,并实现对Student增删改查的Web API。 4.1 新建Student model和对应的表 我们可以先在MySQL中创建Student表: CREATE TABLE `student` ( `Id` int(11) NOT NULL, `Name` varchar(10), `Birthdate` date , `Gender` tinyint(1) , `Score` int(11), PRIMARY KEY (`Id`) ) 然后在model文件夹下新建Student.go文件,增加Student对象: type Student struct { Id int Name string Birthdate string Gender bool Score int } 4.2初始化ORM模块 我们要通过ORM来操作对象和数据库,但是ORM需要初始化才能使用,我们需要在main.go文件中增加以下内容: import ( "github.com/astaxie/beego/orm" _ "github.com/go-sql-driver/mysql" ) func init() { orm.RegisterDriver("mysql", orm.DRMySQL) orm.RegisterDataBase("default", "mysql", "zengyi:123@tcp(127.0.0.1:3306)/testdb?charset=utf8") } 这里需要注意的是数据库连接字符串和普通的写法不一样,要写成如下格式: 用户名:密码@tcp(MySQL服务器地址:端口)/数据库名字?charset=utf8 4.3 提供数据库查询Student的方法 接下来就是数据库访问方法了。我们可以仿照user.go一样,把方法都写在Student.go文件里面。这是完整的Student.go文件: package models import ( "github.com/astaxie/beego/orm" "fmt" "time" ) type Student struct { Id int Name string Birthdate string Gender bool Score int } func GetAllStudents() []*Student { o := orm.NewOrm() o.Using("default") var students []*Student q:= o.QueryTable("student") q.All(&students) return students } func GetStudentById(id int) Student{ u:=Student{Id:id} o := orm.NewOrm() o.Using("default") err := o.Read(&u) if err == orm.ErrNoRows { fmt.Println("查询不到") } else if err == orm.ErrMissPK { fmt.Println("找不到主键") } return u } func AddStudent(student *Student) int{ o := orm.NewOrm() o.Using("default") o.Insert(student) return student.Id } func UpdateStudent(student *Student) { o := orm.NewOrm() o.Using("default") o.Update(student) } func DeleteStudent(id int){ o := orm.NewOrm() o.Using("default") o.Delete(&Student{Id:id}) } func init() { // 需要在init中注册定义的model orm.RegisterModel(new(Student)) } 4.4 创建StudentController对外提供Student的增加、删除、修改、查询一个、查询所有的方法 这里我们也可以仿照usercontroller,直接改写成我们需要的StudentController.go。这是内容: package controllers import "github.com/astaxie/beego" import ( "testApi/models" "encoding/json" ) type StudentController struct { beego.Controller } // @Title 获得所有学生 // @Description 返回所有的学生数据 // @Success 200 {object} models.Student // @router / [get] func (u *StudentController) GetAll() { ss := models.GetAllStudents() u.Data["json"] = ss u.ServeJSON() } // @Title 获得一个学生 // @Description 返回某学生数据 // @Param id path int true "The key for staticblock" // @Success 200 {object} models.Student // @router /:id [get] func (u *StudentController) GetById() { id ,_:= u.GetInt(":id") s := models.GetStudentById(id) u.Data["json"] = s u.ServeJSON() } // @Title 创建用户 // @Description 创建用户的描述 // @Param body body models.Student true "body for user content" // @Success 200 {int} models.Student.Id // @Failure 403 body is empty // @router / [post] func (u *StudentController) Post() { var s models.Student json.Unmarshal(u.Ctx.Input.RequestBody, &s) uid := models.AddStudent(&s) u.Data["json"] = uid u.ServeJSON() } // @Title 修改用户 // @Description 修改用户的内容 // @Param body body models.Student true "body for user content" // @Success 200 {int} models.Student // @Failure 403 body is empty // @router / [put] func (u *StudentController) Update() { var s models.Student json.Unmarshal(u.Ctx.Input.RequestBody, &s) models.UpdateStudent(&s) u.Data["json"] = s u.ServeJSON() } // @Title 删除一个学生 // @Description 删除某学生数据 // @Param id path int true "The key for staticblock" // @Success 200 {object} models.Student // @router /:id [delete] func (u *StudentController) Delete() { id ,_:= u.GetInt(":id") models.DeleteStudent(id) u.Data["json"] = true u.ServeJSON() } 这里需要注意的是,函数上面的注释是很重要的,有一定的格式要求,Swagger就是根据这些注释来展示的,所以必须写正确。 4.5 将StudentController注册进路由 现在大部分工作已经完成,我们只需要把新的StudentController注册进路由即可,打开router.go,增加以下内容: beego.NSNamespace("/student", beego.NSInclude( &controllers.StudentController{}, ), ), 当然对于系统默认的user和object,如果我们不需要,可以注释掉。 4.6 运行并通过Swagger测试 我们的编码已经完成。接下来使用bee命令来运行我们的项目: bee run -gendoc=true -downdoc=true 我们就可以看到我们新的student Controller了。并且可以通过调用API来完成对student表的CRUD操作。 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
距离上一次大版本的发布已经很久很久了,中间是不是会收到一些用户的来信,提出新的需求,于是只是做小版本的更新,终于积累了一些更新后,打算做个大版本的发布了。 深蓝词库转换是一个输入法的词库互转和生成软件,支持市面上主流的各种输入法软件的词库(加密的除外)和各种输入法(拼音、五笔、二笔,甚至台湾的注音、仓颉等),除了汉语还支持英文词库的导入导出以及根据词典生成英文输入法词库。 说回这次新版本的发布,除了修复之前版本中的Bug外,主要是实现了以下新功能: 1.支持手心输入法 手心输入法是最近几年横空出世的一款输入法,在360上的推广比较猛,现在已经更新到2.7版了,很幸运的是这个输入法支持文本格式的词库导入导出,所以我们只需要简单处理就可以实现对该输入法词库的支持。 比如我们要将搜狗细胞词库中的某个词库导入到手心输入法,可以进行以下操作: 1.下载scel细胞词库到本地硬盘。 2.打开深蓝词库转换,选择该细胞词库,源选择“搜狗细胞词库scel”,目标选择“手心输入法”,然后点击转换按钮: 3.转换完成后把转换后的词库保存到本地硬盘。 4.打开手心输入法的设置界面,选择“词库”选项卡,点击“导入”按钮,即可把刚才转换后的词库导入到手心输入法中。 2.支持Win10微软拼音输入法 Win10自带的微软拼音输入法本身并不兼容之前的微软拼音输入法词库,而且也不支持文本文件词库的导入导出,而是以某种自定义的二进制格式在“用户自定义短语”中进行导入导出。由于是二进制,所以分析起来比较困难,前期我一直没有解决这个问题,这里再次感谢一下hhggit,他解析了微软拼音的二进制格式,并告诉了我。 如果要将某个搜狗细胞词库导入到Win10微软拼音输入法中,可以进行如下操作: 1.到搜狗输入法官网下载我们需要的细胞词库。 2.打开深蓝词库转换,选择我们要转换的细胞词库,源选择“搜狗细胞词库scel”,目标选择“Win10微软拼音”,点击“转换“按钮,系统就会在本地文件夹创建一个叫Win10微软拼音词库.txt的二进制文件。 3.打开Win10微软拼音的选项卡,找到用户自定义短语这个选项 4.进入这个选项后,可以看到我们已经定义好的自定义短语。 5.点击“导入”按钮,选择我们刚才生成的词库文件,系统提示导入成功,并将词库内容显示在下面。 如果我们之前添加了很多自定义的短语,现在想导出成其他输入法的词库,那么也是类似的操作: 1.在Win10微软拼音的设置界面,点击用户自定义短语的“导出”按钮,把词库文件保存到本地。 2.打开深蓝词库转换,选择刚才导出的词库文件,源选为“Win10微软拼音”,目标选为其他词库格式,比如QQ拼音,点击“转换”按钮 3.将导出的词库内容保存到本地,接下来就可以去其他输入法导入我们刚才生成的词库了。 需要说一下就是据用户反应,Win10微软拼音输入法的用户自定义短语是有词条数限制的,对于太多的词条,虽然提示导入成功,但是并不会真的全部导入进去,具体限制是多少条,我也没研究过。 希望最新的2.2版深蓝词库转换能够为用户带来更多的便利。下载地址在: https://github.com/studyzy/imewlconverter/releases/download/V2.2/Release_V2.2.zip【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
最近在看区块链和比特币的知识,顺便简单研究了一下BitCoin的脚本语言,发现OP_RETURN这个命令可以在后面放入自己想说的内容,很多侧链啊,公证之类就是利用了这个特性,可以把一句话,或者一个哈希值放在这个命令后面,于是我也想试一试,看看能不能成功。 由于本人对C#很熟悉,所以采用的是网上的.Net BitCoin的开源库NBitcoin。下面是实现过程。 1. 去买一定量的比特币。 这个不用多说了,到火币网,OKCoin等比特币交易所都可以购买,因为是实验的需要,所以并不需要买很多,几元~十几元人民币对应的比特币即可,多买点也行,以后留着说不定还能升值。 2. 下载并安装Bitcoin Core,然后把交易网站购买的比特币提现到本地的比特币钱包中。 接下来我们需要安装比特币官方的钱包,BitCoin Core,官网下载地址是:https://bitcoin.org/en/download 最好是选择Windows安装版,下载并安装好后,打开bitcoin-qt.exe,系统会给我们创建一个新的比特币接收地址,当然,我们也可以自己创建新的比特币接收地址。在“文件”-》“正在接收地址”下,可以看到当前钱包的接收地址。 在比特币交易网站,选择比特币提现,输入我们本地钱包的收款地址,就可以把网站上的比特币提现到本地钱包。需要注意的是,比特币的每一笔交易是要出交易费的,而交易费的多少就决定了转账到本地的快慢。以我之前提现的经验,我设置的交易费是0.0001比特币,大概也就是人民币8毛钱,这种情况下需要2天才到账,所以大家想快点到账,就得提高点手续费了。 3. 在Bitcoin Core中,导出私钥。 现在比特币已经在我们本地钱包的账户下了,接下来就需要导出本地钱包收款地址对应的私钥。具体做法是打开“帮助”-》“调试窗口”,在控制台的选项卡下,输入: walletpassphrase 本地钱包密码 600 这个命令是解锁钱包,以便于接下来导出密钥。 以我的这个收款地址“1DobCXYvc4xVSmdPdnZ6xUPGwetaSCma5C”为例,我们再运行以下命令,BitCoin Core就会输出该地址对应的密钥了: dumpprivkey 1DobCXYvc4xVSmdPdnZ6xUPGwetaSCma5C 把这个密钥字符串保存下来,有这个密钥,就能花费该收款地址中的比特币,所以千万不要告诉别人!我们接下来通过程序创建一笔交易的时候就会用到该密钥。 4. 在VS中新建一个命令行程序,添加NBitcoin的引用。 在.Net环境下,最好的比特币开发库是NBitcoin,我们要新建一笔交易,包含我们要在放区块链上的一句话,就可以用这个库轻松完成。 新建VS下的控制台应用程序,使用nuget添加NBitcoin的引用。 5. 找到上一次提现到比特币钱包的TransactionID,这就是我们要创建一笔新交易的比特币的输入。 回到比特币钱包BitCoin Core,在主界面的“交易记录”选项卡中,可以看到之前提现比特币的交易记录。 把这个Transaction ID复制下来,我们到网上查询这个Transaction的具体情况: https://blockchain.info/tx/0327f4669b3eea71ef351c8d89877b037fa1a270095426877d7961a8a4de5892 我们可以看到在这边交易中,有2个Output,其中我们的地址1DobCXYvc4xVSmdPdnZ6xUPGwetaSCma5C是第一个,也就是Index为0。 接下来在C#中新建一个Transaction,把这个交易作为新建交易的输入: var blockr = new BlockrTransactionRepository(); Transaction inputTran = blockr.Get("0327f4669b3eea71ef351c8d89877b037fa1a270095426877d7961a8a4de5892");//0.00052 Transaction payment = new Transaction(); payment.Inputs.Add(new TxIn() { PrevOut = new OutPoint(inputTran.GetHash(), 0) //前面通过网站查询,我们的Index是0 }); 6. 在比特币钱包中新建一个收款地址,作为我们这边交易的比特币接收方。 重新回到比特币钱包BitCoin Core,在“文件”-》“正在接收地址”中,我们可以新建一个比特币收款地址,把这个地址作为我们交易的输出。这里,我采用的地址是:18fNiqtV1gQPF9A5BwGis6VfX66R5Tjq7p 于是我们对应的C#语句是: BitcoinAddress receiveAddress = new BitcoinPubKeyAddress("18fNiqtV1gQPF9A5BwGis6VfX66R5Tjq7p", Network.Main); payment.Outputs.Add(new TxOut() { Value = Money.Coins(0.0004198m), ScriptPubKey = receiveAddress.ScriptPubKey }); 这里我需要说明一下我的比特币流转的安排,我的输入比特币是0.00052比特币,我打算把其中的0.0004198转移到新建的地址中,接下来还有另一个Output,就是放一句话的地方,我打算意思性的放0.0000001,而Input和Output的差额0.0001001作为手续费! 7. 编码一句话,并放在OP_RETURN后面,将这个脚本也作为另一个输出。 最重要的地方到了,我们需要放入我们自定义的内容(一句话,一个Hash值都行),根据网上的文档(https://en.bitcoin.it/wiki/OP_RETURN),后面可以跟80字节的内容,也就是说如果是汉字的话,可以放40个了!虽然没有微博的140字那么长,但是应该也够我们用了吧! 这是我们放0.0000001比特币在这上面,其实放0也是可以的!因为用英文更容易被国外的网站Decode,所以我建议采用ASCII编码英文。 string text = "Input what you want to say!"; var bytes = Encoding.ASCII.GetBytes(text); payment.Outputs.Add(new TxOut() { Value = Money.Coins(0.0000001m), ScriptPubKey = TxNullDataTemplate.Instance.GenerateScriptPubKey(bytes) }); 8. 使用前面步骤3导出的私钥,对这笔交易签名。 现在我们整个payment对象已经有了输入和输出,剩下的就是对输入进行签名,也就是说证明我对1DobCXYvc4xVSmdPdnZ6xUPGwetaSCma5C这个地址上的比特币有使用权。 BitcoinSecret pkBitcoinSecret = new BitcoinSecret("这里是私钥字符串"); payment.Inputs[0].ScriptSig = pkBitcoinSecret.ScriptPubKey; payment.Sign(pkBitcoinSecret, false); 9. 调用Bitcoin Core,将我们这笔交易发布到网络上。 至此,我们的工作已经完成,接下来就是等待旷工处理我们的这笔交易了。如果我们给的交易费高,那么可能很快。 using (var node = Node.ConnectToLocal(Network.Main)) //Connect to the node { node.VersionHandshake(); //Say hello node.SendMessage(new InvPayload(InventoryType.MSG_TX, payment.GetHash())); node.SendMessage(new TxPayload(payment)); Thread.Sleep(10000); //Wait a moment } 这里发送交易到网络是调用了BitCoin Core的,所以必须保证BitCoin Core是打开的。 一旦发送成功,我们可以在比特币钱包中看到多了一笔交易记录: 如果吝啬一点手续费,给的很低,就像我这里这样,要等2天甚至可能更久才会等到这笔交易被矿工写入区块链。 https://blockchain.info/tx/19ebbdd3911e3dede7e2daa158c4f6f0d316f6c73666bf7764ad3a1a013b819d 总结 好了,就这么简单,只需要花费一点点的交易手续费,我们就可以把想说的话放在比特币的区块链上。放在上面也就意味着,永远不会被删除,被全世界的人都能看到。是不是很酷?感觉就是技术宅的表白神器啊!当前全世界的人表白,而且被写入历史的哦!所以一定要三思,不然表白没成功,或者成了前女友,这句话又永远无法被删除,以后怎么给新女友交代啊?! 当然这个功能我们也可以用于存在性证明。我写了一篇文章,拍了一张照片,或者其他数字的东西,我们就可以把这个数字文件的Hash放在OP_RETURN后面,相当于就是对全世界宣布,我在这个时候有这个作品,以后其他人需要我证明的时候,我可以把Hash值拿出来和区块链上的比对,以证明在当时我就已经拥有它了。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
最近开始研究区块链,对这个新兴的技术有了基本概念上的了解,所以打算基于一个开源项目做做实验。如果是做数字货币,那么比特币的源代码是最好的了,不过这算是区块链1.0吧,已经有很多改进的竞争币和山寨币出来了,所以打算对区块链2.0,也就是智能合约入手。 智能合约比较成功的就是以太坊了。以太坊主要是公有链,其实对企业应用来说并不是特别合适,而且本身并没有权限控制功能,面向企业的,主要还是超级账本HyperLedger的Fabric和刚刚开源出来的R3的Corda。关于这些项目的应用场景和区别,我觉得这篇文章写的比较好:http://geek.csdn.net/news/detail/134967 经过比较,觉得Fabric目前比较合适,所以就以这个项目为基础,学习智能合约。 一、环境准备 1.1 安装VirtualBox并在其中安装好Ubuntu 这一步其实没啥好说的,下载好最新版的VirtualBox,下载Ubuntu Server,我用的是16.10 X64。在安装完Ubuntu后,需要保证apt source是国内的,不然如果是国外的话会很慢很慢的。具体做法是 sudo vi /etc/apt/sources.list 打开这个apt源列表,如果其中看到是http://us.xxxxxx之类的,那么就是外国的,如果看到是http://cn.xxxxx之类的,那么就不用换的。我的是美国的源,所以需要做一下批量的替换。在命令模式下,输入: :%s/us./cn./g 就可以把所有的us.改为cn.了。然后输入:wq即可保存退出。 sudo apt-get update 更新一下源。 然后安装ssh,这样接下来就可以用putty或者SecureCRT之类的客户端远程连接Ubuntu了。 sudo apt-get install ssh 1.2 安装Docker 安装Docker也会遇到外国网络慢的问题,幸好国内有很好的镜像,推荐DaoClound,安装Docker的命令是: curl -sSL https://get.daocloud.io/docker | sh 安装完成后,运行以下脚本将当前用户添加到Docker的组中 sudo usermod -aG docker studyzy 重新登录当前用户,接下来修改 Docker 服务配置(/etc/default/docker 文件)。 sudo vi /etc/default/docker 添加以下内容: DOCKER_OPTS="$DOCKER_OPTS -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --api-cors-header='*'" 接下来就需要设置国内的Docker镜像地址,需要注册一个账号,然后在加速器页面提供了设置Docker镜像的脚本,加速器页面是: https://www.daocloud.io/mirror 我提供的脚本是: curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://d4cc5789.m.daocloud.io 运行完脚本后,重启Docker服务 sudo service docker restart 1.3 安装docker-compose Docker-compose是支持通过模板脚本批量创建Docker容器的一个组件。在安装Docker-Compose之前,需要安装Python-pip,运行脚本: sudo apt-get install python-pip 安装完成后,接下来从DaoClound安装Docker-compose,运行脚本: curl -L https://get.daocloud.io/docker/compose/releases/download/1.10.1/docker-compose-`uname -s`-`uname -m` > ~/docker-compose sudo mv ~/docker-compose /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose 二、Fabric部署 2.1 下载Fabric镜像 Fabric的Docker镜像是在https://hub.docker.com/r/hyperledger/ 我们要做实验主要用到peer,baseimage,membersrvc,先现在Peer和membersrvc,这两个镜像提供了latest版本,所以直接pull下来即可。 docker pull hyperledger/fabric-peer docker pull hyperledger/fabric-membersrvc 但是baseimage是没有latest版本,所以我们可以下载一个新一点的版本,然后rename成latest。 docker pull hyperledger/fabric-baseimage:x86_64-0.3.0 docker tag hyperledger/fabric-baseimage:x86_64-0.3.0 hyperledger/fabric-baseimage:latest 现在我们运行docker images命令,可以看到我们准备好的镜像: REPOSITORY TAG IMAGE ID CREATED SIZE hyperledger/fabric-baseimage latest f4751a503f02 7 days ago 1.27 GB hyperledger/fabric-baseimage x86_64-0.3.0 f4751a503f02 7 days ago 1.27 GB hyperledger/fabric-membersrvc latest b3654d32e4f9 3 months ago 1.42 GB hyperledger/fabric-peer latest 21cb00fb27f4 3 months ago 1.42 GB 2.2 使用Git下载Docker-compose模板 如果没有安装Git,那么需要先安装Git,安装Git很简单: sudo apt-get install git 感谢yeasy提供的很好的HyperLedger的模板,我们先克隆到本地: git clone https://github.com/yeasy/docker-compose-files 2.3 以PBFT模式启动Fabric 先进入Git下载下来的Docker-compose目录: cd docker-compose-files/hyperledger/0.6/pbft/ 这里提供了多种模式的启动方案,一种是启动4个节点的Peer,没有权限认证:4-peers.yml 另一种是在4节点Peer的基础上,再加上MembershipService节点,也就是需要权限认证的:4-peers-with-membersrvc.yml 另外还有再进一步,提供了web的Explorer的:4-peers-with-membersrvc-explorer.yml 这里我们就简单点,直接忽略掉MembershipService和Explorer,只启用4个节点的PBFT: docker-compose -f 4-peers.yml up 系统会打印出启动的日志: Creating network "pbft_default" with the default driver Creating pbft_vp0_1 Creating pbft_vp3_1 Creating pbft_vp2_1 Creating pbft_vp1_1 …… 至此,我们的环境搭建完毕,接下来我们就可以在上面跑链上代码了。 三、测试Fabric 3.1 在CLI中测试Example02 我们前面创建了4个容器,开启另外一个命令行窗口,输入docker ps命令,可以看到当前容器的状态: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2131cede4ade hyperledger/fabric-peer:latest "peer node start" 3 minutes ago Up 3 minutes 7050-7059/tcp pbft_vp1_1 5acea88f21bc hyperledger/fabric-peer:latest "peer node start" 3 minutes ago Up 3 minutes 7050-7059/tcp pbft_vp2_1 546b103d904d hyperledger/fabric-peer:latest "peer node start" 3 minutes ago Up 3 minutes 7050-7059/tcp pbft_vp3_1 327ab874b2e3 hyperledger/fabric-peer:latest "peer node start" 3 minutes ago Up 3 minutes 0.0.0.0:7050->7050/tcp, 7051-7059/tcp pbft_vp0_1 这里我们可以看到,最后一个容器pbft_vp0_1其启用了端口映射的,容器上面的7050端口会映射到Ubuntu的7050端口上。我们要执行命令行代码,需要先连接到这个容器内部: docker exec -it pbft_vp0_1 bash 进入容器后,命令行会变为:root@vp0:/opt/gopath/src/github.com/hyperledger/fabric# 这里的容器已经帮我们把测试代码都放在了容器里面,所以我们不需要再下载测试代码。 3.1.1部署Go语言的ChainCode并初始化 下面我们部署Example02到Fabric上: peer chaincode deploy -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 -c '{"Function":"init", "Args": ["a","100", "b", "200"]}' 这个示例是初始化两个账户a和b,a有余额100元,b有余额200元,这是运行结果: root@vp0:/opt/gopath/src/github.com/hyperledger/fabric# peer chaincode deploy -p github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02 -c '{"Function":"init", "Args": ["a","100", "b", "200"]}' 08:37:12.187 [chaincodeCmd] chaincodeDeploy -> INFO 001 Deploy result: type:GOLANG chaincodeID:<path:"github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02" name:"ee5b24a1f17c356dd5f6e37307922e39ddba12e5d2e203ed93401d7d05eb0dd194fb9070549c5dc31eb63f4e654dbd5a1d86cbb30c48e3ab1812590cd0f78539" > ctorMsg:<args:"init" args:"a" args:"100" args:"b" args:"200" > Deploy chaincode: ee5b24a1f17c356dd5f6e37307922e39ddba12e5d2e203ed93401d7d05eb0dd194fb9070549c5dc31eb63f4e654dbd5a1d86cbb30c48e3ab1812590cd0f78539 08:37:12.188 [main] main -> INFO 002 Exiting..... 这里我们可以看到已经部署成功,并返回了ChainCode的ID:ee5b24a1f17c356dd5f6e37307922e39ddba12e5d2e203ed93401d7d05eb0dd194fb9070549c5dc31eb63f4e654dbd5a1d86cbb30c48e3ab1812590cd0f78539 3.1.2查询ChainCode 下面我们把这个ID放入一个变量中: CC_ID="ee5b24a1f17c356dd5f6e37307922e39ddba12e5d2e203ed93401d7d05eb0dd194fb9070549c5dc31eb63f4e654dbd5a1d86cbb30c48e3ab1812590cd0f78539" 下面我们来查询一下a账户的余额: peer chaincode query -n ${CC_ID} -c '{"Function": "query", "Args": ["a"]}' 这是运行结果: root@vp0:/opt/gopath/src/github.com/hyperledger/fabric# peer chaincode query -n ${CC_ID} -c '{"Function": "query", "Args": ["a"]}' 08:41:17.780 [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Successfully queried transaction: chaincodeSpec:<type:GOLANG chaincodeID:<name:"ee5b24a1f17c356dd5f6e37307922e39ddba12e5d2e203ed93401d7d05eb0dd194fb9070549c5dc31eb63f4e654dbd5a1d86cbb30c48e3ab1812590cd0f78539" > ctorMsg:<args:"query" args:"a" > > Query Result: 100 08:41:17.781 [main] main -> INFO 002 Exiting..... 可以看到查询结果是100元。 注意:这里如果遇到了抛出异常: LedgerError - ResourceNotFound: ledger: resource not found 那么就得看log,到底是什么地方错了,我们可以切换回docker-compose的那个窗口,那个窗口会打印错误日志,或者我们再打开一个窗口,运行命令: docker logs -f pbft_vp0_1 查看peer日志,找到原因。我之前一直遇到这个异常,后来发现是baseimage没有latest版的造成的,所以2.1步骤不能出错。 3.1.3调用ChainCode 接下来,我们让a给b转账10元,运行命令: peer chaincode invoke -n ${CC_ID} -c '{"Function": "invoke", "Args": ["a", "b", "10"]}' 这是调用后的结果: root@vp0:/opt/gopath/src/github.com/hyperledger/fabric# peer chaincode invoke -n ${CC_ID} -c '{"Function": "invoke", "Args": ["a", "b", "10"]}' 08:44:19.903 [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Successfully invoked transaction: chaincodeSpec:<type:GOLANG chaincodeID:<name:"ee5b24a1f17c356dd5f6e37307922e39ddba12e5d2e203ed93401d7d05eb0dd194fb9070549c5dc31eb63f4e654dbd5a1d86cbb30c48e3ab1812590cd0f78539" > ctorMsg:<args:"invoke" args:"a" args:"b" args:"10" > > (94c9cbd9-ea04-436f-9cf8-3436303554d2) 08:44:19.904 [main] main -> INFO 002 Exiting..... 3.1.4检查调用ChainCode后的结果 现在已经转账完毕,我们再来查询一下a账户的余额: peer chaincode query -n ${CC_ID} -c '{"Function": "query", "Args": ["a"]}' 查询结果: root@vp0:/opt/gopath/src/github.com/hyperledger/fabric# peer chaincode query -n ${CC_ID} -c '{"Function": "query", "Args": ["a"]}' 08:45:33.937 [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Successfully queried transaction: chaincodeSpec:<type:GOLANG chaincodeID:<name:"ee5b24a1f17c356dd5f6e37307922e39ddba12e5d2e203ed93401d7d05eb0dd194fb9070549c5dc31eb63f4e654dbd5a1d86cbb30c48e3ab1812590cd0f78539" > ctorMsg:<args:"query" args:"a" > > Query Result: 90 08:45:33.937 [main] main -> INFO 002 Exiting..... 可以看到,a账户变成90元了。 3.2 在REST API中测试Example02 前面我们已经说到,容器的7050端口会映射成Ubuntu的7050端口,我们在Ubuntu下,运行ifconfig,可以看到Ubuntu的IP,然后我们回到Windows,就可以通过REST的Client来测试,这里我喜欢用Chrome的插件DHC,很好用,强烈推荐!不过要FQ才能装。 这里我Ubuntu的IP是192.168.100.129,下面就用DHC进行REST API的Example02部署。 3.2.1通过REST API部署GO语言的ChainCode POST 192.168.100.129:7050/chaincode Body是: { "jsonrpc": "2.0", "method": "deploy", "params": { "type": 1, "chaincodeID":{ "path":"github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02" }, "ctorMsg": { "function":"init", "args":["a", "1000", "b", "2000"] } }, "id": 1 } 这里为了区别,我们把a账户初始化1000元,b账户初始化2000元。返回的结果是: { "jsonrpc": "2.0", "result":{ "status": "OK", "message": "04233c6dd8364b9f0749882eb6d1b50992b942aa0a664182946f411ab46802a88574932ccd75f8c75e780036e363d52dd56ccadc2bfde95709fc39148d76f050" }, "id": 1 } 这里04233c6dd8364b9f0749882eb6d1b50992b942aa0a664182946f411ab46802a88574932ccd75f8c75e780036e363d52dd56ccadc2bfde95709fc39148d76f050就是部署后的ChainCodeID。 3.2.2通过REST API查询ChainCode POST 192.168.100.129:7050/chaincode Body内容是: { "jsonrpc": "2.0", "method": "query", "params": { "type": 1, "chaincodeID":{ "name":"04233c6dd8364b9f0749882eb6d1b50992b942aa0a664182946f411ab46802a88574932ccd75f8c75e780036e363d52dd56ccadc2bfde95709fc39148d76f050" }, "ctorMsg": { "function":"query", "args":["a"] } }, "id": 2 } 系统返回的结果是: { "jsonrpc": "2.0", "result":{ "status": "OK", "message": "1000" }, "id": 2 } 一切正常,返回a账户的1000元。 3.2.3通过REST API调用ChainCode 我们试着从a向b转账100元: POST 192.168.100.129:7050/chaincode Body内容是: { "jsonrpc": "2.0", "method": "invoke", "params": { "type": 1, "chaincodeID":{ "name":"04233c6dd8364b9f0749882eb6d1b50992b942aa0a664182946f411ab46802a88574932ccd75f8c75e780036e363d52dd56ccadc2bfde95709fc39148d76f050" }, "ctorMsg": { "function":"invoke", "args":["a", "b", "100"] } }, "id": 3 } 返回的结果: { "jsonrpc": "2.0", "result":{ "status": "OK", "message": "2ac78b5f-6d35-400d-b7c4-75ef81e14d3e" }, "id": 3 } 3.2.4通过REST API检查调用ChainCode后的结果 这里我们来查询一下b账户。 POST 192.168.100.129:7050/chaincode Body内容改为: { "jsonrpc": "2.0", "method": "query", "params": { "type": 1, "chaincodeID":{ "name":"04233c6dd8364b9f0749882eb6d1b50992b942aa0a664182946f411ab46802a88574932ccd75f8c75e780036e363d52dd56ccadc2bfde95709fc39148d76f050" }, "ctorMsg": { "function":"query", "args":["b"] } }, "id": 4 } 返回结果: { "jsonrpc": "2.0", "result":{ "status": "OK", "message": "2100" }, "id": 4 } 一切正常,b账户果然真假了100元。 关于更多的REST API,我们可以参考这里:https://github.com/hyperledger-archives/fabric/blob/master/docs/API/CoreAPI.md#rest-api 3.3测试Java版Chain Code Fabric除了支持本身的Go语言的ChainCode,也可以支持其他语言,比如最常用的Java语言。Fabric的源代码中也提供了Java示例,这里我们就用SimpleSample这个示例: https://github.com/hyperledger/fabric/tree/master/examples/chaincode/java/SimpleSample 3.3.1在CLI中部署该Java代码的ChainCode到Fabric 命令是: peer chaincode deploy -l java -p /opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/java/SimpleSample -c '{"Function":"init", "Args": ["a","100", "b", "200"]}' 运行结果为: root@vp0:/opt/gopath/src/github.com/hyperledger/fabric# peer chaincode deploy -l java -p /opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/java/SimpleSample -c '{"Function":"init", "Args": ["a","100", "b", "200"]}' 09:20:16.857 [chaincodeCmd] chaincodeDeploy -> INFO 001 Deploy result: type:JAVA chaincodeID:<path:"/opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/java/SimpleSample" name:"0f5b1d65041bc6d500bd0f1cab50eb6154c291ef0f4596d64b6797e8ef8f7c34a179b5a2cea82253ff3d74e768512fe0481503eadcf13d18f9761bbb8133efd0" > ctorMsg:<args:"init" args:"a" args:"100" args:"b" args:"200" > Deploy chaincode: 0f5b1d65041bc6d500bd0f1cab50eb6154c291ef0f4596d64b6797e8ef8f7c34a179b5a2cea82253ff3d74e768512fe0481503eadcf13d18f9761bbb8133efd0 09:20:16.857 [main] main -> INFO 002 Exiting..... 接下来的各种查询,调用都是差不多的,我就不再累述了。 3.3.2通过REST API部署Java ChainCode到Fabric POST 192.168.100.129:7050/chaincode Body为 { "jsonrpc": "2.0", "method": "deploy", "params": { "type": 4, "chaincodeID":{ "path":"/opt/gopath/src/github.com/hyperledger/fabric/examples/chaincode/java/SimpleSample" }, "ctorMsg": { "function":"init", "args":["a", "1000", "b", "2000"] } }, "id": 1 } 系统返回的结果为: { "jsonrpc": "2.0", "result":{ "status": "OK", "message": "27cb2925013a5e8f27b41be748e6767c3fbc7bfdfe2453c2640f9069e75c4db38735fa3b6b8cac78e212a1c97193f3bfb2f9b810ce0a11f437a96b330d508fbd" }, "id": 1 } 这里需要注意的是type:4,不再是1。1是Go语言的,而Java语言是4.接下来的操作也是类似的了,我就不累述了。 总的来说,Fabric基于Docker容器技术,部署的ChainCode在运行时会基于baseimage重新创建Docker容器,运行的链上代码越多,容器就会越多。运行docker ps会看到很多容器被创建。docker images也可以看到多了很多镜像。需要注意清理。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
很多年前就给前公司的年会做过年会抽奖,基本要求就是年会入场时签到,签到的员工都参与抽奖(也可以设置公司高管过滤,不参与抽奖),奖品是预设好的,到时候就是给所有签到员工编号,然后抽奖过程中不断生成一组随机数,这些随机数对应的编号的员工姓名和照片就显示出来,这是很容易想到的算法。 但是还要一种情况就是互联网模式的抽奖,有点像双十一之前,阿里派发红包一样,大家都可以在开始抽奖的时候去抽,奖品也是预设好的,比如1000W的奖金池,派发完毕就抽奖完毕,每个用户可以抽取多次。这种抽奖方式主要是应对抽奖人数不确定的情况,谁也不需要提前签到报名,到了抽奖时间只要注册用户都可以抽奖。 因为抽奖人数不确定,所以采用一人多次抽奖的方案是很好的,对用户来说也是,如果第一次没有抽中,还可以尝试第二次,第三次。具体算法上,其实更简单,因为用户点击抽奖的顺序是随机的,所以我们连随机数都不用用,直接给用户的一次抽奖请求编个自增的号,如果这个号满足中奖规则,那么就分配礼品,返回该抽奖请求中奖结果,如果不满足中奖规则,那么我们就返回未中奖。 为了避免用户频繁的点击,造成服务器过高的负担,我们可以在客户端设置一个动画过程,比如转盘抽奖,可以转几秒以后才请求服务器,看是否中奖,对用户来说也增加了趣味性。为了避免用户不通过客户端,直接发起频繁的HTTP请求来刷奖,我们甚至可以在服务器设置同一个用户的请求时间间隔。 下面贴出我写的一个示例代码部分,我设置了一个自增的整数Sequence ,每个正常的抽奖请求,则Sequence ++,另外设置默认的抽奖基数baseNumber=100,如果能够Sequence能够被baseNumber整除,那么就中奖,否则不中奖: [RoutePrefix("api/Lottery")] public class LotteryController : AbpApiController { private static volatile int Sequence = 1; private static IList<int> winnerList=new List<int>(); /// <summary> /// 抽奖开始标记,请通过StartNewLotteryRound打开 /// </summary> private static bool start = false; /// <summary> /// 所有产品都被抽完了的标记 /// </summary> private static bool allPrizeOut = false; /// <summary> /// 当前轮次ID /// </summary> private static int currentRoundId = 0; public ILotteryAppService LotteryAppService { get; set; } /// <summary> /// 抽奖基数,只要被该数整除就中奖 /// </summary> private static int baseNumber =100; private static IDictionary<int,DateTime> userDrawTime=new Dictionary<int, DateTime>(); private bool CheckUserDrawTime(int userId) { if (userDrawTime.ContainsKey(userId)) { return userDrawTime[userId].AddSeconds(8) < DateTime.Now;//8s后可以抽奖 } else { return true; } } /// <summary> /// 抽奖一次 /// </summary> /// <param name="userId"></param> /// <returns></returns> [HttpGet] [Route("Draw/{userId}")] public DrawResult Draw(int userId) { if (!start) { return new DrawResult(400,0, "抽奖未开始"); } if (allPrizeOut) { return new DrawResult(400, 0, "所有奖品已抽完"); } if (!CheckUserDrawTime(userId)) { return new DrawResult(400, 0, "请求过于频繁,请稍后再试"); } int myNumber = Sequence++; userDrawTime[userId] = DateTime.Now;//记录用户这次抽奖的时间 if (myNumber%baseNumber == 0) //中奖啦! { if (winnerList.Contains(userId)) { //用户已经中奖,不用再抽 return new DrawResult(200, 0, "您已经中过奖了"); } var result = LotteryAppService.WriteAWinner(userId, currentRoundId); switch (result.ExceptionType) { case LotteryExceptionType.NoException: { winnerList.Add(userId); return new DrawResult(200, result.PrizeId, ""); } case LotteryExceptionType.AllPrizeOut: { allPrizeOut = true; return new DrawResult(400, 0, "所有奖品已抽完"); } case LotteryExceptionType.InvalidLotteryRound: { return new DrawResult(400, 0, "抽奖轮次无效"); } default: { return new DrawResult(400, 0, "当前用户无效"); } } } return new DrawResult(200, 0, ""); } /// <summary> /// 获得我的奖品对象 /// </summary> /// <param name="userId"></param> /// <returns></returns> [HttpGet] [Route("MyPrize/{userId}")] public IList<LotteryDto> GetMyPrize(int userId) { return LotteryAppService.GetMyPrize(userId); } /// <summary> /// 开始新一轮的抽奖 /// </summary> /// <param name="roundId"></param> [HttpPost] [Route("StartNewLotteryRound")] [AbpApiAuthorize(PermissionNames.Admin)] public bool StartNewLotteryRound(int roundId) { start = true; allPrizeOut = false; currentRoundId = roundId; return true; } /// <summary> /// 获得当前轮次的奖品和获奖者 /// </summary> /// <returns></returns> [HttpGet] [Route("")] public IList<LotteryDto> GetLotteries() { return LotteryAppService.GetLotteries(currentRoundId); } /// <summary> /// 获得所有的奖品和获奖者 /// </summary> /// <returns></returns> [HttpGet] [Route("All")] public IList<LotteryDto> GetAllLotteries() { return LotteryAppService.GetLotteries(0); } /// <summary> /// 清空中奖结果,各种缓存 /// </summary> /// <returns></returns> [HttpPost] [Route("Clean")] [AbpApiAuthorize(PermissionNames.Admin)] public bool Clean() { Sequence = 1; start = false; winnerList.Clear(); LotteryAppService.CleanLotteries(); return true; } /// <summary> /// 获取是否显示抽奖图标 /// </summary> /// <returns></returns> [HttpGet] [Route("ShowLotteryIcon")] public bool GetShowLotteryIcon() { return LotteryAppService.ShowLotteryIcon; } /// <summary> /// 设置是否显示抽奖图标 /// </summary> /// <param name="show"></param> /// <returns></returns> [HttpPut] [Route("ShowLotteryIcon/{show}")] public HttpResponseMessage SetShowLotteryIcon(bool show) { try { LotteryAppService.ShowLotteryIcon = show; return Request.CreateResponse(HttpStatusCode.OK, true); } catch (Exception ex) { var resp = new HttpResponseMessage(HttpStatusCode.BadGateway) { Content = new StringContent("设置ShowLotteryIcon失败:" + ex.Message), ReasonPhrase = "Gateway failed" }; throw new HttpResponseException(resp); } } /// <summary> /// 设置Base Number /// </summary> /// <param name="number"></param> /// <returns></returns> [HttpPut] [AbpApiAuthorize(PermissionNames.Admin)] [Route("SetBaseNumber/{number}")] public bool SetBaseNumber(int number) { baseNumber = number; return true; } } 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
Hive是Hadoop生态中的一个重要组成部分,主要用于数据仓库。前面的文章中我们已经搭建好了Hadoop的群集,下面我们在这个群集上再搭建Hive的群集。 1.安装MySQL 1.1安装MySQL Server 在Ubuntu下面安装MySQL的Server很简单,只需要运行: sudo apt-get install mysql-server 系统会把MySQL下载并安装好。这里我们可以把MySQL安装在master机器上。 安装后需要配置用户名密码和远程访问。 1.2配置用户名密码 首先我们以root身份登录到mysql服务器: sudo mysql -u root 然后修改root的密码,并允许root远程访问: GRANT ALL PRIVILEGES ON *.* TO root@'%' IDENTIFIED BY "123456"; 我们这里还可以为hive建立一个用户,而不是用root用户: GRANT ALL PRIVILEGES ON *.* TO hive@'%' IDENTIFIED BY "hive"; 运行完成后quit命令即可退出mysql的命令行模式。 1.3配置远程访问 默认情况下,MySQL是只允许本机访问的,要允许远程机器访问需要修改配置文件 sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf 找到bind-address的配置部分,然后改为: bind-address = 0.0.0.0 保存,重启mysql服务 sudo service mysql restart 重启完成后,我们可以在Windows下,用MySQL的客户端连接master上的MySQL数据库,看是否能够远程访问。 2.下载并配置Hive 2.1下载Hive 首先我们到官方网站,找到Hive的下载地址。http://www.apache.org/dyn/closer.cgi/hive/ 会给出一个建议的网速快的下载地址。 然后在master服务器上,wget下载hive的编译好的文件,我现在最新版是Hive 2.1.1 : wget http://mirror.bit.edu.cn/apache/hive/hive-2.1.1/apache-hive-2.1.1-bin.tar.gz 下载完成后,解压这个压缩包 tar xf apache-hive-2.1.1-bin.tar.gz 按之前Hadoop的惯例,我们还是把Hive安装到/usr/local目录下吧,所以移动Hive文件: sudo mv apache-hive-2.1.1-bin /usr/local/hive 2.2配置环境变量 sudo vi /etc/profile 增加如下配置: export HIVE_HOME=/usr/local/hive export PATH=$PATH:$HIVE_HOME/bin export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib:/usr/local/hive/lib 2.3配置hive-env.sh 所有Hive的配置是在/usr/local/hive/conf目录下,进入这个目录,我们需要先基于模板新建hive-env.sh文件: cp hive-env.sh.template hive-env.sh vi hive-env.sh 指定Hadoop的路径,增加以下行: HADOOP_HOME=/usr/local/hadoop 2.4配置hive-site.xml cp hive-default.xml.template hive-site.xml vi hive-site.xml 首先增加mysql数据库的连接配置: <property> <name>javax.jdo.option.ConnectionURL</name> <value>jdbc:mysql://master:3306/hive?createDatabaseIfNotExist=true</value> <description>JDBC connect string for a JDBC metastore</description> </property> <property> <name>javax.jdo.option.ConnectionDriverName</name> <value>com.mysql.jdbc.Driver</value> <description>Driver class name for a JDBC metastore</description> </property> <property> <name>javax.jdo.option.ConnectionUserName</name> <value>hive</value> <description>username to use against metastore database</description> </property> <property> <name>javax.jdo.option.ConnectionPassword</name> <value>hive</value> <description>password to use against metastore database</description> </property> 然后需要修改临时文件夹的路径,找到以下2个配置,并改为正确的路径: <property> <name>hive.exec.local.scratchdir</name> <value>/home/hduser/iotmp</value> <description>Local scratch space for Hive jobs</description> </property> <property> <name>hive.downloaded.resources.dir</name> <value>/home/hduser/iotmp</value> <description>Temporary local directory for added resources in the remote file system.</description> </property> 这里因为我当前用户是hduser,所以我在hduser的目录下创建一个iotmp文件夹,并授权: mkdir -p /home/hduser/iotmp chmod -R 775 /home/hduser/iotmp 2.5修改hive-config.sh 进入目录/usr/local/hive/bin vi hive-config.sh 在该文件的最前面加入以下配置: export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 export HADOOP_HOME=/usr/local/hadoop export HIVE_HOME=/usr/local/hive 2.6下载MySQL JDBC驱动 去MySQL的官网,https://dev.mysql.com/downloads/connector/j/ 下载JDBC驱动到master服务器上。 wget https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.40.tar.gz 下载完后解压 tar xf mysql-connector-java-5.1.40.tar.gz 然后进入解压后的目录,把jar包复制到Hive/lib目录下面 cp mysql-connector-java-5.1.40-bin.jar /usr/local/hive/lib/ 2.7在HDFS中创建目录和设置权限 启动Hadoop,在Hadoop中创建Hive需要用到的目录并设置好权限: hadoop fs -mkdir /tmp hadoop fs -mkdir -p /user/hive/warehouse hadoop fs -chmod g+w /tmp hadoop fs -chmod g+w /user/hive/warehouse 2.8初始化meta数据库 进入/usr/local/hive/lib目录,初始化Hive元数据对应的MySQL数据库: schematool -initSchema -dbType mysql 3.使用Hive 在命令行下,输入hive命令即可进入Hive的命令行模式。我们可以查看当前有哪些数据库,哪些表: show databases; show tables; 关于hive命令下有哪些命令,具体介绍,可以参考官方文档:https://cwiki.apache.org/confluence/display/Hive/Home 3.1创建表 和普通的SQL创建表没有太大什么区别,主要是为了方便,我们设定用\t来分割每一行的数据。比如我们要创建一个用户表: create table Users (ID int,Name String) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'; 3.2插入数据 是insert语句可以插入单条数据: insert into Users values(1,'Devin'); 如果要导入数据 我们在Ubuntu下创建一个name.txt文件,然后编辑其中的内容,添加如下内容: 2 Edward 3 Mindy 4 Dave 5 Joseph 6 Leo 列直接我是用Tab隔开的。 如果想把这个txt文件导入hive的Users 表,那么只需要在hive中执行: LOAD DATA LOCAL INPATH '/home/hduser/names.txt' into table Users ; 3.3查询数据 仍然是sql语句: select * from Users ; 当然我们也可以跟条件的查询语句: select * from Users where Name like 'D%'; 3.4增加一个字段 比如我们要增加生日这个字段,那么语句为: alter table Users add columns (BirthDate date); 3.5查询表定义 我们看看表的结构是否已经更改,查看Users表的定义: desc Users; 3.6其他 另外还有重名了表,删除表等,基本也是SQL的语法: alter table Users rename to Student; 删除一个表中的所有数据: truncate table Student; 【另外需要注意,Hive不支持update和delete语句。似乎只有先truncate然后在重新insert。】 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
在前一篇文章中,我们已经搭建好了Hadoop的群集,接下来,我们就是需要基于这个Hadoop群集,搭建Spark的群集。由于前面已经做了大量的工作,所以接下来搭建Spark会简单很多。 首先打开三个虚拟机,现在我们需要安装Scala,因为Spark是基于Scala开发的,所以需要安装Scala。在Ubuntu下安装Scala很简单,我们只需要运行 sudo apt-get install scala 就可以安装Scala了。 安装完成后运行scala -version可以看到安装的Scala的版本,我现在2.11版,安装目录是在/usr/share/scala-2.11 。 接下来下载Spark。到官方网站,找到最新版的Spark的下载地址,选择Hadoop版本, http://spark.apache.org/downloads.html wget http://spark下载地址 当下载完毕后解压文件: tar xvf spark-2.0.2-bin-hadoop2.7.tgz 接下来我们需要将解压的文件夹移动到指定目录,因为之前我们Hadoop安装到/usr/local/hadoop,所以我们也可以把Spark放在/usr/local/spark下: sudo mv spark-2.0.2-bin-hadoop2.7 /usr/local/spark 进入spark文件夹下的conf文件夹,里面有个spark-env.sh.template文件,是spark环境变量设置的目标,我们可以复制一个出来: cp spark-env.sh.template spark-env.sh 然后编辑该文件 vi spark-env.sh 在文件的末尾我们添加上以下内容: export SCALA_HOME=/usr/share/scala-2.11 export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 export HADOOP_HOME=/usr/local/hadoop export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop SPARK_MASTER_IP=master SPARK_LOCAL_DIRS=/usr/local/spark SPARK_DRIVER_MEMORY=1G export LD_LIBRARY_PATH=/usr/local/hadoop/lib/native/:$LD_LIBRARY_PATH 这里的内容是根据我虚拟机的环境来的,如果安装的版本和路径不一样,可以根据实际情况更改。 接下来设置slaves文件。 cp slaves.template slaves vi slaves 将内容改为 slave01 slave02 Spark在一台机器上就算配置完毕,接下来在另外两台机器上也做一模一样的配置即可。 启动Spark 在master上,我们先启动Hadoop,然后运行 /usr/local/spark/sbin/start-all.sh 便可启动Spark。 运行jps看看Java进程: 2929 Master 2982 Jps 2294 SecondaryNameNode 2071 DataNode 1929 NameNode 2459 ResourceManager 2603 NodeManager 发现比Hadoop启动的时候多了Master进程。 切换到slave01节点上,运行JPS,看看进程: 1889 Worker 1705 NodeManager 1997 Jps 1551 DataNode 这里比Hadoop的时候多了一个Worker进程。说明我们的Spark群集已经启动成功。 下面访问Spark的网站: http://192.168.100.40:8080/ 可以看到2个worker都启动。 最后,我们运行一下Spark的示例程序: /usr/local/spark/bin/run-example SparkPi 10 --slave01 local[2] 可以在结果中找到 Pi is roughly 3.14XXXXX 说明我们运行成功了。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
前面我搭建的Hadoop都是单机伪分布式的,并不能真正感受到Hadoop的最大特点,分布式存储和分布式计算。所以我打算在虚拟机中同时开启3台机器,实现分布式的Hadoop群集。 1.准备3台Ubuntu Server 1.1复制出3台虚拟机 我们可以用之前编译和安装好Hadoop的虚拟机作为原始版本,在VirtualBox中复制三台新的虚拟机出来,也可以完全重新安装一台全新的Ubuntu Server,然后在VirtualBox中复制出2台,就变成了3台虚拟机。 1.2修改主机名 主机名保存在/etc/hostname文件中,我们可以运行 sudo vi /etc/hostname 命令,然后为三台机器起不同的名字,这里我们就分别起名: master slave01 slave02 1.3修改为固定IP Ubuntu的IP地址保存到/etc/network/interfaces文件中,我们需要为3台虚拟机分别改为固定的IP,这里我的环境是在192.168.100.*网段,所以我打算为master改为192.168.100.40,操作如下: sudo vi /etc/network/interfaces 然后可以看到每个网卡的配置,我这里网卡名是叫enp0s3,所以我改对应的内容为: # The primary network interface auto enp0s3 iface enp0s3 inet static address 192.168.100.40 gateway 192.168.100.1 netmask 255.255.255.0 对slave01改为192.168.100.41,slave02改为192.168.100.42。 1.4修改Hosts 由于三台虚拟机是使用的默认的DNS,所以我们需要增加hosts记录,才能直接用名字相互访问。hosts文件和Windows的Hosts文件一样,就是一个域名和ip的对应表。 编辑hosts文件: sudo vi /etc/hosts 增加三条记录: 192.168.100.40 master 192.168.100.41 slave01 192.168.100.42 slave02 1.5重启 这一切修改完毕后我们重启一下三台机器,然后可以试着各自ping master,ping slave01 ping slave02看能不能通。按照上面的几步操作,应该是没有问题的。 1.6新建用户和组 这一步不是必须的,就采用安装系统后的默认用户也是可以的。 sudo addgroup hadoop sudo adduser --ingroup hadoop hduser 为了方便,我们还可以把hduser添加到sudo这个group中,那么以后我们在hduser下就可以运行sudo xxx了。 sudo adduser hduser sudo 切换到hduser: su – hduser 1.7配置无密码访问SSH 在三台机器上首先安装好SSH sudo apt-get install ssh 然后运行 ssh-keygen 默认路径,无密码,会在当前用户的文件夹中产生一个.ssh文件夹。 接下来我们先处理master这台机器的访问。我们进入这个文件夹, cd .ssh 然后允许无密码访问,执行: cp id_rsa.pub authorized_keys 然后要把slave01和slave02的公钥传给master,进入slave01 scp ~/.ssh/id_rsa.pub hduser@master:/home/hduser/.ssh/id_rsa.pub.slave01 然后在slave02上也是: scp ~/.ssh/id_rsa.pub hduser@master:/home/hduser/.ssh/id_rsa.pub.slave02 将 slave01 和 slave02的公钥信息追加到 master 的 authorized_keys文件中,切换到master机器上,执行: cat id_rsa.pub.slave01 >> authorized_keys cat id_rsa.pub.slave02 >> authorized_keys 现在authorized_keys就有3台机器的公钥,可以无密码访问任意机器。只需要将authorized_keys复制到slave01和slave02即可。在master上执行: scp authorized_keys hduser@slave01:/home/hduser/.ssh/authorized_keys scp authorized_keys hduser@slave02:/home/hduser/.ssh/authorized_keys 最后我们可以测试一下,在master上运行 ssh slave01 如果没有提示输入用户名密码,而是直接进入,就说明我们配置成功了。 同样的方式测试其他机器的无密码ssh访问。 2.安装相关软件和环境 如果是直接基于我们上一次安装的单机Hadoop做的虚拟机,那么这一步就可以跳过了,如果是全新的虚拟机,那么就需要做如下操作: 2.1配置apt source,安装JDK sudo vi /etc/apt/sources.list 打开后把里面的us改为cn,如果已经是cn的,就不用再改了。然后运行: sudo apt-get update sudo apt-get install default-jdk 2.2下载并解压Hadoop 去Hadoop官网,找到最新稳定版的Hadoop下载地址,然后下载。当然如果是X64的Ubuntu,我建议还是本地编译Hadoop,具体编译过程参见这篇文章。 wget http://www.apache.org/dyn/closer.cgi/hadoop/common/hadoop-2.7.3/hadoop-2.7.3.tar.gz 下载完毕后然后是解压 tar xvzf hadoop-2.7.3.tar.gz 最后将解压后的Hadoop转移到正式的目录下,这里我们打算使用/usr/local/hadoop目录,所以运行命令: sudo mv hadoop-2.7.3 /usr/local/hadoop 3.配置Hadoop 3.1配置环境变量 编辑.bashrc或者/etc/profile文件,增加如下内容: # Java Env export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64 export JRE_HOME=$JAVA_HOME/jre export CLASSPATH=.:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:$JRE_HOME/lib export PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin # Hadoop Env export HADOOP_HOME=/usr/local/hadoop export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin 3.2进入Hadoop的配置文件夹: cd /usr/local/hadoop/etc/hadoop (1)修改hadoop-env.sh 增加如下配置: export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64 export HADOOP_PREFIX=/usr/local/hadoop (2)修改core-site.xml <configuration> <property> <name>hadoop.tmp.dir</name> <value>/home/hduser/temp</value> <description>A base for other temporary directories.</description> </property> <property> <name>fs.defaultFS</name> <value>hdfs://master:9000</value> </property> </configuration> 这里我们指定了一个临时文件夹的路径,这个路径必须存在,而且有权限访问,所以我们在hduser下创建一个temp目录。 (3)hdfs-site.xml 设置HDFS复制的数量 <configuration> <property> <name>dfs.replication</name> <value>3</value> </property> </configuration> (4)mapred-site.xml 这里可以设置MapReduce的框架是YARN: <configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> </configuration> (5)配置YARN环境变量,打开yarn-env.sh 里面有很多行,找到JAVA_HOME,设置: export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 (6)配置yarn-site.xml <configuration> <!-- Site specific YARN configuration properties --> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.resourcemanager.hostname</name> <value>master</value> </property> </configuration> (7)最后打开slaves文件,设置有哪些slave节点。 由于我们设置了3份备份,把master即是Name Node又是Data Node,所以我们需要设置三行: master slave01 slave02 3.3配置slave01和slave02 在slave01和slave02上做前面3.1 3.2相同的设置。 一模一样的配置,这里不再累述。 4.启动Hadoop 回到Master节点,我们需要先运行 hdfs namenode –format 格式化NameNode。 然后执行 start-all.sh 这里Master会启动自己的服务,同时也会启动slave01和slave02上的对应服务。 启动完毕后我们在master上运行jps看看有哪些进程,这是我运行的结果: 2194 SecondaryNameNode 2021 DataNode 1879 NameNode 3656 Jps 2396 ResourceManager 2541 NodeManager 切换到slave01,运行jps,可以看到如下结果: 1897 NodeManager 2444 Jps 1790 DataNode 切换到slave02也是一样的有这些服务。 那么说明我们的服务网都已经启动成功了。 现在我们在浏览器中访问: http://192.168.100.40:50070/ 应该可以看到Hadoop服务已经启动,切换到Datanodes可以看到我们启动的3台数据节点: 【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
在之前的文章中介绍了如何直接在Ubuntu中安装Hadoop。但是对于64位的Ubuntu来说,官方给出的Hadoop包是32位的,运行时会得到警告: WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable 所以我们最好是自己在Ubuntu中编译Hadoop。先介绍一下我的环境是Ubuntu 16.1 X64 Server版,当前最新的Hadoop是2.7.3。我们可以先下载源代码,在其中有BUILDING.txt,指导我们如何在Ubuntu中编译Hadoop: Installing required packages for clean install of Ubuntu 14.04 LTS Desktop: * Oracle JDK 1.7 (preferred) $ sudo apt-get purge openjdk* $ sudo apt-get install software-properties-common $ sudo add-apt-repository ppa:webupd8team/java $ sudo apt-get update $ sudo apt-get install oracle-java7-installer * Maven $ sudo apt-get -y install maven * Native libraries $ sudo apt-get -y install build-essential autoconf automake libtool cmake zlib1g-dev pkg-config libssl-dev * ProtocolBuffer 2.5.0 (required) $ sudo apt-get -y install libprotobuf-dev protobuf-compiler Optional packages: * Snappy compression $ sudo apt-get install snappy libsnappy-dev * Bzip2 $ sudo apt-get install bzip2 libbz2-dev * Jansson (C Library for JSON) $ sudo apt-get install libjansson-dev * Linux FUSE $ sudo apt-get install fuse libfuse-dev 我用的Linux16.1 X64 Server版本,也是大同小异,接下来是编译Hadoop的过程: 1.更新apt源 首先需要更新我们的apt源,因为如果是国外源的话,接下来安装会很慢。使用命令 sudo vi /etc/apt/sources.list 打开这个apt源列表,如果其中看到是http://us.xxxxxx之类的,那么就是外国的,如果看到是http://cn.xxxxx之类的,那么就不用换的。我的是美国的源,所以需要做一下批量的替换。在命令模式下,输入: :%s/us./cn./g 就可以把所有的us.改为cn.了。然后输入:wq即可保存退出。 sudo apt-get update 更新一下源。 2.安装必备软件 2.1安装SSH sudo apt-get install ssh 安装完毕后我们就可以用putty或者SecureCRT连接到Ubuntu了。 2.2安装JDK sudo apt-get install default-jdk 安装后可以运行java –version看安装的版本 2.3安装Maven sudo apt-get install maven 这是编译Hadoop的工具,安装完成后,可以运行mvn -–version看安装的版本 2.4安装依赖库 sudo apt-get install g++ autoconf automake libtool cmake zlib1g-dev pkg-config libssl-dev 2.5安装ProtocolBuffer 2.5.0 注意,我要编译的是Hadoop2.7.3,必须安装的ProtocolBuffer是2.5这个版本,如果不是的话,接下来会编译失败: protoc version is 'libprotoc 3.0.0, expected version is '2.5.0' 如果我们运行文档中的: sudo apt-get -y install libprotobuf-dev protobuf-compiler protoc –version 会告诉我们安装的是3.0版本,这是不对的。我们需要的是2.5版。怎么办呢?只有去GitHub找到2.5版,然后重新编译安装。过程如下: wget https://github.com/google/protobuf/releases/download/v2.5.0/protobuf-2.5.0.tar.gz tar -xzf protobuf-2.5.0.tar.gz cd protobuf-2.5.0/ ./autogen.sh ./configure make make install 现在我们重新运行protoc –version 会看到版本是2.5了。 2.6更新Maven镜像 由于Maven默认连接的是国外的服务器,会很慢,所以我们需要更新Maven源为国内的服务器。推荐还有阿里云的Maven源:http://maven.aliyun.com/ 。 具体做法是: cd ~/.m2 (如果没有这个文件夹,那么就在~目录mkdir .m2创建这个文件夹) vi settings.xml 然后输入以下的内容: <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <mirrors> <mirror> <id>nexus-aliyun</id> <mirrorOf>*</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> </mirrors> </settings> 3.编译Hadoop 首先我们下载最新的Hadoop源代码,访问官网http://hadoop.apache.org/releases.html,可以看到最新的是2.7.3,所以我们点击2.7.3的source,会给我们一个比较快的下载地址。 wget http://mirror.bit.edu.cn/apache/hadoop/common/hadoop-2.7.3/hadoop-2.7.3-src.tar.gz 下载完毕后解压: tar -zxvf hadoop-2.7.3-src.tar.gz cd hadoop-2.7.3-src 最后,就是用Maven编译Hadoop: mvn package -Pdist,native -DskipTests –Dtar 这是一个比较漫长的过程,可能要等10~20来分钟。如果一切正常,那么运行完毕后,我们会看到成功编译的通知: 我是在虚拟机中,花了23分钟,我们的Hadoop X64版本就编译出来了。 编译好的Hadoop是在: hadoop-dist/target/ 目录下,hadoop-2.7.3.tar.gz文件便是。 我们可以把这个包下载到本地,或者传输到其他服务器,接下来就是用这个包安装Hadoop,具体安装配置过程参见我这篇博客。
用过MySQL的应该都会注意到,默认情况下,Linux下安装的MySQL是大小写敏感的,也就是说Table1和table1可以同时存在。而Windows下的MySQL却是大小写不敏感的,所有表名和数据库名都会变成小写。 对于怎么启用或者停用MySQL数据库的大小写敏感,这个网上随便都能找到,就是改改参数lower_case_table_names,然后重启即可。 但是,如果我们的数据库中已经有了多个区分大小写的数据库,现在要改为不区分大小写的,那么就会报错:Table 'databasenamexxx.tablenamexxx' doesn't exist. 为此,我们需要将MySQL改为大小写敏感的模式,然后去重命名每个表名和数据库名。 MySQL确实很神奇的一点是不允许重命名数据库,所以如果我们要重命名Test1为test1,那么只有新建一个test1的数据库,然后把Test1中的表全面rename到test1数据库中。 而且在rename的过程中,我们也需要将表面从大小写的形式改为全部小写的形式。 为了批量的做这么一件事,与,我写了一个存储过程,通过读取系统表,获得数据库表名,然后用游标的方式依次执行rename操作。 DELIMITER // CREATE PROCEDURE renametables(olddb VARCHAR(50),newdb VARCHAR(50)) BEGIN DECLARE done BOOLEAN DEFAULT 0; DECLARE tmp VARCHAR(100); -- 定义局部变量 DECLARE tbcur CURSOR FOR SELECT TABLE_NAME FROM `information_schema`.`TABLES` WHERE table_schema=olddb AND Table_Type='BASE TABLE'; DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1; OPEN tbcur; -- 打开游标 REPEAT FETCH tbcur INTO tmp; IF done=0 THEN SET @sqlstring=CONCAT( 'RENAME TABLE ',olddb,'.`',tmp,'` TO ',newdb,'.`',LOWER(tmp),'`;'); SELECT @sqlstring; -- 这一句可以不要,只是打印我们拼接后要运行的SQL是什么 PREPARE s1 FROM @sqlstring; -- 执行拼接出来的SQL EXECUTE s1; DEALLOCATE PREPARE s1; END IF; UNTIL done END REPEAT; CLOSE tbcur; -- 关闭游标,释放游标使用的所有内部内存和资源 END// 我们在新数据库中建立了该存储过程,然后调用即可: CALL renametables('Test1','test1') 这样所有Test1中的大小写混合的表,就全部转换到了test1数据库中,而且表名都变成了小写了。 一个一个的数据库去这么做,然后再把MySQL的参数改为大小写不敏感,这样才能正常使用。 这里我只是做了表的迁移,接下来存储过程和视图的迁移,由于不涉及到数据,所以比较简单,找到当年的DDL或者我们在大小写敏感的时候就导出View和存储过程的定义,然后用文本编辑器把整个SQL变成小写的,然后到新数据库中去执行,重新创建即可。
最近在研究虚拟化,容器和大数据,所以从Docker入手,下面介绍一下在Windows下怎么玩转Docker。 Docker本身在Windows下有两个软件,一个就是Docker,另一个是Docker Toolbox。这里我选择的是Docker Toolbox,为什么呢?参见官方文档: https://blog.docker.com/2015/08/docker-toolbox/ 首先我们从官网下载最新版的Windows Docker Toolbox。安装后会安装一个VirtualBox虚拟机,一个Kitematic,这是GUI管理Docker的工具,没有发布正式版,不推荐使用,另外还有就是我们在命令行下用到的docker-machine和docker命令了。 基本使用 安装完成Toolbox后会有一个Docker Quickstart Terminal的快捷方式,双击运行如果报错,那可能是因为你已经安装了Hyper-v,所以VirtualBox无法用64位的虚拟机。需要卸载Hyper-v。 运行后会在Virtualbox中创建一个叫做default的虚拟机,然后很有可能会卡在waiting for an IP的命令下,然后就死活不动了。我的做法是彻底放弃Docker Quickstart Terminal,根本不用这玩意儿,关掉,我们用PowerShell进行虚拟机的管理。 打开PowerShell,输入: docker-machine ls 我们可以看到我们当前的Docker虚拟机的状态。如果什么都没有的话,那么我们可以使用以下命令创建一个Docker虚拟机。 docker-machine create --driver=virtualbox default 创建完毕后,我们在用docker-machine ls确认我们的Docker虚拟机在运行中。 然后使用以下命令获得虚拟机的环境变量: docker-machine env default 然后再输入: docker-machine env default | Invoke-Expression 这样我们就把当前的PowerShell和虚拟机里面的Docker Linux建立的连接,接下来就可以在PowerShell中使用docker命令了。 比如我们要查看当前有哪些镜像: docker images 当前有哪些容器: docker ps –a 其他各种docker命令我就不在这里累述了。 Docker虚拟机文件地址修改 默认情况下,docker-machine创建的虚拟机文件,是保存在C盘的C:\Users\用户名\.docker\machine\machines\default 目录下的,如果下载和使用的镜像过多,那么必然导致该文件夹膨胀过大,如果C盘比较吃紧,那么我们就得考虑把该虚拟机移到另一个盘上。具体操作如下: 1.使用docker-machine stop default停掉Docker的虚拟机。 2.打开VirtualBox,选择“管理”菜单下的“虚拟介质管理”,我们可以看到Docker虚拟机用的虚拟硬盘的文件disk。 3.选中“disk”,然后点击菜单中的“复制”命令,根据向导,把当前的disk复制到另一个盘上面去。 4.回到VirtualBox主界面,右键“default”这个虚拟机,选择“设置”命令,在弹出的窗口中选择“存储”选项。 5.把disk从“控制器SATA”中删除,然后重新添加我们刚才复制到另外一个磁盘上的那个文件。 这是我设置好后的界面,可以看到我在步骤3复制的时候,复制到E:\VirtualBox\default\dockerdisk.vdi文件去了。 6.确定,回到PowerShell,我们使用docker-machine start default就可以启动新地址的Docker虚拟机了。确保新磁盘的虚拟机没有问题。就可以把C盘那个disk文件删除了。 【注意:不要在Window中直接去复制粘贴disk文件,这样会在步骤5的时候报错的,报错的内容如下,所以一定要在VirtualBox中去复制!】 Failed to open the hard disk file D:\Docker\boot2docker-vm\boot2docker-vm.vmdk. Cannot register the hard disk 'D:\Docker\boot2docker-vm\boot2docker-vm.vmdk' {9a4ed2ae-40f7-4445-8615-a59dccb2905c} because a hard disk C:\Users\用户名\.docker\machine\machines\default\disk.vmdk' with UUID {9a4ed2ae-40f7-4445-8615-a59dccb2905c} already exists. Result Code: E_INVALIDARG (0x80070057) Component: VirtualBox Interface: IVirtualBox {fafa4e17-1ee2-4905-a10e-fe7c18bf5554} Callee RC: VBOX_E_OBJECT_NOT_FOUND (0x80BB0001) 镜像加速 在国内使用Docker Hub的话就特别慢,为此,我们可以给Docker配置国内的加速地址。我看了一下,DaoCloud和阿里云的镜像加速还不错,另外还有网易的蜂巢。选一个就行了。以DaoClound为例,注册账号,然后在https://www.daocloud.io/mirror 就可以看到DaoClound提供给您的镜像加速的URL。然后到PowerShell中去依次执行: docker-machine ssh default sudo sed -i "s|EXTRA_ARGS='|EXTRA_ARGS='--registry-mirror=加速地址 |g" /var/lib/boot2docker/profile exit docker-machine restart default 这样重启Docker后就可以用国内的镜像来加速下载了。 试一下下载一个mysql看看快不快: docker pull mysql 下载完镜像,我们运行一个容器: docker run -d -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=123 mysql:latest 接下来我们打开windows下的mysql客户端,服务器地址填docker虚拟机的IP地址,通过docker-machine env可以看到,我这里是192.168.99.100,然后用户名root,密码123,这样我们就可以连接到docker容器里面的mysql了。 【注意,Docker容器是在VirtualBox的虚拟机里面,不是在Windows里面,所以不能用127.0.0.1访问】
ASP.NET Boilerplate(简称ABP)是在.Net平台下一个很流行的DDD框架,该框架已经为我们提供了大量的函数,非常方便与搭建企业应用。 关于这个框架的介绍我就不多说,有兴趣的可以参见官方文档:http://www.aspnetboilerplate.com/Pages/Documents 使用ABP+EF+SQL Server是比较推荐的组合,但是既然我们使用的是EF,那么就应该是和数据库分离的,也就意味着我们应该可以采用其他的数据库,比如MySQL。 ABP初始化的项目模板还提供了Module Zero项目,为我们提供了用户、角色、权限等等通用功能,但是在使用初始化的模板连接MySQL却会报错,无法运行,下面我来解决ABP+MySQL的问题。这是操作步骤: 1.从官方网站下载ABP项目模板,并解压到本地,用VS打开,这里我们新建一个项目ConnectMySql。 2.设置XXX.Web为启动项目,Build这个Solution,使得NuGet下载相关的包。 3.准备好一个SQL Server数据库,修改Web.config数据库的ConnectionString,连接到SQL Server数据库。 4.打开Package Manager Console窗口,选择XXX.EntityFramework为默认项目,运行Update-Database命令,系统会在SQL Server中创建数据库和对应的表。 5.打开SSMS,连接到上一步新建的数据库,选择生成脚本命令,并在高级选项中选择“Schema and data”生成一个创建表和填充数据的脚本文件。 6.接下来就是比较繁琐的SQL Server脚本转MySQL脚本了,我采用NotePad++,做了多次的批量替换,把脚本转换成MySQL支持的内容。 为了方便大家,我直接把转换好的SQL脚本放出,大家直接运行即可。 脚本下载 7.打开MySQL Server,新建一个MySQL数据库,并运行前一步骤准备好的脚本。 8.我们回到VS,为XXX.EntityFramework和XXX.Web,通过Nuget添加MySql.Data.Entity: 9.打开Web项目的Web.config,由于上一步添加了MySql.Data.Entity,所以Web.config已经添加了MySql的相关配置。我们只需要修改连接字符串,注释掉SQL Server的字符串,添加新的连接字符串: <add name="Default" providerName="MySql.Data.MySqlClient" connectionString="server=localhost;port=3306;database=test;uid=root;password=xxx" /> 10.打开EntityFramework项目的Configuration对象,在Migrations文件夹中,修改构造函数,指定使用MySQL的SQL生成器。 public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "ConnectMySql"; SetSqlGenerator("MySql.Data.MySqlClient", new MySql.Data.Entity.MySqlMigrationSqlGenerator()); } 11.现在我们来试一试是否可以针对MySQL使用EF的Code First。我们在XXX.Core中创建一个测试用的实体Student: public class Student:Entity { [StringLength(50)] public string Name { get; set; } public DateTime Birthdate { get; set; } public bool Gender { get; set; } } 12.在XXX.EntityFramework中找到ConnectMySqlDbContext,并添加Student的应用: public class ConnectMySqlDbContext : AbpZeroDbContext<Tenant, Role, User> { //TODO: Define an IDbSet for your Entities... public IDbSet<Student> Students { get; set; } /* NOTE: * Setting "Default" to base class helps us when working migration commands on Package Manager Console. * But it may cause problems when working Migrate.exe of EF. If you will apply migrations on command line, do not * pass connection string name to base classes. ABP works either way. */ public ConnectMySqlDbContext() : base("Default") { } …… 13.我们编译一下这个Solution,然后在Package Manager Console窗口中,输入命令Add-Migration AddStudent,这里的AddStudent是对我们这次更改的一个命令。命令运行完成后,在Migrations文件夹中,会创建升级数据库的C#代码。 14.继续在Package Manager Console窗口中,输入命令Update-Database,系统会将数据库更改应用到我们的MySQL数据库中。 15.我们到MySQL数据库中,刷新,可以看到系统自动创建了Students数据库: 16.最后,我们Build整个Solution,运行网站,可以看到我们网站正常运行了。
我在很久之前的一篇文章中介绍了数据库模型设计中的基本三范式,今天,我来说一说更高级的BC范式和第四范式。 回顾 我用大白话来回顾一下什么是三范式: 第一范式:每个表应该有唯一标识每一行的主键。 第二范式:在复合主键的情况下,非主键部分不应该依赖于部分主键。 第三范式:非主键之间不应该有依赖关系。 这是我们设计数据库的基本规则,但是只有这三个规则并不能完全解决数据的增删改的异常情况,下面就来看看BC范式的例子。 BC范式 BC范式(BCNF)是Boyce-Codd范式的缩写,其定义是:在关系模式中每一个决定因素都包含候选键,也就是说,只要属性或属性组A能够决定任何一个属性B,则A的子集中必须有候选键。BCNF范式排除了任何属性(不光是非主属性,2NF和3NF所限制的都是非主属性)对候选键的传递依赖与部分依赖。 比如我们有一个学生导师表,其中包含字段:学生ID,专业,导师,专业GPA,这其中学生ID和专业是联合主键。 StudentId Major Advisor MajGPA 1 人工智能 Edward 4.0 2 大数据 William 3.8 1 大数据 William 3.7 3 大数据 Joseph 4.0 这个表的设计满足三范式,有主键,不存在主键的部分依赖,不存在非主键的传递依赖。但是这里存在另一个依赖关系,“专业”函数依赖于“导师”,也就是说每个导师只做一个专业方面的导师,只要知道了是哪个导师,我们自然就知道是哪个专业的了。 所以这个表的部分主键依赖于非主键部分,那么我们可以进行以下的调整,拆分成2个表: 学生导师表: StudentId Advisor MajGPA 1 Edward 4.0 2 William 3.8 1 William 3.7 3 Joseph 4.0 导师表: Advisor Major Edward 人工智能 William 大数据 Joseph 大数据 第四范式 如果满足了BC范式,那么就不再会有任何由于函数依赖导致的异常,但是我们还可能会遇到由于多值依赖导致的异常。 比如我们建立课程教师和教材的模型,我们规定,每门课程有对应的一组教师,每门课程也有对应的一组教材,一门课程使用的教程和教师没有关系。这样我们首先肯定有三个实体表,分别表示课程,教师和教材。现在我们要建立这三个对象的关系,于是我们建立的关系表,定义如下: 课程ID,教师ID,教程ID;这三列作为联合主键。 以下是示例,为了表述方便,我们用Name代替ID,这样更容易看懂: Course Teacher Book 英语 Bill 人教版英语 英语 Bill 美版英语 英语 Jay 美版英语 高数 William 人教版高数 高数 Dave 美版高数 这个表除了主键,就没有其他字段了,所以肯定满足BC范式,但是却存在多值依赖导致的异常。 我们先来看看多值依赖的定义: 一个关系,至少存在三个属性(A、B、C),才能存在这种关系。对于每一个A值,有一组确定的B值和C值,并且这组B的值独立于这组C的值。 假如我们下学期想采用一本新的英版高数教材,但是还没确定具体哪个老师来教,那么我们就无法在这个表中维护Course高数和Book英版高数教材的的关系。 解决办法是我们把这个多值依赖的表拆解成2个表,分别建立关系。这是我们拆分后的表: Course Teacher 英语 Bill 英语 Jay 高数 William 高数 Dave Course Book 英语 人教版英语 英语 美版英语 高数 人教版高数 高数 美版高数 第四范式的定义很简单:已经是BC范式,并且不包含多值依赖关系。 除了第四范式外,我们还有更高级的第五范式和域键范式(DKNF),第五范式处理的是无损连接问题,这个范式基本没有实际意义,因为无损连接很少出现,而且难以察觉。而域键范式试图定义一个终极范式,该范式考虑所有的依赖和约束类型,但是实用价值也是最小的,只存在理论研究中。
我在很久之前的一篇文章中介绍了数据库模型设计中的基本三范式,今天,我来说一说更高级的BC范式和第四范式。 回顾 我用大白话来回顾一下什么是三范式: 第一范式:每个表应该有唯一标识每一行的主键。 第二范式:在复合主键的情况下,非主键部分不应该依赖于部分主键。 第三范式:非主键之间不应该有依赖关系。 这是我们设计数据库的基本规则,但是只有这三个规则并不能完全解决数据的增删改的异常情况,下面就来看看BC范式的例子。 BC范式 BC范式(BCNF)是Boyce-Codd范式的缩写,其定义是:在关系模式中每一个决定因素都包含候选键,也就是说,只要属性或属性组A能够决定任何一个属性B,则A的子集中必须有候选键。BCNF范式排除了任何属性(不光是非主属性,2NF和3NF所限制的都是非主属性)对候选键的传递依赖与部分依赖。 比如我们有一个学生导师表,其中包含字段:学生ID,专业,导师,专业GPA,这其中学生ID和专业是联合主键。 StudentId Major Advisor MajGPA 1 人工智能 Edward 4.0 2 大数据 William 3.8 1 大数据 William 3.7 3 大数据 Joseph 4.0 这个表的设计满足三范式,有主键,不存在主键的部分依赖,不存在非主键的传递依赖。但是这里存在另一个依赖关系,“专业”函数依赖于“导师”,也就是说每个导师只做一个专业方面的导师,只要知道了是哪个导师,我们自然就知道是哪个专业的了。 所以这个表的部分主键依赖于非主键部分,那么我们可以进行以下的调整,拆分成2个表: 学生导师表: StudentId Advisor MajGPA 1 Edward 4.0 2 William 3.8 1 William 3.7 3 Joseph 4.0 导师表: Advisor Major Edward 人工智能 William 大数据 Joseph 大数据 第四范式 如果满足了BC范式,那么就不再会有任何由于函数依赖导致的异常,但是我们还可能会遇到由于多值依赖导致的异常。 比如我们建立课程教师和教材的模型,我们规定,每门课程有对应的一组教师,每门课程也有对应的一组教材,一门课程使用的教程和教师没有关系。这样我们首先肯定有三个实体表,分别表示课程,教师和教材。现在我们要建立这三个对象的关系,于是我们建立的关系表,定义如下: 课程ID,教师ID,教程ID;这三列作为联合主键。 以下是示例,为了表述方便,我们用Name代替ID,这样更容易看懂: Course Teacher Book 英语 Bill 人教版英语 英语 Bill 美版英语 英语 Jay 美版英语 高数 William 人教版高数 高数 Dave 美版高数 这个表除了主键,就没有其他字段了,所以肯定满足BC范式,但是却存在多值依赖导致的异常。 我们先来看看多值依赖的定义: 一个关系,至少存在三个属性(A、B、C),才能存在这种关系。对于每一个A值,有一组确定的B值和C值,并且这组B的值独立于这组C的值。 假如我们下学期想采用一本新的英版高数教材,但是还没确定具体哪个老师来教,那么我们就无法在这个表中维护Course高数和Book英版高数教材的的关系。 解决办法是我们把这个多值依赖的表拆解成2个表,分别建立关系。这是我们拆分后的表: Course Teacher 英语 Bill 英语 Jay 高数 William 高数 Dave Course Book 英语 人教版英语 英语 美版英语 高数 人教版高数 高数 美版高数 第四范式的定义很简单:已经是BC范式,并且不包含多值依赖关系。 除了第四范式外,我们还有更高级的第五范式和域键范式(DKNF),第五范式处理的是无损连接问题,这个范式基本没有实际意义,因为无损连接很少出现,而且难以察觉。而域键范式试图定义一个终极范式,该范式考虑所有的依赖和约束类型,但是实用价值也是最小的,只存在理论研究中。【本文章出自博客园深蓝居,转载请注明作者出处,如果您觉得博主的文章对您有很大帮助,欢迎点击右侧打赏按钮对博主进行打赏。】打个招聘广告,博主正在主导开发一个跨链区块链项目:PalletOne,一直在招Go程序员,待遇丰厚,坐标北京酒仙桥,希望有识之士加入!
题出自https://leetcode.com/problems/rotate-image/ 内容为: You are given an n x n 2D matrix representing an image. Rotate the image by 90 degrees (clockwise). Follow up: Could you do this in-place? 简单的说就是给出一个n*n的二维数组,然后把这个数组进行90度顺时针旋转,而且不能使用额外的存储空间。 最初拿到这道题想到的就是找出每个坐标的旋转规律。假设我们是2*2的矩阵: a b c d 进行旋转后,那么就变成了: c a d b 所以就转换成对4个数字进行轮换,而不使用额外空间的问题。最常用的交换数值而不使用额外空间的算法就是异或,比如要交换a,b的值,那么可以写为: a=a^b; b=a^b; a=a^b; 现在是对4个数字进行轮换,轮换后的结果为a=c,b=a,c=d,d=b; 所以改写成异或的算法,那么就是: a = a ^ b ^ c ^ d; b = a ^ b ^ c ^ d; d = a ^ b ^ c ^ d; c = a ^ b ^ c ^ d; a = a ^ b ^ c ^ d; 接下来就是找出二维数组中角标与a,b,c,d的关系,这个其实不难。另外,我们在进行旋转处理时,我们只需要处理1/4的区域即可,因为处理一次就是调整了4个数,所以我们只处理二维数组中左上角的数值。 下面就是具体的代码: public void Rotate(int[,] matrix) { int n = matrix.GetLength (0); for (var i = 0; i < (n + 1)/2; i++) { for (var j = 0; j < n/2; j++) { //var a = matrix[i, j]; //var b = matrix[j, n - i - 1]; //var d = matrix[n - i - 1, n - j - 1]; //var c = matrix[n - j - 1, i]; matrix[i, j] = matrix[i, j] ^ matrix[j, n - i - 1] ^ matrix[n - i - 1, n - j - 1] ^ matrix[n - j - 1, i]; matrix[j, n - i - 1] = matrix[i, j] ^ matrix[j, n - i - 1] ^ matrix[n - i - 1, n - j - 1] ^ matrix[n - j - 1, i]; matrix[n - i - 1, n - j - 1] = matrix[i, j] ^ matrix[j, n - i - 1] ^ matrix[n - i - 1, n - j - 1] ^ matrix[n - j - 1, i]; matrix[n - j - 1, i] = matrix[i, j] ^ matrix[j, n - i - 1] ^ matrix[n - i - 1, n - j - 1] ^ matrix[n - j - 1, i]; matrix[i, j] = matrix[i, j] ^ matrix[j, n - i - 1] ^ matrix[n - i - 1, n - j - 1] ^ matrix[n - j - 1, i]; } } } 使用异或并不是很直观,另外一个比较直观的交换两个数据的方法是加减法: a=a+b; b=a-b; a=a-b; 我们使用异或而不使用更直观的加减法是因为a+b的时候可能溢出,那么接下来的结果就不对了,所以不能用加减法而应该用异或。
最近在培训PowerShell,在讲到Pipeline的时候,对于我这种长期和数据(数据库)打交道的人来说,觉得很实用,所以写此博文,记录一下。 无论是在Linux中写Bash脚本还是在Window上写PowerShell,管道符”|“是一个非常有用的工具。它提供了将前一个命令的输出作为下一个命令的输入的功能。在数据处理中,我们也可以使用管道符对数据进行各种操作。 Import&Export导入导出 先说导入导出是为了能够为接下来的数据处理准备数据。在PowerShell中我们也可以通过各种Get-XXX命令获得各种各样需要的数据,但是并不是所有操作系统和各个版本的PowerShell都支持某个命令的。比如Get-Volume命令,用于获得每个磁盘的信息,但是这个命令不能在Win7下运行,只能在Win8或Win2012Server下运行。 最常见,最简单的外部数据源就是CSV文件了。我们可以使用Export-Csv命令将PowerShell中的对象转换为CSV格式,持久化到磁盘上。比如我们将当前的所有进程信息导出为CSV文件,命令为: Get-Process | Export-Csv C:\test.csv -Encoding Unicode (注意,如果是有中文内容建议设置Encoding为Unicode或者UTF8) Import-Csv命令是导入外部的CSV文件到内存。比较刚才导出的CSV文件,我们接下来要对这个文件进行处理。我们可以将文件的内容保存到变量$data中。命令为: $data=Import-Csv C:\test.csv -Encoding Unicode 当然,我们也可以先进行类型转换,然后保存。命令为: $data | ConvertTo-Csv | Out-File C:\test.csv -Encoding utf8 Sorting排序 前面我们已经将CSV的内容载入到$data变量中了,那么如果我们要按照某一个字段排序,可以使用Sort-Object命令。 比如我们要Name这个字段排序,并输出排序后的结果,那么命令为: $data | Sort-Object Name 也可以简写为: $data | Sort Name 如果是需要多个字段排序,那么可以将字段列在后面,字段之间用逗号隔开。 $data | Sort Name,Handles 如果是逆向排序,那么需要在字段后面加参数-Descending $data | Sort Name –Descending Selecting选取 选取相当于SQL中的SELECT命令。对应的PowerShell命令是Select-Object,可以简写为Select。该命令后面跟上要选取的列名即可。如果是要选取所有的列,也可以使用*表示。 $data | select Name,VM 选取所有列,那么命令就是: $data | select * 如果是只选取前面几条数据,那么可以使用-First参数。比如我们按Handles排序,只查看头10条进程记录的名字和Handles。命令为: $data | sort Handles | select Handles,Name -First 10 另外还有参数-Last选取的是最后几条记录,-Skip可以选择跳过一定记录。 Calculate计算列 在SELECT的时候,我们可以使用函数对其中的列进行运算,使用的语法是: @{ n='New Column Name'; e={ $_.xxxCalc } } 其中的$_就是表示当前的记录。 比如VM列记录的是以Byte为单位的数据,我们先新建一列名为”VM(MB)”,其值是换算成MB的结果,那么我们可以写为: $data | select Name,VM,@{n="VM(MB)";e={$_.VM/1MB}} Measuring度量 说度量可能有点不是很清晰,其实就是对应SQL中的聚合函数。比如 SUM, Max,Min之类的,需要使用Measure-Object命令。比如要查看有多少个程序,最小的Handles和最大的Handles,那么命令是: $data | Measure-Object -Property Handles -Minimum -Maximum 既然说到SQL中的聚合函数,那么自然就会想到另外一个关键字Group By。在PowerShell中也有对应的命令Group-Object。如果我们想要按进程的Name进行分组,查看每个进程名对应的VM总大小。那么我们可以先按Name进行Group: $data | Group-Object Name 这时我们可以看到系统返回的结果有3列:Count,Name,Group。而我们要进行聚合的VM值是在Group中。这时需要用到前面提到的Select命令。 $data | Group-Object Name | select Name,Count,@{n="TotalVM";e={($_.Group | Measure-Object -Property VM -Sum).Sum}} Filter过滤 过滤相当于SLQ中的Where语句,在PowerShell中使用Where-Object命令。可以简写为Where,甚至可以简写为”?”。在普通程序里面我们遇到的比较运算和逻辑运算在PowerShell中有所不同,是这样的参数: Comparison Case-InSensitive Case-sensitive Equality -eq -ceq Inequality -ne -cne Greater than -gt -cgt Less than -lt -clt Greater than or equal to -ge -cge Less than or equal to -le -cle Wildcard equality -like -clike -and 和-or用于逻辑运算。 仍然以前面load的$data为例,我们要查看以W开头的进程的Handles和Name,那么命令为: $data | ?{ $_.Name -like 'W*'}| select Handles,Name 如果是多个条件,既要以w开头,还要VM大于100M的进程,那么命令为: $data | ?{ $_.Name -like 'W*' -and $_.VM -gt 100MB}| select Handles,Name,VM Enumeration枚举 枚举相当于C#中的Foreach函数,或者说是SQL中的游标,对于每一行数据,都进行一个运算或者函数处理。在PowerShell中对应的命令是ForEach-Object,可以简写为ForEach,还可以进一步简写为”%“。 比如我们要将VM改为MB为单位,可以对每一行数据进行运算: $data | % {$_.VM=$_.VM/1MB} 运行该命令后我们再查看$data就会发现VM列已经改变了。 $data | select Name,VM 另外对于Foreach命令,还有两个比较有用的参数-Begin –End,用于在做For循环之前调用和循环结束后调用。 比如我们想把某一列写入一个文件,我们可以在-Begin时创建文件,记录开始的时间,然后Foreach中Append内容到文件,最后把结束时间写入: $data | % -Begin { Get-Date | Out-File C:\test.txt } -Process { $_.Name | Out-File C:\test.txt -Append} -End { Get-Date | Out-File C:\test.txt -Append}
PowerShell是一个面向对象的语言,在申明变量的时候不强制要求申明数据类型,使用$开头来申明变量即可。 基本数据类型 PowerShell本身是基于.Net开发出来的,所以在.Net中的基本数据类型,在PowerShell中也可以使用,只是在PowerShell中用”[]”来标识具体的数据类型。比如[int],[long],[string],[bool],[double]等。 使用-is来判断某个变量是否指定的数据类型,和C#中的is关键字是一样的。比如: $a=10; $a -is [int] $a -is [double] 第二行返回True,第三行返回False。 我们也可以在定义变量时指定数据类型。比如我们要定义decimal类型的10,那么可以写为: [decimal]$c=10 $c.GetType() 可以看到我们的类型为Decimal。 数据类型转换 关于默认数据类型转换,PowerShell和C#的行为有所不同。PowerShell会根据第一个变量的类型作为目标类型,然后将运算后面的联系转换为第一个类型。比如我们申明两个变量: $a=10; $b="10"; 如果我们使用加法运算: $a+$b 该运算会返回20,因为第一个变量是int类型的,所以后面的变量都会转换为int类型。如果我们调整变量的顺序: $b+$a 该运算返回的结果为1010,因为第一个变量是string类型。习惯了C#的默认类型转换,那么我们可以强制进行类型转换后再进行运算。强制类型转换的方法也是与C#相同。 比如我们要按int类型来计算,那么我们第二个加法可以改为: [int]$b+$a 我们也可以使用-as命令,那么第二个加法改为: ($b -as [int])+$a List/Array类型 我们平时使用的各种Get-XXX命令,很多都是返回对应类型的Array,比如Get-Process. 如果我们要定义一个集合,那么可以使用”@(对象1,对象2,对象3…)”的格式申明集合。比如我们定义一个字符串集合: $a=@('a','bb','ccc') 其实不使用@和括号也是可以的,只需要用逗号分割各个Item即可。 $a='aaa','bb','c' 如果是申明一个空的集合,就必须写为: $a=@() 与C#不同的是,在PowerShell中,往集合中添加元素,可以使用+=符号。 $a=@() $a+="abc" $a+="dddd" Write-Host $a 如果要移除某个元素,那么就不简单了,需要使用Where查询(简写为?),找出要保留的元素,然后将保留的元素集合再重新赋值会变量。比如对于字符串集合,我们要移除字符c,那么操作如下: $a=@('aaa','bb','c') $a= $a | ? {$_ -ne 'c'} Write-Host $a 访问某个元素使用[idx]即可和C#相同。至于对集合的各种操作,可以参见我上一篇博文。 Hashtable/Dictionary类型 哈希表就是一个Key-Value对的集合。哈希表的创建格式如下: @{Key1=Value1;Key2=Value2;…} 这里Key一般是字符串,但是并不需要用引号引起来(当然,使用了引号更好),Value可以是任意类型。比如我们创建一个员工和部门的Hashtable,命令如下: $a=@{Devin="IT";Edward="Finance";Jeneen="Sale"} 使用keys属性可以获得哈希表的Key列表,使用values属性可以获得Value列表。 如果要往哈希表中添加元素,可以使用.Add(Key,Value)方法。比如添加一个员工: $a.Add("Julia","Logisitcs") 这里需要注意的是Key必须要带引号。而且哈希表的Key是不允许重复的,如果已经存在相同的值,添加会报错。可以先判断Key是否存在,然后再添加: if(-not $a.ContainsKey("Julia")) { $a.Add("Julia","Logisitcs") } 如果要移除某个Key对应的元素,那么可以调用.Remove(Key)函数即可。 $a.Remove("Julia") 如果要访问某个Key对应的值,有两种方法: $a["Devin"] $a.Devin 需要注意的是,直接对哈希表进行Sort-Object是没有效的,我们必须先调用GetEnumerator方法,把哈希表转换后在执行Sort。 $a.GetEnumerator() | Sort-Object Name
前一篇博客我已经把各个实体分析了一遍,从分析中可以看到,这个公司是做本地采购,生产,然后通过网站和门店进行国际销售的。所以这里会涉及到一些国际化的问题。接下来就来分析一下有哪些国际化需要注意的问题和数据库模型中的解决方案。 语言 AdventureWorks数据模型中,只有对ProductDescription进行了多语言设置。关于多语言的建模,我曾经写了一篇文章,详细介绍了多语言建模的几种方法,可以参考:http://www.cnblogs.com/studyzy/archive/2013/04/03/2998322.html 回过头来看看AdventureWorks是怎么处理多语言的。 他在ProductDescription的Description字段中用各种语言维护了一些描述信息,但是却并没有任何一个字段说明这一行维护的是什么语言。反而时建立了ProductDescription和Culture的多对多关系。这是一个很奇怪的设计,Culture和ProductDescription应该是一对多的关系,一种语言会维护很多句描述信息,而具体的一条描述信息,在写入Description的时候就应该已经确定了唯一的一种语言。比如我随便找了一行描述“充电式双光束车灯。”这行描述会对应英语?法语?会是对应多种语言吗?而实际的查询也证明了我这个观点: SELECT [ProductDescriptionID] ,count([CultureID]) as CultureCountFROM [Production].[ProductModelProductDescriptionCulture]group by ProductDescriptionIDhaving count(CultureID)>1 返回0行,不会有哪个ProductDescription会去对应多个Culture。 那正确的模型应该是什么样的呢?简单的改法是:Culture和ProductDescription是一对多关系,ProductModel和ProductDescription是多对多关系,如下图所示: 这种模型可以使得每一行的Description在定义输入的时候就指定了输入的到底是什么语言。但是这个模型有一个缺点,就是同一个ProductModel,在不同的语言情况下可能对应的描述不一致。比如有产品样品A,他关联的中文描述是: 描述1 描述2 而他关联的英文描述是: Description 2 Description 3 Description 4 所以这个模型引用的多语言描述可能是混乱的。那么我们可以进一步改进模型。需要增加一个表ProductStandardDescription,这个表中维护了最常用的语言的描述,比如里面维护了英文描述。然后ProductStandardDescription和Culture形成多对多关系,把除了英文外的其他语言的描述信息翻译好维护进去。最后ProductStandardDescription和ProductModel也是一个多对多关系。 这样的好处是可以避免前面提到的不同语言关联的描述不一致的问题,而且以英语为标准描述,可以很容易找到对应的其他语言是否存在,不存在的话就使用默认语言英语。 格式 格式是一个在应用程序中需要注意的问题,主要是对日期和数字的显示格式处理。在数据库建模中,为了避免格式问题,不要用字符串类型去存储日期时间和数字。如果知道是日期类型的那么就使用Date类型,如果是要包含日期和时间的,那么就用datetime类型,只需要记录时间就用time类型。而对于数字或者是金额,也一定要使用对应的数字类型int decimal和金额类型money。 如果使用varchar来存储日期会出现什么问题呢?美国用户在输入日期时使用的格式是MM/dd/yy,而中国用户习惯的输入格式是yyyy-MM-dd或者yyyy/MM/dd,到英国又不一样,而这些格式存储到了数据库中,那么将无法进行日期大小的比较,而且在展示的时候也按原来用户输入的格式再展示给另外一个国家的用户,那么很可能引起误解。 时间类型也有类似的问题,美国用户喜欢使用AM PM来表示上午下午,而中国用户使用24小时制,按字符串存储到数据库中也会存在无法正确排序和比较的问题。 数字的字符串问题在于有的使用逗号作为千分符,而有的国家是使用顿号作为千分符,有的用户又不使用千分符。 金额的话有的输入前面会带货币符号,有的又不带。 回过头来看AdventureWorks数据库模型,他对数据类型的使用都很准确,不存在乱用varchar类型的问题。 币种和汇率 如果在某个表单中涉及到多个币种问题,那么必须将币种属性添加到表单中。比如SalesOrderHeader。而对于采购订单PurchaseOrder中,由于都是本国采购,全部采用美元结算,所以在PurchaseOrder中没有币种字段。这里需要注意的是,并不是只要某个表单中只采用一种币种,就不需要记录币种信息了,表单的币种如果与财务核算的币种不一致,那么在财务做账时就得进行币种转换,这里由于采购的币种和财务核算的币种都是美元,所以才不需要记录币种。 我们在系统中记录币种信息主要是为了实现币种转换,而币种转换的关键就是汇率,而汇率是一个很复杂的东西,因为汇率是随着时间不断变化的。在系统进行汇率转换时应该取哪个汇率呢?两个币种的汇率存在买入汇率、卖出汇率、中间汇率等,基本介绍可以参考百度文档:http://wenku.baidu.com/link?url=MP0nC_0sIGEIlAfbr-rWSSKcE_bmqQrNclr80WHDfc4kAFZY6S9dskMt5PzPDzkm88iseIhGDhAz9SZEnoQVvtAIRVo13p1kFpTGnCEUVqe 汇率的时间取值可以是日汇率,月底中心汇率,月平均汇率,年度预算汇率等,一般系统都是以“日”为单位,每日记录一次汇率信息,使用中间汇率进行计算的较多。从以上的分析,我们可以建立汇率转换表如下: 接下来在记录币种信息时有以下几种建模方法: 1. 完全范式化,只记录交易币种 范式化后汇率表我们可以变成多个表,而在表单中只记录交易币种,那是因为我们可能有多种核算方法,比如同一个系统,欧洲区希望以欧元为币种看到报表,总公司希望看到美元为币种的报表,那么转换后的币种是不确定的,所以不需要记录转换后币种,我们只需要在表单中记录原币种和日期,剩下的就由系统计算得到。 这样做后系统是最灵活的,可以应对各种情况,交易时的真实币种为LocalCurrency,可以对应CurrencyRate中的FromCurrency,OrderDate就是CurrencyRateDate,而核算币种StatisticsCurrency(本位币)由系统输入,可以对应CurrencyRate中的ToCurrency,最后再由系统带人CurrencyRateType即可知道本位币的金额。但是由于范式化,也是最慢的,因为在出报表时需要进行多个表的Join,才能得到结果。 2.固定本位币,将汇率类型以列展示 毕竟对于大多数公司来说,核算时永远是用一种固定的本位币(比如美元)这也是AdventureWorks所采取的方式,那么我们可以将系统模型简化,也就是说CurrencyRate表中没有CurrencyRateType栏位,如果要记录日平均汇率进记入“AverageRate”字段,如果要记录当天的最后中间汇率,那么就记入“EndOfDayRate”中,如果我们还想记录更多类型的汇率值,只需要在这个表中增加栏位即可。我们再来看看AdventureWorks系统的模型: SalesOrder在录入时就已知了LocalCurrency和CurrencyRateDate(一般也就是OrderDate),而我们的本位币是固定的USD,所以基于这3个字段就唯一的确定了CurrencyRate中的一条记录。所以在SalesOrder中并没有记录LocalCurrency,而是引用CurrencyRateID即可。这种模型虽然不会像上一种方案那么灵活,但是由于只需要Join一张表,所以查询效率比上面会快很多。 3. 固定本位币,固定汇率类型,反范式化 如果我们公司不仅本位币是唯一的,而且采用的汇率类型也是固定的(只使用每人中间汇率),那么我们的模型可以出于效率的考虑,进一步反范式化: 每个在记录金额的字段,都加上对应的货币字段,同时也加上按当时的汇率换算成本位币后的金额。 这样做的特点是在后期做各种财务统计时不需要在进行表的链接也不需要进行汇率换算,在录入数据时都已经计算好,所以统计速度会很快。当然缺点也是显而易见的,一方面时反范式化后带来的缺点,另一方面是可能在录入数据时并不知道当时的汇率,所以本位币金额就无法计算,只有等公布了当天的汇率后再去补上本位币金额,这是相当麻烦的。还有一个缺点是本位币金额在计算时取的是中间汇率,如果哪天我们希望再以月平均汇率来统计本位币金额,那么还是得重新计算。 时间 时间问题主要是时区的问题,是个比较头疼的问题,在中国可能意识不到这个问题,因为整个中国都是实行+8区的统一时区,而像俄罗斯、美国等国家,他们从东部到西部都是实行不同的时区,而我们的IT系统如果是国际化的,那么就更会遇到时区问题。 时区 时区问题分为录入和显示两个方向。比如一个中国的客户,在2015-5-2早上9:00在系统中下了一个单,那么我们系统记录的是什么时间呢?直接计入2015-5-2 9:00:00吗?但是对于伦敦的管理员来说,他看到这个时间就会很奇怪,因为他们现在才2015-5-2 1:00:00啊,怎么会有未来的单子。所以我们必须将时区作为一个日期的部分,考虑到数据库的模型中。 关于时区,我们可以有两种解决方案: 1.直接记录时区到数据库 为了解决时区问题,SQL Server数据库专门提供了一个数据类型DateTimeOffset,以及相关的函数,用于处理带时区的日期时间。比如我们前面说到的订单日期,我们就可以将其数据类型改为datetimeoffset。而获取时间和时区,有三种数据源: 客户端时间 应用服务器时间 数据库服务器时间 如果使用用户输入的时间值,那么就必须在读取用户输入的值的同时,也得读到用户电脑所设置的时区(这个无论是BS才是CS应用都很容易读取到),然后把时间和时区两个值传到后台数据库,最终以DatetimeOffset的格式存储到数据库中。如果是只需要取当前时间,那么就可以取应用服务器或者数据库服务器的时间和时区。数据库函数为:SYSDATETIMEOFFSET ( ) 如果我们使用的数据库没有DateTimeOffSet这种数据类型,那么我也可以将来DateTime和Timezone两个字段来存储时间和时区。 2.转换为UTC时间存到数据库 前面说到我们可以建立Timezone字段来存储时区信息,但是这样做最大的缺点就是不方便数据库中的排序。而且新建的字段也会额外占用存储空间,导致性能降低。除了使用专门为时区而设计的DateTimeOffset数据类型外,我们还可以在应用程序中做时区转换,把所有时间都转换为UTC时间,然后在数据库中就存储UTC时间。 如果是用户从客户端输入或者是应用程序服务器时间,那么只需要在代码中调用系统函数做个简单的转换即可,如果是取数据库当前UTC时间,也有GETUTCDATE()函数可以用。 上面说到的都是录入,而在显示上面,应用程序也只需要读取客户端的时区,然后将数据库中的时间以新时区展示即可。 夏令时 夏令时问题也是一个在中国意识不到的问题,因为中国现在不实行夏令时制(以前实行过几年,后来取消了)。夏令时问题和时区问题独立出来,那是因为我们的系统可能并不是一个国际化的系统,只在一个时区使用,但是这个国家实行了夏令时制,那么我们就需要考虑夏令时给系统带来的影响。 如果我们的系统记录数据库服务器的时间为订单创建的时间,那么在凌晨1:59创建了一个订单,然后一分钟后由于夏令时时间调整,接下来下的订单就变成了1:00创建的了,这个时候如果我们按创建订单的时间进行排序就会有问题,明明先创建的订单,结果却排在了后创建订单的后面。 解决办法很简单,也是就跟时区问题一样,我们可以把时间转换为UTC时间再存储。 度量衡 度量衡问题是在国际化过程中遇到的最麻烦的问题,因为度量衡的东西太多(长度、面积、体积、温度重量……)而其相互之间的换算还不一样(华氏度和摄氏度有个换算公式,磅和千克又有一个公式,米和英里、英尺、英尺又是不同的公式)这些足够让人抓狂。所以现在大部分系统都回避这个问题,只使用系统录入的时候输入的值和选择的单位。也就是说如果用户在系统中录入一件商品重10磅,那么所有用户看到的都是10磅,对于中国用户来说,根本不知道10磅是啥概念,只有自己去百度磅和斤怎么换算,然后自己拿个计算器算一下到底有几斤重。 AdventureWorks的模型中,只是使用UnitMeasure表存储了系统中用到的度量衡的单位名称,并没有涉及到度量衡的转换问题。假如我们要啃度量衡这块硬骨头,那我们的数据库模型该怎么设计呢? 首先,我们在数据库尽量要以国际标准的度量衡单位为准,存储数值(温度就用摄氏度做标准单位就行了,没必要用开做单位)。然后建立度量衡表,里面设置了以下栏位:单位名称,单位符号,换算方法(乘以倍率,套用换算公式,查询换算表)与标准单位的换算倍率,与标准单位的换算公式/函数(有些单位的换算不是简单的乘以一个倍数就能搞定的,摄氏度和华氏度就是个特例)。换算表表名(比如鞋子尺码的换算,我们可以通过查表获得)具体单位换算规则我们可以参考:http://baike.baidu.com/view/43851.htm 因为很少有系统在国际化中涉及到度量衡换算问题,所以我接下来举一个具体的例子,说明我这个模型的可行性。 我们以一个重量,一个温度和一个鞋码为例子。 先按模型创建表: create table StandardUnit ( StandardUnitCode varchar(10) not null, StandardUnitName nvarchar(10) not null,constraint PK_STANDARDUNIT primary key (StandardUnitCode) )gocreate table Test ( TestId int identity not null, StandardUnitCode varchar(10) not null, TestValue decimal(18,2) not null,constraint PK_TEST primary key (TestId) )goalter table Testadd constraint FK_TEST_RELATIONS_STANDARD foreign key (StandardUnitCode)references StandardUnit (StandardUnitCode)gocreate table UnitMeasure ( UnitId int identity not null, StandardUnitCode varchar(10) not null, UnitName nvarchar(10) not null, UnitCode varchar(10) not null, ConvertType varchar(50) not null, ConvertRate double precision null, ConvertFunction varchar(50) null, ConvertTable varchar(50) null,constraint PK_UNITMEASURE primary key (UnitId) )goalter table UnitMeasureadd constraint FK_UNITMEAS_RELATIONS_STANDARD foreign key (StandardUnitCode)references StandardUnit (StandardUnitCode)gocreate table ShoeSize ( StandardValue decimal(10,2) not null, ToCode varchar(10) not null, ToValue decimal(10,2) not null,constraint PK_SHOESIZE primary key (StandardValue, ToCode) ) go 接下来我们初始化一些数据: insert into StandardUnit values('Kg',N'千克'),('C',N'摄氏度'),('OS',N'欧码');insert into UnitMeasure values('C',N'华氏度','F','Function',null,'dbo.ConvertC2F',null);insert into UnitMeasure values('OS',N'美码','US','Table',null,null,'dbo.ShoeSize');insert into Test values('Kg',0.5),('C',100),('OS',43);insert into ShoeSize values(41,'US',8.5),(42,'US',9),(43,'US',9.5); 这里面涉及到一个温度转换函数,我们需要创建数据库函数dbo.ConvertC2F: create function dbo.ConvertC2F (@c decimal(10,2) )returns decimal(10,2)asbeginreturn @c*1.8+32;end 好了,一切准备就绪,现在写一个SQL,把所有Test中的值,转换为磅,华氏度,美码显示出来,那么我们的SQL写为: select t.TestId,um.UnitName,t.TestValue*ConvertRate as NewValuefrom Test tinner join UnitMeasure umon t.StandardUnitCode=um.StandardUnitCodewhere um.ConvertType='Rate'union allselect t.TestId,um.UnitName,dbo.ConvertC2F(t.TestValue) as NewValuefrom Test tinner join UnitMeasure umon t.StandardUnitCode=um.StandardUnitCodewhere um.ConvertType='Function'union allselect t.TestId,um.UnitName,ss.ToValue as NewValuefrom Test tinner join UnitMeasure umon t.StandardUnitCode=um.StandardUnitCodeinner join dbo.ShoeSize sson t.TestValue=ss.StandardValue and ss.ToCode=um.UnitCode where um.ConvertType='Table' 这里由于Function和Table是动态配置的,所以这个SQL是程序先读取了UnitMeasure中的值,然后动态生成的。如果不依靠程序动态生成SQL,我们可以修改模型,去掉ConvertFunction和ConvertTable两个字段,写死一个固定的函数和查找表,毕竟需要用到转换函数的,我目前也就找到了温度,其他基本上都是乘以一个系数就搞定。下面我们就来看看改进后的模型: 对应的SQL为: create table StandardUnit ( StandardUnitCode varchar(10) not null, StandardUnitName nvarchar(10) not null,constraint PK_STANDARDUNIT primary key (StandardUnitCode) )gocreate table Test ( TestId int identity not null, StandardUnitCode varchar(10) not null, TestValue decimal(18,2) not null,constraint PK_TEST primary key (TestId) )gocreate table UnitMeasure ( UnitCode varchar(10) not null, StandardUnitCode varchar(10) not null, UnitName nvarchar(10) null, ConvertType varchar(50) null, ConvertRate double precision null,constraint PK_UNITMEASURE primary key (UnitCode) )gocreate table UnitValueMapping ( StandardUnitCode varchar(10) not null, StandardValue decimal(18,2) not null, UnitCode varchar(10) not null, ToValue decimal(18,2) null,constraint PK_UNITVALUEMAPPING primary key (StandardUnitCode, UnitCode, StandardValue) )goalter table Testadd constraint FK_TEST_RELATIONS_STANDARD foreign key (StandardUnitCode)references StandardUnit (StandardUnitCode)goalter table UnitMeasureadd constraint FK_UNITMEAS_RELATIONS_STANDARD foreign key (StandardUnitCode)references StandardUnit (StandardUnitCode)goalter table UnitValueMappingadd constraint FK_UNITVALU_RELATIONS_STANDARD foreign key (StandardUnitCode)references StandardUnit (StandardUnitCode)goalter table UnitValueMappingadd constraint FK_UNITVALU_RELATIONS_UNITMEAS foreign key (UnitCode)references UnitMeasure (UnitCode)go 初始化的数据也差不多,这里就不需要再写初始化脚本了,我们来看看转换语句: select t.TestId,um.UnitName,t.TestValue*ConvertRate as NewValuefrom Test tinner join UnitMeasure umon t.StandardUnitCode=um.StandardUnitCodewhere um.ConvertType='Rate'union allselect t.TestId,um.UnitName,t.TestValue*1.8+32 as NewValuefrom Test tinner join UnitMeasure umon t.StandardUnitCode=um.StandardUnitCodewhere um.ConvertType='Function'union allselect t.TestId,um.UnitName,m.ToValue as NewValuefrom Test tinner join UnitMeasure umon t.StandardUnitCode=um.StandardUnitCodeinner join dbo.UnitValueMapping mon t.TestValue=m.StandardValue and m.UnitCode=um.UnitCode and m.StandardUnitCode=t.StandardUnitCode where um.ConvertType='Table' 【其实鞋码转换问题不能算是度量衡问题,只是为了说明这个模型的扩展性,增加个查找表的转换模式,所以举了这个例子。】
在业务需求中,经常需要我们在系统中能够记录历史信息,能够查看到历史变动情况,这时我们可以通过增加开始结束时间字段来记录数据的历史版本。对数据的历史记录主要分为:关系、属性历史,实体历史和变更历史。 关系、属性历史记录 所谓关系历史记录就是指两个实体之间的关系存在历史版本。比如部门表和员工表,对于某一个时刻来说,一个部门有多个员工,一个员工只属于一个部门,所以是个一对多的关系。而我们希望把这个关系记录下历史变动,那么就会形成多对多关系。多对多关系就形成中间表,然后我们在中间表上加入“开始时间”字段和“结束时间”字段即可记录这个关系的历史。 对某个实体的属性记录历史记录会形成一对多的关系表,比如产品价格属性,我们希望把所有历史定价都记录下来,那么就会形成产品和价格一对多的关系。 在AdventureWorks数据库中,我们可以看到大量的这种记录关系历史的设计。比如: 员工、部门、轮班的历史记录: 这就是前面提到的一对多关系因为记录历史变为多对多关系的例子。 产品对成本和售价的历史记录: 这就是典型的属性历史记录,对于产品的众多属性,我们之关系成本和售价这两个属性的历史,所有可以建立一对多关系的价格历史表。 销售和区域以及销售配额的历史记录: 区域和销售本来也是普通的一对多关系,一个销售属于某个片区,一个区域对应多个销售。现在由于历史记录,所以形成多对多的关系表SalesTerritoryHistory。而对于销售配额,因为是记录到季度的,一季度只有一个销售配额,所以不需要开始时间和结束时间,只需要一个季度第一天即可(结束时间是可以根据这个季度的第一天而计算出来的,所以不需要再存储)。 区域与销售人员的关系在增加了中间表形成多对多后,仍然保留了原来的一对多关系,从数据上来看不是这样的,因为两个表的数据是不一致的,所以我推断这是另外一个一对多关系,而不是原来的区域和销售的分配对应关系表。 小结: 当需要对关系或属性记录历史时,会把关系提升一个复杂度,也就是说原来是一对一的,现在会变成一对多,原来是一对多的,现在会变成多对多。在历史记录表中增加“开始时间”和“结束时间”两个字段来表示该行数据的时间有效性。AdventureWorks数据库中使用了NULL值设为“结束时间”来表明这条数据是当前有效的,但是笔者并不推荐这么做,最好是把两个字段都设置为NOT NULL,在比较时可以得到统一的查询语句: where @d between StartDate and EndDate 另外SalesTerritoryHistory这个表只记录“开始时间”而不记录“结束时间”这也是一个不好的设计,虽然结束时间是可以计算出来的,但是每次查询的时候还需要去计算结束时间,真不是一个好方法。最好是把两个字段都保留,用户只需要输入开始时间,由前端程序去初始化结束时间,然后一并保存。 实体历史记录 主实体历史记录 实体的历史记录是指对一个实体数据的任何更改,都把整条数据都产生一条新记录,而不是只针对某个属性或者关系。对实体进行历史记录,我们也可以采用添加开始时间结束时间的方式,但是更多的时候我们对整个实体记录历史并不是为了随时查询历史上某个时间点这个实体的值,而是为了记录一个“版本Version”信息,方便在审计某个实体的变更时对比。如果我们是出于审计的需要而记录的历史版本,那么这些历史数据平时是不会参与到业务查询中的,所以并不需要记录开始时间,结束时间,取而代之的,我们可以增加“版本”字段,当然还有审计用到的“最后更新时间”和“最后更新人”, 这样就实体的变化情况,如果我们仅仅是增加Version字段,在查询当前版本时会很麻烦,因为我们必须拿到最高的那个版本号,然后才能把这个最新版本的记录作为当前记录,为了优化这个性能问题,我们一般还需要再添加布尔型的“是否当前版本IsCurrent”字段来标识当前版本。增加了这个字段后,那么在更改实体数据时就会更麻烦一些。首先需要将老数据版本号获得,+1生成新的版本号,然后将老数据的“是否当前版本”字段置为0,更新老数据的“最后更新时间”和“最后更新人”,然后插入新版本号的数据,而且新版本是当前版本。我在AdventureWorks数据库中并没有看到关于实体的历史记录的设计,不过我们可以看SharePoint的数据库设计,就是采用我这里提到的版本设计的方法。有兴趣的可以查看一下SharePoint的ContentDB的AllUserData表,tp_Version就是记录版本的,tp_IsCurrent和tp_IsCurrentVersion就是标记当前版本的。 附属实体的历史记录 在进行实体历史记录时,还面临的一个问题是,附属的子实体是否也需要一并进行历史记录。比如我们要对采购订单这么一个实体进行历史记录,每次对采购订单的修改都会生成一个新版本的采购订单。如果一个采购订单下面有100条采购明细,那么我们在编辑了采购订单主表后,创建了新版本的采购主表数据,是否对这100条明细也创建对应的新版本数据呢?如果创建,那么采购明细表的数据量就会飞涨,而且实际上我们这里并没有编辑这100条明细,新版本的明细数据是一模一样的,如果不创建,那么怎么保持这种外键约束呢?毕竟明细表上面的外键对应的可是老版本的采购订单的ID啊! 其实两种方案都可以,第一种方案开发简单,如果明细并不是那么多,或者本身单据的数据量并不大,那么重复一点明细表并不会带来太大的影响。第二种方案开发会很复杂,需要新老数据逐条对比,找到差异,如果主表有更改,那么为主表创建新版本,如果100条明细中有2条更改,那么就为这2条创建新版本。 下面详细说一下采用第二种的解决方案的模型设计。首先,我们需要断开主表和附属表的外键,将Form和Item作为两个独立的实体,各自添加“版本”,“是否当前版本”等属性。为Form添加业务主键“FormNumber”,用于唯一标识一个表单(由于版本记录的原因,所以FormNumber不是Form的主键),然后在Item表中添加“FormNumber”,用于标识这些Item是属于哪个表单。 select *from Form where IsCurrent=1 and IsDeleted=0 and FormNumber=@formNumber;select *from Item where IsCurrent=1 and IsDeleted=0 and FormNumber=@formNumber; 变更历史记录 无论前面讲到的对关系,属性还是整个实体的历史记录,都会在业务表中形成新的数据,数据的增加一方面会导致查询的效率变低,另一方面也使得每次查询时都需要带上额外的查询条件,非常不方便。于是我们想到了另一种保存历史记录的方式,那就是我们像记录日志一样,把变更了的部分记录到日志表中。 记录变更日志的好处是不影响现有数据库模型的设计,也就是说所有实体和关系都不需要改,我们只需要增加一个变更日志表即可。但是变更日志一般是前端程序通过对比前后记录,找到变更的属性,然后写入的,并不是数据库做的事。坏处也显而易见,那就是还原历史数据不方便,不能像前面的模型那样可以快速的查询数据的历史状态。 所以变更日志表这种处理方式只用于审计的需求,而不能用于业务上要对历史数据的查询需求。在AdventureWorks数据库中有一个TransactionHistory表,用于记录各个订单事务的,虽然不是记录订单变更的,但是也有和变更历史记录类似的结构。 历史数据查询优化 前面提到由于保留历史数据的原因,所以会将数据库中对应表的数据量增加很多倍,数据量的增加必然导致查询变慢,所以我们在记录历史数据后很有必要对表进行查询优化。优化可以采用以下解决方案: 归档表 如果我们的历史数据在平时的业务中并不需要,只有在特殊场景才会用到历史数据表,那么我们可以将历史数据表建立一模一样结构的归档表,然后定时将业务系统中的历史数据转移到归档表中。当然,前端软件系统也要做对应的修改,对于老的历史数据需要查询归档表,而新的数据是查询当前表。在AdventureWorks只对TransactionHistory就建立了对应的归档表。 分区 建立分区比归档表的好处是在物理上,老数据和新数据可以存储在不同的地方,新老数据可以各自建立各自的索引树,而在逻辑上对程序来说仍然是访问一个表,前端程序不需要做什么修改。比如对于开始结束日期的历史数据记录方式,我们可以把结束日期为9999-12-31的数据(当前有效数据)分到一个区,剩下的分到另一个区。对于版本记录的方式,我们可以将“是当前版本”分到一个区,把其他的数据分到另一个区。 分区后在更新数据时会导致老数据的区块转移,因为老数据本来是在Current区块的,现在由于更改了实体,老数据需要转移到Old区块,然后将新数据插入到Current区块,除了分区的移动还有对应的索引的变动,所以更新数据时会相对慢一些。 索引 如果对于Oracle数据库,那么我们可以对IsCurrentVersion字段建立位图索引,如果是SQL Server这种不支持位图索引的数据库,那么我们也可以在建立B树索引时把IsCurrentVersion放在第一列,因为这个列是必然放入过滤条件的。
最近在读一本《数据库系统 设计、实现与管理》的书,其中的数据库设计部分写的挺好的,另外在本书中也讲到了数据库生命周期的概念,我觉得有所收益,特写下此博文! 在软件开发中,我们经常会提到软件系统开发的生命周期,大致分为:计划、分析、设计、实现、运维几个阶段,整体流程和动作如下图所示: 而针对数据库建模和数据库应用开发来说,也有其自己的“数据库生命周期”,database life cycle,简称DBLC。DBLC大致上分为6个阶段:数据库初始研究,数据库设计,实现和装载,测试和评价,运行,维护和演化。其对于的生命周期图为: 也许作为一个数据库模型设计人员或者开发人员来说,只关心参与3个阶段,但是其实每个阶段都应该参与其中,毕竟这6个阶段是不断迭代的过程。 下面我们来分别说明一下这6个阶段。 1.数据库初步研究 简单的说就是前期的需求调研阶段,只不过软件开发中的需求调研是站在软件的角度,而数据库设计人员则应该站在数据库的角度分析用户的需求,主要做到以下目标: 分析公司的状况。 定义问题和约束。 定义目标。 定义范围和边界。 2.数据库设计 这是数据库生命周期中最重要的环节,也是最烧脑细胞的环节。这个环节工作的好坏直接关系到最终软件是否满足用户和系统的需求。数据库设计又进一步划分为几个阶段:概念设计、DBMS的选择、逻辑设计、物理设计。 概念设计 概念设计阶段需要根据用户和系统的需求,设计出实体关系模型ERM,所以这个阶段的产出是一个ERM。至于怎么分析用户需求后定义实体,定义关系,定义属性,范式化与反范式化,以及对概念模型的验证,那都是很深的学问,都可以单独写一本书了。我在之前的博客中粗略的讲解了如何进行概念模型的设计,可以参考:http://www.cnblogs.com/studyzy/category/466850.html 尤其是其中一篇(分析与设计数据库模型的简单过程)把ERM的建模过程演示了一遍。 而对概念模型的验证,一方面需要检查用户需求中的对象和属性是否都在概念模型中,其次,检查CRUD在模型上的操作是否会造成异常,另外也需要从报表的角度考虑,是否能够写出对应的报表的查询,查询效率是否可接受。在整个模型验证过程中,可能把一些属性独立出来成新的实体,也可能把关系从一对多改为多对多,也可能出于性能上的考虑,对一些表进行反范式化处理。对概念模型的验证一般以模块为单位进行验证,而且概念模型的定义是独立于硬件和软件的,保证了模型的简洁。 DBMS的选择 目前市面上的DBMS可选择性并不是很大,企业级DBMS就是Oracle,IBM DB2和SQL Server,这些DBMS功能强大完备,但是价格昂贵,而免费开源的有MySQL,PostgreSQL,这都是很流行的开源数据库,而如果系统小而简单的话,还可以考虑Sqlite,Access等单机数据库。这前面说的都是RDBMS,也就是关系型的数据库,还有其他对象数据库,文档数据库,层次数据库如果需要也可进行选择,尤其是随着互联网的兴起,现在NoSQL非常火,也增加了DBMS的选择范围。 不管怎么说,DBMS的选择主要还是考虑以下几个方面: 开销/预算。这里除了软件和硬件本身的采购价格,还需要包括学习成本,运维开销,转换成本等。 DBMS的特征和工具。如关注系统的可用性,安全性,扩展性等。 基础模型。是关系型的还是对象型的,或者文档型。 便利性。DBMS可以便利的在不同平台,系统,语言之间进行移植。 硬件要求。 逻辑设计 逻辑模型就是将概念模型转换为特定DBMS支持的模型,所以逻辑模型是与软件相关的。逻辑模型中的表、外键是可以通过概念模型的实体、关系转换而来,但是对于视图、存储过程、函数、用户等,都需要在逻辑模型中设计。 物理设计 物理模型是与具体的物理硬件相关,可以通过逻辑模型转换而来。在物理设计中,需要考虑具体的数据存储,数据分布等,在物理模型中要求设计师充分了解软件和硬件环境,充分发挥软件和硬件的特性。 3.实现和装载 常用的数据库建模工具如PowerDesigner或者ERWin都可以将物理模型生成对应的SQL语句,然后我们在DBMS中运行SQL,便可实现我们设计的数据库模型。在实现了数据库模型后,我们还需要进一步研究其性能,安全,备份与恢复,以及完整性和公司标准。这些一般都是由DBMS提供的工具支持的。 4.测试和评价 数据一旦装载到数据库后,DBA就要对数据库的性能,完整性,并发访问和安全约束进行测试和优化。这个测试和评价阶段是与软件开发并行进行的。如果测试和评价结果不满足要求,就需要对系统和模型进行调整。其中包括: 调整DBMS的配置参数,修改物理设计(比如索引和分区的修改),修改逻辑设计(比如增加冗余字段),更新或者更换DBMS的软硬件平台。 5.运行 数据库通过了评测阶段,就认为是可运行的了。在实际生产环境的运行过程中,产生了真实的数据,一些在测试阶段无法预见的问题可能会被遇到,比如查询缓慢,数据不一致,死锁等问题都可能遇到。棘手的问题需要紧急补丁,而一些小Bug则可能在下一个版本中修正,而这些在运行中对数据库的补丁和修改,就是一个维护和演化的过程。 6.维护和演化 数据库的日常维护工作包括备份与恢复,用户权限分配,系统监控,系统定期安全审计等。对于系统补丁和新版本开发,则是对模型的演化,需要在更新生产系统数据库时对数据库模型进行同步的更新,这便进入了数据库生命周期的迭代过程。
最近在朋友圈看到别人分享的一篇知乎回答:https://www.zhihu.com/question/36426051/answer/76031743 我觉得写得挺有道理的,作为一个写了10多年C#代码的老程序员来说,很多地方我能感同身受,所以也谈谈我的自我感受。 1.重构是程序员的主力技能。 是的,我之前经常也提到一点,就是好多设计模式不是提前就设计出来的,而是重构出来的。很多情况是我们在做设计的时候考虑不到的,是写代码时也考虑不到的,只有在项目上线后,客户使用过程中才会反应出来,这个时候就需要对项目进行扩展,版本升级,这时就体现老程序员实力的时候了,就是根据已有的情形,结合新的客户需求,使用合适的设计模式,使得代码能够优雅的扩展。 2.工作日志能提升脑容量。 这个我没有什么体会,我平时也写工作日志,但是那是项目工作的需要,不是我本人的主观意愿。不过我个人觉得技术博客能够提升脑容量才是真的。很多项目中遇到的问题,解决了,也许以后还会再次遇到,也许别人也会遇到,那么就写成博客,自我总结,方便以后自己或者其他程序员遇到同样的问题。 3.先用profiler调查,才有脸谈优化。 是的,我之前也专门做过SQL Server的性能优化,很有体会,Profiler是第一步。如果做.net代码的优化,也有对应的Profiler工具,这个可以帮我们快速的定位瓶颈在哪里。找到了瓶颈才有接下来的优化工作。 4.注释贵精不贵多。杜绝大姨妈般的“例注”。漫山遍野的碎碎念注释,实际就是背景噪音。 我不是很同意这个说法,还有更极端的观点是不需要注释,命名就是注释,好的命名就能注释一切。我觉得好的命名那是必须的,但是在复杂的逻辑中,我们有必要在代码中注释我们的思路,为什么会用这样一种写法。 5.普通程序员+google=超级程序员。 确实,很多不懂的,解决不了的就Google吧,一般Google会告诉你,Stackoverflow知道答案。 6.单元测试总是合算的。 这个观点我赞同,也许对于很多程序员来说,单元测试就是浪费时间,但是当项目复杂了以后,真的很需要单元测试,尤其是在不断的hotfix和版本升级的过程中。 7.不要先写框架再写实现。最好反过来,从原型中提炼框架。 这个就是我前面第一点提到的一样,很多框架设计好了,但是不一定适应当前这个项目,那就是画蛇添足。 8.代码结构清晰,其它问题都不算事儿。 这个就是编码规范的问题,代码写的漂亮,让Debug没那么痛苦,让别人Review你的代码也没那么痛苦。 9.好的项目作风硬派,一键测试,一键发布,一键部署; 烂的项目生性猥琐,口口相传,不立文字,神神秘秘。 这个也是我最近在研究的CI(持续集成),适应TeamCity可以把测试,发布,部署都自动化搞定。 10.编码不要畏惧变化,要拥抱变化。 基于接口的编程,我们只关注接口,实现嘛,随时可以变。 11.常充电。程序员只有一种死法:土死的。 好吧,程序员的命就是这样,技术变化太快了。 12. 编程之事,隔离是方向,起名是关键,测试是主角,调试是补充,版本控制是后悔药。 面向接口,控制反转与依赖注入,都是编写复杂的软件的必备良药。测试,调试,没啥可说的,必备。版本控制,那是必须的!即使是只有一个开发人员的项目,也需要版本控制。 13. 一行代码一个兵。形成等建制才能有效指挥。单位规模不宜过大。千人班,万人排,容易变成万人坑。 这里说的一个关于函数的规范问题,有一种说法是一个函数的内容不应该超过7行,如果超过7行,那么肯定是把多个Function合并到一个函数中的,应该拆分成多个函数。这个要求可能有点高,很难做到。不过上百行,上千行的函数那是不应该的,必须拆分! 14. 重构/优化/修复Bug,同时只能作一件。 这个我还是有点体会的,把多个目标合并到一次修改中,那是多么困难的事情,真的不好做。最好是分开,先重构,保证重构后的功能和原来的功能一致,然后再Fix Bug。 15. 简单模块注意封装,复杂模块注意分层。 面向对象编程基本要点,封装,企业应用架构的基础就是分层。最经典的三层架构做企业应用的应该都知道。 16. 人脑性能有限,整洁胜于杂乱。读不懂的代码,尝试整理下格式; 不好用的接口,尝试重新封装下。 还是说到编码规范的问题,简洁易懂,接口要清晰。 17. 迭代速度决定工作强度。想多快好省,简化开发流程,加快迭代速度。 软件工程中的快速迭代,敏捷开发,涉及到前面提到的持续集成。 18. 忘掉优化写代码,忘掉代码作优化。因为过早优化,往往事倍功半; 不通过全局性能度量,优化也难有建树。 不是很认同,有经验的程序员,在写代码时采用的就是最优的算法,最好的查询方式。没有什么忘掉优化写代码的事情,在写代码时,想到的就是最优的算法,因为在他看来就这种算法才是对的。 19. 最好的工具是纸笔;其次好的是markdown。 纸和笔只适用于在Face 2 Face的交流过程中,交流后顶多拍照留存,根本无法建立有效的知识库,以后想到之前的讨论,怎么检索?怎么修改?。写Wiki才是王道,Markdown只是一种写Wiki的方式罢了。 20. leader问你任务时间,你答不上来。很可能是任务拆分不够细。细分到没有疑问吧。 应该是的,如果不知道任务时间,那么说明要么你根本不懂这个任务怎么做,完全不会,要么就是任务太大了,不好估计时间。 21. 宁可多算一周,不可少估一天。别总因为你的“乐观”而boss受惊吓。 是啊。程序员在估计工时的时候总是太乐观。随便开口就是一个小时就能搞定,半天就能做完。完全没有想到该修改对其他模块的影响。一个修改后的单元测试,可接受测试,UAT环境测试,再到上线,很多地方都得花时间的。一旦某个测试不通过,然后又得调试,修改,再进行单元测试,可接受测试~~~~,好吧,谁能保证每次修改都是一次通过呢。 22. 最有用的语言是English。其次的可能是Python。 好吧,我英语不好,Python更不懂。我不评论。 23. 百闻不如一见。画出结果,调试耗时将急剧缩短。 没懂这里在说什么。 24. 资源、代码应一道受版本管理。资源匹配错误远比代码匹配错误更难排查。 这个应该是这样。在项目文件夹中,有很多个子文件夹,其中一个文件夹叫src,那里存放的才是代码,那么其他的文件夹呢?就可能存放相关的设计啊、测试啊、工具之类的。 25. 不要基于想象开发, 要基于原型开发。原型的价值是快速验证想法,帮大家节省时间。 恩,是啊,最好是先画出原型。有了原型才方便讨论,明确需求。 26. 序列化首选明文文本 。诸如二进制、混淆、加密、压缩等等有需要时再加。 应该是吧,比如Json是比较好的序列化选项。 27. 编译器永远比你懂微观优化。只能向它不擅长的方向努力。 有了好的设计和算法,谁关系编译器内部怎么做的。 28. 不要定过大、过远、过细的计划。即使定了也没有用。 过大过远的目标还是可以定吧,规划一下下一个版本的Roadmap,也许还没有开始做,但是愿景可以建立。只是经常会有计划赶不上变化的情况,所以远期的计划不需要太详细,反正也会不断变。 29. 至少半数时间将花在集成上。 这得看做什么项目了吧,很多项目就是一个完全独立的孤岛,没啥好集成的。最近的基础可能就是单点登录的集成,太简单花不了多少时间。另外常见的是HR系统的员工数据的集成还有财务系统的财务数据集成,确实很花时间。 30. 与主流意见/方法/风格/习惯相悖时,先检讨自己最可靠。 没啥说的。 31. 出现bug主动查,不管是不是你的。这能让你业务能力猛涨、个人形象飙升; 如果你的bug被别人就出来,那你会很被动~≧﹏≦ 查Bug是也很难的事情,自己做的项目,自己再支持运维一段时间,看看自己的代码写的有多烂,有多难修改,多难调试。真的可以让自己能力提升很多。 32. 不知怎么选技术书时就挑薄的。起码不会太贵,且你能看完。 我很懒,很多书都看了一半就看不下去了。 33. git是最棒的。简单,可靠,免费。 源代码管理,必选Git,自己可以架设Git Server,也可以用GitHub。 34. 仅对“可预测的非理性”抛断言。 恩。是啊,尤其用户输入的时候。 35. Log要写时间与分类。并且要能重定向输出。 这个用现成的Log组件即可。有Log4J,Log4Net,真的很好用。 36. 注释是稍差的文档。更好的是清晰的命名。让代码讲自己的故事。 前面已经说过了。 37. 造轮子是很好的锻炼方法。前提是你见过别的轮子。 这里说的是程序员的自我修炼的过程。确实,对于一个需求场景,我们应该先想想有没有现成的开源项目可以用,然后再看能否把开源项目拿来改,最后自身足够强大了,就自己做一个轮子。 38. code review最好以小组或结对为主。因为对业务有足够了解建议才更有价值。而且不会成为负担。注意,提交过程中的管理员review很容易成为瓶颈。 这点我做的不好,在我这么多年的工作中,也只有为数不多的Code Review Meeting。 39. 提问前先做调研。节约大家的时间。 是啊,Google能够直接告诉你答案的,那就不用再问别人了。 40. 永远别小看程序媛(╯3╰) 只要是正在的码农,在我看来是没有区别的。所以没有小看或者高看的意思。 以上都是我的个人感受写给自己,看看差距,希望以后能做的更好吧。
最近在研究企业文档管理,这个是基本上所有企业都需要的软件,当然也是有很多种解决方案。对于企业文档来说,最基本的需求就是独立存储,共享。这种需求只需要建立一个Windows共享文件夹或者架一个Samba服务器即可实现,无法做复杂的权限管理,统计等。另一种方案就是架一个Web应用,比如SharePoint,就可以实现。 既然是WEB应用,进一步的需求是能够在线查看文档,根据用户需求可能不允许下载,不允许打印文档。这一点微软的高级解决方案是使用RMS,能够设置每个用户的打开权限,是否打印等,要求必须是域内,而且只管理Office文件的权限,对txt,pdf就没办法了。另外一个解决方案是在线文档预览,用户在网页中查看文档内容,用户无需拿到原始文档,如果有权限的话,可以允许用户下载文档。这就就是百度文库,豆丁之类的网站的功能。下面来说说怎么实现。 1.文档统一转换为pdf 这里的文档我们要看是什么格式,不同的格式有不同的转换方法。 1.1 Office文档转换pdf 对于Office文档(Word,Excel,PowerPoint),那么可以调用Office提供的COM接口,把文档另存为PDF。这个要求服务器上必须安装Office,同时要注意权限,不然很容易导致在本地调试时可以转换为PDF,但是一旦部署到服务器上去就不行。另外还需要注意的是,如果Office转换pdf时发生异常,可能导致Office的进程驻留在服务器,不断驻留Office进程会导致服务器资源耗尽。 这是Office文档转换为pdf的代码: /// <summary> /// 将word文档转换成PDF格式 /// </summary> /// <param name="sourcePath"></param> /// <param name="targetPath"></param> /// <returns></returns> public static bool ConvertWord2Pdf(string sourcePath, string targetPath) { bool result; Word.WdExportFormat exportFormat= Word.WdExportFormat.wdExportFormatPDF; object paramMissing = Type.Missing; Word.Application wordApplication = new Word.Application(); Word.Document wordDocument = null; try { object paramSourceDocPath = sourcePath; string paramExportFilePath = targetPath; Word.WdExportFormat paramExportFormat = exportFormat; Word.WdExportOptimizeFor paramExportOptimizeFor = Word.WdExportOptimizeFor.wdExportOptimizeForPrint; Word.WdExportRange paramExportRange = Word.WdExportRange.wdExportAllDocument; int paramStartPage = 0; int paramEndPage = 0; Word.WdExportItem paramExportItem = Word.WdExportItem.wdExportDocumentContent; Word.WdExportCreateBookmarks paramCreateBookmarks = Word.WdExportCreateBookmarks.wdExportCreateWordBookmarks; wordDocument = wordApplication.Documents.Open( ref paramSourceDocPath, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing, ref paramMissing); if (wordDocument != null) wordDocument.ExportAsFixedFormat(paramExportFilePath, paramExportFormat, false, paramExportOptimizeFor, paramExportRange, paramStartPage, paramEndPage, paramExportItem, true, true, paramCreateBookmarks, true, true, false, ref paramMissing); result = true; } finally { if (wordDocument != null) { wordDocument.Close(ref paramMissing, ref paramMissing, ref paramMissing); wordDocument = null; } if (wordApplication != null) { wordApplication.Quit(ref paramMissing, ref paramMissing, ref paramMissing); wordApplication = null; } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); GC.WaitForPendingFinalizers(); } return result; }/// <summary> /// 将excel文档转换成PDF格式 /// </summary> /// <param name="sourcePath"></param> /// <param name="targetPath"></param> /// <returns></returns> public static bool ConvertExcel2Pdf(string sourcePath, string targetPath) { bool result; object missing = Type.Missing; Excel.XlFixedFormatType targetType= Excel.XlFixedFormatType.xlTypePDF; Excel.Application application = null; Excel.Workbook workBook = null; try { application = new Excel.Application(); object target = targetPath; workBook = application.Workbooks.Open(sourcePath, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing); workBook.ExportAsFixedFormat(targetType, target, Excel.XlFixedFormatQuality.xlQualityStandard, true, false, missing, missing, missing, missing); result = true; } catch { result = false; } finally { if (workBook != null) { workBook.Close(true, missing, missing); workBook = null; } if (application != null) { application.Quit(); application = null; } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); GC.WaitForPendingFinalizers(); } return result; }/// <summary> /// 将ppt文档转换成PDF格式 /// </summary> /// <param name="sourcePath"></param> /// <param name="targetPath"></param> /// <returns></returns> public static bool ConvertPowerPoint2Pdf(string sourcePath, string targetPath) { bool result; PowerPoint.PpSaveAsFileType targetFileType= PowerPoint.PpSaveAsFileType.ppSaveAsPDF; PowerPoint.Application application = null; PowerPoint.Presentation persentation = null; try { application = new PowerPoint.Application(); persentation = application.Presentations.Open(sourcePath, MsoTriState.msoTrue, MsoTriState.msoFalse, MsoTriState.msoFalse); persentation.SaveAs(targetPath, targetFileType, MsoTriState.msoTrue); result = true; } catch { result = false; } finally { if (persentation != null) { persentation.Close(); persentation = null; } if (application != null) { application.Quit(); application = null; } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); GC.WaitForPendingFinalizers(); } return result; } 1.2 纯文本转换pdf 如果是文本需要转换为PDF,我们可以使用iTextSharp这个组件,对于纯文本,注意的是源文件中没有设置字体之类的,需要在转换成PDF时指定字体,否则对于中文可能由于没有设置字体而转换不出来。 /// <summary> /// 将Txt转换为PDF /// </summary> /// <param name="sourcePath"></param> /// <param name="targetPath"></param> /// <returns></returns> public static bool ConvertText2Pdf(string sourcePath, string targetPath) { var text = FileHelper.ReadTextFile(sourcePath); Document document = new Document(PageSize.A4); try { //step 2:创建一个writer用于监听Document以及通过PDF-stream指向一个文件 PdfWriter.GetInstance(document, new FileStream(targetPath, FileMode.Create)); // step 3: 打开document document.Open(); var f = GetFont(); // step 4: 添加一段话到document中 document.Add(new Paragraph(text, f)); } catch (Exception ex) { return false; } finally { if (document.IsOpen()) // step 5: 关闭document document.Close(); } return true; } private static Font GetFont() { var fontPath = (string) ConfigurationManager.AppSettings["FontPath"]; if (string.IsNullOrEmpty(fontPath))//没有指定字体就用楷体 { var fontName = "楷体"; if (!FontFactory.IsRegistered(fontName)) { fontPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows) + @"\Fonts\simkai.ttf"; FontFactory.Register(fontPath); } return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); } BaseFont bfChinese = BaseFont.CreateFont(fontPath,BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED); Font fontChinese = new Font(bfChinese, 16f, Font.NORMAL); return fontChinese; } 1.3 HTML转换pdf HTML中包含的元素较多,比较复杂,主要有两种方法,一种是调用浏览器的接口,让浏览器把HTML打印为PDF,另外就是ITextSharp提供了专门的XML/HTML转换组件:XML Worker,这个已经独立出来,不包含在ITextSharp中,需要单独下载。 public static bool ConvertHtml2Pdf(string text, string pdfPath) { Document document = new Document(PageSize.A4); try { PdfWriter.GetInstance(document, new FileStream(pdfPath, FileMode.Create)); document.Open(); var fontName = "楷体"; if (!FontFactory.IsRegistered(fontName)) { var fontPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows) + @"\Fonts\simkai.ttf"; FontFactory.Register(fontPath); } var elements = iTextSharp.tool.xml.XMLWorkerHelper.ParseToElementList(text, @"body { font-size: 16px; color: #F00; font-family: 楷体; }"); //iTextSharp.text. foreach (var element in elements) { document.Add(element); } } catch (DocumentException de) { Console.Error.WriteLine(de.Message); } catch (IOException ioe) { Console.Error.WriteLine(ioe.Message); } document.Close(); return true; } 1.4添加水印 以上都是转换成pdf的功能,在转换后,我们可以进一步使用ITextSharp对pdf进行加工,比较常见的添加水印功能。其实就是做一个淡淡的背景透明的图片,然后打开pdf文件,在每一页中画上水印图片即可。 /// <summary> /// 添加水印 /// </summary> /// <param name="inputPath">源PDF文件路径</param> /// <param name="outputPath">加水印后的PDF路径</param> /// <param name="watermarkPath">水印图片的路径</param> /// <param name="error"></param> /// <returns></returns> public static bool AddWatermark(string inputPath, string outputPath, string watermarkPath, ref string error) { try { PdfReader pdfReader = new PdfReader(inputPath); int numberOfPages = pdfReader.NumberOfPages; FileStream outputStream = new FileStream(outputPath, FileMode.Create); PdfStamper pdfStamper = new PdfStamper(pdfReader, outputStream); PdfContentByte waterMarkContent; iTextSharp.text.Image image = iTextSharp.text.Image.GetInstance(watermarkPath); image.SetAbsolutePosition(10, 10); for (int i = 1; i <= numberOfPages; i++) { waterMarkContent = pdfStamper.GetUnderContent(i); waterMarkContent.AddImage(image); } pdfStamper.Close(); pdfReader.Close(); outputStream.Close(); return true; } catch (Exception ex) { error = ex.StackTrace; return false; } } 2.在线预览pdf文档 前面已经统一转换为pdf文档,接下来就是对pdf的在线预览。这个在以前是不现实的,现在有了HTML5,只要浏览器支持HTML5就可以使用pdf.js库,将服务器上的pdf文件转换成HTML5代码展示在浏览器上。另外还有一个解决方案是使用Flash,需要把pdf文件进一步转换为swf文件,然后由Flash播放器来播放这个文档。可惜Flash已经是一个过时即将淘汰的技术了,像iPad,iPhone就不支持Flash,所以使用HTML5才是更明智的选择。 pdf.js网站已经提供了库和示例,浏览页面是http://mozilla.github.io/pdf.js/web/viewer.html,我们要打开我们转换的文件,只需要在URL中添加参数即可: /web/viewer.html?file=yourpdf.pdf 我们可以进一步修改viewer.html中的代码,根据需求去掉下载,打印等按钮,禁止用户下载和打印文件。
OData是一个非常灵活的RESTful API,如果要做出强大的查询API,那么OData就强烈推荐了。http://www.odata.org/ OData的特点就是可以根据传入参数动态生成Entity Framework的查询,最终实现动态的SQL的查询。但是在项目有时我们并没有采用Entity Framework,而是采用的NHibernate,那么该怎么用OData呢? 经过一段时间的Google和研究,终于找到了一个好的方案。 在OData API查询时,用户前端是url跟参数,但是在服务器端,我们是接收到的是一个ODataQueryOptions<T>对象,其实我们需要做的就是把这个对象进行解析,生成NHibernate能够理解的查询形式,比如HQL。网上找到微软官方已经写了这么个转换方法,主要是对ODataQueryOptions对象下的Filter和OrderBy进行转换,另外两个参数Top和Skip很简单,就是一个整数。 public static string ToHql(this ODataQueryOptions query,out int top,out int skip) { string queryString = "from " + query.Context.ElementClrType.Name + " $it" + Environment.NewLine; if (query.Filter != null) { // convert $filter to HQL where clause. string where = ToString(query.Filter); queryString += where; } if(query.OrderBy!=null) { // convert $orderby to HQL orderby clause. string orderBy = ToString(query.OrderBy); // create a query using the where clause and the orderby clause. queryString += orderBy; } top = query.Top?.Value ?? 0; skip = query.Skip?.Value ?? 0; return queryString; } ODataQueryOptions转换为HQL的项目在这里: http://aspnet.codeplex.com/SourceControl/changeset/view/72014f4c779e#Samples/WebApi/NHibernateQueryableSample/System.Web.Http.OData.NHibernate/NHibernateFilterBinder.cs Filter和OrderBy属性都会被转换成HQL,然后我们就需要进行NHibernate的查询了。 public QueryResult<T> FindByPaging(string hql, int top, int skip) { bool paging = top > 0; var query = Session.CreateQuery(hql); var querys = Session.CreateMultiQuery(); if (paging) { query = query.SetFirstResult(skip).SetMaxResults(top); } querys.Add(query); if (paging) { var countQuery = Session.CreateQuery("select count(*) " + hql); querys.Add(countQuery); } var queryResults = querys.List(); var result = new QueryResult<T>(); result.TotalCount = paging ? Convert.ToInt32( ((IList) queryResults[1])[0]) : ((IList) queryResults[0]).Count; result.ResultSet = ((IList) queryResults[0]).Cast<T>().ToList(); return result; } 对于一般的分页查询来说,我们应该会有两个查询,一个是查询满足条件的数据总条数,另一个是返回当前页的数据集。但是似乎OData并不支持返回这样的数据类型,OData支持的是Entity的List,如果我们重新定义了一个对象QueryResult: [DataContract] public class QueryResult<T> { [DataMember] public int TotalCount { get; set; } [DataMember] public IList<T> ResultSet { get; set; } public QueryResult() { } public QueryResult(int count, IList<T> list) { this.TotalCount = count; this.ResultSet = list; } } 然后在Controller中返回QueryResult,那么系统就会报406的错误。其实系统给我们提供了一个专门分页返回的对象System.Web.Http.OData.PageResult<T>,我们可以将Service返回的QueryResult封装成PageResult再返回即可。 PageResult里面有个NextPage的URI参数,我们可以传Null。
在SQL Server中经常会用到模糊匹配字符串的情况,最简单的办法就是使用like关键字(like语法http://msdn.microsoft.com/en-us/library/ms179859.aspx)。但是如果我们使用的前后都加%的方式,是没办法用到索引进行快速查询的,所以很多情况下我们使用左匹配的方式。最常见的一个例子就是在搜索框中,用户输入了一部分关键字,系统可以通过用户的输入进行左匹配,找出相关的结果列出来。使用左匹配的好处是可以使用到SQL Server中对该字段建立的索引,使得查询效率很高,但是不好的SQL语句仍然会导致索引无法使用。 假设我们现在有个表YCMRSALE,其中有个字段MATNR存储了料号信息,如果我们要从这个表中查询出以AB开头的料号,如果使用NHibernate,那么我们常用的写法有: //QueryOver的写法 var result = session.QueryOver<Ycmrsale>().WhereRestrictionOn(c => c.Matnr).IsLike("AB", MatchMode.Start).List<Ycmrsale>(); //Linq to NHibernate result = session.Query<Ycmrsale>().Where(c => c.Matnr.StartsWith("AB")).ToList(); //Criteria写法 result = session.CreateCriteria<Ycmrsale>().Add(Expression.Like("Matnr", "AB", MatchMode.Start)).List<Ycmrsale>(); 这几种写法本质上都是生成了如下的where条件语句: where ycmrsale0_.Matnr like 'AB%' 如果使用EntityFramework,那么查询的C#代码也和NHibernate类似: var result = bwEntities.YCMRSALEs.Select(s => s.MATNR).Where(s => s.StartsWith("AB")); where条件也是一样的: WHERE [Extent1].[MATNR] LIKE 'AB%' 这里只是举了最简单的情况,如果我们要查询的料号本身就包含%,比如要查询以”%00”开头的料号,那么怎么保证这里的%是百分号而不是表示模糊匹配的意思呢? 使用EntityFramework就很简单,什么都不需要修改,系统会根据传入的字符串生成不同的SQL语句: var result = bwEntities.YCMRSALEs.Select(s => s.MATNR).Where(s => s.StartsWith("%00")); 生成的SQL Where条件: WHERE [Extent1].[MATNR] LIKE '~%00%' ESCAPE '~' 对开发人员来说,真是很简单,什么输入都不用管。但是如果用NHibernate就要麻烦点了,我们必须要判断用户输入的字符串里面是否有特殊转移符,如果有,那么就需要进行替换,而且C#查询语句也有所不同。 string input = "%00"; Regex regex=new Regex(@"[~%\[\]_]"); input= regex.Replace(input, delegate(Match m) { return "~" + m.Value; }); var result = session.QueryOver<Ycmrsale>().WhereRestrictionOn(c => c.Matnr).IsLike(input, MatchMode.Start,'~').List<Ycmrsale>(); 生成的SQL Where条件: WHERE this_.Matnr like @p0 escape '~';@p0 = '~%00%' 以上说的都是在ORMapping的工具中进行左匹配查询,如果我们要在SQL语句中直接进行查询还有一种写法就是用left函数。同样以YCMRSALE表举例,如果我们有另一表matnr,该表中的matnr列存储了不完整的料号,现在需要将两个表join起来,使用matnr列进行左匹配,那么我们的SQL可以写成: select * from YCMRSALE s inner join matnr m on left(s.MATNR,len(m.matnr))=m.matnr 这个写法能够得到我们想要的结果,但是由于对MATNR列使用了函数,所以无法使用索引,所以查询速度很慢。 如果我们要改写成like的形式,那么就需要对matnr表中的matnr列进行处理,将特殊字符进行替换,将~%_[]这几个字符都替换掉。所以我们的SQL查询就会变成这样: select * from YCMRSALE s inner join matnr m on s.MATNR like replace(replace(replace(replace( replace(m.matnr,'~','~~'),'_','~_'),'[','~['),']','~]'),'%','~%')+'%' escape '~' 这里的SQL虽然看起来比较Ugly,但是却可以用上YCMRSALE表上对MATNR建立的索引,所以效率较高。 除了ESCAPE这个关键字的处理方式外,微软官方还给出了另一种解决办法,那就是使用“[]”将转义字符括起来。这种写法比escape关键字的写法要简单点,对应的SQL为: select * from YCMRSALE s inner join matnr m on s.MATNR like replace(replace(replace( m.matnr,'[','[[]'),'_','[_]'),'%','[%]')+'%' 甚至我们还可以先写个自定义函数对转移字符进行处理对于join的情况,那就非常复杂了。。。 create function OpStr(@input varchar(50)) returns varchar(100) as begin declare @i int=1; declare @result varchar(100)=''; declare @c char(1) while(@i<=len(@input)) begin set @c=substring(@input,@i,1); if (@c='[' or @c='%' or @c='_') begin set @result+='['+@c+']'; end else begin set @result+=@c; end set @i+=1; end return @result end 然后在查询中调用这个自定义的函数即可。 select * from YCMRSALE s inner join matnr m on s.MATNR like dbo.OpStr(m.matnr)+'%'
前段时间结识了两位创业做输入法的朋友,花了一个下午和他们畅聊了下输入法,也开拓了下自己的思路,于是写此博文以记之。 目前中国PC市场的输入法基本上已经被搜狗垄断了,剩下的就是QQ,谷歌,百度等几家大公司的输入法,当然也有拼音加加这种老牌输入法的死忠粉丝,所以可以说PC市场的输入法大局已定,没有什么机会了。而眼下手机输入法还是一片蓝海,虽然搜狗、百度、QQ等手机输入法都在攻城略地,但是仍然是大有可为的一片市场。 在国内输入法之外,国外输入法是一个更大的市场,在PC时代,国外拉丁文用户可以不需要输入法,直接在键盘上打字即可,但是在智能机时代,没有了键盘,即使输入英文也得用一款输入法软件才行。所以国外手机输入法市场是一个比国内输入法市场要大好几倍的市场。 现在无论是在手机输入法市场上风生水起的触宝输入法,还是能够跨平台的RIME输入法,他们都有一个共同的特点,就是同一款输入法软件,只需要下载不同的词库,就可以实现不同的输入法。也就是说输入法软件本身只是做了一个通用的框架,通过不同的配置文件和核心词库文件来实现不同语言,不同输入方式。这是一个发展趋势,以后输入法可能都是这样被统一。 在输入方式上,中文的输入最常见的就是拼音了,其次时五笔,然后就是些乱七八糟的各种国人发明的输入方式。在台湾流行的是注音输入法和仓颉输入法,其实就是对应大陆的拼音和五笔。英文或者说拉丁语系的输入方式就简单多了,常见的就是键盘上直接输入,还有就是现在比较流行的滑动输入Swype。之前看了吴军老师的《数学之美》,里面也提到了输入法,对于中文而言,拼音输入才是更接近人本性的一种输入方式,虽然现在的拼音输入法重码率高,导致输入效率还不够高,但从长远来看,拼音输入法必将打败五笔输入法。其实现在搜狗拼音已经做得很不错了,整句整句的输入,使得重码的几率低了很多。 说到搜狗拼音输入法,这个目前大陆输入法市场的老大,那么就必须要说到搜狗输入法当年做得很成功的词库。输入法的词库分为三部分: 核心词库 分类词库 用户词库 核心词库是一个语言的核心,定义了最常用的词汇,核心词库的好坏直接决定了输入法的好坏。前面提到输入法框架,只需要配上核心词库和输入设置,就可以变成一个全新的输入法。核心词库是对一个语言通用的,还记得当年谷歌输入法出来的时候,就被搜狗告了,就是因为谷歌输入法盗用了搜狗输入法的核心词库。 分类词库(细胞词库)我不知道是不是搜狗输入法的首创,但搜狗输入法是做得最成功的。分类词库是对某个特定人群才使用得到的词库,默认情况下用户是没有分类词库的,用户可以根据自己的情况下载对应的分类词库。比如笔者是四川人,搞计算机的,所以就会下载“计算机词汇”,“四川地名”等分类词库。因为分类词库是针对特定人群的,所以对于一个北京的销售人员来说,就没必要下载笔者的这些分类词库。搜狗成功的将分类词库的创建使用众包的方式交给用户自己来完成,使得分类词库蓬勃发展,现在已经有27K+个词库了。 用户词库是针对用户个人而创建的词汇列表,该词库只对用户个人有用,对其他人来说,可能没有任何意义。比如笔者在写Email时经常会输入朋友的姓名,绰号等,这些都是笔者的好朋友的姓名,绰号,不会存在于核心词库和细胞词库中,创建这样的用户词库对笔者下次输入时非常有帮助,但是对于其他人来说,根本永远不会输入这些词汇,或者对别人来说,这根本就不是词汇。搜狗很好的将用户输入过的这些用户词库记录下来,然后同步到用户账号的服务器数据中,这样既方便了用户使用多台电脑时词库同步的问题,也避免了重装系统导致的数据丢失的问题。 凭借着对这三种词库的特点和其他优化,使得搜狗迅速占领了市场,接下来搜狗输入法就发展其他各种花哨功能去了。毕竟当年大家的输入法还是只能做到60分的时候,搜狗输入法能够做到90分,那就是极大的成功,现在大家都能做到90分了,接下来搜狗再大量投入也只能把90分做到95分,对普通用户来说,改善不明显,所以只能靠不断的扩展新的功能来进一步扩大用户群。 输入法的竞争其实就是词库的竞争,毕竟现在输入法框架已经很成熟,软件上的差异越来越小,大家都能做到很高的正确率。核心词库是由专家仔细精挑细选出来的,适用与每个人;分类词库是通过众包的方式,由各行各业的热心人士统计筛选出来的,网上都可免费下载;个人词库是由用户在使用输入法的过程中自己创造出来的,具有一定的用户粘性,使得用户不愿更换其他输入法。对于分类词库和个人词库,“深蓝词库转换”都给出了很好的解决方案,帮助用户从一种输入法切换成另一种输入法。比如之前一直用搜狗输入法,现在想换成谷歌输入法,但是又觊觎搜狗输入法的细胞词库,那么可以用深蓝词库转换将搜狗细胞词库转换成谷歌拼音词库,导入谷歌拼音。对于个人词库,也是如此,只需要在搜狗输入法中将个人词库备份,然后使用深蓝词库转换将备份文件转换成谷歌拼音的词库导入即可。 虽然深蓝词库转换解决了输入法切换的问题,但是还有一个摆在所有输入法面前的问题,用户词库从哪里来?必须让用户在第一次输入时一个字一个字的选吗?如果我之前用的输入法没有设置账号同步到服务器,或者用户词库丢失了,难道真的必须让用户再痛苦一会,一个字一个字的重新选。 用户之前已经进行了大量输入,比如用户的QQ聊天记录、Email,或者写博客,QQ空间,写微博、说说、心情、微信等,更或者用户发表过很多论文,写过书、网络小说等;这些都是构建用户词库的素材,如果我们能够分析这些素材,那么就可以构建一个强大的用户词库,使得用户的输入法更加个性化,输入效率自然更高。 收集这些用户词库的素材就是一个比较麻烦的事情,毕竟用户输入的地方太多了,然后就是进行解析,这需要对汉语进行分词,这是个麻烦的事情,最后就是将解析后的语料进行处理,生成用户词库。我想下一个项目能够做做这一块,毕竟这东西对很多人来说,是个好东西!
今天贤内给了我一道很实际的算法题,把我彻底难住了,实在想不出来,于是写此博文以记之。 背景是这样的,现在有一个付款明细的Excel,里面有为哪个发票,哪个公司应付多少钱的明细,明细数据是62条,现在知道我们已经付出的金额为Sum,请问到底哪些发票是已付款的。 这是62条明细数据: 653165.00 356029.11 220896.45 146362.00 1847670.00 3018518.91 1347553.07 145010.74 339784.84 199350.28 1206114.00 882000.00 253246.13 720000.00 24194.07 1518952.00 139453.48 200415.00 812044.00 9032764.57 3960608.05 1855126.31 7409087.18 608094.66 225519.59 627912.23 109897.52 1215819.87 4220245.50 94299.00 96547.00 92616.01 597100.54 880440.00 343991.59 70468.19 1092418.47 66911.94 80138.65 1398551.14 172287.48 691097.86 2371693.44 3773148.63 83898.33 89922.75 2619220.46 1179477.63 3440250.98 700000.00 997545.00 272523.00 3009976.00 451891.44 2111314.00 306377.00 142329.00 2057178.00 9340.00 249027.00 60811.50 51188.50 付款的金额为: 35857936.42 这听起来是一个很简单的算法题,其实就是算组合嘛,把每种组合的金额进行相加,如果等于Sum金额,那么就输出这种组合。于是网上找找组合函数的代码,很快就写出了这个程序。而且使用了一些简单的测试程序,确认计算是正确的。但是真正用到这个事情中,却崩溃了,计算量太大,根本算不出来。 仔细一想,对于每个数字,要么出现,要么不出现,那么其计算复杂度就是O(2^n),这里n=62,那么差不多就得计算2的62次,遍历每一种组合,才能找到全部答案。天啊!2的62次方! 根本不可能完成啊。想了又想,怎么都没有想到好的办法把复杂度降下来,伤心。不知道有没有大神能够解决这个问题。 这还只是一次数据,以后说不定还有100条明细,200条明细的,就这破算法,那更是天文数字,怎么可能算得出来啊?! 附上现有的代码下载。 更新: 好吧,看来我太无知了,这个问题是没有解决办法的,StackOverflow的讨论:http://stackoverflow.com/questions/4355955/subset-sum-algorithm 而且还有专门的维基百科页面:http://en.wikipedia.org/wiki/Subset_sum_problem#Pseudo-polynomial_time_dynamic_programming_solution
我从初中开始基本上就是一个英语很烂的人,数理化再好有什么用,工作了,结果发现数理化都没啥用,最有用的还是当年学的最烂的英语。于是在2011年年底开始了学习英语的课程,在学习的过程中,外教经常会放英剧美剧给我们看,看了以后回答问题,讲解,挺有意思的。印象最深刻的就是Neil给我的Doctor Who还有另外一个外教放的Friends。后来在课程快结束的时候,萌发了一个想法,能不能只看英文字幕来看美剧(当然还有英剧),这样没有中文字幕的话才能在看美剧的过程中联系阅读与听力。但是美剧中很多词汇不懂,一旦句子中出现了两个不懂的词汇,那么这句话基本上就不懂是啥意思了。那么我能不能根据我的实际词汇量,对字幕就行修改,如果是认识的单词,那么就不管,如果是不认识的单词,那么就给出其中文意思,这样能够便于理解整个句子,而且在潜移默化中慢慢的提高词汇量。 一年多前萌发的这个想法,于是按照这个思路写了一个字幕注释的小程序,可惜只写了一大半,然后由于工作的原因,就停了,最近突然想起这个东西,于是想能够把这个程序完成。(最近射手网和人人字幕组的关闭,让我觉得这个软件的必要) 整个程序的设计思路是这样的: 1.第一次运行这个程序时,需要设置词汇量,根据柯林斯词典提供的词频分级表,按词频分成5到0级词汇,0级最难最少用,5级最常用。用户根据对自己的估计进行选择,如果英语烂,那么就选择只认识5级词汇。如果英语不错,过了四六级那么可以选择4级或者3级,如果是英语专8水平啥的,可以选择更高级的词汇。选中后就会把这些等级的词汇记录到已认识的词汇表中(如果有些词不认识,可以通过用户词汇管理功能进行调整). 2.提供生词本导入功能,如果用户是开心词场,有道词典之类的软件的用户,那么可以将这些软件的记录导入到这个程序中,便于完善用户自己的认识和不认识的词汇列表。 3.用户下载带有英文字幕的srt或者ass格式的字幕文件,这个字幕文件可以是全英文的,也可以是中文英文都包含的,程序会将中文字幕全部移除,只保留英文字幕部分。 4.对英文字幕中的每个句子进行转换和分解,分解成词汇,然后用分解出的词汇和用户词汇表进行比对,如果发现是用户认识的单词,那么就忽略,如果是用户不认识的单词,那么就查询字典(默认采用的是维科英汉词典10W词汇,基本满足日常词汇需要),得到该单词的中文解释,如果词典中查不到这个词,那么就忽略,查的到就显示出来。 5.用户根据显示出来的所有词,再选择哪些是认识的,如果认识就可以标记为认识,以后也不会被注释。如果是不认识的,那么可能这个词存在多种注释,用户可以选择哪种注释在这个句子中更合理。 6.根据用户选择,把英文字幕进行替代,不认识的词汇会在旁边加上简短的中文注释。如果用户觉得整句话都很难,想把整句话都翻译了,那么可以调用网上的翻译服务(有道,百度,微软,谷歌),对整句话进行翻译。 7.用户可以进一步在界面上手工调整注释后的字幕,然后只需要保存这个替换后的英文字幕,然后用播放软件导入这个新字幕即可。 在编写这个程序的时候,遇到了很多关于英语上的问题,挺有意思的,下面列举一下: 1.如何得到一个单词的原型。 英语单词有很多种变形,比如复数+s/es,过去式+ed,现在进行时+ing,比较级+er等,我们一般不会说认识单词do,却不认识单词doing,程序必须找到doing的原型do,然后再到用户词汇表中去查用户是否认识do这个词。这个我之前的处理办法很复杂,现在的处理办法很高效,很实用。 2.如何知道一个词是人名/地名。 在美剧中必然会大量的出现人名地名啥的,如果人名本身没有其他意义那还好说系统会忽略,但是如果人名有其他意义就会对整个句子的意思造成影响。比如He is Bush.这么一个句子,如果把Bush作为单词,那么就会翻译成“他是灌木丛”,这也太搞了,这里程序应该意识到Bush是人名,对于人名就不需要翻译。怎么知道一个单词是人名呢?我目前的做法简单粗暴直接,维护了一个常见的人名列表,如果首字母大写的,那么就查询这个人名列表,存在则说明是人名,不存在就当普通词汇处理。地名目前没有维护,没有处理,毕竟地名出现的频率没有人名高。 3.对于一词多性多义,怎么判断取哪个意义。 有些单词既可以做动词用,也可以做名词用,当名词时和动词时的意思完全不一样,这是一个问题。比如book,可以做n.那么就是书的意思,也可以做v.那么就是预定的意思。这两个意思毫无关联,那么怎么确定一个句子中的book到底是哪个http://blog.sina.com.cn/s/blog_48b0011f0102v6zc.html意思呢? 一个是看是否变形,如果是booking或者booked,那么这是动词的变形,所以必然取动词的解释:预定。 二是看前后单词的词性,如果book的前面是adj.那么这里的book就是名词。 三分析整个句子,看book是做谓语还是主语/宾语,如果是谓语那就是动词,主语宾语就是名词。 4.对于一词多义,而且词性还相同,那怎么取。 这个有难度,我程序没办法解决,把每个意义都列出来,让用户根据上下文,自己选择。比如I like this date.这里Date可以是日期的意思,可以是约会的意思,也可以是枣子的意思,从语法上讲都是对的,只有根据上下文,让用户自己选择。 5.原型和变形是两个单词,那么怎么决定采用原型还是变形。 比如comforting是令人欣慰的意思,adj.,但是如果查原型comfort,只有n.和v.所以不能将comforting转换成原型再查下其意思,在作为形容词的时候,必须保持这个形式。还是只有从词性入手,如果是形容词,那么后面应该跟名词,如果是动词ing形式,那么应该是现在进行时的语法形式。
我在4年多前,写了一篇Excel处理空白Cell的文章,http://www.cnblogs.com/studyzy/archive/2010/04/07/1706203.html,其实在数据库中也会遇到这种情况。对于普通的OLTP系统来说,应该不会出现,主要是在做OLAP,导入外部数据源时,可能导入系统的就是带有空白记录的数据。 为了方便说明,我举了一个简单的例子,假设一个学生成绩表,有字段“学生ID”和“成绩”,学生ID是主键,自增,成绩只有NULL和1,2,3,4,5这几个值。在录入学生成绩的时候,如果成绩为NULL,就表示该学生成绩和上一个学生的成绩相同。现在要查询某个学生ID的成绩,该怎么查呢?或者要将成绩字段改为不允许为空,怎么把所有NULL的行填上成绩呢? 首先我们先建立示例表: 1 create table t1 2 ( 3 ID int identity primary key, 4 Score int null 5 ); 6 insert t1 7 values(3),(4),(null),(3),(null),(null),(5); 8 9 select *10 from t1 从结果我们可以看到如果要查询学生6的成绩,那么应该先去查学生5的成绩,由于学生5也是空,所以要继续查前一个学生4的成绩,得到分数3,所以学生6的成绩是3.这显然是一个递归问题,如果一直是空,会继续递归下去,直到找到一个成绩为止。要在SQL中使用递归,那么第一个应该想到的就是公用表表达式CTE。关于CTE的语法和说明可以看MSDN:https://msdn.microsoft.com/zh-cn/library/ms186243.aspx 那么我们这里递归的终点是什么呢?是不为空的成绩,递归的链接条件是上一个学生ID=当前学生ID-1.于是我们可以将此次的公用表表达式写为: 1 with t 2 as 3 ( 4 select * from t1 where Score is not null 5 union all 6 select t1.ID,t.Score 7 from t 8 inner join t1 9 on t.ID+1=t1.ID10 where t1.Score is null11 )12 select *13 from t14 order by ID; 得到的结果为: 这里的情况比较特殊ID是连续的,那么如果ID不连续会怎么样呢?我们试着删除ID=5 delete from t1 where ID=5 这个时候如果还是运行上面的CTE就会查不到ID=6的记录,因为inner join的条件不成立了。那么简单的办法就是使用开窗函数给每一行数据增加一列连续自增的列,SQL Server中的函数是ROW_NUMBER().这样就变成了两个CTE嵌套使用,请看代码: 1 with t1new 2 as 3 ( 4 select *,ROW_NUMBER() over(order by ID) as RowNo 5 from t1 6 ) 7 , t 8 as 9 (10 select Id,Score,RowNo from t1new where Score is not null11 union all12 select t1new.ID,t.Score,t1new.RowNo13 from t14 inner join t1new15 on t.RowNo+1=t1new.RowNo16 where t1new.Score is null17 )18 19 select *20 from t21 order by ID 公用表表达式真的很强大,另外在使用View出Report的时候,也可以用CTE,因为在View中不能用临时表,所以使用CTE代替临时表是个不错的解决方案。
今天在读一篇关于数据库索引介绍的文章时,该文章提到了前缀索引,对于我这个搞数据库应用开发那么多年的人来说,这个词还真是一个新词,没用过。于是打算研究一番。 前缀索引似乎是MySQL中的一个概念,在SQL Server和Oracle中没提出这个概念。于是就安装了一个MySQL来做实验,搞清楚前缀索引。 前缀索引说白了就是对文本的前几个字符(具体是几个字符在建立索引时指定)建立索引,这样建立起来的索引更小,所以查询更快。有点相当于Oracle中对字段使用Left函数,建立函数索引,只不过MySQL的这个前缀索引在查询时是内部自动完成匹配的,并不需要使用left函数。 别的文章中提到: MySQL 前缀索引能有效减小索引文件的大小,提高索引的速度。但是前缀索引也有它的坏处:MySQL 不能在 ORDER BY 或 GROUP BY 中使用前缀索引,也不能把它们用作覆盖索引(Covering Index)。 建立前缀索引的语法为: ALTER TABLE table_name ADD KEY(column_name(prefix_length)); 这里最关键的参数就是prefix_length,这个值需要根据实际表的内容,得到合适的索引选择性(Index Selectivity)。索引选择性就是不重复的个数与总个数的比值。 select 1.0*count(distinct column_name)/count(*)from table_name 比如我们现在有个Employee表,其中有个FirstName字段,是varchar(50)的,我们查询该字段的索引选择性: select 1.0*count(distinct FirstName)/count(*)from Employee 得到结果0.7500,然后我们希望对FirstName建立前缀索引,希望前缀索引的选择性能够尽量贴近于对整个字段建立索引时的选择性。我们先看看3个字符,如何: select 1.0*count(distinct left(FirstName,3))/count(*)from Employee 得到的结果是0.58784,好像差距有点大,我们再试一试4个字符呢: select 1.0*count(distinct left(FirstName,4))/count(*)from Employee 得到0.68919,已经提升了很多,再试一试5个字符,得到的结果是0.72297,这个结果与0.75已经很接近了,所以我们这里认为前缀长度5是一个合适的取值。所以我们可以为FirstName建立前缀索引: alter table test.Employee add key(FirstName(5)) 建立前缀索引后查询语句并不需要更改,如果我们要查询所有FirstName为Devin的Employee,那么SQL仍然写成: select *from Employee ewhere e.FirstName='Devin'; 下面总结一下什么情况下使用前缀索引: 字符串列(varchar,char,text等),需要进行全字段匹配或者前匹配。也就是=‘xxx’ 或者 like ‘xxx%' 字符串本身可能比较长,而且前几个字符就开始不相同。比如我们对中国人的姓名使用前缀索引就没啥意义,因为中国人名字都很短,另外对收件地址使用前缀索引也不是很实用,因为一方面收件地址一般都是以XX省开头,也就是说前几个字符都是差不多的,而且收件地址进行检索一般都是like ’%xxx%’,不会用到前匹配。相反对外国人的姓名可以使用前缀索引,因为其字符较长,而且前几个字符的选择性比较高。同样电子邮件也是一个可以使用前缀索引的字段。 前一半字符的索引选择性就已经接近于全字段的索引选择性。如果整个字段的长度为20,索引选择性为0.9,而我们对前10个字符建立前缀索引其选择性也只有0.5,那么我们需要继续加大前缀字符的长度,但是这个时候前缀索引的优势已经不明显,没有太大的建前缀索引的必要了。
键盘输入 调用edit函数,比如我们要让用户输入一个长度为5的向量并赋值给变量a,那么可以: a<-vector("integer",5) a<-edit(a) 另外也可以用函数fix来直接编辑变量,而不需要再赋值变量。所以上面编辑a变量的命令可以改为: a<-vector("integer",5) fix(a) 读取文本文件 read.table函数可以读取csv文件,也可以读取其他分隔符分割的文本文件。如果是Tab键分割,那么就是“\t”比如: y<-read.table("hw1_data.txt",header=TRUE,sep=“\t”) 如果是标准的CSV文件,那么可以使用read.table还可以使用read.csv函数读取: x<-read.csv("hw1_data.csv") 读取Excel格式的文件 Excel格式分为老的xls和新的xlsx两种,其实读取方法是一样的,一般现在使用的都是xlsx格式的Excel文件了,要读取这种格式的文件,需要安装package: xlsx。 library(xlsx) excelFile<-"test1.xlsx" excel<-read.xlsx(excelFile,1) 最后那个参数1表示读取第一个Sheet,如果要读取第二个Sheet就将该参数改为2. 读取Url 如果我们想直接读取一个Url文件,那么可以使用url函数建立一个connection,然后使用readLines函数得到该Url的内容。 比如: b<-url("http://www.baidu.com") html<-readLines(b) 这些需要说明的是,除了HTTP协议,还可以使用ftp协议file://共享文件夹。另外还可以设置访问网络的代理。 读取数据库 如果需要在R中连接数据库,主要是使用ODBC来连接,需要安装包RODBC。如果是Linux或者Mac平台,对于MySQL数据库,可以安装RMySQL包。 因为我现在是Mac,就以MySQL为例,我在MySQL的test数据库中建立了一个表Employee,现在需要读取该表。 library(RMySQL) conn<-dbConnect(MySQL(),dbname="test",host="127.0.0.1") 接下来我们要查看有哪些表,可以: dbListTables(conn) 如果我要查询Employee表中的所有数据,那么: emp<-dbGetQuery(conn,"select * from Employee”) 查询完了数据库记得关闭连接,这是一个好习惯: dbDisconnect(conn) 如果我们连接的不是MySQL,那么就需要安装对应的数据库连接的包。比如: ROracle RPostgreSQL RSQLite 另外也可以用JDBC来访问数据库,包是RJDBC
Data Frame一般被翻译为数据框,感觉就像是R中的表,由行和列组成,与Matrix不同的是,每个列可以是不同的数据类型,而Matrix是必须相同的。 Data Frame每一列有列名,每一行也可以指定行名。如果不指定行名,那么就是从1开始自增的Sequence来标识每一行。 初始化 使用data.frame函数就可以初始化一个Data Frame。比如我们要初始化一个student的Data Frame其中包含ID和Name还有Gender以及Birthdate,那么代码为: student<-data.frame(ID=c(11,12,13),Name=c("Devin","Edward","Wenli"),Gender=c("M","M","F"),Birthdate=c("1984-12-29","1983-5-6","1986-8-8”)) 另外也可以使用read.table() read.csv()读取一个文本文件,返回的也是一个Data Frame对象。读取数据库也是返回Data Frame对象。 查看student的内容为: ID Name Gender Birthdate 1 11 Devin M 1984-12-29 2 12 Edward M 1983-5-6 3 13 Wenli F 1986-8-8 这里只指定了列名为ID,Name,Gender和Birthdate,使用names函数可以查看列名,如果要查看行名,需要用到row.names函数。这里我们希望将ID作为行名,那么可以这样写: row.names(student)<-student$ID 更简单的办法是在初始化date.frame的时候,有参数row.names可以设置行名的向量。 访问元素 与Matrix一样,使用[行Index,列Index]的格式可以访问具体的元素。 比如访问第一行: student[1,] 访问第二列: student[,2] 使用列的Index或者列名可以选取要访问的哪些列。比如要ID和Name,那么代码为: idname<-student[1:2] 或者是 idname<-student[c("ID","Name”)] 如果是只访问某一列,返回的是Vector类型的,那么可以使用[[或者$来访问。比如我们要所有student的Name,代码为: name<-student[[2]] 或者name<-student[[“Name”]] 或者name<-student$Name 使用attach和detach函数可以使得访问列时不需要总是跟着变量名在前面。 比如要打印所有Name,那么可以写成: attach(student) print(Name) detach(student) 还可以换一种简洁一点的写法就是用with函数: with(student,{ n<-Name print(n) }) 这里的n作用域只在大括号内,如果想在with函数中对全局的变量进行赋值,那么需要使用<<-这样一个运算符。 修改列数据类型 接下来我们查看该对象每列的类型,使用str(student)可以得到如下结果: 'data.frame':3 obs. of 4 variables: $ ID : num 1 2 3 $ Name : Factor w/ 3 levels "Devin","Edward",..: 1 2 3 $ Gender : Factor w/ 2 levels "F","M": 2 2 1 $ Birthdate: Factor w/ 3 levels "1983-5-6","1984-12-29",..: 2 1 3 默认情况下,字符串向量都会被自动识别成Factor,也就是说,ID是数字类型,其他的3个列都被定义为Factor类型了。显然这里Name应该是字符串类型,Birthdate应该是Date类型,我们需要对列的数据类型进行更改: student$Name<-as.character(student$Name) student$Birthdate<-as.Date(student$Birthdate) 下面我们再运行str(student)看看修改后的结果: 'data.frame':3 obs. of 4 variables: $ ID : num 11 12 13 $ Name : chr "Devin" "Edward" "Wenli" $ Gender : Factor w/ 2 levels "F","M": 2 2 1 $ Birthdate: Date, format: "1984-12-29" "1983-05-06" "1986-08-08” 添加新列 对于以及存在的student对象,我们希望增加Age列,该列是根据Birthdate算出来的。首先需要知道怎么算年龄。我们可以使用日期函数Sys.Date()获得当前的日期,然后使用format函数获得年份,然后用两个年份相减就是年龄。好像R并没有提供几个能用的日期函数,我们只能使用format函数取出年份部分,然后转换为int类型相减。 student$Age<-as.integer(format(Sys.Date(),"%Y"))-as.integer(format(student$Birthdate,"%Y”)) 这样写似乎太长了,我们可以用within函数,这个函数和之前提到过的with函数类似,可以省略变量名,不同的地方是within函数可以在其中修改变量,也就是我们这里增加Age列: student<-within(student,{ Age<-as.integer(format(Sys.Date(),"%Y"))-as.integer(format(Birthdate,"%Y")) }) 查询/子集 查询一个Date Frame,返回一个满足条件的子集,这相当于数据库中的表查询,是非常常见的操作。使用行和列的Index来获取子集是最简单的方法,前面已经提到过。如果我们使用布尔向量,配合which函数,可以实现对行的过滤。比如我们要查询所有Gender为F的数据,那么我们首先对student$Gender==“F”,得到一个布尔向量:FALSE FALSE TRUE,然后使用which函数可以将布尔向量中TRUE的Index返回,所以我们的完整查询语句就是: student[which(student$Gender=="F"),] 注意这里列Index并没有输入,如果我们只想知道所有女生的年龄,那么可以改为: student[which(student$Gender=="F"),"Age”] 这样的查询写法还是复杂了点,可以直接使用subset函数,那么查询会简单些,比如我们把查询条件改为年龄<30的女性,查姓名和年龄,那么查询语句为: subset(student,Gender=="F" & Age<30 ,select=c("Name","Age")) 使用SQL查询Data Frame 对于我这种使用了多年SQL的人来说,如果能够直接写SQL语句对Data Frame进行查询操作,那是多么方便美妙的啊,结果还真有这么一个包:sqldf。 同样是前面的需求,对应的语句就是: library(sqldf) result<-sqldf("select Name,Age from student where Gender='F' and Age<30") 连接/合并 对于数据库来说,对多表进行join查询是一个很正常的事情,那么在R中也可以对多个Data Frame进行连接,这就需要使用merge函数。 比如除了前面申明的student对象外,我们再申明一个score变量,记录了每个学生的科目和成绩: score<-data.frame(SID=c(11,11,12,12,13),Course=c("Math","English","Math","Chinese","Math"),Score=c(90,80,80,95,96)) 我们看看该表的内容: SID Course Score 1 11 Math 90 2 11 English 80 3 12 Math 80 4 12 Chinese 95 5 13 Math 96 这里的SID就是Student里面的ID,相当于一个外键,现在要用这个ID进行inner join操作,那么对应的R语句就是: result<-merge(student,score,by.x="ID",by.y="SID") 我们看看merge以后的结果: ID Name Gender Birthdate Age Course Score 1 11 Devin M 1984-12-29 31 Math 90 2 11 Devin M 1984-12-29 31 English 80 3 12 Edward M 1983-05-06 32 Math 80 4 12 Edward M 1983-05-06 32 Chinese 95 5 13 Wenli F 1986-08-08 29 Math 96 正如我们期望的一样join在了一起。 除了join,另外一个操作就是union,这也是数据库常用操作,那么在R中如何将两个列一样的Data Frame Union联接在一起呢?虽然R语言中有union函数,但是不是SQL的Union的意思,我们要实现Union功能,需要用到rbind函数。 rbind的两个Data Frame必须有相同的列,比如我们再申明一个student2,将两个变量rbind起来: student2<-data.frame(ID=c(21,22),Name=c("Yan","Peng"),Gender=c("F","M"),Birthdate=c("1982-2-9","1983-1-16"),Age=c(32,31)) rbind(student,student2)
R语言中有几个常用的函数,可以按组对数据进行处理,apply, lapply, sapply, tapply, mapply,等。这几个函数功能有些类似,下面介绍下这几个函数的用法。 Apply 这是对一个Matrix或者Array进行某个维度的运算。其格式是: Apply(数据,维度Index,运算函数,函数的参数) 对于Matrix来说,其维度值为2,第二个参数维度Index中,1表示按行运算,2表示按列运算。下面举一个例子: m<-matrix(1:6,2,3) 构建一个简单的2行3列的矩阵,内容为: [,1] [,2] [,3] [1,] 1 3 5 [2,] 2 4 6 如果我们要计算每一行的sum值,那么我们可以写为: apply(m,1,sum) [1] 9 12 如果要计算每一列的mean值,那么改为: apply(m,2,mean) [1] 1.5 3.5 5.5 假如某个值为NA,那么要忽略NA值,进行每一行的SUM怎么办呢? m[2,2]<-NA [,1] [,2] [,3] [1,] 1 3 5 [2,] 2 NA 6 apply(m,1,sum) [1] 9 NA 本身sum函数有一个参数na.rm,我们可以将这个参数带人到apply函数中,作为第4个参数: apply(m,1,sum,na.rm=TRUE) [1] 9 8 需要注意的是如果是Data Frame,那么系统会将其转为Matrix,如果所有Column不是数字类型或者类型不一致,导致转换失败,那么apply是运算不出任何一列的结果的。 Lapply 前面说到apply是对于matrix和array的,针对list,我们可以使用lapply函数。该函数接收list,返回的结果也是一个list。其调用如下: Apply(数据,运算函数,函数的参数) 对于Data Frame来说,如果不同的列有不同的数据类型,不能转换成Matrix,但是却可以转换成List,然后使用lapply函数。 我们建立一个学生名字,年龄和成绩的Data Frame,然后统计平均年龄和平均成绩,由于name列不是数值类型,所以无法算平均值,所以我们可以对非数值的数据只取count数量。这里就需要用到自定义函数。 函数可以是匿名函数,也可以是之前定义好的函数,由于这里逻辑简单,我们可以用匿名函数解决。 s<-data.frame(name=c("Devin","Edward","Lulu"),age=c(30,33,29),score=c(95,99,90)) name age score 1 Devin 30 95 2 Edward 33 99 3 Lulu 29 90 lapply(s,function(x){if(is.numeric(x)){mean(x)}else{length(x)}}) $name [1] 3 $age [1] 30.66667 $score [1] 94.66667 我们可以看到返回了一个List的结果,里面包含3个项,每个项是函数执行的结果。lapply返回的结果和传入的List的结构相同,传入多少个Item,返回的也是多少个Item。 Sapply Sapply函数和Lapply函数很类似,也是对List进行处理,只是在返回结果上,Sapply会根据结果的数据类型和结构,重新构建一个合理的数据类型返回。调用格式如下: Apply(数据,运算函数,函数的参数,simplify = TRUE, USE.NAMES = TRUE) 对于其中的simplify参数,就是指明是否对返回的结果集重新组织,如果为FALSE,那么就相当于lapply了。USE.NAMES是对字符串数据处理时,是否使用字符串作为命名的。 还是上面的例子,只是把lapply换成sapply: sapply(s,function(x){if(is.numeric(x)){mean(x)}else{length(x)}}) name age score 3.00000 30.66667 94.66667 我们可以看到结果集变成了一个数字向量,而不是List了。 Mapply 这是对多个数据(multivariate)进行sapply处理,只是调用是参数位置有所变化,先把函数放前面: mapply(运算函数,函数的参数,第一个传入参数,第二个数据…,SIMPLIFY = TRUE,USE.NAMES = TRUE) 比如我们自定义一个函数m3,接受3个数值参数,然后将3个数字相乘返回结果: m3<-function(a,b,c){a*b*c} 然后我们构建3个向量,他们具有相同的长度: a<-1:5 b<-2:6 c<-5:1 现在我们要求a,b,c中的对应各位数进行m3函数的运算,也就是把a,b,c的第一个数做运算,然后把a,b,c的第二个数做运算,然后第三个数~~~这时候就用mapply很方便: mapply(m3,a,b,c) [1] 10 24 36 40 30 OK,就这么简单,实现了对应的各位元素的运算。 Tapply 前面介绍的几个apply函数都是对整体数据进行处理,而tapply是对向量中的数据进行分组处理。先看看tapply函数的调用格式: tapply(向量数据,分组标识,运算函数,函数的参数,simplify = TRUE) 我们以一个学生数据的Data Frame为例来讲解tapply函数,先构建一个新的学生数据,包含name,age,score,class,gender: s<-data.frame(name=c("Devin","Edward","Lulu","Jeneen"),age=c(30,33,29,32),score=c(95,99,90,88),class=c(1,2,1,2),gender=c("M","M","F","F")) name age score class gender 1 Devin 30 95 1 M 2 Edward 33 99 2 M 3 Lulu 29 90 1 F 4 Jeneen 32 88 2 F 如果我们要计算每个班的平均成绩,那么使用tapply的方法是: tapply(s$score,s$class,mean) 1 2 92.5 93.5 如果改为按gender算平均成绩,那么就是: tapply(s$score,s$gender,mean) F M 89 97 如果同时按class和gender来看呢?这里就需要把两个向量构建成list作为第二个参数传入: tapply(s$score,list(s$class,s$gender),mean) F M 1 90 95 2 88 99