寫在前面
寫這個主要是為了記錄下自己的學(xué)習(xí)過程,同時如果能幫助到同樣想搭建私有鏈的朋友們,那是再好不過了
Step 1 環(huán)境搭建
私鏈搭建有三寶,環(huán)境,終端和錢包。我這里用到的是Geth客戶端,所以環(huán)境當(dāng)然就是指Go語言運行環(huán)境。Ethereum的終端(客戶端)有很多語言(C++,Python...bala..bala)的實現(xiàn)版本,這里我用的是Go語言的實現(xiàn)版本,也是使用較多的版本,這里就隨個大流,畢竟用的越多,資料越豐富[1]。
Go環(huán)境
Go環(huán)境的安裝還是算方便的,在不用理解各種目錄的情況下,直接下載客戶端安裝好就ok了。以前Go是被墻了的,不過現(xiàn)在谷歌推出了中國開發(fā)者的官網(wǎng),但是我進去后也沒看到下載。為了方便大家,我在網(wǎng)盤保存了一份,大家可以下載,Windows版密碼:jq7a,Linux版密碼:ngbp,Mac版密碼:lavf。想要了解具體Go環(huán)境安裝及其目錄關(guān)系,大家可以自行搜索。Geth客戶端
Geth的安裝很簡單。
Windows的用戶很方便,直接下載客戶端即可。不過貌似被墻了,我這里提前下載過一個,放在了百度網(wǎng)盤。雖然個人很不喜歡流氓網(wǎng)盤,因為不買會員,下載速度奇慢,百兆寬帶也枉然,不過也實在沒啥好地方放。大家可以下載安裝,密碼:zzch
Mac,Ubuntu的同學(xué)也可以方便的安裝端執(zhí)行以下命令即可完成安裝,F(xiàn)reeBSD等其余Linux版本的同學(xué)可以下載源碼編譯安裝。
Mac同學(xué)
brew tap ethereum/ethereum
brew install ethereum
//開發(fā)版的安裝可以加上 --devel參數(shù)(如下),我沒加,直接用的上面的命令,二選一即可吧
brew install ethereum --devel
Ubuntu同學(xué)
sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum
- Mist或Ethereum錢包
我用的Mist錢包源碼,及Ethereum的錢包安裝包。錢包的安裝包安裝方式與之前無異,這個安裝比較簡單,下載對應(yīng)平臺的安裝包即可。我也提供我放到百度網(wǎng)盤的Ethereum錢包安裝包。Mac安裝包下載密碼:qfug,Linux的deb安裝包下載密碼:8jvr,Windows安裝包下載密碼:epul
我是下載Mist錢包源碼,然后安裝了開發(fā)環(huán)境的。事實證明,如果不是非要弄山寨幣修改錢包,還是不要折騰源碼錢包,直接用安裝包裝Ethereum錢包。如果對自家網(wǎng)絡(luò)比較有信心,不妨一試,我反正運行命令后,去城市郊區(qū)玩兒了兩天回來,親眼見證了安裝完成的最后一刻。
開發(fā)環(huán)境安裝[2]:
先安裝Node.js環(huán)境,我選擇的推薦的8.9.4LTS安裝
然后依次運行下面的命令,安裝依賴(我知道,你肯定會直接拷貝命令的,別把$也拷貝下來了):
//安裝依賴:
$ curl https://install.meteor.com/ | sh
$ curl -o- -L https://yarnpkg.com/install.sh | bash
$ yarn global add electron@1.7.9
$ yarn global add gulp
//下載錢包源碼并運行,相信我,你一定會看見錢包連接節(jié)點的界面!
$ git clone https://github.com/ethereum/mist.git
$ cd mist
$ yarn
此時,三個小時已過去,一下午沒了...下載上傳,碼字不易。不過這一切是值得的,你即將運行以太坊,激不激動!我反正已經(jīng)肝兒顫了。接下來,進入主題,運行以太坊私有鏈!
Step2 修改創(chuàng)世塊
以太坊,比特幣等的區(qū)塊鏈都是從創(chuàng)世塊開始的(你可以簡單理解成鏈表的頭結(jié)點),創(chuàng)世塊是要手動配置后生成的。下面是創(chuàng)世塊的配置文件(也就是一個Json文件)。修改好后保存為genesis.json即可。 當(dāng)然,你想換個canglaoshi.json也沒人說什么[1]。
{
"config": {
//區(qū)塊鏈的ID,你隨便給一個就可以
"chainId": 21,
//下面三個參數(shù)暫時不知道干啥的
//等我知道了補上,或者有哪位大神知道
//可以在評論里指點我,謝謝
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
//用來預(yù)置賬號以及賬號的以太幣數(shù)量,應(yīng)該也就是所謂的預(yù)挖
//我這里不需要預(yù)挖,所以給了個空對象
//如果需要可以這樣加
//"alloc": {
//"0x0000000000000000000000000000000000000001": {"balance": "111111111"},
//"0x0000000000000000000000000000000000000002": {"balance": "222222222"}
//}
"alloc" : {},
//幣基地址,也就是默認(rèn)的錢包地址,因為我沒有地址,所以全0,為空
//后面運行Geth后創(chuàng)建新賬戶時,如果Geth發(fā)現(xiàn)沒有幣基地址,會默認(rèn)將第一個賬戶的地址設(shè)置為幣基地址
//也就是礦工賬號
"coinbase" : "0x0000000000000000000000000000000000000000",
//挖礦難度,你可以隨便控制哦,這里設(shè)置的難度比較小,因為我喜歡錢來得快
"difficulty" : "0x4000",
//附加信息,隨便填個文本或不填也行,類似中本聰在比特幣創(chuàng)世塊中寫的報紙新聞
"extraData" : "",
//gas最高限制,以太坊運行交易,合約等所消耗的gas最高限制,這里設(shè)置為最高
"gasLimit" : "0xffffffff",
//64位隨機數(shù),用于挖礦,注意他和mixhash的設(shè)置需要滿足以太坊黃皮書中的要求
//直接用我這個也可以
"nonce" : "0x0000000000000042",
//與nonce共同用于挖礦,注意他和nonce的設(shè)置需要滿足以太坊黃皮書中的要求
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
//上一個區(qū)塊的Hash值,因為是創(chuàng)世塊,石頭里蹦出來的,沒有在它前面的,所以是0
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
//創(chuàng)世塊的時間戳,這里給0就好
"timestamp" : "0x00"
}
Step3 打開你的Geth客戶端,給它找點事做
修改好創(chuàng)世塊的Json文件后,我們就可以利用它來創(chuàng)建私有鏈了。創(chuàng)建一個文件夾來存放你的創(chuàng)世塊文件。我這里就叫eth_test,里面放著我的創(chuàng)世塊Json文件
//--datadir 后面跟的eth的工作目錄,你隨便給一個文件夾就行,區(qū)塊的數(shù)據(jù)會存在這個文件夾里
// init 后面跟的參數(shù)是genesis.json文件所在位置。我是在genesis.json文件所在的目錄打開的終端,所以不需要給genesis.json的路徑,給出文件名即可
geth --datadir "your/ethdata/filelocation" init your/genesis.json/loaction

如果你指定的目錄下面出現(xiàn)了紅框的文件夾,終端中出現(xiàn)Successfully wrote 等信息,恭喜你,創(chuàng)世塊創(chuàng)建完成!
然后我們開啟一個Geth節(jié)點,輸入下面的命令:
geth --datadir "/Users/guojh/Documents/ethTestFiles/eth_test" --identity "Guo Chain" --networkid 19900418 --port 61916 --rpcport 8206 console
//--datadir 后面跟的是你指定的工作目錄
//--identity 后面跟的是你的區(qū)塊鏈標(biāo)識,隨便寫
//--networkid 后面跟的是你的網(wǎng)絡(luò)id,這個是區(qū)別區(qū)塊鏈網(wǎng)絡(luò)的關(guān)鍵
//--port 和 --rpcport 你隨便給一個就行,別跟在用的端口重復(fù)就行
如果你得到的結(jié)果如下圖,說明你成功開啟了Geth的節(jié)點,并進入JavaScript終端。注意箭頭,第一個箭頭位置就是創(chuàng)世塊中配置的chainId。最后一條INFO告訴你ipc文件位置,這個后面會用到。

進入JavaScript終端后,你可以輸入下面三個命令,創(chuàng)建一個賬戶。在創(chuàng)建賬戶之前,coinbase地址是空的,創(chuàng)建完賬戶后,coinbase為剛才創(chuàng)建的賬戶地址。
//創(chuàng)建一個新賬戶
personal.newAccount("123456")
//user1變量保存剛才創(chuàng)建的賬戶,可以看出,eth.accounts數(shù)組存放了賬戶地址
user1 = eth.accounts[0]
//解鎖剛才創(chuàng)建的賬戶,如果不解鎖,不能轉(zhuǎn)賬
//Geth隔一段時間就會鎖定賬戶,所以需要解鎖
personal.unlockAccount(user1, "123456")
//查看coinbase
eth.coinbase

輸入命令查看賬戶余額
eth.getBalance(user1)
可以看到賬戶余額為0

現(xiàn)在,開始為自己賺錢吧~ 輸入挖礦命令:
miner.start()
如果你看到終端不停有輸出,那就對了。如果想停止挖礦,輸入停止命令:
miner.stop()
在輸入的時候你會發(fā)現(xiàn)輸入的文字被打印出的log打亂了,不用擔(dān)心,輸你的,不影響。此時再查看余額,你變成富翁了!
到這里,我想到個問題,現(xiàn)在沒有任何交易,區(qū)塊里也沒有任何交易信息,這也能得到以太幣獎勵?后來在Gitter的go-ethereum討論組中咨詢得知,只要是能產(chǎn)生區(qū)塊,就有獎勵,即使區(qū)塊中沒有任何有用信息。

Step4 多節(jié)點測試
只有一個節(jié)點略顯孤單,我們再創(chuàng)建一個節(jié)點,讓他倆有情人終成眷屬 :)。這里我們會在創(chuàng)建節(jié)點命令中增加一個參數(shù) bootnodes,在創(chuàng)建節(jié)點的同時,讓新節(jié)點連接上剛才創(chuàng)建的節(jié)點。bootnodes跟的參數(shù)是節(jié)點地址。如果沒有加bootnodes也不怕,創(chuàng)建好節(jié)點后調(diào)用admin.addPeer("enode"),將enode替換成節(jié)點地址即可。
將Step3中開啟節(jié)點的命令替換成下面的命令(這里的genesis.json和第一個節(jié)點的必須一樣,否則就是兩個鏈了。另外,兩個端口號不要和第一個節(jié)點重復(fù),工作目錄也不要重復(fù),但是networkid必須一致):
略有點長
geth --datadir "your/ethdata/filelocation" init your/genesis.json/loaction
geth --datadir "your/ethdata/filelocation" --identity "Guo Chain" --networkid 19900418 --port 61917 --rpcport 8207 --bootnodes "enode://40fadf14ab5084f03dcea80f1380e60ce270d423f45e1ba71e37ba892d9822bb0e681cf3c551e13f5a82ced6468c4dc4f3942925878ea0f57165ab5e1299bd2b@192.168.3.32:61916" console
這里的enode可以在第一次創(chuàng)建的節(jié)點中輸入:
admin.nodeInfo.enode
終端會顯示出節(jié)點enode信息,用你的本機IP替換[::]

