https通配符证书配置

通配符证书

即一个证书能给ldsun.comwww.ldsun.combtc.www.ldsun.com等所有的*.ldsun.com域名使用的证书,四不四很爽。

免费证书

使用 let's encrypted 免费开源https证书,目前已经支持通配符证书申请。

使用脚本

使用国人开发的脚本acme.sh简化配置流程。

DNS解析商

鉴于国内DNS服务商的尿性,选择digitalocean作为DNS解析商,抛弃dnspoddnspod在设置证书过程中出现各种问题。

digitalocean需要PayPal支付验证。验证后添加域名,添加解析记录。

配置步骤

1、安装acme脚本

1
2
curl https://get.acme.sh | sh
source ~/.bashrc

2、申请设置 Digitalocean API

1
export DO_API_KEY="753112c4ca779ac39a19f635213b573b49ce92ae126553ebd61ac3a300934834cc"

3、申请通配符证书

1
acme.sh --issue -d ldsun.com -d '*.ldsun.com' --dns

4、安装证书

1
2
3
4
acme.sh --install-cert -d ldsun.com \
--key-file /etc/nginx/ssl/ldsun.com.key \
--fullchain-file /etc/nginx/ssl/ldsun.com.crt \
--reloadcmd "service nginx force-reload"

5、配置Nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;

server_name ldsun.com www.ldsun.com;

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:2368;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location ^~ /wp-content/ {
if (!-e $request_filename) {
rewrite ^/wp-content/uploads/(.*)$ https://obmv8nk23.qnssl.com/$1 last;
break;
}
}

# 我们采用强大的迪菲-赫尔曼密钥交换,生成命令 openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;

ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;

# 此处是证书文件
ssl_certificate /etc/nginx/ssl/ldsun.com.crt;
ssl_certificate_key /etc/nginx/ssl/ldsun.com.key;

# 开启 HSTS Preload 支持
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

access_log /var/log/nginx/ldsun.com.access.log;
error_log /var/log/nginx/ldsun.com.error.log;
}

server {
if ($host = www.ldsun.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = ldsun.com) {
return 301 https://$host$request_uri;
}

listen 80;
server_name ldsun.com www.ldsun.com;
return 404;
}
...

6、续期

1
2
acme.sh --renew -d ldsun.com -d '*.ldsun.com' --force
# 默认是自动更新,添加 --force 强制手动更新

参考

https://sb.sb/blog/linux-acme-sh-lets-encrypt-ssl/
https://sb.sb/blog/linux-lets-encrypt-wildcard-ssl/

从一道菜开始重操旧业——土豆鸡块

闲下来一看,已经四个月没写博客了。

入币圈这么久,这半年多却是最让人“忙”的。忙的记不起更博,虽然中途学了很多新技术,做了几个好菜想要记录,留了一年的辫子也剪成寸头。

正是去年年末,比特币一举达到2w多美元。从那时起,我的微信群、QQ群、小密圈等等爆发式的多了起来,每天微信和qq显示的未读消息都是999+,实际上如果软件支持的话。这个数字可能是9999+。

币圈的信息淹没了我。第一次感到精力完全不够用,每天都有数十个币种、ico在涌来。被信息淹没的后果,不是错过了很多信息,而是开始什么都不看了。是的,每个群,每个公众号都慢慢开始被封印。从一开始的生怕错过一条信息,到后面的无动于衷,中间的转换也是一种经历。

从一个吃瓜码农,到每天账户余额两位数波动。短短的一年,我竟然体验了很多的大起大落。虽然结局比较忧桑,但我却有一丝满足,因为有了这些体验,整个人的世界观价值观有了一次升级。这或许是每个参与到这种“世纪泡沫”中的人都有的体会,一切都是一个轮回,一个泡沫的萌生,高潮,退潮,再次觉醒…能参与到这种泡沫中的人都是“幸福的”,毕竟这种机会少说也是几十年一遇,而一辈子也就几十年。

经历了这么多之后,币圈已经变成常态。生活回归正常。

那么今天的主角登场:土豆鸡块的做法

土豆鸡块算是很实用的家常硬菜,基本是个人都喜欢吃,做法也不算复杂,直接开始把。写完去看IG vs VG九保一思聪战术了。

材料

  • 土豆
  • 鸡块/鸡腿
  • 辣椒、洋葱、红萝卜、木耳…(自选)
  • 葱、姜、蒜、干辣椒(花椒大料选用)
  • 豆瓣酱/火锅底料(选用)
  • 生抽、老抽、料酒(自选)、白糖(自选)

土豆鸡块,当然是土豆和鸡块必不可少,然后葱姜蒜也不可少,生抽老抽必备。其余加的配菜如辣椒、洋葱、红萝卜,以及调味的豆瓣酱、火锅底料都个自由选用。

开搞

1、准备食材:
食材准备,土豆切块洗净(不洗的话淀粉容易粘锅),鸡肉用开水烫洗,洗净血水。可以用料酒,生抽加调料腌制一会,更加入味。其余食材切好备用;

2、炒鸡肉:
开火热锅,倒油。油量根据食材多少自行决定。油七分热时倒入葱、姜、蒜、干辣椒、(火锅底料),炒香后倒入鸡块翻炒,炒至肉变色,倒入老抽给肉上色。期间可加入胡椒粉、豆瓣酱等自选;

3、炒土豆:
倒入土豆(胡萝卜等自选)继续翻炒,翻炒2分钟左右;

4、倒水收汁:
倒入生抽/适量白糖(后续收汁用,根据个人口味选择咸、甜),倒入白开水没过鸡肉和土豆;

5、加菜提味
大火闷煮5分钟左右,这时候土豆和鸡肉已经熟透入味,汤汁也基本见底。这时候加入辣椒洋葱,翻炒一分钟左右,加入鸡精/味精提味。喜欢酸味的可以加入少许醋;

6、出锅。

土豆鸡块的做法也有多种,比如先把土豆炒至金黄捞出等…各个口味不一,感兴趣的自行尝试。以上做法是从母亲学来的祖传做法。

图片

没图说的口都干了。

cmd-markdown-logo
cmd-markdown-logo
cmd-markdown-logo
cmd-markdown-logo
cmd-markdown-logo
cmd-markdown-logo
cmd-markdown-logo
cmd-markdown-logo
cmd-markdown-logo
cmd-markdown-logo

之前做的几次的汇总图,不太全,比较乱…凑合看吧

cmd-markdown-logo

Hyperledger Fabric 环境搭建

所有操作在centos7下完成,其他系统(ubuntu/macos..)类似,修改相应指令即可。

1. 安装Golang

golang官网 获取最新版本 https://dl.google.com/go/go1.10.1.linux-amd64.tar.gz

下载最新版本

1
wget https://dl.google.com/go/go1.10.1.linux-amd64.tar.gz

解压

1
sudo tar -C /usr/local -xzf go1.10.1.linux-amd64.tar.gz

设置环境变量vi ~/.bash_profile,增加以下内容

1
2
3
4
export PATH=$PATH:/usr/local/go/bin 
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:$HOME/go/bin

重新载入环境变量

1
source ~/.bash_profile

至此 golang 安装成功,输入go version查看版本

2. 安装Docker

下载并安装,使用阿里的源:

1
curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh -

替换docker hub源的阿里源,加快镜像下载速度:

1
2
3
4
5
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://obou6wyb.mirror.aliyuncs.com"]
}
EOF

修改完后,重启docker,查看docker版本

1
2
3
systemctl daemon-reload
sudo systemctl restart docker
docker version

3. 安装docker-compose

1、安装pip

1
sudo get install python-pip

2、 下载docker-compose

1
curl -L https://get.daocloud.io/docker/compose/releases/download/1.12.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose

