欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-10-29 11:25:22
起因 为什么做这个东西,是突然间听一后端同事说起 "Annie" 这个东西,发现这个东西下载视频挺方便的,会自动爬取网页中的视频,然后整理成列表。发现用命令执行之后是下面的样子: 心里琢磨了下,整一个界面玩一下吧。然后就做成下面这个样子了。 列表 下载列表 本文地址仓库: "https://gith ......

起因

为什么做这个东西,是突然间听一后端同事说起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(在代码中由parentidchildid组成)和客户端生成的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的进度条回传是不会出现问题的。

最后总结

多读书,多看报,少吃零食,多睡觉