同樣看到下圖,進入JavaScript控制臺,就是看到親人了

但是如何驗證兩個節(jié)點連接上了呢?見證奇跡的時刻到了。在新節(jié)點中創(chuàng)建賬戶,創(chuàng)建完成后,看!發(fā)現(xiàn)沒,節(jié)點在同步區(qū)塊數(shù)據(jù)了!說明兩個節(jié)點連上了!

Step5 連接錢包
在終端中進入之前下載好的Mist錢包的源碼文件目錄中?,F(xiàn)在就要用到之前啟動節(jié)點時創(chuàng)建的ipc文件了
- 首先來說說使用Ethereum錢包的連接方式[4][5]
我使用的是Mac,就是在終端輸入下面的命令,給錢包一個rpc參數(shù),其余平臺應(yīng)該也類似,就是通過終端啟動錢包,并提供參數(shù)(geth.ipc文件就是你啟動完節(jié)點后,自動生成的,就在節(jié)點目錄下,錢包連接到私有鏈需要提供這個文件,否則會連接到主鏈上):
open -a /Applications/Ethereum\ Wallet.app/ --args --rpc /Users/guojh/Documents/ethTestFiles/eth_test/geth.ipc

瞬間發(fā)現(xiàn)我還挺有錢的。注意到那個紅框了嗎,說明確實連接到的是私有鏈。
接下來我們可以轉(zhuǎn)賬了!
在另一個節(jié)點查看地址(可以直接輸入user1查看),拷貝下來地址,粘貼到錢包中轉(zhuǎn)賬


