【WebSocket】通信协议基于 node 的简单实践和心跳机制和断线重连的实现

前后端 WebSocket 连接

阮一峰大佬 WebSocket 技术博客

H5 中提供的 WebSocket 协议是基于 TCP 的全双工传输协议。它属于应用层协议,并复用 HTTP 的握手通道。它只需要一次握手就可以创建持久性的连接。

那么什么是全双工呢?

全双工是计算机网络中的一个网络传输方式:数据在线路中的传送方式。一般来说,传送方式有三种方式:单工、半双工、全双工。

全双工:允许数据同时在两个方向上进行传输。这就需要通信的两端设备都需要具备有发送数据和发送数据的能力。

WebSocket 时代之前

在 WebSocket 以前,我们想要实现类似实时聊天这样的功能一般都是使用 AJAX 轮询(轮询、长轮询)实现,
也就是浏览器每隔一段时间主动向服务器发送 HTTP 请求。

轮询:客户端定期向服务器发送请求

长轮询:在客户端发送请求后,保持连接打开,等待新数据响应后再关闭连接。

由于需要每隔一段时间请求服务端,这就带来一定的缺点:只能由客户端发送请求才返回最新的内容给客户端。在某些场景下(实时聊天应用、实时协作应用、实时数据推送、多人在线游戏、在线客服和客户支持等),这就导致了消息的实时性不好,在应用使用人数过少时产生没有必要的网络开销。

WebSocket

在有这些前提了解以后,我们来看看 WebSocket,它出现的原因就是解决客户端和服务端通信的问题。它可以支持服务端主动向客户端发送消息,这样就大大减少了网络开销,同时还保证了一定的消息实时性。

一般来说,WS 连接流程为:客户端在连接前向服务端发送一个常规的 GET 请求:请求将连接方式改为 WebSocket,这个时候请求状态码将为 101 Switching Protocols,请求头中将会有 Upgrade: websocket 字段,表示将连接方式改为 WebSocket。如果服务器响应,那么将会在响应头中带有 Connection 且值为 Upgrade,响应头中还有 Upgrade: websocket 字段,这时候两端就建立起了 ws 连接通道了。

image-20231127180904076

简单实例

接下来我们就简单上手一下 WebSocket 吧。

前端:

<!DOCTYPE html>
<html lang="zh-CN">
    <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,user-scalable=0,maximum-scale=1.0,minimum-scale=1.0">
        <title>独往独来银粟地,一行一步玉沙声</title>
    </head>
    <body>
        <div>前后端 Websocket 连接交互</div>
        <script>
        const ws = new WebSocket('ws://localhost:3000');

        ws.addEventListener('open', function (e) {
            console.log('ws 已经连接', e);
            ws.send('Hello server');
        });

        ws.addEventListener('error', function (error) {
            console.log('ws 异常', error);
        });

        ws.addEventListener('message', function (e) {
            console.log('Message from server ', e.data);
        });

        ws.addEventListener('close', function (e) {
            console.log('ws 已经关闭');
        });
        </script>
    </body>
</html>

后端:

后端我就采用 node 来实现了

# 初始化
yarn init -y

# 引入依赖
yarn add ws

app.js

// 引入 WebSocket模块
const WebSocket = require('ws');

// 创建 WebSocket 服务器,监听端口3000
const server = new WebSocket.Server({port: 3000});

// 当有客户端连接时触发
server.on('connection', (socket) => {
    console.log('客户端已连接...');

    // 处理收到的消息
    socket.on('message', (data) => {
        console.log(`收到客户端发送的消息: ${data}`);
    });

    socket.send('hello client!');
});
console.log("ws 服务示例已经启动 ws://localhost:3000")

效果如下:
在这里插入图片描述

WebSocket 心跳机制

RFC

看完简单示例,我们就来说说目前 WebSocket 存在的缺点以及使用什么方式来解决。

  • 兼容性

这是因为 WS 协议不是所有的浏览器都支持,所以在开发旧版浏览器就需要考虑兼容性问题了。兼容性可使用 node 可使用 socket.io 包,如果用户使用旧版浏览器,那么它就会将 WS 连接转为轮询方式。

  • 连接稳定性

