Introduction

到目前为止,我们已经构建了一个具有所有关键功能的区块链:匿名,安全和随机生成的地址;区块链数据存储;工作证明制度;存储交易的可靠方式。虽然这些功能至关重要,但这还不够。是什么让这些功能真正发挥作用,使加密货币成为可能的是网络。在单台计算机上运行这种区块链实现有什么用?当只有一个用户时,基于密码术的功能有什么用?它的网络使所有这些机制起作用并且有用。

您可以将区块链功能视为规则,类似于人们希望一起生活和繁荣时所建立的规则。一种社会安排。区块链网络是遵循相同规则的程序社区,它遵循使网络活跃的规则。同样,当人们分享相同的想法时,他们会变得更强大,可以共同创造更好的生活。如果有人遵循不同的规则,他们将生活在一个单独的社会(州,公社等)。同样,如果区块链节点遵循不同的规则,它们将形成一个单独的网络。

这是非常重要的:没有网络,没有大多数节点共享相同的规则,这些规则是没用的!

免责声明:不幸的是,我没有足够的时间来实现真正的P2P网络原型。在本文中,我将演示一个最常见的场景,它涉及不同类型的节点。改善这种情况并使其成为P2P网络对您来说是一个很好的挑战和实践!此外,我不能保证除了本文中实现的方案之外的其他方案也可以。抱歉!这部分介绍了重要的代码更改,因此在这里解释所有这些都没有意义。请参阅this page查看自上一篇文章以来的所有更改。

Blockchain Network

区块链网络是分散的,这意味着没有服务器可以使用服务器来获取或处理数据。在区块链网络中有节点,每个节点都是网络的成熟成员。节点就是一切:它既是客户端又是服务器。记住这一点非常重要,因为它与通常的Web应用程序非常不同。

区块链网络是P2P(点对点)网络,这意味着节点彼此直接连接。它的拓扑结构是扁平的,因为节点角色中没有层次结构。这里的示意图如下:

(Business vector created by Dooder - Freepik.com)

这种网络中的节点更难实现,因为它们必须执行大量操作。每个节点必须与多个其他节点交互,它必须请求其他节点的状态,将其与自己的状态进行比较,并在其过时时更新其状态。

Node Roles

尽管是完整的,区块链节点可以在网络中扮演不同的角色。他们来了:

  1. Miner.这些节点在强大的或专用的硬件(如ASIC)上运行,其唯一目标是尽可能快地挖掘新块。矿工只能在使用工作证明的区块链中使用,因为挖掘实际上意味着解决PoW难题。例如,在Proof-of-Stake区块链中,没有采矿。
  2. Full node.这些节点验证矿工开采的块并验证交易。要做到这一点,他们必须拥有区块链的全部副本。此外,这样的节点执行这样的路由操作,例如帮助其他节点发现彼此。对于网络而言,拥有许多完整节点非常重要,因为正是这些节点做出了决策:他们决定一个块或事务是否有效。
  3. SPV.SPV代表简化付款验证。这些节点不存储区块链的完整副本,但它们仍然能够验证事务(不是所有事务,而是一个子集,例如,发送到特定地址的子集)。 SPV节点依赖于完整节点来获取数据,并且可能有许多SPV节点连接到一个完整节点。 SPV使钱包应用成为可能:一个不需要下载完整的区块链,但仍然可以验证他们的交易。

Network simplification

要在我们的区块链中实现网络,我们必须简化一些事情。问题是我们没有很多计算机来模拟具有多个节点的网络。我们可以使用虚拟机或Docker来解决这个问题,但它可能会使一切变得更加困难:你必须解决可能的虚拟机或Docker问题,而我的目标只是集中在区块链实现上。因此,我们希望在一台机器上运行多个区块链节点,同时我们希望它们具有不同的地址。要实现这一点,我们将使用端口作为节点标识符而不是IP地址。例如,将有节点具有地址:127.0.0.1:3000, 127.0.0.1:3001, 127.0.0.1:3002我们将调用端口节点ID并使用NODE_ID环境变量来设置它们。因此,您可以打开多个终端窗口,设置不同NODE_IDs并运行不同的节点。

这种方法还需要具有不同的区块链和钱包文件。它们现在必须依赖于节点ID并命名为blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db, etc.

Implementation