點擊send,輸入密碼,轉(zhuǎn)賬完成!
去另外一個節(jié)點終端查看,卻發(fā)現(xiàn)余額是0

這是怎么回事呢?朋友,不要忘了,交易是需要礦工確認(rèn)的。礦工在哪里呢?礦工就是你自己。交易通知到P2P網(wǎng)絡(luò)中的節(jié)點,但是沒有礦工確認(rèn)交易,所以交易沒有執(zhí)行。我們現(xiàn)在有兩個節(jié)點,隨便哪個開啟挖礦,就能確認(rèn)交易。當(dāng)然,也可以玩兒玩兒,兩個節(jié)點同時開啟挖礦,看看誰能搶先確認(rèn)交易。你可以看到,這邊在挖礦,錢包就收到了確認(rèn)交易的消息

這時,再查看另一個節(jié)點的余額,竊喜吧,朋友,你有錢了

- 下面說下Mist錢包源碼方式
輸入下面的命令:
yarn dev:electron --rpc /Users/guojh/Documents/ethTestFiles/eth_test/geth.ipc --node-ipcpath /Users/guojh/Documents/ethTestFiles/eth_test/geth.ipc
敲擊回車,你有可能看到如下界面

這就略顯尷尬了,一片空白。咋辦呢,我猜想可能是區(qū)塊同步有問題,要不開開采礦試試看錢包連上沒。結(jié)果就連上了!激動?。〉司褪琴v啊,我想看看是不是挖礦就一定能連上,立馬關(guān)閉錢包,再試一次,然后悲劇了,從此以后,Mist錢包就打不開了,每次都是一閃而過,就消失了,終端提示窗口被關(guān)閉。有沒有哪位大神知道原因?