3、 将docker-compose移动至/usr/local/bin文件夹当中,使其作为可执行命令文件

1
mv ./docker-compose /usr/local/bin/docker-compose

4、修改docker-compose权限

1
chmod 755 /usr/local/bin/docker-compose

5、查看版本

1
docker-compose version

4. 下载Fabric源码

切换至go的GOROOT路径下,即第一步安装go的时候配置的路径,也可输入go env查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@VM_162_219_centos ~]# go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/root/go"
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build440227295=/tmp/go-build -gno-record-gcc-switches"

切换至/usr/local/go目录后,进入src代码目录。如果不存在github.com文件夹,则新建一个。在github.com下新建hyperledger文件夹并进入,此时所在路径为:

1
/usr/local/go/src/github.com/hyperledger

在当前目录下,从github拉取fabric源码

1
git clone https://github.com/hyperledger/fabric.git

本次使用1.0.0版本(与后面安装的docker fabric镜像版本保持一致!)

1
2
cd fabric
git checkout v1.0.0

到此为止,fabric源码下载成功。

5. 下载Docker Fabric镜像

切换到下载的fabric源码目录

1
cd /usr/local/go/src/github.com/hyperledger/fabric/examples/e2e_cli

利用源码中的脚本下载1.0.0版本的fabric docker镜像

1
source ./download-dockerimages.sh -c x86_64-1.0.0 -f x86_64-1.0.0

下载完后可以执行docker images查看镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@VM_162_219_centos e2e_cli]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dev-peer1.org2.example.com-mycc-1.0 latest 93a9797573ad 2 hours ago 173 MB
dev-peer0.org1.example.com-mycc-1.0 latest bef7e3211370 2 hours ago 173 MB
dev-peer0.org2.example.com-mycc-1.0 latest 2884f1a84570 2 hours ago 173 MB
docker.io/hello-world latest f2a91732366c 4 months ago 1.85 kB
docker.io/hyperledger/fabric-tools x86_64-1.0.0 0403fd1c72c7 8 months ago 1.32 GB
hyperledger/fabric-tools latest 0403fd1c72c7 8 months ago 1.32 GB
docker.io/hyperledger/fabric-couchdb x86_64-1.0.0 2fbdbf3ab945 8 months ago 1.48 GB
hyperledger/fabric-couchdb latest 2fbdbf3ab945 8 months ago 1.48 GB
docker.io/hyperledger/fabric-kafka x86_64-1.0.0 dbd3f94de4b5 8 months ago 1.3 GB
hyperledger/fabric-kafka latest dbd3f94de4b5 8 months ago 1.3 GB
docker.io/hyperledger/fabric-zookeeper x86_64-1.0.0 e545dbf1c6af 8 months ago 1.31 GB
hyperledger/fabric-zookeeper latest e545dbf1c6af 8 months ago 1.31 GB
docker.io/hyperledger/fabric-orderer x86_64-1.0.0 e317ca5638ba 8 months ago 179 MB
hyperledger/fabric-orderer latest e317ca5638ba 8 months ago 179 MB
hyperledger/fabric-peer latest 6830dcd7b9b5 8 months ago 182 MB
docker.io/hyperledger/fabric-peer x86_64-1.0.0 6830dcd7b9b5 8 months ago 182 MB
docker.io/hyperledger/fabric-javaenv x86_64-1.0.0 8948126f0935 8 months ago 1.42 GB
hyperledger/fabric-javaenv latest 8948126f0935 8 months ago 1.42 GB
docker.io/hyperledger/fabric-ccenv x86_64-1.0.0 7182c260a5ca 8 months ago 1.29 GB
hyperledger/fabric-ccenv latest 7182c260a5ca 8 months ago 1.29 GB
docker.io/hyperledger/fabric-ca x86_64-1.0.0 a15c59ecda5b 8 months ago 238 MB
hyperledger/fabric-ca latest a15c59ecda5b 8 months ago 238 MB
docker.io/hyperledger/fabric-baseos x86_64-0.3.1 4b0cab202084 10 months ago 157 MB

6. 启动Fabric网络

启动Fabric网络实际上是启动下载的Fabric Docker镜像,并执行一系列命令

1
2
3
cd /usr/local/go/src/github.com/hyperledger/fabric/examples/e2e_cli

./network_setup.sh up

最终出现以下提示时启动成功:

1
2
3
4
5
6
7
8
9
10
===================== Query on PEER3 on channel 'mychannel' is successful =====================

===================== All GOOD, End-2-End execution completed =====================


_____ _ _ ____ _____ ____ _____
| ____| | \ | | | _ \ | ____| |___ \ | ____|
| _| | \| | | | | | _____ | _| __) | | _|
| |___ | |\ | | |_| | |_____| | |___ / __/ | |___
|_____| |_| \_| |____/ |_____| |_____| |_____|

启动成功后,可以control+c退出,fabric并不会中止,而是在后台运行在docker中。

7. 使用合约测试Fabric网络

前面下载的fabric源码中包含了一个合约示例,当启动Fabric网络之后,启动了一个名为mychannel的channel及一个名为mycc的链码。

首先连接到Fabric网络所在的docker容器:

1
docker exec -it cli bash

mycc的合约定义了两个账户a和b,分别查看他们的账户余额:

1
2
3
peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}'

peer chaincode query -C mychannel -n mycc -c '{"Args":["query","b"]}'

Query Result为账户余额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@6762dba419ff:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}'
2018-04-03 12:03:14.367 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2018-04-03 12:03:14.367 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity
2018-04-03 12:03:14.367 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-04-03 12:03:14.367 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
2018-04-03 12:03:14.367 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0A95070A6708031A0C0882DB8DD60510...6D7963631A0A0A0571756572790A0161
2018-04-03 12:03:14.367 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: 51A66C3205697E626DDDD25E6995FBCC04BDBD6D602E68959D2995EC13DEC8E1
Query Result: 90
2018-04-03 12:03:14.373 UTC [main] main -> INFO 007 Exiting.....
2018-04-03 12:03:14.373 UTC [main] main -> INFO 007 Exiting.....
root@6762dba419ff:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -C mychannel -n mycc -c '{"Args":["query","b"]}'
2018-04-03 12:08:36.201 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2018-04-03 12:08:36.201 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity
2018-04-03 12:08:36.201 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-04-03 12:08:36.201 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
2018-04-03 12:08:36.201 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0A94070A6608031A0B08C4DD8DD60510...6D7963631A0A0A0571756572790A0162
2018-04-03 12:08:36.201 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: 0E7915778933F605D5B7D04D52FFF7608F390E0AF2A7B37C6C3FA6027747DF56
Query Result: 210
2018-04-03 12:08:36.207 UTC [main] main -> INFO 007 Exiting.....

使用合约的invoke方法,从a账户转账20给b账户:

1
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和b的余额,此时余额已经发生变化。转账成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@6762dba419ff:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}'
2018-04-03 12:03:14.367 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2018-04-03 12:03:14.367 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity
2018-04-03 12:03:14.367 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-04-03 12:03:14.367 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
2018-04-03 12:03:14.367 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0A95070A6708031A0C0882DB8DD60510...6D7963631A0A0A0571756572790A0161
2018-04-03 12:03:14.367 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: 51A66C3205697E626DDDD25E6995FBCC04BDBD6D602E68959D2995EC13DEC8E1
Query Result: 70
2018-04-03 12:03:14.373 UTC [main] main -> INFO 007 Exiting.....
root@6762dba419ff:/opt/gopath/src/github.com/hyperledger/fabric/peer# peer chaincode query -C mychannel -n mycc -c '{"Args":["query","b"]}'
2018-04-03 12:08:36.201 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2018-04-03 12:08:36.201 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining default signing identity
2018-04-03 12:08:36.201 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-04-03 12:08:36.201 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
2018-04-03 12:08:36.201 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0A94070A6608031A0B08C4DD8DD60510...6D7963631A0A0A0571756572790A0162
2018-04-03 12:08:36.201 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: 0E7915778933F605D5B7D04D52FFF7608F390E0AF2A7B37C6C3FA6027747DF56
Query Result: 230
2018-04-03 12:08:36.207 UTC [main] main -> INFO 007 Exiting.....

