Node配合WebSocket做多文件下载以及进度回传
程序员文章站
2023-10-29 11:25:22
起因 为什么做这个东西,是突然间听一后端同事说起 "Annie" 这个东西,发现这个东西下载视频挺方便的,会自动爬取网页中的视频,然后整理成列表。发现用命令执行之后是下面的样子: 心里琢磨了下,整一个界面玩一下吧。然后就做成下面这个样子了。 列表 下载列表 本文地址仓库: "https://gith ......
起因
为什么做这个东西,是突然间听一后端同事说起annie这个东西,发现这个东西下载视频挺方便的,会自动爬取网页中的视频,然后整理成列表。发现用命令执行之后是下面的样子:
心里琢磨了下,整一个界面玩一下吧。然后就做成下面这个样子了。
列表
下载列表
本文地址仓库: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
研究不深,敬请谅解^_^)
下载部分细节
首先浏览器会连接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
的进度条回传是不会出现问题的。
最后总结
多读书,多看报,少吃零食,多睡觉