大文件上传实践分享
suiw9 2024-12-01 04:01 29 浏览 0 评论
一、方案背景:
在此前的项目中有个需求是用户需要通过前端页面上传大约1.5G的压缩包,存储到OSS,后提供给其他用户下载。于是我开始了大文件上传方案的探索。本文主要探究的是前端技术实现,后端给予相应的支持。
二、 原理探索之路
2.1大文件上传想要实现的目标
在此项目中,我想实现的目标是
- 能够快速的将1.5G的文件上传到服务端, 由服务端进行存储,之后提供给其他设备下载。
- 能够支持在网络条件不好时实现 断点续传 。
- 能够在不同用户上传同一个文件包时执行秒传。
2.2 实现思路
- spark-md5 计算文件的内容hash,以此来确定文件的唯一性
- 将文件hash发送到服务端进行查询,以此来确定该文件在服务端的存储情况,这里可以分为三种: 未上传、已上传、上传部分。(前提:分块大小固定)
- 根据服务端返回的状态执行不同的上传策略:
- 已上传: 执行秒传策略,即快速上传(实际上没有对该文件进行上传,因为服务端已经有这份文件了),用户体验下来就是上传得飞快,嗖嗖嗖。。。
- 未上传、上传部分: 执行计算待上传分块的策略
- 并发上传还未上传的文件分块。
- 当传完最后一个文件分块时,向服务端发送合并的指令,即完成整个大文件的分块合并,实现在服务端的存储。
整体流程如下:
总结一下:将大文件通过切分成N个小文件,通过并发多个HTTP请求,实现快速上传;在每次上传前计算文件hash,带着这个文件hash去服务端查询该文件在服务端的存储状态,通过状态来判断需要上传的分块,实现断点续传、秒传。
三、实践之路
3.1 文件hash计算
本项目中计算文件hash的使用spark-md5。
import SparkMD5 from 'spark-md5'
const CHUNK_SIZE = 1024 * 1024 * 5 // 5M
// 对大文件进行分片
function sliceFile2chunk(file) {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
const fileChunks = []
if (file.size <= CHUNK_SIZE) {
fileChunks.push({ file })
} else {
let chunkStartIndex = 0
while (chunkStartIndex < file.size) {
fileChunks.push({ file: blobSlice(file, chunkStartIndex, chunkStartIndex + CHUNK_SIZE) })
chunkStartIndex = chunkStartIndex + CHUNK_SIZE
}
}
return fileChunks
}
function getFileHash(file) {
let hashProcess = 0
let fileHash = null
// 这里需要使用异步执行,保证获取到hash后执行下一步
return new Promise((resolve) => {
const fileChunks = sliceFile2Chunk(file)
const spark = new SparkMD5.ArrayBuffer()
let hadReadChunksNum = 0
const readFile = (chunkIndex) => {
const fileReader = new FileReader()
fileReader.readAsArrayBuffer(fileChunks[chunkIndex]?.file)
fileReader.onload = (e) => {
hadReadChunksNum++
spark.append(e.target.result)
if (hadReadChunksNum === fileChunks.length) {
hashProcess = 100
fileHash = spark.end()
fileReader.onload = null
resolve(fileHash)
} else {
hashProcess = Math.floor((hadReadChunksNum / fileChunks.length) * 100);
readFile(hadReadChunksNum)
}
}
}
readFile(0)
})
}
// await 用于表示这里是一个异步操作
const fileHash = await getFileHash(file)
const fileChunks = sliceFile2chunk(file)
这里将文件hash发送给服务端,获取服务端对该文件的存储状态
// 采用表单形式提交数据,不是必须这样
const fileInfo = new FormData()
fileInfo.append('fileHash', fileHash)
fileInfo.append('fileName', name)
// getFileStatusFn是向服务端请求的文件初始状态的 http 方法, await 标识这里是一个异步请求
const res = await getFileStatusFn(fileInfo)
3.2 根据服务端返回的状态执行不同的上传策略
根据服务端返回的状态,来计算出需要上传的文件分块,以分块下标来区分不同的块。
- 0 未上传
- 1 上传部分
- 2 上传完成
// 这里的 res 是文件在服务端的状态
function createWait2UploadChunks(res) {
if (res.data) {
const wait2UploadChunks = []
if (res.data.result === 0 ) {
// 3.1中得到的文件 chunks
fileChunks.forEach((item, index) => {
const chunk = formateChunk(item, index)
wait2UploadChunks.push(chunk)
}, this)
}
if (res.data.result === 1) {
const restFileChunksIndex = []
// tagList 是服务端返回的已上传的文件块标识 类型是Array
res.data.tagList.forEach((item) => {
restFileChunksIndex.push(item.index)
}, this)
fileChunks.forEach((item, index) => {
if (!restFileChunksIndex.includes(index)) {
const chunk = formateChunk(item, index)
wait2UploadChunks.push(chunk)
}
})
}
if(res.data.result === 2) {
console.log('执行自定义的秒传操作')
}
return wait2UploadChunks
}
}
// 该函数式对文件块进行标准化,这里可以与后端做协商得出的,看后端需要什么样的数据
function formateChunk(item, index) {
const chunkFormData = new FormData()
chunkFormData.append("file", item.file);
chunkFormData.append("index", index);
chunkFormData.append("partSize", item.file.size);
chunkFormData.append("fileHash", fileHash);
return chunkFormData
}
// 入参是 3.2 得到的response, 出参事最终需要上传的分片
const wait2UploadChunks = createWait2UploadChunks(res)
3.3 并发上传还未上传的文件分块
这一步主要是将待上传的分块传输到服务端, 这里采用并发5(页面资源请求时,浏览器会同时和服务器建立多个TCP连接,在同一个TCP连接上顺序处理多个HTTP请求。所以浏览器的并发性就体现在可以建立多个TCP连接,来支持多个http同时请求。Chrome浏览器最多允许对同一个域名Host建立6个TCP连接,不同的浏览器有所区别。)个HTTP请求的方式进行上传,每当有一个请求完成后就新增一个分块传输请求,确保一直并发5个请求。
const currentHttpNum = 0
const maxHttpNum = 5
const hasUploadedChunkNum = 0
const nextChunkIndex = 4
const uploadProcess = 0
uploadFileChunks()
function uploadFileChunks() {
wait2UploadChunks.slice(0, maxHttpNum).forEach((item) => {
uploadFileChunk(item)
}, this)
}
async function uploadFileChunk(chunkFormData) {
try {
currentHttpNum++
const res = await uploadChunkFn(chunkFormData) // uploadChunkFn是执行文件上传的HTTP请求
currentHttpNum--
if (res.code === 200) {
if (hasUploadedChunkNum < wait2UploadChunks.length) {
hasUploadedChunkNum++
}
if (wait2UploadChunks.length > ++nextChunkIndex) {
uploadFileChunk(wait2UploadChunks[nextChunkIndex])
}
uploadProcess = Math.floor((hasUploadedChunkNum / wait2UploadChunks.length) * 100)
if (currentHttpNum <= 0) {
// 定义在 3.5
mergeChunks() // 第五步执行的函数
}
}
} catch (error) {
console.log(error);
}
}
3.4 向服务端发送合并的指令
当最后一个分块完成传输时,执行合并指令
async mergeChunks() {
try {
const res = await mergeChunkFn({ //mergeChunkFn 是HTTP请求
fileHash: fileHash,
})
} catch (error) {
console.log(error);
}
}
四、可优化点
4.1 hash计算优化
hash计算可以利用 web worker 协程来计算,这里提供一下worker的实现:
// worker.js
self.addEventListener('message', function (e) {
self.postMessage('You said: ' + e.data);
}, false);
self.close() // self代表子线程自身,即子线程的全局对象
// 主线程
const worker = new Worker('./worker.js') // 传入的是一个脚本
worker.postMessage('Hello World');
worker.onmessage = function (e) {
console.log(e.data);
}
4.2 分块大小合理化
本项目实测用的5M的分片,具体的环境信息如下:
- 网络带宽: 10M/s
- 服务器: 2台 4核32G
各位可根据自己的实际条件,根据网络情况, 合理去制定分块大小。
4.3 多个客户端上传同一个文件包来缩减上传时间
大家可以考虑一下如何通过多个客户端来同时上传一个文件,以此来实现更快的上传?
最后欢迎大家交流学习,优化方案,共同成长。留下你的赞
备注参考资料: 文件上传
相关推荐
- 10款超实用JavaScript音频库(js播放音频代码)
-
HTML5提供了一种新的音频标签实现和规范用一个简单的HTML对象而无需音频插件来控制音频。这只是一个简单的整合这些新的HTML5音频特征及使用JavaScript来创建各种播放控制。下面将介绍10款...
- PROFINET转Modbus网关——工业协议融合的智能枢纽
-
三格电子SG-PNh750-MOD-221,无缝连接Profinet与Modbus,赋能工业物联产品概述...
- 简单实用的Modbus类库,支持从站和DTU
-
一、简介...
- [西门子PLC] S7-200 SMART PROFINET :通过GSD组态PLC设备
-
从S7-200SMARTV2.5版本开始,S7-200SMART开始支持做PROFINETIO通信的智能设备。从而,两个S7-200SMART之间可以进行PROFINETI...
- Modbus(RTU / TCP)有什么异同(modbus tcp和tcp)
-
Modbus是一种广泛使用的工业自动化通信协议,它支持设备之间的数据交换。Modbus协议有两个主要的变体:ModbusRTU(二进制模式)和ModbusTCP(基于TCP/IP网络的模式)。尽管...
- Modbus通信调试步骤详解(modbus调试工具怎么用)
-
Modbus通信调试步骤详解 Modbus通信分为串口和以太网,无论是串口还是以太网,只要是标准Modbus,就可以用Modbus模拟器进行调试。按以下几步进行调试。...
- 理解Intel手册汇编指令(intel 汇编指令手册)
-
指令格式...
- 「西门子PLC」S7-200 SMART的Modbus RTU通讯
-
S7-200SMART集成的RS485端口(端口0)以及SBCM01RS485/232信号板(端口1)两个通信端口可以同时做MODBUSRTU主站,或者一个做MODBUSRTU主站一个做MO...
- InfiniBand网络运维全指南:从驱动安装到故障排查
-
一、InfiniBand网络概述InfiniBand(直译为“无限带宽”技术,缩写为IB)是一种用于高性能计算的计算机网络通信标准,具有极高的吞吐量和极低的延迟,用于计算机与计算机之间的数据互连。它...
- 一加回归 OPPO,背后的秘密不可告人
-
有这样一个手机品牌,它诞生于互联网品牌。在大众群体看来,它的身世似乎模糊不清,许多人以为它是国外品牌。它的产品定位是极客群体,深受国内发烧友,甚至国外极客玩家喜爱。...
- [西门子PLC] S7-200SMART快速高效的完成Modbus通信程序的设计
-
一、导读Modbus通信是一种被广泛应用的通信协议,在变频器、智能仪表还有其他一些智能设备上都能见到它的身影。本文呢,就把S7-200SMART系列PLC当作Modbus主站,把...
- 狂肝10个月手搓GPU,他们在我的世界中玩起我的世界,梦想成真
-
梦晨衡宇萧箫发自凹非寺量子位|公众号QbitAI自从有人在《我的世界》里用红石电路造出CPU,就流传着一个梗:...
- [西门子PLC] 博途TIA portal SCL编程基础入门:1-点动与自锁
-
一、S7-SCL编程语言简介...
- 工作原理系列之:Modbus(modbus工作过程)
-
MODBUS是一种在自动化工业中广泛应用的高速串行通信协议。该协议是由Modion公司(现在由施耐德电气公司获得)于1979年为自己的可编程逻辑控制器开发的。该协议充当了PLCS和智能自动化设备之间的...
你 发表评论:
欢迎- 一周热门
-
-
Linux:Ubuntu22.04上安装python3.11,简单易上手
-
宝马阿布达比分公司推出独特M4升级套件,整套升级约在20万
-
MATLAB中图片保存的五种方法(一)(matlab中保存图片命令)
-
别再傻傻搞不清楚Workstation Player和Workstation Pro的区别了
-
如何提取、修改、强刷A卡bios a卡刷bios工具
-
Linux上使用tinyproxy快速搭建HTTP/HTTPS代理器
-
Element Plus 的 Dialog 组件实现点击遮罩层不关闭对话框
-
日本组合“岚”将于2020年12月31日停止团体活动
-
MacOS + AList + 访达,让各种云盘挂载到本地(建议收藏)
-
SpringCloud OpenFeign 使用 okhttp 发送 HTTP 请求与 HTTP/2 探索
-
- 最近发表
-
- 10款超实用JavaScript音频库(js播放音频代码)
- Howler.js,一款神奇的 JavaScript 开源网络音频工具库
- PROFINET转Modbus网关——工业协议融合的智能枢纽
- 简单实用的Modbus类库,支持从站和DTU
- [西门子PLC] S7-200 SMART PROFINET :通过GSD组态PLC设备
- Modbus(RTU / TCP)有什么异同(modbus tcp和tcp)
- Modbus通信调试步骤详解(modbus调试工具怎么用)
- 理解Intel手册汇编指令(intel 汇编指令手册)
- 「西门子PLC」S7-200 SMART的Modbus RTU通讯
- InfiniBand网络运维全指南:从驱动安装到故障排查
- 标签列表
-
- dialog.js (57)
- importnew (44)
- windows93网页版 (44)
- yii2框架的优缺点 (45)
- tinyeditor (45)
- qt5.5 (60)
- windowsserver2016镜像下载 (52)
- okhttputils (51)
- android-gif-drawable (53)
- 时间轴插件 (56)
- docker systemd (65)
- slider.js (47)
- android webview缓存 (46)
- pagination.js (59)
- loadjs (62)
- openssl1.0.2 (48)
- velocity模板引擎 (48)
- pcre library (47)
- zabbix微信报警脚本 (63)
- jnetpcap (49)
- pdfrenderer (43)
- fastutil (48)
- uinavigationcontroller (53)
- bitbucket.org (44)
- python websocket-client (47)