原文,需要科学上网。省略了一些无关的内容。

上篇文章我们学习了如何编写区块链并且对进行Hash运算和验证,但所有都运行在一个节点。我们如何让其他的节点连接到我们的主节点并让这些新节点贡献新区块呢?并且,我们如何通知所有的节点区块链有更新呢?

这篇文章将进行就解答。

工作流

bk1

  • 第一个终端负责创建创世块和TCP服务以便新节点可以连接它

第一步

  • 打开一个新终端并使用TCP连接到第一个终端
  • 向第一个终端添加一个新区块

第二步

  • 第一个终端对区块进行验证
  • 第一个终端对所有节点进行广播通知有新区块生成

第三步

  • 所有终端都有同步后的新区块链

完成这篇文章后,尝试:让每个新终端都和第一个终端一样,有自己的TCP端口让其他节点连接,组成一个真实的网络!

在这篇文章中,你将:

  • 运行一个生成创世块的终端
  • 运行更多的终端并且向第一个终端写如区块
  • 第一个终端可以通过广播使其他终端的区块链更新

在这篇文章中,没讲:
就像上篇文章一样,这篇文章的目的是创建一个可以工作的基础网络,为你以后的区块链学习之路做好铺垫。所以你不能在其他网络中的电脑向第一个终端写入区块信息。 但通过把程序部署在云服务器上可以容易的实现这点。同样,区块链广播也将在其他节点上被模仿。不用担心,我们晚点再解释。

开始编码

我们将用到上篇文章中写好的一些函数,比如创建区块、Hash和验证函数。但不会再使用和HTTP相关的函数,因为我们接下来将使用TCP协议在终端中进行操作。

TCP和HTTP啥区别?

这里我们不打算介绍太多,TCP是一种网络传输基本协议,而HTTP则是基于TCP在浏览器和网络的一种封装。当你访问一个网页时,你在使用HTTP协议,它是由TCP底层数据传输协议支持的。

前期设置、引用包和回顾

前期设置

在根目录创建一个.env文件并写入:

1
ADDR=9000

我们将TCP端口存储在ADDR这个环境变量中。然后安装下面2个包:

go get github.com/davecgh/go-spew/spew 用来更好的在终端打印我们的区块链结构。

go get github.com/joho/godotenv 用来载入我们的.env文件。

接下来创建一个空的main.go文件。

引用包

添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"bufio"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net"
"os"
"strconv"
"time"

"github.com/davecgh/go-spew/spew"
"github.com/joho/godotenv"
)

回顾
下面几个函数在之前的文章中已经详细说过了,我们把这些复制过来。

创建Block结构并声明一个由Block组成的slices变量Blockchain

1
2
3
4
5
6
7
8
9
10
11
// Block represents each 'item' in the blockchain
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}

// Blockchain is a series of validated Blocks
var Blockchain []Block

接下来是创建区块需要的Hash函数:

1
2
3
4
5
6
7
8
// SHA256 hashing
func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}

区块生成函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// create a new block using previous block's hash
func generateBlock(oldBlock Block, BPM int) (Block, error) {

var newBlock Block

t := time.Now()

newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.BPM = BPM
newBlock.PrevHash = oldBlock.Hash
newBlock.Hash = calculateHash(newBlock)

return newBlock, nil
}

验证函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// make sure block is valid by checking index, and comparing the hash of the previous block
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}

if oldBlock.Hash != newBlock.PrevHash {
return false
}

if calculateHash(newBlock) != newBlock.Hash {
return false
}

return true
}

确保最长的链是主链:

1
2
3
4
5
6
// make sure the chain we're checking is longer than the current blockchain
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}

现在我们已经把和区块相关的函数复制过来并且剥离了和HTTP相关的函数,接下来处理网络部分。

网络

现在我们来设置网络,可以通过这个来进行区块分发,并把其加入到区块链并把新区块链广播回网络中。

首先我们在其他结构体声明位置下面声明一个全局变量bcServer(short for blockchain server),这是一个channel用来接收传入的区块。

1
2
// bcServer handles incoming concurrent Blocks
var bcServer chan []Block

题外话:channel是GO语言中最流行的一个特性,准许我们优雅的进行数据读写,经常用来解决数据竞争问题。当和go routines同时使用时将十分强大,因为多个go routines可以写入同一个channel。在Java或者类c语言中,你不得不通过互斥锁来控制数据的写入。尽管GO中也有互斥锁,但channel让这些变得简单。更多相关可以看这里

接下来编写main函数并从.env文件中载入环境变量,记住我们将在晚一些使用环境变量ADDR9090作为TCP端口使用。同样,在main函数中初始化bcServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}

bcServer = make(chan []Block)

// create genesis block
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
}

接下来编写TCP服务,你可以认为这个和HTTP十分类似,但没有浏览器支持。所有的数据传输都通过终端进行,并且能处理多个连接。
添加如下代码:

1
2
3
4
5
6
// start TCP and serve TCP server
server, err := net.Listen("tcp", ":"+os.Getenv("ADDR"))
if err != nil {
log.Fatal(err)
}
defer server.Close()

这里监听9090端口,并且defer server.Close()是很重要的,当我们不再需要连接后可以简洁的断开。关于defer可以看这里

当每次有新的连接请求时我们都需要创建一个新的连接来处理,添加如下代码:

1
2
3
4
5
6
7
for {
conn, err := server.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}

我们创建了一个死循环用来处理连接,并且通过go handleConn(conn)并发的处理每一个连接,所以没阻塞for循环。这也是能够并发处理连接的原因。

细心的读者可能发现了,我们并没有handleConn函数,别急,先看看完整的main函数:

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
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}

bcServer = make(chan []Block)

// create genesis block
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)

// start TCP and serve TCP server
server, err := net.Listen("tcp", ":"+os.Getenv("ADDR"))
if err != nil {
log.Fatal(err)
}
defer server.Close()

for {
conn, err := server.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}

}

下面编写handleConn函数,这个函数接受一个net.Conn类型的接口作为参数。接口非常神奇,在我看来,接口让GO和其他类C语言产生本质区别。并发和go routines得到了大量关注,但接口才是最强大的特性。如果你目前不使用接口,那么尽快的熟悉它,接口将助你成为GO大神。

首先创建一个骨架并且加入defer语句来关闭连接:

1
2
3
func handleConn(conn net.Conn) {
defer conn.Close()
}

现在我们需要准许客户端添加区块到区块链中,我们还以BPM为例,需要:

  • 提示客户端输入BPM值
  • 使用stdin获取输入值
  • 使用之前的generateBlockisBlockValidreplaceChain创建新区块
  • 将新区块链放入channel中,以便向网络中广播
  • 准许客户端输入新的BPM

代码如下:

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
io.WriteString(conn, "Enter a new BPM:")

scanner := bufio.NewScanner(conn)

// take in BPM from stdin and add it to blockchain after conducting necessary validation
go func() {
for scanner.Scan() {
bpm, err := strconv.Atoi(scanner.Text())
if err != nil {
log.Printf("%v not a number: %v", scanner.Text(), err)
continue
}
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], bpm)
if err != nil {
log.Println(err)
continue
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
}

bcServer <- Blockchain
io.WriteString(conn, "\nEnter a new BPM:")
}
}()

我们创建了一个新的scanner,for scanner.Scan()是阻塞调用,所以我们把他放到go routines中以便支持并发处理连接。并且将字符串转换为整数,完成区块生成和验证后,我们创建了含有新区块的区块链。

bcServer <- Blockchain这句是把新区块链加入到channel中,接下来我们提示客户端输入新的BPM来创建新区块。

广播

我们需要将新区块链广播给所有建立连接的客户端,由于我们仅仅是在一台电脑上进行测试,我们将模拟数据传递到客户端的过程。在handleConn函数的最后一行,我们需要做:

  • 将区块链转换成JSON格式方便阅读
  • 将新区块链写到每个建立了连接的客户端
  • 设置一个计时器定期执行,这样我们就不会被数据淹没了。这在区块链网络中很常见,每隔X分钟新区块链被广播一次。这里我们设定30秒。
  • 在第一个终端中优雅的打印主链,这样我们就可以知道发生了什么并且确保其他节点产生的区块被正常添加到了主链中。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// simulate receiving broadcast
go func() {
for {
time.Sleep(30 * time.Second)
output, err := json.Marshal(Blockchain)
if err != nil {
log.Fatal(err)
}
io.WriteString(conn, string(output))
}
}()

for _ = range bcServer {
spew.Dump(Blockchain)
}

一切都完成了!完整代码在这里

娱乐时间

让我们来试试,执行go run main.go:

bk2

正如预料之中的,我们看到了创世块并且启动了一个TCP服务监听9000端口。

接下来启动一个新终端并使用命令nc localhost 9000建立连接,你可以创建多个终端执行这个命令几次。输入BPM,并且我们可以在第一个终端看到新区块已经被添加了!

bk3

bk4

等待30秒,查看另一个终端,你将看到新区块链被广播到所有建立了连接的终端中了,即便这个终端没输入过BPM!

bk5

接下来

恭喜!你不仅仅创建了区块链还为其添加了网络功能,接下来你还可以:

  • 在本地创建一个更大的网络,复制代码到不同的目录并且更换不同的端口,对于每个终端都可以互相连接并读写数据。
  • 在多个端口实现数据流,这是另一个文章的主题了。

这就是完整的区块链网络,它接收数据并且广播数据到外部。挖矿时在同一个终端中实现这两者是十分高效的。

  • 如果你想和朋友一起测试这个程序,把它部署到云服务器上让你的朋友连接并发送数据即可。这里会有些额外的安全问题,如果有需要我们会专门写篇教程。

你已经近距离的了解了关于区块链的几个概念,这里我们强烈建议你去阅读一些共识算法,比如POW或者POS。

或者……等我们的下一篇文章:-)