websocket 实时通信实现
轮询和websocket对比
开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式:
轮询
:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果这种方式比较古老,但是兼容性强。
缺点就是不断请求,耗费了大量的带宽和 CPU 资源,而且存在一定的延迟性
websocket
长连接:全双工通信
,客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,更加方便
websocket 实现
创建 websocket 连接
建立ws连接,有如下两种形式:
ws
代表明文,默认端口号为 80,例如ws://http://www.example.com:80, 类似http
wss
代表密文,默认端口号为 443,例如wss://http://www.example.com:443, 使用SSL/TLS加密,类似https
const useWebSocket = (params: wsItem) => {
// 定义传参 url地址 phone手机号
let { url = "", phone = "" } = params;
const ws = (useRef < WebSocket) | (null > null);
// ws数据
const [wsData, setMessage] = (useState < wsDataItem) | (null > null);
// ws状态
const [readyState, setReadyState] =
useState < any > { key: 0, value: "正在连接中" };
// 是否在当前页
const [isLocalPage, setIsLocalPage] = useState(true);
// 创建Websocket
const createWebSocket = () => {
try {
window.slWs = ws.current = new WebSocket(
`wss://${url}/ws/message/${phone}`
);
// todo 全局定义发送函数
window.slWs.sendMessage = sendMessage;
// todo 准备初始化
initWebSocket();
} catch (error) {
// 创建失败需要进行异常捕获
slLog.error("ws创建失败", error);
// todo 准备重连
reconnect();
}
};
return { isLocalPage, wsData, closeWebSocket, sendMessage };
};
初始化 websocket
当前的连接状态定义如下,使用常量数组控制:
const stateArr = [
{ key: 0, value: "正在连接中" },
{ key: 1, value: "已经连接并且可以通讯" },
{ key: 2, value: "连接正在关闭" },
{ key: 3, value: "连接已关闭或者没有连接成功" },
];
主要有四个事件,连接成功的回调函数(onopen)、连接关闭的回调函数(onclose)、连接失败的回调函数(onerror)、收到消息的回调函数(onmessage)
const initWebSocket = () => {
ws.current.onopen = (evt) => {
slLog.log("ws建立链接", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 心跳检查重置
keepHeartbeat();
};
ws.current.onclose = (evt) => {
slLog.log("ws链接已关闭", evt);
};
ws.current.onerror = (evt) => {
slLog.log("ws链接错误", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 重连
reconnect();
};
ws.current.onmessage = (evt) => {
slLog.log("ws接受消息", evt.data);
if (evt && evt.data) {
setMessage({ ...JSON.parse(evt.data) });
}
};
};
websocket 心跳机制
在使用 ws 过程中,可能因为网络异常或者网络比较差,导致 ws 断开链接了,此时 onclose 事件未执行,无法知道 ws 连接情况。就需要有一个心跳机制,监控 ws 连接情况,断开后,可以进行重连操作。
目前的实现方案就是:前端每隔 5s 发送一次心跳消息,服务端连续 1 分钟没收到心跳消息,就可以进行后续异常处理了
const timeout = 5000; // 心跳时间间隔
let timer = null; // 心跳定时器
// 保持心跳
const keepHeartbeat = () => {
timer && clearInterval(timer);
timer = setInterval(() => {
if (ws.current?.readyState == 1) {
// 发送心跳 消息接口可以自己定义
sendMessage({
cmd: "SL602",
content: { type: "heartbeat", desc: "发送心跳维持" },
});
}
}, timeout);
};
如下图所示,为浏览器控制台中的截图,可以查看ws连接请求及消息详情。
注意:正常情况下,是需要对消息进行加密的,最好不要明文传输。
websocket 重连处理
let lockFlag = false; // 避免重复连接
// 重连
const reconnect = () => {
try {
if (lockFlag) {
// 是否已经执行重连
return;
}
lockFlag = true;
// 没连接上会一直重连
// 设置延迟避免请求过多
lockTimer && clearTimeout(lockTimer);
var lockTimer = setTimeout(() => {
closeWebSocket();
ws.current = null;
createWebSocket();
lockFlag = false;
}, timer);
} catch (err) {
slLog.error("ws重连失败", err);
}
};
websocket 关闭事件
关闭事件需要暴露出去,给外界控制
// 关闭 WebSocket
const closeWebSocket = () => {
ws.current?.close();
};
websocket 发送数据
发送数据时,数据格式定义为对象形式,如{ cmd: '', content: '' }
// 发送数据
const sendMessage = (message) => {
if (ws.current?.readyState === 1) {
// 需要转一下处理
ws.current?.send(JSON.stringify(message));
}
};
页面可见性
监听页面切换到前台,还是后台,可以通过visibilitychange
事件处理。
当页面长时间处于后台时,可以进行关闭或者异常的逻辑处理。
// 判断用户是否切换到后台
function visibleChange() {
// 页面变为不可见时触发
if (document.visibilityState === "hidden") {
setIsLocalPage(false);
}
// 页面变为可见时触发
if (document.visibilityState === "visible") {
setIsLocalPage(true);
}
}
useEffect(() => {
// 监听事件
document.addEventListener("visibilitychange", visibleChange);
return () => {
// 监听销毁事件
document.removeEventListener("visibilitychange", visibleChange);
};
}, []);
页面关闭
页面刷新或者是页面窗口关闭时,需要做一些销毁、清除的操作,可以通过如下事件执行:
beforeunload
:当浏览器窗口关闭或者刷新时会触发该事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。
onunload
:当文档或一个子资源正在被卸载时,触发该事件。beforeunload在其前面执行,如果点的浏览器取消
按钮,不会执行到该处。
function beforeunload(ev) {
const e = ev || window.event;
// 阻止默认事件
e.preventDefault();
if (e) {
e.returnValue = "关闭提示";
}
return "关闭提示";
}
function onunload() {
// 执行关闭事件
ws.current?.close();
}
useEffect(() => {
// 初始化
window.addEventListener("beforeunload", beforeunload);
window.addEventListener("unload", onunload);
return () => {
// 销毁
window.removeEventListener("beforeunload", beforeunload);
window.removeEventListener("unload", onunload);
};
}, []);
执行 beforeunload
事件时,会有如下取消、确认弹框
参考文档:
来源:juejin.cn/post/7249204284180086842