NodeJS落地WebSocket實踐前端架構(gòu)師破局技術(shù)
網(wǎng)絡(luò)協(xié)議進(jìn)化
HTTP 協(xié)議是前端最熟悉的網(wǎng)絡(luò)通信協(xié)議。我們通常的打開網(wǎng)頁,請求接口,都屬于 HTTP 請求。
HTTP 請求的特點是:請求-> 響應(yīng)??蛻舳税l(fā)起請求,服務(wù)端收到請求后進(jìn)行響應(yīng),一次請求就完成了。也就是說,HTTP 請求必須由客戶端發(fā)起,服務(wù)端才能被動響應(yīng)。
除此之外,發(fā)起 HTTP 請求之前,還需要通過三次握手建立 TCP 連接。HTTP/1.0 的特點是,每通信一次,都要經(jīng)歷 “三步走” 的過程 —— TCP 連接 -> HTTP 通信 -> 斷開 TCP 連接。
這樣的每一次請求都是獨(dú)立的,一次請求完成連接就會斷開。
HTTP1.1 對請求過程做了優(yōu)化。TCP 連接建立之后,我們可以進(jìn)行多次 HTTP 通信,等到一個時間段無 HTTP 請求發(fā)起 TCP 才會斷開連接,這就是 HTTP/1.1 帶來的長連接技術(shù)。
但是即便如此,通信方式依然是客戶端發(fā)起,服務(wù)端響應(yīng),這個根本邏輯不會變。
隨著應(yīng)用交互的復(fù)雜,我們發(fā)現(xiàn),有一些場景是必須要實時獲取服務(wù)端消息的。
比如即時聊天,比如消息推送,用戶并不會主動發(fā)起請求,但是當(dāng)服務(wù)器有了新消息,客戶端需要立刻知道并且反饋給用戶。
HTTP 不支持服務(wù)端主動推送,但是這些場景又急需解決方案,于是早期出現(xiàn)了輪詢(polling)。輪詢是客戶端定時向服務(wù)器發(fā)起請求,檢測服務(wù)端是否有更新,如果有則返回新數(shù)據(jù)。
這種輪詢方式雖然簡單粗暴,但很顯然有兩個弊端:
- 請求消耗太大??蛻舳瞬粩嗾埱螅速M(fèi)流量和服務(wù)器資源,給服務(wù)器造成壓力。
- 不能保證及時??蛻舳诵枰胶饧皶r性和性能,請求間隔必然不能太小,因此會有延遲。
隨著 HTML5 推出 WebSocket
,即時通訊場景終于迎來了根本解決方案。WebSocket
是全雙工通信協(xié)議,當(dāng)客戶端與服務(wù)端建立連接之后,雙方可以互相發(fā)送數(shù)據(jù),這樣的話就不需要客戶端通過輪詢這種低效的方式獲取數(shù)據(jù),服務(wù)端有新消息直接推送給客戶端即可。
傳統(tǒng) HTTP 連接方式如下:
## 普通連接 http://localhost:80/test ## 安全連接 https://localhost:80/test
WebSocket 是另一種協(xié)議,連接方式如下:
## 普通連接 ws://localhost:80/test ## 安全連接 wss://localhost:80/test
但是 WebSocket 也不是完全脫離 HTTP 的,若要建立 WebSocket 連接,則必須要客戶端主動發(fā)起一個建立連接的 HTTP 請求,連接成功之后客戶端與服務(wù)端才能進(jìn)行雙向通信。
Socket.IO?
提起用 Node.js 實現(xiàn) WebSocket,大家一定會想到一個庫:Socket.IO
沒錯,Socket.IO 是目前 Node.js 在生產(chǎn)環(huán)境中開發(fā) WebSocket 應(yīng)用最好的選擇。它功能強(qiáng)大,高性能,低延遲,并且可以一步集成到 express
框架中。
但是也許你不清楚,Socket.IO 并不是一個純粹的 WebSocket 框架。它是將 Websocket 和輪詢機(jī)制以及其它的實時通信方式封裝成了通用的接口,以實現(xiàn)更高效的雙向通信。
嚴(yán)格來說,Websocket 只是 Socket.IO 的一部分。
也許你會問:既然 Socket.IO 在 WebSocket 的基礎(chǔ)上做了那么多的優(yōu)化,并且非常成熟,那為什么還要搭一個原生 WebSocket 服務(wù)?
首先,Socket.IO 不能通過原生的 ws
協(xié)議連接。比如你在瀏覽器試圖通過 ws://localhost:8080/test-socket
這種方式連接 Socket.IO 服務(wù),是連接不上的。因為 Socket.IO 的服務(wù)端必須通過 Socket.IO 的客戶端連接,不支持默認(rèn)的 WebSocket 方式連接。
其次,Socket.IO 封裝程度非常高,使用它可能不利于你了解 WebSocket 建立連接的原理。
因此,我們本篇就用 Node.js 中基礎(chǔ)的 ws
模塊,從頭開始實現(xiàn)一個原生的 WebSocket 服務(wù),并且在前端用 ws 協(xié)議直接連接,體驗一把雙向通信的感覺!
ws 模塊實現(xiàn)
ws 是 Node.js 下一個簡單快速,并且定制程度極高的 WebSocket 實現(xiàn)方案,同時包含了服務(wù)端和客戶端。
用 ws
搭建起來的服務(wù)端,瀏覽器可以通過原生 WebSocket
構(gòu)造函數(shù)直接連接,非常便捷。ws 客戶端則是模擬瀏覽器的 WebSocket 構(gòu)造函數(shù),用于連接其他 WebSocket 服務(wù)器進(jìn)行通信。
注意一點:ws
只能在 Node.js 環(huán)境中使用,瀏覽器中不可用,瀏覽器請直接使用原生 WebSocket 構(gòu)造函數(shù)。
下面開始接入,第一步,安裝 ws:
$ npm install ws
安裝好后,我們先搭建一個 ws 服務(wù)端。
服務(wù)端
搭建 websocket 服務(wù)器需要用 WebSocketServer 構(gòu)造函數(shù)。
const { WebSocketServer } = require('ws') const wss = new WebSocketServer({ port: 8080 }) wss.on('connection', (ws, req) => { console.log('客戶端已連接:', req.socket.remoteAddress) ws.on('message', data => { console.log('收到客戶端發(fā)送的消息:', data) }) ws.send('我是服務(wù)端') // 向當(dāng)前客戶端發(fā)送消息 })
把這段代碼寫進(jìn) ws-server.js
然后運(yùn)行:
$ node ws-server.js
客戶端
上一步建好了 WebSocket 服務(wù)器,現(xiàn)在我們在前端連接并監(jiān)聽消息:
var ws = new WebSocket('ws://localhost:8080') ws.onopen = function(mevt) { console.log('客戶端已連接') } ws.onmessage = function(mevt) { console.log('客戶端收到消息: ' + evt.data) ws.close() } ws.onclose = function(mevt) { console.log('連接關(guān)閉') }
將代碼寫入 wsc.html
然后用瀏覽器打開,看到打印如下:
可以看到,瀏覽器連接成功后,收到服務(wù)端主動推送過來的消息,然后瀏覽器可以主動關(guān)閉連接。
Node.js 環(huán)境下我們看 ws 模塊如何發(fā)起連接:
const WebSocket = require('ws') var ws = new WebSocket('ws://localhost:8080') ws.on('open', () => { console.log('客戶端已連接') }) ws.on('message', data => { console.log('客戶端收到消息: ' + data) ws.close() }) ws.on('close', () => { console.log('連接關(guān)閉') })
代碼與瀏覽器的邏輯一摸一樣,只是寫法稍有些不同,注意區(qū)別。
需要特殊說明的一點,瀏覽器端監(jiān)聽 message
事件的回調(diào)函數(shù),參數(shù)是一個 MessageEvent 的實例對象,服務(wù)端發(fā)來的實際數(shù)據(jù)需要通過 mevt.data
獲取。
而在 ws 客戶端,這個參數(shù)就是服務(wù)端的實際數(shù)據(jù),直接獲取即可。
Express 集成
ws 模塊一般不會單獨(dú)使用,更優(yōu)的方案是集成到現(xiàn)有的框架中。這節(jié)我們將 ws 模塊集成到 Express 框架。
集成到 Express 框架的優(yōu)點是,我們不需要單獨(dú)監(jiān)聽一個端口,使用框架啟動的端口即可,并且我們還可以指定訪問到某個路由,才發(fā)起 WebSocket 連接。
幸運(yùn)的是這一切不需要手動實現(xiàn),express-ws 模塊已經(jīng)幫我們做好了大部分的集成工作。
首先安裝,然后在入口文件引入:
var expressWs = require('express-ws')(app)
和 Express 的 Router 一樣,express-ws 也支持注冊全局路由和局部路由。
先看全局路由,通過 [host]/test-ws
連接:
app.ws('/test-ws', (ws, req) => { ws.on('message', msg => { ws.send(msg) }) })
局部路由則是注冊在一個路由組下面的子路由。配置一個名為 websocket
的路由組并指向 websocket.js
文件,代碼如下:
// websocket.js var router = express.Router() router.ws('/test-ws', (ws, req) => { ws.on('message', msg => { ws.send(msg) }) }) module.exports = router
連接 [host]/websocket/test-ws
就可以訪問到這個子路由。
路由組的作用是定義一個 websocket 連接組,不同需求連接這個組下的不同子路由。比如可以將 單聊 和 群聊 設(shè)置為兩個子路由,分別處理各自的連接通信邏輯。
完整代碼如下:
var express = require('express') var app = express() var wsServer = require('express-ws')(app) var webSocket = require('./websocket.js') app.ws('/test-ws', (ws, req) => { ws.on('message', msg => { ws.send(msg) }) }) app.use('/websocket', webSocket) app.listen(3000)
實際開發(fā)中獲取常用信息的小方法:
// 客戶端的IP地址 req.socket.remoteAddress // 連接參數(shù) req.query
WebSocket 實例
WebSocket 實例是指客戶端連接對象,以及服務(wù)端連接的第一個參數(shù)。
var ws = new WebSocket('ws://localhost:8080') app.ws('/test-ws', (ws, req) => {}
代碼中的 ws
就是 WebSocket 實例,表示建立的連接。
瀏覽器
瀏覽器的 ws 對象中包含的信息如下:
{ binaryType: 'blob' bufferedAmount: 0 extensions: '' onclose: null onerror: null onmessage: null onopen: null protocol: '' readyState: 3 url: 'ws://localhost:8080/' }
首先非常關(guān)鍵的是四個監(jiān)聽屬性,用于定義函數(shù):
onopen
:連接建立后的函數(shù)onmessage
:收到服務(wù)端推送消息的函數(shù)onclose
:連接關(guān)閉的函數(shù)onerror
:連接異常的函數(shù)
其中最常用的是 onmessage
屬性,賦值為一個函數(shù)來監(jiān)聽服務(wù)端消息:
ws.onmessage = mevt => { console.log('消息:', mevt.data) }
還有一個關(guān)鍵屬性是 readyState
,表示連接狀態(tài),值為一個數(shù)字。并且每個值都可以用常量表示,對應(yīng)關(guān)系和含義如下:
0
: 常量WebSocket.CONNECTING
,表示正在連接1
: 常量WebSocket.OPEN
,表示已連接2
: 常量WebSocket.CLOSING
,表示正在關(guān)閉3
: 常量WebSocket.CLOSED
,表示已關(guān)閉
當(dāng)然最重要的還有 send
方法用于發(fā)送信息,向服務(wù)端發(fā)送數(shù)據(jù):
ws.send('要發(fā)送的信息')
服務(wù)端
服務(wù)端的 ws 對象表示當(dāng)前發(fā)起連接的一個客戶端,基本屬性與瀏覽器大致相同。
比如上面客戶端的四個監(jiān)聽屬性,readyState
屬性,以及 send
方法都是一致的。不過因為服務(wù)端是 Node.js 實現(xiàn),因此會有更豐富的支持。
比如下面兩種監(jiān)聽事件的寫法效果是一樣的:
// Node.js 環(huán)境 ws.onmessage = str => { console.log('消息:', str) } ws.on('message', str => { console.log('消息:', mevt.data) })
詳細(xì)的屬性和介紹可以查閱官方文檔
消息廣播
WebSocket 服務(wù)器不會只有一個客戶端連接,消息廣播的意思就是把信息發(fā)給所有已連接的客戶端,像一個大喇叭一樣,所有人都聽得到,經(jīng)典場景就是熱點推送。
那么廣播之前,就必須要解決一個問題,如何獲取當(dāng)前已連接(在線)的客戶端?
其實 ws
模塊提供了快捷的獲取方法:
var wss = new WebSocketServer({ port: 8080 }) // 獲取所有已連接客戶端 wss.clients
方便吧。再看 express-ws
怎么獲?。?/p>
var wsServer = expressWebSocket(app) var wss = wsServer.getWss() // 獲取所有已連接客戶端 wss.clients
拿到 wss.clients
后,我們看看它到底是什么樣子。經(jīng)過打印,發(fā)現(xiàn)它的數(shù)據(jù)結(jié)構(gòu)比想象到還要簡單,就是由所有在線客戶端的 WebSocket 實例組成的一個 Set
集合。
那么,獲取當(dāng)前在線客戶端的數(shù)量:
wss.clients.size
簡單粗暴的實現(xiàn)廣播:
wss.clients.forEach(client => { if (client.readyState === 1) { client.send('廣播數(shù)據(jù)') } })
這是非常簡單,基礎(chǔ)的實現(xiàn)方式。試想一下如果此刻在線客戶有 10000 個,那么這個循環(huán)多半會卡死吧。因此才會有像 socket.io 這樣的庫,對基礎(chǔ)功能做了大量優(yōu)化和封裝,提高并發(fā)性能。
上面的廣播屬于全局廣播,就是將消息發(fā)給所有人。然而還有另一種場景,比如一個 5 人的群聊小組聊天,這時的廣播只是給這 5 人小團(tuán)體發(fā)消息,因此這也叫 局部廣播。
局部廣播的實現(xiàn)要復(fù)雜一些,一般會揉合具體的業(yè)務(wù)場景。這就需要我們在客戶端連接時,對客戶端數(shù)據(jù)做持久化處理了。比如用 Redis
存儲在線客戶端的狀態(tài)和數(shù)據(jù),這樣檢索分類更快,效率更高。
局部廣播實現(xiàn),那一對一私聊就更容易了。找到兩個客戶端對應(yīng)的 WebSocket 實例互發(fā)消息就行。
安全與認(rèn)證
前面搭建好的 WebSocket 服務(wù)器,默認(rèn)任何客戶端都可以連接,這在生產(chǎn)環(huán)境肯定是不行的。我們要對 WebSocket 服務(wù)器做安全保障,主要是從兩個方面入手:
- Token 連接認(rèn)證
- wss 支持
下面說一說我的實現(xiàn)思路。
Token 連接認(rèn)證
HTTP 請求接口我們一般會做 JWT 認(rèn)證,在請求頭中帶一個指定 Header,將一個 token 字符串傳過去,后端會拿這個 token 做校驗,校驗失敗則返回 401 錯誤阻止請求。
我們上面說過,WebSocket
建立連接的第一步是客戶端發(fā)起一個 HTTP 的連接請求,那么我們在這個 HTTP 請求上做驗證,如果驗證失敗,則中段 WebSocket 的連接創(chuàng)建,不就可以了?
順著這個思路,我們來改造一下服務(wù)端代碼。
因為要在 HTTP 層做校驗,所以用 http
模塊創(chuàng)建服務(wù)器,關(guān)掉 WebSocket 服務(wù)的端口。
var server = http.createServer() var wss = new WebSocketServer({ noServer: true }) server.listen(8080)
當(dāng)客戶端通過 ws://
連接服務(wù)端時,服務(wù)端會進(jìn)行協(xié)議升級,也就是將 http 協(xié)議升級成 websocket 協(xié)議,此時會觸發(fā) upgrade
事件:
server.on('upgrade', (request, socket) => { // 用 request 獲取參數(shù)做驗證 // 1. 驗證不通過判斷 if ('驗證失敗') { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n') socket.destroy() return } // 2. 驗證通過,繼續(xù)建立連接 wss.handleUpgrade(request, socket, _, ws => { wss.emit('connection', ws, request) }) }) // 3. 監(jiān)聽連接 wss.on('connection', (ws, request) => { console.log('客戶端已連接') ws.send('服務(wù)端信息') })
這樣服務(wù)端認(rèn)證添加完畢,具體的認(rèn)證方法結(jié)合客戶端的傳參方式來定。
WebSocket 客戶端連接不支持自定義 Header,因此不能用 JWT 的方案,可用方案有兩種:
- Basic Auth
- Quary 傳參
Basic Auth
認(rèn)證簡單說就是賬號+密碼認(rèn)證,而且賬號密碼是帶在 URL 里的。
假設(shè)我有賬號是 ruims,密碼是 123456,那么客戶端連接是這樣:
var ws = new WebSocket('ws://ruims:123456@localhost:8080')
那么服務(wù)端就會收到這樣一個請求頭:
wss.on('connection', (ws, req) => { if(req.headers['authorization']) { let auth = req.headers['authorization'] console.log(auth) // 打印的值:Basic cnVpbXM6MTIzNDU2 } }
其中 cnVpbXM6MTIzNDU2 就是 ruims:123456
的 base64 編碼,服務(wù)端可以獲取到這個編碼來做認(rèn)證。
Quary
傳參比較簡單,就是普通的 URL 傳參,可以帶一個短一點的加密字符串過去,服務(wù)端獲取到該字符串然后做認(rèn)證:
var ws = new WebSocket('ws://localhost:8080?token=cnVpbXM6MTIzNDU2')
服務(wù)端獲取參數(shù):
wss.on('connection', (ws, req) => { console.log(req.query.token) }
wss 支持
WebSocket 客戶端使用 ws://
協(xié)議連接,那 wss 是什么意思?
其實非常簡單,和 https 原理一摸一樣。
https
表示安全的 http 協(xié)議,組成是 HTTP + SSL
wss
則表示安全的 ws 協(xié)議,組成是 WS + SSL
那為什么一定要用 wss 呢?除了安全性,還有一個關(guān)鍵原因是:如果你的 web 應(yīng)用是 https 協(xié)議,你在當(dāng)前應(yīng)用中使用 WebSocket 就必須是 wss 協(xié)議,否則瀏覽器拒絕連接。
配置 wss 直接在 https 配置中加一個 location
即可,直接上 nginx 配置:
location /websocket { proxy_pass http://127.0.0.1:8080; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }
然后客戶端連接就變成了這樣:
var ws = new WebSocket('wss://[host]/websocket')
BFF 應(yīng)用
BFF 或許你聽說過,全稱是 Backend For Frontend
,意思是為前端服務(wù)的后端,在實際應(yīng)用架構(gòu)中屬于前端和后端的一個 中間層。
這個中間層一般是由 Node.js 實現(xiàn),那么它有什么作用呢?
眾所周知,現(xiàn)在后端的主流架構(gòu)是微服務(wù),微服務(wù)情況下 API 會劃分的非常細(xì),商品服務(wù)就是商品服務(wù),通知服務(wù)就是通知服務(wù)。當(dāng)你想在商品上架時給用戶發(fā)一個通知,可能至少需要調(diào)兩個接口。
這樣的話對前端其實是不友好的,于是后來出現(xiàn)了 BFF 中間層,相當(dāng)于一個后端請求的中間代理站,前端可以直接請求 BFF 的接口,然后 BFF 再向后端接口請求,將需要的數(shù)據(jù)組合起來,一次返回前端。
那我們在上面講的一大堆 WebSocket 的知識,在 BFF 層如何應(yīng)用呢?
我想到的應(yīng)用場景至少有 4 個:
- 查看當(dāng)前在線人數(shù),在線用戶信息
- 登錄新設(shè)備,其他設(shè)備退出登錄
- 檢測網(wǎng)絡(luò)連接/斷開
- 站內(nèi)消息,小圓點提示
這些功能以前是在后端實現(xiàn)的,并且會與其他業(yè)務(wù)功能耦合?,F(xiàn)在有了 BFF,那么 WebSocket 完全可以在這一層實現(xiàn),讓后端可以專注核心數(shù)據(jù)邏輯。
由此可見,掌握了 WebSocket 在 Node.js 中的實踐應(yīng)用,作為前端的我們可以破除內(nèi)卷,在另一個領(lǐng)域繼續(xù)發(fā)揮價值,豈不美哉?
源碼地址:https://github.com/ruidoc/blog-codes
以上就是NodeJS落地WebSocket實踐前端架構(gòu)師破局技術(shù)的詳細(xì)內(nèi)容,更多關(guān)于前端架構(gòu)NodeJS WebSocket的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
nodejs socket實現(xiàn)的服務(wù)端和客戶端功能示例
這篇文章主要介紹了nodejs socket實現(xiàn)的服務(wù)端和客戶端功能,結(jié)合具體實例形式分析了nodejs基于socket通信實現(xiàn)的服務(wù)端與客戶端功能相關(guān)操作技巧,需要的朋友可以參考下2017-06-06Node.js中Process.nextTick()和Process.setImmediate()的區(qū)別
這篇文章介紹了Node.js中Process.nextTick()和Process.setImmediate()的區(qū)別,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07詳解如何使用node.js的開發(fā)框架express創(chuàng)建一個web應(yīng)用
這篇文章主要介紹了詳解如何使用node.js的開發(fā)框架express創(chuàng)建一個web應(yīng)用,網(wǎng)上各種搜索后,整理了下快速搭建express框架的步驟。非常具有實用價值,需要的朋友可以參考下2018-12-12使用nodeJS中的fs模塊對文件及目錄進(jìn)行讀寫,刪除,追加,等操作詳解
nodeJS中fs模塊對系統(tǒng)文件及目錄進(jìn)行讀寫操作,本文將詳細(xì)介紹nodejs中的文件操作模塊fs的使用方法2020-02-02