那么,当您下载比特币核心并首次运行它时会发生什么?它必须连接到某个节点才能下载区块链的最新状态。考虑到您的计算机不知道所有或某些比特币节点,这个节点是什么?

比特币核心中的节点地址硬编码将是一个错误:节点可能受到攻击或关闭,这可能导致新节点无法加入网络。相反,在比特币核心中,有DNS seeds硬编码。这些不是节点,而是知道某些节点地址的DNS服务器。当你启动一个干净的比特币核心时,它将连接到其中一个种子并获得一个完整节点列表,然后它将从中下载区块链。

在我们的实施中,将会集中化。我们将有三个节点:

  1. 中心节点。这是所有其他节点将连接到的节点,这是将在其他节点之间发送数据的节点。
  2. 矿工节点。此节点将在mempool中存储新事务,并且当有足够的事务时,它将挖掘新块。
  3. 钱包节点。此节点将用于在钱包之间发送硬币。与SPV节点不同,它会存储区块链的完整副本。

The Scenario

本文的目标是实现以下场景:

  1. 中心节点创建区块链。
  2. 其他(钱包)节点连接到它并下载区块链。
  3. 另一个(矿工)节点连接到中央节点并下载区块链。
  4. 钱包节点创建一个事务。
  5. 矿工节点接收事务并将其保留在其内存池中。
  6. 当内存池中有足够的事务时,矿工开始挖掘新块。
  7. 当开采新块时,它将发送到中央节点。
  8. 钱包节点与中心节点同步。
  9. 钱包节点的用户检查他们的付款是否成功。

这就是比特币的样子。即使我们不打算建立一个真正的P2P网络,我们也将实现一个真实的,比特币的主要和最重要的用例。

version

节点通过消息进行通信。运行新节点时,它会从DNS种子中获取多个节点并发送它们version消息,在我们的实现中将如下所示:

type version struct {Version    intBestHeight intAddrFrom   string
}
复制代码

我们只有一个区块链版本,所以Version字段不会保留任何重要信息。BestHeight存储节点区块链的长度。AddFrom存储发件人的地址。

接收节点的节点应该是什么?version消息呢?它会以自己的方式回应version信息。这是一种握手:没有事先互相问候,就不可能有其他互动。但这不仅仅是礼貌:version用于查找更长的区块链。当节点收到一个version消息它检查节点的区块链是否长于值BestHeight。如果不是,节点将请求并下载丢失的块。

为了接收消息,我们需要一个服务器:

var nodeAddress string
var knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {nodeAddress = fmt.Sprintf("localhost:%s", nodeID)miningAddress = minerAddressln, err := net.Listen(protocol, nodeAddress)defer ln.Close()bc := NewBlockchain(nodeID)if nodeAddress != knownNodes[0] {sendVersion(knownNodes[0], bc)}for {conn, err := ln.Accept()go handleConnection(conn, bc)}
}
复制代码

首先,我们对中央节点的地址进行硬编码:每个节点必须知道最初连接到哪里。minerAddress参数指定接收采矿奖励的地址。这件作品:

if nodeAddress != knownNodes[0] {sendVersion(knownNodes[0], bc)
}
复制代码

意味着如果当前节点不是中心节点,则必须发送version消息到中央节点以查明其区块链是否过时。

func sendVersion(addr string, bc *Blockchain) {bestHeight := bc.GetBestHeight()payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})request := append(commandToBytes("version"), payload...)sendData(addr, request)
}
复制代码

我们在较低级别的消息是字节序列。前12个字节指定命令名称(在本例中为“version”),后面的字节将包含gob- 编码的消息结构。commandToBytes 看起来像这样:

func commandToBytes(command string) []byte {var bytes [commandLength]bytefor i, c := range command {bytes[i] = byte(c)}return bytes[:]
}
复制代码

它创建一个12字节的缓冲区并使用命令名填充它,将rest字节留空。有一个相反的功能:

func bytesToCommand(bytes []byte) string {var command []bytefor _, b := range bytes {if b != 0x0 {command = append(command, b)}}return fmt.Sprintf("%s", command)
}
复制代码

当节点收到命令时,它会运行bytesToCommand使用正确的处理程序提取命令名称和处理命令体:

