Node配合WebSocket做多文件下載以及進(jìn)度回傳
起因
為什么做這個(gè)東西,是突然間聽一后端同事說(shuō)起Annie這個(gè)東西,發(fā)現(xiàn)這個(gè)東西下載視頻挺方便的,會(huì)自動(dòng)爬取網(wǎng)頁(yè)中的視頻,然后整理成列表。發(fā)現(xiàn)用命令執(zhí)行之后是下面的樣子:

心里琢磨了下,整一個(gè)界面玩一下吧。然后就做成下面這個(gè)樣子了。
列表

下載列表

本文地址倉(cāng)庫(kù):https://github.com/Rynxiao/yh-tools,如果喜歡,歡迎star.
涉及技術(shù)
- Express 后端服務(wù)
- Webpack 模塊化編譯工具
- Nginx 主要做文件gzip壓縮(發(fā)現(xiàn)Express添加gzip有點(diǎn)問(wèn)題,才棄坑nginx)
- Ant-design 前端UI庫(kù)
- React + React Router
- WebSocket 進(jìn)度回傳服務(wù)
其中還有點(diǎn)小插曲,最開始是使用docker起了一個(gè)nginx服務(wù),但是發(fā)現(xiàn)內(nèi)部轉(zhuǎn)發(fā)一直有問(wèn)題,同時(shí)獲取宿主主機(jī)IP也出現(xiàn)了點(diǎn)問(wèn)題,然后折磨了好久放棄了。(docker研究不深,敬請(qǐng)諒解^_^)
下載部分細(xì)節(jié)

首先瀏覽器會(huì)連接WebSocket服務(wù)器,同時(shí)在WebSocket服務(wù)器上存在一個(gè)所有客戶端的Map,瀏覽器端生成一個(gè)uuid作為瀏覽器客戶端id,然后將這個(gè)鏈接作為值存進(jìn)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;
}
服務(wù)端:
// 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服務(wù)器,就將當(dāng)前連接保存到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);
});
}
然后在瀏覽器端點(diǎn)擊下載的時(shí)候,會(huì)傳遞兩個(gè)主要的字段resourceId(在代碼中由parentId和childId組成)和客戶端生成的bClientId。這兩個(gè)id有什么用呢?
每次點(diǎn)擊下載,都會(huì)在Web服務(wù)器中生成一個(gè)WebSocket的客戶端,那么這個(gè)resouceId就是作為在服務(wù)器中生成的WebSocket服務(wù)器的key值。
bClientId主要是為了區(qū)分瀏覽器的客戶端,因?yàn)榭紤]到同時(shí)可能會(huì)有多個(gè)瀏覽器接入,這樣在WebSocket服務(wù)器中產(chǎn)生消息的時(shí)候,就可以用這個(gè)id來(lái)區(qū)分應(yīng)該發(fā)送給哪個(gè)瀏覽器客戶端
客戶端:
// 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) {
//...
// 當(dāng)annie下載時(shí),會(huì)進(jìn)行數(shù)據(jù)監(jiān)聽,這里會(huì)用到節(jié)流,防止進(jìn)度回傳太快,websocket服務(wù)器無(wú)法反應(yīng)
downloadProcess.stdout.on('data', throttle((chunk) => {
try {
if (!chunk) {
isDownloading = false;
}
// 這里主要做的是解析數(shù)據(jù),然后發(fā)送進(jìn)度和速度等信息給websocket服務(wù)器
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));
}
服務(wù)端收到進(jìn)度以及速度的消息后,回傳給客戶端,如果進(jìn)度達(dá)到了100%,那么就刪除掉存在server中的服務(wù)器中起的websocket的客戶端,并且發(fā)送一個(gè)客戶端被關(guān)閉的通知,通知瀏覽器已經(jīng)下載完成。
// 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);
// 發(fā)送從web服務(wù)器中傳過(guò)來(lái)的進(jìn)度、速度給瀏覽器
browserClient.send(JSON.stringify(data));
// 如果進(jìn)度已經(jīng)達(dá)到了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將當(dāng)前的這個(gè)由web服務(wù)器創(chuàng)建的websocket客戶端移除
// 然后關(guān)閉當(dāng)前連接
// 同時(shí)發(fā)送下載完成的消息給瀏覽器
clientsMap.delete(serverClientId);
serverClient.send(JSON.stringify({ connectionId: serverClientId, event: 'complete' }));
serverClient.close('download completed');
}
}
}
整體來(lái)說(shuō)就這么多,有一點(diǎn)需要指出,annie在解析的時(shí)候有時(shí)候可能消息處理不是很穩(wěn)定,導(dǎo)致我數(shù)據(jù)解析的時(shí)候出現(xiàn)了一些問(wèn)題,但是我用mock的數(shù)據(jù)以及mock的進(jìn)度條回傳是不會(huì)出現(xiàn)問(wèn)題的。
相關(guān)文章
docker中編譯nodejs并使用nginx啟動(dòng)
這篇文章主要介紹了docker中編譯nodejs并使用nginx啟動(dòng)的相關(guān)資料,需要的朋友可以參考下2017-06-06
使用node-media-server搭建一個(gè)簡(jiǎn)易的流媒體服務(wù)器
這篇文章主要介紹了使用node-media-server搭建一個(gè)簡(jiǎn)易的流媒體服務(wù)器,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
基于nodejs res.end和res.send的區(qū)別
今天小編就為大家分享一篇基于nodejs res.end和res.send的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
node.js 使用ejs模板引擎時(shí)后綴換成.html
本文給大家分享一個(gè)nodejs的小技巧,將ejs模板引擎的模板后綴改成.html的使用方法,非常的簡(jiǎn)單實(shí)用,這里推薦給大家。2015-04-04
Pycharm配置Node.js運(yùn)行js代碼詳細(xì)過(guò)程
在PyCharm中寫JavaScript代碼并進(jìn)行調(diào)試是非常方便的,但是有些用戶可能對(duì)如何在PyCharm中準(zhǔn)確地運(yùn)行JavaScript代碼感到困惑,這篇文章主要給大家介紹了關(guān)于Pycharm配置Node.js運(yùn)行js代碼的相關(guān)資料,需要的朋友可以參考下2023-11-11