看完上面的内容,相信大家都大概知道 WS 是一个保证客户端和服务端长连接的协议。既然是长连接那么就涉及到一个问题:如果在通信的时候,一断突然掉线了,那另外一方肯定会马上知道的,但是如果链路上没有数据在传输,那么双发就不知道对方是否在线了。想象一下:小明和他女朋友晚上在打电话,小明给她讲笑话,讲完如果小明他女朋友笑了,那么小明知道他成功了,自己的这个笑话是好笑的,但是如果小明讲完以后他女朋友也没有什么回应,那么小明就不知道他是不是女朋友是睡着了。所以,为了防止这种情况发生我们就需要一种机制,能让双方都知道对方还在线。那引出了我们的心跳机制了。

需要知道一下 WebSocket 中必不可少的心跳机制了。那心跳机制是是什么呢?

其实它是 Websocket 协议中的一种保活机制,主要用于维持客户端和服务端两端的长连接,保证两端在连接过程中是否有一端因为意外的错误或者防止长时间不通讯的机制。

通俗一点就是,这种机制可以让客户端和服务端保证双方都在线。比如:客户端发送每隔一段时间发送心跳包通知服务端我还在线,服务端收到这个心跳包以后也发送一个心跳包给客户端同时我也在线。这就好比小明的笑话实在太好笑了,他每讲完一句话,他女朋友就笑出声了,这样小明也知道女朋友没有睡着。

前端实现心跳机制主要有两种方式:

  • 使用 setTimeoutsetInterval 定时器方法定时发送心跳包–没有实际数据,仅用于维持连接状态
  • 前端监听到 WebSocket().close 事件后重新创建 WebSocket 连接

一般来说,第一种方式因为需要定时发送心跳包,就会消耗掉服务器资源。而第二种方式虽然减轻了服务器的负担,但是在重连时很有可能会丢失一部分数据。

这里就重点说一下第一种方式的实现过程吧:

1、客户端和服务端建立 WS 连接
2、客户端向服务端发送心跳包,服务端接收并返回一个表示接收到心跳包的响应
3、当服务端长时间没有接收到心跳包时,服务端将向客户端发送一个关闭连接的请求
4、服务端定时向客户端发送一个心跳包,客户端接收并返回一个表示接收到心跳包的响应
5、当客户端没有接收到服务器发送的心跳包时,客户端会发起重新连接 WS

客户端要实现一个封装好的 socket 类应该具备以下功能:

心跳检测

1、定时发送心跳包
2、客户端发送 ping 的同时需要检测服务端是否响应(设置一个延时器,检测是否有返回 pong,如果没有返回那就开启重连策略 )

断线重连

1、客户端监听发生错误或者掉线就开启重连策略
2、设置重连锁,防止发送多个重连请求
3、开启重连次数限制,超过限制次数就停止重连

其中,这两个功能都需要使用到计时器,所以我们在运行过程中一定一定要记得消除定时器,否则将有可能导致内存泄漏问题。

代码如下,下面的封装是我看了一下网上大多数的案例再结合自己的需求整合出来的,如果对这部分代码还有疑惑或者优化的建议还烦请大家赐教!大家一起讨论才可以一起进步🥰🥰🥰

