go實現簡易分散式系統

lightTrace發表於2018-04-05

眾所周知,go語言在網路服務模組有著得天獨厚的優勢,今天看了一些資料,準備自己寫一個簡單的分散式系統,具有以下功能:

  • 能夠傳送和接收請求/響應
  • 能夠連線到叢集
  • 如果無法連線到叢集(假設它是第一個節點),則可以作為主節點啟動節點
  • 每個節點有唯一的標識
  • 能夠在節點之間交換json資料包
  • 接收命令列引數的所有資訊

    一 搭建模型
    首先我們需要一個儲存節點資訊的結構體,包括節點id、節點ip、節點埠號:

//結構體物件可以和json互轉
type NodeInfo struct {
    NodeId     int    `json:"nodeId"`     //節點ID,通過隨機數生成
    NodeIpAddr string `json:"nodeIpAddr"` //節點ip地址
    Port       string `json:"port"`       //節點埠號
}

其次我們需要一個節點到叢集的一個請求或者響應的標準格式的結構體

type AddToClusterMessage struct {
    Source  NodeInfo `json:"source"`
    Dest    NodeInfo `json:"dest"`
    Message string   `json:"message"`
}

然後分別實現結構體的tostring格式化方法,整體程式碼distribute.go如下:

package main

import (
    "fmt"
    "math/rand"
    "net"
    "strconv"
    "time"
)

//用於json和結構體物件的互轉
type NodeInfo struct {
    NodeId     int    `json:"nodeId"`     //節點ID,通過隨機數生成
    NodeIpAddr string `json:"nodeIpAddr"` //節點ip地址
    Port       string `json:"port"`       //節點埠號
}

//新增一個節點到叢集的一個請求或者響應的標準格式
type AddToClusterMessage struct {
    Source  NodeInfo `json:"source"`
    Dest    NodeInfo `json:"dest"`
    Message string   `json:"message"`
}

//將節點資訊格式化輸出
func (node *NodeInfo) String() string {
    return "NodeInfo {nodeId:" + strconv.Itoa(node.NodeId) + ", nodeIpAddr:" + node.NodeIpAddr + ", port:" + node.Port + "}"
}

//將新增節點資訊格式化
func (req AddToClusterMessage) String() string {
    return "AddToClusterMessage:{\n  source:" + req.Source.String() + ",\n  dest: " + req.Dest.String() + ",\n  message:" + req.Message + " }"
}

func main() {

    // makeMasterOnError := flag.Bool("makeMasterOnError", false, "make this node master if unable to connect to the cluster ip provided.")
    // clusterip := flag.String("clusterip", "127.0.0.1:8001", "ip address of any node to connnect")
    // myport := flag.String("myport", "8001", "ip address to run this node on. default is 8001."

    rand.Seed(time.Now().UTC().UnixNano()) //種子
    myid := rand.Intn(9999999)
    fmt.Println(myid)

    //獲取ip地址
    myIp, _ := net.InterfaceAddrs()
    fmt.Println(myIp[13])

    //建立nodeInfo結構體
    me := NodeInfo{NodeId: myid, NodeIpAddr: myIp[13].String(), Port: "8001"}
    fmt.Println(me.String())

}

然後跑一下程式碼:

go run distribute.go

輸出如圖:
這裡寫圖片描述

二 接受命令列引數解析

我們使用flag模組來解析命令列引數,先看一個簡單的例子瞭解下flag:

package main



import (

"flag"

"fmt"

)



func main() {

//第一個引數,為引數名稱,第二個引數為預設值,第三個引數是說明

username := flag.String("name", "", "Input your username")

flag.Parse()

fmt.Println("Hello, ", *username)

}

編譯:

go build flag.go

執行:

./flag -name=world

輸出:

Hello, world

如果不輸入name引數:

./flag

則輸出:

Hello,

