一直以来我认为Websocket就是长连接。长连接就是客户端和服务器一直保持连接,并且两者都可以主动发消息给另一方
基于现在的理解列出上面这句话中错误的知识点:
- websocket和长连接是两个不同的概念
- 主动发消息的功能不是长连接的特点
- 加粗部分对长连接这个概念的描述不够透彻,太过片面,不算错误(后面会讲到)
概念混淆(长连接,Websocket)
长连接:一个网站中通常会有很多请求,如果每个请求都需要进行TCP连接的建立和断开,那么是相当耗时的,处理速度也会大大降低,在Http 1.1中支持进行长连接(Connection: keep-alive),即:一个http操作建立TCP连接后,进行数据传输,结束后并不会立即断开而是保持连接,后面的http请求使用同一个连接进行数据传输,不需要再次建立连接,始终使用一个TCP连接
Websocket:一种在单个TCP连接上进行全双工通信的协议。使得客户端和服务器之间的数据交换更加简单,允许服务端主动向客户端推送数据。在Websocket的API中,浏览器和服务器只要完成一次握手,就可以创建持久性的连接,并进行双向数据传输。(HTML5中新定义的协议,可以很好的节省服务器的资源和带宽,实时进行通讯)
根据对这两个概念的理解,得出Websocket协议本身就实现了长连接,因为它是基于单个TCP连接进行通信,在此基础上还实现了服务器可主动发送数据,进行双向通信的功能
为什么要用Websocket
有这样的需求场景:很多网站需要实现推送功能,这就需要服务器主动向浏览器发送请求,但是Http协议的弊端就是通信只能由客户端发起,那么我们为了实现推送功能,就需要客户端不停的去问服务器,于是就有了下面的对话。这就是最常见的轮询技术,要定时的向服务器发请求,但是请求中可能包含较长的头部,真正有效的数据只有小部分,因此这种方式会浪费很多资源,增加服务器压力。而Websocket实现了服务器主动向客户端推送消息,因此像聊天室这种业务场景更多的会使用Wescoket来实现
client:你有没有消息哇。。。
server:暂时没有,你过会再来
client:我又来了,现在有没有。。。
server:有了有了,给你拿去。。。
Websocket使用的关键点——心跳保活策略
Websocket提供的API使用都比较简单,这里不再赘述,主要想说心跳保活的问题,因为在连接建立后,有可能存在网络断开或者别的异常情况,这个时候是没法触发浏览器或者服务器的onclose事件,无法知道断开连接,也就无法进行重连,并且收发的数据也会丢失掉,因此我们就有了Websocekt的心跳,就是说明连接还处于正常状态。
心跳的检测的机制:每隔一段时间客户端向服务器发送一个心跳包,服务器收到后会返回一个数据包,以此来确认两者都活着,否则任意一方在规定时间内没有收到心跳包,则会认定网络断开,需要重连。若重连N次还是失败,则会提示用户网络问题,依赖着心跳检测才可以保障长连接的存在
前后端约定的规则:
- 每隔30秒发送一次心跳包
- 6秒内未返回心跳包断开重连
- 重连超过10次不再连接
createWebSocket () {
this.ws = new WebSocket(`ws://localhost:3000`)
this.ws.onopen = () => {
this.reconnectCount = 0
this.heartCheck().start()
console.log('Connection to server opened')
}
this.ws.onmessage = (event) => {
const response = JSON.parse(event.data)
this.heartCheck().reset()
console.log('Client received a message', response)
}
this.ws.onclose = () => {
console.log('connection closed')
clearTimeout(this.heartTimer)
this.reconnect()
}
}
// ws重连操作
reconnect () {
this.reconnectCount = this.reconnectCount + 1
console.log('重连' + this.reconnectCount + '次')
if (this.reconnectCount > 10) {
throw new Error('重新连接失败')
}
this.createWebSocket()
}
// 心跳检测
heartCheck () {
const _self = this
return {
start: function () {
_self.heartTimer = setTimeout(function () {
_self.ws.send(JSON.stringify({
'type': 1,
'timestamp': new Date().getTime()
}))
_self.closeTimer = setTimeout(() => _self.ws.close(), 6000) // 超过6秒未返回心跳包断开连接
}, 30000) // 每隔30秒发心跳包检测
},
reset: function () {
clearTimeout(_self.heartTimer)
clearTimeout(_self.closeTimer)
this.start()
}
}
}
页面滑动的分页功能
需求:在联系人列表页滑动,当滚动条距离底部小于50px触发请求获取第二页的数据然后展示
问题拆解:
- 监听dom的滚动事件
- 计算滚动条距离底部的距离
对Dom进行事件监听比较容易,我当时在第二个问题上花的时间比较久,最终明白了如何计算。
先搞明白Dom的三个属性:scrollHeight、clientHeight、scrollTop
- scrollHeight:容器里所有内容的高度
- clientHeight:容器的高度,可见高度
- scrollTop:滚动条滚动过的距离
举例:一个DIV的高度是400px(clientHeight),里面有一个很长的列表,这个列表的高度是1000px(scrollHeight),说明还有600px的内容是不可见的,要想显示剩余的内容,就需要拖动滚动条来显示。当不拉滚动条时,此时scrollTop是0,如果把滚动条拉到底,剩下600px的内容显示出来,此时scrollTop就是600,于是我们认为scrollTop就是滚动条可以滚动的距离,也就是剩余内容的高度[0, 600],
scrollHeight - clientHeight - scrollTop = 滚动条距离底部的距离
有了这个公式我们的滑动分页功能就没多大问题了
但是在后面自测的时候发现:在滑动的过程中不可能精准的在小于50px的范围内停一次,有可能在45,34用户都停下了,这就导致请求发了不止一次,而我们理想是只发一次。最后我用一个变量isLoading去判断,请求发出后isLoading为true,回来后改为false,在scroll事件处理函数中去判断这个变量的值,为true,则不发请求。
// 滚动条距离底部小于50px时触发请求
handleScrollEvent () {
const element = this.$refs.scrollUserList
if (element.scrollHeight - element.clientHeight - element.scrollTop < 50 && !this.isLoading) {
this.page += 1
this.getFriendsList(this.page)
}
}
async getFriendsList (page = 0) {
this.isLoading = true
const params = {
wechatId: localStorage.getItem('loginWechatId'),
productId: 211,
token: 'dalfdll',
page,
limit: 20
}
const response = await chatService.getFriendsList(params)
const { success } = response
if (success) {
this.isLoading = false
}
}