<!DOCTYPE html>
<html lang="zh-CN">
    <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,user-scalable=0,maximum-scale=1.0,minimum-scale=1.0">
        <title>独往独来银粟地,一行一步玉沙声</title>
    </head>
    <body>
        <div>前后端 Websocket 连接交互</div>
        <button id="createBtn">创建连接</button>
        <button id="closeBtn">断开连接</button>
        <button id="sendBtn">发送消息</button>
        
        <script>
            const createBtn = document.getElementById('createBtn');
            const closeBtn = document.getElementById('closeBtn');
            const sendBtn = document.getElementById('sendBtn');

            let ws;

            const WsOption = {
                url: 'ws://localhost:3000',
                timeout: 5,
                isHeartBeat: true,
                isReconnect: true,
            };

            /**
             * 创建 ws 连接
             */
            createBtn.addEventListener('click', function () {
                ws = new Socket(WsOption);
                ws.connect();
            });

            /**
             * 关闭连接按钮
             */
            closeBtn.addEventListener('click', function () {
                if (ws && ws.readyState === ws.OPEN) ws.clientCloseHandler();
            });

            /**
             * 发送消息按钮
             */
            sendBtn.addEventListener('click', function () {
                ws.sendHandler({type: 'info', date: new Date(), info: 'Hello Server'});
            });

            const WS_STATUS = {
                OPEN: 'open',
                CLOSE: 'close',
                READY: 'ready',
                ERROR: 'error',
                RECONNECT: 'reconnect',
            };

            export default class Socket {
                /**
                 * @param ws ws 实例
                 * @param name ws id
                 * @param status ws 状态
                 * @param timer 重连计时器
                 * @param url ws 连接地址
                 * @param pingInterval 心跳计时器
                 * @param isHeartBeat 是否开启心跳检测
                 * @param timeout 心跳频率
                 * @param isReconnect 是否开启断开重连
                 * @param reconnectNum 最大重连次数
                 * @param lockReconnect 重连锁
                 * @param pingTimeout 心跳返回检查时间计时
                 * @param pingTimer 心跳返回检查计时器
                 */
                constructor(option) {
                    this.ws = null;
                    this.status = null;
                    this.timer = null;
                    this.pingInterval = null;
                    this.pingTimer = null;
                    this.pingTimeout = (3 * 1000) | option.pingTimeout;
                    this.url = option.url;
                    this.name = option.name || 'default';
                    this.reconnectNum = option.reconnectNum || 5;
                    this.lockReconnect = true;
                    this.reconnectTimeout = (option.reconnectTimeout * 1000) | (5 * 1000);
                    this.timeout = option.timeout * 1000 || 2 * 1000;
                    this.isHeartBeat = option.isHeartBeat || false;
                    this.isReconnect = option.isReconnect || false;
                }

                /**
                 * 入口
                 */
                connect() {
                    if (!this.ws) {
                        this.ws = new WebSocket(this.url);
                        this.status = WS_STATUS.READY;
                        console.log(`[WS STATUS] ${this.status}`);

                        // 连接
                        this.ws.onopen = (e) => {
                            this.openHandler(e);
                        };

                        // 收到信息
                        this.ws.onmessage = (e) => {
                            if (JSON.parse(e.data).type === 'pong') {
                                clearTimeout(this.pingTimer)
                            }
                            this.receiveHandler(JSON.parse(e.data));
                        };

                        // 关闭
                        this.ws.onclose = (e) => {
                            this.serverCloseHandler(e);
                        };

                        // 意外错误
                        this.ws.onerror = (e) => {
                            this.errorHandler(e);
                        };
                    }
                }

                /**
                 * ws 连接处理
                 * @param {*} e
                 */
                openHandler(e) {
                    this.status = WS_STATUS.OPEN;
                    console.log(`[WS CONNECT] ${this.url} connect`);

                    if (this.pingInterval) clearTimeout(this.pingInterval);
                    
                    this.sendHandler({
                        type: 'init',
                        date: new Date(),
                        data: `i am ${this.name}`,
                    });

                    if (this.isHeartBeat) {
                        this.startHeartCheck();
                    }
                }

                /**
                 * 收到服务端信息处理
                 * @param {*} data
                 */
                receiveHandler(data) {
                    console.log(`[WS RECEIVE] receive: ${JSON.stringify(data)}`);
                }

                /**
                 * 服务端关闭 ws 连接
                 */
                serverCloseHandler() {
                    if (this.pingInterval) clearInterval(this.pingInterval);
                    this.status = WS_STATUS.CLOSE;
                    console.log(`[WS STATUS] ${this.status}`);
                }

                /**
                 * ws 错误处理
                 * @param {*} e
                 */
                errorHandler(e) {
                    this.status = WS_STATUS.ERROR;
                    console.log(`[WS STATUS] ${this.status}`);

                    if (this.pingInterval) clearInterval(this.pingInterval);
                    if (this.isReconnect) {
                        this.status = WS_STATUS.RECONNECT;
                        console.log(`[WS STATUS] ${this.status}`);
                        if (this.isReconnect) {
                            this.reconnectHandler();
                        }
                    }
                }

                /**
                 * 客户端发送消息处理
                 * @param {*} data
                 */
                sendHandler(data) {
                    console.log(`[SEND MSG] ${JSON.stringify(data)}`);
                    if (this.pingInterval) clearInterval(this.pingInterval);

                    this.ws.send(JSON.stringify(data));
                    if (this.isHeartBeat) {
                        this.startHeartCheck();
                    }
                }

                /**
                 * 重连
                 */
                reconnectHandler() {
                    console.log('[WS ERROR] reconnection mechanism enabled!');
                    if (this.pingInterval) clearInterval(this.pingInterval);

                    // 重连锁
                    if (this.lockReconnect) {
                        this.lockReconnect = false;

                        // 重连次数限制
                        if (this.reconnectNum === 0) {
                            console.log('[WS ERROR] server is offline!!!');
                            this.lockReconnect = true;
                            return;
                        }
                        setTimeout(() => {
                            this.ws = null;
                            this.connect();
                            console.log(`拉取请求还剩下 ${this.reconnectNum}`);
                            this.reconnectNum--;
                            this.lockReconnect = true;
                        }, this.timeout);
                    }
                }

                /**
                 * 心跳检测
                 */
                startHeartCheck() {
                    this.pingInterval = setInterval(() => {
                        if (
                            this.ws.readyState === WebSocket.OPEN &&
                            this.status === 'open'
                        ) {
                            const pingInfo = {type: 'ping', date: new Date()};
                            this.sendHandler(pingInfo);
                        }
                        this.pingTimer = setTimeout(() => {
                            // 未收到 pong 消息,尝试重连...
                            this.reconnectHandler();
                        }, this.pingTimeout);
                    }, this.timeout);
                }

                /**
                 * 客户端关闭 ws 连接
                 */
                clientCloseHandler() {
                    if (this.pingInterval) clearInterval(this.pingInterval);
                    this.ws.close();
                }
            }
        </script>
    </body>