然後在這個簡單的分散式系統裡面我們要做什麼,有三個命令需要解析:

    //當第一個節點啟動用這個命令來將第一個節點作為主節點
    makeMasterOnError := flag.Bool("makeMasterOnError", false, "make this node master if unable to connect to the cluster ip provided.")
    //設定要連線的目的地ip地址
    clusterip := flag.String("clusterip", "127.0.0.1:8001", "ip address of any node to connnect")
    //設定要連線的目的地埠號
    myport := flag.String("myport", "8001", "ip address to run this node on. default is 8001.")
    flag.Parse()

三 連線函式以及監聽函式

連線目的地節點ip地址以及埠號:

func connectToCluster(me NodeInfo, dest NodeInfo) bool {
    //連線到socket的相關細節資訊
    connOut, err := net.DialTimeout("tcp", dest.NodeIpAddr+":"+dest.Port, time.Duration(10)*time.Second)
    if err != nil {
        if _, ok := err.(net.Error); ok {
            fmt.Println("不能連線到叢集", me.NodeId)
            return false
        }
    } else {
        fmt.Println("連線到叢集")
        text := "Hi nody.. 請新增我到叢集"
        requestMessage := getAddToClusterMessage(me, dest, text)
        json.NewEncoder(connOut).Encode(&requestMessage)

        decoder := json.NewDecoder(connOut)
        var responseMessage AddToClusterMessage
        decoder.Decode(&responseMessage)
        fmt.Println("得到資料響應:\n" + responseMessage.String())
        return true
    }
    return false
}

getAddToClusterMessage函式用來返回響應資訊:

//傳送請求時格式化json包有用的工具
func getAddToClusterMessage(source NodeInfo, dest NodeInfo, message string) AddToClusterMessage {
    return AddToClusterMessage{
        Source: NodeInfo{
            NodeId:     source.NodeId,
            NodeIpAddr: source.NodeIpAddr,
            Port:       source.Port},
        Dest: NodeInfo{
            NodeId:     dest.NodeId,
            NodeIpAddr: dest.NodeIpAddr,
            Port:       dest.Port},
        Message: message,
    }
}

本節點連線目的地節點成功或者成為第一個主節點後需要開啟監聽:

//me節點連線其它節點成功或者自身成為主節點之後開始監聽別的節點在未來可能對它自身的連線
func listenOnPort(me NodeInfo) {
    //監聽即將到來的資訊
    ln, _ := net.Listen("tcp", fmt.Sprint(":"+me.Port))
    //接受連線
    for {
        connIn, err := ln.Accept()
        if err != nil {
            if _, ok := err.(net.Error); ok {
                fmt.Println("Error received while listening.", me.NodeId)
            }
        } else {
            var requestMessage AddToClusterMessage
            json.NewDecoder(connIn).Decode(&requestMessage)
            fmt.Println("Got request:\n" + requestMessage.String())

            text := "已新增你到叢集"
            responseMessage := getAddToClusterMessage(me, requestMessage.Source, text)
            json.NewEncoder(connIn).Encode(&responseMessage)
            connIn.Close()
        }
    }
}

四 完整程式碼

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "math/rand"
    "net"
    "strconv"
    "strings"
    "time"
)

//用於json和結構體物件的互轉
type NodeInfo struct {
    NodeId     int    `json:"nodeId"`     //節點ID,通過隨機數生成
    NodeIpAddr string `json:"nodeIpAddr"` //節點ip地址
    Port       string `json:"port"`       //節點埠號
}

//新增一個節點到叢集的一個請求或者響應的標準格式
type AddToClusterMessage struct {
    Source  NodeInfo `json:"source"`
    Dest    NodeInfo `json:"dest"`
    Message string   `json:"message"`
}

//將節點資訊格式化輸出
func (node *NodeInfo) String() string {
    return "NodeInfo {nodeId:" + strconv.Itoa(node.NodeId) + ", nodeIpAddr:" + node.NodeIpAddr + ", port:" + node.Port + "}"
}

//將新增節點資訊格式化
func (req AddToClusterMessage) String() string {
    return "AddToClusterMessage:{\n  source:" + req.Source.String() + ",\n  dest: " + req.Dest.String() + ",\n  message:" + req.Message + " }"
}

