WebSocket(簡稱ws), 在2011年標準化成RFC6455.
WebSocket最新支援版本的查詢.
目前最新版本為13.
跟以往的API這種短連接相比, ws允許瀏覽器跟Server只需要經過一次的交握過程, 就可能建立起一條雙工的長連線, 並進行雙向資料傳輸.
WebSocket跟Http一樣也支援憑證, 協定開頭就從ws://
變成wss://
, 加上一層TLS保護.
因為該協定的出現跟瀏覽器支援的普及, 讓Server能夠主動的發訊息給瀏覽器.
而不用瀏覽器定期的Long polling來取最新資料,
因為很可能絕大部分資料都沒變更(搞不好超過7成請求都是), 而回應http 304.
Server也是要去查詢資料庫跟做比對.
有了這樣的雙向傳輸行為, 能夠作到類似觀察者模式的行為, 有異動才通知, 有需要才通知.
雙方的CPU能有更多的時間去處理更多其他的業務.
瀏覽器第一次請求, 會先發起一個http請求帶有Upgrade的Connection Header來到ws server.
還有Sec-WebSocket-Key, 這是一個base64的值, 這個會跟後面server回應的response是個成套的.
Sec-WebSocket-Version: 版本號, 就是ws的版本.
之後瀏覽器會回應一個status 101的回應給瀏覽器, 表示已經了解客戶端的請求, 讓它切換到改用ws protocol.
Sec-WebSocket-Accept就是經過server確認的base64 encode的值. 客戶端拿到就能解密看看是不是原來的伺服器發回來的了, 避免惡意的連結或是意外的連結.
Sec-WebSocket-Accept = base64(sha1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")).
這樣就完成了ws的交握了.
然後就能來Open長連線.
為了偶爾確認對方的死活XD
ws會透過ping(0x9)和pong(0xA), 來彼此確認對方狀況.很像是心跳的收縮.
要是server收到ping在一定時間內沒回pong, cleint會視為server斷線而close()或是重連.
安裝gorilla/websocket
go get github.com/gorilla/websocket
老樣子剛提到要先作http upgrade.
所以要有請求來處理, 建立一個websocketHandler.go
package handler
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func WsPing(ctx *gin.Context) {
ws, err := upGrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
return
}
defer ws.Close()
for {
// 讀取ws Socket傳來的訊息
mt, message, err := ws.ReadMessage()
if err != nil {
break
}
// 如果是ping
if string(message) == "ping" {
// 就回pong
message = []byte("pong")
} else {
// 如果是其他, 就回文字訊息類型, 內容就是回聲 (鸚鵡XD)
ws.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintln("got it : "+string(message))))
}
// 寫入Websocket
err = ws.WriteMessage(mt, message)
if err != nil {
break
}
}
}
當然也要有前端來發起http upgrade to ws request.
修改之前的index.tmpl
, 加入一段ws的code.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/assetPath/css/bootstrap.min.css">
<link rel="stylesheet" href="/assetPath/css/bootstrap-grid.min.css">
<link rel="stylesheet" href="/assetPath/css/bootstrap-reboot.min.css">
<script rel="script" src="/assetPath/js/bootstrap.bundle.js"></script>
<title>Gin Hello</title>
<script>
var ws = new WebSocket("ws://localhost:8080/ping");
// onOpen被觸發時, 去嘗試連線
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
// onMessage被觸發時, 來接收ws server傳來的訊息
ws.onmessage = function(evt) {
console.log("Received Message: " + evt.data);
};
// 由ws server發出的onClose事件
ws.onclose = function(evt) {
console.log("Connection closed.");
};
// 每秒發出一個現在時間的訊息
var timeInterval = setInterval(() => ws.send(Date.now()), 1000)
</script>
</head>
<body>
<h1>Hi {{.title}}</h1>
</body>
</html>
註冊路由SetupRouter.go
pingRouting := router.Group("/ping")
{
pingRouting.GET("", handler.WsPing)
}
來跑看看
這裡出現前面提的 upgrade請求
Sec-WebSocket-Key: xSNxfnX2LO/xX6wzzSwQ2Q==
來執行看看前面的計算sec-ws-accept的程式
一模一樣XD, 成功切到ws後, 就會發出ping, server就會回pong.
就建立好了ws長連線.
綠色箭頭表示ws.send()
紅色箭頭表示ws.onmessage()
可以看到server有收到, 且正常的推送訊息過來.
WebSocket能傳送的訊息類型還蠻多的, 除了剛剛看到的
Ping, Pong, Close, Text, 還有Binary.
// The message types are defined in RFC 6455, section 11.8.
const (
// TextMessage denotes a text data message. The text message payload is
// interpreted as UTF-8 encoded text data.
TextMessage = 1
// BinaryMessage denotes a binary data message.
BinaryMessage = 2
// CloseMessage denotes a close control message. The optional message
// payload contains a numeric code and text. Use the FormatCloseMessage
// function to format a close message payload.
CloseMessage = 8
// PingMessage denotes a ping control message. The optional message payload
// is UTF-8 encoded text.
PingMessage = 9
// PongMessage denotes a pong control message. The optional message payload
// is UTF-8 encoded text.
PongMessage = 10
)
如果要作廣播或者是分房, 就要自己實做Hub了.
WebSocket每條連線基本上都是吃記憶體的.
所以要單機撐起百萬條連線, 還是得要有足夠記憶體.
且OS要有些設定 像是fs.file-max, soft limit跟hard limit等的調校 等等的.
為了方便測試, 改寫一下websocketHandler.go
,
單獨把讀檔跟處理的部份拉出來, 因為我們就只要測試這個的行為.
package handler
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func ProcessWs(ws *websocket.Conn) {
for {
// 讀取ws Socket傳來的訊息
mt, message, err := ws.ReadMessage()
if err != nil {
break
}
// 如果是ping
if string(message) == "ping" {
// 就回pong
message = []byte("pong")
} else {
// 如果是其他, 就回文字訊息類型, 內容就是回聲 (鸚鵡XD)
ws.WriteMessage(websocket.TextMessage, []byte(fmt.Sprint("got it : "+string(message))))
}
// 寫入Websocket
err = ws.WriteMessage(mt, message)
if err != nil {
break
}
}
}
func WsPing(ctx *gin.Context) {
ws, err := upGrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
return
}
defer ws.Close()
ProcessWs(ws)
}
websocketRouter_test.go
package test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gorilla/websocket"
"github.com/tedmax100/gin-angular/handler"
)
var upgrader = websocket.Upgrader{}
func echo(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
handler.ProcessWs(c)
}
func TestExample(t *testing.T) {
// 建立一個測試server
s := httptest.NewServer(http.HandlerFunc(echo))
defer s.Close()
// http://127.0.0.1 to ws://127.0.0.
u := "ws" + strings.TrimPrefix(s.URL, "http")
// 嘗試連線
ws, _, err := websocket.DefaultDialer.Dial(u, nil)
if err != nil {
t.Fatalf("%v", err)
}
defer ws.Close()
// 連線成功, 發送訊息並且接收後驗證
if err := ws.WriteMessage(websocket.TextMessage, []byte("hello")); err != nil {
t.Fatalf("%v", err)
}
_, p, err := ws.ReadMessage()
if err != nil {
t.Fatalf("%v", err)
}
result := string(p)
if result != "got it : hello" {
t.Fatalf(result)
}
}
簡單的WebSocket就玩到這, 但我還是覺得node的socket.io方便很多XD.
Go好像比較多服務都是走gRPC了. 改天玩看看