</html>

效果如下:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/207017.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

台灯显色指数多少好?最适合学生的护眼台灯推荐

世界卫生组织最新研究报告显示&#xff0c;2020年全球近视患者约为25亿&#xff0c;其中中国近视人数达6亿&#xff0c;青少年近视率已高居世界第一&#xff0c;且呈现出发病年龄小、发病率高、近视度数深的特点。一款好用的护眼台灯&#xff0c;可以有效的预防近视&#xff0c…

JTAG下载配置

JTAG除了用FT232HL外&#xff0c;还有没有经济一点的方案? 功能验证完以后。投板不用再需要焊接FT232HL这个器件了。可以通过SD卡升级到FLASH或者EMMC。开发一个板子可以不用EMMC且不用SD卡且不用串口。直接就设计上去&#xff0c;如果批量的就不再焊接。 这个USB口是JTAG&a…

第16届中国R会议暨2023X-AGI大会开幕,和鲸科技分享ModelOps在数据科学平台中的实践与应用

11月25日&#xff0c;第 16 届中国 R 会议暨 2023 X-AGI 大会在在中国人民大学逸夫会堂拉开帷幕&#xff0c;本次会议由中国人民大学统计学院、中国人民大学应用统计科学研究中心、统计之都、原灵科技和中国商业统计学会人工智能分会&#xff08;筹&#xff09;主办&#xff0c…

Linux:docker的数据管理(6)

数据管理操作*方便查看容器内产生的数据 *多容器间实现数据共享 两种管理方式数据卷 数据卷容器 1.数据卷 数据卷是一个供容器使用的特殊目录&#xff0c;位于容器中&#xff0c;可将宿主机的目录挂载到数据卷上&#xff0c;对数据卷的修改操作立刻可见&#xff0c;并且更新数…

Apache Flink(四):Flink 其他实时计算框架对比

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…

用高性价比CRM系统,轻松管理复杂的销售流程

对于中小企业来说&#xff0c;选型CRM系统的重点便是性价比。中小企业预算有限&#xff0c;要在满足需求的前提下选择符合预算的CRM系统。那么&#xff0c;有没有性价比高的CRM系统&#xff1f;Zoho CRM就很不错哦。 Zoho CRM是一款SaaS云端CRM客户管理系统&#xff0c;多次荣…

4D雷达目标检测跟踪算法设计

1.算法流程 4D雷达点云跟踪处理沿用3D毫米波雷达的处理流程&#xff0c;如下图&#xff1a; 从接收到点云开始&#xff0c;先对点云做标定、坐标转换、噪点剔除、动静分离&#xff0c;再分别对动态目标和静态目标做聚类&#xff0c;然后根据聚类结果做目标的特征分析和检测等&a…

Vue3-数据交互请求工具设计

1.安装axios pnpm add axios 2.利用axios.create创建一个自定义的axios来使用 参考官网&#xff1a;axios中文文档|axios中文网 | axios 在src/utils文件夹下新建request.js&#xff0c;封装axios模块 import axios from axios const baseURL const instance axios.creat…

2023.11.12 阿里云产品全线故障

阿里云 11.12 故障原因曝光&#xff1a;访问密钥服务 (Access Key) 异常 (baidu.com) 故障原因竟然是因为生成白名单代码逻辑缺陷。

