手摸手教你使用WebSocket[其實WebSocket也不難]
在本篇文章之前,Socket/">WebSocket
很多人聽說過,沒見過,沒用過,以為是個很高大上的技術,實際上這個技術並不神祕,可以說是個很容易就能掌握的技術,希望在看完本文之後,馬上把文中的栗子拿出來自己試一試,實踐出真知。
游泳、健身瞭解一下:部落格、ofollow,noindex">前端積累文件 、公眾號 、GitHub
WebSocket
解決了什麼問題:
客戶端(瀏覽器)和伺服器端進行通訊,只能由客戶端發起ajax
請求,才能進行通訊,伺服器端無法主動向客戶端推送資訊。
當出現類似體育賽事、聊天室、實時位置之類的場景時,客戶端要獲取伺服器端的變化,就只能通過輪詢(定時請求)來了解伺服器端有沒有新的資訊變化。
輪詢效率低,非常浪費資源(需要不斷髮送請求,不停連結伺服器)
WebSocket的出現,讓伺服器端可以主動向伺服器端傳送資訊,使得瀏覽器具備了實時雙向通訊的能力,這就是WebSocket
解決的問題
一個超簡單的栗子:
新建一個html
檔案,將本栗子找個地方跑一下試試,即可輕鬆入門WebSocket
:
function socketConnect(url) { // 客戶端與伺服器進行連線 let ws = new WebSocket(url); // 返回`WebSocket`物件,賦值給變數ws // 連線成功回撥 ws.onopen = e => { console.log('連線成功', e) ws.send('我傳送訊息給服務端'); // 客戶端與伺服器端通訊 } // 監聽伺服器端返回的資訊 ws.onmessage = e => { console.log('伺服器端返回:', e.data) // do something } return ws; // 返回websocket物件 } let wsValue = socketConnect('ws://121.40.165.18:8800'); // websocket物件 複製程式碼
上述栗子中WebSocket
的介面地址出自:WebSocket 線上測試,在開發的時候也可以用於測試後端給的地址是否可用。
webSocket的class類:
當專案中很多地方使用WebSocket,把它封成一個class類,是更好的選擇。
下面的栗子,做了非常詳細的註釋,建個html檔案也可直接使用,websocket的常用API
都放進去了。
下方註釋的程式碼,先不用管,涉及到心跳機制,用於保持WebSocket連線的
class WebSocketClass { /** * @description: 初始化例項屬性,儲存引數 * @param {String} url ws的介面 * @param {Function} msgCallback 伺服器資訊的回撥傳資料給函式 * @param {String} name 可選值 用於區分ws,用於debugger */ constructor(url, msgCallback, name = 'default') { this.url = url; this.msgCallback = msgCallback; this.name = name; this.ws = null;// websocket物件 this.status = null; // websocket是否關閉 } /** * @description: 初始化 連線websocket或重連webSocket時呼叫 * @param {*} 可選值 要傳的資料 */ connect(data) { // 新建 WebSocket 例項 this.ws = new WebSocket(this.url); this.ws.onopen = e => { // 連線ws成功回撥 this.status = 'open'; console.log(`${this.name}連線成功`, e) // this.heartCheck(); if (data !== undefined) { // 有要傳的資料,就發給後端 return this.ws.send(data); } } // 監聽伺服器端返回的資訊 this.ws.onmessage = e => { // 把資料傳給回撥函式,並執行回撥 // if (e.data === 'pong') { //this.pingPong = 'pong'; // 伺服器端返回pong,修改pingPong的狀態 // } return this.msgCallback(e.data); } // ws關閉回撥 this.ws.onclose = e => { this.closeHandle(e); // 判斷是否關閉 } // ws出錯回撥 this.onerror = e => { this.closeHandle(e); // 判斷是否關閉 } } // heartCheck() { //// 心跳機制的時間可以自己與後端約定 //this.pingPong = 'ping'; // ws的心跳機制狀態值 //this.pingInterval = setInterval(() => { //if (this.ws.readyState === 1) { //// 檢查ws為連結狀態 才可傳送 //this.ws.send('ping'); // 客戶端傳送ping //} //}, 10000) //this.pongInterval = setInterval(() => { //this.pingPong = false; //if (this.pingPong === 'ping') { //this.closeHandle('pingPong沒有改變為pong'); // 沒有返回pong 重啟webSocket //} //// 重置為ping 若下一次 ping 傳送失敗 或者pong返回失敗(pingPong不會改成pong),將重啟 //console.log('返回pong') //this.pingPong = 'ping' //}, 20000) // } // 傳送資訊給伺服器 sendHandle(data) { console.log(`${this.name}傳送訊息給伺服器:`, data) return this.ws.send(data); } closeHandle(e = 'err') { // 因為webSocket並不穩定,規定只能手動關閉(調closeMyself方法),否則就重連 if (this.status !== 'close') { console.log(`${this.name}斷開,重連websocket`, e) // if (this.pingInterval !== undefined && this.pongInterval !== undefined) { //// 清除定時器 //clearInterval(this.pingInterval); //clearInterval(this.pongInterval); // } this.connect(); // 重連 } else { console.log(`${this.name}websocket手動關閉`) } } // 手動關閉WebSocket closeMyself() { console.log(`關閉${this.name}`) this.status = 'close'; return this.ws.close(); } } function someFn(data) { console.log('接收伺服器訊息的回撥:', data); } // const wsValue = new WebSocketClass('ws://121.40.165.18:8800', someFn, 'wsName'); // 這個連結一天只能傳送訊息50次 const wsValue = new WebSocketClass('wss://echo.websocket.org', someFn, 'wsName'); // 阮一峰老師教程連結 wsValue.connect('立即與伺服器通訊'); // 連線伺服器 // setTimeout(() => { //wsValue.sendHandle('傳訊息給伺服器') // }, 1000); // setTimeout(() => { //wsValue.closeMyself(); // 關閉ws // }, 10000) 複製程式碼
栗子裡面我直接寫在了一起,可以把class
放在一個js檔案裡面,export
出去,然後在需要用的地方再import
進來,把引數傳進去就可以用了。
WebSocket不穩定
WebSocket並不穩定,在使用一段時間後,可能會斷開連線,貌似至今沒有一個為何會斷開連線的公論,所以我們需要讓WebSocket保持連線狀態,這裡推薦兩種方法。
WebSocket設定變數,判斷是否手動關閉連線:
class
類中就是用的這種方式:設定一個變數,在webSocket關閉/報錯的回撥中,判斷是不是手動關閉的,如果不是的話,就重新連線,這樣做的優缺點如下:
- 優點:請求較少(相對於心跳連線),易設定。
- 缺點:可能會導致丟失資料,在斷開重連的這段時間中,恰好雙方正在通訊。
WebSocket心跳機制:
因為第一種方案的缺點,並且可能會有其他一些未知情況導致斷開連線而沒有觸發Error或Close事件。這樣就導致實際連線已經斷開了,而客戶端和服務端卻不知道,還在傻傻的等著訊息來。
然後聰明的程式猿們想出了一種叫做心跳機制 的解決方法:
客戶端就像心跳一樣每隔固定的時間傳送一次ping
,來告訴伺服器,我還活著,而伺服器也會返回pong
,來告訴客戶端,伺服器還活著。
具體的實現方法,在上面class的註釋中,將其開啟,即可看到效果。
關於WebSocket
怕一開始就堆太多文字性的內容,把各位嚇跑了,現在大家已經會用了,我們再回頭來看看WebSocket的其他知識點。
WebSocket的當前狀態:WebSocket.readyState
下面是WebSocket.readyState
的四個值(四種狀態):
- 0: 表示正在連線
- 1: 表示連線成功,可以通訊了
- 2: 表示連線正在關閉
- 3: 表示連線已經關閉,或者開啟連線失敗
我們可以利用當前狀態來做一些事情,比如上面栗子中當WebSocket連結成功後,才允許客戶端傳送ping
。
if (this.ws.readyState === 1) { // 檢查ws為連結狀態 才可傳送 this.ws.send('ping'); // 客戶端傳送ping } 複製程式碼
WebSocket
還可以傳送/接收 二進位制資料
這裡我也沒有試過,我是看阮一峰老師的WebSocket教程才知道有這麼個東西,有興趣的可以再去谷歌,大家知道一下就可以。
二進位制資料包括:blob
物件和Arraybuffer
物件,所以我們需要分開來處理。
// 接收資料 ws.onmessage = function(event){ if(event.data instanceof ArrayBuffer){ // 判斷 ArrayBuffer 物件 } if(event.data instanceof Blob){ // 判斷 Blob 物件 } } // 傳送 Blob 物件的例子 let file = document.querySelector('input[type="file"]').files[0]; ws.send(file); // 傳送 ArrayBuffer 物件的例子 var img = canvas_context.getImageData(0, 0, 400, 320); var binary = new Uint8Array(img.data.length); for (var i = 0; i < img.data.length; i++) { binary[i] = img.data[i]; } ws.send(binary.buffer); 複製程式碼
如果你要傳送的二進位制資料很大的話,如何判斷髮送完畢:
webSocket.bufferedAmount
屬性,表示還有多少位元組的二進位制資料沒有傳送出去:
var data = new ArrayBuffer(10000000); socket.send(data); if (socket.bufferedAmount === 0) { // 傳送完畢 } else { // 傳送還沒結束 } 複製程式碼
上述栗子出自阮一峰老師的WebSocket教程
WebSocket的優點:
最後再吹一波WebSocket:
-
雙向通訊(一開始說的,也是最重要的一點)。
-
資料格式比較輕量,效能開銷小,通訊高效
協議控制的資料包頭部較小,而HTTP協議每次通訊都需要攜帶完整的頭部
-
更好的二進位制支援
-
沒有同源限制,客戶端可以與任意伺服器通訊
-
與 HTTP 協議有著良好的相容性。預設埠也是80和443,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器
結語
看了本文之後,如果還是有點迷糊的話,一定要把文中的兩個栗子,新建個html檔案跑起來,自己鼓搗鼓搗一下。不然讀多少部落格/教程都沒有用,實踐才出真知,切勿紙上談兵。