当前位置: 首页 > article >正文

【面试题】利用Promise实现Websocket阻塞式await wsRequest() 请求

逻辑实现过程

1. 目标与基础设计

  • 目标:实现一个类似 HTTP 请求的阻塞式调用接口(如 await wsRequest(...)),让开发者无需手动处理 WebSocket 的事件回调,而是通过 Promise 和 async/await 获得同步体验。

  • 基础设计:

    • 创建一个 WebSocketClient 类,封装 WebSocket 的连接和消息处理逻辑。

    • 使用 Promise 将 WebSocket 的异步消息机制转化为请求-响应模型。

    • 为每个请求生成唯一的 requestId,用于匹配客户端请求和服务器响应。

2. WebSocket 连接管理

  • 初始化连接:

    • 在构造函数中实例化 WebSocket 对象,并绑定基本事件监听(onopen、onmessage、onerror、onclose)。

    • 维护一个 isConnected 状态,跟踪连接是否可用。

  • 事件处理:

    • onopen:标记连接成功,触发队列处理。

    • onmessage:解析收到的消息,提取 requestId 和 data,通过 requestMap 找到对应的 Promise 并完成它。

    • onerror 和 onclose:标记连接失败,清理未完成请求并尝试重连。

3. wsRequest 方法的核心逻辑

  • 请求发送:

    • 生成唯一 requestId,与请求数据组成一个对象(如 { requestId, data })。

    • 检查连接状态(isConnected 和 ws.readyState),确保请求只在连接可用时发送。

    • 将请求数据序列化为 JSON 并通过 ws.send 发送。

  • Promise 封装:

    • 创建一个 Promise,将其 resolve 函数存储到 requestMap 中,以 requestId 为键。

    • 当收到服务器响应时,通过 requestId 找到对应的 resolve 并执行。

  • 超时处理:

    • 为每个请求设置超时计时器(默认 5 秒),超时后移除 requestMap 中的记录并触发重试或失败。

4. 消息队列与重连机制

  • 消息队列:

    • 当连接不可用时,将请求(包括数据和 Promise 的 resolve/reject)存入 messageQueue。

    • 连接恢复后,逐步处理队列中的请求。

  • 重连机制:

    • 如果连接断开,通过 reconnect 方法延迟(默认 1 秒)尝试重新连接。

    • 重连成功后触发 flushQueue,处理积压的请求。

5. 队列处理的异步优化

  • 初始实现:

    • 使用 while 循环同步处理队列,可能导致主线程阻塞。

  • 优化为异步:

    • 使用 setTimeout 递归调用 flushQueue,每次处理一个请求后将控制权交还给事件循环。

    • 在请求完成后(.finally),异步触发下一次处理。

6. 错误与清理

  • 错误处理:

    • 连接错误或关闭时,通过 rejectAllPending 拒绝所有未完成请求。

    • 超时或重试超限时,返回明确的错误信息。

  • 资源清理:

    • 在超时或请求完成时清理 requestMap,避免内存泄漏。

    • 提供 close 方法,手动关闭 WebSocket 连接。

7. 最终接口

  • 用户通过 await wsClient.wsRequest({data: xxx}) 调用,获得类似 HTTP 请求的阻塞式体验。

  • 支持配置选项(如超时时间、重试次数、重试间隔),增强灵活性。


考虑的问题与解决方案

1. WebSocket 的异步特性

  • 问题:WebSocket 是事件驱动的,没有内置的请求-响应匹配机制。

  • 解决方案:

    • 使用 requestId 标识每个请求,服务器返回时带上相同的 requestId。

    • 用 Promise 封装请求,监听 onmessage 时根据 requestId 完成对应的 Promise。

2. 连接状态的不确定性

  • 问题:WebSocket 可能未连接、断开或出错,导致请求失败。

  • 解决方案:

    • 检查 readyState 和 isConnected,未连接时将请求加入队列并尝试重连。

    • 提供重试机制(默认 3 次),确保网络不稳定时仍有机会成功。

3. 请求超时的风险

  • 问题:服务器可能延迟响应,导致 Promise 无限等待。

  • 解决方案:

    • 为每个请求设置超时(默认 5 秒),超时后触发重试或失败。

    • 在 requestMap 中动态更新 resolve,确保超时后仍能正确清理。

