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

news2025/2/27 9:09:20

前后端 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>

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Cloud 原理(第一节)

一、百度百科 Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发&#xff0c;如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等&#xff0c;都可以用Spring Boot的开发风格做到一键启动和部署。Spri…

力扣611题 有效三角形的个数 双指针算法

611. 有效三角形的个数 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 示例 1: 输⼊: nums [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使⽤第⼀个 2) 2,3,4 (使⽤第⼆个 2) 2,2,3 ⽰例 2: 输⼊: nums [4,2,3,4] 输出: 4 解…

我们需要什么样的HA

作为DBA,大家在运维数据库的时候都会遇到 数据库发生 Failover /Switchover 切换的场景。数据库发生切换导致业务连续性受损&#xff0c;少则分钟级&#xff0c;多则小时级别。(最近互联网的故障比较多)。 本文 基于 MySQL 数据库架构场景来分析我们在遇到数据库 HA 切换时是系…

Vue的Nuxt项目部署在服务器,pm2动态部署和npm run build静态部署

Nuxt项目的部署有两种方式&#xff0c;一种是静态部署&#xff0c;一种是动态部署 静态部署需要关闭项目的ssr功能&#xff0c;动态部署则不需关闭&#xff0c;所以怎么部署项目就看你用不用ssr功能了 。 1.静态部署 先说静态部署&#xff0c;很简单&#xff0c;只需要在nuxt…

【自动化测试】pytest 用例执行中print日志实时输出

author: jwensh date: 20231130 pycharm 中 pytest 用例执行中 print 日志 standout 实时命令行输出 使用场景 在进行 websocket 接口进行测试的时候&#xff0c;希望有一个 case 是一直执行并接受接口返回的数据 def on_message(ws, message):message json.loads(message)…

OSU(Optical Service Unit,光业务单元)简介

文章目录 应用场景和功能OSU关键技术基于PB的帧结构划分方式分组业务映射到OSU带宽无损调整机制 标准进展OSU构建电力系统全光底座&#xff0c;赋能新型电力系统 光传送网&#xff08;OTN&#xff09;具有大带宽、硬管道、多业务承载能力、电信级的OAM机制等技术优势&#xff0…

macOS本地调试k8s源码

目录 准备工作创建集群注意点1. kubeconfig未正常加载2. container runtime is not running3. The connection to the server 172.16.190.132:6443 was refused - did you specify the right host or port?4. 集群重置5.加入子节点 代码调试 准备工作 apple m1芯片 安装vmwa…

【JavaScript】3.4 JavaScript在现代前端开发中的应用

文章目录 1. 用户交互2. 动态内容3. 前端路由4. API 请求总结 JavaScript 是现代前端开发的核心。无论是交互效果&#xff0c;还是复杂的前端应用&#xff0c;JavaScript 都发挥着关键作用。在本章节中&#xff0c;我们将探讨 JavaScript 在现代前端开发中的应用&#xff0c;包…

Docker 镜像及其命令

文章目录 镜像Docker 镜像加载原理联合文件系统bootfs和rootfs镜像分层 镜像分层的优势容器层常用命令 镜像 镜像是一种轻量级、可执行的独立软件包&#xff0c;它包含运行某个软件所需的所有内容&#xff0c;我们把应用程序和配置依赖打包好形成一个可交付的运行环境&#xff…

外贸独立站建站详细操作流程一览,跨境电商卖家营销必看!

独立站是一个独立的网站&#xff0c;包括有独立的服务器&#xff0c;独立的网站程序以及网站域名。关于独立站 的优势已经说了很多&#xff0c;本文就不再细谈&#xff0c;想了解的小伙伴可以自行查找之前发布的文章观看。 今天就来说说搭建独立站的详细步骤都有哪些&#xff1…

word模板导出word文件

前期准备工作word模板 右键字段如果无编辑域 ctrlF9 一下&#xff0c;然后再右键 wps 直接 ctrlF9 会变成编辑域 pom.xml所需依赖 <dependencies> <!--word 依赖--> <dependency><groupId>fr.opensagres.xdocreport</groupId><artifactId…