func handleConnection(conn net.Conn, bc *Blockchain) {request, err := ioutil.ReadAll(conn)command := bytesToCommand(request[:commandLength])fmt.Printf("Received %s command\n", command)switch command {...case "version":handleVersion(request, bc)default:fmt.Println("Unknown command!")}conn.Close()
}
复制代码

好的,version命令处理程序如下:

func handleVersion(request []byte, bc *Blockchain) {var buff bytes.Buffervar payload verzionbuff.Write(request[commandLength:])dec := gob.NewDecoder(&buff)err := dec.Decode(&payload)myBestHeight := bc.GetBestHeight()foreignerBestHeight := payload.BestHeightif myBestHeight < foreignerBestHeight {sendGetBlocks(payload.AddrFrom)} else if myBestHeight > foreignerBestHeight {sendVersion(payload.AddrFrom, bc)}if !nodeIsKnown(payload.AddrFrom) {knownNodes = append(knownNodes, payload.AddrFrom)}
}
复制代码

首先,我们需要解码请求并提取有效负载。这与所有处理程序类似,因此我将在以后的代码片段中省略这一部分。

然后一个节点比较它BestHeight与消息中的那个。如果节点的区块链更长,它将回复version信息;否则,它会发送getblocks message.

getblocks

type getblocks struct {AddrFrom string
}
复制代码

getblocks意思是“告诉我你有什么块”(在比特币中,它更复杂)。注意,它没有说“给我所有的块”,而是它请求块哈希列表。这样做是为了减少网络负载,因为可以从不同的节点下载块,我们不想从一个节点下载几十GB。

处理命令很简单:

func handleGetBlocks(request []byte, bc *Blockchain) {...blocks := bc.GetBlockHashes()sendInv(payload.AddrFrom, "block", blocks)
}
复制代码

在我们的简化实现中,它将返回所有块的hash.

inv

type inv struct {AddrFrom stringType     stringItems    [][]byte
}
复制代码

Bitcoin 使用 inv向其他节点显示当前节点具有哪些块或事务。同样,它不包含整个块和事务,只包含它们的哈希值。该Type字段说这些是块还是交易。

处理 inv 更困难:

func handleInv(request []byte, bc *Blockchain) {...fmt.Printf("Recevied inventory with %d %s\n", len(payload.Items), payload.Type)if payload.Type == "block" {blocksInTransit = payload.ItemsblockHash := payload.Items[0]sendGetData(payload.AddrFrom, "block", blockHash)newInTransit := [][]byte{}for _, b := range blocksInTransit {if bytes.Compare(b, blockHash) != 0 {newInTransit = append(newInTransit, b)}}blocksInTransit = newInTransit}if payload.Type == "tx" {txID := payload.Items[0]if mempool[hex.EncodeToString(txID)].ID == nil {sendGetData(payload.AddrFrom, "tx", txID)}}
}
复制代码

如果传输了块哈希,我们希望将它们保存在blocksInTransit变量来跟踪下载的块。这允许我们从不同节点下载块。在将块放入运输状态后,我们发送getdata命令给发件人inv消息和更新blocksInTransit。在真正的P2P网络中,我们希望从不同节点传输块。

在我们的实施中,我们永远不会发送有多个哈希的inv。这就是为什么payload.Type == "tx"只有第一个哈希值。然后我们检查我们的mempool中是否已经有哈希值,如果没有,getdata 消息就被发送出去了.

getdata

type getdata struct {AddrFrom stringType     stringID       []byte
}
复制代码

getdata是对某个块或事务的请求,它只能包含一个块/事务ID。

func handleGetData(request []byte, bc *Blockchain) {...if payload.Type == "block" {block, err := bc.GetBlock([]byte(payload.ID))sendBlock(payload.AddrFrom, &block)}if payload.Type == "tx" {txID := hex.EncodeToString(payload.ID)tx := mempool[txID]sendTx(payload.AddrFrom, &tx)}
}
复制代码

处理程序很简单:如果它们请求块,则返回块;如果他们请求交易,则返回交易。请注意,我们不会检查我们是否确实拥有此块或事务。这是一个缺陷:)

block and tx

type block struct {AddrFrom stringBlock    []byte
}type tx struct {AddFrom     stringTransaction []byte
}
复制代码

这些消息实际上是传输数据的。

处理 block 消息很简单:

