在本篇文章之前,WebSocket
很多人聽說過,沒見過,沒用過,以爲是個很高大上的技術,實際上這個技術並不神祕,可以說是個很容易就能掌握的技術,希望在看完本文之後,馬上把文中的栗子拿出來自己試一試,實踐出真知。
游泳、健身瞭解一下:博客、前端積累文檔、公衆號、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文件跑起來,自己鼓搗鼓搗一下。不然讀多少博客/教程都沒有用,實踐纔出真知,切勿紙上談兵。
希望看完的朋友可以點個喜歡/關注,您的支持是對我最大的鼓勵。
以上2018.10.22
參考資料: