通過Websocket與gRPC互動 | gRPC雙向資料流的互動控制系列(2)
在本系列第一篇文章《gRPC雙向資料流的互動控制系列(1).初步實現》(http://www.ituring.com.cn/article/507208)中,我們完成了通過控制檯進行gRPC雙向資料流互動控制的實驗。但是隻是用控制檯互動大大限制了客戶端的使用範圍,如果我們要在網頁或者移動端與gRPC進行雙向資料流的互動怎麼辦?熟悉前端開發的朋友可能馬上就會想到:用Websocket啦!
Websocket簡介
WebSocket協議誕生於2008年,2011年成為國際標準,現代瀏覽器都已經支援Websocket,移動端不管是安卓還是iOS也沒有問題。Websocket提供一種在單個TCP 連線上進行全雙工通訊的協議,使得客戶端和服務端只需要做一個握手的動作,然後,客戶端和服務端之間就形成了一條快速通道,兩者之間就直接可以進行雙向的資料傳輸。看到這我們可以發現Websocket與gRPC雙向資料流之間簡直是天作之合!
整合Websocket服務端 + gRPC客戶端
主流的各種語言都有庫可供Websocket的服務端使用,為與前文(http://www.ituring.com.cn/article/507208)保持一致,我們仍然用Go語言來時實現。Go語言中常用的Websocket庫是gorrila websocket (github.com/gorilla/websocket),為了使程式碼看起來更簡潔一些,我們本次採用封裝了gorrila websocket的微型框架 melody (github.com/olahol/melody)和 gin框架(github.com/gin-gonic/gin) 來實現。
gin在本程式中的作用比較簡單,就是提供路由,一個是靜態資源(index.html)的路由,一個是websocket的路由。
melody有三個最重要的函式:一個是HandleConnect,用於響應websocket客戶端的連線事件;一個是HandleMessage,用於處理websocket客戶端輸入的訊息;一個是HandleDisconnect,用於處理websocket客戶端斷開連線事件。melody封裝了session,並且可以利用session存取自定義資料。
ws-server-grpc-client.go
package main
import (
"context"
"encoding/json"
"flag"
"io"
"log"
"net/http"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"gopkg.in/olahol/melody.v1"
proto "chat" // 自動生成的 proto程式碼
)
var 服務端地址 string
func init() {
flag.StringVar(&服務端地址, "server", "localhost:3000", "服務端地址")
}
func main() {
// 解析命令列引數
flag.Parse()
// 設定log
log.SetFlags(log.LstdFlags)
// 建立gRPC連線
conn, err := grpc.Dial(服務端地址, grpc.WithInsecure())
if err != nil {
log.Printf("連線失敗: [%v]\n", err)
return
}
defer conn.Close()
// 宣告客戶端
client := proto.NewChatClient(conn)
r := gin.Default()
m := melody.New()
// 靜態頁面路由
r.GET("/", func(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "html/index.html")
})
// websocket路由
r.GET("/ws", func(c *gin.Context) {
m.HandleRequest(c.Writer, c.Request)
})
// 處理websocket客戶端新連線,併為每一個新連線建立一個 雙向資料流
m.HandleConnect(func(s *melody.Session) {
log.Println("有新使用者接入")
// 給每個連入的新使用者建立一個資料流
// 宣告 context
ctx := context.Background()
// 建立雙向資料流
stream, err := client.BidStream(ctx)
if err != nil {
log.Printf("建立資料流失敗: [%v]\n", err)
// 如果建立資料流失敗,向客戶端傳送失敗資訊 同時 關閉websocket連線
s.CloseWithMsg([]byte("建立資料流失敗:" + err.Error()))
return
}
// 如果建立成功,將資料流儲存在 session中
s.Set("stream", stream)
// 啟動一個 goroutine 用於接收從服務端返回的訊息
go func() {
for {
// 接收從 服務端返回的資料流
響應, err := stream.Recv()
if err == io.EOF {
log.Println("⚠️ 收到服務端的結束訊號")
s.CloseWithMsg([]byte("⚠️ 收到服務端的結束訊號"))
return
}
if err != nil {
// TODO: 處理接收錯誤
log.Println("接收資料出錯:", err)
s.CloseWithMsg([]byte("接收資料出錯" + err.Error()))
return
}
log.Printf("[客戶端收到]: %s", 響應.Output)
// 如果成功收到從服務端返回的訊息, 將訊息序列化後返回給 websocket 客戶端
要返回的byte, _ := json.Marshal(響應)
s.Write(要返回的byte)
}
}()
})
// 處理使用者發來的訊息
m.HandleMessage(func(s *melody.Session, msg []byte) {
log.Println("收到訊息:", msg)
// 把使用者輸入的資訊原樣返回 websocket 客戶端
s.Write(msg)
// 將 []byte 型別的 msg 解析為 proto.Request
var 輸入資訊 proto.Request
if err := json.Unmarshal(msg, &輸入資訊); err != nil {
log.Println("解析輸入資訊失敗:", err)
s.CloseWithMsg([]byte("輸入資訊解析失敗"))
return
}
// 從 session中取出 stream
被儲存的資料流, ok := s.Get("stream")
if !ok {
s.CloseWithMsg([]byte("沒有找到stream!"))
return
}
// 斷言stream
stream, ok := 被儲存的資料流.(proto.Chat_BidStreamClient)
if !ok {
s.CloseWithMsg([]byte("被儲存的資料流不是Chat_BidStreamClient!"))
return
}
if err := stream.Send(&輸入資訊); err != nil {
s.CloseWithMsg([]byte("向gRPC服務端傳送訊息失敗:" + err.Error()))
return
}
})
// 處理 websocket 連線斷開事件,並關閉session 中 stream的連線
m.HandleDisconnect(func(s *melody.Session) {
log.Println("websocket客戶端斷開連線")
// 從 session中取出 stream
被儲存的資料流, ok := s.Get("stream")
if !ok {
log.Println("沒有找到stream!")
return
}
// 斷言stream
stream, ok := 被儲存的資料流.(proto.Chat_BidStreamClient)
if !ok {
log.Println("被儲存的資料流不是Chat_BidStreamClient!")
return
}
if err := stream.CloseSend(); err != nil {
log.Println("斷開stream連線出錯:", err)
}
})
r.Run(":8080")
}
websocket客戶端
為簡便起見,本例中的客戶端沒有使用任何框架,而是用了最原始的html和javascript,所以介面比較簡陋。我們完全可以用Angular、React、Vue、jQuery……或者基於移動端進行開發。
index.html
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket + gRPC雙向資料流</title>
<style>
#chat {
text-align: left;
background: #f1f1f1;
width: 500px;
min-height: 300px;
padding: 20px;
}
</style>
</head>
<body>
<center>
<h3>對話</h3>
<h4 id="clientId"></h4>
<div id="output"/>
<pre id="chat"></pre>
<input placeholder="請輸入資訊,回車傳送" id="text" type="text">
</center>
<script>
var url = "ws://" + window.location.host + "/ws";
var ws = new WebSocket(url);
var chat = document.getElementById("chat");
var text = document.getElementById("text");
var output = document.getElementById("output");
var clientId = document.getElementById("clientId");
var name = "客戶編號:" + Math.floor(Math.random() * 1000);
clientId.innerHTML = name;
// 列印連線狀態
var printStatus = function(狀態) {
var d = document.createElement("div");
d.innerHTML = 狀態;
output.appendChild(d);
};
// 獲取當前時間
var now = function () {
var iso = new Date().toISOString();
return iso.split("T")[1].split(".")[0];
};
// 處理websocket訊息
ws.onmessage = function (msg) {
var msg = JSON.parse(msg.data);
if (msg.input){
var line = now() + " " + msg.input + "\n";
}
if (msg.output){
var line = now() + " " + msg.output + "\n";
}
if(line){
chat.innerText += line;
}
};
// 處理連線事件
ws.onopen = function(evt) {
printStatus(now() + ' ' + '<span style="color: green;">成功連線</span>');
}
// 處理斷開連線事件
ws.onclose = function(evt) {
printStatus(now() + ' ' + '<span style="color: red;">連線已關閉</span>');
ws = null;
}
// 對話方塊監聽Enter鍵 併傳送訊息
text.onkeydown = function (e) {
if (e.keyCode === 13 && text.value !== "") {
ws.send(JSON.stringify({"input": text.value}))
text.value = "";
}
};
</script>
</body>
</html>
gRPC服務端
與《gRPC雙向資料流的互動控制系列(1).初步實現》(http://www.ituring.com.cn/article/507208)相比,server.go 做了小幅調整,主要是響應指令的條件上做了修改,讀者們可以自行比較。
server.go
package main
import (
"io"
"log"
"net"
"strconv"
"google.golang.org/grpc"
proto "chat" // 自動生成的 proto程式碼
)
// Streamer 服務端
type Streamer struct{}
// BidStream 實現了 ChatServer 介面中定義的 BidStream 方法
func (s *Streamer) BidStream(stream proto.Chat_BidStreamServer) error {
ctx := stream.Context()
for {
select {
case <-ctx.Done():
log.Println("收到客戶端通過context發出的終止訊號")
return ctx.Err()
default:
// 接收從客戶端發來的訊息
輸入, err := stream.Recv()
if err == io.EOF {
log.Println("客戶端傳送的資料流結束")
return nil
}
if err != nil {
log.Println("接收資料出錯:", err)
return err
}
// 如果接收正常,則根據接收到的 字串 執行相應的指令
switch 輸入.Input {
case "結束對話\n", "結束對話": //⚠️ 此處增加了匹配條件
log.Println("收到'結束對話'指令")
if err := stream.Send(&proto.Response{Output: "收到結束指令"}); err != nil {
return err
}
// 收到結束指令時,通過 return nil 終止雙向資料流
return nil
case "返回資料流\n", "返回資料流": //⚠️ 此處增加了匹配條件
log.Println("收到'返回資料流'指令")
// 收到 收到'返回資料流'指令, 連續返回 10 條資料
for i := 0; i < 10; i++ {
if err := stream.Send(&proto.Response{Output: "資料流 #" + strconv.Itoa(i)}); err != nil {
return err
}
}
default:
// 預設情況下, 返回 '服務端返回: ' + 輸入資訊
log.Printf("[收到訊息]: %s", 輸入.Input)
if err := stream.Send(&proto.Response{Output: "服務端返回: " + 輸入.Input}); err != nil {
return err
}
}
}
}
}
func main() {
log.Println("啟動服務端...")
server := grpc.NewServer()
// 註冊 ChatServer
proto.RegisterChatServer(server, &Streamer{})
address, err := net.Listen("tcp", ":3000")
if err != nil {
panic(err)
}
if err := server.Serve(address); err != nil {
panic(err)
}
}
執行效果
先啟動服務端程式 server.go 再啟動客戶端程式 ws-server-grpc-client.go
開啟瀏覽器,進入指定的地址,如 127.0.0.1:8080 輸入訊息,結果類似下圖:
小結
本文的例子充分利用了Websocket全雙工通訊的特性,實現了前端程式與gRPC服務端通過雙向資料流進行互動。在下一篇文章中,筆者將介紹如何利用nginx最新特性實現gRPC服務端的負載均衡。
相關文章
- gRPC雙向資料流的互動控制(go語言實現)| gRPC雙向資料流的互動控制系列(1)RPCGo
- 通過Nginx實現gRPC服務的負載均衡 | gRPC雙向資料流的互動控制系列(3)NginxRPC負載
- 透過Nginx實現gRPC服務的負載均衡 | gRPC雙向資料流的互動NginxRPC負載
- grpc雙向流RPC
- Silverlight與HTML雙向互動HTML
- Hive 與 ElasticSearch 的資料互動HiveElasticsearch
- 資料互動
- WPF和js互動 WebBrowser資料互動JSWeb
- (譯)通過WebChannel/WebSockets與QML中的HTML互動WebHTML
- 【網路協議】TCP的互動資料流和成塊資料流協議TCP
- grpc套路(四)php通過grpc呼叫golang的grpc介面服務RPCPHPGolang
- Android與WebView資料互動AndroidWebView
- 在 go websocket server 與 javascript websocket client 互動中使用 flatbuffersGoWebServerJavaScriptclient
- Flask資料互動Flask
- gRPC 客戶端和服務端一次互動的全流程(九)RPC客戶端服務端
- iOS 自定義的卡片流互動控制元件iOS控制元件
- gRPC(四)基礎:gRPC流RPC
- python與mysql資料庫互動PythonMySql資料庫
- SqlSugar與資料庫互動官網SqlSugar資料庫
- 微互動(五)——微互動的迴圈與模式模式
- 資料互動筆記筆記
- flask筆記:flask與資料庫的互動Flask筆記資料庫
- Redis資料庫4:Go與Redis的互動Redis資料庫Go
- MySQL資料庫5:Go與MySQL的互動MySql資料庫Go
- 雙向通訊之websocketWeb
- Android中程式與Service互動的方式——互動方式Android
- PHP與Python進行資料互動PHPPython
- RN與原生互動(二)——資料傳遞
- [乾貨]資料互動與本地儲存
- Java與Excel的互動!-JavaExcel
- Lua 與 ObjC 的互動OBJ
- webview與JS的互動WebViewJS
- Android 與 JavaScript 互動 支援的資料型別AndroidJavaScript資料型別
- Android FlatBuffers資料互動Android
- Ajax 資料非同步互動非同步
- React Native與Android通訊互動React NativeAndroid
- 2-程式與使用者互動
- [譯] 構建流暢的互動介面