4. 主线程阻塞

  • 问题:使用 while 处理队列可能阻塞主线程,影响页面响应性。

  • 解决方案:

    • 改为 setTimeout 异步递归处理队列,每次只处理一个请求,避免同步循环。

5. 服务器配合

  • 问题:WebSocket 本身不保证响应格式,依赖服务器正确实现。

  • 解决方案:

    • 假设服务器返回 JSON 格式的消息,包含 requestId 和 data。

    • 文档中明确说明服务器端需配合返回匹配的 requestId。

6. 资源管理与内存泄漏

  • 问题:未完成的请求可能堆积,导致内存泄漏。

  • 解决方案:

    • 在超时、连接关闭或错误时清理 requestMap。

    • 队列处理失败时移除对应项,避免无限积压。

7. 用户体验与灵活性

  • 问题:开发者需要简单易用的接口,同时支持自定义配置。

  • 解决方案:

    • 提供阻塞式接口(await wsRequest),隐藏复杂的事件逻辑。

    • 通过 options 支持配置超时、重试次数等参数。

class WebSocketClient {constructor(url, options = {}) {this.url = url;this.ws = null;this.requestMap = new Map(); // 存储 requestId 和 Promise resolverthis.messageQueue = []; // 未连接时的消息队列this.isConnected = false;this.options = {maxRetries: options.maxRetries || 3, // 默认最大重试次数retryDelay: options.retryDelay || 1000, // 默认重试间隔 1 秒timeout: options.timeout || 5000, // 默认超时 5 秒};this.connect(); // 初始化连接}// 建立 WebSocket 连接connect() {this.ws = new WebSocket(this.url);this.ws.onopen = () => {console.log('WebSocket 连接已建立');this.isConnected = true;this.flushQueue(); // 连接成功后发送队列中的消息};this.ws.onmessage = (event) => {const response = JSON.parse(event.data);const { requestId, data } = response;const resolver = this.requestMap.get(requestId);if (resolver) {resolver(data);this.requestMap.delete(requestId);}};this.ws.onerror = (error) => {console.error('WebSocket 错误:', error);this.isConnected = false;};this.ws.onclose = () => {console.log('WebSocket 连接已关闭');this.isConnected = false;this.rejectAllPending('WebSocket 连接关闭');};}// 发送请求并返回 PromisewsRequest(data, retryCount = 0) {return new Promise((resolve, reject) => {// 生成唯一 requestIdconst requestId = Date.now() + Math.random().toString(36).slice(2);const request = { requestId, data };// 检查连接状态if (!this.isConnected || this.ws.readyState !== WebSocket.OPEN) {if (retryCount < this.options.maxRetries) {console.log(`WebSocket 未连接,第 ${retryCount + 1} 次重试...`);this.messageQueue.push({ request, resolve, reject, retryCount: retryCount + 1 });this.reconnect();return;}return reject(new Error('WebSocket 未连接且重试次数已达上限'));}// 存储 resolverthis.requestMap.set(requestId, resolve);// 发送请求this.ws.send(JSON.stringify(request));// 设置超时const timeoutId = setTimeout(() => {if (this.requestMap.has(requestId)) {if (retryCount < this.options.maxRetries) {console.log(`请求超时,第 ${retryCount + 1} 次重试...`);this.requestMap.delete(requestId);this.wsRequest(data, retryCount + 1).then(resolve).catch(reject);} else {reject(new Error('请求超时且重试次数已达上限'));this.requestMap.delete(requestId);}}}, this.options.timeout);// 清理超时时绑定this.requestMap.set(requestId, (value) => {clearTimeout(timeoutId);resolve(value);});});}// 重连逻辑reconnect() {if (!this.isConnected) {setTimeout(() => {console.log('尝试重连...');this.connect();}, this.options.retryDelay);}}// 使用 setTimeout 异步处理消息队列flushQueue() {if (!this.isConnected || this.messageQueue.length === 0) return;const { request, resolve, reject, retryCount } = this.messageQueue.shift();this.wsRequest(request.data, retryCount).then(resolve).catch(reject).finally(() => {// 递归调用,确保队列逐步处理setTimeout(() => this.flushQueue(), 0);});}// 拒绝所有未完成的请求rejectAllPending(errorMessage) {this.requestMap.forEach((resolver, requestId) => {resolver(Promise.reject(new Error(errorMessage)));this.requestMap.delete(requestId);});}// 关闭连接close() {if (this.ws) {this.ws.close();}}
}// 使用示例
async function testWebSocket() {const wsClient = new WebSocketClient('ws://example.com/socket', {maxRetries: 3,retryDelay: 1000,timeout: 5000,});try {// 发送阻塞式请求const response = await wsClient.wsRequest({ action: 'getData', value: 'xxx' });console.log('收到响应:', response);// 模拟未连接时发送请求wsClient.ws.close(); // 手动关闭连接以测试重试和队列const response2 = await wsClient.wsRequest({ action: 'update', value: 'yyy' });console.log('收到响应:', response2);} catch (error) {console.error('请求失败:', error);} finally {wsClient.close();}
}testWebSocket();

相关文章:

【面试题】利用Promise实现Websocket阻塞式await wsRequest() 请求

逻辑实现过程 1. 目标与基础设计 目标&#xff1a;实现一个类似 HTTP 请求的阻塞式调用接口&#xff08;如 await wsRequest(...)&#xff09;&#xff0c;让开发者无需手动处理 WebSocket 的事件回调&#xff0c;而是通过 Promise 和 async/await 获得同步体验。 基础设计&a…...

数据库----单表、多表

数据库 create database 数据库名称;---创建数据库create database 数据库名称 default charsetutf8mb4;---创建数据库&#xff0c;同时指定编码show databases;---查看当前数据库管理下存在多少数据库show databases like "db_%";---查询以db_开头的数据库select d…...

ubuntu 22.04 一键安装 lxd

LXD系列 LXD是一个现代、安全且功能强大的系统容器和虚拟机管理器。 它为在容器或虚拟机中运行和管理完整的 Linux 系统提供了统一的体验。LXD 支持大量 Linux 发行版的映像(官方 Ubuntu 映像和社区提供的映像),并且围绕...

HO与OH差异之Navigation三

在上一篇内容中我们介绍了HO与OH差异之Navigator&#xff0c;我们也了解了Navigator的基本概念和大致了解了一下他的基础用法&#xff0c;既然谈到差异肯定就不止这两种差异&#xff0c;今天就让我们来了解第三种差异NavRouter&#xff0c;其中在HO中我们并没有这种路由方式但是…...

Zookeeper运维指南:服务端与客户端常用命令详解

#作者&#xff1a;任少近 文章目录 1 Zookeeper服务端常用命令2 Zookeeper客户端常用命令2.1Ls命令2.2创建节点create2.3Get命令2.4删除命令2.5修改命令 1 Zookeeper服务端常用命令 启动ZK服务: bin/zkServer.sh start # ./zkServer.sh startZooKeeper JMX enabled by defau…...

linux scp复制多层级文件夹到另一服务器免密及脚本配置

文章目录 生成 SSH 密钥对将公钥复制到目标服务器验证免密登录scp 多级文件夹复制脚本 生成 SSH 密钥对 在本地机器上&#xff0c;使用 ssh-keygen 命令生成 SSH 密钥对。打开终端并执行以下命令&#xff1a; ssh-keygen -t rsa 按提示连续按回车键&#xff0c;默认会在 ~/.ss…...

模型压缩与迁移:基于蒸馏技术的实战教程

1.前言 模型蒸馏&#xff08;Model Distillation&#xff09;&#xff0c;又称为知识蒸馏&#xff08;Knowledge Distillation&#xff09;&#xff0c;是一种将大型、复杂的模型&#xff08;通常称为教师模型&#xff0c;Teacher Model&#xff09;的知识转移到小型、简单模型…...

XSS通关技巧

目录 第一关&#xff1a; 第二关&#xff1a; 第三关&#xff1a; 第四关&#xff1a; 第五关&#xff1a; 第六关&#xff1a; 第七关&#xff1a; 第八关&#xff1a; 第九关&#xff1a; 第十关&#xff1a; 第十一关&#xff1a; 第十二关&#xff1a; 第十三关&#xff1a…...

el-tree树多选,将选中的树对象中某个字段值改为true,并过滤出所有为true的对象,组成新的数组

功能实现&#xff1a; el-tree树多选&#xff0c;将选中的树对象中某个字段值改为true,并过滤出所有为true的对象&#xff0c;组成新的数组提交给后端 <template><div><!-- 树形菜单 --><el-tree:data"stageList"show-checkboxdefault-expand-…...

大文件版本管理git-lfs

1. 安装 Git Large File Storage (LFS) 是一个 开源的 Git 扩展&#xff0c;用于替换 Git 仓库中的大文件&#xff0c;用指针文件替代实际的大文件&#xff0c;可以在保持仓库轻量级的同时&#xff0c;有效地管理大型文件。 如果install提示失败&#xff0c;多试几次&#xf…...

Android RemoteViews:跨进程 UI 更新的奥秘与实践

目录 一、RemoteViews 的舞台:使用场景 (一)通知栏:动态交互的窗口 (二)桌面小部件:桌面上的动态名片 二、RemoteViews 的本质:定义与架构 (一)什么是 RemoteViews? (二)架构设计:层次分明的协作 (三)操作限制:能力边界在哪里? 三、RemoteViews 的引擎…...

es 3期 第27节-运用Script脚本实现复杂需求

#### 1.Elasticsearch是数据库&#xff0c;不是普通的Java应用程序&#xff0c;传统数据库需要的硬件资源同样需要&#xff0c;提升性能最有效的就是升级硬件。 #### 2.Elasticsearch是文档型数据库&#xff0c;不是关系型数据库&#xff0c;不具备严格的ACID事务特性&#xff…...

玩转python: 掌握Python常用库之数据分析pandas

pandas是Python中最强大的数据处理和分析库之一&#xff0c;它提供了高效、灵活且易于使用的数据结构&#xff0c;使数据清洗、转换和分析变得简单直观。 1、pandas库简介 pandas由Wes McKinney于2008年开发&#xff0c;名称来源于"panel data"&#xff08;面板数据…...

融合YOLO11与行为树的人机协作智能框架:动态工效学优化与自适应安全决策

人工智能技术要真正发挥其价值&#xff0c;必须与生产生活深度融合&#xff0c;为产业发展和人类生活带来实际效益。近年来&#xff0c;基于深度学习的机器视觉技术在工业自动化领域取得了显著进展&#xff0c;其中YOLO&#xff08;You Only Look Once&#xff09;算法作为一种…...

Postgresql源码(142)子查询提升pull_up_sublinks

1 案例 drop table t_fun01; create table t_fun01 (image_id numeric primary key, content_id varchar(50), file_code varchar(20)); create index idx3 on t_fun01(content_id); create index idx4 on t_fun01(file_code); insert into t_fun01 select t.i, t.i%10, t.i%1…...

通过 ECNWP 洋流、海浪可视化开发的方法和架构

第一篇:ECNWP 洋流、海浪可视化开发方法 1. 数据获取与预处理 ECNWP(Eastern Central North Pacific)区域的洋流和海浪数据通常来源于 NOAA、EMC 等权威机构。数据包括流速、流向、波高、波向、周期等。 代码示例:数据下载与预处理 import pandas as pd import numpy as…...

如何 编译 px4

DeepSeek-R1满血版 回答完成 深度思考完成 用时27秒&#xff0c;搜索全网29篇资料 以下是详细的PX4固件编译教程&#xff0c;综合多平台及常见问题解决方案&#xff1a; 一、环境准备 ‌系统要求‌ ‌Ubuntu 20.04/22.04‌&#xff08;推荐&#xff09;或 ‌MacOS 12‌ Windo…...

23种设计模式-桥接(Bridge)设计模式

桥接设计模式 &#x1f6a9;什么是桥接设计模式&#xff1f;&#x1f6a9;桥接设计模式的特点&#x1f6a9;桥接设计模式的结构&#x1f6a9;桥接设计模式的优缺点&#x1f6a9;桥接设计模式的Java实现&#x1f6a9;代码总结&#x1f6a9;总结 &#x1f6a9;什么是桥接设计模式…...

【黑皮书】 AVL树

目录 前言 一 AVL树的介绍 二 单旋转 二 双旋转 总结 前言 AVL树的学习 一 AVL树的介绍 AVL树是带有平衡条件的二叉查找树&#xff0c;这个平衡条件要持续保持&#xff0c;而且必须保证树的深度为O(log(N))最简单的想法就是要求左右子树具有相同的高度 一棵AVL树是…...

【机器学习】什么是决策树?

什么是决策树&#xff1f; 决策树是一种用于分类和回归问题的模型。它通过一系列的“决策”将数据逐步分裂&#xff0c;最终得出预测结果。可以把它看作是一个“树”&#xff0c;每个节点表示一个特征的判断&#xff0c;而每个分支代表了可能的判断结果&#xff0c;最终的叶子…...

【商城实战(74)】数据采集与整理,夯实电商运营基石

【商城实战】专栏重磅来袭&#xff01;这是一份专为开发者与电商从业者打造的超详细指南。从项目基础搭建&#xff0c;运用 uniapp、Element Plus、SpringBoot 搭建商城框架&#xff0c;到用户、商品、订单等核心模块开发&#xff0c;再到性能优化、安全加固、多端适配&#xf…...

使用独立服务器的最佳方式指南

在寻找合适的主机服务方案时&#xff0c;可以考虑独立服务器&#xff0c;因为它拥有管理员权限以及更高的性能配置。在本指南中&#xff0c;我们将介绍独立服务器的多种用途&#xff0c;并分析为什么选择独立服务器可能是处理高性能、资源密集型应用和大流量网站的最佳方案。 搭…...

视频格式转换:畅享多平台无缝视频体验

视频格式转换&#xff1a;畅享多平台无缝视频体验 视频已成为我们日常生活中不可或缺的一部分&#xff0c;不论是工作中展示方案的演示&#xff0c;还是生活里记录美好瞬间的短片&#xff0c;视频的存在无处不在。然而&#xff0c;面对各类设备、平台对视频格式的不同要求&…...

【HTML 基础教程】HTML 属性

HTML 属性 属性是 HTML 元素提供的附加信息。 属性通常出现在 HTML 标签的开始标签中&#xff0c;用于定义元素的行为、样式、内容或其他特性。 属性总是以 name"value" 的形式写在标签内&#xff0c;name 是属性的名称&#xff0c;value 是属性的值。 HTML 属性 …...

爬虫问题整理(2025.3.27)

此时此刻&#xff0c;困扰我一天的两个问题终于得到了解决&#xff0c;在此分享给大家。 问题1&#xff1a;使用anaconda prompt无法进行pip安装&#xff0c;这里只是一个示例&#xff0c;实际安装任何模块都会出现类似报错。 解决办法&#xff1a;关掉梯子......没错&#xf…...

短信验证码安全需求设计

背景&#xff1a; 近期发现部分系统再短信充值频繁&#xff0c;发现存在恶意消耗短信额度现象&#xff0c;数据库表排查&#xff0c;发现大量非合法用户非法调用短信接口API导致额度耗尽。由于系统当初设计存在安全缺陷&#xff0c;故被不法分子进行利用&#xff0c;造成损失。…...

若依专题——基础应用篇

若依搭建 搭建后端项目 ① Git 克隆并初始化项目 ② MySQL 导入与配置 ③ 启动 Redis 搭建后端项目注意事项&#xff1f; ① 项目初始化慢&#xff0c;执行clean、package ② MySQL导入后&#xff0c;修改application-druid.yml ③ Redis有密码&#xff0c;修改ap…...

给AI装“记忆U盘“:LangChain记忆持久化入门指南

&#x1f9e0; 什么是记忆持久化&#xff1f; 想象AI对话就像和朋友聊天&#xff1a; ​普通模式&#xff1a;每次重启都忘记之前聊过什么​持久化模式&#xff1a;给AI配了个"记忆U盘"&#xff0c;聊天记录永不丢失 核心组件三件套 #mermaid-svg-ORm8cbBXsaRy2sZ…...

AI for CFD入门指南(传承版)

AI for CFD入门指南 前言适用对象核心目标基础准备传承机制 AI for CFDLibtorch的介绍与使用方法PytorchAutogluon MakefileVscodeOpenFOAMParaviewGambit 前言 适用对象 新加入课题组的硕士/博士研究生对AICFD交叉领域感兴趣的本科生实习生需要快速上手组内研究工具的合作研…...

DeepSeek+RAG局域网部署

已经有很多平台集成RAG模式&#xff0c;dify&#xff0c;cherrystudio等&#xff0c;这里通过AI辅助&#xff0c;用DS的API实现一个简单的RAG部署。框架主要技术栈是Chroma,langchain,streamlit&#xff0c;答案流式输出&#xff0c;并且对答案加上索引。支持doc,docx,pdf,txt。…...