websocket 是一種網(wǎng)絡(luò)通信協(xié)議,一般用來(lái)進(jìn)行實(shí)時(shí)通信會(huì)使用到
為什么要用 websocket
websocket 協(xié)議和 http 協(xié)議類似,http 協(xié)議有一個(gè)缺陷,只能由客戶方端發(fā)起請(qǐng)求,服務(wù)端根據(jù)請(qǐng)求 url 和傳過(guò)去的參數(shù)返回對(duì)應(yīng)結(jié)果
websocket 是雙向通信的,只要 websocket 連接建立起來(lái),可以由客戶端給服務(wù)端發(fā)送數(shù)據(jù),也可以由服務(wù)端主動(dòng)給客戶端發(fā)送數(shù)據(jù)
websocket 適用場(chǎng)景:聊天室
簡(jiǎn)介
websocket 相關(guān)簡(jiǎn)介,可以看阮老師的文章
用法
服務(wù)端nodejs-websocket
nodejs 可以通過(guò)nodejs-websocket來(lái)實(shí)現(xiàn)創(chuàng)建一個(gè) websocket 的服務(wù)
// websocket.js
const ws = require('nodejs-websocket')
const createServer = () => {
let server = ws.createServer(connection => {
connection.on('text', function(result) {
console.log('發(fā)送消息', result)
})
connection.on('connect', function(code) {
console.log('開(kāi)啟連接', code)
})
connection.on('close', function(code) {
console.log('關(guān)閉連接', code)
})
connection.on('error', function(code) {
console.log('異常關(guān)閉', code)
})
})
return server
}
module.exports = createServer()
nodejs-websocket用法
文檔地址:https://www.npmjs.com/package/nodejs-websocket
node 創(chuàng)建的 websocket 服務(wù),主要包含三個(gè)概念
WS: 引入nodejs-websocket后的主要對(duì)象
- ws.createServer([options], [callback]):創(chuàng)建一個(gè) server 對(duì)象
- ws.connect(URL, [options], [callback]):創(chuàng)建一個(gè) connect 對(duì)象,一般由客戶端鏈接服務(wù)端 websocket 服務(wù)時(shí)創(chuàng)建
- ws.setBinaryFragmentation(bytes):設(shè)置傳輸二進(jìn)制文件的最小尺寸,默認(rèn) 512kb
- setMaxBufferLength:設(shè)置傳輸二進(jìn)制文件的最大尺寸,默認(rèn) 2M
Server:通過(guò) ws.createServer 創(chuàng)建
Function
- server.listen(port, [host], [callback]): 傳入端口和主機(jī)地址后,開(kāi)啟一個(gè) websocket 服務(wù)
- server.close([callback]): 關(guān)閉 websocket 服務(wù)
- server.connections: 返回包含所有 connection 的數(shù)組,可以用來(lái)廣播所有消息
// 服務(wù)端廣播
function broadcast(server, msg) {
server.connections.forEach(function(conn) {
conn.sendText(msg)
})
}
Event
可以通過(guò)server.on('event', (res) => {console.log(res)})調(diào)用
- Event: 'listening()':調(diào)用
server.listen會(huì)觸發(fā)當(dāng)前事件 - Event: 'close()': 當(dāng)服務(wù)關(guān)閉時(shí)觸發(fā)該事件,如果有任何一個(gè) connection 保持鏈接,都不會(huì)觸發(fā)該事件
- Event: 'error(errObj)':發(fā)生錯(cuò)誤時(shí)觸發(fā),此事件后會(huì)直接調(diào)用 close 事件
- Event: 'connection(conn)':建立新鏈接(完成握手后)觸發(fā),conn 是連接的實(shí)例對(duì)象
Connection:每一個(gè)客戶端創(chuàng)建連接時(shí)的實(shí)例
Function
- connection.sendText(str, [callback]):發(fā)送字符串給另一側(cè),可以由服務(wù)端發(fā)送字符串?dāng)?shù)據(jù)給客戶端
- connection.beginBinary():要求連接開(kāi)始傳輸二進(jìn)制,返回一個(gè)
WritableStream - connection.sendBinary(data, [callback]): 發(fā)送一個(gè)二進(jìn)制塊,類似
connection.beginBinary().end(data) - connection.send(data, [callback]): 發(fā)送一個(gè)字符串或者二進(jìn)制內(nèi)容到客戶端,如果發(fā)送的是文本,類似于
sendText(),如果發(fā)送的是二進(jìn)制,類似于sendBinary(),
callback將監(jiān)聽(tīng)發(fā)送完成的回調(diào) - connection.close([code, [reason]]):開(kāi)始關(guān)閉握手(發(fā)送一個(gè)關(guān)閉指令)
- connection.server:如果服務(wù)是 nodejs 啟動(dòng),這里會(huì)保留 server 的引用
- connection.readyState:一個(gè)常量,表示連接的當(dāng)前狀態(tài)
connection.CONNECTING:值為 0,表示正在連接
connection.OPEN:值為 1,表示連接成功,可以通信了
connection.CLOSING:值為 2,表示連接正在關(guān)閉。
connection.CLOSED:值為 3,表示連接已經(jīng)關(guān)閉,或者打開(kāi)連接失敗。
- connection.outStream: 存儲(chǔ)
connection.beginBinary()返回的OutStream對(duì)象,沒(méi)有則返回 null - connection.path:表示建立連接的路徑
- connection.headers:只讀請(qǐng)求頭的 name 的 value 對(duì)應(yīng)的 object 對(duì)象
- connection.protocols:客戶端請(qǐng)求的協(xié)議數(shù)組,沒(méi)有則返回空數(shù)組
- connection.protocol:同意連接的協(xié)議,如果有這個(gè)協(xié)議,它會(huì)包含在
connection.protocols數(shù)組里面
Event
- Event: 'close(code, reason)': 連接關(guān)閉時(shí)觸發(fā)
- Event: 'error(err)':發(fā)生錯(cuò)誤時(shí)觸發(fā),如果握手無(wú)效,也會(huì)發(fā)出響應(yīng)
- Event: 'text(str)':收到文本時(shí)觸發(fā),str 時(shí)收到的文本字符串
- Event: 'binary(inStream)':收到二進(jìn)制內(nèi)容時(shí)觸發(fā),
inStream時(shí)一個(gè)ReadableStream
var server = ws
.createServer(function(conn) {
console.log('New connection')
conn.on('binary', function(inStream) {
// 創(chuàng)建空的buffer對(duì)象,收集二進(jìn)制數(shù)據(jù)
var data = new Buffer(0)
// 讀取二進(jìn)制數(shù)據(jù)的內(nèi)容并且添加到buffer中
inStream.on('readable', function() {
var newData = inStream.read()
if (newData)
data = Buffer.concat([data, newData], data.length + newData.length)
})
inStream.on('end', function() {
// 讀取完成二進(jìn)制數(shù)據(jù)后,處理二進(jìn)制數(shù)據(jù)
process_my_data(data)
})
})
conn.on('close', function(code, reason) {
console.log('Connection closed')
})
})
.listen(8001)
- Event: 'connect()':連接完全建立后發(fā)出
具體代碼實(shí)現(xiàn)
const ws = require('nodejs-websocket')
// 可以通過(guò)不同的code可以表示要后端實(shí)現(xiàn)的不同邏輯
const {
RECEIEVE_MESSAGE,
SAVE_USER_INFO,
CLOSE_CONNECTION
} = require('../constants/config')
// 當(dāng)前聊天室的用戶
let chatUsers = []
// 廣播通知
const broadcast = (server, info) => {
console.log('broadcast', info)
server.connections.forEach(function(conn) {
conn.sendText(JSON.stringify(info))
})
}
// 服務(wù)端獲取到某個(gè)用戶的信息通知到所有用戶
const broadcastInfo = (server, info) => {
let count = server.connections.length
let result = {
code: RECEIEVE_MESSAGE,
count: count,
...info
}
broadcast(server, result)
}
// 返回當(dāng)前剩余的在線用戶
const sendChatUsers = (server, user) => {
let chatIds = chatUsers.map(item => item.chatId)
if (chatIds.indexOf(user.chatId) === -1) {
chatUsers.push(user)
}
let result = {
code: SAVE_USER_INFO,
count: chatUsers.length,
chatUsers: chatUsers
}
broadcast(server, result)
}
// 觸發(fā)關(guān)閉連接,在離開(kāi)頁(yè)面或者關(guān)閉頁(yè)面時(shí),需要主動(dòng)觸發(fā)關(guān)閉連接
const handleCloseConnect = (server, user) => {
chatUsers = chatUsers.filter(item => item.chatId !== user.chatId)
let result = {
code: CLOSE_CONNECTION,
count: chatUsers.length,
chatUsers: chatUsers
}
console.log('handleCloseConnect', user)
broadcast(server, result)
}
// 創(chuàng)建websocket服務(wù)
const createServer = () => {
let server = ws.createServer(connection => {
connection.on('text', function(result) {
let info = JSON.parse(result)
let code = info.code
if (code === CLOSE_CONNECTION) {
handleCloseConnect(server, info)
// 某些情況如果客戶端多次觸發(fā)連接關(guān)閉,會(huì)導(dǎo)致connection.close()出現(xiàn)異常,這里try/catch一下
try {
connection.close()
} catch (error) {
console.log('close異常', error)
}
} else if (code === SAVE_USER_INFO) {
sendChatUsers(server, info)
} else {
broadcastInfo(server, info)
}
})
connection.on('connect', function(code) {
console.log('開(kāi)啟連接', code)
})
connection.on('close', function(code) {
console.log('關(guān)閉連接', code)
})
connection.on('error', function(code) {
// 某些情況如果客戶端多次觸發(fā)連接關(guān)閉,會(huì)導(dǎo)致connection.close()出現(xiàn)異常,這里try/catch一下
try {
connection.close()
} catch (error) {
console.log('close異常', error)
}
console.log('異常關(guān)閉', code)
})
})
// 所有連接釋放時(shí),清空聊天室用戶
server.on('close', () => {
chatUsers = []
})
return server
}
const server = createServer()
module.exports = server
部分前端代碼
前端主要是創(chuàng)建
WebSocket連接后,在onopen事件觸發(fā)時(shí),初始化用戶的一些信息,比如每個(gè)用戶包含唯一的chatId之類的,以及保持用戶昵稱,用戶頭像啥的
再就是監(jiān)聽(tīng)onmessage事件,通過(guò)后端返回的 message 信息執(zhí)行對(duì)應(yīng)的操作,建議前后端約定一些 code 來(lái)表示某一種類似的 message 信息
然后就是監(jiān)聽(tīng)頁(yè)面的一些觸發(fā)事件,將信息通過(guò)send方法發(fā)送給服務(wù)端
let websocket = new WebSocket(wsConfig.WS_ROOT_PATH)
websocket.onopen = () => {
console.log('websocket連接開(kāi)啟...')
if (!this.chatId) {
this.initChatId()
}
this.sendUserName()
}
websocket.onmessage = event => {
let data = event.data
let result = JSON.parse(data)
let code = result.code
let count = result.count
this.updateChatCount(count)
if (code === RECEIEVE_MESSAGE) {
this.pushMessage(result)
this.onMessageScroll()
} else if (code === SAVE_USER_INFO || code === CLOSE_CONNECTION) {
this.updateChatUser(result.chatUsers)
}
console.log('數(shù)據(jù)已接收...', code, result)
}
websocket.onclose = this.onWebsocketClose
websocket.onerror = this.onWebsocketError
// 發(fā)送message
sendMessage(info) {
if (this.websocket && typeof this.websocket.send === 'function') {
this.websocket.send(JSON.stringify(info))
}
}
問(wèn)題
如果瀏覽器進(jìn)入其它頁(yè)面或者關(guān)閉瀏覽器,鏈接會(huì)異常關(guān)閉,經(jīng)常會(huì)導(dǎo)致后端出現(xiàn)異常報(bào)錯(cuò)
// 前端代碼監(jiān)聽(tīng)頁(yè)面關(guān)閉或者刷新
window.onunload = () => {
this.closeConnect()
}
// vue里跳轉(zhuǎn)到其它頁(yè)面
beforeRouteLeave(to, from, next) {
this.closeConnect()
next()
}
總結(jié)
這次使用 websocket 實(shí)現(xiàn)一個(gè)基本的聊天室功能,個(gè)人感覺(jué)還比較簡(jiǎn)單,只是中間會(huì)出現(xiàn)一些由于鏈接異常斷開(kāi),導(dǎo)致后端服務(wù)拋出異常掛掉的情況
記住前端關(guān)閉頁(yè)面或者刷新頁(yè)面時(shí),先把連接關(guān)掉,每次進(jìn)入頁(yè)面時(shí)創(chuàng)建連接,然后后端將由于異常關(guān)閉導(dǎo)致的出錯(cuò) try/catch 一下,避免拋出異常,阻塞進(jìn)程
websocket 對(duì)于實(shí)現(xiàn)聊天室這樣的功能,真的很方便,其實(shí)還能擴(kuò)展到多人合作或者網(wǎng)絡(luò)游戲等功能