過了很久之后我又連接上過一次,錢包操作方法和Ethereum錢包一樣,然后就又打不開了,本文的遺憾...后面我找到原因再來補充更新這個地方
如果不想用錢包,也可以使用命令來轉(zhuǎn)賬,你需要輸入from,轉(zhuǎn)賬來源,to,轉(zhuǎn)賬目的地址,value,轉(zhuǎn)賬金額,這里把1個ether轉(zhuǎn)成以太幣最小單位Wei來發(fā)送
eth.sendTransaction({from: "0x5fba50fce50baf0b8a7314200ba46336958ac97e", to: "0x0a8c35653d8b229c16f0c9ce6f63cffb877cfdcf", value: web3.toWei(1, "ether")})
回車后開啟挖礦,一樣可以轉(zhuǎn)賬。
Step6 創(chuàng)建你的代幣
在以太坊上創(chuàng)建代幣很簡單,但是這種代幣的交易是基于以太坊,也就是交易費還是要用以太幣支付。如果需要修改礦工獎勵,有自己的錢包等,還是需要修改以太坊源碼的,這里我先介紹最簡單的代幣創(chuàng)建[6]。
為了簡便,我們在私有鏈上創(chuàng)建代幣,跟在以太坊主鏈上創(chuàng)建代幣是一樣的操作方法。按照Step5的方法打開錢包(Mist或者Ethereum錢包都可以,看哪個你能打開...),連接到你的私有鏈上。
點擊右上方的合約按鈕(CONTRACTS),然后點擊部署新合約(DEPLOY NEW CONTRACT)