R语言30分钟上手

文章目录 1. 环境&安装1.1. rstudio保存工作空间 2. 创建数据集2.1. 数据集概念2.2. 向量、矩阵2.3. 数据框2.3.1. 创建数据框2.3.2. 创建新变量2.3.3. 变量的重编码2.3.4. 列重命名2.3.5. 缺失值2.3.6. 日期值2.3.7. 数据框排序2.3.8. 数据框合并(合并沪深300和中证500收盘…

深度学习(一):Pytorch之YOLOv8目标检测

1.YOLOv8 2.模型详解 2.1模型结构设计 和YOLOv5对比&#xff1a; 主要的模块&#xff1a; ConvSPPFBottleneckConcatUpsampleC2f Backbone ----->Neck------>head Backdone 1.第一个卷积层的 kernel 从 6x6 变成了 3x3 2. 所有的 C3 模块换成 C2f&#xff0c;可以发现…

AIGC实战——生成对抗网络(Generative Adversarial Network)

AIGC实战——生成对抗网络 0. 前言1. 生成对抗网络1.1 生成对抗网络核心思想1.2 深度卷积生成对抗网络 2. 数据集分析3. 构建深度卷积生成对抗网络3.1 判别器3.2 生成器3.3 DCGAN 模型训练 4. GAN 训练技巧4.1 判别器强于生成器4.2 生成器强于判别器4.3 信息量不足4.4 超参数 小…

机器人AGV小车避障传感器测距

一、A22超声波传感器 该模块是基于机器人自动控制应用而设计的超声波避障传感器&#xff0c;针对目前市场上对于超声波传感器模组盲区大、测量角度大、响应时间长、安装适配性差等问题而着重设计。 具备了盲区小、测量角度小、响应时间短、过滤同频干扰、体积小、安装适配性高…

邀请函 | 合作发展,赋能增效--新架构下汽车电子软件研发技术研讨会

会议介绍 随着汽车智能化、网联化快速演进&#xff0c;“软件定义汽车、架构定义软件”愈发形成行业共识。汽车上的软件应用在提升用户体验、推动行业技术创新方面发挥着至关重要的作用。 在此背景下&#xff0c;如何有效地提升软件开发效率、更好地管理软件质量、满足行业安全…

iOS Swift 代码格式化工具

如果你的代码写得很乱&#xff0c;想一键盘整理代码&#xff0c;像大家推荐一款工具 &#xff08;PS&#xff1a;Xcode本身并没有代码格式化工具&#xff0c;这款工具为第三方开发的&#xff09; 这款工具名为&#xff1a;SwiftFormat 1&#xff1a;在GitHub上搜索“SwiftFo…

YOLOv8改进 | 2023 | SCConv空间和通道重构卷积(精细化检测,又轻量又提点)

一、本文介绍 本文给大家带来的改进内容是SCConv&#xff0c;即空间和通道重构卷积&#xff0c;是一种发布于2023.9月份的一个新的改进机制。它的核心创新在于能够同时处理图像的空间&#xff08;形状、结构&#xff09;和通道&#xff08;色彩、深度&#xff09;信息&#xf…

中国毫米波雷达产业分析5——毫米波雷达应用案例介绍

一、上海某区康养标杆工程 项目甲方&#xff1a;上海某康养中心 项目地点&#xff1a;上海徐汇区 项目时间&#xff1a;2023年8月 供应商&#xff1a;南京苗米科技有限公司 &#xff08;一&#xff09;项目需求 该康养社区集中收治了区内75岁以上老龄化人群和部分阿尔茨海默…

RubyMine 2023 年下载、安装、使用教程,详细图解

大家好&#xff0c;今天为大家带来的是RubyMine 2023 年下载、安装、使用教程&#xff0c;详细图解。 文章目录 1 RubyMine 简介2 RubyMine 下载、安装教程RubyMine 下载RubyMine 安装 3 RubyMine 汉化4. 常用快捷键一级必会二级进阶 1 RubyMine 简介 RubyMine 是一个为 Ruby …