> 文章列表 > WebSocket详解-02

WebSocket详解-02

WebSocket详解-02

一、背景

        Websocket是html5提出的一个协议规范,是为解决客户端服务端实时通信。
        WebSocket在连接关闭时会触发onclose事件, 在连接异常时会触发onerror事件。但在弱网环境下, 它们的触发灵敏度不高, 往往延迟很久才触发, 前端再去进行重连操作, 这样很不友好。

        本文将介绍使用心跳重连机制来改善这一问题。

二、原理

        心跳重连机制:前端在WS连接成功的情况下,开始执行心跳函数,首先向服务器端发送ping信息, 服务器内若收到信息则会返回pong信息。在一定时间内, 前端收到服务器返回的信息, 则表示此连接是正常的, 便重置心跳函数; 若前端在一定时间内没有收到心跳函数, 则表明没有连接成功, 此时前端关闭ws, 再执行重连操作。

三、代码

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><style>#container {display: grid;/*三列, 每列宽50px*/grid-template-columns: repeat(4, 50px);/*三行, 每行高50px*/grid-template-rows: 50px 50px 50px;margin-top: 20px;}.item {border: 1px solid #e5e4e9;cursor: pointer;font-size: 12px;color: #ffffff;display: flex;justify-content: center;align-items: center;}.item-1 {background-color: #ef342a;}</style></head><body><h1>Websocket心跳检测</h1><div id="container"><div id="btn" class="item item-1">手动停止</div></div><script>class Socket {constructor(options) {// Websocket地址this.url = options.url;// 监听服务器返回数据this.handleMessage = options.handleMessage;// Websocket实例对象this.ws = null;// 心跳函数定时器1:定时发送指令this.pingTimer = null;// 心跳函数定时器2:定时关闭重连this.serverTimer = null;this.timeout = options.timeout;}/*** 建立连接*/connect() {this.ws = new WebSocket(this.url);this.ws.onopen = (e) => {console.log("连接成功", e);// 启动心跳检测this.handleHeartCheck();};this.ws.onmessage = (e) => {this.handleMessage(e.data);// 接收到消息则重置心跳函数this.handleHeartCheck();};this.ws.onclose = (e) => {console.log("连接关闭", e);this.handleClose(e);};this.ws.onerror = (e) => {console.log("连接报错", e);this.handleClose(e);};}/*** 心跳检查*/handleHeartCheck() {if (this.pingTimer) {clearTimeout(this.pingTimer);this.pingTimer = null;}if (this.serverTimer) {clearTimeout(this.serverTimer);this.serverTimer = null;}this.pingTimer = setTimeout(() => {// 发送探测指令this.ws.send(JSON.stringify({ type: "ping" }));// 超过一定时间还没响应, 手动关闭,再重连this.serverTimer = setTimeout(() => {console.log("断线重连");this.ws.close();}, this.timeout);}, this.timeout);}/*** 关闭webSocket*/handleClose(e) {if (this.pingTimer) {clearTimeout(this.pingTimer);this.pingTimer = null;}if (this.serverTimer) {clearTimeout(this.serverTimer);this.serverTimer = null;}this.connect();}}</script><script>// 初始化const ws = new Socket({url: "ws://localhost:8765",timeout: 3000, // 心跳检测时间handleMessage: function (data) {// 监听服务器返回信息console.log("服务器返回信息", data);},});ws.connect();document.getElementById("btn").onclick = () => {console.log("手动关闭");ws.ws.close();};</script></body>
</html>

四、Stomp介绍

        概念:面向消息的简单文本协议

        websocket定义了两种传输信息类型:文本信息和二进制信息。类型虽然被确定,但是他们的传输体是没有规定的。所以,需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议。

        STOMP是基于帧的协议,客户端和服务器使用STOMP帧流通讯。

        一个STOMP客户端是一个可以以两种模式运行的用户代理,可能是同时运行两种模式。

        作为生产者,通过SEND框架将消息发送给服务器的某个服务。

        作为消费者,通过SUBSCRIBE制定一个目标服务,通过MESSAGE框架,从服务器接收消息。

