看這篇文章的時候,千萬不要懼怕代碼,重要的核心的都加註釋了,原理很簡單!!祝閱讀順利javascript
當學習一門新的語言的時候,老是喜歡先創建一個Demo項目,經過構建一個基本的項目,來了解語言的特色。
對於web的交互,之前經常使用的技術主要是Ajax、Form表單提交這類,若是要作長鏈接,可使用Websocketphp
關於websocket和socket實際上是兩個東西,若是要比較的話,應該是拿websocket和http 來比較。前端
websocket發送json這是一種常規的方式vue
值得一提的是,Vue框架中使用axios發送POST請求的時候,默認Content-Type是application/json,因此在後端接受的時候,要作流處理。java
好比像PHP的話,要用php://input
,若是是go,那麼就要使用下面的代碼,來獲取請求body的所有內容,而後使用json.Unmarshal
來解析ios
func Receive(w http.ResponseWriter, r *http.Request) { b, _ := ioutil.ReadAll(r.Body) }
咱們繼續來看Vue的websocket部分git
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
事件。github
經過websocket來發送json,其實是傳遞了一個json的字符串,對於基於golang的後端,咱們一樣須要搭建websocket 服務端來接收消息。golang
websocket是在http協議上進行的升級。web
這裏咱們使用的是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 函數以下定義,不要懼怕這麼長一段代碼,其實核心很簡單
// 消息回覆類型,包含了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"` }
這裏面用到的方法主要是
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來創建長鏈接,而且與go的websocket服務進行通訊
使用websocket來避免輪詢已是一個減輕服務器請求壓力的辦法了,那麼咱們可否在傳輸協議上在作一些改變,來減少傳輸的包體大小。
關於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
先引入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文件,須要配合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(); } }
只看上面註釋的部分便可,其實分爲兩部分
注意:
proto.ChatResponse.deserializeBinary
是一個靜態方法,不須要New必定要修改成arraybuffer,二進制數組
ws.binaryType = 'arraybuffer';
經過上面的流程,咱們已經基本瞭解了protobuf在websocket中的配合使用,除此以外還有一個protobuf.js 也很火,可是沒有作特別的研究,比較喜歡官方的。
可是這裏有個細節問題,若是要創建通訊,通常上來說,咱們不會直接將信息返回回去。由於websocket是全雙工通訊,不像http同樣請求一次、返回一次、close。
若是咱們要用websocket,第一個反應是長鏈接,搞個聊天室實時聊天。那麼下面咱們來實現一個聊天能力。
那麼聊天能力,其實有一個特色就是廣播,一我的說話,全部人都能收到。這樣特別有意思,好久之前用ajax來作的話,還須要把數據存起來,而後每次再輪詢讀取輸出給前端。如今都不用存到數據庫裏了。
核心:
看下實現
先創建一個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的聊天能力來講有以下的流程
其中上面的例子,並無在client退出的時候,從clients中將連接刪除,實際上能夠用下面的形式,來刪除,而且conn.close關閉鏈接。
delete(clients,cli)
但願上面的內容,對你們有所幫助。詳細問題能夠留言討論