程序员的自我救赎---12.2.3: 虚拟币交易平台(区块链) 下 【C#与以太坊通讯】
虚拟币交易平台(区块链)下 【C#与以太坊通讯】
这一篇,也是区块链代币交易平台的最后一篇博客。所以内容都是基于前面两篇博客的,没有看过前面两篇的建议先过一遍。
12.2.1 :虚拟币交易平台(区块链) 上 【发行区块链代币】
12.2.2: 虚拟币交易平台(区块链) 中 【开发交易所】
说会到终点,我们还是接着代币交易平台的前提往前讲。前面我们讲述了如何基于以太坊发行区块链代币 以及 如何开发一个代币交易平台。
但是关键点是这代币和交易平台如何对接?
我们知道既然在交易平台交易,关于区块链代币的操作就至少有三种:
1,通过交易所创建用户代币钱包。 (注册)
2,客户从钱包客户端转入代币到交易所。(充值)
3,从交易所将代币转出到钱包客户端。 (提币)
其他的操作就是交易所(Web)做的事情,基本就不涉及和区块链之间的交互。今天我们还是以以太坊的测试环境Rinkeby为例。
我们先打开Geth客户端,加载Rinkeby的创始区块,并启动控制台。
geth --datadir=$HOME/.rinkeby init rinkeby.json
由于,我前面两篇博客是用另外一台电脑写的,那台电脑geth安装是在C盘。现在这台电脑C盘空间不是很够所以安装在D盘,这里我们只要cd定位一下就行。
再来我们启动以太坊的控制台Console。
geth --networkid=4 --datadir=$HOME/.rinkeby --syncmode=light --bootnodes=enode://a24ac7c5484ef4ed0c5eb2d36620ba4e4aa13b8c84684e1b4aab0cebea2ae45cb4d375b77eab56516d34bfbd3c1a833fc51296ff084b770b94fb9028c4d25ccf@52.169.42.101:30303 --identity "NearEth" --rpc --rpccorsdomain "*" --rpcapi "db,eth,net,web3,personal" console
这里跟【发行区块链代币】那篇文章中的“同步区块”命令略有一点不同。
首先,我用的是Light node(轻节点),轻节点相比Archive node(归档节点) 去掉了历史日志。所以同步区块的速度会快很多,前面的操作如何一直用的是归档节点就继续用归档节点。https://www.rinkeby.io/#geth
其次,我加入了--rpc ,--rpccorsdomain , --rpcapi 。这个是设置允许通过RCP接口访问,设置可以有哪些访问权限。这里还可以设置访问的端口,没设置的话默认是:8545。 参考:https://www.cnblogs.com/tinyxiong/p/7918706.html
最后,console命令。进入控制台。
下面我们做四个最基本的命令,顺便也回顾一下geth的操作命令(如何不知道有哪些命令的话就直接打eth 或者 personal 看他的API文档):
1,创建钱包:personal.newAccount('123456') //参数为钱包客户端密码
2,查询余额:eth.getBalance("0x820858f59bc885dcc088349e0ed8dcab6bfbf948") //参数为创建的客户端钱包地址
3,解锁钱包:personal.unlockAccount("0x820858f59bc885dcc088349e0ed8dcab6bfbf948","123456") //参数1:钱包地址;参数2:钱包密码。 钱包默认状态是加锁了。要转账的话需要先解锁。
4,钱包转账:eth.sendTransaction({from:"0x820858f59bc885dcc088349e0ed8dcab6bfbf948",to:"0xcbe33208f86166a1c1cb52a3105a0a36b20c35a5",value:web3.toWei(0.3,"ether")}) //参数:json数据:from ,to,value
(这里要说明以太币有很多单位 可参考:https://www.jianshu.com/p/b56552b1d1a0 )
创建钱包,查询余额,解锁钱包 都是立马可以看到的,转账需要等待12个区块确认后才能成功,转账操作完成之后会给一个区块地址,这个区块地址可以直接上区块浏览器去查询交易。
https://www.rinkeby.io/#explorer
这里四个基本的操作要多尝试几次熟练一下!
======================================华丽的分割线================================================
下面我用C# 来做以上4个命令的操作,其实也挺简单的。以太坊是专门有提供各个编程语言的解决方案,用的最多的应该是JavaScript。这里感兴趣的自行去百度一下,我们重点说C#。
先来我们看到以太坊的官网:http://www.ethdocs.org/en/latest/connecting-to-clients/nethereum/index.html 。
官网是直接有提供SDK的,而且代码是托管在GitHub的,我可以直接访问Github看到Nethereum: https://github.com/Nethereum/Nethereum
直接用NuGet去添加程序集:
PM > Install-Package Nethereum.Web3
接下来我们创建一个Winform程序,然后添加Nethereum。
调用代码还是比较简单的,我直接贴一下代码,一会将项目github。
using Nethereum.Hex.HexTypes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; namespace EthClient
{
public partial class Form1 : Form
{ static string rpcUrl = "http://127.0.0.1:8545"; Nethereum.Web3.Web3 web3 = new Nethereum.Web3.Web3(rpcUrl); public Form1()
{
InitializeComponent();
} private async void btn_queryAllAccount_Click(object sender, EventArgs e)
{ var accounts = await web3.Eth.Accounts.SendRequestAsync();
var acclist = accounts.ToList();
for (int i = ; i < acclist.Count; i++)
{
this.tb_logList.Text += "账户" + i + ":" + acclist[i] + "\r\n";
}
} private async void btn_newAccount_Click(object sender, EventArgs e)
{
string pwd = this.tb_Pwd.Text.Trim();
if (string.IsNullOrEmpty(pwd))
{
MessageBox.Show("密码不能为空");
return;
}
var createAccount = await web3.Personal.NewAccount.SendRequestAsync(pwd);
this.tb_logList.Text = createAccount;
} private async void btn_BlanceOf_Click(object sender, EventArgs e)
{
string accountAddress = this.tb_address.Text.Trim();
if (string.IsNullOrEmpty(accountAddress))
{
MessageBox.Show("不能为空");
return;
} var value= await web3.Eth.GetBalance.SendRequestAsync(accountAddress);
this.tb_logList.Text = value.Value.ToString();
} private async void btn_unlock_Click(object sender, EventArgs e)
{
string accountAddress = this.tb_lockAccount.Text.Trim();
string unLockPwd = this.tb_unlockPwd.Text; if (string.IsNullOrEmpty(accountAddress) || string.IsNullOrEmpty(unLockPwd))
{
MessageBox.Show("不能为空");
return;
} try
{
var res = await web3.Personal.UnlockAccount.SendRequestAsync(accountAddress, unLockPwd, );
this.tb_logList.Text = res.ToString();
}
catch (Exception ex)
{
this.tb_logList.Text ="密码错误!";
}
} private async void btn_sendTransfer_Click(object sender, EventArgs e)
{
string AccountFrom = this.tb_from.Text.Trim();
string AccountTo = this.tb_to.Text; if (string.IsNullOrEmpty(AccountFrom) || string.IsNullOrEmpty(AccountTo))
{
MessageBox.Show("不能为空");
return;
} string AccountPwd= InputBox.ShowInputBox("请输入密码!"); bool unlockres = false;
try
{
unlockres = await web3.Personal.UnlockAccount.SendRequestAsync(AccountFrom, AccountPwd, ); }
catch (Exception ex)
{
this.tb_logList.Text = "钱包密码错误!";
return;
} if (!unlockres)
{
this.tb_logList.Text = "钱包密码错误!";
return;
} Nethereum.RPC.Eth.DTOs.TransactionInput input = new Nethereum.RPC.Eth.DTOs.TransactionInput();
input.From = AccountFrom;
input.To = AccountTo; HexBigInteger gas = new HexBigInteger(); //设置转账小号的gas
input.Gas = gas; HexBigInteger price = new HexBigInteger(); //单位:wei
input.Value = price; var Block = await web3.Eth.Transactions.SendTransaction.SendRequestAsync(input); this.tb_logList.Text = Block; }
}
}
这里我直接提供一下GitHub下载地址: https://github.com/demon28/Nethereum
===================================================华丽的分割线======================================
以上讲述了如何通过Nethereum的SDK让C# 这门语言去操作geth。 那么接下来问题来了,上面的操作是操作ETH的,也就是以太币。
那么我们自己发现的代币如何操作?
首先,如果对于使用以太坊发行区块链代币不清楚如何操作的可以先看一遍 【发行区块链代币】 。
接下来,我们基于我们自己发行的一个NearCoin来进行操作。
我们来学习一个新知识“ABI”,什么是ABI?发行代币也就是部署一个智能合约, 简单来说ABI就是智能合约的API接口。我们可以在这里找他到:
有了它就可以和自己发行的代币通讯了。这里不啰嗦直接贴代码,一目了然:
using Nethereum.Hex.HexTypes;
using Nethereum.Web3;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; namespace NearCoinClient
{
public partial class Form1 : Form
{ readonly static string rpcUrl = "http://127.0.0.1:8545"; readonly static string abi = @"[ { ""constant"": true, ""inputs"": [], ""name"": ""name"", ""outputs"": [ { ""name"": """", ""type"": ""string"", ""value"": ""NearCoin"" } ], ""payable"": false, ""stateMutability"": ""view"", ""type"": ""function"" }, { ""constant"": false, ""inputs"": [ { ""name"": ""_spender"", ""type"": ""address"" }, { ""name"": ""_value"", ""type"": ""uint256"" } ], ""name"": ""approve"", ""outputs"": [ { ""name"": ""success"", ""type"": ""bool"" } ], ""payable"": false, ""stateMutability"": ""nonpayable"", ""type"": ""function"" }, { ""constant"": true, ""inputs"": [], ""name"": ""totalSupply"", ""outputs"": [ { ""name"": """", ""type"": ""uint256"", ""value"": ""100000000000000000"" } ], ""payable"": false, ""stateMutability"": ""view"", ""type"": ""function"" }, { ""constant"": false, ""inputs"": [ { ""name"": ""_from"", ""type"": ""address"" }, { ""name"": ""_to"", ""type"": ""address"" }, { ""name"": ""_value"", ""type"": ""uint256"" } ], ""name"": ""transferFrom"", ""outputs"": [ { ""name"": ""success"", ""type"": ""bool"" } ], ""payable"": false, ""stateMutability"": ""nonpayable"", ""type"": ""function"" }, { ""constant"": true, ""inputs"": [], ""name"": ""decimals"", ""outputs"": [ { ""name"": """", ""type"": ""uint8"", ""value"": ""10"" } ], ""payable"": false, ""stateMutability"": ""view"", ""type"": ""function"" }, { ""constant"": false, ""inputs"": [ { ""name"": ""_value"", ""type"": ""uint256"" } ], ""name"": ""burn"", ""outputs"": [ { ""name"": ""success"", ""type"": ""bool"" } ], ""payable"": false, ""stateMutability"": ""nonpayable"", ""type"": ""function"" }, { ""constant"": true, ""inputs"": [ { ""name"": """", ""type"": ""address"" } ], ""name"": ""balanceOf"", ""outputs"": [ { ""name"": """", ""type"": ""uint256"", ""value"": ""0"" } ], ""payable"": false, ""stateMutability"": ""view"", ""type"": ""function"" }, { ""constant"": false, ""inputs"": [ { ""name"": ""_from"", ""type"": ""address"" }, { ""name"": ""_value"", ""type"": ""uint256"" } ], ""name"": ""burnFrom"", ""outputs"": [ { ""name"": ""success"", ""type"": ""bool"" } ], ""payable"": false, ""stateMutability"": ""nonpayable"", ""type"": ""function"" }, { ""constant"": true, ""inputs"": [], ""name"": ""symbol"", ""outputs"": [ { ""name"": """", ""type"": ""string"", ""value"": ""NC"" } ], ""payable"": false, ""stateMutability"": ""view"", ""type"": ""function"" }, { ""constant"": false, ""inputs"": [ { ""name"": ""_to"", ""type"": ""address"" }, { ""name"": ""_value"", ""type"": ""uint256"" } ], ""name"": ""transfer"", ""outputs"": [], ""payable"": false, ""stateMutability"": ""nonpayable"", ""type"": ""function"" }, { ""constant"": false, ""inputs"": [ { ""name"": ""_spender"", ""type"": ""address"" }, { ""name"": ""_value"", ""type"": ""uint256"" }, { ""name"": ""_extraData"", ""type"": ""bytes"" } ], ""name"": ""approveAndCall"", ""outputs"": [ { ""name"": ""success"", ""type"": ""bool"" } ], ""payable"": false, ""stateMutability"": ""nonpayable"", ""type"": ""function"" }, { ""constant"": true, ""inputs"": [ { ""name"": """", ""type"": ""address"" }, { ""name"": """", ""type"": ""address"" } ], ""name"": ""allowance"", ""outputs"": [ { ""name"": """", ""type"": ""uint256"", ""value"": ""0"" } ], ""payable"": false, ""stateMutability"": ""view"", ""type"": ""function"" }, { ""inputs"": [ { ""name"": ""initialSupply"", ""type"": ""uint256"", ""index"": 0, ""typeShort"": ""uint"", ""bits"": ""256"", ""displayName"": ""initial Supply"", ""template"": ""elements_input_uint"", ""value"": ""10000000"" }, { ""name"": ""tokenName"", ""type"": ""string"", ""index"": 1, ""typeShort"": ""string"", ""bits"": """", ""displayName"": ""token Name"", ""template"": ""elements_input_string"", ""value"": ""NearCoin"" }, { ""name"": ""tokenSymbol"", ""type"": ""string"", ""index"": 2, ""typeShort"": ""string"", ""bits"": """", ""displayName"": ""token Symbol"", ""template"": ""elements_input_string"", ""value"": ""NC"" } ], ""payable"": false, ""stateMutability"": ""nonpayable"", ""type"": ""constructor"" }, { ""anonymous"": false, ""inputs"": [ { ""indexed"": true, ""name"": ""from"", ""type"": ""address"" }, { ""indexed"": true, ""name"": ""to"", ""type"": ""address"" }, { ""indexed"": false, ""name"": ""value"", ""type"": ""uint256"" } ], ""name"": ""Transfer"", ""type"": ""event"" }, { ""anonymous"": false, ""inputs"": [ { ""indexed"": true, ""name"": ""from"", ""type"": ""address"" }, { ""indexed"": false, ""name"": ""value"", ""type"": ""uint256"" } ], ""name"": ""Burn"", ""type"": ""event"" } ]";
readonly static string contractAddress = "0xB03Aa55003C1a9C235A11B244e437Cbf062fB998"; //智能合约地址 Nethereum.Web3.Web3 web3 = new Nethereum.Web3.Web3(rpcUrl); Nethereum.Contracts.Contract NearCoinContract; public Form1()
{
InitializeComponent(); NearCoinContract = web3.Eth.GetContract(abi, contractAddress); web3.TransactionManager.DefaultGas = ; //设置默认消耗的gas
} private async void btn_queryAmount_Click(object sender, EventArgs e)
{
string accountAddress = this.tb_account.Text.Trim();
if (string.IsNullOrEmpty(accountAddress))
{
MessageBox.Show("不能为空");
return;
} var getBalance = NearCoinContract.GetFunction("balanceOf"); //方法名为智能合约中的方法名 var amount = await getBalance.CallAsync<Int64>(accountAddress); //注意单位Gwei this.tb_loglist.Text = amount.ToString();
} private async void btn_send_Click(object sender, EventArgs e)
{
string acc1 = this.tb_from.Text.Trim();
string acc2 = this.tb_to.Text.Trim();
int amount =int.Parse(this.tb_amount.Text.Trim());
string pwd = this.tb_pwd.Text.Trim(); if (string.IsNullOrEmpty(acc1)|| string.IsNullOrEmpty(acc2) || amount< || string.IsNullOrEmpty(pwd) )
{
MessageBox.Show("不能为空或金额不能为0");
return;
} bool unlockAccountResult;
try
{
unlockAccountResult = await web3.Personal.UnlockAccount.SendRequestAsync(acc1, pwd, );
}
catch (Exception)
{
this.tb_loglist.Text = "账户解锁失败!";
return;
} if (!unlockAccountResult)
{
this.tb_loglist.Text = "账户解锁失败,或密码错误!";
return;
} var transfer = NearCoinContract.GetFunction("transfer");
var value = await transfer.SendTransactionAsync(acc1, acc2, Convert.ToString(amount,)); //金额为16进制 this.tb_loglist.Text = value.ToString(); }
}
}
这里有个问题需要注意的是,关于数值接收,看下图:
如果智能合约中小数点太长,C#没有uint256 这么大的类型,尤其是官网默认的智能合约是18位数字,需要修改。
这里我们修改一下官网智能合约的小数点长度,然后重新部署就可以了。
界面图:
代码已经提交到Github了, 可以直接下载下来。地址:https://github.com/demon28/Nethereum。
区块链也好,以太坊也好都是一种新兴的技术,如果有哪里写的不对,或者是我理解错误的地方还望指正!
============================================================华丽的分割线======================================================
连续写了三篇博客,我想做一个总结:
在上一篇文章“开发交易所”中,我也写了很多。现在我们的国家政策对于比特币包括所有区块链代币都是打压的,对区块链技术提倡深入研究。
但是到今天为止,我们能看见区块链应用成功的项目也就只有“比特币”以及他的变种币。虽然说区块链技术诸多特性对未来技术性方向有着绝对的影响,但是区块链不是唯一的,或者说全面性替代现有技术的。
区块链,只是技术方向的选择之一,如同我们从互联网时代,到移动互联网时代。电脑始终存在,不会说智能手机全面替代台式机、笔记本。
另外,技术始终是技术。既然是技术那就让技术员去搞,现在互联网上天天在追捧什么“三点钟社群”,每天讨论区块链。有时候我都在怀疑是不是一堆庄家在造势,利用“技术概念” 让人觉得对知识缺乏,造成焦虑、甚至恐慌。
当人恐慌的时候就会不自觉的想,我是不是也得每天看 某某某 的微博来更新知识? 我是不是该买谁谁谁的书籍来提高自己的储备? 我是不是得去买两个比特币体验一下?
这都是现代人的焦虑,现在这样一个信息爆炸的年代,其实哪里跟的过来。 这些年听太多了:“云计算”、“物联网”、“O2o”,“共享经济”,“分享经济”。
其实,完全不用那么担心,真正能改变世界的一定是简单易用,让生活更方便的。
最后。区块链这么火,弄得我也冒出一种想法。能不能基于C# 写一个区块链? 我知道有一些区块链是可以基于C# 这门语言来部署智能合约,但是底层不是C#的。
可能是我对C# 有种归属感吧,这两年移动互联网的时代,C# 这门语言也没发力。身边好多人都转Java了,总感觉C#令人唏嘘!
当然,以我现在的知识储备来讲,搞个区块链还远远不可能的事。
有园友在我上一篇博客中留言说了一下NEO,我看了一下就是C#写的区块链,看的我心一凉,哈哈。
好了,关于区块链代币交易所三篇文章就写完了。其实这里我只是侧重于开发交易平台,但是以太坊提出一个新名词DApp,有兴趣的去琢磨琢磨吧!
(其实可以不用下载钱包客户端,直接上Remix IDE 部署智能合约就行!)
======================================================华丽的分割线=====================================
有兴趣一起探讨Winner框架的可以加我们QQ群:261083244。
如果我这篇博客对您有帮助,请打赏:
程序员的自我救赎---12.2.3: 虚拟币交易平台(区块链) 下 【C#与以太坊通讯】的更多相关文章
- 程序员的自我救赎---1.4.2: 核心框架讲解(BLL&Tool)
<前言> <目录> (一) Winner2.0 框架基础分析 (二) 短信中心 (三)SSO单点登录 (四)PLSQL报表系统 (五)钱包系统 (六)GPU支付中心 (七)权限 ...
- 程序员的自我救赎---11.3:WinService服务
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
- 程序员的自我救赎---11.4:FileSystem文件服务
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
- 程序员的自我救赎---11.1:RPC接口使用规范
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
- 程序员的自我救赎---1.4.1:核心框架讲解(DAL)
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
- 程序员的自我救赎---3.1:理解Oauth2.0
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
- 程序员的自我救赎---3.2:SSO及应用案例
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
- 程序员的自我救赎---1.4.3: 核心框架讲解(MVC)
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
- 程序员的自我救赎---10.1:APP版本控制系统
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
随机推荐
- 使用log4net日志组件经验分享
常见步骤: 第一:在项目中引用log4net组件. 第二:配置log4net,一般都写在web.config中. 第三:调用部分. 具体怎么配置,大家可以参考博客其它博友写的,这里我只写我 ...
- 使用Scanner获取键盘输入
使用Scanner类可以很方便地便获取用户的键盘输入,Scanner是一个基于正则表达式的文本扫描器,它可以从文件.输入流 .字符串中解析出基本类型值和字符串值.Scanner类提供了多个构造器,不同 ...
- CocosCreator游戏开发---菜鸟学习之路(二)SocketIO简易教程
请先参考教程司令部-SocketIO教程进行相关操作 开发完成后部分用户会出现持续输出 a userConnected的BUG 如下图所示 经过一段时间的BUG检查终于发现了问题所在.每个人碰到的情况 ...
- 织梦去除版权中的Power by DedeCms
找到文件 \include\dedesql.class.php 注释或删除下面代码,大概在588行 $arrs1 = array(0x63,0x66,0x67,0x5f,0x70,0x6f,0x77, ...
- 手把手教你用.NET Core写爬虫
写在前面 自从上一个项目58HouseSearch从.NET迁移到.NET core之后,磕磕碰碰磨蹭了一个月才正式上线到新版本. 然后最近又开了个新坑,搞了个Dy2018Crawler用来爬dy20 ...
- java获取windows下面的文件对象
import javax.swing.*;import javax.swing.filechooser.FileSystemView;import java.io.File; FileSystemVi ...
- 关于 Java 面试,你应该准备这些知识点
来源:占小狼, www.jianshu.com/p/1b2f63a45476 马老师说过,员工的离职原因很多,只有两点最真实: 钱,没给到位 心,受委屈了 当然,我是想换个平台,换个方向,想清楚为什么 ...
- eclipse导入/编译hadoop源代码
1. 确保安装好JDK和eclipse 详细教程见: http://blog.csdn.net/kangdakangdaa/article/details/11364985 2. 安装 Subclip ...
- Centos定时启动和清除任务
因为需要定时并发执行任务,所以查到了crontab这个工具,介绍一下其用法: SHELL=/bin/bash PATH=/sbin:/bin:/usr/sbin:/usr/bin MAILTO=roo ...
- 重温.NET下Assembly的加载过程
最近在工作中牵涉到了.NET下的一个古老的问题:Assembly的加载过程.虽然网上有很多文章介绍这部分内容,很多文章也是很久以前就已经出现了,但阅读之后发现,并没能解决我的问题,有些点写的不是特别详 ...