如何让服务器主动给客户端推送消息?
我们可以非常轻松的捕获浏览器上发生的事件(比如用户点击了盒子),这个事件可以轻松产生与服务器的数据交互(比如Ajax)。但是,反过来却是不可能的:服务器端发生了一个事件,服务器无法将这个事件的信息实时主动通知它的客户端。只有在客户端查询服务器的当前状态的时候,所发生事件的信息才会从服务器传递到客户端。
让服务器主动给客户端推送消息常见的做法有下面几种方式。
-
长轮询:客户端每隔很短的时间,都会对服务器发出请求,查看是否有新的消息,只要轮询速度足够快,例如1 秒,就能给人造成交互是实时进行的印象。这种做法是无奈之举,实际上对服务器、客户端双方都造成了大量的性能浪费。
-
长连接:浏览器和服务器只需要要做一个握手的动作,在建立连接之后,双方可以在任意时刻,相互推送信息。同时,服务器与客户端之间交换的头信息很小。
WebScoket 是一种让客户端和服务器之间能进行双向实时通信的技术。它是HTML 最新标准HTML5 的一个协议规范,本质上是个基于TCP 的协议,它通过HTTP/HTTPS 协议发送一条特殊的请求进行握手后创建了一个TCP 连接,此后浏览器/客户端和服务器之间便可以通过此连接来进行双向实时通信。
后端代码的实现
WebSocket 的使用
- 安装WebSocket 包
npm i ws -S
- 创建WebSocket 实例对象
const WebSocket = require("ws")
// 创建出WebSocket实例对象
const wss = new WebSocket.Server({
port: 9998
})
- 监听事件
wss.on("connection", client => {
console.log("有客户端连接...")
// 成功接收的信息的回调
client.on("message", msg => {
console.log("客户端发送数据过来了")
// 发送数据给客户端
client.send('hello socket')
})
})
- 前端的测试代码如下:
<body>
<button id="connect">连接</button>
<button id="send" disabled="true">发送数据</button> <br>
从服务器接收的数据如下:<br>
<span id="content"></span>
<script>
var connect = document.querySelector('#connect')
var send = document.querySelector('#send')
var content = document.querySelector('#content')
var ws = null
connect.onclick = function() {
// WebSocket对象为浏览器自带
ws = new WebSocket('ws://localhost:9998')
ws.onopen = () => {
console.log('连接服务器成功')
send.disabled = false
}
ws.onmessage = msg => {
console.log('从服务器接收到了数据')
content.innerHTML = msg.data
}
ws.onclose = e => {
console.log('服务器关闭了连接')
send.disabled = true
}
}
send.onclick = function(){
// 通过WebSocket发送数据
ws.send('hello websocket from frontend')
}
</script>
</body>
- 后端的测试代码如下:
const WebSocket = require("ws")
// 创建出WebSocket实例对象
const wss = new WebSocket.Server({
port: 9998
})
module.exports.listen = function() {
// 客户端成功连接
wss.on("connection", ws => {
console.log("有客户端连接...")
// 客户端接收到消息后的message事件
ws.on("message", msg => {
wss.clients.forEach(client => {
console.log("*****************************************")
client.send(msg)
})
})
})
}
前端代码的改造
通过单例方式创建WebSocket 实例对象
定义单例:
export default class SocketService {
/**
* 单例
*/
static instance = null
static get Instance() {
if (!this.instance) {
this.instance = new SocketService()
}
return this.instance
}
监听WebSocket 事件
定义connect 函数,将创建的WebSocket 赋值给实例属性:
export default class SocketService {
......
// 实例属性
ws = null
// 初始化连接websocket
connect () {
if (!window.WebSocket) {
return console.log('您的浏览器不支持 WebSocket!')
}
this.ws = new WebSocket('ws://localhost:9998')
}
}
在 connect 函数监听事件:
connect () {
if (!window.WebSocket) {
return console.log('您的浏览器不支持 WebSocket!')
}
this.ws = new WebSocket('ws://localhost:9998')
// 监听连接成功
this.ws.onopen = () => {
console.log('WebSocket 连接成功')
}
// 1.服务器连接不成功 2.服务器关闭了连接
this.ws.onclose = e => {
console.log('服务器关闭了连接')
}
// 监听接收消息
this.ws.onmessage = msg => {
console.log('WebSocket 接收到数据')
}
}
连接服务端
import SocketService from '@/utils/socket_service'
SocketService.Instance.connect()
发送数据给服务端
在socket_service.js 中定义发送数据的方法
export default class SocketService {
......
send (data) {
console.log('发送数据给服务器:')
this.ws.send(JSON.stringify(data))
}
}
运行代码, 发现数据发不出去
因为在刷新界面之后, 客户端和服务端的连接并不会立马连接成功, 在处于连接状态下就调用 send 是发送不成功的, 因此需要修改service_socket.js 中的send 方法进行容错处理
// 是否已经连接成功
connected = false
sendRetryCount = 0
send (data) {
console.log('发送数据给服务器:')
if (this.connected) {
this.sendRetryCount = 0
this.ws.send(JSON.stringify(data))
} else {
setTimeout(() => {
this.sendRetryCount++
this.send(data)
}, 200 * this.sendRetryCount) // 发送数据尝试的次数越大, 则下一次连接的
延迟也就越长
}
在onopen 时设置connected 的值
connect () {
......
this.ws.onopen = () => {
console.log('WebSocket 连接成功')
this.connected = true
}
}
断开重连机制
如果初始化连接服务端不成功, 或者连接成功了, 后来服务器关闭了, 这两种情况都会触onclose 事件,我们需要在这个事件中,进行重连:
connectRetryCount = 0 // 重连次数, 重连次数越大, 下一次再发起重连的延时也就越长
connect () {
this.ws.onopen = () => {
......
this.connectRetryCount = 0 // 连接成功之后, 重置重连次数
}
......
// 1.服务器连接不成功 2.服务器关闭了连接
this.ws.onclose = e => {
console.log('服务器关闭了连接')
setTimeout(() => {
this.connectRetryCount++
this.connect()
}, 200 * this.connectRetryCount)
}
}
总结
WebSocket 可以保持着浏览器和客户端之间的长连接, 通过WebSocket 可以实现数据由后端推送到前端,保证了数据传输的实时性。通过以上的代码可以有效地实现前端与后端的长连接,确保数据的实时性。