func main() {

    makeMasterOnError := flag.Bool("makeMasterOnError", false, "make this node master if unable to connect to the cluster ip provided.")
    clusterip := flag.String("clusterip", "127.0.0.1:8001", "ip address of any node to connnect")
    myport := flag.String("myport", "8001", "ip address to run this node on. default is 8001.")
    flag.Parse()

    rand.Seed(time.Now().UTC().UnixNano()) //種子
    myid := rand.Intn(9999999)

    //獲取ip地址
    myIp, _ := net.InterfaceAddrs()

    //建立nodeInfo結構體
    me := NodeInfo{NodeId: myid, NodeIpAddr: myIp[13].String(), Port: *myport}
    dest := NodeInfo{NodeId: -1, NodeIpAddr: strings.Split(*clusterip, ":")[0], Port: strings.Split(*clusterip, ":")[1]}
    fmt.Println("我的節點資訊:", me.String())
    //嘗試連線到叢集,在已連線的情況下向叢集傳送請求
    ableToConnect := connectToCluster(me, dest)

    //如果dest節點不存在,則me節點為主節點啟動,否則直接退出系統
    if ableToConnect || (!ableToConnect && *makeMasterOnError) {
        if *makeMasterOnError {
            fmt.Println("將啟動me節點為主節點")
        }
        listenOnPort(me)
    } else {
        fmt.Println("正在退出系統,請設定me節點為主節點")
    }
}

//傳送請求時格式化json包有用的工具
func getAddToClusterMessage(source NodeInfo, dest NodeInfo, message string) AddToClusterMessage {
    return AddToClusterMessage{
        Source: NodeInfo{
            NodeId:     source.NodeId,
            NodeIpAddr: source.NodeIpAddr,
            Port:       source.Port},
        Dest: NodeInfo{
            NodeId:     dest.NodeId,
            NodeIpAddr: dest.NodeIpAddr,
            Port:       dest.Port},
        Message: message,
    }
}
func connectToCluster(me NodeInfo, dest NodeInfo) bool {
    //連線到socket的相關細節資訊
    connOut, err := net.DialTimeout("tcp", dest.NodeIpAddr+":"+dest.Port, time.Duration(10)*time.Second)
    if err != nil {
        if _, ok := err.(net.Error); ok {
            fmt.Println("不能連線到叢集", me.NodeId)
            return false
        }
    } else {
        fmt.Println("連線到叢集")
        text := "Hi nody.. 請新增我到叢集"
        requestMessage := getAddToClusterMessage(me, dest, text)
        json.NewEncoder(connOut).Encode(&requestMessage)

        decoder := json.NewDecoder(connOut)
        var responseMessage AddToClusterMessage
        decoder.Decode(&responseMessage)
        fmt.Println("得到資料響應:\n" + responseMessage.String())
        return true
    }
    return false
}

//me節點連線其它節點成功或者自身成為主節點之後開始監聽別的節點在未來可能對它自身的連線
func listenOnPort(me NodeInfo) {
    //監聽即將到來的資訊
    ln, _ := net.Listen("tcp", fmt.Sprint(":"+me.Port))
    //接受連線
    for {
        connIn, err := ln.Accept()
        if err != nil {
            if _, ok := err.(net.Error); ok {
                fmt.Println("Error received while listening.", me.NodeId)
            }
        } else {
            var requestMessage AddToClusterMessage
            json.NewDecoder(connIn).Decode(&requestMessage)
            fmt.Println("Got request:\n" + requestMessage.String())

            text := "已新增你到叢集"
            responseMessage := getAddToClusterMessage(me, requestMessage.Source, text)
            json.NewEncoder(connIn).Encode(&responseMessage)
            connIn.Close()
        }
    }
}

go install之後啟動第一個節點:

main --makeMasterOnError true

這裡寫圖片描述

另開一個終端連線剛剛啟動的主節點8001:

main --myport 8002 --clusterip 127.0.0.1:8001

這裡寫圖片描述

可以發現得到8001節點的響應,8001節點也得到8002的請求

當然也可以使用8003節點去連線8002節點:

main --myport 8003 --clusterip 127.0.0.1:8002

這裡寫圖片描述

相關文章