摘要:export default { data () { return { username:'', email:'', content:'', message_list:[ { email:'', content:'', } ], ws:null } }, methods: { // 處理protobuf內容 handleRecv:function(data) { // 這裏對接收到的二進制消息進行解碼 var rep = proto.ChatResponse.deserializeBinary(data) // 可以獲取data和code console.info(rep.getData()) console.info(rep.getCode()) // 這裏拼接消息,message_list是vue的一個列表,不要關心 this.message_list.unshift({email:rep.getData().getEmail(),content:rep.getData().getContent()}) }, wsOpen: function () { var that = this var ws = new WebSocket("ws://localhost:9000/ws") // 這個地方特別重要,websocket默認是Uint8array ws.binaryType = 'arraybuffer'。func boardcast() { // 始終讀取messages for msg := range messages { // 讀取到之後進行廣播,啓動協程,是爲了立即處理下一條msg go func() { for cli := range clients { // protobuf協議 pbrp := &pb.ChatResponse{Code: http.StatusOK, Data: &pb.ChatRequest{Email: msg.Email, Content: msg.Content}} b, err := proto.Marshal(pbrp) if err。

看這篇文章的時候,千萬不要害怕代碼,重要的核心的都加註釋了,原理很簡單!!祝閱讀順利

當學習一門新的語言的時候,總是喜歡先建立一個Demo項目,通過構建一個基本的項目,來了解語言的特點。

對於web的交互,以前常用的技術主要是Ajax、Form表單提交這類,如果要做長連接,可以使用Websocket

關於websocket和socket其實是兩個東西,如果要比較的話,應該是拿websocket和http 來比較。

websocket 發送json

websocket發送json這是一種常規的方式

值得一提的是,Vue框架中使用axios發送POST請求的時候,默認Content-Type是application/json,所以在後端接受的時候,要做流處理。

比如像PHP的話,要用 php://input ,如果是go,那麼就要使用下面的代碼,來獲取請求body的全部內容,然後使用 json.Unmarshal 來解析

func Receive(w http.ResponseWriter, r *http.Request) {
    b, _ := ioutil.ReadAll(r.Body)
}

我們繼續來看Vue的websocket部分

export default {
    data () {
        return {
            username:'',
            email:'',
            content:'',
            message_list:[],
            ws:null
        }
    },
    methods: {
        handleRecv:function(data) {
            var jsonData = JSON.parse(data)
            this.message_list.unshift(jsonData.data)
        },
        wsOpen: function () {
            var that = this
            var ws = new WebSocket("ws://localhost:9000/ws")

            ws.onopen = function () {
                console.info("ws open")
            }

            ws.onmessage = function (evt) {
                that.handleRecv(evt.data)
            }

            ws.onclose  = function () {
                console.info("ws close")
            }

            this.ws = ws
        },
        wsSend: function() {
            if(this.ws == null) {
                console.info("連接尚未打開")
            }

            this.ws.send(JSON.stringify({
                email:this.email,
                content:this.content
            }))
        }
    },
    mounted(){
        this.wsOpen();
    }
}

上面這段代碼,是定義在Vue的組件中的。其實核心就是通過 mounted 組件掛載完成之後,來調用 new WebSocket 創建連接,並且註冊 onOpen , onMessage , onClose 事件。

通過websocket來發送json,實際上是傳遞了一個json的字符串,對於基於golang的後端,我們同樣需要搭建websocket 服務端來接收消息。

golang的websocket服務

websocket是在http協議上進行的升級。

這裏我們使用的是websocket包

"github.com/gorilla/websocket"

對於main函數,我們構建如下

// 定義了flag參數
var addr = flag.String("addr",":9000","http service address")

var upgrader = websocket.Upgrader{}

func main() {
    flag.Parse()

    http.HandleFunc("/ws",echo)

    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatalf("%v", err)
    }
}

echo 函數如下定義,不要害怕這麼長一段代碼,其實核心很簡單

  1. 我們首先建立結構體
// 消息回覆類型,包含了code和data主體
type MessageRet struct {
    Code int `json:"code"`
    Data interface{} `json:"data"`
}

// card包含了接受參數,存儲json解析後的對象
type Card struct {
    Email string  `json:"email"`
    Content string `json:"content"`
}
  1. 下面是主要的echo函數,完成的就是升級http,監聽某個鏈接的請求,並且進行回覆

這裏面用到的方法主要是

  • upgrader.CheckOrigin 解決跨域問題
  • upgrader.Upgrade()
  • c.ReadMessage()
  • c.WriteMessage()
func echo(w http.ResponseWriter, r *http.Request) {
    // 跨域
    upgrader.CheckOrigin = func(r *http.Request) bool {
        return true
    }
    // 升級http
    c, err := upgrader.Upgrade(w,r,nil)
    if err != nil {
        log.Fatalf("%v",err)
    }

    defer c.Close()

    // 結構體指針
    var card = &Card{}

    for {
        // 阻塞,等待讀取消息
        mt, message, err := c.ReadMessage()
        if err !=nil {
            log.Println("read",nil)
            break
        }
        
        // 解析參數
        err = json.Unmarshal(message, card)
        if err != nil {
            log.Fatalf("json parse error %v",err)
            break
        }
        // 這裏可以自定義handle處理
        card.Email = "server - "+card.Email
        card.Content = "server - "+card.Content
        
        // 重新打包,返回~~~~
        var ret = MessageRet{Code:http.StatusOK,Data:*card}
        b, _ := json.Marshal(ret)

        log.Printf("recv : %s", b)

        err = c.WriteMessage(mt, b)
        if err != nil {
            log.Fatalf("write",nil)
            break
        }
    }
}

注意上面 c.WriteMessage(mt,b) 的第一個參數是MessageType類型,有兩個值分別是代表 二進制、文本

websocket結合protobuf

在上面的例子當中,我們看到通過websocket來建立長連接,並且與go的websocket服務進行通信

使用websocket來避免輪詢已經是一個減輕服務器請求壓力的辦法了,那麼我們能否在傳輸協議上在做一些改變,來減小傳輸的包體大小。

使用protobuf

關於protobuf,是一種編碼協議,可以想象一下json、xml。

protoc是proto文件的編譯器,proto-gen-go是protoc的插件,可以根據proto文件,編譯成go文件。

google-protobuf 現在也支持了生成 js文件。

用protobuf的還有一個好處是指,如果我在go的服務端,定義好了Request的包體內容,以及Response的包體內容,那麼生成go文件後,也可以同時生成js文件

這樣雙方就可以按照同樣的參數模式來進行開發,就等於proto文件,相當於接口文檔了

那我們先生成一個proto文件,比如說websocket要發請求,goserver要返回內容,那就涉及了兩個消息結構的定義

syntax = "proto3";
// 請求
message ChatRequest {
    string email = 1;
    string content = 2;
}
// 響應
message ChatResponse {
    int32 code = 1;
    ChatRequest data = 2;
}

然後使用protoc來生成文件go/js

protoc --go_out=plugins=grpc:. *.proto
protoc --js_out=import_style=commonjs,binary:. *.proto

proto go文件的使用

先引入go的proto文件

import (
    pb "message_board_api/proto/chat"
)

然後再代碼中獲取到websocket的消息後,進行proto解析,非常簡單~~~~

// 使用protobuf解析
        pbr := &pb.ChatRequest{}
        err = proto.Unmarshal(message, pbr)
        if err != nil {
            log.Fatalf("proto 解析失敗 %v", err)
            break
        }

proto js文件使用

proto的js文件,需要配合 google-protobuf.js 一起使用,根據官網文檔來講,如果要在瀏覽器中使用,需要用browserify進行打包。

在Vue的組件中,引入包

import "google-protobuf"
    import proto from  "../proto/chat_pb.js"

下面我們來看websocket結合protobuf,與傳統的json有什麼不同,同樣不要害怕這麼一大段代碼,我們主要看method部分

export default {
    data () {
        return {
            username:'',
            email:'',
            content:'',
            message_list:[
                {
                    email:'',
                    content:'',
                }
            ],
            ws:null
        }
    },
    methods: {
        // 處理protobuf內容
        handleRecv:function(data) {
            // 這裏對接收到的二進制消息進行解碼
            var rep = proto.ChatResponse.deserializeBinary(data)
            // 可以獲取data和code
            console.info(rep.getData())

            console.info(rep.getCode())
            // 這裏拼接消息,message_list是vue的一個列表,不要關心
            this.message_list.unshift({email:rep.getData().getEmail(),content:rep.getData().getContent()})
        },
        wsOpen: function () {
            var that = this
            var ws = new WebSocket("ws://localhost:9000/ws")
            // 這個地方特別重要,websocket默認是Uint8array
            ws.binaryType = 'arraybuffer';

            ws.onopen = function () {
                console.info("ws open")
            }

            ws.onmessage = function (evt) {
                console.info(evt)
                console.info("Received message:"+evt.data)
                that.handleRecv(evt.data)
            }

            ws.onclose  = function () {
                console.info("ws close")
            }

            this.ws = ws
        },
        wsSend: function() {
            if(this.ws == null) {
                console.info("連接尚未打開")
            }
            // 發送消息同樣很簡單,我們不需要關心格式
            var chat = new proto.ChatRequest()
            chat.setEmail(this.email)
            chat.setContent(this.content)

            this.ws.send(chat.serializeBinary())
        }
    },
    mounted(){
        this.wsOpen();
    }
}

只看上面註釋的部分即可,其實分爲兩部分

  1. 按照約定的消息結構,set數據
  2. 按照約定的消息結構,get數據

注意:

  • proto.ChatResponse.deserializeBinary 是一個靜態方法,不需要New
  • 一定要修改爲arraybuffer,二進制數組

    ws.binaryType = 'arraybuffer';

通過上面的流程,我們已經基本瞭解了protobuf在websocket中的配合使用,除此之外還有一個protobuf.js 也很火,但是沒有做特別的研究,比較喜歡官方的。

廣播boardcast

但是這裏有個細節問題,如果要建立通信,一般上來講,我們不會直接將信息返回回去。因爲websocket是全雙工通信,不像http一樣請求一次、返回一次、close。

如果我們要用websocket,第一個反應是長連接,搞個聊天室實時聊天。那麼下面我們來實現一個聊天能力。

那麼聊天能力,其實有一個特點就是廣播,一個人說話,所有人都能收到。這樣特別有意思,很久以前用ajax來做的話,還需要把數據存起來,然後每次再輪詢讀取輸出給前端。現在都不用存到數據庫裏了。

核心:

  1. 廣播實際上就是把消息廣播給所有與服務器建立連接的客戶端

看下實現

先建立一個map,用來儲存客戶端連接,在建立個消息緩衝通道

// 客戶端集合
var clients = make(map[*websocket.Conn]string)

// 消息緩衝通道
var messages = make(chan *pb.ChatRequest, 100)

每次新建連接之後,都會將鏈接保存到client當中,進行緩存。

c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatalf("%v", err)
    }
    defer c.Close()

    // 將鏈接的地址寫入到clients中
    who := c.RemoteAddr().String()
    log.Println(who)
    clients[c] = who