至此fabric环境搭建成功,测试成功。

退出docker容器exit

停止fabric docker进程,中止fabric网络

1
2
3
cd /usr/local/go/src/github.com/hyperledger/fabric/examples/e2e_cli

./network_setup.sh down

搭建ETH私链并创建ERC20 Token

1、下载ETH钱包

下载 Ethereum Wallet或Mist其一即可,二者功能相同。

2、安装geth

Mac下使用:

1
brew install geth

其余平台参考安装方法

3、编辑创世区块配置文件

genesis.json:(参考示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ludis@MacBook  ~  cd Desktop/ethereum
ludis@MacBook ~/Desktop/ethereum cat > genesis.json
{
"config": {
"chainId": 33,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"nonce": "0x0000000000000033",
"timestamp": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "0x8000000",
"difficulty": "0x100",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x3333333333333333333333333333333333333333",
"alloc": {}
}
^c

4、初始化

初始化区块链,并且创建一个文件夹来存储区块数据。由于第一步下载的钱包,其默认的区块数据存储在~/Library/Ethereum目录,所以初始化时,将存储区块数据的文件夹指向该目录,这样钱包就和我们创建的私链绑定起来:

1
geth init genesis.json --datadir ~/Library/Ethereum

启动私链

1
geth --networkid 10 --rpc --rpcapi "admin,debug,eth,miner,net,personal,shh,txpool,web3" rpcaddr "0.0.0.0" --rpccorsdomain "*" --nodiscover  --dev console

终端另开一个窗口,连接本地私链节点,开始挖矿。miner.start(1)传的参数为挖矿所用的线程。

1
2
geth attach 'http://127.0.0.1:8545'
miner.start(1)

如果提示挖矿账户错误,则需要设置挖矿账户为自己的钱包地址:

1
miner.setEtherbase("7df9a875a174b3bc565e6424a0050ebc1b2d1d82")

5、启动钱包

当私链启动后,打开钱包,这是会显示连接到私链成功。点击LAUNCH APPLICATION进入钱包后,创建一个账户即可。

如果打开钱包,显示在同步节点数据,则说明本地私链启动失败,或与钱包绑定失败。

6、创建ERC20 token

创建一个基于ERC20规范的token,合约代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
pragma solidity ^0.4.19;

// ----------------------------------------------------------------------------------------------
// Sample fixed supply token contract
// Enjoy. (c) BokkyPooBah 2017. The MIT Licence.
// ----------------------------------------------------------------------------------------------

// ERC Token Standard #20 Interface
// https://github.com/ethereum/EIPs/issues/20

contract ERC20Interface {
// 获取总的支持量
function totalSupply() constant returns (uint256 totalSupply);

// 获取其他地址的余额
function balanceOf(address _owner) constant returns (uint256 balance);

// 向其他地址发送token
function transfer(address _to, uint256 _value) returns (bool success);

// 从一个地址想另一个地址发送余额
function transferFrom(address _from, address _to, uint256 _value) returns (bool success);

//允许_spender从你的账户转出_value的余额,调用多次会覆盖可用量。某些DEX功能需要此功能
function approve(address _spender, uint256 _value) returns (bool success);

// 返回_spender仍然允许从_owner退出的余额数量
function allowance(address _owner, address _spender) constant returns (uint256 remaining);

// token转移完成后出发
event Transfer(address indexed _from, address indexed _to, uint256 _value);

// approve(address _spender, uint256 _value)调用后触发
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}

//继承接口后的实例

contract FixedSupplyToken is ERC20Interface {
string public constant symbol = "XC"; //单位
string public constant name = "X Coin"; //名称
uint8 public constant decimals = 18; //小数点后的位数
uint256 _totalSupply = 100000000000000000000000000000; //发行总量

// 智能合约的所有者
address public owner;

// 每个账户的余额
mapping(address => uint256) balances;

// 帐户的所有者批准将金额转入另一个帐户。从上面的说明我们可以得知allowed[被转移的账户][转移钱的账户]
mapping(address => mapping (address => uint256)) allowed;

// 只能通过智能合约的所有者才能调用的方法
modifier onlyOwner() {
assert(msg.sender == owner);
_;
}

// 构造函数
function FixedSupplyToken(string _symbol,string _name) public {
owner = msg.sender;
balances[owner] = _totalSupply;
//symbol = _symbol;
//name = _name;
}

function totalSupply() constant public returns (uint256 totalSupply) {
totalSupply = _totalSupply;
}

// 特定账户的代币余额
function balanceOf(address _owner) constant public returns (uint256 balance) {
return balances[_owner];
}

// 转移余额到其他账户
function transfer(address _to, uint256 _amount) public returns (bool success) {
if (balances[msg.sender] >= _amount && _amount > 0 && balances[_to] + _amount > balances[_to]) {
balances[msg.sender] -= _amount;
balances[_to] += _amount;
// balances[交易所地址] += 1;
Transfer(msg.sender, _to, _amount);
return true;
} else {
return false;
}
}

//从一个账户转移到另一个账户,前提是需要有允许转移的余额
function transferFrom(
address _from,
address _to,
uint256 _amount
) public returns (bool success) {
if (balances[_from] >= _amount
&& allowed[_from][msg.sender] >= _amount
&& _amount > 0
&& balances[_to] + _amount > balances[_to]) {
balances[_from] -= _amount;
allowed[_from][msg.sender] -= _amount;
balances[_to] += _amount;
Transfer(_from, _to, _amount);
return true;
} else {
return false;
}
}

//允许账户从当前用户转移余额到那个账户,多次调用会覆盖
function approve(address _spender, uint256 _amount) public returns (bool success) {
allowed[msg.sender][_spender] = _amount;
Approval(msg.sender, _spender, _amount);
return true;
}

//返回被允许转移的余额数量
function allowance(address _owner, address _spender) constant public returns (uint256 remaining) {
return allowed[_owner][_spender];
}
}

7、创建项目

1. 创建一个node项目,来实现合约的编译、部署功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ludis@MacBook  ~  cd Desktop/contract
ludis@MacBook ~ /Desktop/contract npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (contract)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /Users/ludis/Desktop/contract/package.json:

{
"name": "contract",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}


Is this ok? (yes)

一路回车后初始化了一个node项目,接着安装所需的依赖:

1
ludis@MacBook  ~/Desktop/contract cnpm i -S ganache-cli solc web3

创建以下目录结构:

1
2
3
4
5
6
7
.
├── build
├── compile.js
├── contracts
├── deploy.js
├── node_modules
└── package.json

将第六步的合约命名为FixedSupplyToken.sol保存在contracts文件夹

2. compile.js为编译合约用的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//compile.js 赋值编译智能合约

//require('库名/路径')
const solc = require('solc');
//fs: file system
const fs = require('fs');

const path = require('path');


//1.读取智能合约
//readFileSync 同步读取文件

//__dirname =>返回当前路径
console.log(__dirname);
//resolve路径拼接的方法
const filepath = path.resolve(__dirname,'contracts','FixedSupplyToken.sol');
const contractFile = fs.readFileSync(filepath,'utf8');

// console.log(contractFile);

//通过solc库来编译智能合约

var output = solc.compile(contractFile,1);
// console.log(output);
//output = {contracts:'',errors:'',sourceLiser:''}
const contracts = output.contracts;
// console.log(contracts);
//for in 操作: contract = 对象 中的 属性名
for (let contract in contracts) {
// console.log(contract)
//contracts[contract]
//写文件 fs
// console.log(contract);
let newfilepath = path.resolve(__dirname, 'build', contract.replace(':','') + '.json');
// console.log(newfilepath);
//fs.writeFileSync(目标路径, 写入内容)
//对象, tostring()方法, 返回是一个[object object] 字符串
//JSON.stringify => 将对象装换为字符串
fs.writeFileSync(newfilepath, JSON.stringify(contracts[contract]));
}
//console.log(contracts);

3. deploy.js为部署合约用的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

const ganache = require('ganache-cli');
const Web3 = require('web3');
const fs = require('fs');
const path = require('path');

// web3 小写是实例, Web3 是类
// web3 的构造函数需要传入一个rpc 地址 => provider(http, websocket)
// 内存虚拟节点
// const web3 = new Web3(ganache.provider());
// 直接通过 http://127.0.0.1:8545链接到自己的节点
const web3 = new Web3('http://127.0.0.1:8545');

// 获取编译后的合约代码
const contractpath = path.resolve(__dirname,'build/FixedSupplyToken.json')
// 解析成为json对象
const contract = JSON.parse(fs.readFileSync(contractpath,'utf8'));

// 构建一个智能合约对象

let myContract = new web3.eth.Contract(JSON.parse(contract.interface));

web3.eth.getAccounts()
.then(accounts => {
// web3.personal.unlockAccount(eth.accounts[0],"asdqwe123", 15000)
myContract.deploy({
data: '0x' + contract.bytecode,// bytecode
arguments: ["MT", 'My TOKEN'] // 合约构造函数参数s
})
.send({
from:accounts[0],
gas: 1500000, // gas 消耗最大值
gasPrice: web3.utils.toWei("0.0000002", "ether")
})
.then(d => { // 部署合约的返回结果是实例对象
d.methods.name().call().then(r => {
console.log(r);
});
// 合约方法调用
// 合约实例.methods.方法名(方法参数).call()
d.methods.totalSupply().call().then(r => {
console.log(r);
})
// console.log(d)
})
})

8、编译部署

编译: node compile.js,编译成功会在build文件夹生成编译完的json文件

部署: node deploy.js,部署时会提示钱包账户解锁问题,只需终端执行web3.personal.unlockAccount(eth.accounts[0],"password", 15000),然后再部署即可。

部署成功后终端挖矿页面会显示区块的打包信息,当合约部署成功时会显示合约地址。然后把合约地址添加到钱包的contracts->token中,就会显示该合约下的token数量。可以新建多个用户转账测试。

DAPP结合IPFS — 去中心化图床

内容:打造一款去中心化图床,用户可上传图片至IPFS上,文件hash保存在以太坊的区块上,以此实现永存的去中心化图床。

技术栈:依旧使用truffle框架快速构建项目truffle unbox react

1、什么是 IPFS

星际文件系统IPFS(InterPlanetary File System)是一个面向全球的、点对点的分布式版本文件系统,目标是为了补充(甚至是取代)目前统治互联网的超文本传输协议(HTTP),将所有具有相同文件系统的计算设备连接在一起。原理用基于内容的地址替代基于域名的地址,也就是用户寻找的不是某个地址而是储存在某个地方的内容,不需要验证发送者的身份,而只需要验证内容的哈希,通过这样可以让网页的速度更快、更安全、更健壮、更持久。

直白了说,就是类似BT下载的p2p文件存储、传输系统。

2、安装 IPFS

IPFS官网下载对应系统的安装包(需要翻墙)
以Mac为例,终端执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cd /Users/ludis/Downloads

tar xvfz go-ipfs_v0.4.13_darwin-amd64.tar.gz

cd go-ipfs

mv ipfs /usr/local/bin/ipfs

// 创建本地节点
ipfs init

// 查看节点ID
ipfs id

// 启动节点服务器(可以上传文件至外网/同步外网文件)
ipfs daemon

ipfs节点服务器启动之后可以浏览器访问http://localhost:5001/webui查看管理界面,有本地配置信息、节点连接信息、本地节点文件信息等等。

3、IPFS 基本操作

1、新建文件并添加至IPFS节点

1
2
3
4
5
6
7
ludis@MacBook ~/Desktop/test cat > file.txt
hello ipfs!
^C
ludis@MacBook ~/Desktop/test cat file.txt
hello ipfs!
ludis@MacBook ~/Desktop/test ipfs add file.txt
added QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte file.txt

将文件添加至ipfs节点后,会返回文件的hash,上例QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte

2、查看IPFS上的文件

1
2
ludis@MacBook ~/Desktop/test ipfs cat QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte
hello ipfs!

此时文件只是添加到了本地的IPFS节点,可以通过终端读取到。
当通过ipfs daemon启动本地节点服务器后,也可以通过http://localhost:8080/ipfs/QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte访问到文件。
在启动节点服务器后,会将本地节点文件同步至外网,当同步完成后,就可以通过https://ipfs.io/iofs/QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte访问到文件。至此你的文件已经永存在ipfs网络上了!
由于目前IPFS网络暂未加入代币机制,所以存储读取文件均免费,当然了,速度也慢很多。

3、下载IPFS上的文件

1
2
3
ludis@MacBook ~/Desktop/test ipfs get QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte
Saving file(s) to QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte
20 B / 20 B [========================================] 100.00% 0s

通过get命令,会下载文件到当前目录。

4、新建目录

1
2
3
4
5
6
7
8
ludis@MacBook  ~/Desktop/test  ipfs files mkdir /ludis
ludis@MacBook ~/Desktop/test ipfs files cp /ipfs/QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte /ludis/readme.txt
ludis@MacBook ~/Desktop/test ipfs files ls
ludis
ludis@MacBook ~/Desktop/test ipfs files ls /ludis
readme.txt
ludis@MacBook ~/Desktop/test ipfs files read /ludis/readme.txt
hello ipfs!

5、上传整个目录

1
ipfs add -r files/

上传整个目录时,所有文件都有其对应的hash,并且每个目录都有其hash,访问某个文件有两种方式:

  1. 直接通过问价hash访问
  2. 通过目录hash/文件名访问

IPFS还有很多有趣的地方。例如可以上传一个静态网站到ipfs,并通过浏览器访问,这样就创建了一个永存的网站。往IPFS上传相同的文件,由于hash相同,系统会自动识别,只给后上传的用户建立已有文件的索引,而不是上传一份相同的文件,这样节省很多空间。同时如果一个文件是在另一个文件的基础上修改了写内容而导致hash不同,那么他们相同的内容也不会重复存储,而只是在原文件上拼接不同的部分。总之,IPFS系统有很多高明之处,需要仔细研究。

4、设置跨域

当我们在前端通过js接口操作ipfs时,会遇到老生常谈的跨域问题,只需终端执行以下配置即可:

1
2
3
4
5
ipfs config —json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT","GET", "POST", "OPTIONS"]'
ipfs config —json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
ipfs config —json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
ipfs config —json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]'
ipfs config —json API.HTTPHeaders.Access-Control-Expose-Headers '["Location"]'

5、IPFS 与 DAPP 结合

由于以太坊区块的特性,往区块上存储大文件显然是不合理的。所以通用做法是文件存储到IPFS,之后将文件的hash存储到以太坊区块。当读取时,首先从以太坊区块上取到文件的hash,然后通过hash去IPFS网络上读取文件。

依然使用之前的truffle unbox react创建项目,不同的是只需要多安装一个依赖库ipfs-api,直接cnpm i -S ipfs-api安装即可,显而易见,这就是IPFS系统的js api,这样我们就能在前端调用IPFS的接口上传、读取文件。

话不多说,直接上代码,一个是智能合约,一个是前端react文件,合约交互前几篇已经比较熟悉了,主要看一下怎么通过ipfs-api上传下载文件。一切尽在代码中:

SimpleStorage.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity ^0.4.19;

contract SimpleStorage {

string[] public photoArr;

mapping(address => uint) storeAddress;

function storePhoto(string hash) public {
if(storeAddress[msg.sender]==0){
photoArr.push(hash);
storeAddress[msg.sender] = 1;
}
}

function getPhoto(uint index) public view returns (uint, string){
if(photoArr.length==0){
return (0, "");
}else{
return (photoArr.length, photoArr[index]);
}
}

function isStored() public view returns (bool) {
if(storeAddress[msg.sender]==0){
return false;
}else{
return true;
}
}

}

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import React, {Component} from 'react'
import SimpleStorageContract from '../build/contracts/SimpleStorage.json'
import getWeb3 from './utils/getWeb3'

import './css/oswald.css'
import './css/open-sans.css'
import './css/pure-min.css'
import './App.css'

import ipfsAPI from 'ipfs-api'
const ipfs = ipfsAPI({host: 'localhost', port: '5001', protocal: 'http'})

const contractAddress = "0x7ebeb83816b74da8173e3f406aeac012cf1718f5"
let simpleStorageInstance

// Promise 存储文件至ipfs
let saveImageOnIpfs = (reader) => {
return new Promise(function (resolve, reject) {
const buffer = Buffer.from(reader.result);
ipfs.add(buffer).then((response) => {
console.log(response)
resolve(response[0].hash);
})
.catch((err) => {
console.error(err)
reject(err);
})
})
}

class App extends Component {
constructor(props) {
super(props)

this.state = {
photos: [],
count: 0,
web3: null
}
}

componentWillMount() {
// Get network provider and web3 instance. See utils/getWeb3 for more info.
getWeb3.then(results => {
this.setState({web3: results.web3})
this.instantiateContract()
}).catch(() => {
console.log('Error finding web3.')
})
}

instantiateContract() {
const that = this
const contract = require('truffle-contract')
const simpleStorage = contract(SimpleStorageContract)
simpleStorage.setProvider(this.state.web3.currentProvider)

this.state.web3.eth.getAccounts((error, accounts) => {
simpleStorage.at(contractAddress).then((instance) => {
simpleStorageInstance = instance
})
.then(result => {
console.log('inint success')
return simpleStorageInstance.getPhoto(0)
})
.then(result => {
console.log(result)
let imgNum = result[0].c[0]
if(imgNum===0){
return
}
if(imgNum===1){
this.setState({
count: imgNum,
photos: this.state.photos.concat([result[1]])
})
}
if(imgNum>1){
// 闭包,读取存储的所有图片
for(let i=0;i<imgNum;i++){
(function(i){
simpleStorageInstance.getPhoto(i)
.then(result => {
that.setState({
photos: that.state.photos.concat([result[1]])
})
})
})(i)
}
}
})
})
}

render() {
let doms = [],
photos = this.state.photos
for(let i=0; i<photos.length;i++){
doms.push(<div key={i}><img src={"http://localhost:8080/ipfs/" + photos[i]}/></div>)
}

return (
<div className="App">
<header>上传图片至ipfs,并保存信息至以太坊区块</header>
<div className="upload-container">
<label id="file">选择图片</label>
<input type="file" ref="file" id="file" name="file" multiple="multip
le"/>
<button onClick={() => this.upload()}>上传</button>
</div>
<div className="img-container">
{doms}
</div>
</div>
);
}

upload() {
console.log("upload");
let isStored = false
simpleStorageInstance.isStored()
.then(result => {
console.log("is stored", result)
if(result) {
isStored = true
}
})
if(isStored) {
alert("每个钱包地址只能上传一张图片哦😯 ~")
return
}
var file = this.refs.file.files[0];
console.log(file)
var reader = new FileReader();
// reader.readAsDataURL(file);
reader.readAsArrayBuffer(file)
reader.onloadend = (e) => {
//console.log(reader);
saveImageOnIpfs(reader).then((hash) => {
console.log(hash);
return simpleStorageInstance.storePhoto(hash, {from: this.state.web3.eth.accounts[0]})
.then(result => {
console.log("写入区块成功", result)
this.setState({
photos: this.state.photos.concat([hash])
})
})
});
}
}

}

export default App

完整代码:GitHub

一个完整的智能合约—区块链上永存的留言

目标:

基于以太坊开发一个完整的DAPP应用,实现留言及随机展示留言功能。

总结:

本次从零开发发哦部署上线约耗时六小时,前期在solidity合约开发上费时较多,主要是对其语言特性不了解,后续在react的动画上有些费时,源于对动画的生疏及类库的选择。
整个的开发流程比较清晰:

  1. 编写、调试合约
  2. 编写react前端页面、与合约交互的逻辑、显示逻辑、动画等
  3. 部署合约,与前端联调
  4. 编译项目,上传代码,配置nginx解析。

补充:

整个项目的的实现比较简单,这里不做具体分析,可以git查看,同时参考前几篇文章即可。这里贴一下solidity合约开发中开始的一些“美(错)好(误)想(代)法(码)”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pragma solidity ^0.4.19;

contract SimpleStorage {

// 定义存储留言的数据结构
struct Message {
string word; // 留言
string time; // 写入时间
}

mapping(address => Message[]) public words; // 以字典形式存储

// 写入留言
function setWord(string s, string t) public {
if(words[msg.sender].length==0) {
addrArr.push(msg.sender);
}
words[msg.sender].push(Message({
word: s,
time: t
}));
}

// 获取某地址对应的所有留言(获取我的所有留言)
function getWordByAddress() public view returns (Message[]) {
return words[msg.sender];
}

// 随机返回十条留言
function getRandomWord() public view returns (Message[]) {
Message[] memory result;
for w, i := range words {
result.push(w)
if(i==9) break;
}
return result;
}

}

solidity熟悉的大佬应该看完几声冷笑。是的,以下是重点!!!:

  1. solidity中的字典不支持枚举/遍历
  2. solidity中对外暴露的函数不能返回字典/结构体/数组(简单的数字类型数组除外)

这两点实在是让我觉得solidity很鸡肋?没有了这两个语言特性,使得智能合约的功能复杂度上限制很大,这或许就是没有“杀手级”应用出现的原因之一。不过话说回来,毕竟solidity还很年轻,相信随着时间推进,这门语言会愈加完善健全。

组织

喜欢智能合约开发的同志,请加qq交流群 236380268

基于React+truffle的完整智能合约构建

内容:使用solidity的truffle框架开发智能合约,前端使用react框架,最终完成智能合约从前端到后端,从开发到部署的完整流程。

1、truffle安装

在nodejs安装完成的环境下,全局安装truffle:cnpm i -g truffle

Truffle Boxes是truffle框架集成的脚手架工具,可以使用这个脚手架快捷的生成完备的DAPP的项目结构,其中集成了前端视图、编译压缩工具等。可以在http://truffleframework.com/boxes/中查看并选择合适的模板来进行项目初始化。

2、项目初始化

这里我在http://truffleframework.com/boxes/上选择react模板来开发DAPP,直接在终端执行truffle unbox React即可完成项目的初始化,期间需要安装nodejs依赖,耐心等待即可(都没个安装进度提示,差评!)。

1
2
3
4
5
ludis@MacBook -> ~/Desktop/golang -> mkdir truffle && cd truffle
ludis@MacBook -> ~/Desktop/golang/truffle -> truffle unbox React
Downloading...
Unpacking...
Setting up...

项目初始化完成之后,目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
.
├── box-img-lg.png
├── box-img-sm.png
├── config
│   ├── env.js
│   ├── jest
│   ├── paths.js
│   ├── polyfills.js
│   ├── webpack.config.dev.js
│   └── webpack.config.prod.js
├── contracts
│   ├── Migrations.sol
│   └── SimpleStorage.sol
├── migrations
│   ├── 1_initial_migration.js
│   └── 2_deploy_contracts.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── scripts
│   ├── build.js
│   ├── start.js
│   └── test.js
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── css
│   ├── fonts
│   ├── index.css
│   ├── index.js
│   └── utils
├── test
│   ├── TestSimpleStorage.sol
│   └── simplestorage.js
├── tmp-8766pK0xGOxZkgsX
│   └── box
├── truffle-config.js
└── truffle.js
  • contractsmigrations文件夹上篇已经介绍过,分别为solidity合约文件及相应配置文件。
  • src目录下是react前端代码。

3、合约编写、编译和部署

在项目初始化完成后,有一个默认的合约示例SimpleStorage.sol,我们直接用这个示例做测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.18;

contract SimpleStorage {
uint storedData;

function set(uint x) public {
storedData = x;
}

function get() public view returns (uint) {
return storedData;
}
}

这个合约代码是最简单的读和写两个操作,也是最基础最常用的操作。

编译过程非常简单,在项目根目录下执行truffle compile即可,会在根目录生成build编译目录。这里的编译与部署合约无关,只是为了后续前端代码的调用。

接着将合约部署到以太坊网络上,同理,可以选择部署到正式网络/测试网络/本地,这里我们选择测试网络,部署过程详见区块链学习 - solidity合约部署,小狐狸选择测试网络,将代码粘贴至左侧编辑器,右侧选择对应选项后即可部署成功,部署成功后我们便能得到部署的十六位的合约地址,以我部署的地址0x2dc69582315624fba54ae69655abea27fd48468e为例。

合约部署

4、前端与合约交互

上步部署完合约后,需要将合约地址记下,后续对合约的所有操作都需要通过合约地址进行。

src目录下便是react的前端代码,index.js作为入口文件:

1
2
3
4
5
6
7
8
9
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
<App />,
document.getElementById('root')
);

