什么是WebSocket?
WebSocket是一种网络传输协议,可在单个TCP链接上进行全双工通信,位于OSI模型的应用层。
特点:
- TCP链接,与HTTP协议兼容
- 双向通信,主动推送(服务端向客户端)
- 无同源限制,协议标识符是ws(加密wss)
应用场景:
- 聊天,消息,点赞(后台消息的及时通知)
- 直播评论(弹幕)
- 游戏,协同编辑,基于位置的应用
传统做法:ajax轮询
缺点:因为http的无状态特征,轮询的间隙的变化无法感知,频繁的请求给服务器造成很大的压力,造成效率低下,浪费资源
进行WebSocket开发常用的两个库
- ws:基于原生进行实现,效率高
- socket.io:很好的实现了向下兼容,但因为代码臃肿,运行效率比较低
初识websocket应用
项目地址:https://github.com/chenhui-syz/ws-chat-demo
项目根目录名称:ws-chat-demo
项目文件结构:
新建两个文件夹:
- client
- server
分别在这个两个文件夹中进行初始化并安装ws
- npm init -y
- npm install ws@7.2.1 -S
在index.html文件模板的script写入下面代码
var ws = new WebSocket('ws://127.0.0.1:3000')
这就相当于在客户端通过ws协议,向本地服务器的3000端口发送了一个WebSocket连接请求。
此时在client文件夹的index.js入口文件中初始化一个WebSocket的Server服务,并监听连接请求
const WebSocket = require('ws')
const wss = new WebSocket.Server({
port: 3000
})
wss.on('connection', function connection(ws) {
console.log('one client is connected')
})
node index.js运行这个文件,并在浏览器中打开index.html,此时终端中会打印“one client is connected”,这代表客户端发起的ws请求已成功连接了。
WebSocket通信的最大特点就是既可以在客户端主动发起请求,也可以在服务端主动发起请求。
上面的操作websocket长连接已经建立起来了,但是还没有开始去发送消息
在index.js中增加监听接收到消息的回调事件以及主动发送消息的代码:
wss.on('connection', function connection(ws) {
console.log('one client is connected')
// 接收客户端发来的消息
ws.on('message', function (msg) {
console.log('接收客户端发来的消息', msg)
})
// 主动发送消息给客户端
ws.send('一条来自服务端的消息')
})
在客户端监听“建立连接”以及接收消息:
var ws = new WebSocket('ws://127.0.0.1:3000')
// 监听“建立连接”
ws.onopen = function () {
ws.send('客户端建立了连接')
}
// 监听“接受到新消息”
ws.onmessage = function (event) {
console.log('客户端接收到的消息', event.data)
}
在client文件夹新建一个testClient.js,可以实现在客户端通过ws去发起建立连接请求,并主动向服务端发送消息
const WebSocket = require('ws')
const ws = new WebSocket('ws://127.0.0.1:3000')
ws.on('open', function () {
console.log('client is connected to Server')
ws.send('client say hello to server')
ws.on('message', function (msg) {
console.log('接收服务端发来的消息', msg)
})
})
分别在终端中通过node运行index.js和testClient.js文件,
index.js打印如下结果:
one client is connected
接收客户端发来的消息 client say hello to server
testClient.js打印如下结果:
client is connected to Server
接收服务端发来的消息 一条来自服务端的消息
同样的,将testClient.js的代码建立在server文件夹中,可以实现在node端建立client服务
我们最终要实现的还是在浏览器端实现client服务,在node端实现server服务,客户端和服务端的常用事件基本一样,区别只是在客户端是下面这个监听方式:
ws.onopen = function () {
ws.send('客户端建立了连接')
}
服务端:
ws.on('open', function () {
ws.send('服务端建立了连接')
})
认识ws.readyState
Constant | Value |
---|---|
WebSocket.CONNECTING | 0 |
WebSocket.OPEN | 1 |
WebSocket.CLOSING | 2 |
WebSocket.CLOSED | 3 |
在node端如果通过ctrl+c停止服务运行,websocket自然也就强行停止了,此时ws.readyState变成3
客户端可以通过触发事件去手动关闭ws连接
<button type="button" id="app">按钮</button>
<script>
document.getElementById('app').addEventListener('click', function () {
ws.close()
})
</script>
如果客户端发起的连接请求没有找到对应的服务端进行相应,则会触发error事件回调
ws.onerror = function () {
console.log('error', ws.readyState)
}
此时的readyState也是3
多人聊天室应用
定制脚本:
npm install -D nodemon
package.json的script中添加"dev":"nodemon index.js"
下次npm run dev启动server服务就可以了。
服务端通过监听“message”然后send消息,只是针对此次事件进行了回应,当前发来消息的客户端可以接收到,而其他客户端接收不到,这正好和我们的需求实现是相反的。
ws.on('message', function (msg) {
console.log('接收客户端发来的消息', msg)
// 服务端收到消息之后把这个消息再发送回去
ws.send('form server'+msg)
})
改写成下面这样:
ws.on('message', function (msg) {
console.log('接收客户端发来的消息', msg)
// 服务端收到消息之后把这个消息再发送回去
// ws.send('form server'+msg)
// 广播消息
wss.clients.forEach((client) => {
// 判断非自己的客户端并且是连接状态,才去广播消息
if (ws !== client && client.readyState === WebSocket.OPEN) {
client.send(msg)
}
})
})
wx.send只能发送字符串或者是二进制底层数据,所以为了添加更多的标识,需要将对象转换为字符串进行发送
this.ws.send(JSON.stringify({
event: 'enter',
message: this.name
}))
后台还是原来那样把收到的字符串原样返回来,然后前端进行处理
onMessage: function (event) {
var obj = JSON.parse(event.data)
if (obj.event === 'enter') {
// 当有一个新用户进入聊天室
this.lists.push('欢迎' + obj.message + '加入聊天室!')
} else {
this.lists.push(obj.message)
}
},
统计进入聊天室的人数wss.clients.size
,但是如果想统计最近时间段有发送消息的客户端数量,可以定义一个全局变量num,然后再ws.on('message', function (msg) {...}进行++,过滤掉一些挂机状态的连接
但是上面的做法又存在如果已发送消息的客户端断开连接,num没有--,所以需要增加对close的监听,同时增加xxxx离开聊天室的广播消息,需要注意的是,浏览器f5也会相当于触发了断开和重新连接的操作
ws.on('close', function () {
// ws.name有值,则表示当前连接是有效的
if (ws.name) {
num--
}
let msgObj = {}
wss.clients.forEach((client) => {
// 连接状态,才去广播消息
if (client.readyState === WebSocket.OPEN) {
// 给返回的消息添加name
msgObj.name = ws.name
// 添加当前在线人数
msgObj.num = num
msgObj.event = 'out'
client.send(JSON.stringify(msgObj))
}
})
})
多聊天室功能实现,首先需要前端传递用户想要进入的聊天室
this.ws.send(JSON.stringify({
event: 'enter',
message: this.name,
roomid: this.roomid
}))
在后台代码里,ws特指当前客户端和服务端进行的当此连接
后台根据拿到的roomid进行相应的广播区别操作
WebSocket鉴权
在服务端发起的链接则可以去直接自定义headers
const WebSocket = require('ws')
const ws = new WebSocket('ws://127.0.0.1:3000',{
headers: {
token: 'demo123'
}
})
浏览器发送ws链接不支持自定义headers
浏览器端也没法去引用ws库,因为浏览器端发起的ws请求会被自动浏览器降级为http自带的WebSocket
所以在客户端jwt鉴权直接添加到headers的方法没法使用
协议本身在握手阶段不提供鉴权方案,所以只能在建立连接之后再专门发送一次消息进行token的验证
解决办法:浏览器在连接成功之后主动发送一次消息,此时消息的作用就是发送本地的token给服务端进行本次连接的鉴权,如果鉴权通过,则本次ws连接后续的所有操作都放行,否则就给弹到login登录页面,让重新获取合法的token
心跳检测
原理:服务端去定时的向客户端发送消息,客户端收到消息后进行回应
心跳检测的请求一定要放在鉴权消息的后面,也就是说建立连接之后第一时间是发送鉴权消息再进行其他操作,否则会导致后面的消息发送鉴权失败。
心跳检测建立之后,客户端的应用:
心跳间隔时间+网络时延=判定时间,每次收到ping,都开启一个定时器,如果判定时间过后,还没有收到下次的ping,就主动去断开当此连接,并开启下一次连接
checkServer: function () {
var _this = this
clearTimeout(this.handle)
_this.handle = setTimeout(function () {
_this.onClose()
_this.init()
}, 2000)
}
自动断线重连的包推荐:
ES5:
reconnecting-websocket
https://github.com/joewalnes/reconnecting-websocket
将var ws = new WebSocket('ws://....');
改为var ws = new ReconnectingWebSocket('ws://....');
不需要额外的代码,同时支持很多options的配置
ES6包:
https://www.npmjs.com/package/reconnecting-websocket