【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 连接通道了。
简单实例
接下来我们就简单上手一下 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 协议中的一种保活机制,主要用于维持客户端和服务端两端的长连接,保证两端在连接过程中是否有一端因为意外的错误或者防止长时间不通讯的机制。
通俗一点就是,这种机制可以让客户端和服务端保证双方都在线。比如:客户端发送每隔一段时间发送心跳包通知服务端我还在线,服务端收到这个心跳包以后也发送一个心跳包给客户端同时我也在线。这就好比小明的笑话实在太好笑了,他每讲完一句话,他女朋友就笑出声了,这样小明也知道女朋友没有睡着。
前端实现心跳机制主要有两种方式:
- 使用
setTimeout
或setInterval
定时器方法定时发送心跳包–没有实际数据,仅用于维持连接状态 - 前端监听到
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>
效果如下:
相关文章:
【WebSocket】通信协议基于 node 的简单实践和心跳机制和断线重连的实现
前后端 WebSocket 连接 阮一峰大佬 WebSocket 技术博客 H5 中提供的 WebSocket 协议是基于 TCP 的全双工传输协议。它属于应用层协议,并复用 HTTP 的握手通道。它只需要一次握手就可以创建持久性的连接。 那么什么是全双工呢? 全双工是计算机网络中的…...
【有ISSN、ISBN号!往届均已完成EI检索】第三届电子信息工程、大数据与计算机技术国际学术会议(EIBDCT 2024)
第三届电子信息工程、大数据与计算机技术国际学术会议(EIBDCT 2024) 2024 3rd International Conference on Electronic Information Engineering, Big Data and Computer Technology 第三届电子信息工程、大数据与计算机技术国际学术会议(…...
【Windows】使用SeaFile搭建本地私有云盘并结合内网穿透实现远程访问
1. 前言 现在我们身边的只能设备越来越多,各种智能手机、平板、智能手表和数码相机充斥身边,需要存储的数据也越来越大,一张手机拍摄的照片都可能有十多M,电影和视频更是按G计算。而智能设备的存储空间也用的捉襟见肘。能存储大量…...
Windows本地搭建WebDAV服务并使用内网穿透远程访问【无公网IP】
windows搭建WebDAV服务,并内网穿透公网访问【无公网IP】 文章目录 windows搭建WebDAV服务,并内网穿透公网访问【无公网IP】1. 安装IIS必要WebDav组件2. 客户端测试3. cpolar内网穿透3.1 打开Web-UI管理界面3.2 创建隧道3.3 查看在线隧道列表3.4 浏览器访…...
责任链设计模式
package com.jmj.pattern.responsibility;/*** 请假条类*/ public class LeaveRequest {//姓名private String name;//请假天数private int num;//请假内容private String content;public LeaveRequest(String name, int num, String content) {this.name name;this.num num;…...
12.4 C++ 作业
完成沙发床的多继承 #include <iostream>using namespace std;//封装 沙发 类 class Sofa { private:string *sitting; public://无参构造函数Sofa(){cout << "Sofa::无参构造函数" << endl;}//有参构造函数Sofa(string s):sitting(new string(s)…...
基于ssm品牌会员在线商城源码
基于ssm品牌会员在线商城源码708 idea mysql数据库 navcat 开发技术:后端 ssm 后台管理 vue 用户端 vue.jshtml 演示视频: 基于ssm品牌会员在线商城源码 DROP TABLE IF EXISTS address; /*!40101 SET saved_cs_client character_set_client */; /…...
骨传导耳机音量大了有害吗?骨传导能保护听力吗?
无论是传统耳机还是骨传导耳机,只要使用音量过大,都会对有一定的损伤,然而由于骨传导耳机的传声原理和佩戴方式比较特殊,所以对人体的损伤比较小,想要知道骨传导耳机能否保护听力,就要先了解骨传导耳机的传…...
百望云供应链协同解决方案入选北大创新评论产业研究案例库
11月28日-29日,百望云受邀出席《北大创新评论》2023 Inno China 中国产业创新大会,从战略构建、生态塑造、科技创新等议题出发,与学术专家、产业专家、企业代表共赴盛会,思享汇聚。会上,《北大创新评论产业研究案例库&…...
selenium中元素定位正确但是操作失败,6种解决办法全搞定
selenium中元素定位正确但是操作失败的原因无外乎以下4种: 01 页面没加载好 解决方法:添加等待方法,如:time.sleep() 02 页面提交需要等待给数据后台 解决方法:添加等待方法,如:time.sleep(…...
触控板绘画工具Inklet mac功能介绍
Inklet mac是一款触控板绘画工具,把你的触控板变成画画的板子,意思是,你点在触控板的哪里,鼠标就会出现载相应的地方。例如,但你把手指移动到触控盘左下角,那么鼠标也会出现在左下角,对于用户而…...
〔005〕虚幻 UE5 像素流多用户部署
✨ 目录 ▷ 为什么要部署多用户▷ 开启分发服务器▷ 配置启动多个信令服务器▷ 配置启动客户端▷ 多用户启动整体流程和预览▷ 注意事项 ▷ 为什么要部署多用户 之前的像素流部署,属于单用户,是有很大的弊端的打开多个窗口访问,可以看到当一…...
11. 哈希冲突
上一节提到,通常情况下哈希函数的输入空间远大于输出空间,因此理论上哈希冲突是不可避免的。比如,输入空间为全体整数,输出空间为数组容量大小,则必然有多个整数映射至同一桶索引。 哈希冲突会导致查询结果错误&#…...
12.04 二叉树中等题
513. 找树左下角的值 给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1 思路:找到最低层中最左侧的节点值,比较适合层序遍历,返回最…...
Redis的安装
本文采用原生的方式安装Redis,Redis的版本为5.0.5 安装 下载 下载网站:https://download.redis.io/releases/ wget http://download.redis.io/releases/redis-5.0.5.tar.gz解压 tar -zxvf redis-5.0.5.tar.gz进入redis目录 cd redis-5.0.5执行编译…...
JDK安装太麻烦?一篇文章搞定
JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVMJava系统类库)和JAVA工具。 JDK包含的基本组件包括: javac – 编译器…...
漫谈HBuilderX App-Jenkins热更新构建
漫谈Uniapp App热更新包-Jenkins CI/CD打包工具链的搭建 零、写在前面 HBuilderX是DCloud旗下的IDE产品,目前只提供了Windows和Mac版本使用。本项目组在开发阶段经常需要向测试环境提交热更新包,使用Jenkins进行CD是非常有必要的一步。尽管HBuilderX提…...
技术前沿丨Teranode如何实现无限扩容
发表时间:2023年9月15日 BSV区块链协会的技术团队目前正在努力开发Teranode,这是一款比特币节点软件,其最终目标是实现比特币的无限扩容。然而,正如BSV区块链协会网络基础设施负责人Jake Jones在2023年6月举行的伦敦区块链大会…...
世岩清上:如何制作年终工作汇报宣传片
年终工作汇报宣传片是一种以视觉和口头语言为主要表现形式的宣传手段,旨在向领导、同事、客户等展示一年来的工作成果和亮点。以下是制作年终工作汇报宣传片的几个关键步骤: 明确目的和受众:在制作宣传片前,要明确宣传片的目的和受…...
练习十一:简单卷积器的设计
简单卷积器的设计 1,任务目的:2,明确设计任务2.1,目前这部分代码两个文件没找到,见第5、6节,待解决中。 ,卷积器的设计,RTL:con1.v4,前仿真和后仿真,测试信号…...
外包干了4年,技术退步太明显了。。。。。
先说一下自己的情况,本科生生,18年通过校招进入武汉某软件公司,干了接近4年的功能测试,今年国庆,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测…...
微服务实战系列之EhCache
前言 书接前文,继续深耕。上一篇博主对Redis进行了入门级介绍,大体知道了Redis可以干什么以及怎么使用它。 今日博主继续带着大家学习如何使用EhCache,这是一款基于Java的缓存框架。 微服务实战系列之Redis微服务实战系列之Cache微服务实战…...
SpringBoot:SpringMVC(上)
文章目录 前言一、SpringMVC是什么?1.1 MVC的定义:1.2 MVC 和 Spring MVC 的关系 二、Spring MVC 创建和连接2.1创建springmvc2.2接下来,创建⼀个 UserController 类,实现⽤户到 Spring 程序的互联互通,具体实现代码如…...
一文搞懂Go语言中包导入
一文搞懂Go语言中包导入 定义包 在go语言中,定义包的关键字为package,如package main等,在go语言中有一个约定俗成的标准,那就是包名与目录名把持一致。 //service目录下 package servicepackage utils 可以看到,我…...
Vue2学习笔记(事件处理)
一、基本使用 1.使用v-on:xxx 或 xxx 绑定事件,其中xxx是事件名;2.事件的回调需要配置在methods对象中,最终会在vm上;3.methods中配置的函数,不要用箭头函数!否则this就不是vm了;4.methods中配…...
【2023第十二届“认证杯”数学中国数学建模国际赛】A题 太阳黑子预报完整解题思路
A题 太阳黑子预报 题目任务思路分析第一问第二问第三问 题目 太阳黑子是太阳光球上的一种现象,表现为比周围区域更暗的临时斑点。它们是由于磁通量集中而导致表面温度降低的区域,磁通量的集中抑制了对流。太阳黑子出现在活跃区域内,通常成对…...
Huawei FusionSphere FusionCompte FusionManager
什么是FusionSphere FusionSphere 解决方案不独立发布软件,由各配套部件发布,请参 《FusionSphere_V100R005C10U1_版本配套表_01》。 目前我们主要讨论FusionManager和FusionCompute两个组件。 什么是FusionCompte FusionCompute是华为提供的虚拟化软…...
GSLB是什么?谈谈对该技术的一点理解
GSLB是什么?它又称为全局负载均衡,是主流的负载均衡类型之一。众所周知,负载均衡位于服务器的前面,负责将客户端请求路由到所有能够满足这些请求的服务器,同时最大限度地提高速度和资源利用率,并确保无任何…...
【接口测试】POST请求提交数据的三种方式及Postman实现
1. 什么是POST请求? POST请求是HTPP协议中一种常用的请求方法,它的使用场景是向客户端向服务器提交数据,比如登录、注册、添加等场景。另一种常用的请求方法是GET,它的使用场景是向服务器获取数据。 2. POST请求提交数据的常见编…...
SpringBoot系列之集成Jedis教程
SpringBoot系列之集成Jedis教程,Jedis是老牌的redis客户端框架,提供了比较齐全的redis使用命令,是一款开源的Java 客户端框架,本文使用Jedis3.1.0加上Springboot2.0,配合spring-boot-starter-data-redis使用࿰…...
怎么计算网站开发费用/深圳网站关键词排名优化
2019独角兽企业重金招聘Python工程师标准>>> 电脑系统平台:OS X EI Capitan 10.11 在以前的旧的QQ版本,QQ的截图的偏好还有一个开机自启动的选项: 现在新的版本,却没有了"开机自动运行"的选项,然…...
茂名网站建设方案外包/米拓建站
经纬度计算距离和方位角方位角(azimuthangle):从某点的指北方向线起,依顺时针方向到目标方向线之间的水平夹角,叫方位角。(一)方位角的种类由于每点都有真北、磁北和坐标纵线北三种不同的指北方向线,因此,从某点到某一…...
设计网站登录框ps怎么做/李飞seo
问题导读:1.Flume的存在些什么问题?2.基于开源的Flume美团增加了哪些功能?3.Flume系统如何调优?在《基于Flume的美团日志收集系统(一)架构和设计》中,我们详述了基于Flume的美团日志收集系统的架构设计,以及…...
做网站高流量赚广告费/北京seo优化方案
概述 什么是分库分表 数据数量是不可控的,随着时间和业务发展,造成表里面数据越来越多,如果再去对数据库表CURD操作时,就会有性能问题。 解决方案 为了解决由于数据量过大而造成数据库性能降低问题,主要有下面两种…...
网站制作文件/河北seo网络推广
Flex是Flexible Box的缩写,意为”弹性布局”,用来为盒状模型提供最大的灵活性。 任何一个容器都可以指定为Flex布局。 .box{display: flex; }行内元素也可以使用Flex布局。 .box{display: inline-flex; }Webkit内核的浏览器,必须加上-webk…...
什么身一什么网站建设/引擎网站推广法
本节书摘来自异步社区《数字短片创作(修订版)》一书中的第1章,头脑风暴获取软件功能,作者 【美】Sherri Sheridan ,译者 ACG国际动画教育,任秀静,郝佳,刘璐,更多章节内容可以访问云栖社区“异步…...