可以看到index.js中将App.js作为主组件引入,打开App.js,头部引用为:

1
2
3
import React, { Component } from 'react'
import SimpleStorageContract from '../build/contracts/SimpleStorage.json'
import getWeb3 from './utils/getWeb3'

通过头部引用可以看出前端与合约交互的流程:

  1. 首先前端使用了react框架;
  2. 第二句import SimpleStorageContract from '../build/contracts/SimpleStorage.json'导入的实际上是上步合约代码编译后产生的json文件,该文件中包含两个重要信息:一个是abi字段,通俗讲就是合约代码编译后产生的二进制接口,其中会声明合约代码暴露的可供调用的接口;另一个是bytecode字段,为合约代码的十六进制码。通过这两个重要信息就可以对一个合约进行相应操作。
  3. 导入了web3.jsweb3.js是以太坊提供的一个JavaScript库,他封装了以太坊的JSON RPC API,提供了一系列与区块链交互的JavaScript对象和函数,包括查看网络状态,查看本地账户、查看交易和区块、发送交易、编译/部署智能合约、调用智能合约等,其中最重要的就是与智能合约交互的API。

明白了前端与合约交互的原理,剩下来的就是使用react语法结合web3.js提供的接口对合约操作即可。

App.js文件进行一些修改,实现:在网页输入框中输入值,点击确定后可以修改合约中的值(写操作),写入成功后读取合约中的新值,并自动显示到网页(读操作),也就是通过前端网页对上述合约中定义的的set()get()两个方法的调用。