將下面的代碼拷貝到SOLIDITY CONTRACT SOURCE CODE編輯框中(編輯框中默認(rèn)的代碼全部刪除)
pragma solidity ^0.4.16;
interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }
contract TokenERC20 {
// Public variables of the token
string public name;
string public symbol;
uint8 public decimals = 18;
// 18 decimals is the strongly suggested default, avoid changing it
uint256 public totalSupply;
// This creates an array with all balances
mapping (address => uint256) public balanceOf;
mapping (address => mapping (address => uint256)) public allowance;
// This generates a public event on the blockchain that will notify clients
event Transfer(address indexed from, address indexed to, uint256 value);
// This notifies clients about the amount burnt
event Burn(address indexed from, uint256 value);
/**
* Constrctor function
*
* Initializes contract with initial supply tokens to the creator of the contract
*/
function TokenERC20(
uint256 initialSupply,
string tokenName,
string tokenSymbol
) public {
totalSupply = initialSupply * 10 ** uint256(decimals); // Update total supply with the decimal amount
balanceOf[msg.sender] = totalSupply; // Give the creator all initial tokens
name = tokenName; // Set the name for display purposes
symbol = tokenSymbol; // Set the symbol for display purposes
}
/**
* Internal transfer, only can be called by this contract
*/
function _transfer(address _from, address _to, uint _value) internal {
// Prevent transfer to 0x0 address. Use burn() instead
require(_to != 0x0);
// Check if the sender has enough
require(balanceOf[_from] >= _value);
// Check for overflows
require(balanceOf[_to] + _value > balanceOf[_to]);
// Save this for an assertion in the future
uint previousBalances = balanceOf[_from] + balanceOf[_to];
// Subtract from the sender
balanceOf[_from] -= _value;
// Add the same to the recipient
balanceOf[_to] += _value;
Transfer(_from, _to, _value);
// Asserts are used to use static analysis to find bugs in your code. They should never fail
assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
}
/**
* Transfer tokens
*
* Send `_value` tokens to `_to` from your account
*
* @param _to The address of the recipient
* @param _value the amount to send
*/
function transfer(address _to, uint256 _value) public {
_transfer(msg.sender, _to, _value);
}
/**
* Transfer tokens from other address
*
* Send `_value` tokens to `_to` on behalf of `_from`
*
* @param _from The address of the sender
* @param _to The address of the recipient
* @param _value the amount to send
*/
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_value <= allowance[_from][msg.sender]); // Check allowance
allowance[_from][msg.sender] -= _value;
_transfer(_from, _to, _value);
return true;
}
/**
* Set allowance for other address
*
* Allows `_spender` to spend no more than `_value` tokens on your behalf
*
* @param _spender The address authorized to spend
* @param _value the max amount they can spend
*/
function approve(address _spender, uint256 _value) public
returns (bool success) {
allowance[msg.sender][_spender] = _value;
return true;
}
/**
* Set allowance for other address and notify
*
* Allows `_spender` to spend no more than `_value` tokens on your behalf, and then ping the contract about it
*
* @param _spender The address authorized to spend
* @param _value the max amount they can spend
* @param _extraData some extra information to send to the approved contract
*/
function approveAndCall(address _spender, uint256 _value, bytes _extraData)
public
returns (bool success) {
tokenRecipient spender = tokenRecipient(_spender);
if (approve(_spender, _value)) {
spender.receiveApproval(msg.sender, _value, this, _extraData);
return true;
}
}
/**
* Destroy tokens
*
* Remove `_value` tokens from the system irreversibly
*
* @param _value the amount of money to burn
*/
function burn(uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value); // Check if the sender has enough
balanceOf[msg.sender] -= _value; // Subtract from the sender
totalSupply -= _value; // Updates totalSupply
Burn(msg.sender, _value);
return true;
}
/**
* Destroy tokens from other account
*
* Remove `_value` tokens from the system irreversibly on behalf of `_from`.
*
* @param _from the address of the sender
* @param _value the amount of money to burn
*/
function burnFrom(address _from, uint256 _value) public returns (bool success) {
require(balanceOf[_from] >= _value); // Check if the targeted balance is enough
require(_value <= allowance[_from][msg.sender]); // Check allowance
balanceOf[_from] -= _value; // Subtract from the targeted balance
allowance[_from][msg.sender] -= _value; // Subtract from the sender's allowance
totalSupply -= _value; // Update totalSupply
Burn(_from, _value);
return true;
}
}

接下來選擇Token ERC 20

