前言
一直对实时通信功能蛮感兴趣的。之前在上家公司做聊天功能的时候,以为终于可以实操下websocket了,谁知道too young too simple
,后端没搞过websocket也不想调研,最后采取的方案是轮询
,然后这个实时通信功能就搁置了~~~
直到最近公司的项目迭代中有一个服务端实时推送消息给前端的功能,不由得又勾起了我对长连接的兴趣。
长连接的前世今生
技术来源于实际需求。
对于实时性方面的需求,经过技术不断的迭代,目前分为三大块:长轮询、SSE 和 websocket
一,长轮询
长轮询采用的还是HTTP协议
,为了实现不断接收到服务端返回的数据,有两种方法
-
通过
Ajax
向服务器发送数据,并在获取到数据后再次发送请求。因为请求会被“挂起”,所以也被称为“挂起式请求”。 -
固定通过
setInterval
每隔2s发送一次请求,页面卸载/切除后台,清除定时器
优点:
- 学习成本低,实现简单。
缺点:
- 需要不断地发送请求和关闭连接,在高并发情况下可能影响服务器性能。
二,SSE
一种实现实时推送、单向通信
的技术,是基于 HTTP协议
中的持久连接。
优点:
-
学习成本低,实现简单。
-
单向通信(服务端 -> 客户端)
缺点:
- 不支持跨域请求(需要使用CORS解决)。
1, SSE示例
客户端
var source = new EventSource('/接口');
// 监听连接是否建立
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();
服务器端(Node.js)
app.get('/接口', 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通信过程
三,websocket
能够实现实时双向通信
,基于TCP协议
,为了解决HTTP
不能服务端主动推消息
- WebSocket的连接过程中,客户端会发起HTTP请求,3次握手后建立TCP连接,通过这个TCP连接进行全双工通信
优点:
-
实时性
-
双向通信
缺点:
- 持久连接对服务器的资源消耗较大
1, Websocket 和 EventSource都是windows自带的吗?
不是。是浏览器环境提供的。
2, 浏览器、浏览器环境、v8引擎、jsCore引擎、 windows对象、node中的global对象 分别有什么联系?
3, 在项目中使用的小技巧
3.1, 心跳
主要有三个优点
-
维持连接的活跃性:定期发送心跳可以防止长时间不通信连接被关闭
-
检测连接的状态:如果特殊场景下,服务端想在客户端同意的情况下断开长连接,可以通过心跳携带连接信息来判断
-
节省资源和带宽:服务端长时间检测不到心跳,可以主动关闭长连接降低性能损耗
3.2, 建立和断开连接的时机
场景:在app端使用的时候,页面打开时建立长连接,切出后台时(回到手机桌面/切到其他app)断开长连接,因为长连接一直开着,会浪费服务端性能。
方案(uniapp举例):
-
onShow生命周期可以判断你当前的页面是否在可视区域,这时候
建立websocket连接
-
onHide生命周期可以判断你当前的页面是否脱离了可视区域,这时候
断开websocket连接
建立连接
onShow(){
...doSth
}
断开连接:
onHide(){
...doSth
}
断线重连:某些场景因为网络问题长连接建立失败,可以不用识别到立即弹窗,进行断线重连可以提高用户体验
3.3, 连接断开时,有新数据咋办?
场景: 连接成功 => 连接断开 => 有几条新数据未推给前端 => 重新连接 => 可能会出现这段时间有新数据
所以重新连接后,应该在onConnected
回调中查询历史数据
ws = new WS({
data: {},
onConnected: () => {
// 场景:连接成功 => 连接断开 => 有几条新数据未推给前端 => 重连成功后调用最新数据
getDetailData()
},
})
3.4,大量用户同时保持长连接对服务端性能消耗过大的解决方案?
-
服务器加带宽(立竿见影)
-
根据心跳判断,把长时间不通信的连接置为睡眠状态,睡眠状态的连接做减少资源分配的事
- 减少心跳的发送频率
- 限制带宽使用
- ...
3.5,回调
封装基本离不开回调
上文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()
}
下面是白嫖党的福音,笔者的项目源码区域。不想慢慢看文章内容的,可以直接收藏吃灰。
4, 如何在项目中使用websocket
主要介绍在uniapp和原生H5中的具体使用方法
4.1, uniapp中
为了方便小伙伴查看,放到下面的代码片段中啦
要点解析:
1, protocols
protocols
是uniapp
给我们提供的,它主要的目的是为了提供token
给后端,去鉴权
因为笔者实际开发了uniapp转的APP端
和 APP端内嵌的H5端
,这里的protocols
也是为了两端统一,后端写一套逻辑就行啦。
this.socketTask = uni.connectSocket({ url, protocols: [store.getters.token], success() { } })
4.2, 原生H5页面中
在原生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是啥玩意?
jsb是jsbridge的意思,实现APP与H5之间通信的技术,允许原生代码和H5页面之间相互调用和传递数据
说白了 这里就是通过uniapp自带的@message
和uni.postMessage
,然后封装了一波。小伙伴要是没这个业务,就忽略它。
本文的重点是websocket长连接
// APP端
<web-view ref="webview" class="webview" @message="handlePostMessage"></web-view>
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开发实时通信,项目提测后给我的感觉就是
卧槽,听着好牛逼的技术 -> 嗯?这么简单 -> 还是有点细节的 -> 学到的,常规操作吧
欢迎转载,但请注明来源。
最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。