⚡浅谈项目中怎么优雅使用websocket和它的兄弟萌 您所在的位置:网站首页 websocket在项目中的应用 ⚡浅谈项目中怎么优雅使用websocket和它的兄弟萌

⚡浅谈项目中怎么优雅使用websocket和它的兄弟萌

2023-06-13 15:35| 来源: 网络整理| 查看: 265

前言

一直对实时通信功能蛮感兴趣的。之前在上家公司做聊天功能的时候,以为终于可以实操下websocket了,谁知道too young too simple,后端没搞过websocket也不想调研,最后采取的方案是轮询,然后这个实时通信功能就搁置了。

直到最近公司的项目迭代中有一个服务端实时推送消息给前端的功能,不由得又勾起了我对长连接的兴趣。

image.png

长连接的前世今生

技术来源于实际需求。

对于实时性方面的需求,经过技术不断的迭代,目前分为三大块:长轮询、SSE 和 websocket

一,长轮询

长轮询采用的还是HTTP协议,为了实现不断接收到服务端返回的数据,有两种方法

通过Ajax向服务器发送数据,并在获取到数据后再次发送请求。因为请求会被“挂起”,所以也被称为“挂起式请求”。

固定通过setInterval每隔2s发送一次请求,页面卸载/切除后台,清除定时器

优点:

学习成本低,实现简单。

应用场景较广。

对服务器压力较小。

缺点:

需要不断地发送请求和关闭连接,会增加网络流量和延迟。

会占用服务器IO资源。

在高并发情况下,会导致大量的挂起式请求,可能影响服务器性能。

二,SSE

SSE(Server-Send Events)是HTML5规范中新增的一种实现实时推送、单向通信的技术,是基于 HTTP协议中的持久连接。

优点:

学习成本低,实现简单。

性能更好,因为客户端和服务器之间的连接是持久的,减少了网络流量和延迟。

可以自动重连,保证了可靠性。

单向通信(服务端 -> 客户端)

缺点:

不支持跨域请求(需要使用CORS解决)。

需要浏览器和服务器的支持,可能存在兼容性问题。

1, 下面是一个使用SSE实现实时推送的示例代码: var source = new EventSource(“/api/sse”); // 监听连接是否建立 source.onopen = function(event){ console.log(event) } // 接收消息 source.onmessage = function (event) { console.log(“Got data:”, event.data); //处理返回的数据//… }; // 监听错误信息 source.onerror= function(event){ console.log(event.readyState) } // 断开SSE连接 source.close();

在服务器端,我们需要启用SSE并持续向客户端发送消息。下面是Node.js服务端代码:

app.get(‘/api/sse’, function(req, res) { res.writeHead(200, {‘Content-Type’: ‘text/event-stream’, ‘Cache-Control’: ‘no-cache’, ‘Connection’: ‘keep-alive’} ); //持续发送消息 setInterval(function() { var data = generateNewData(); res.write(‘data: ‘ + JSON.stringify(data) + ‘\n\n’);}, 2000); }); }) 2, SSE通信过程

image.png

三,websocket

WebSocket协议是基于TCP协议的,与HTTP协议有一定的关联。

关联的点:WebSocket协议使用HTTP协议进行握手,但一旦握手成功,通信就会转换为基于TCP的持久连接,不需要像HTTP那样频繁地建立和断开连接。

WebSocket能够实现实时双向通信,允许服务器主动向客户端发送数据。客户端也能主动向服务端发送数据。

优点:

实时性:WebSocket提供了持久的连接,允许服务器主动推送数据给客户端,实现了实时性的双向通信。

较低的延迟:由于WebSocket建立了持久连接,避免了多次握手和断开连接的开销,使得数据传输的延迟较低。

更小的数据传输量:WebSocket使用了较小的数据帧头部,减少了数据传输的开销。

更好的兼容性:WebSocket协议得到了广泛支持,现代浏览器都提供了对WebSocket的原生支持。

缺点:

较高的服务器资源消耗:WebSocket需要保持持久连接,对服务器的资源消耗较大,特别是在大规模并发连接的情况下。

需要较新的浏览器支持:较旧的浏览器版本可能不支持WebSocket,这会限制应用程序的兼容性。

长连接可能导致代理服务器限制:某些代理服务器可能会限制长时间的连接,这可能会影响WebSocket的可用性。

1, Websocket 和 EventSource都是windows自带的吗?

不是。是浏览器环境提供的。

2, 浏览器、浏览器环境、v8引擎、jsCore引擎、 windows对象、node中的global对象 分别有什么联系?

image.png

3, 在项目中使用的小技巧 3.1, 心跳

主要有三个优点

长时间空闲连接:在一些场景中,WebSocket连接可能会长时间处于空闲状态,没有数据传输。为了防止底层网络设备或中间代理等组件关闭空闲连接,使用心跳可以发送定期的消息来维持连接的活跃性。

断线重连:如果WebSocket连接断开,心跳可以作为检测连接断开的信号。当检测到连接断开时,客户端可以尝试重新连接服务器,以恢复通信。

节省资源和带宽:WebSocket心跳通常是一个简短的消息,与正常数据传输相比较小。这样可以减少不必要的数据传输和资源消耗,同时节省带宽和网络资源。

3.2, 建立和断开连接的时机

场景:在app端使用的时候,页面打开时建立长连接,切出后台时(回到手机桌面/切到其他app)断开长连接。

因为长连接一直开着,会浪费服务端性能。

方案(uniapp举例):

onShow生命周期可以判断你当前的页面是否在可视区域,这时候建立websocket连接

onHide生命周期可以判断你当前的页面是否脱离了可视区域,这时候断开websocket连接

建立连接

onShow(){ ...doSth }

断开连接:

onHide(){ ...doSth } 3.3, 连接断开时,有新数据咋办?

场景: 连接成功 => 连接断开 => 有几条新数据未推给前端 => 重新连接 => 可能会出现这段时间有新数据

所以重新连接后,应该在onConnected回调中查询历史数据

ws = new WS({ data: {}, onConnected: () => { // 场景:连接成功 => 连接断开 => 有几条新数据未推给前端 => 重连成功后调用最新数据 getDetailData() }, }) 3.4,回调

封装基本离不开回调

上文new WS({ onMessage:()=>{} })中,在实例化WS类时,传入了一个onMessage函数,目的是为了在监听到服务端推送给客户端的最新消息。

用回调的写法,可以在websocket.js中通过this.options.onMessage(res)把最新消息传递给你任意的vue页面,便于vue页面里的业务开发。

3.5,我想让关闭socket变为同步行为,可以吗?

可以,上文的ws.close()封装了一层,返回的是promise

可以通过async + await变为同步。比如,把tab切换的时候,关闭长连接再重新调一个其他接口。

async switchTab(){ await ws.close() getData() } 下面是白嫖党的福音,笔者的项目源码区域。不想慢慢看文章内容的,可以直接收藏吃灰。

image.png

4, 如何在项目中使用websocket

主要介绍在uniapp和原生H5中的具体使用方法

4.1, uniapp中

封装一个websocket.js

// 心跳间隔、重连websocket间隔,5秒 import store from '@/store' import constants from '@/configs/constants' const interval = 5000 // 重连最大次数 const maxReconnectMaxTime = 5 export default class WS { constructor(options) { // 配置 this.options = options // WS实例 this.socketTask = null // 正常关闭 this.normalCloseFlag = false // 重新连接次数 this.reconnectTime = 1 // 重新连接Timer this.reconnectTimer = null // 心跳Timer this.heartTimer = null // 发起连接 this.initWS() // 关闭WS this.close = () => new Promise((resolve) => { if (!this.socketTask) { resolve(false) return } // 正常关闭状态 this.normalCloseFlag = true // 关闭websocket this.socketTask.close() // 关闭心跳定时器 clearInterval(this.heartTimer) // 关闭重连定时器 clearTimeout(this.reconnectTimer) this.socketTask = null resolve(true) }) } initWS() { const url = store.getters.envUrl || '' this.socketTask = uni.connectSocket({ url, protocols: [store.getters.token], success() { } }) // 监听WS this.watchWS() } watchWS() { // 监听 WebSocket 连接打开事件 this.socketTask.onOpen(() => { console.log('websocket连接成功!') // 连接成功 this.options.onConnected() // 重置连接次数 this.reconnectTime = 1 // 发送心跳 this.onHeartBeat() // 监听消息 this.onMessage() // 关闭Toast uni.hideLoading() }) // 监听websocket 错误 this.socketTask.onError(() => { // 关闭并重连 this.socketTask.close() }) // 监听 WebSocket 连接关闭事件 this.socketTask.onClose((res) => { console.log('websocket连接关闭') // 连接错误,发起重连接 if (!this.normalCloseFlag) { this.onDisconnected(res) } }) } // 监听消息 onMessage() { // 监听websocket 收到消息 this.socketTask.onMessage((res) => { // 收到消息 if (res.data) { this.options.onMessage(res) } else { console.log('未监听到消息:原因:', JSON.stringify(res)) } }) } // 断开连接 onDisconnected(res) { console.log('websocket断开连接,原因:', JSON.stringify(res)) // 关闭心跳 clearInterval(this.heartTimer) // 全局Toast提示,防止用户继续发送 uni.showLoading({ title: '消息收取中…' }) // 尝试重新连接 this.onReconnect() } // 断线重连 onReconnect() { clearTimeout(this.reconnectTimer) if (this.reconnectTime < maxReconnectMaxTime) { this.reconnectTimer = setTimeout(() => { console.log(`第【${this.reconnectTime}】次重新连接中……`) this.initWS() this.reconnectTime++ }, interval) } else { uni.hideLoading() uni.showModal({ title: '温馨提示', content: '服务器开小差啦~', showCancel: false, confirmText: '我知道了', success: () => { console.log('关闭弹窗') } }) } } /** @心跳 * */ onHeartBeat() { this.heartTimer = setInterval(() => { this.socketTask.send({ data: 'heart:测试ing', success() { console.log('心跳发送成功!') } }) }, interval) } } 项目中的使用方法(就是vue2写法) import WS from './websocket' let ws = null export default { onShow() { this.connectSocket() // 正常获取你页面里的数据 this.getData() }, onHide() { this.closeSocket() this.closeSessionDec() }, methods:{ closeSocket() { if (!this.$store.getters.token) return if (!ws) return return ws.close() }, // 开始建立连接 connectSocket() { if (!this.$store.getters.token) return ws = new WS({ // 连接websocket所需参数 data: {}, // 首次连接成功/断线重新连接后触发(防止断线期间对方发送消息未接收到) onConnected: () => { this.load() }, // 监听接收到服务器消息 onMessage: (e) => { console.log('收到消息', e) // 监听服务端推送给你的新数据,然后更新到页面上 // ... doSth } }) }, } } 要点解析:

1, protocols

protocols是uniapp给我们提供的,它主要的目的是为了提供token给后端,去鉴权

因为笔者实际开发了uniapp转的APP端 和 APP端内嵌的H5端,这里的protocols也是为了两端统一,后端写一套逻辑就行啦。

this.socketTask = uni.connectSocket({ url, protocols: [store.getters.token], success() { } }) 4.2, 原生H5页面中

封装一个websocket.js

// 心跳间隔、重连websocket间隔,5秒 const interval = 5000 // 重连最大次数 const maxReconnectMaxTime = 5 function SocketTask(ws) { this.ws = ws this.close = () => { this.ws.close() } this.onopen = (fn) => { this.ws.onopen = fn } this.onerror = (fn) => { this.ws.onerror = fn } this.onclose = (fn) => { this.ws.onclose = fn } this.onmessage = (fn) => { this.ws.onmessage = fn } this.send = (data) => { this.ws.send(data) } } import store from '@/store' import jsb from '@/utils/webview-jsbridge' export default class WS { constructor(options) { // 配置 this.options = options // WS实例 this.socketTask = null // 正常关闭 this.normalCloseFlag = false // 重新连接次数 this.reconnectTime = 1 // 重新连接Timer this.reconnectTimer = null // 心跳Timer this.heartTimer = null // 发起连接 this.initWS() // 关闭WS this.close = () => new Promise((resolve) => { if (!this.socketTask) { resolve(false) return } // 正常关闭状态 this.normalCloseFlag = true // 关闭websocket this.socketTask.close() // 关闭心跳定时器 clearInterval(this.heartTimer) // 关闭重连定时器 clearTimeout(this.reconnectTimer) this.socketTask = null resolve(true) }) } async initWS() { let env = '' let url = '' await uni.getEnv(function (res) { console.log('当前环境:' + JSON.stringify(res)) env = JSON.stringify(res) }) if (env.includes('h5')) { url = 'wss://你的域名/api/webSocket/1' } else { let res = await jsb.getAppEnvUrl() url = res.envUrl } console.log('看这里看这里url:', url) // this.options.data 连接websocket所需参数 let ws = new WebSocket(url, [store.getters['user/token']]) this.socketTask = new SocketTask(ws) // 监听WS this.watchWS() } watchWS() { // 监听 WebSocket 连接打开事件 this.socketTask.onopen(() => { console.log('websocket连接成功!') // 连接成功 this.options.onConnected() // 重置连接次数 this.reconnectTime = 1 // 发送心跳 this.onHeartBeat() // 监听消息 this.onMessage() // 关闭Toast // uni.hideLoading() }) // 监听websocket 错误 this.socketTask.onerror(() => { // 关闭并重连 this.socketTask.close() }) // 监听 WebSocket 连接关闭事件 this.socketTask.onclose((res) => { console.log('websocket连接关闭') // 连接错误,发起重连接 if (!this.normalCloseFlag) { this.onDisconnected(res) } }) } // 监听消息 onMessage() { // 监听websocket 收到消息 this.socketTask.onmessage((res) => { // 收到消息 if (res.data) { try { let data = JSON.parse(res.data) this.options.onMessage({ ...data, data: data.heart ? data : JSON.parse(data.data), }) } catch (error) { console.log(error) } } else { console.log('未监听到消息:原因:', res) } }) } // 断开连接 onDisconnected(res) { console.log('websocket断开连接,原因:', JSON.stringify(res)) // 关闭心跳 clearInterval(this.heartTimer) // 全局Toast提示,防止用户继续发送 // uni.showLoading({ title: '消息收取中…' }) // 尝试重新连接 this.onReconnect() } // 断线重连 onReconnect() { clearTimeout(this.reconnectTimer) if (this.reconnectTime < maxReconnectMaxTime) { this.reconnectTimer = setTimeout(() => { console.log(`第【${this.reconnectTime}】次重新连接中……`) this.initWS() this.reconnectTime++ }, interval) } else { } } /** @心跳 * */ onHeartBeat() { this.heartTimer = setInterval(() => { this.socketTask.send(`heart:${this.options.data.userId}`) }, interval) } } 在原生H5里的使用

在H5里的使用和上面uniapp项目中的使用方法里中的方式大同小异,就不过多赘述啦

要点解析:

1,SocketTask函数

SocketTask函数是我为了和uniapp端的js写法保持统一,手戳的一个中转站~~~

小伙伴们直接用就行

2,websocket.js里 在也有个new WebSocket?

此处的WebSocket是浏览器环境提供的,上文有提到。而你的vue页里的new websocket其实是引用了一个类,这个类是websocket.js本身。

H5端的这段代码

new WebSocket(url, [store.getters['user/token']])

等于 uniapp的这段代码

uni.connectSocket({ url, protocols: [store.getters.token], success() { } })

3,jsb是啥玩意?

我上文有介绍,这里业务场景是uniapp端有一个长连接,通过webview内嵌的h5页面也有长连接。

jsb是jsbridge的意思,实现APP与H5之间通信的技术,允许原生代码和H5页面之间相互调用和传递数据

说白了 这里就是通过uniapp自带的@message和uni.postMessage,然后封装了一波。小伙伴要是没这个业务,就忽略它。

本文的重点是websocket长连接

// APP端 handlePostMessage(data){ // data是H5传递过来的数据 } // H5端 uni.postMessage({ data: { action, payload, }, })

4,重点说下H5中怎么自己实现uniapp里的onShow和onHide生命周期

使用visibilitychange, 简单来说onShow 和 onHide本质上就是监听页面是否在可视区域里。

const dealGetData = () => { if (document.visibilityState === 'visible') { console.log('浏览器的当前页签onShow时,do something') init() } else { console.log('浏览器的当前页签onHide时,do something') // 切出后台 closeSocket() } } onMounted(() => { init() document.addEventListener('visibilitychange',dealGetData) }) onBeforeUnmount(() => { // 页面卸载-针对路由切换 closeSocket() document.removeEventListener('visibilitychange', dealGetData) })

document.addEventListener('visibilitychange',dealGetData) 监听可视区域变化的回调函数dealGetData一定要放在document.removeEventListener('visibilitychange', dealGetData)进行事件移除,不然这个监听的动作会一直存在,即使通过路由到了下个页面,也会存在。

就会出现 A -> B页面,B页面切出后台再回到B页面的时候,network会同时触发AB两个页面的接口请求。

不要问我怎么知道的,都是坑啊~~~

完结

这篇文章本该发出来啦,只是一直没时间搞,都用来做其他的事了。这里把开发过程中的笔记和遇到的一些知识点整理了一下,希望对小伙伴有帮助。

websocket开发实时通信,项目提测后给我的感觉就是

卧槽,听着好牛逼的技术 -> 嗯?这么简单 -> 还是有点细节的 -> 学到的,常规操作吧

欢迎转载,但请注明来源。

最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。

image.png



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有