最终App.js如下,主要修改分以下几点:

  • 将部署完成的合约地址作为变量contractAddress保存,以供后续调用合约使用。
  • 声明simpleStorageInstance变量,作用是通过合约地址得到合约实例后将合约实例保存起来,方便后续调用。
  • 添加输入框及按钮,点击按钮时读取输入框的值,通过合约实例及web3提供的方法修改合约中的storedData值。
  • 使用Promise语法,当写操作成功后,执行读操作,并在读取成功后,修改react的组件状态state中的变量值,使前端显示自动更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import React, { Component } from 'react'
import SimpleStorageContract from '../build/contracts/SimpleStorage.json'
import getWeb3 from './utils/getWeb3'

import './css/oswald.css'
import './css/open-sans.css'
import './css/pure-min.css'
import './App.css'

const contractAddress = "0x2dc69582315624fba54ae69655abea27fd48468e"; // 合约地址
var simpleStorageInstance; // 合约实例

class App extends Component {
constructor(props) {
super(props)

this.state = {
storageValue: 0,
web3: null
}
}

componentWillMount() {
// Get network provider and web3 instance.
// See utils/getWeb3 for more info.

getWeb3
.then(results => {
this.setState({
web3: results.web3
})

// Instantiate contract once web3 provided.
this.instantiateContract()
})
.catch(() => {
console.log('Error finding web3.')
})
}

instantiateContract() {
/*
* SMART CONTRACT EXAMPLE
*
* Normally these functions would be called in the context of a
* state management library, but for convenience I've placed them here.
*/

const contract = require('truffle-contract')
const simpleStorage = contract(SimpleStorageContract)
simpleStorage.setProvider(this.state.web3.currentProvider)

// Declaring this for later so we can chain functions on SimpleStorage.


// Get accounts.
this.state.web3.eth.getAccounts((error, accounts) => {

// 获取合约地址获取合约实例并保存
simpleStorage.at(contractAddress).then((instance) => {
simpleStorageInstance = instance

// Stores a given value, 5 by default.
// return simpleStorageInstance.set(5, {from: accounts[0]})
return;
}).then((result) => {
// Get the value from the contract to prove it worked.
return simpleStorageInstance.get.call(accounts[0])
}).then((result) => {
// Update state with the result.
console.log(result);
return this.setState({ storageValue: result.c[0] })
})
})
}

render() {
return (
<div className="App">
<nav className="navbar pure-menu pure-menu-horizontal">
<a href="#" className="pure-menu-heading pure-menu-link">Truffle Box</a>
</nav>

<main className="container">
<div className="pure-g">
<div className="pure-u-1-1">
<h1>Good to Go!</h1>
<p>Your Truffle Box is installed and ready.</p>
<h2>Smart Contract Example</h2>
<p>If your contracts compiled and migrated successfully, below will show a stored value of 5 (by default).</p>
<p>Try changing the value stored on <strong>line 59</strong> of App.js.</p>
<p>The stored value is: {this.state.storageValue}</p>
</div>
</div>
</main>

<input ref="myvalue" className="myinput"/>
<button
className="mybutton"
onClick={() => {
var num = Number(this.refs.myvalue.value);
console.log("点击了button");
console.log(num);
simpleStorageInstance.set(num, {from: this.state.web3.eth.accounts[0]}).then(() => {
console.log("数据修改成功");
simpleStorageInstance.get.call(this.state.web3.eth.accounts[0]).then((result) => {
console.log("数据读取成功");
console.log(result);
// 修改状态变量的值,在react中,一旦状态变量的值发生变化,就会调用render函数重新渲染UI
this.setState({ storageValue: result.c[0] })
});

})

}}
style={{height: 40,marginLeft: 50}}>修改合约数据</button>

</div>
);
}
}

