一个基于web服务器的PoW案例
一、安装第三方库
go get github.com/davecgh/go-spew/spew
这个库的功能是在命令行格式化输出内容。
go get github.com/gorilla/mux
这个开发包是用来编写Web处理程序的。
go get github.com/joho/godotenv
这个工具包是读取.env后缀名的文件中的数据,如果是Linux环境,.env文件放置在项目的根目录即可,如果是Windows和Mac OS,.env文件需要放在GOPATH/src目录下。
二、定义区块信息、难度系数
constdifficulty=4
typeBlockstruct {
Indexint
Timestampstring
BMPint
HashCodestring
PreHashstring
Diffint
Nonceint
}
varBlockchain []Block
typeMessagestruct {
BlockPostMessageint
}
varmutex=&sync.Mutex{}
这里我们定义一下挖矿生成区块的难度值,然后定义区块,包含区块高度、时间戳、交易信息、当前的Hash值和上一个的Hash值,还有难度和随机值。
然后定义区块链,用区块数组。
然后我们这里要根据Get请求和Post请求来生成区块,所以定义一个消息结构体用于存储Post信息。
最后定义一个互斥锁。
三、生成区块
funcgenerateBlock(oldBlockBlock, BlockPostMessageint) Block {
varnewBlockBlock
newBlock.PreHash=oldBlock.HashCode
newBlock.Index=oldBlock.Index+1
t :=time.Now()
newBlock.Timestamp=t.String()
newBlock.BlockPostMessage=BlockPostMessage
newBlock.Diff=difficulty
fori :=0; ; i++ {
newBlock.Nonce++
hash :=calculateHash(newBlock)
fmt.Println(hash)
ifisHashValid(hash, newBlock.Diff) {
fmt.Println("挖矿成功")
newBlock.HashCode=hash
returnnewBlock
}
}
}
每次生成新的区块前,先获取先前区块的Hash值放置在这个区块的上一个区块Hash值,然后获取当前时间,通过String()方法转换成为时间戳后放入区块的Timestamp。然后将Post传递的消息放入区块,将我们固定不变的困难值放入区块。
然后循环挖矿,每次挖矿将随机数加一,然后先不管这个区块能不能成功并入区块链,得先计算它的哈希值才能知道,然后校验哈希值的前导0,如果成功就输出挖矿成功。
四、生成哈希值
funccalculateHash(blockBlock) string {
hashed :=strconv.Itoa(block.Index) +block.Timestamp+
strconv.Itoa(block.Nonce) +strconv.Itoa(block.BlockPostMessage) +
block.PreHash
sha :=sha256.New()
sha.Write([]byte(hashed))
hash :=sha.Sum(nil)
returnhex.EncodeToString(hash)
}
很简单的逻辑,将区块的数据拼接后用sha256进行加密,得到hash值。
五、区块校验
funcisHashValid(hashstring, difficultyint) bool {
prefix :=strings.Repeat("0", difficulty)
returnstrings.HasPrefix(hash, prefix)
}
这个我们本专栏之前的文章介绍了,在此简单说一下,这里我们就校验一下哈希值前面的零的数量是不是和难度值一致。
六、启动HTTP服务器
funcrun() error {
mux :=makeMuxRouter()
httpAddr :=os.Getenv("PORT")
log.Println("Listening on ", httpAddr)
s :=&http.Server{
Addr: ":"+httpAddr,
Handler: mux,
ReadTimeout: 10*time.Second,
WriteTimeout: 10*time.Second,
MaxHeaderBytes: 1<<20,
}
iferr :=s.ListenAndServe(); err!=nil {
returnerr
}
returnnil
}
我们先从.env文件中获取PORT的值。然后监听获取的端口号。http.Server是设置http服务器的参数,其中Addr是地址,ReadTimeout、WriteTimeout分别是读写超时时间,然后是设置请求头的数据大小的最大值,1 << 20是位运算,算出来就是1MB。!!!最重要的就是回调函数了,这里需要我们自己编写来处理Get和Post请求。
然后我们就来监听事件并且根据监听到的事件来服务。
七、回调函数的编写
funcmakeMuxRouter() http.Handler {
muxRouter :=mux.NewRouter()
muxRouter.HandleFunc("/",
handGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/",
handWriteBlock).Methods("POST")
returnmuxRouter
}
mux.NewRouter()是用来创建路由,muxRouter.HandleFunc("/",handGetBlockchain).Methods("GET")是根据你访问的目录和请求类型来调用指定的方法。这里是使用Get方法访问根目录就调用handGetBlockchain方法。同样的,muxRouter.HandleFunc("/",handWriteBlock).Methods("POST")就是使用Post请求访问根目录时就调用handWriteBlock方法。
八、处理Get请求
funchandGetBlockchain(whttp.ResponseWriter, r*http.Request) {
bytes, err :=json.MarshalIndent(Blockchain, "", "\t")
iferr!=nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, string(bytes))
}
我们需要将数据转换为json格式,便于与前端进行交互。
同样我们的参数分别是响应和请求。然后处理错误,当出现500错误时,也就是http.StatusInternalServerError,我们将err.Error()写入w:
如果没出错,就将json数据写入w。
九、处理POST请求
funchandWriteBlock(writerhttp.ResponseWriter, request*http.Request) {
writer.Header().Set("Content-Type", "application/json")
varmessageMessage
decoder :=json.NewDecoder(request.Body)
iferr :=decoder.Decode(&message); err!=nil {
responseWithJSON(writer, request, http.StatusNotFound, request.Body)
}
deferrequest.Body.Close()
mutex.Lock()
newBlock :=generateBlock(Blockchain[len(Blockchain)-1], message.BPM)
mutex.Unlock()
ifisBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
Blockchain=append(Blockchain, newBlock)
spew.Dump(Blockchain)
}
//返回响应信息
responseWithJSON(writer, request, http.StatusCreated, newBlock)
}
因为需要服务器响应结果为json,先设置响应头的"Content-Type"为"application/json"。然后从request中读取JSON数据,将JSON数据转成Message。如果转换失败,就交给下一步处理异常,如果成功就创建新的区块。
这里使用defer,说明我们要记得关闭请求哦~
然后添加区块时要记得上锁,可以防止同个时间点多个POST请求生成区块。
接下来就要校验生成的区块是否正确,如果正确就加入区块链中。
十、处理异常
func responseWithJSON(writer http.ResponseWriter, request *http.Request,
code int, inter interface{}) {
writer.Header().Set("Content-Type", "application/json")
response, err := json.MarshalIndent(inter, "", "\t")
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
writer.Write([]byte("HTTP 500:Server Error"))
return
}
writer.WriteHeader(code)
writer.Write(response)
}
如果将传入的inter转换为json格式的数据没有出现错误就往响应头写入响应码,并将数据写入。
十一、校验区块是否正确
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.HashCode != newBlock.PreHash {
return false
}
if calculateHash(newBlock) != newBlock.HashCode {
return false
}
return true
}
这里校验了新区块的index是否等于原来最后一个区块的index加一,新区块的PreHash应该等于之前区块链最后一个区块的HashCode。然后还需要再一次计算区块的哈希值,进行比对。
十二、主逻辑
然后我们现在用Go实现通过http请求来完成区块链。
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
go func() {
t := time.Now()
genessisBlock := Block{}
genessisBlock = Block{0, t.String(),
0, calculateHash(genessisBlock),
"", difficulty, 0}
mutex.Lock()
Blockchain = append(Blockchain, genessisBlock)
mutex.Unlock()
spew.Dump(genessisBlock)
}()
//创建http服务器的启动函数
log.Fatal(run())
}
godotenv.Load()加载一个文件,如果不填写参数,就默认是加载.env文件。
这个.env文件我们这里就只需要填写一个端口号。
这里我们先将创世区块加入区块链。然后用spew.Dump()将其格式化输出到命令行。
最后我们会要用run来启动http服务器。
十三、运行结果
我们可以使用curl来进行get和post请求。
这是get请求,得到区块链。
这是进行post请求,新建一个区块加到了区块链。
可以看到再次get请求,已经有新的区块在区块链中了。