var ws = new WebSocket("ws://hostname/path", ["protocol1", "protocol2"])
第一個參數是服務端websocket地址,若是是https+websocket,那麼前綴寫成wssjavascript
第二個參數並非必須的,它約定了雙方通信使用的自定義子協議,會被放到這個Header中: Sec-WebSocket-Protocolhtml
子協議在某些場合是很必要的,例如服務端要與多個客戶端版本兼容,那麼若干個版本以後,服務端設定支持子協議 v1.5, v2.0, 而客戶端發送的倒是 v1.0,那麼他們就能夠在握手階段失敗,不會繼續通訊下去致使奇奇怪怪的錯誤。java
WebSocket構造函數只有兩個變量,不能提供經過設置自定義Header的方式來攜帶其它信息,但仍能夠經過一些取巧的辦法攜帶額外的信息,用於認證等:git
經過ws地址填寫形如 ws://username:password@hostname/path, 即構造出了 Authorization Headergithub
經過ws地址填寫形如 ws://:password@hostname/path ,即構造出了 Bearer Token Headergolang
經過在Cookie中加入值,也可以攜帶額外的信息web
所以,在服務端設計握手階段認證時,應當避免使用這三種方式外攜帶的信息來進行認證(例如設置一個自定義的頭部),固然也能夠在websocket鏈接創建後,再經過自定義的認證協議,走websocket進行認證。瀏覽器
Google本身提供一個Websocket包 : golang.org/x/net/websocket緩存
不過他們親口認可這個包缺少一些特性,也缺少維護,他們推薦用 github.com/gorilla/websocket (原文見 https://godoc.org/golang.org/x/net/websocket)服務器
// 這裏代碼使用了go-restful 做爲http框架,換成http也無妨 conn, err := websocket.Upgrade(resp.ResponseWriter, req.Request, nil, 0, 0) if err != nil { resp.WriteError(http.StatusBadRequest, err) return } defer conn.Close()
也能夠手動建立一個 Upgrader 來處理子協議協商問題, 若是協商經過,就能夠很容易的得到最終協商好的子協議,從而使用正確版本的數據格式和處理方法。
WebSocket發送的數據都是「幀(Frames)」,主要有這麼幾種:
JS中提供了send方法,可以發送文本幀或二進制幀:
ws.send('{"abc":"def"}')
經過調用 ws.close(code, msg), 能夠發送關閉信息,若是不提供,那麼默認code爲1005(正常關閉),而不明確關閉,那麼服務端收到的多是1006.
當服務端對鏈接發起Ping時,瀏覽器中活躍的WebSocket對象會自動回覆Pong, 這能夠用於鏈接的活躍檢測。
服務端向客戶端發送的數據就要自由的多了,在此很少講,參考包文檔便可。
ws.onmessage = function(event) { graphData = JSON.parse(event.data) console.log('Received graph data:', graphData) if(graphData.error != null) { loadGraphData(null, graphData.error) return } loadGraphData(graphData, 'success') };
在服務端的數據接收通常須要一個單獨的go routine進行處理,可使用 NextReader, ReadMessage, ReadJSON這幾個方法進行讀取。須要注意的是,對於同一個WebSocket鏈接, 這些讀取方法應當在同一個go routine中順序執行,不然讀取操做將致使上一個進行中的讀取失敗。
JS和Go中都提供了WebSocket的關閉事件監聽,如:
ws.onclose = function(event) { if (event.wasClean) { //alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { console.log('WSError:', event) showError('服務暫不可用,請稍後刷新') } };
conn.SetCloseHandler(func(code int, msg string) error { c.closed <- msg return nil })
可是須要知道的是,WebSocket的關閉事件是基於收到明確的關閉消息狀況下才會出現的。換言之, 你在服務端監聽WebSocket關閉事件,當瀏覽器頁面刷新或關閉時,服務端並不能及時發現舊的WebSocket已經關閉,甚至向它們發送信息仍然不會收到任何錯誤(瀏覽器不關閉的狀況下)。
一樣的,當服務器忽然關閉,而關閉前又沒有明確關閉全部WebSocket鏈接時,那麼在JS中寫的onclose事件也不會起做用。
所以,服務端要想避免無用的WebSocket佔用資源,應當維護一種心跳機制,而WebSocket協議已經提供了Ping/Pong幀用於作這件事,而JS中的WebSocket對象也默認可以迴應Ping幀,所以心跳方案是:
heartbeat := time.Duration(s.cfg.Heartbeat) * time.Second tick := time.NewTicker(heartbeat) for { select { case <-tick.C: err := c.WriteControl(websocket.PingMessage, []byte(key), time.Now().Add(heartbeat/2)) if err != nil { log.Printf("Websocket Idle connection %s: Ping received error %s", key, err.Error()) return } case <-s.ctx.Done(): log.Printf("Websocket closed for client %s: server is closing\n", key) return case msg := <-c.closed: log.Printf("Websocket closed for client %s: %s\n", key, msg) return } }
這種方案的優勢是利用了JS自帶的Pong迴應,不須要寫額外的JS代碼,而服務端實現也比較簡單。
先前網上查了一些方案,是在客戶端藉助send發送數據幀來進行心跳,一樣的服務端也要單獨針對這種數據幀進行處理,有些過於複雜了。
WebSocket鏈接創建後,客戶端與服務端之間創建了一條長鏈接,其後全部的數據通信都要在這一個socket上傳輸。當客戶端頻繁發送數據時,數據就可能會堵塞在本地,JS中提供了一種方法能夠限制發送速率:
// 每隔100毫秒發送數據, 這裏限制爲只有在沒有緩存時纔會發送數據 setInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); } }, 100);
經過讀取 bufferedAmount 就能夠知道當前鏈接是否堵塞, 能夠決定是否繼續發送。
握手階段出現錯誤:
若是因爲子協議協商等緣由,WebSocket未能成功升級,那麼會在 onclose 事件中收到消息
其它錯誤在ws.onerror 中處理
ws.onclose 事件接收到的 event主要有 wasClean(bool), code(int), reason(string) 幾個成員,主要做用是:
wasClean 當js主動發起ws.close時,onclose接收到的事件中該值爲true,其餘狀況無論服務器以什麼狀態碼關閉,是false
code: 關閉時的狀態碼
reason: 關閉時發送的關閉消息,通常用於詳細說明狀況(如:服務器正在關閉/重啓,等等)
在服務端調用 websocket.Conn.Close() 的做用是關閉底層socket, 不能讓client收到有效的消息,正確的關閉應當手動發送Close消息:
conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Server is closing"), time.Now().Add(time.Second))
同理,在JS客戶端,不須要使用websocket時應當手動關閉WebSocket,頁面刷新或離開時也應當關閉WebSocket:
<body onunload="leavePage()"> ... <script> function leavePage() { ws.close(1000, 'Client closed') } </script>
WebSocket關閉的經常使用狀態碼,參見 websocket/conn.go 或RFC文檔,幾個常見的WebSocket關閉碼:
1000: 正常關閉
1001: 服務端暫停服務,或客戶端離開頁面
1005: 此次關閉沒有狀態碼
1006: 這是一次不正常關閉,沒有發送關閉幀
JS: https://javascript.info/websocket#opening-a-websocket
Go: https://godoc.org/github.com/gorilla/websocket
WebSocket: https://github.com/HJava/myBlog/tree/master/WebSocket%20%E5%8D%8F%E8%AE%AE%20RFC%20%E6%96%87%E6%A1%A3