原創不易,但願能關注下咱們,再順手點個贊~~ |
本文首發於政採雲前端團隊博客: WebSocket 原理淺析與實現簡單聊html
隨着 Web 的發展,用戶對於 Web 的實時推送要求也愈來愈高,在 WebSocket 出現以前,大多數狀況下是經過客戶端發起輪詢來拿到服務端實時更新的數據,由於 HTTP1.x 協議有一個缺陷就是通訊只能由客戶端發起,服務端無法主動給客戶端推送。這種方式在對實時性要求比較高的場景下,好比即時通信、即時報價等,顯然會十分低效,體驗也很差。爲了解決這個問題,便出現了 WebSocket 協議,實現了客戶端和服務端雙向通訊的能力。介紹 WebSocket 以前,仍是讓咱們先了解下輪詢實現推送的方式。前端
短輪詢的實現思路就是瀏覽器端每隔幾秒鐘向服務器端發送 HTTP 請求,服務端在收到請求後,不管是否有數據更新,都直接進行響應。在服務端響應完成,就會關閉這個 TCP 鏈接,代碼實現也最簡單,就是利用 XHR , 經過 setInterval 定時向後端發送請求,以獲取最新的數據。git
setInterval(function() {
fetch(url).then((res) => {
// success code
})
}, 3000);
複製代碼
客戶端發送請求後服務器端不會當即返回數據,服務器端會阻塞請求鏈接不會當即斷開,直到服務器端有數據更新或者是鏈接超時才返回,客戶端纔再次發出請求新建鏈接、如此反覆從而獲取最新數據。大體效果以下:github
客戶端代碼以下:web
function async() {
fetch(url).then((res) => {
async();
// success code
}).catch(() => {
// 超時
async();
})
}
複製代碼
前面提到的短輪詢(Polling)和長輪詢(Long-Polling), 都是先由客戶端發起 Ajax 請求,才能進行通訊,走的是 HTTP 協議,服務器端沒法主動向客戶端推送信息。算法
當出現相似體育賽事、聊天室、實時位置之類的場景時,輪詢就顯得十分低效和浪費資源,由於要不斷髮送請求,鏈接服務器。WebSocket 的出現,讓服務器端能夠主動向客戶端發送信息,使得瀏覽器具有了實時雙向通訊的能力。express
沒用過 WebSocket 的人,可能會覺得它是個什麼高深的技術。其實否則,WebSocket 經常使用的 API 很少也很容易掌握,不過在介紹如何使用以前,讓咱們先看看它的通訊原理。後端
當客戶端要和服務端創建 WebSocket 鏈接時,在客戶端和服務器的握手過程當中,客戶端首先會向服務端發送一個 HTTP 請求,包含一個 Upgrade 請求頭來告知服務端客戶端想要創建一個 WebSocket 鏈接。瀏覽器
在客戶端創建一個 WebSocket 鏈接很是簡單:安全
let ws = new WebSocket('ws://localhost:9000');
複製代碼
相似於 HTTP 和 HTTPS,ws 相對應的也有 wss 用以創建安全鏈接,本地已 ws 爲例。這時的請求頭以下:
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: Upgrade // 表示該鏈接要升級協議
Cookie: _hjMinimizedPolls=358479; ts_uid=7852621249; CNZZDATA1259303436=1218855313-1548914234-%7C1564625892; csrfToken=DPb4RhmGQfPCZnYzUCCOOade; JSESSIONID=67376239124B4355F75F1FC87C059F8D; _hjid=3f7157b6-1aa0-4d5c-ab9a-45eab1e6941e; acw_tc=76b20ff415689655672128006e178b964c640d5a7952f7cb3c18ddf0064264
Host: localhost:9000
Origin: http://localhost:9000
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: 5fTJ1LTuh3RKjSJxydyifQ== // 與響應頭 Sec-WebSocket-Accept 相對應
Sec-WebSocket-Version: 13 // 表示 websocket 協議的版本
Upgrade: websocket // 表示要升級到 websocket 協議
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
複製代碼
響應頭以下:
Connection: Upgrade
Sec-WebSocket-Accept: ZUip34t+bCjhkvxxwhmdEOyx9hE=
Upgrade: websocket
複製代碼
此時響應行(General)中能夠看到狀態碼 status code 是 101 Switching Protocols , 表示該鏈接已經從 HTTP 協議轉換爲 WebSocket 通訊協議。 轉換成功以後,該鏈接並無中斷,而是創建了一個全雙工通訊,後續發送和接收消息都會走這個鏈接通道。
注意,請求頭中有個 Sec-WebSocket-Key 字段,和相應頭中的 Sec-WebSocket-Accept 是配套對應的,它的做用是提供了基本的防禦,好比惡意的鏈接或者無效的鏈接。Sec-WebSocket-Key 是客戶端隨機生成的一個 base64 編碼,服務器會使用這個編碼,並根據一個固定的算法:
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 一個固定的字符串
accept = base64(sha1(key + GUID)); // key 就是 Sec-WebSocket-Key 值,accept 就是 Sec-WebSocket-Accept 值
複製代碼
其中 GUID 字符串是 RFC6455 官方定義的一個固定字符串,不得修改。
客戶端拿到服務端響應的 Sec-WebSocket-Accept 後,會拿本身以前生成的 Sec-WebSocket-Key 用相同算法算一次,若是匹配,則握手成功。而後判斷 HTTP Response 狀態碼是否爲 101(切換協議),若是是,則創建鏈接,大功告成。
下面來實現一個純文字消息類型的一對一聊天(單聊)功能,廢話很少說,直接上代碼,注意看註釋。
客戶端:
function connectWebsocket() {
ws = new WebSocket('ws://localhost:9000');
// 監聽鏈接成功
ws.onopen = () => {
console.log('鏈接服務端WebSocket成功');
ws.send(JSON.stringify(msgData)); // send 方法給服務端發送消息
};
// 監聽服務端消息(接收消息)
ws.onmessage = (msg) => {
let message = JSON.parse(msg.data);
console.log('收到的消息:', message)
elUl.innerHTML += `<li class="b">小秋:${message.content}</li>`;
};
// 監聽鏈接失敗
ws.onerror = () => {
console.log('鏈接失敗,正在重連...');
connectWebsocket();
};
// 監聽鏈接關閉
ws.onclose = () => {
console.log('鏈接關閉');
};
};
connectWebsocket();
複製代碼
從上面能夠看到 WebSocket 實例的 API 很容易理解,簡單好用,經過 send() 方法能夠發送消息,onmessage 事件用來接收消息,而後對消息進行處理顯示在頁面上。 當 onerror 事件(監聽鏈接失敗)觸發時,最好進行執行重連,以保持鏈接不中斷。
服務端 Node : (這裏使用 ws 庫)
const path = require('path');
const express = require('express');
const app = express();
const server = require('http').Server(app);
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server: server });
wss.on('connection', (ws) => {
// 監聽客戶端發來的消息
ws.on('message', (message) => {
console.log(wss.clients.size);
let msgData = JSON.parse(message);
if (msgData.type === 'open') {
// 初始鏈接時標識會話
ws.sessionId = `${msgData.fromUserId}-${msgData.toUserId}`;
} else {
let sessionId = `${msgData.toUserId}-${msgData.fromUserId}`;
wss.clients.forEach(client => {
if (client.sessionId === sessionId) {
client.send(message); // 給對應的客戶端鏈接發送消息
}
})
}
})
// 鏈接關閉
ws.on('close', () => {
console.log('鏈接關閉');
});
});
複製代碼
同理,服務端也有對應的發送和接收的方法。完整示例代碼見 這裏
這樣瀏覽器和服務端就能夠愉快的發送消息了,效果以下:
其中綠色箭頭表示發出的消息,紅色箭頭表示收到的消息。
在實際使用 WebSocket 中,長時間不通消息可能會出現一些鏈接不穩定的狀況,這些未知狀況致使的鏈接中斷會影響客戶端與服務端以前的通訊,
爲了防止這種的狀況的出現,有一種心跳保活的方法:客戶端就像心跳同樣每隔固定的時間發送一次 ping ,來告訴服務器,我還活着,而服務器也會返回 pong ,來告訴客戶端,服務器還活着。ping/pong 實際上是一條與業務無關的假消息,也稱爲心跳包。
能夠在鏈接成功以後,每隔一個固定時間發送心跳包,好比 60s:
setInterval(() => {
ws.send('這是一條心跳包消息');
}, 60000)
複製代碼
經過上面的介紹,你們應該對 WebSocket 有了必定認識,其實並不神祕,這裏對文章內容簡單總結一下。當建立 WebSocket 實例的時候,會發一個 HTTP 請求,請求報文中有個特殊的字段 Upgrade ,而後這個鏈接會由 HTTP 協議轉換爲 WebSocket 協議,這樣客戶端和服務端創建了全雙工通訊,經過 WebSocket 的 send 方法和 onmessage 事件就能夠經過這條通訊鏈接交換信息。
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「 5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com