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 相关文章推荐
避免 showModalDialog 弹出新窗体的原因分析
May 31 Javascript
jqgrid 编辑添加功能详细解析
Nov 08 Javascript
node.js中的path.isAbsolute方法使用说明
Dec 08 Javascript
javascript实现页面刷新时自动清空表单并选中的方法
Jul 18 Javascript
JS组件Bootstrap Table表格多行拖拽效果实现代码
Dec 08 Javascript
浅析JavaScript动画模拟拖拽原理
Dec 09 Javascript
微信JSAPI Ticket接口签名详解
Jun 28 Javascript
jquery select插件异步实时搜索实例代码
Oct 20 jQuery
浅谈AngularJs 双向绑定原理(数据绑定机制)
Dec 07 Javascript
jQuery实现鼠标响应式透明度渐变动画效果示例
Feb 13 jQuery
layui复选框限制选择个数的方法
Sep 18 Javascript
jquery实现弹窗(系统提示框)效果
Dec 10 jQuery
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
php的$_FILES的临时储存文件与回收机制实测过程
2013/07/12 PHP
PHP使用SOAP调用.net的WebService数据
2013/11/12 PHP
Yii2分页的使用及其扩展方法详解
2016/05/23 PHP
ThinkPHP中获取指定日期后工作日的具体日期方法
2018/10/14 PHP
jQuery表格行换色的三种实现方法
2011/06/27 Javascript
jQuery操作CheckBox的方法介绍(选中,取消,取值)
2014/02/04 Javascript
js校验表单后提交表单的三种方法总结
2014/02/28 Javascript
兼容Firefox的Javascript XSLT 处理XML文件
2014/12/31 Javascript
javascript中一些util方法汇总
2015/06/10 Javascript
基于jQuery通过jQuery.form.js插件使用ajax提交form表单
2015/08/17 Javascript
基于Flowplayer打造一款免费的WEB视频播放器附源码
2015/09/06 Javascript
老生常谈ES6中的类
2017/07/31 Javascript
通过js控制时间,一秒一秒自己动的实例
2017/10/25 Javascript
微信小程序rich-text富文本用法实例分析
2019/05/20 Javascript
nodejs和react实现即时通讯简易聊天室功能
2019/08/21 NodeJs
详解JavaScript 的执行机制
2020/09/18 Javascript
python显示天气预报
2014/03/02 Python
用ReactJS和Python的Flask框架编写留言板的代码示例
2015/12/19 Python
Python过滤列表用法实例分析
2016/04/29 Python
python安装cx_Oracle模块常见问题与解决方法
2017/02/21 Python
Python实现简单的获取图片爬虫功能示例
2017/07/12 Python
基于Python socket的端口扫描程序实例代码
2018/02/09 Python
Python Pywavelet 小波阈值实例
2019/01/09 Python
Python3.7.0 Shell添加清屏快捷键的实现示例
2020/03/23 Python
Python调用ffmpeg开源视频处理库,批量处理视频
2020/11/16 Python
马来西亚最大的电器网站:Senheng
2017/10/13 全球购物
Clarks其乐鞋荷兰官网:Clarks荷兰
2019/07/05 全球购物
方法名是否可以与构造器的名字相同
2012/06/04 面试题
质量月活动策划方案
2014/03/10 职场文书
建筑工地宣传标语
2014/06/18 职场文书
群众路线剖析材料范文
2014/10/09 职场文书
乡镇党的群众路线教育实践活动个人整改方案
2014/10/31 职场文书
优化经济发展环境工作总结
2015/08/11 职场文书
2016年读书月活动总结范文
2016/04/06 职场文书
代码解析React中setState同步和异步问题
2021/06/03 Javascript
分享MySQL常用 内核 Debug 几种常见方法
2022/03/17 MySQL