实时通信的轮询方案
1,ajax短连接:客户端每隔一秒钟发一次请求,服务器收到请求后会立刻返回结果,不管有没有新数据。
2,ajax长连接:客户端发送一次请求,服务器端收到请求后查询有没有新数据,如果没有新数据就阻塞这个请求,直到有新数据或者超时为止。客户端每次收到请求返回结果后立刻再发一次请求。comet貌似就是这个原理。
3,WebSocket:这就不是一个HTTP协议了,而是一个tcp协议,而且Socket这个玩意顾名思义就是一个流了,可以双向操作。缺点是有些浏览器不支持。
对比延迟:
假设网络延迟是m毫秒,那么ajax短连接的延迟在m到1000毫秒之间,另外两种基本只有m毫秒的延迟。
对比资源占用:
应该是1>2>3。但是1和2的比较要看情况,如果两次请求间隔时间很长的话应该是2>1>3。
实时通信技术在股票价格、新闻报道、余票查询、交通情况等领域中有着广泛的应用,
但是目前的实时Web应用的实现方式,都是基于HTTP协议,围绕着轮询和其他服务器推送技术展开的,所以不可避免的产生大量的额外的报头数据,造成传输时延。HTML5中的WebSocket协议是基于浏览器与服务器全双工通信的新理论。客户端通过WebSocket与服务器进行通信时,只有第一次握手交互信息比较复杂,在握手成功后便进入全双工的数据传输阶段,降低了数据流量与传输时延。
流程图
WebSocketObj类
使用单例模式创建websocket连接,暴露出手动开启/关闭/重连的函数,内部实现断线重连和心跳检测机制,通过onMessage状态钩子触发接收下行消息,发送该下行type对应的事件通知,由SocketEvent类实现调用对应的事件数组。
import { PING, ROOMID, SESSION, TIME_SPACE, EV_TYPE } from './constant'
import { tpToType } from './mapHandle'
import SocketEvent from './socketEvent'
import Api from 'globals/api/getCommon';
let wsUrl = null; // ws链接地址
let ws = null; // 初始化ws实例
let lockReconnect = false; // 避免重复连接
// ws对象
class WebSocketObj {
// 获取ws地址
async getWsUrl() {
const params = {
roomid: ROOMID,
session: SESSION
}
const { dm_error, url } = await Api.getWebsocketUrl(params);
if (+dm_error === 0) {
console.log('获取到长链接地址')
console.log(url)
wsUrl = url
}
}
// 创建websocket连接
async createWebSocket() {
!wsUrl && await this.getWsUrl();
this.initWebSocket();
this.socketHandle();
}
// 确保只有一个Websocket实例
initWebSocket() {
if (!ws) {
if (!wsUrl) return
ws = new WebSocket(wsUrl);
} else {
console.log('已有websocket实例')
}
return ws;
}
// websocket 钩子
socketHandle = () => {
// 创建成功
const self = this;
if (!ws) return
ws.onopen = function () {
var dt = new Date();
var str = dt.getFullYear() + '-' + (dt.getMonth() + 1) + '-' + dt.getDate() + ' ' + dt.getHours() + ':' + dt.getMinutes() + ':' + dt.getSeconds();
// 重置心跳检测
self.heartbeatCheck.init();
self.heartbeatCheck.start();
console.log(str)
console.log('*************** websocket连接成功 ***************');
};
// 接收到推送消息
ws.onmessage = function (evt) {
var data = JSON.parse(evt.data);
// 重置心跳检测
self.heartbeatCheck.init();
self.heartbeatCheck.start();
const { b: { ev }, ms } = data;
if (ev === EV_TYPE.PONG) {
console.log('心跳正常')
} else if (ev === EV_TYPE.SM) {
console.log('------------ 收到推送消息 ------------')
console.log(data)
self.postMessage(ms)
}
};
// 长链接被关闭
ws.onclose = function (evt) {
self.heartbeatCheck.init();
console.log('------------ 长连接关闭 ------------')
console.log(evt)
// this.reconnect();
};
// 长链接报错
ws.onerror = function (evt) {
console.log('------------ 长连接报错 ------------')
console.log(evt)
this.reconnect();
};
}
// 手动开启
wsOpen() {
this.createWebSocket();
}
// 手动关闭
wsClose() {
ws && ws.close();
ws = null
}
// 更新websocket地址
async updateWsUrl() {
this.wsClose();
await this.getWsUrl();
this.initWebSocket();
this.socketHandle();
}
// 上行ping消息
wsSendPing() {
console.log('------------ 心跳检测 ------------')
ws && ws.send(JSON.stringify(PING))
}
// 重连机制
reconnect = () => {
// 重连中,不重复执行
if (lockReconnect) {
return
}
lockReconnect = true;
// 没连接上会一直重连,设置延迟避免请求过多
setTimeout(function () {
this.createWebSocket();
lockReconnect = false;
}, 2000);
}
// 心跳检测
heartbeatCheck = {
sendTimer: null, // 定时发送ping
receiveTimer: null, // timeSpace时间内接收pong
// 初始化心跳
init: () => {
clearTimeout(this.sendTimer)
clearTimeout(this.receiveTimer)
},
// 开启心跳检测
start: () => {
this.sendTimer = setTimeout(() => {
this.wsSendPing(); // 发送ping
this.receiveTimer = setTimeout(() => {
console.log('心跳检测失败,正在尝试重连...')
this.wsClose(); // 先关闭当前ws
// this.wsOpen(); // 重新打开ws
this.createWebSocket();
}, TIME_SPACE)
}, TIME_SPACE)
}
}
// 发送事件
postMessage(sm) {
sm.forEach(data => {
const { tp, bd } = data;
const type = tpToType[tp]
if (type) {
SocketEvent.executeEvent(type, bd)
} else {
console.log('该长链接未定义:', tp)
}
});
}
}
export default new WebSocketObj();
SocketEvent类
实现发布订阅模式,抛出addEvent添加事件,removeEvent移除事件和executeEvent执行事件的函数,各业务页面通过addEvent来订阅对应业务下行消息的type事件,可以通过removeEvent手动移除当前页面的指定订阅函数,在WebSocketObj类中接收到长链下行的时候触发对应type的回调函数来实现发布操作。
// 事件容器,放置事件数组
const eventHandlers = {};
class SocketEvent {
// 添加事件
addEvent(type, handler) {
// 判断事件数组中是否已存在
if (!(type in eventHandlers)) {
eventHandlers[type] = [];
}
// 添加入事件数组
eventHandlers[type].push(handler)
}
// 删除事件
removeEvent(type, handler) {
// 不存在当前事件类型
if (!(type in eventHandlers)) {
return;
}
// 未传入指定handle,删除type所有handler
if (!handler) {
delete eventHandlers[type]
} else {
const tempIndex = eventHandlers[type].findIndex(handlerItem => {
return handlerItem === handler
});
if (tempIndex === -1) {
// console.log('不存在事件handle:', handler);
} else {
eventHandlers[type].splice(tempIndex, 1)
if (eventHandlers[type].length === 0) {
delete eventHandlers[type]
}
}
}
}
// 执行事件
executeEvent(type, ...params) {
if (!(type in eventHandlers)) {
console.log('未注册该事件:', type)
return;
}
eventHandlers[type].forEach(handlerItem => {
handlerItem(...params)
});
}
}
export default new SocketEvent();
websocket Model
定义一个websocket的model,暴露出开关长链接和添加移除事件,方便业务组件通过model来调用。
import WebSocketObj from '@/webSocket/webSocket'
import SocketEvent from '@/webSocket/socketEvent'
export default {
name: 'webSocket',
state: {
},
effects: {
// 创建长链接
* createWebSocket({ payload }, { call, put, select }) {
yield WebSocketObj.createWebSocket()
},
// 更新长链接地址
* updateWebSocket({ payload }, { call, put, select }) {
yield WebSocketObj.updateWsUrl();
},
// 添加事件
* addEvent({ payload }, { call, put, select }) {
const { type, handle } = payload;
yield SocketEvent.addEvent(type, handle)
},
// 移除事件
* removeEvent({ payload }, { call, put, select }) {
const { type, handle } = payload;
yield SocketEvent.removeEvent(type, handle);
},
},
reducers: {
closeWebSocket(state) {
WebSocketObj.wsClose();
return {
...state,
};
},
},
subscriptions: {
},
};
mapHandle类
作为一个映射类,将服务端下行消息的type类型映射到对应的事件名称
// 定义事件列表
export const typeList = {
LIVE_PREVIEW_CHANGE_EVENT: 'LivePreviewChangeEvent',
}
// 定义下行消息对应的事件
export const tpToType = {
'c.xxxxxxxxxxxxx': typeList.LIVE_PREVIEW_CHANGE_EVENT, // 下行事件
}
// 开发环境检测type值是否重复定义
if (process.env.NODE_ENV === 'development') {
const memo = new Set()
Object.values(typeList).forEach(item => {
if (!memo.has(item)) {
memo.add(item)
} else {
throw new Error('重复定义tp:', item)
}
})
}
业务组件使用例子
import React, { useState, useEffect } from 'react'
import { history, useDispatch } from 'umi'
import { typeList } from '@/webSocket/mapHandle'
import $common from '@/utils/common';
import Header from 'common-comp/Header';
const WebSocketTest = props => {
const [eventData, setEventData] = useState()
const [canAdd1, setCanAdd1] = useState(true)
const [canAdd2, setCanAdd2] = useState(true)
const dispatch = useDispatch()
const testFun1 = (data) => {
console.log('testFun1=========', data)
setEventData(data)
}
const testFun2 = (data) => {
console.log('testFun2=========', data)
}
useEffect(() => {
window.reloadNewUrl = url => {
const oldUrlParams = $common.getUrlParams(window.location.href);
const newUrlParams = $common.getUrlParams(url);
if (oldUrlParams.tourist !== newUrlParams.tourist) {
if (window.history.replaceState) {
window.history.replaceState(null, document.title, url);
}
const params = $common.getUrlParams(url)
const {
uid, sid, tourist, smid
} = params;
// 更新cookie
dispatch({
type: 'user/appUpdateLogin',
payload: {
uid, sid, smid, tourist
}
})
}
};
return () => {
delete window.reloadNewUrl;
}
}, [])
useEffect(() => {
// 添加响应事件
if (canAdd1) {
dispatch({
type: 'webSocket/addEvent',
payload: {
type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
handle: testFun1
}
})
}
if (canAdd2) {
dispatch({
type: 'webSocket/addEvent',
payload: {
type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
handle: testFun2
}
})
}
// 移除响应事件
return () => {
dispatch({
type: 'webSocket/removeEvent',
payload: {
type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
handle: testFun1
}
})
dispatch({
type: 'webSocket/removeEvent',
payload: {
type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
handle: testFun2
}
})
}
}, [testFun1, testFun2])
return <div>
<Header
leftIcon="home"
rightContent={[
{
icon: 'shop-car',
handleClick: () => history.push('/shoppingCart'),
},
{
icon: 'user',
},
]}
/>
<div
style={{ backgroundColor: 'gray', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
onClick={() => {
$common.goLoginPage();
}}
>
登录
</div>
<div
style={{ backgroundColor: 'pink', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
onClick={() => {
dispatch({
type: 'webSocket/createWebSocket',
})
}}
>
手动开启
</div>
<div
style={{ backgroundColor: 'yellow', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
onClick={() => {
dispatch({
type: 'webSocket/closeWebSocket',
})
}}
>
手动关闭
</div>
<div
style={{ backgroundColor: 'red', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
onClick={() => {
setCanAdd1(false)
dispatch({
type: 'webSocket/removeEvent',
payload: {
type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
handle: testFun1
}
})
}}
>移除事件回调1</div>
<div
style={{ backgroundColor: 'red', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
onClick={() => {
setCanAdd2(false);
dispatch({
type: 'webSocket/removeEvent',
payload: {
type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
handle: testFun2
}
})
}}
>移除事件回调2</div>
<div>
下行数据回显:{eventData?.preview_list[0]?.img}
</div>
</div>
}
export default WebSocketTest;
从layout入口开启全局的长连接
运行页面
疑难点:
为什么还要手动实现心跳检测?
http协议和websocket协议都是应用层协议,都是依赖于TCP连接,通过TCP协议来传输的,而TCP协议本身是有KeepAlive保活机制,但是Keepalive 技术只是TCP协议中的一个可选项。因为不当的配置可能会引起一些问题,所以默认是关闭的,而且TCP协议默认2小时的KeepAlive基本不可能长连接保活,所有需要手动实现心跳检测