Node配合WebSocket做多文件下载以及进度回传


Posted in Javascript onNovember 07, 2019

起因

为什么做这个东西,是突然间听一后端同事说起Annie这个东西,发现这个东西下载视频挺方便的,会自动爬取网页中的视频,然后整理成列表。发现用命令执行之后是下面的样子:

Node配合WebSocket做多文件下载以及进度回传

心里琢磨了下,整一个界面玩一下吧。然后就做成下面这个样子了。

列表

Node配合WebSocket做多文件下载以及进度回传

下载列表

Node配合WebSocket做多文件下载以及进度回传

本文地址仓库:https://github.com/Rynxiao/yh-tools,如果喜欢,欢迎star.

涉及技术

  • Express 后端服务
  • Webpack 模块化编译工具
  • Nginx 主要做文件gzip压缩(发现Express添加gzip有点问题,才弃坑nginx)
  • Ant-design 前端UI库
  • React + React Router
  • WebSocket 进度回传服务

其中还有点小插曲,最开始是使用docker起了一个nginx服务,但是发现内部转发一直有问题,同时获取宿主主机IP也出现了点问题,然后折磨了好久放弃了。(docker研究不深,敬请谅解^_^)

下载部分细节

Node配合WebSocket做多文件下载以及进度回传

首先浏览器会连接WebSocket服务器,同时在WebSocket服务器上存在一个所有客户端的Map,浏览器端生成一个uuid作为浏览器客户端id,然后将这个链接作为值存进Map中。

客户端:

// list.jsx
await WebSocketClient.connect((event) => {
 const data = JSON.parse(event.data);
 if (data.event === 'close') {
  this.updateCloseStatusOfProgressBar(list, data);
 } else {
  this.generateProgressBarList(list, data);
 }
});
// src/utils/websocket.client.js
async connect(onmessage, onerror) {
 const socket = this.getSocket();
 return new Promise((resolve) => {
  // ...
 });
}
getSocket() {
 if (!this.socket) {
  this.socket = new WebSocket(
   `ws://localhost:${CONFIG.PORT}?from=client&id=${clientId}`,
   'echo-protocol',
  );
 }
 return this.socket;
}

服务端:

// public/javascript/websocket/websocket.server.js
connectToServer(httpServer) {
 initWsServer(httpServer);
 wsServer.on('request', (request) => {
  // uri: ws://localhost:8888?from=client&id=xxxx-xxxx-xxxx-xxxx
  logger.info('[ws server] request');
  const connection = request.accept('echo-protocol', request.origin);
  const queryStrings = querystring.parse(request.resource.replace(/(^\/|\?)/g, ''));
  
  // 每有连接连到websocket服务器,就将当前连接保存到map中
  setConnectionToMap(connection, queryStrings);
  connection.on('message', onMessage);
  connection.on('close', (reasonCode, description) => {
   logger.info(`[ws server] connection closed ${reasonCode} ${description}`);
  });
 });

 wsServer.on('close', (connection, reason, description) => {
  logger.info('[ws server] some connection disconnect.');
  logger.info(reason, description);
 });
}

然后在浏览器端点击下载的时候,会传递两个主要的字段resourceId(在代码中由parentId和childId组成)和客户端生成的bClientId。这两个id有什么用呢?

每次点击下载,都会在Web服务器中生成一个WebSocket的客户端,那么这个resouceId就是作为在服务器中生成的WebSocket服务器的key值。

bClientId主要是为了区分浏览器的客户端,因为考虑到同时可能会有多个浏览器接入,这样在WebSocket服务器中产生消息的时候,就可以用这个id来区分应该发送给哪个浏览器客户端

客户端:

// list.jsx
http.get(
 'download',
 {
  code,
  filename,
  parent_id: row.id,
  child_id: childId,
  download_url: url,
  client_id: clientId,
 },
);
// routes/api.js
router.get('/download', async (req, res) => {
 const { code, filename } = req.query;
 const url = req.query.download_url;
 const clientId = req.query.client_id;
 const parentId = req.query.parent_id;
 const childId = req.query.child_id;
 const connectionId = `${parentId}-${childId}`;
 const params = {
  code,
  url,
  filename,
  parent_id: parentId,
  child_id: childId,
  client_id: clientId,
 };
 const flag = await AnnieDownloader.download(connectionId, params);
 if (flag) {
  await res.json({ code: 200 });
 } else {
  await res.json({ code: 500, msg: 'download error' });
 }
});
// public/javascript/annie.js
async download(connectionId, params) {
  //...
 // 当annie下载时,会进行数据监听,这里会用到节流,防止进度回传太快,websocket服务器无法反应
 downloadProcess.stdout.on('data', throttle((chunk) => {
  try {
   if (!chunk) {
    isDownloading = false;
   }
   // 这里主要做的是解析数据,然后发送进度和速度等信息给websocket服务器
   getDownloadInfo(chunk, ws, params);
  } catch (e) {
   downloadSuccess = false;
   WsClient.close(params.client_id, connectionId, 'download error');
   this.stop(connectionId);
   logger.error(`[server annie download] error: ${e}`);
  }
 }, 500, 300));
}

服务端收到进度以及速度的消息后,回传给客户端,如果进度达到了100%,那么就删除掉存在server中的服务器中起的websocket的客户端,并且发送一个客户端被关闭的通知,通知浏览器已经下载完成。