TP4056是一款单节锂离子电池恒流/恒压线性充电器IC

TP4056 1A 锂电池充电器 概述: TP4056 是一款单节锂离子电池恒流 / 恒压线性充电器 , 采用底部带散热片的ESOP-8L封装以及简单的外部应用电路&#xff0c;非常适合便携式设备应用,适合USB电源和适配器电源工作,内部采用防倒充电路&#xff0c;不需要外部隔离二极管。热反馈可对…

JVM 字节码

JVM概述 问题引出 你是否也遇到过这些问题&#xff1f; 运行着的线上系统突然卡死&#xff0c;系统无法访问&#xff0c;甚至直接OOM&#xff01;想解决线上JVM GC问题&#xff0c;但却无从下手。新项目上线&#xff0c;对各种JVM参数设置一脸茫然&#xff0c;直接默认吧&…

完美的输出打印 SQL 及执行时长[MyBatis-Plus系列]

导读 Hi,大家好,我是悟纤。过着爱谁谁的生活,活出不设限的人生。 在我们日常开发工作当中,避免不了查看当前程序所执行的SQL语句,以及了解它的执行时间,方便分析是否出现了慢SQL问题。 MyBatis-Plus提供了两种SQL分析打印的方式,用于输出每条SQL语句及其执行时间,针…

现代化工安全保障迎来巡查无人机新时代

当今现代化工企业呈现出规模不断扩大&#xff0c;设备逐渐趋向大型化的局面&#xff0c;由此导致化工安全生产面临日益严峻的挑战。然而&#xff0c;随着巡查无人机技术的成熟&#xff0c;这种新的高效手段正在提高化工安全检测的工作效率。 一、传统化工安全巡检存在弊端 化工…

博捷芯:半导体芯片切割,一道精细工艺的科技之门

在半导体制造的过程中&#xff0c;芯片切割是一道重要的环节&#xff0c;它不仅决定了芯片的尺寸和形状&#xff0c;还直接影响到芯片的性能和使用效果。随着科技的不断进步&#xff0c;芯片切割技术也在不断发展&#xff0c;成为半导体制造领域中一道精细工艺的科技之门。 芯片…

【MySQL:从零开始练级】环境安装与基础认识

hello大家好&#xff0c;失踪人口回归&#xff0c;今天开始新专栏MySQL&#xff1a;从零开始练级,今天给大家分享MySQL的环境安装与基础认识&#xff0c;希望大家能有所学习收获。 目录 1️⃣ Centos 7环境下安装 2️⃣什么是数据库 3️⃣服务器、数据库、表关系 4️⃣MySQ…

训练 CNN 对 CIFAR-10 数据中的图像进行分类-keras实现

1. 加载 CIFAR-10 数据库 import keras from keras.datasets import cifar10# 加载预先处理的训练数据和测试数据 (x_train, y_train), (x_test, y_test) cifar10.load_data() 2. 可视化前 24 个训练图像 import numpy as np import matplotlib.pyplot as plt %matplotlib …

出一个画质demo

#灵感# 画质demo, 适应的场景不一定多&#xff0c;但演示的功能却一个不少。简单列一下&#xff0c;短时间出一个画质demo的流程。 目录 1、基础检查 2、目标确认 3、调试 4、释放demo 参数 1、基础检查 分辨率、帧率、max gain、bit depth&#xff08;输出raw图像位宽&a…

【Flink进阶】-- Flink kubernetes operator 快速入门与实战

1、课程目录 2、课程链接 https://edu.csdn.net/course/detail/38831

新型信息基础设施下的IP追溯技术:构建数字化安全新境界

随着新型信息基础设施的快速发展&#xff0c;IP&#xff08;Internet Protocol&#xff09;追溯技术在数字化安全领域变得愈发重要。IP追溯不仅能够帮助识别网络攻击源&#xff0c;提升网络安全水平&#xff0c;还有助于数字证据追踪、合规性审计等方面。本文将探讨新型信息基础…

Linux shell for jar test

Linux shell 脚本&#xff0c;循环解析命令行传入的所有参数&#xff0c;并按照不同的传参实现对不同的 java jar文件 进行测试执行。 [rootlocalhost demo]# cat connTest.sh #!/bin/bash# Linux shell for qftool java jar test# modes DEFAULT_MODE2jarfiles[1]common-1.0…