export default App

5、启动项目,查看效果

启动本地的react项目,直接终端执行npm start即可。会用node在本地启一个3000端口的服务,浏览器会自动跳转到http://localhost:3000,就会显示react编写的前端页面,在输入框修改相应数值,点击确定,然后小狐狸会弹出支付提示(记得小狐狸切换到测试网络)(所有的写操作均需要支付手续费),确认支付后耐心等待被打包成功,就能看到网页上数值的变化了。

本地测试

6、总结

一个完整的覆盖前后端的DAPP实际上就两点,跟传统互联网项目的前后端类似:

  1. 合约编写、部署
  2. 前端调用

基于以太坊开发DAPP实际上比较简单,重点是在合约的逻辑性、安全性上。从这也可以看出来以太坊生态的强大和完整,便捷完备的开发语言、工具,确实是目前继大饼之后最牛的项目。

solidity函数类型及truffle使用

1、solidity类的多继承、重写

solidity类具有多继承的特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.19

contract Animal1 {
uint age;
}

contract Animal2 {
string weight;
}

contract Dog is Animal1, Animal2 {
/** Dog 会继承 Animal1 及 Animal2 两个类 */
}

重写与其他语言相通,即子类的同名函数会覆盖从父类继承的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^ 0.4 .19;

contract Animal {
function testFunc() public pure returns(string) {
return "Animal testFunc";
}
}