// public/javascript/websocket/websocket.server.js
function onMessage(message) {
 const data = JSON.parse(message.utf8Data);
 const id = data.client_id;
 if (data.event === 'close') {
  logger.info('[ws server] close event');
  closeConnection(id, data);
 } else {
  getConnectionAndSendProgressToClient(data, id);
 }
}
function getConnectionAndSendProgressToClient(data, clientId) {
 const browserClient = clientsMap.get(clientId);
 // logger.info(`[ws server] send ${JSON.stringify(data)} to client ${clientId}`);
 if (browserClient) {
  const serverClientId = `${data.parent_id}-${data.child_id}`;
  const serverClient = clientsMap.get(serverClientId);
  // 发送从web服务器中传过来的进度、速度给浏览器
  browserClient.send(JSON.stringify(data));
  // 如果进度已经达到了100%
  if (data.progress >= 100) {
   logger.info(`[ws server] file has been download successfully, progress is ${data.progress}`);
   logger.info(`[ws server] server client ${serverClientId} ready to disconnect`);
   // 从clientsMap将当前的这个由web服务器创建的websocket客户端移除
   // 然后关闭当前连接
   // 同时发送下载完成的消息给浏览器
   clientsMap.delete(serverClientId);
   serverClient.send(JSON.stringify({ connectionId: serverClientId, event: 'complete' }));
   serverClient.close('download completed');
  }
 }
}

整体来说就这么多,有一点需要指出,annie在解析的时候有时候可能消息处理不是很稳定,导致我数据解析的时候出现了一些问题,但是我用mock的数据以及mock的进度条回传是不会出现问题的。

Javascript 相关文章推荐
js 文件引入实现代码
Apr 23 Javascript
javascript字符串拼接的效率问题
Dec 25 Javascript
输入框的字数时时统计—关于 onpropertychange 和 oninput 使用
Oct 21 Javascript
JQuery对ASP.NET MVC数据进行更新删除
Jul 13 Javascript
JavaScript使用原型和原型链实现对象继承的方法详解
Apr 05 Javascript
浅谈Angular4实现热加载开发旅程
Sep 08 Javascript
vue 将页面公用的头部组件化的方法
Dec 18 Javascript
JS动画定时器知识总结
Mar 23 Javascript
vue里面v-bind和Props 利用props绑定动态数据的方法
Aug 27 Javascript
vue 内置过滤器的使用总结(附加自定义过滤器)
Dec 11 Javascript
Three.js实现简单3D房间布局
Dec 30 Javascript
解决element ui select下拉框不回显数据问题的解决
Feb 20 Javascript
vue 实现单选框设置默认选中值
Nov 07 #Javascript
js使用文档就绪函数动态改变页面内容示例【innerHTML、innerText】
Nov 07 #Javascript
vue获取data数据改变前后的值方法
Nov 07 #Javascript
使用JS监听键盘按下事件(keydown event)
Nov 07 #Javascript
vue.js循环radio的实例
Nov 07 #Javascript
vue 解决遍历对象显示的顺序不对问题
Nov 07 #Javascript
vue遍历对象中的数组取值示例
Nov 07 #Javascript
You might like
基于mysql的bbs设计(三)
2006/10/09 PHP
PHP生成静态页面详解
2006/12/05 PHP
PHP源码之 ext/mysql扩展部分
2009/07/17 PHP
php模板中出现空行解决方法
2011/03/08 PHP
php 多关键字 高亮显示实现代码
2012/04/23 PHP
使用php验证复选框有效性的示例
2013/11/13 PHP
Linux操作系统安装LAMP环境
2015/06/26 PHP
PHP5.5迭代生成器用法实例详解
2016/03/16 PHP
Laravel与CI框架中截取字符串函数
2016/05/08 PHP
php similar_text()函数的定义和用法
2016/05/12 PHP
Javascript模块模式分析
2008/05/16 Javascript
用JS判别浏览器种类以及IE版本的几种方法小结
2011/08/02 Javascript
jQuery获取对象简单实现方法小结
2014/10/30 Javascript
javaScript中push函数用法实例分析
2015/06/08 Javascript
全面解析JavaScript的Backbone.js框架中的Router路由
2016/05/05 Javascript
jQuery实现别踩白块儿网页版小游戏
2017/01/18 Javascript
mpvue+vuex搭建小程序详细教程(完整步骤)
2018/09/30 Javascript
js取小数点后两位四种方法
2019/01/18 Javascript
vue使用axios上传文件(FormData)的方法
2019/04/14 Javascript
[02:52]DOTA2新手基础教程 米波
2014/01/21 DOTA
python2使用bs4爬取腾讯社招过程解析
2019/08/14 Python
python计算n的阶乘的方法代码
2019/10/25 Python
Python文件操作方法详解
2020/02/09 Python
Python实现Excel自动分组合并单元格
2021/02/22 Python
基于css3仿造window7的开始菜单
2010/06/17 HTML / CSS
CSS3 Flexbox中flex-shrink属性的用法示例介绍
2013/12/30 HTML / CSS
CSS3制作文字半透明倒影效果的两种实现方式
2014/08/08 HTML / CSS
台湾母婴用品购物网站:Infant婴之房
2018/06/15 全球购物
教师节促销方案
2014/03/22 职场文书
小学生优秀评语大全
2014/04/22 职场文书
人事行政部各岗位职责说明书!
2019/07/15 职场文书
关于vue中如何监听数组变化
2021/04/28 Vue.js
雄兵连:第三季先行图公开,天使恶魔联合,银河之力的新力量
2021/06/11 国漫
JavaScript严格模式不支持八进制的问题讲解
2021/11/07 Javascript
HTML CSS 一个标签实现带动画的抖音LOGO
2022/04/26 HTML / CSS
kubernetes集群搭建Zabbix监控平台的详细过程
2022/07/07 Servers