websocket 是一种网络通信协议,一般用来进行实时通信会使用到
为什么要用 websocket
websocket 协议和 http 协议类似,http 协议有一个缺陷,只能由客户方端发起请求,服务端根据请求 url 和传过去的参数返回对应结果
websocket 是双向通信的,只要 websocket 连接建立起来,可以由客户端给服务端发送数据,也可以由服务端主动给客户端发送数据
websocket 适用场景:聊天室
简介
websocket 相关简介,可以看阮老师的文章
用法
服务端nodejs-websocket
nodejs
可以通过nodejs-websocket
来实现创建一个 websocket 的服务
// websocket.js
const ws = require('nodejs-websocket')
const createServer = () => {
let server = ws.createServer(connection => {
connection.on('text', function(result) {
console.log('发送消息', result)
})
connection.on('connect', function(code) {
console.log('开启连接', code)
})
connection.on('close', function(code) {
console.log('关闭连接', code)
})
connection.on('error', function(code) {
console.log('异常关闭', code)
})
})
return server
}
module.exports = createServer()
nodejs-websocket
用法
文档地址:https://www.npmjs.com/package/nodejs-websocket
node 创建的 websocket 服务,主要包含三个概念
WS: 引入nodejs-websocket
后的主要对象
- ws.createServer([options], [callback]):创建一个 server 对象
- ws.connect(URL, [options], [callback]):创建一个 connect 对象,一般由客户端链接服务端 websocket 服务时创建
- ws.setBinaryFragmentation(bytes):设置传输二进制文件的最小尺寸,默认 512kb
- setMaxBufferLength:设置传输二进制文件的最大尺寸,默认 2M
Server:通过 ws.createServer 创建
Function
- server.listen(port, [host], [callback]): 传入端口和主机地址后,开启一个 websocket 服务
- server.close([callback]): 关闭 websocket 服务
- server.connections: 返回包含所有 connection 的数组,可以用来广播所有消息
// 服务端广播
function broadcast(server, msg) {
server.connections.forEach(function(conn) {
conn.sendText(msg)
})
}
Event
可以通过server.on('event', (res) => {console.log(res)})
调用
- Event: 'listening()':调用
server.listen
会触发当前事件 - Event: 'close()': 当服务关闭时触发该事件,如果有任何一个 connection 保持链接,都不会触发该事件
- Event: 'error(errObj)':发生错误时触发,此事件后会直接调用 close 事件
- Event: 'connection(conn)':建立新链接(完成握手后)触发,conn 是连接的实例对象
Connection:每一个客户端创建连接时的实例
Function
- connection.sendText(str, [callback]):发送字符串给另一侧,可以由服务端发送字符串数据给客户端
- connection.beginBinary():要求连接开始传输二进制,返回一个
WritableStream
- connection.sendBinary(data, [callback]): 发送一个二进制块,类似
connection.beginBinary().end(data)
- connection.send(data, [callback]): 发送一个字符串或者二进制内容到客户端,如果发送的是文本,类似于
sendText()
,如果发送的是二进制,类似于sendBinary()
,
callback
将监听发送完成的回调 - connection.close([code, [reason]]):开始关闭握手(发送一个关闭指令)
- connection.server:如果服务是 nodejs 启动,这里会保留 server 的引用
- connection.readyState:一个常量,表示连接的当前状态
connection.CONNECTING:值为 0,表示正在连接
connection.OPEN:值为 1,表示连接成功,可以通信了
connection.CLOSING:值为 2,表示连接正在关闭。
connection.CLOSED:值为 3,表示连接已经关闭,或者打开连接失败。
- connection.outStream: 存储
connection.beginBinary()
返回的OutStream
对象,没有则返回 null - connection.path:表示建立连接的路径
- connection.headers:只读请求头的 name 的 value 对应的 object 对象
- connection.protocols:客户端请求的协议数组,没有则返回空数组
- connection.protocol:同意连接的协议,如果有这个协议,它会包含在
connection.protocols
数组里面
Event
- Event: 'close(code, reason)': 连接关闭时触发
- Event: 'error(err)':发生错误时触发,如果握手无效,也会发出响应
- Event: 'text(str)':收到文本时触发,str 时收到的文本字符串
- Event: 'binary(inStream)':收到二进制内容时触发,
inStream
时一个ReadableStream
var server = ws
.createServer(function(conn) {
console.log('New connection')
conn.on('binary', function(inStream) {
// 创建空的buffer对象,收集二进制数据
var data = new Buffer(0)
// 读取二进制数据的内容并且添加到buffer中
inStream.on('readable', function() {
var newData = inStream.read()
if (newData)
data = Buffer.concat([data, newData], data.length + newData.length)
})
inStream.on('end', function() {
// 读取完成二进制数据后,处理二进制数据
process_my_data(data)
})
})
conn.on('close', function(code, reason) {
console.log('Connection closed')
})
})
.listen(8001)
- Event: 'connect()':连接完全建立后发出
具体代码实现
const ws = require('nodejs-websocket')
// 可以通过不同的code可以表示要后端实现的不同逻辑
const {
RECEIEVE_MESSAGE,
SAVE_USER_INFO,
CLOSE_CONNECTION
} = require('../constants/config')
// 当前聊天室的用户
let chatUsers = []
// 广播通知
const broadcast = (server, info) => {
console.log('broadcast', info)
server.connections.forEach(function(conn) {
conn.sendText(JSON.stringify(info))
})
}
// 服务端获取到某个用户的信息通知到所有用户
const broadcastInfo = (server, info) => {
let count = server.connections.length
let result = {
code: RECEIEVE_MESSAGE,
count: count,
...info
}
broadcast(server, result)
}
// 返回当前剩余的在线用户
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)
}
// 触发关闭连接,在离开页面或者关闭页面时,需要主动触发关闭连接
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)
}
// 创建websocket服务
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)
// 某些情况如果客户端多次触发连接关闭,会导致connection.close()出现异常,这里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('关闭连接', code)
})
connection.on('error', function(code) {
// 某些情况如果客户端多次触发连接关闭,会导致connection.close()出现异常,这里try/catch一下
try {
connection.close()
} catch (error) {
console.log('close异常', error)
}
console.log('异常关闭', code)
})
})
// 所有连接释放时,清空聊天室用户
server.on('close', () => {
chatUsers = []
})
return server
}
const server = createServer()
module.exports = server
部分前端代码
前端主要是创建
WebSocket
连接后,在onopen
事件触发时,初始化用户的一些信息,比如每个用户包含唯一的chatId
之类的,以及保持用户昵称,用户头像啥的
再就是监听onmessage
事件,通过后端返回的 message 信息执行对应的操作,建议前后端约定一些 code 来表示某一种类似的 message 信息
然后就是监听页面的一些触发事件,将信息通过send
方法发送给服务端
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('数据已接收...', code, result)
}
websocket.onclose = this.onWebsocketClose
websocket.onerror = this.onWebsocketError
// 发送message
sendMessage(info) {
if (this.websocket && typeof this.websocket.send === 'function') {
this.websocket.send(JSON.stringify(info))
}
}
问题
如果浏览器进入其它页面或者关闭浏览器,链接会异常关闭,经常会导致后端出现异常报错
// 前端代码监听页面关闭或者刷新
window.onunload = () => {
this.closeConnect()
}
// vue里跳转到其它页面
beforeRouteLeave(to, from, next) {
this.closeConnect()
next()
}
总结
这次使用 websocket 实现一个基本的聊天室功能,个人感觉还比较简单,只是中间会出现一些由于链接异常断开,导致后端服务抛出异常挂掉的情况
记住前端关闭页面或者刷新页面时,先把连接关掉,每次进入页面时创建连接,然后后端将由于异常关闭导致的出错 try/catch 一下,避免抛出异常,阻塞进程
websocket 对于实现聊天室这样的功能,真的很方便,其实还能扩展到多人合作或者网络游戏等功能
网友评论