/** 子类重写了从父类继承过来的方法,会以子类的方法为基准 */
contract Dog is Animal {
function testFunc() public pure returns(string) {
return "Dog testFunc";
}
}

2、solidity函数的访问权限

solidity函数分为四种访问权限:

  • private: 私有函数。内部正常访问,外部无法访问,子类无法继承。
  • internal: 内部函数。内部正常访问,外部无法访问,子类可继承。
  • public: 公共函数。内部正常访问,外部正常访问,子类可继承。
  • external: 外部函数。内部不能访问,外部正常访问,子类可继承。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pragma solidity ^0.4.19;

contract Animal {
/** public 公有:外部、内部、子类都可使用 */
function testPublic() public pure returns (string) {
return "public";
}
/** private 私有:合约内部可以正常访问 */
function testPrivate() private pure returns (string) {
return "private";
}
/** internal 内部:合约内部可以正常访问 */
function testInternal() internal pure returns (string) {
return "internal";
}
/** external 外部:只能供外部访问 */
function testExternal() external pure returns (string) {
return "external";
}
/** 未做任何修改时,使用pure */
function f() public pure {
testPublic();
testInternal();
testPrivate();
//testExternal(); // 报错,只能供外部访问
}
}

contract Dog is Animal {
/** 继承 public、internal、external 类型的函数 */
}

// Dog.testPublic() 继承,可用
// Dog.testInternal() 继承,不可用(internal函数外部无法访问)
// Dog.testExternal() 继承,可用
// Dog.f() 继承,可用

3、solidity函数中pureviewconstant的区别

solidity函数的完整声明格式为:

function 函数名(参数)  public|private|internal|external  pure|view|constant  无返回值|returns (返回值类型)

首先来个测试案例,根据编辑器提示补齐函数类型,然后总结不同之处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pragma solidity ^0.4.19;

contract Animal {
string homeAddress = "北京市";
uint weight;

/** pure */
function getAge() public pure returns (uint) {
return 30;
}

/** view */
function getCurrentAddress() public view returns (address) {
return msg.sender;
}

/** view */
function getHomeAddress() public view returns (string) {
return homeAddress;
}

function setWeight(uint w) public {
weight = w;
}

/** constant/view都行 */
function getWeight() public constant returns (uint) {
weight = 200;
return weight;
}
}

结论如下:

  • 只有当函数有返回值的情况下,才需要使用pureviewconstant
  • pure: 当函数返回值为自变量而非变量时,使用pure
  • view: 当函数返回值为全局变量或属性时,使用view
  • constant: 可以理解为view的旧版本,与view是等价的

如果一个函数有返回值,函数中正常来讲需要有 pureviewconstant关键字,如果没有,在调用函数的过程中,需要主动去调用底层的call方法。

注:如果一个函数中带了关键字viewconstant,就不能修改状态变量的值。但凡是是带了这两个关键字,区块链就默认只是向区块链读取数据,读取数据不需要花gas,但是不花gas就不可能修改状态变量的值。写入数据或者是修改状态变量的值都需要花费gas。

4、truffle初识

truffle是以太坊solidity编程语言的开发框架。

1.安装truffle

cnpm i -g truffle

2.创建项目工程

1
2
mkdir truffle && cd truffle
truffle init

3.项目结构
contracts目录下存的是solidity合约代码
migrations中存的是js脚本
test中存的是测试用例

4.编写代码、部署、调试
contracts中新建Hello.sol文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.17;

contract Hello {

string weight = "18cm";
string height = "180cm";

function getAge() public pure returns (uint){
return 30;
}

function getWeight() public view returns (string) {
return weight;
}

function getHeight() public constant returns (string) {
return height;
}

function test() public returns (uint) {
return 250;
}
}

注意:

  • 类名Hello需要跟文件名Hello.sol保存一致
  • Migrations.sol文件不能删除

然后在migrations目录下添加对应的js脚本2_depoly_hello.js:

1
2
3
4
5
var myHello = artifacts.require("./Hello.sol");

module.exports = function(deployer) {
deployer.deploy(myHello);
};

代码添加完后,打开终端,切换到项目所在路径,执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//启动测试网络
truffle develop

//编译
compile

//将合约部署到本地测试网络,成功后会返回合约地址(如:0x75c35c980c0d37ef46df04d31a140b65503c0eed)
migrate

//通过合约地址,得到合约对象,赋值给变量c
var c;
Hello.at('0x75c35c980c0d37ef46df04d31a140b65503c0eed').then((obj) => {
c = obj;
})

//调用合约暴露的方法
c.getAge()

//如果合约中暴露出的有返回值的函数,没有用 pure/view/constant 声明,则需要调用底层的 call 方法才能调用方法
//如上面代码中的test方法
c.test.call()

//修改完代码,编译后,重新部署时需重置之前的合约
migrate --reset

补充:调用migrate编译之后会build文件夹,存储的是编译之后生成的json文件,而这个json文件就是合约部署在虚拟机中的形式。

编译生成的的json文件中,有两个重要的额键值:

abi:通俗讲,abiapi类似,都是接口abi是合约的二进制接口。

bytecode:是合约代码的16进制码

通过abibytecode就能完成合约的部署。

5、基本值类型、引用类型

uint为值类型,只能深拷贝
string为引用类型,既可以深拷贝,也可以浅拷贝

uintstring深拷贝(默认的声明方式即为深拷贝):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
string a = "100";
uint b = 100;

// 浅拷贝
// string aa 等同于 string memory aa
function m(string aa) private {
aa = "1000";
}
function n(uint bb) private {
bb = 1000;
}

function f() public {
m(a)
n(b)
}

string类型浅拷贝(加storage关键字):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
string a = "100";
function f() public {
m(a)
}

// 深拷贝
// memory(深拷贝)、 storage(浅拷贝)
// 当函数参数为 storage 类型时,函数类型只能为 private 或 internal,否则报错
function m(string storage aa) private {
//aa = "1000"; // string不能直接修改,需要转换为可变的字节数组
bytes(aa)[0] = '6';
}

function getA() public view returns (string) {
return a;
}

区块链学习 - solidity合约部署

区块链原理

通过https://anders.com/blockchain/hash.html可以直观的了解hashblock(区块)、blockchain(区块链)、distributed(分布)、tokens(代币)各个概念。

开发环境配置

1、 MetaMask钱包

1.MetaMask是什么?

MetaMask是一款在谷歌浏览器Chrome上使用的插件类型的以太坊钱包,该钱包不需要下载,只需要在谷歌浏览器添加对应的扩展程序即可,非常轻量级,使用起来也非常方便。

2.MetaMask安装

