通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大
小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了,电断了没
有上传完成,需要客户重新上传,这是致命的,所以对于大文件上传的要求最基本的是断点续传。
断点续传
断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传,下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。
上传流程
1. 上传前把文件分块
2.一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传
3.各分块上传完成最后合并文件
文件分块与合并
文件分块的流程如下:
1、获取源文件长度
2、根据设定的分块文件的大小计算出块数
3、从源文件读数据依次向每一个块文件写数据。
@Test
public void testChunk() throws IOException {
//文件源目录
File sourceFile = new File("D:/develop/video/test.mp4");
//文件分片下载路径
String chunkPath = "D:/develop/video/chunk/";
File fileFolder = new File(chunkPath);
if (!fileFolder.exists()){
//文件夹不存在
fileFolder.mkdir();
}
//分块大小
long chunkSize = 1024*1024*1;
//分块数量
long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize); // 向上取整 15.1 = 16
if (chunkNum <= 0){ //文件小,不用分片,默认分1片
chunkNum = 1;
}
//创建缓冲区
byte[] b = new byte[1024];
//使用RandomAccessFile访问文件
RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r"); // 参数 r 代表这 只读,写会报错
//分块
for(int i=0;i<chunkNum;i++){
//创建分块文件
File file = new File(chunkPath+i);
boolean isSuccess = file.createNewFile();
if (isSuccess){
//向分块文件中写数据
RandomAccessFile raf_write = new RandomAccessFile(file, "rw");
int len = -1;
while ((len = raf_read.read(b)) != -1){
raf_write.write(b, 0, len);
if (file.length() > chunkSize){
break;
}
}
raf_write.close();
}
}
raf_read.close();
}
文件合并
1、找到要合并的文件并按文件合并的先后进行排序。
2、创建合并文件
3、依次从合并的文件中读取数据向合并文件写入数
@Test
public void testMerge() throws IOException {
// 块文件目录
File chunkFolder = new File("D:/develop/video/chunk/");
//合并文件
File mergeFile = new File("D:/develop/video/test1.mp4");
if (mergeFile.exists()){ //文件存在,那就删除文件
mergeFile.delete();
}
//创建新文件
mergeFile.createNewFile();
//用于写文件
RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
//指针指向文件开头处 0
raf_write.seek(0);
//缓冲区
byte[] b = new byte[1024];
//获取文件列表
File[] files = chunkFolder.listFiles();
// 转成集合,便于排序
List<File> fileList = new ArrayList<File>(Arrays.asList(files));
//从小到大排序
Collections.sort(fileList, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName()) ? -1 : 1;
}
});
//合并文件
for (File chunkFile : fileList) {
//读取每个文件
RandomAccessFile raf_read = new RandomAccessFile(chunkFile,"rw");
int len = -1;
while((len = raf_read.read(b)) != -1){
raf_write.write(b,0,len);
}
raf_read.close();
}
raf_write.close();
}
RandomAccessFile类
RandomAccessFile是Java提供用来访问一些保存数据记录的文件的类,可以进行读取操作,也可以进行写入操作,写入的数据则以byte的形式存储;支持随机访问,也就是可以访问文件的任意位置(通过文件指针实现)。
我在网上找了一篇个人觉得比较好的介绍 RandomAccessFile的帖子 点我看帖
回到断点续传的话题,常用的断点续传方式有以下几种
1、通过Flash上传,比如SWFupload、Uploadify。
2、安装浏览器插件,变相的pc客户端,用的比较少。
3、Html5
因为html5能给出更好的客户体验,不用下载 flash啊,插件啥的....
使用WebUploader完成大文件上传功能的开发,WebUploader官网地址:
http://fexteam.gz01.bdysite.com/webuploader/
特性如下:
上传流程如下,
钩子方法
在webuploader中提供很多钩子方法,下边列出一些重要的:
1)before-send-file
在开始对文件分块儿之前调用,可以做一些上传文件前的准备工作,比如检查文件目录是否创建完成等。
2) before-send
在上传文件分块之前调用此方法,可以请求服务端检查分块是否存在,如果已存在则此分块儿不再上传。
3) after-send-file
在所有分块上传完成后触发,可以请求服务端合并分块文件.
注册钩子方法源代码:
WebUploader.Uploader.register(
"before‐send‐file":"beforeSendFile",
"before‐send":"beforeSend",
"after‐send‐file":"afterSendFile"
}
构建WebUploader
使用webUploader前需要创建webUploader对象。
指定上传分块的地址:/api/media/upload/uploadchunk
// 创建uploader对象,配置参数
this.uploader = WebUploader.create({
swf:"/static/plugins/webuploader/dist/Uploader.swf",//上传文件的flash文件,浏览器不支持h5时启动
flashserver:"/api/media/upload/uploadchunk",//上传分块的服务端地址,注意跨域问题
fileVal:"file",//文件上传域的
namepick:"#picker",//指定选择文件的按钮容器
auto:false,//手动触发上传
disableGlobalDnd:true,//禁掉整个页面的拖拽功能
chunked:true,// 是否分块上传
chunkSize:1*1024*1024, // 分块大小(默认5M)
threads:3, // 开启多个线程(默认3个)
prepareNextFile:true// 允许在文件传输时提前把下一个文件准备好
})
before-send-file
文件开始上传前前端请求服务端准备上传工作
参考源代码如下:
type:"POST",
url:"/api/media/upload/register",
data:{// 文件唯一表示
fileMd5:this.fileMd5,
fileName: file.name,
fileSize:file.size,
mimetype:file.type,
fileExt:file.ext
}
before-send
上传分块前前端请求服务端校验分块是否存在。
参考源代码如下:
type:"POST",
url:"/api/media/upload/checkchunk",
data:{// 文件唯一表示
fileMd5:this.fileMd5,
// 当前分块下标
chunk:block.chunk,
// 当前分块大小
chunkSize:block.end‐block.start
}
after-send-file
在所有分块上传完成后触发,可以请求服务端合并分块文件
参考代码如下:
type:"POST",
url:"/api/media/upload/mergechunks",
data:{
fileMd5:this.fileMd5,
fileName: file.name,
fileSize:file.size,
mimetype:file.type,
fileExt:file.ext
}
下面是页面效果
页面代码
<template>
<div><br/>
操作步骤:<br/>
1、点击“选择文件”,选择要上传的文件<br/>
2、点击“开始上传”,开始上传文件<br/>
3、如需重新上传请重复上边的步骤。<br/><br/>
<div id="uploader" class="wu-example">
<div class="btns" style="float:left;padding-right: 20px">
<div id="picker">选择文件</div>
</div>
<div id="ctlBtn" class="webuploader-pick" @click="upload()">开始上传</div>
</div>
<!--用来存放文件信息-->
<div id="thelist" class="uploader-list" >
<div v-if="uploadFile.id" :id='uploadFile.id'><span>{{uploadFile.name}}</span> <span class='percentage'>{{percentage}}%</span></div>
</div>
</div>
</template>
<script>
import $ from '../../../../static/plugins/jquery/dist/jquery.js'
import webuploader from '../../../../static/plugins/webuploader/dist/webuploader.js'
import '../../../../static/css/webuploader/webuploader.css'
export default{
data(){
return{
uploader:{},
uploadFile:{},
percentage:0,
fileMd5:''
}
},
methods:{
//开始上传
upload(){
if(this.uploadFile && this.uploadFile.id){
this.uploader.upload(this.uploadFile.id);
}else{
alert("请选择文件");
}
}
},
mounted(){
// var fileMd5;
// var uploadFile;
WebUploader.Uploader.register({
"before-send-file":"beforeSendFile",
"before-send":"beforeSend",
"after-send-file":"afterSendFile"
},{
beforeSendFile:function(file) {
// 创建一个deffered,用于通知是否完成操作
var deferred = WebUploader.Deferred();
// 计算文件的唯一标识,用于断点续传
(new WebUploader.Uploader()).md5File(file, 0, 100*1024*1024)
.then(function(val) {
this.fileMd5 = val;
this.uploadFile = file;
// alert(this.fileMd5 )
//向服务端请求注册上传文件
$.ajax(
{
type:"POST",
url:"/api/media/upload/register",
data:{
// 文件唯一表示
fileMd5:this.fileMd5,
fileName: file.name,
fileSize:file.size,
mimetype:file.type,
fileExt:file.ext
},
dataType:"json",
success:function(response) {
if(response.success) {
//alert('上传文件注册成功开始上传');
deferred.resolve();
} else {
alert(response.message);
deferred.reject();
}
}
}
);
}.bind(this));
return deferred.promise();
}.bind(this),
beforeSend:function(block) {
var deferred = WebUploader.Deferred();
// 每次上传分块前校验分块,如果已存在分块则不再上传,达到断点续传的目的
$.ajax(
{
type:"POST",
url:"/api/media/upload/checkchunk",
data:{
// 文件唯一表示
fileMd5:this.fileMd5,
// 当前分块下标
chunk:block.chunk,
// 当前分块大小
chunkSize:block.end-block.start
},
dataType:"json",
success:function(response) {
if(response.fileExist) {
// 分块存在,跳过该分块
deferred.reject();
} else {
// 分块不存在或不完整,重新发送
deferred.resolve();
}
}
}
);
//构建fileMd5参数,上传分块时带上fileMd5
this.uploader.options.formData.fileMd5 = this.fileMd5;
this.uploader.options.formData.chunk = block.chunk;
return deferred.promise();
}.bind(this),
afterSendFile:function(file) {
// 合并分块
$.ajax(
{
type:"POST",
url:"/api/media/upload/mergechunks",
data:{
fileMd5:this.fileMd5,
fileName: file.name,
fileSize:file.size,
mimetype:file.type,
fileExt:file.ext
},
success:function(response){
//在这里解析合并成功结果
if(response && response.success){
alert("上传成功")
}else{
alert("上传失败")
}
}
}
);
}.bind(this)
}
);
// 创建uploader对象,配置参数
this.uploader = WebUploader.create(
{
swf:"/static/plugins/webuploader/dist/Uploader.swf",//上传文件的flash文件,浏览器不支持h5时启动flash
server:"/api/media/upload/uploadchunk",//上传分块的服务端地址,注意跨域问题
fileVal:"file",//文件上传域的name
pick:"#picker",//指定选择文件的按钮容器
auto:false,//手动触发上传
disableGlobalDnd:true,//禁掉整个页面的拖拽功能
chunked:true,// 是否分块上传
chunkSize:1*1024*1024, // 分块大小(默认5M)
threads:3, // 开启多个线程(默认3个)
prepareNextFile:true// 允许在文件传输时提前把下一个文件准备好
}
);
// 将文件添加到队列
this.uploader.on("fileQueued", function(file) {
this.uploadFile = file;
this.percentage = 0;
}.bind(this)
);
//选择文件后触发
this.uploader.on("beforeFileQueued", function(file) {
// this.uploader.removeFile(file)
//重置uploader
this.uploader.reset()
this.percentage = 0;
}.bind(this));
// 监控上传进度
// percentage:代表上传文件的百分比
this.uploader.on("uploadProgress", function(file, percentage) {
this.percentage = Math.ceil(percentage * 100);
}.bind(this));
//上传失败触发
this.uploader.on("uploadError", function(file,reason) {
console.log(reason)
alert("上传文件失败");
});
//上传成功触发
this.uploader.on("uploadSuccess", function(file,response ) {
console.log(response)
// alert("上传文件成功!");
});
//每个分块上传请求后触发
this.uploader.on( 'uploadAccept', function( file, response ) {
if(!(response && response.success)){//分块上传失败,返回false
return false;
}
});
}
}
</script>
<style scoped>
</style>
后台的话需要提供四个接口,
分别是
文件上传注册
校验文件是否存在,创建文件存放目录啥的,做初始化工作
post 接口 url:"/api/media/upload/register" 返回值 {"success":"boolean","message":"操作信息提示"}
参数
(String fileMd5,
String fileName,
Long fileSize,
String mimetype,
String fileExt)
分块检查
效验文件分块目录是否存在,及分块是否存在,如果存在分块,就不上传该分块....
type:"POST", --方法类型
url:"/api/media/upload/checkchunk", -- url
参数
(String fileMd5,
Integer chunk,
Integer chunkSize)
返回值
{"fileExist":"boolean"} -- 文件存在,返回true,则返回false
上传分块
服务器上传各个分块请求的url url -- 路径 server:"/api/media/upload/uploadchunk"
参数
MultipartFile file,
Integer chunk, //分片序号,第几片分片
String fileMd5
返回值
{"success":"boolean"} //是否上传成功
合并分块
执行完上传分块,最后请求合并分块的钩子
type:"POST", --方法类型 url:"/api/media/upload/mergechunks",
参数
String fileMd5,
String fileName,
Long fileSize,
String mimetype,
String fileExt
返回值
{"success":"boolean"} //是否合并成功,
把以上的接口实现就能实现断点传续啦,