Node配合WebSocket做多文件下載以及進度回傳

  • 2019 年 11 月 7 日
  • 筆記

起因

為什麼做這個東西,是突然間聽一後端同事說起Annie這個東西,發現這個東西下載影片挺方便的,會自動爬取網頁中的影片,然後整理成列表。發現用命令執行之後是下面的樣子:

list

心裡琢磨了下,整一個介面玩一下吧。然後就做成下面這個樣子了。

列表

videolist

下載列表

downloadlist

本文地址倉庫: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研究不深,敬請諒解^_^)

下載部分細節

flow

首先瀏覽器會連接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的進度條回傳是不會出現問題的。

最後總結

多讀書,多看報,少吃零食,多睡覺???