proto解析的部分已經講過了,這裏將解析的內容寫入到messages通道里面。

// 使用protobuf解析
        pbr := &pb.ChatRequest{}
        err = proto.Unmarshal(message, pbr)
        if err != nil {
            log.Fatalf("proto 解析失敗 %v", err)
            break
        }

        // 解析後的內容寫入messages 進行廣播
        pbr.Email = pbr.Email + "<" + who + ">"
        messages <- pbr

下面就是核心的boardcast方法

func boardcast() {
    // 始終讀取messages
    for msg := range messages {
        // 讀取到之後進行廣播,啓動協程,是爲了立即處理下一條msg
        go func() {
            for cli := range clients {
                // protobuf協議
                pbrp := &pb.ChatResponse{Code: http.StatusOK, Data: &pb.ChatRequest{Email: msg.Email, Content: msg.Content}}
                b, err := proto.Marshal(pbrp)
                if err != nil {
                    log.Fatalf("proto marshal error %v", err)
                }

                // 二進制發送
                cli.WriteMessage(websocket.BinaryMessage, b)
            }
        }()
    }
}

上面的boardcast方法,要交給協程goroutine處理,不然for range messages 會阻塞,所以在main方法中使用協程

func main() {
    flag.Parse()

    http.HandleFunc("/ws", echo)
    // 廣播
    go boardcast()

    // pprof 這是一個狀態監控,可以忽略,有空也可以研究下
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // 這裏的ListenAndServe 已經開啓了goroutine協程了
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatalf("%v", err)
    }
}

小結一下

對於創建websocket和protobuf的聊天能力來說有如下的流程

  1. 雙方要實現websocket通信
  2. 約定proto消息協議,生成go和js兩個版本
  3. 使用goroutine協程,來進行廣播

其中上面的例子,並沒有在client退出的時候,從clients中將鏈接刪除,實際上可以用下面的形式,來刪除,並且conn.close關閉連接。

delete(clients,cli)

希望上面的內容,對大家有所幫助。詳細問題可以留言討論

相關文章