Node配合WebSocket做多文件下载以及进度回传
起因
为什么做这个东西,是突然间听一后端同事说起Annie这个东西,发现这个东西下载视频挺方便的,会自动爬取网页中的视频,然后整理成列表。发现用命令执行之后是下面的样子:
心里琢磨了下,整一个界面玩一下吧。然后就做成下面这个样子了。
列表
下载列表
本文地址仓库:https://github.com/Rynxiao/yh-tools,如果喜欢,欢迎star.
涉及技术
- Express后端服务
- Webpack模块化编译工具
- Nginx主要做文件gzip压缩(发现Express添加gzip有点问题,才弃坑nginx)
- Ant-design前端UI库
- React+ReactRouter
- WebSocket进度回传服务
其中还有点小插曲,最开始是使用docker起了一个nginx服务,但是发现内部转发一直有问题,同时获取宿主主机IP也出现了点问题,然后折磨了好久放弃了。(docker研究不深,敬请谅解^_^)
下载部分细节
首先浏览器会连接WebSocket服务器,同时在WebSocket服务器上存在一个所有客户端的Map,浏览器端生成一个uuid作为浏览器客户端id,然后将这个链接作为值存进Map中。
客户端:
//list.jsx
awaitWebSocketClient.connect((event)=>{
constdata=JSON.parse(event.data);
if(data.event==='close'){
this.updateCloseStatusOfProgressBar(list,data);
}else{
this.generateProgressBarList(list,data);
}
});
//src/utils/websocket.client.js
asyncconnect(onmessage,onerror){
constsocket=this.getSocket();
returnnewPromise((resolve)=>{
//...
});
}
getSocket(){
if(!this.socket){
this.socket=newWebSocket(
`ws://localhost:${CONFIG.PORT}?from=client&id=${clientId}`,
'echo-protocol',
);
}
returnthis.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('[wsserver]request');
constconnection=request.accept('echo-protocol',request.origin);
constqueryStrings=querystring.parse(request.resource.replace(/(^\/|\?)/g,''));
//每有连接连到websocket服务器,就将当前连接保存到map中
setConnectionToMap(connection,queryStrings);
connection.on('message',onMessage);
connection.on('close',(reasonCode,description)=>{
logger.info(`[wsserver]connectionclosed${reasonCode}${description}`);
});
});
wsServer.on('close',(connection,reason,description)=>{
logger.info('[wsserver]someconnectiondisconnect.');
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;
consturl=req.query.download_url;
constclientId=req.query.client_id;
constparentId=req.query.parent_id;
constchildId=req.query.child_id;
constconnectionId=`${parentId}-${childId}`;
constparams={
code,
url,
filename,
parent_id:parentId,
child_id:childId,
client_id:clientId,
};
constflag=awaitAnnieDownloader.download(connectionId,params);
if(flag){
awaitres.json({code:200});
}else{
awaitres.json({code:500,msg:'downloaderror'});
}
});
//public/javascript/annie.js
asyncdownload(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,'downloaderror');
this.stop(connectionId);
logger.error(`[serveranniedownload]error:${e}`);
}
},500,300));
}
服务端收到进度以及速度的消息后,回传给客户端,如果进度达到了100%,那么就删除掉存在server中的服务器中起的websocket的客户端,并且发送一个客户端被关闭的通知,通知浏览器已经下载完成。
//public/javascript/websocket/websocket.server.js
functiononMessage(message){
constdata=JSON.parse(message.utf8Data);
constid=data.client_id;
if(data.event==='close'){
logger.info('[wsserver]closeevent');
closeConnection(id,data);
}else{
getConnectionAndSendProgressToClient(data,id);
}
}
functiongetConnectionAndSendProgressToClient(data,clientId){
constbrowserClient=clientsMap.get(clientId);
//logger.info(`[wsserver]send${JSON.stringify(data)}toclient${clientId}`);
if(browserClient){
constserverClientId=`${data.parent_id}-${data.child_id}`;
constserverClient=clientsMap.get(serverClientId);
//发送从web服务器中传过来的进度、速度给浏览器
browserClient.send(JSON.stringify(data));
//如果进度已经达到了100%
if(data.progress>=100){
logger.info(`[wsserver]filehasbeendownloadsuccessfully,progressis${data.progress}`);
logger.info(`[wsserver]serverclient${serverClientId}readytodisconnect`);
//从clientsMap将当前的这个由web服务器创建的websocket客户端移除
//然后关闭当前连接
//同时发送下载完成的消息给浏览器
clientsMap.delete(serverClientId);
serverClient.send(JSON.stringify({connectionId:serverClientId,event:'complete'}));
serverClient.close('downloadcompleted');
}
}
}
整体来说就这么多,有一点需要指出,annie在解析的时候有时候可能消息处理不是很稳定,导致我数据解析的时候出现了一些问题,但是我用mock的数据以及mock的进度条回传是不会出现问题的。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。