chrome浏览器安装MetaMask,直接用MetaMask创建钱包或者通过myetherwallet.com创建钱包,然后用MetaMask导入钱包私钥即可。
开发测试时,最好新建一个测试钱包,万一自己的币搞没就裂了~~

3.MetaMask常用功能


如上图所示,第一个为正式网络,也就是ETH的公链,真实的币就存在公链上。后两个是两个测试网络,点击后便会切换到测试网络,钱包的地址也会随之改变。为了测试方便,在测试网络的钱包中免费获取一些测试用的代币。

4.测试代币获取

Ropsten Test Network测试网络下,点击BUY->ROPSTEN TEST FAUCET会跳转到https://faucet.metamask.io/页面,可以请求获取测试用的eth代币,也可以给别人发送测试代币。操作完成后钱包会显示相应的测试代币数量。

Kovan Test Network测试网络下,获取测试代币的方式是:加入gitter中的faucet组织,链接为https://gitter.im/kovan-testnet/。加入后,MateMask钱包在Kovan Test Network测试网络下,复制当前测试环境下的钱包地址,然后在gitter的社区中,发送自己的钱包地址(@管理员epheph),这位雷锋大兄弟会给你的钱包发送5个测试代币,最后别忘了来句日常感谢~

2、solidity开发环境配置

atom编辑器安装以下两个插件:
autocomplete-solidity
language-ethereum

3、solidity代码示例

solidity代码文件后缀为.sol
以下是完整的,最简单的solidity合约代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// pragam 关键字
// solidity 当前语言
// 0.4 主版本
// .19 副版本
// ^向上兼容,solidity (0.4.19~0.4.99)都可以编译,0.5则不能编译
pragam solidity ^0.4.19;

// construct 关键字声明类
contract Person {
uint age = 0; // 默认类型为internal
string internal name = "佩奇";
string public homeAddress = "北京市朝阳区"; // public声明的属性,会自动生成同名的get函数,返回该属性值
string private company = "孔壹学院";

// 构造函数,与类名同名,首次初始化时调用
function Person() public {
age = 18;
}

// set方法
function setCompany(uint a) public {
age = a;
}
// get方法
function getCompany() view public returns (uint) {
return age;
}

function getCurrentAddres() view public returns (address) {
// msg.sender 返回当前操作合约的钱包的地址
return msg.sender;
}

// 普通函数
function kill() public{
// 析构函数,调用时销毁当前合约
selfdestruct(msg.sender);
}
}

contract ChildPerson is Person {
function test() public {
age = 100;
name = "AWM";
}
}

solidity代码中一定要加;!!!!!!,包括第一句pragam solidity ^0.4.19;

属性访问权限:
public:
1.会生成一个和属性名同名的get函数,这个函数返回属性自己
2.合约内部可直接访问
3.子类可继承属性

internal:
1.合约内部可直接访问in
2.子类可继承属性

private:
1.合约内部可直接访问

4、部署合约

  1. 打开http://remix.ethereum.org/网站,url后会自动补齐当前最先版的solidity``sdk版本,注意自己写的代码向上兼容性。

  2. 将上述代码直接粘贴到左侧编辑器中,会自动编译查错,正常编译通过后,可以在右侧面板点击run选项。各选项解释如下:

    • Environment指合约要部署的网路环境,JavaScript VM是本地的测试网络;injected Web3是发布到公链。

    • Account指部署合约的钱包地址,因为部署合约需要往公链上写数据,所以需要消耗代币。选择本地测试网络的话,有默认的五个免费的钱包,里面各有100以太币。选择部署到公链的话,通过选择MateMask的网络环境(主网或两个测试网络),Account会显示对应的网络环境下的钱包地址(钱包中必须要有代币才行)

    • Gas limitValue默认即可

  3. 完后下方可以选择solidity代码中声明的类,也就是我们要部署到网络中的类,对应上述代码就是PersonChildPerson两个类选择其一。

  4. 之后点击create,支付完所需的代币手续费后,等待部署完成即可,部署成功后便可以在下方看到合约的地址信息,及合约中暴露出来的各种方法,如getAgesetAge等,可以调用这些方法从部署在网络上的合约中写入数据(花费代币)及读取数据(免费),调用kill方法时会执行析构函数selfdestruct,从网络中销毁合约。

至此,一个简单的合约,从代码编写到部署上线的整套流程结束。

Golang channel进阶

将向管道中写入数据的称为“生产者”,从管道中读取数据的称为“消费者”。

1、生产者与消费者关系

在上篇文章中,生产者与消费者是1:1n:1的关系,那么能不能实现1:n的关系嘞?即一个生产者向管道添加数据,多个消费者从管道读取?示例如下:

1:2案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
)

// 生产者 对 消费者 :1 -> 2
func main() {

c := make(chan int)
done := make(chan bool)

// 生产者:大黄
go func() {
for i := 0; i < 100; i++ {
fmt.Println("生成者 大黄:", i)
c <- i
}
close(c)
}()

// 消费者:小明
go func() {
// range 会一直不断检测c管道中的数据,如果有,读取,否则等待,直到显示的close关闭通道
for n := range c {
fmt.Println("消费者:小明:", n)
}
done <- true
}()

// 消费者:大明
go func() {
// n 等价于 <-c
for n := range c {
fmt.Println("消费者:大明:", n)
}
done <- true
}()

<-done
<-done
}

修改上述案例,实现1:n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"fmt"
)

// 生产者 对 消费者 :1 -> n
func main() {

c := make(chan int)
done := make(chan bool)
n := 10
// 生产者:大黄
go func() {
for i := 0; i < 100; i++ {
fmt.Println("生成者生产数据:", i)
c <- i
}
close(c)
}()

for i := 0; i < n; i++ {
// 消费者:小明
go func(idx int) {
// range 会一直不断检测c管道中的数据,如果有,读取,否则等待,直到显示的close关闭通道
for n := range c {
fmt.Println("消费者",idx,"消费数据:", n)
}
done <- true
}(i)
}

for i := 0; i < n; i++ {
<-done
}

}

在循环创建消费者分线程时,使用闭包特性,将i值保存在分线程中。

2、封装channel

为了提高代码可读性、复用性,便于维护,可以将channel封装于函数方法中,channel可作为函数的参数或返回值,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import "fmt"

// 生产者和消费者一定是配对的状态

func main() {
// c是一个管道
c := incrementor() //把管道作为返回值
cSum := puller(c) //把管道作为参数
for n := range cSum {
fmt.Println(n) // 0 ...9
}
}

// 类型:func () chan int
// chan int 返回值类型
func incrementor() chan int {
// 创建一个管道
out := make(chan int)
// 通过主线程创建一个分线程
go func() { //子线程
for i := 0; i < 10; i++ {
out <- i //生产数据
}
close(out)
}()
// 返回out管道
return out
}

// 函数类型:func (chan int) chan int
// 返回值类型:chan int
// 参数类型:chan int
func puller(c chan int) chan int {
// 创建一个新的管道
out := make(chan int)
// 创建一个子线程
go func() {
var sum int

// <-c
// 通过range取读取管道c里面的数据,这个for跳出循环的时间为管道c被关闭
for n := range c {
sum += n
}

// out <- sum 什么时候执行?
out <- sum //生产者
close(out)
}()
return out
}

3、单向管道、双向管道

双向通道:
前面我们以var c chan int形式声明的通道,即类型为chan typechannel都是双向通道。双向通道顾名思义,就是能存数据又能读数据
单向通道:
单向通道,就是只能存或只能读的channel
声明单向通道:

1
2
3
4
5
var readChan <-chan int // 只读的channel
var c WriteChan<- int // 只写的channel

readChan <- 1 // 报错
<-writeChan // 报错

对只读的管道执行写操作、对只写的管道执行读操作都会报错。