你會看見下面出現(xiàn)了3個輸入框,填入對應(yīng)信息

下一步,選擇手續(xù)費,這個看你了,不過肯定是越高,礦工處理速度越快(在主鏈要注意這點,我們現(xiàn)在是私有鏈,無所謂),點擊DEPLOY,輸入你賬戶的密碼。


在主鏈上部署時要注意,有時候會提示你部署錯誤,通常原因都是你手續(xù)費給的不夠,調(diào)高一點點手續(xù)費吧。另外,我還碰到過交易提交了,但是沒有任何礦工處理我的交易,幾天都如此,開始我很郁悶,不敢再部署,怕多給錢。不過后面等不下去了,又建了一個,手續(xù)費調(diào)高了一點,馬上就處理了。目測要么是手續(xù)費太低,沒礦工處理,要么是以太坊擁堵(直到現(xiàn)在一個月過去了,還是沒處理,估計廢了)。
最后一步,開啟挖礦,處理自己的交易。其實可以不用等到12個確認(rèn),有一個確認(rèn),你的交易就被處理了。挖礦完成后,再次點擊合約(CONTRACTS),看,代幣做好了,200w個!

你現(xiàn)在是代幣的發(fā)行者了!我們試著把代幣轉(zhuǎn)一些給我們的另一個節(jié)點。
點擊錢包的發(fā)送(SEND),輸入另一個節(jié)點的地址,輸入轉(zhuǎn)賬金額,先轉(zhuǎn)它10w個,幣太多,沒辦法。下面注意了,前面轉(zhuǎn)賬的時候,只有ETHER,現(xiàn)在多了個剛才創(chuàng)建的代幣,毫不猶豫選擇它(注意下,千萬不要把主鏈上的以太幣轉(zhuǎn)到私有鏈的錢包地址,否則你的以太幣就消失在茫茫區(qū)塊鏈中了)。

同樣,選擇手續(xù)費額度,點擊發(fā)送(SEND),輸入密碼后,開啟挖礦處理交易。交易處理完后,再看我們的CONTRACTS里的JHCoin,少了10w個

現(xiàn)在請點擊JHCOIN(也就是你的代幣),拷貝你的代幣地址

我們可以把錢包連接到另一個節(jié)點(如果錢包老是在連接節(jié)點中,開啟錢包連接節(jié)點的挖礦程序,一下就能連上),發(fā)現(xiàn)并沒有看到我們剛才轉(zhuǎn)的代幣,怎么回事呢?是這樣,錢包不會自動識別新代幣,要手動添加后,才能顯示,這就是為什么要拷貝代幣地址的原因。
還是點擊錢包右上方的合約(CONTRACTS)按鈕,點擊最下面的關(guān)注代幣(亂翻譯的...原文是WATCH TOKEN)

將剛才拷貝的代幣地址粘貼過來,點擊OK

再看看,10w代幣到賬!

到此為止,1天過去了,不易啊,腰都坐酸了。
寫在后面
寫這篇教程,或者說我自己搭建的時候,搜索了很多資料,總結(jié)出一個經(jīng)驗,官方文檔或者Github上,都會給出最基本,最簡單的操作方法,結(jié)合網(wǎng)友們的文章看,更容易搭建起來。
在搭建過程中,你會更加具體的感受到區(qū)塊鏈的工作方式,我覺得還是很有助于理解以太坊或其它基于區(qū)塊鏈技術(shù)的項目。
有興趣的可以加群討論,一起學(xué)習(xí) 701477586
參考資料
[1]Geth官方GitHub https://github.com/ethereum/go-ethereum
[2]Mist官方GitHub https://github.com/ethereum/mist
[3]CNBlog http://www.cnblogs.com/zl03jsj/p/6876064.html
[4]StackExchange https://ethereum.stackexchange.com/questions/1018/how-to-run-ethereum-wallet-on-a-custom-chain
[5]Mist官方GitHub https://github.com/ethereum/mist/wiki#connecting-mist-to-local-test-network-from-the-command-line
[6]以太坊官方代幣教程 https://www.ethereum.org/token
[7]CSDN網(wǎng)友 http://blog.csdn.net/u013096666/article/details/72639906