func handleBlock(request []byte, bc *Blockchain) {...blockData := payload.Blockblock := DeserializeBlock(blockData)fmt.Println("Recevied a new block!")bc.AddBlock(block)fmt.Printf("Added block %x\n", block.Hash)if len(blocksInTransit) > 0 {blockHash := blocksInTransit[0]sendGetData(payload.AddrFrom, "block", blockHash)blocksInTransit = blocksInTransit[1:]} else {UTXOSet := UTXOSet{bc}UTXOSet.Reindex()}
}
复制代码

当我们收到一个新区块时,我们将其放入区块链中。如果要下载更多块,我们会从我们下载前一个块的同一节点请求它们。当我们最终下载了所有块时,重新索引UTXO集。

TODO:我们应该在将其添加到区块链之前验证每个传入的块,而不是无条件地信任。TODO:不应运行UTXOSet.Reindex(),而应使用UTXOSet.Update(块),因为如果区块链很大,则需要花费大量时间来重新索引整个UTXO集。

处理 tx消息是最困难的部分:

func handleTx(request []byte, bc *Blockchain) {...txData := payload.Transactiontx := DeserializeTransaction(txData)mempool[hex.EncodeToString(tx.ID)] = txif nodeAddress == knownNodes[0] {for _, node := range knownNodes {if node != nodeAddress && node != payload.AddFrom {sendInv(node, "tx", [][]byte{tx.ID})}}} else {if len(mempool) >= 2 && len(miningAddress) > 0 {MineTransactions:var txs []*Transactionfor id := range mempool {tx := mempool[id]if bc.VerifyTransaction(&tx) {txs = append(txs, &tx)}}if len(txs) == 0 {fmt.Println("All transactions are invalid! Waiting for new ones...")return}cbTx := NewCoinbaseTX(miningAddress, "")txs = append(txs, cbTx)newBlock := bc.MineBlock(txs)UTXOSet := UTXOSet{bc}UTXOSet.Reindex()fmt.Println("New block is mined!")for _, tx := range txs {txID := hex.EncodeToString(tx.ID)delete(mempool, txID)}for _, node := range knownNodes {if node != nodeAddress {sendInv(node, "block", [][]byte{newBlock.Hash})}}if len(mempool) > 0 {goto MineTransactions}}}
}
复制代码

首先要做的是将新事务放入mempool(同样,事务必须在放入mempool之前进行验证)。下一件:

if nodeAddress == knownNodes[0] {for _, node := range knownNodes {if node != nodeAddress && node != payload.AddFrom {sendInv(node, "tx", [][]byte{tx.ID})}}
}
复制代码

检查当前节点是否为中心节点。在我们的实现中,中心节点不会挖掘块。相反,它会将新事务转发到网络中的其他节点。

下一个重要部分仅适用于矿工节点。让我们将它分成更小的部分:

if len(mempool) >= 2 && len(miningAddress) > 0 {
复制代码

miningAddress仅在矿工节点上设置。当当前(矿工)节点的mempool中有2个或更多事务时,开始挖掘。

for id := range mempool {tx := mempool[id]if bc.VerifyTransaction(&tx) {txs = append(txs, &tx)}
}if len(txs) == 0 {fmt.Println("All transactions are invalid! Waiting for new ones...")return
}
复制代码

首先,验证mempool中的所有事务。忽略无效的事务,如果没有有效的事务,则中断挖掘。

cbTx := NewCoinbaseTX(miningAddress, "")
txs = append(txs, cbTx)newBlock := bc.MineBlock(txs)
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()fmt.Println("New block is mined!")
复制代码

已验证的交易被放入一个区块,以及带有奖励的coinbase交易。挖掘块后,重新索引UTXO集。

TODO:同样,应该使用UTXOSet.Update而不是UTXOSet.Reindex

for _, tx := range txs {txID := hex.EncodeToString(tx.ID)delete(mempool, txID)
}for _, node := range knownNodes {if node != nodeAddress {sendInv(node, "block", [][]byte{newBlock.Hash})}
}if len(mempool) > 0 {goto MineTransactions
}
复制代码

交易完成后,它将从mempool中删除。当前节点知道,接收的每个其他节点inv带有新块哈希的消息。他们可以在处理消息后请求块。

Result

让我们播放我们之前定义的场景。

首先, set NODE_ID to 3000 (export NODE_ID=3000)在第一个终端窗口。我会用像徽章一样的徽章NODE 3000 or NODE 3001在下一段之前,让您知道要对哪个节点执行操作。

NODE 3000
创建一个钱包和一个新的区块链:

$ blockchain_go createblockchain -address CENTREAL_NODE
复制代码

(为了清晰和简洁,我将使用虚假地址)

之后,区块链将包含单个发生区块。我们需要保存块并在其他节点中使用它。 Genesis块用作区块链的标识符(在比特币核心中,创世块是硬编码的)。

$ cp blockchain_3000.db blockchain_genesis.db
复制代码

NODE 3001
接下来,打开一个新的终端窗口并将节点ID设置为3001.这将是一个钱包节点。生成一些地址blockchain_go createwallet,我们称之为这些地址WALLET_1, WALLET_2, WALLET_3.

NODE 3000
将一些硬币发送到钱包地址:

$ blockchain_go send -from CENTREAL_NODE -to WALLET_1 -amount 10 -mine
$ blockchain_go send -from CENTREAL_NODE -to WALLET_2 -amount 10 -mine
复制代码

-mineflag表示该块将立即被同一节点挖掘。我们必须拥有此标志,因为最初网络中没有矿工节点。
Start the node:

$ blockchain_go startnode
复制代码

节点必须一直运行直到方案结束。

NODE 3001
使用上面保存的genesis块启动节点的区块链:

$ cp blockchain_genesis.db blockchain_3001.db
复制代码

Run the node:

$ blockchain_go startnode
复制代码

它将从中央节点下载所有块。要检查一切正常,请停止节点并检查余额:

$ blockchain_go getbalance -address WALLET_1
Balance of 'WALLET_1': 10$ blockchain_go getbalance -address WALLET_2
Balance of 'WALLET_2': 10
复制代码

此外,您可以检查平衡CENTRAL_NODE地址,因为节点3001现在有其区块链:

$ blockchain_go getbalance -address CENTRAL_NODE
Balance of 'CENTRAL_NODE': 10
复制代码

NODE 3002
打开一个新的终端窗口并将其ID设置为3002,然后生成一个钱包。这将是一个矿工节点。初始化区块链:

$ cp blockchain_genesis.db blockchain_3002.db
复制代码

启动节点:

$ blockchain_go startnode -miner MINER_WALLET
复制代码

NODE 3001
Send some coins:

$ blockchain_go send -from WALLET_1 -to WALLET_3 -amount 1
$ blockchain_go send -from WALLET_2 -to WALLET_4 -amount 1
复制代码

NODE 3002
很快!切换到矿工节点并看到它挖掘一个新块!另外,检查中央节点的输出。

NODE 3001
切换到钱包节点并启动它:

$ blockchain_go startnode
复制代码

它将下载新开采的块!

停下来检查余额:

$ blockchain_go getbalance -address WALLET_1
Balance of 'WALLET_1': 9$ blockchain_go getbalance -address WALLET_2
Balance of 'WALLET_2': 9$ blockchain_go getbalance -address WALLET_3
Balance of 'WALLET_3': 1$ blockchain_go getbalance -address WALLET_4
Balance of 'WALLET_4': 1$ blockchain_go getbalance -address MINER_WALLET
Balance of 'MINER_WALLET': 10
复制代码

That’s it!

Conclusion

这是该系列的最后一部分。我可以发布一些实现P2P网络真实原型的帖子,但我没有时间做这件事。我希望这篇文章回答你关于比特币技术的一些问题并提出新的问题,你可以自己找到答案。比特币技术中隐藏着更多有趣的东西!祝好运!

PS您可以通过实施来开始改进网络addr消息,如比特币网络协议(链接如下)所述。这是一个非常重要的消息,因为它允许节点相互发现。我开始实现它,但还没有完成!

英文原文:jeiwan.cc/posts/build…

更多文章欢迎访问 http://www.apexyun.com/

联系邮箱:public@space-explore.com

(未经同意,请勿转载)

在Go中构建区块链 第7部分:网络相关推荐

  1. 如何在JAVA中构建区块链?

    区块链炒作继续困扰着技术世界.与此同时,许多银行和软件公司也意识到了这一点,并试图将相应的产品投放市场.然而,任何对区块链技术有更深理解和了解的人都会意识到它的缺点.尽管被认为是一种破坏性技术,但它可 ...

  2. 在Go中构建区块链 第5部分:地址

    Introduction 在上一篇文章,我们开始实施交易.您还了解了交易的非个人性质:没有用户帐户,您的个人数据(例如,姓名,护照号码或SSN)不是必需的,也不存储在比特币的任何地方.但仍然必须有一些 ...

  3. 在Go中构建区块链 第4部分:交易1

    Introduction 交易是比特币的核心,区块链的唯一目的是以安全可靠的方式存储交易,因此没有人可以在创建交易后对其进行修改.今天我们开始实施交易.但是因为这是一个相当大的话题,我将它分为两部分: ...

  4. 基于Java语言构建区块链(四)—— 交易(UTXO)

    基于Java语言构建区块链(四)-- 交易(UTXO) 2018年03月11日 00:48:01 wangwei_hz 阅读数:909 标签: 区块链比特币 更多 个人分类: 区块链 文章的主要思想和 ...

  5. 基于Java语言构建区块链(五)—— 地址(钱包)

    基于Java语言构建区块链(五)-- 地址(钱包) 2018年03月25日 18:02:06 wangwei_hz 阅读数:1292更多 个人分类: 区块链bitcoin比特币 文章的主要思想和内容均 ...

  6. 基于Java语言构建区块链(六)—— 交易(Merkle Tree)

    基于Java语言构建区块链(六)-- 交易(Merkle Tree) 2018年04月16日 10:21:35 wangwei_hz 阅读数:480更多 个人分类: 区块链比特币bitcoin 最终内 ...

  7. fcn从头开始_如何使用Go从头开始构建区块链

    fcn从头开始 介绍 (Introduction) With Web 3.0 and blockchain becoming more mainstream every day, do you kno ...

  8. 基于Go语言构建区块链:part5

    Golang语言和区块链理论学习完毕后,快速入门方法无疑是项目实战.本文将参考https://jeiwan.cc/tags/blockchain/教程,学习如何基于Go语言构建区块链. 1.引言 本文 ...

  9. 银心科技与黑萤科技达成战略合作,联合构建区块链数据库存储生态至高点

    银心科技与黑萤科技达成战略合作,联合构建区块链数据库存储生态至高点 元为开始,旦为天明,2019年元月,分布式存储领域利好不断,先是海外知名项目.全球首个区块链数据库存储ORA甲骨链在熊市逆势获得千万 ...

最新文章

  1. KNN 分类算法原理代码解析
  2. 人脸识别经典算法:特征脸方法(Eigenface)
  3. LeetCode Combination Sum
  4. Nginx的rewrite案例之防盗链
  5. oracle 插入timestamp
  6. 【汇编语言】除法(DIV/IDIV)
  7. 关于ZipOupputStream添加压缩包常见问题
  8. CSS hack技巧大全 案例演示
  9. 【To Do】LeetCode 28. Implement strStr() 和KMP算法
  10. grep命令--文件过滤分割与合并
  11. 软件开发的11种模式
  12. springboot+mybais+mabatisplus(swagger)实现增删改查接口
  13. html div边框添加文字,css通过text-shadow给文字加边框!
  14. Unity3D中玻璃、金属材质制作
  15. 感性电路电流计算_第五讲 正弦交流电路分析
  16. gcc与cmake、qmake与make、ninja
  17. Java SE 8 Archive Downloads (JDK 8u202 and earlier)
  18. 2022最新HTML生成国庆头像网页源码+打开即用
  19. PID控制器——MATLAB/Simulink仿真以及性能比较与分析
  20. dmp标签_[重磅推荐]你必须知道的京准通DMP知识!

热门文章

  1. 网易游戏2016实习生招聘笔试题目--井字棋
  2. Settings.System暂存/读取数据
  3. UIScrollView 使用
  4. iOS布局-autoresizingMask
  5. iOS引入第三发库引发的错误linker command failed with exit code 1
  6. Flutter开发之常用插件安装与卸载(11)
  7. php操作MySQL
  8. 每日命令:(13)more
  9. linux下应用程序性能剖分神器gprofiler-tools-安装和使用
  10. iOS常用第三方开源框架和优秀开发者博客等