五、Stomp常用代码

// 发送消息
stompClient.send('/topic/terminal_chart', {}, "ping") 
// 订阅(subscription是一个对象,包含一个id属性,对应这个这个客户端的订阅ID)
var subscription = client.subscribe("/queue/test", callback);
// 终止订阅消息
subscription.unsubscribe();
// 获取STOMP子协议的客户端对象
stompClient = Stomp.over(socket);
// 心跳发送频率
stompClient.heartbeat.outgoing = 20000;
// 心跳接收频率
stompClient.heartbeat.incoming = 20000;
// 调用.connect方法连接Stomp服务端进行验证
stompClient.connect({}, ()=>{// 成功回调
}, ()=>{// 失败回调(第一次连接失败和连接后断开连接都会调用这个函数)
})
// 判断是否连接
stompClient.connected

六、Stomp自带心跳机制

stomp默认的心跳为10000ms,
heartbeat.outgoing:客户端发给服务端的心跳,* 0表示它不能发送心跳 * 否则它是能保证两次心跳的最小毫秒数
heartbeat.incoming:客户端希望服务端发送的心跳。* 0表示它不想接收心跳 * 否则它表示两次心跳期望的毫秒数
CONNECT
heart-beat:<cx>,<cy> 客户端

CONNECTED:
heart-beat:<sx>,<sy> 服务端
对于client发送server的心跳: * 如果<cx>为0(client不能发送心跳)或者<sy>为0(server不想接收心跳),将不起任何作用。心跳频率为MAX(<cx>,<sy>)毫秒数.
对于server发送client的心跳:心跳频率为MAX(<cy>,<sx>)毫秒数.

七、Stomp手写心跳检测

        如果服务端禁止接收心跳,则stomp自带心跳机制将不起作用,如下手写的代替。

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./stomp.js"></script></head><body><script>class Socket {constructor(options) {// 地址this.url = options.url;// 目的地this.target = options.target;// 客户端对象this.stompClient = null;// 心跳频率this.timeout = options.timeout;// 定时发送心跳this.pingTimeout = null;// 定时重连this.serverTimer = null;// 服务端返回的数据处理this.handleMessage = options.handleMessage;}connect() {let websocket = new WebSocket(this.url);this.stompClient = Stomp.over(websocket);this.stompClient.connect({},this.connectCallback,this.errorCallback);}// 成功回调connectCallback = (e) => {console.log("连接成功", e);this.handleHeartBeat();this.stompClient.subscribe(this.target, (res) => {if (res.body === "ping") {console.log("心跳返回数据", res);this.handleHeartBeat();} else {// 其他逻辑this.handleMessage(res);}});};// 失败回调(连接失败时的回调函数,此函数重新调用连接方法,形成循环,直到连接成功)errorCallback = () => {console.log("失败重连");if (this.pingTimeout) {clearInterval(this.pingTimeout);this.pingTimeout = null;}if (this.serverTimer) {clearInterval(this.serverTimer);this.serverTimer = null;}this.connect();};handleHeartBeat() {if (this.pingTimeout) {clearInterval(this.pingTimeout);this.pingTimeout = null;}if (this.serverTimer) {clearInterval(this.serverTimer);this.serverTimer = null;}this.pingTimeout = setTimeout(() => {// 发出探测指令this.stompClient.send(this.target, {}, "ping");this.serverTimer = setTimeout(() => {this.errorCallback();}, this.timeout);}, this.timeout);}}</script><script>let ws = new Socket({url: "ws://10.100.31.48:1995/customer/bj-metro-server-customer",target: "/topic/terminal_chart",timeout: 10000,// 订阅信息的返回handleMessage: (res) => {},});ws.connect();</script></body>
</html>