websocket 是一種網(wǎng)絡(luò)通信協(xié)議,一般用來進(jìn)行實時通信會使用到
為什么要用 websocket
websocket 協(xié)議和 http 協(xié)議類似乃沙,http 協(xié)議有一個缺陷赊时,只能由客戶方端發(fā)起請求,服務(wù)端根據(jù)請求 url 和傳過去的參數(shù)返回對應(yīng)結(jié)果
websocket 是雙向通信的,只要 websocket 連接建立起來释移,可以由客戶端給服務(wù)端發(fā)送數(shù)據(jù),也可以由服務(wù)端主動給客戶端發(fā)送數(shù)據(jù)
websocket 適用場景:聊天室
簡介
websocket 相關(guān)簡介寥殖,可以看阮老師的文章
用法
服務(wù)端nodejs-websocket
nodejs
可以通過nodejs-websocket
來實現(xiàn)創(chuàng)建一個 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('開啟連接', 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ù)玩讳,主要包含三個概念
WS: 引入nodejs-websocket
后的主要對象
- ws.createServer([options], [callback]):創(chuàng)建一個 server 對象
- ws.connect(URL, [options], [callback]):創(chuàng)建一個 connect 對象,一般由客戶端鏈接服務(wù)端 websocket 服務(wù)時創(chuàng)建
- ws.setBinaryFragmentation(bytes):設(shè)置傳輸二進(jìn)制文件的最小尺寸嚼贡,默認(rèn) 512kb
- setMaxBufferLength:設(shè)置傳輸二進(jìn)制文件的最大尺寸熏纯,默認(rèn) 2M
Server:通過 ws.createServer 創(chuàng)建
Function
- server.listen(port, [host], [callback]): 傳入端口和主機(jī)地址后,開啟一個 websocket 服務(wù)
- server.close([callback]): 關(guān)閉 websocket 服務(wù)
- server.connections: 返回包含所有 connection 的數(shù)組粤策,可以用來廣播所有消息
// 服務(wù)端廣播
function broadcast(server, msg) {
server.connections.forEach(function(conn) {
conn.sendText(msg)
})
}
Event
可以通過server.on('event', (res) => {console.log(res)})
調(diào)用
- Event: 'listening()':調(diào)用
server.listen
會觸發(fā)當(dāng)前事件 - Event: 'close()': 當(dāng)服務(wù)關(guān)閉時觸發(fā)該事件樟澜,如果有任何一個 connection 保持鏈接,都不會觸發(fā)該事件
- Event: 'error(errObj)':發(fā)生錯誤時觸發(fā)叮盘,此事件后會直接調(diào)用 close 事件
- Event: 'connection(conn)':建立新鏈接(完成握手后)觸發(fā)秩贰,conn 是連接的實例對象
Connection:每一個客戶端創(chuàng)建連接時的實例
Function
- connection.sendText(str, [callback]):發(fā)送字符串給另一側(cè),可以由服務(wù)端發(fā)送字符串?dāng)?shù)據(jù)給客戶端
- connection.beginBinary():要求連接開始傳輸二進(jìn)制柔吼,返回一個
WritableStream
- connection.sendBinary(data, [callback]): 發(fā)送一個二進(jìn)制塊毒费,類似
connection.beginBinary().end(data)
- connection.send(data, [callback]): 發(fā)送一個字符串或者二進(jìn)制內(nèi)容到客戶端,如果發(fā)送的是文本愈魏,類似于
sendText()
觅玻,如果發(fā)送的是二進(jìn)制,類似于sendBinary()
,
callback
將監(jiān)聽發(fā)送完成的回調(diào) - connection.close([code, [reason]]):開始關(guān)閉握手(發(fā)送一個關(guān)閉指令)
- connection.server:如果服務(wù)是 nodejs 啟動培漏,這里會保留 server 的引用
- connection.readyState:一個常量溪厘,表示連接的當(dāng)前狀態(tài)
connection.CONNECTING:值為 0,表示正在連接
connection.OPEN:值為 1牌柄,表示連接成功畸悬,可以通信了
connection.CLOSING:值為 2,表示連接正在關(guān)閉珊佣。
connection.CLOSED:值為 3傻昙,表示連接已經(jīng)關(guān)閉,或者打開連接失敗彩扔。
- connection.outStream: 存儲
connection.beginBinary()
返回的OutStream
對象妆档,沒有則返回 null - connection.path:表示建立連接的路徑
- connection.headers:只讀請求頭的 name 的 value 對應(yīng)的 object 對象
- connection.protocols:客戶端請求的協(xié)議數(shù)組,沒有則返回空數(shù)組
- connection.protocol:同意連接的協(xié)議虫碉,如果有這個協(xié)議贾惦,它會包含在
connection.protocols
數(shù)組里面
Event
- Event: 'close(code, reason)': 連接關(guān)閉時觸發(fā)
- Event: 'error(err)':發(fā)生錯誤時觸發(fā),如果握手無效,也會發(fā)出響應(yīng)
- Event: 'text(str)':收到文本時觸發(fā)须板,str 時收到的文本字符串
- Event: 'binary(inStream)':收到二進(jìn)制內(nèi)容時觸發(fā)碰镜,
inStream
時一個ReadableStream
var server = ws
.createServer(function(conn) {
console.log('New connection')
conn.on('binary', function(inStream) {
// 創(chuàng)建空的buffer對象,收集二進(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ā)出
具體代碼實現(xiàn)
const ws = require('nodejs-websocket')
// 可以通過不同的code可以表示要后端實現(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ù)端獲取到某個用戶的信息通知到所有用戶
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)閉連接绪颖,在離開頁面或者關(guān)閉頁面時,需要主動觸發(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)閉甜奄,會導(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('開啟連接', code)
})
connection.on('close', function(code) {
console.log('關(guān)閉連接', code)
})
connection.on('error', function(code) {
// 某些情況如果客戶端多次觸發(fā)連接關(guān)閉,會導(dǎo)致connection.close()出現(xiàn)異常课兄,這里try/catch一下
try {
connection.close()
} catch (error) {
console.log('close異常', error)
}
console.log('異常關(guān)閉', code)
})
})
// 所有連接釋放時牍氛,清空聊天室用戶
server.on('close', () => {
chatUsers = []
})
return server
}
const server = createServer()
module.exports = server
部分前端代碼
前端主要是創(chuàng)建
WebSocket
連接后,在onopen
事件觸發(fā)時烟阐,初始化用戶的一些信息搬俊,比如每個用戶包含唯一的chatId
之類的,以及保持用戶昵稱,用戶頭像啥的
再就是監(jiān)聽onmessage
事件蜒茄,通過后端返回的 message 信息執(zhí)行對應(yīng)的操作唉擂,建議前后端約定一些 code 來表示某一種類似的 message 信息
然后就是監(jiān)聽頁面的一些觸發(fā)事件,將信息通過send
方法發(fā)送給服務(wù)端
let websocket = new WebSocket(wsConfig.WS_ROOT_PATH)
websocket.onopen = () => {
console.log('websocket連接開啟...')
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))
}
}
問題
如果瀏覽器進(jìn)入其它頁面或者關(guān)閉瀏覽器檀葛,鏈接會異常關(guān)閉楔敌,經(jīng)常會導(dǎo)致后端出現(xiàn)異常報錯
// 前端代碼監(jiān)聽頁面關(guān)閉或者刷新
window.onunload = () => {
this.closeConnect()
}
// vue里跳轉(zhuǎn)到其它頁面
beforeRouteLeave(to, from, next) {
this.closeConnect()
next()
}
總結(jié)
這次使用 websocket 實現(xiàn)一個基本的聊天室功能,個人感覺還比較簡單驻谆,只是中間會出現(xiàn)一些由于鏈接異常斷開,導(dǎo)致后端服務(wù)拋出異常掛掉的情況
記住前端關(guān)閉頁面或者刷新頁面時庆聘,先把連接關(guān)掉胜臊,每次進(jìn)入頁面時創(chuàng)建連接,然后后端將由于異常關(guān)閉導(dǎo)致的出錯 try/catch 一下伙判,避免拋出異常象对,阻塞進(jìn)程
websocket 對于實現(xiàn)聊天室這樣的功能,真的很方便宴抚,其實還能擴(kuò)展到多人合作或者網(wǎng)絡(luò)游戲等功能