序文
ソフトウェア エンジニアリングでは、大規模な同時実行、大規模なデータ ボリューム、および大規模なファイルなど、「大きな」ものを処理する際に、常に問題点と困難がありました. ハードウェアをアップグレードすると、いくつかの問題を解決できますが、これは最も賢明な方法ではありません。上司にとって、これは最も費用のかからない方法ではありません。開発者として、同様の極端な問題に直面した場合、タフになるのではなく、裏をかくだけで済み、手元にある既存のリソースを最大限に活用し、より洗練された方法でユーザーの多様なニーズを満たすことが王道です。今日のトピックも「大きな」質問です。つまり、非常に大きなファイルをアップロードおよびダウンロードする方法は? 実は、この問題を解決する前に、回避できない問題があります。大きなファイルとは何ですか? 特大ファイルとは何ですか? 数十メガバイト?数百メガバイト?それとも数G?これは実際には非常に物議を醸す問題であり、ビジネス シナリオが異なれば、大きなファイルの「大きい」の理解と定義も異なる場合があります。しかし、これはこの記事の焦点ではありません. 私があなたと共有したいのは、いわゆる大きなファイルに直面したときに適切にアップロードする方法です.
どんなに単純な要件でも、ある程度の大きさになると非常に複雑になります。そのため、この記事は 2 つの部分で構成され、少し長くなります. ステップごとに読むのに疲れて結果を直接見たい場合は、記事の最後にフロントエンドとバックエンドの 2 つの主要なファイルが添付されています. 、そしてそれをエディタに直接渡してデバッグし、段階的に見ていくことができます:
Springboot+WebUploader で大容量ファイルのアップロードをエレガントに実現 (1)
Springboot+WebUploader は大きなファイルをエレガントにアップロードします (2)
記事例環境構成情報
jdk バージョン:1.8
開発ツール:Intellij iDEA 2020.1
springboot:2.3.9.RELEASE
目標
特大ファイルのアップロードと再開を実現します。
実装のアイデア
さまざまな「大きな」問題に対して、多くのシナリオは分割統治の概念で設計されています.大きなファイルのアップロードでは、この概念から始めて、大きなファイルを分割してアップロードするのが具体的な方法です.すべての断片をアップロードした後、フラグメントをマージします. . では、大きなファイルの断片化とは何ですか? マージされたシャーディングとは何ですか? マルチパート アップロード プロセス中に個々のパートのアップロードが失敗した場合、もう一度パートをアップロードする必要がありますか? 実際、ファイルのアップロード自体は非常に単純な要件ですが、ある程度の大きさに達すると、非常に複雑になります. 考慮しなければならない問題はたくさんあります. 同様に、他の「大きな」問題も同様です.
大きなファイルの断片化
大きなファイルの断片化は、アップロードする大きなファイルです. 特定のルールに従って、ファイル全体がスライスされます. つまり、大きなデータファイルは小さなデータブロックに分割され、特定の戦略 (シリアルまたはコンカレント) に従ってアップロードされます. ;
シャードをマージする
大きなファイルフラグメントのマージは、大きなファイルをフラグメントでアップロードするフォローアップ操作です. 大きなファイルが小さなデータブロックに分割されてサーバーにアップロードされると、これらの小さなデータブロックは元の順序でマージされ、元の順序に復元される必要があります.オリジナル 大きなファイルの場合、これはマージされた断片化です。
ファイルの断片化と断片化のマージは、実際には簡単に理解できます. 実生活と同様に、入り口が比較的小さい洞窟に大きな飛行機を隠したい場合はどうすればよいでしょうか? 最初に小さな部品に分解し、次に洞窟に輸送し、航空機の組立図に従ってすべての部品を再組み立てする必要があります。原理はそれと同じくらい簡単です。
http
大きなファイルを分割してアップロードする場合、個々の部分でネットワーク障害が発生し、一部の部分がアップロードに失敗する可能性があります.ネットワークが回復すると、失敗した部分からアップロードを開始し、成功した部分をスキップできます.部分ファイルの一部,アップロードを再開すると、時間を節約し、アップロード効率を向上させることができます; 下の図に示すように: 大きなファイルを n 個に分割します. 何らかの理由でアップロードに失敗しました. 再度アップロードすると、正常にアップロードされたすべてのフラグメントが直接スキップされます, アップロードは失敗したフラグメント 2 とフラグメント 3 から直接開始されます. フラグメントをマージすると, すべてのフラグメントが完了したことが検出されます. アップロード後, マージして大きなファイルの元の外観に復元できます. ;
実現原理
実装のアイデアは非常に単純であり、実装の原則は実際には次のように非常に単純です。
1. フロント エンドは、Baidu の WebUploader コンポーネントを使用して、アップロードする大きなファイルを選択し、ファイルの md5 値を計算します。
2. WebUploader コンポーネントがフラグメント アップロードを有効にした後、アップロードするように選択されたファイルは、構成されたフラグメント化ルールに従ってフラグメント化され、フラグメント化されたファイルがアップロードされる前に、フラグメント化されたファイルの md5 値が計算されます。
3、通过webuploader组件api计算出分片的md5值后,会携带文件md5值和分片文件的md5值,调用后台接口检验当前分片是否已经上传过;若上传过,则直接跳过;若未上传过,则开始分片上传;
4、前端往后端传输分片文件的过程可以是并发执行,这里一定注意传递到后台的分片并不一定是按照分片的顺序来的,后端收到分片文件后,会保存分片文件到硬盘、网盘等存储介质上,同时也要保存分片文件md5值、文件md5值等分片参数信息;
5、待所有的分片上传成功后,会触发WebUploader的uploadSuccess事件,然后在这里再发起合并分片请求;
6、后端收到合并分片的请求后,再次检查所有的分片是否上传完整,若上传完整,则开始合并所有分片;
7、这里要特别注意,合并分片的时候,一定要按钮分片时的切割顺序来合并,否则文件就会打不开或运行不了;合并完所有分片文件后,分片文件就没有用了,可以删除了;
md5消息摘要算法,属Hash算法一类,主要特点是不可逆,相同数据的md5值肯定一样,不同数据的md5值不一样;对于数据文件,不管文件名字是否相同,如果数据文件内容相同,则文件的md值是相同的;webuploader组件提供了文件的md5值的计算方法,其计算过程是异步的;
代码实现
大文件分片上传的实现原理,逻辑比较清晰,那么落地到代码实现上,还有几个问题需要解决:
1、webuploader组件中,分片上传怎么开启?
2、webuploader组件中,文件的md5值如何计算?
3、webuploader组件中,分片文件的md5值如何计算?
4、webuploader组件中,分片上传的的请求在哪里触发?
5、前端、后端如何校验分片是否已经上传?
6、后端如何处理分片上传请求?
7、webuploader组件中,合并文件分片的请求在哪里触发?
8、后端如何合并分片请求?
9、分片上传失败后,如何在断点处继续上传?
10、上传的进度条是怎么实现的?
1、webuploader组件中,分片上传怎么开启?
webuploader的分片上传开启实际上很简单,在创建webuploader对象时,设置chunked为true,即表示开启分片上传;chunkSize可以设置分片大小,即以多大的体积进行分片;chunkRetry可以设置重传次数,有的时候由于网络原因,分片上传的会失败,这里即是失败允许重的次数;threads可以设置允许最大由几个进程发起上传请求;
uploader = WebUploader.create({
// swf文件路径
swf: 'http://localhost:8080/lib/Uploader.swf',
// 分片文件上传接口
server: 'http://localhost:8080/file/upload',
// 选择文件的按钮。可选。
pick: '#picker',
fileVal: 'multipartFile',//后端用来接收上传文件的参数名称
chunked: true,//开启分片上传
chunkSize: 1024 * 1024 * 10,//设置分片大小
chunkRetry: 2,//设置重传次数,有的时候由于网络原因,分片上传的会失败,这里即是失败允许重的次数
threads: 3//允许同时最大上传进程数
});
2、webuploader组件中,文件的md5值如何计算?
文件的md5计算可以引用spark-md5.js,据传言是javascript里md5加密计算速度最快的,当然在webuploader.js里也有具体的api可以使用;引入webuploader.js后,调用 WebUploader.Uploader.md5File(...)即可计算文件的md5值,这里需要注意的是md5File(...),有三个参数,分别是file,数据起始位置、数据结束位置,返回的是一个promise对象,要想拿到具体的值还要再调用then(function(val){}),具体步骤如下:
1、当添加完文件后,webuploader的fileQueued事件被触发;
2、fileQueued事件触发后,在回调函数里计算出文件的的md5值,这里注意是要计算出整个文件的md5值,而不是一部分,关键就在md5File(...)方法的后两个参数数据起始位置和数据结束位置,看到很多人实际上是用错了,只计算了文件一部分的md5值,并不是整个文件的md5值;md5的计算过程是异步操作,并且文件越大,计算用时就越长;
3、这里还用到了deferred,deferred的作用就是监控异步计算文件md5值这个异步操作的执行状态;文件md5值计算完成后,更新状态为已完成,这时 deferred.done()会触发;更新md5计算标志位为true,这时再点击开始上传按钮时,就会再有弹窗提示:md5计算中...,请稍侯;
webuploader内部有很多种command,其中有一个叫before-send-file,也可以在文件上传前会触发,此时还没有开始分片,可以用来做文件整体的md5计算;需要注意的是before-send-file的触发时机是要晚于fileQueued事件的;before-send-file是在点击开始上传按钮执行uploader.upload()后才会触发;而fileQueued事件是在选择文件后,立刻触发,不用等到点击开始上传按钮;所以具体在哪里进行计算,可根据实际业务酌情选择;
/**
* 当有文件被添加进队列后触发
* 主要逻辑:1、文件被添加到队列后,开始计算文件的md5值;
* 2、md5的计算过程是异步操作,并且文件越大,计算用时越长;
* 3、变量md5FlagMap是文件md5值计算的标志位,计算完成后,设置当前文件的md5Flag为true
*/
//md5FlagMap用于存储文件md5计算完成的标志位;多个文件时,分别设置标志位,key是文件名,value是true或false;
var md5FlagMap = new Map();
uploader.on('fileQueued', function (file) {
md5FlagMap.set(file.name, false);//文件md5值计算的标志位默认为false
var deferred = WebUploader.Deferred();//deferred用于监控异步计算文件md5值这个异步操作的执行状态
uploader.md5File(file, 0, file.size - 1).then(function (fileMd5) {
file.wholeMd5 = fileMd5;
file_md5 = fileMd5;
deferred.resolve(file.name);//文件md5值计算完成后,更新状态为已完成,这时 deferred.done()会触发
})
//文件越大,文件的md5值计算用时越长,因此md5的计算搞成异步执行是合理的;如果异步执行比较慢的话,会顺序执行到这里
$('#thelist').append('<div id="' + file.id + '" class="item">' +
'<h4 class="info">' + file.name + '</h4>' +
'<p class="state">开始计算大文件的md5......<br/></p>' +
'</div>')
//文件的md5计算完成,会触发这里的回调函数,
deferred.done(function (name) {
md5FlagMap.set(name, true);//更新md5计算标志位为true
$('#' + file.id).find('p.state').append('大文件的md5计算完成<br/>');
})
return deferred.promise();
})
3、webuploader组件中,分片文件的md5值如何计算?
webuploader对象中配置好相关的开启分片设置参数后,当有文件被选中添加后,webuploader会帮你对文件按配置参数进行分片;在分片文件发送到后台之前,webuploader内部另一个command(before-send)会触发,before-send的触发时机在分片上传之前,可以用作在分片发送到后端之前计算出分片文件的md5,调用后台接口做分片是否已经上传的验证:如果分片已经上传成功了,直接跳过,不会再调用分片的上传接口;如果分片未上传,则会把分片的md5值赋给分片block上,webuploader的另一个事件‘uploadBeforeSend’在触发的时候,其回调函数的第一个参数中就可以拿到分片的md5值,然后传递到后台,如果下次再上传时分片时,则可以用于是否已经上传的校验;
WebUploader.Uploader.register({
"add-file": "addFile",
"before-send-file": "beforeSendFile",
"before-send": "beforeSend",
"after-send-file": "afterSendFile"
}, {
addFile: function (file) {
console.log('1', file)
},
beforeSendFile: function (file) {
console.log('2', file)
},
beforeSend: function (block) {
console.log(3)
var file = block.file;
var deferred = WebUploader.Base.Deferred();
(new WebUploader.Uploader()).md5File(file, block.start, block.end).then(function (value) {
$.ajax({
url: 'http://localhost:8080/file/check',//检查当前分片是否已经上传
method: 'post',
data: {chunkMd5: value, fileMd5: file_md5,chunk:block.chunk},
success: function (res) {
if (res) {
deferred.reject();
} else {
deferred.resolve(value);
}
}
});
})
deferred.done(function (value) {
console.log('分片md5:', value)
block.chunkMd5 = value;
})
return deferred;
},
afterSendFile: function (file) {
console.log('4', file)
}
})
4、webuploader组件中,分片上传的的请求在哪里触发?
当选择文件后,就开始计算整体文件的md5值了,未计算完成前,点击开始上传按钮,会直接弹出“md5计算中...,请稍侯”;考虑到多文件上传的情况,这里使用了map对象md5FlagMap来存储每个文件的md5是否计算完成标志,key是文件名称,value是true或false,表示文件md5文件是否计算完成;
如果不想用按钮来触发上传,WebUploader有一个参数是auto,默认是false,可以改为true,选中文件后自动开始上传;
//开始上传按钮被点击时触发
$('#ctlBtn').click(function () {
//md5FlagMap存储有文件md5计算的标志位;
// 同时上传多个文件时,上传前要判断一下已添加文件的md5是否计算完成,
// 如果有未计算完成的,则继续等待计算结果;
//文件上传标志位,如果多个文件有一个没有完成md5计算则不能开始上传;
//这里在实际业务中可以更换成其他交互样式,酌情优化为哪个文件的md5计算完成,则开始哪个文件的上传;
var uploadFloag = true;
md5FlagMap.forEach(function (value, key) {
if (!value) {
uploadFloag = false;
alert('md5计算中...,请稍侯')//文件md5计算未完成,会弹出弹窗提示;
}
})
if (uploadFloag) {
uploader.upload();//文件md5计算完成后,开始分片上传;
}
})
添加的文件的md5计算完成后,再次点击开始上传按钮,调用 uploader.upload(),开始分片上传;但是在实际开始上传前,webuploader的command(before-send)和uploadBeforeSend事件会先后触发,这两个地方有一个共同作用就是在分片正式上传前可以再做点事情,根据官方文档介绍,不同的是before-send可以用作分片是否已上传的验证(详见第3个问题里);uploadBeforeSend事件的作用是在分片上传前添加一些附带参数到后端;
// 分片模式下,当文件的分块在发送前触发
uploader.on('uploadBeforeSend', function (block, data) {
var file = block.file;
//data可以携带参数到后端
data.originalFilename = file.originalFilename;//文件名字
data.md5Value = file.wholeMd5;//文件整体的md5值
data.start = block.start;//分片数据块在整体文件的开始位置
data.end = block.end;//分片数据块在整体文件的结束位置
data.chunk = block.chunk;//分片的索引位置
data.chunks = block.chunks;//整体文件总共分了多少征
data.chunkMd5 = block.chunkMd5;//分片文件md5值
});
小结
Springboot+WebUploader优雅实现大文件的上传(一),主要讲述了大文件上传的实现思路、实现原理、以及主要的前端代码实现的。更多精彩的后端如何与前端协调配合内容,以及大文件分片上传的两个关键代码文件,请移步Springboot+WebUploader优雅实现大文件的上传(二)。