iOS大文件的分片上传和断点上传

iOS大文件的分片上传和断点上传,第1张

由于最近比较忙,所以一直没有写博客,现在分享一些大文件上传的问题!断点续传和分片上传。因为文件过大(比如1G以上),必须要考虑上传过程网络中断的情况。http的网络请求中本身就已经具备了分片上传功能,当传输的文件比较大时,http协议自动会将文件切片(分块),但这不是我们现在说的重点,我们要做的事配伏是保证在网络中断后1G的文件已上传的那部分在下次网络连接时不必再重传。所以我们本地在上传的时候,要将大文件进行分片,比如分成1024*1024B,即将大文件分成1M的片进行上传,服务器在接收后,再将这些片合并成原始文件,这就是分片的基本原理。断点续传要求本地要记录每一片的上传的状态,我通过三个状态进行了标记(wait loading finish),当网络中断,再次连接后,从断猛卖蚂点处进行上传。服务器通过文件名、总片数判断该文件是否已全部上传完成。

下面来说细节:

1、首先获取文件(音视频、图片)

分两种情况,一种是在相册库里直接获取,一种是调用相机。如果是通过UIImagePickerView来获取(细节不详述,网上一大堆),我们会发现当你选定一个视频的时候,会出现图1的压缩页面,最后我们的app获取的视频就是这个经过压缩后的视频(不是视频库里的原始视频,这里有个注意点, *** 作完该压缩视频后记得释放,系统不会帮你释放的,需要你手动来 *** 作,下面会说到),然后通过UIImagePickerView的协议方法中的- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info获取视频的Info

fileInfo = {

UIImagePickerControllerMediaType = "public.movie"

UIImagePickerControllerMediaURL = "file:///private/var/mobile/Containers/Data/Application/2AAE9E44-0E6D-4499-9AC3-93D44D8342EA/tmp/trim.F36EC46C-4219-43C8-96A7-FA7141AB64D2.MOV"

UIImagePickerControllerReferenceURL = "assets-library://asset/asset.MOV?id=DEDA9406-3223-4F87-ABB2-98FB5F5EB9C4&ext=MOV"

}

UIImagePickerControllerMediaType是选取文件的类型,如KUTTypeImage,KUTTypeMovie。这里注意一下movie和video的区别,一个是有声音的视频文件,一个是没有声音的视频文件,当然还有Audio是只有声音没有视频。UIImagePickerControllerMediaURL是视频的URL(如果是相机拍摄的,那么这个就是原始拍摄枝埋得到的视频;如果是在相册库里选择的,那就是压缩之后生成的视频),注意这个URL不指向相册库,通过这个URL你可以 *** 作这个视频如删除,拷贝等,可以获取压缩后的视频的大小。UIImagePickerControllerReferenceURL是一个指向相册的URL,官方的解释是an NSURL that references an asset in the AssetsLibrary framework,通过这个URL,你可以获取视频的所有信息,包括文件名,缩略图,时长等(通过ALAssetsLibrary里的assetsLibraryassetForURL:referenceURLresultBlock:)。

如果是相机拍摄的,注意两个保存方法:图片保存到相册assetsLibrarywriteImageDataToSavedPhotosAlbum:UIImageJPEGRepresentation([infovalueForKey:UIImagePickerControllerOriginalImage],(CGFloat)1.0)metadata:nilcompletionBlock: failureBlock:

高保真压缩图片的方法NSData * UIImageJPEGRepresentation ( UIImage *image, CGFloat compressionQuality)

视频保存到相册:assetsLibrarywriteVideoAtPathToSavedPhotosAlbum:MediaURL completionBlock:failureBlock:

到这里,我们就获取了所有需要的文件以及文件信息。下面要做的就是将文件分片。

2、将获取到的文件分片

首先,我将获取到的文件保存在这这样一个类中

@interface CNFile :NSObject

@property(nonatomic,copy)NSString* fileType//image or movie

@property(nonatomic,copy)NSString* filePath//文件在app中路径

@property(nonatomic,copy)NSString* fileName//文件名

@property(nonatomic,assign)NSIntegerfileSize//文件大小

@property (nonatomic,assign)NSIntegertrunks//总片数

@property(nonatomic,copy)NSString* fileInfo

@property(nonatomic,strong)UIImage* fileImage//文件缩略图

@property(nonatomic,strong) NSMutableArray* fileArr//标记每片的上传状态

@end

这样我们就可以对每一个CNFile对象进行 *** 作了。

-(void)readDataWithChunk:(NSInteger)chunk file:(CNFile*)file{

总片数的获取方法:

intoffset =1024*1024(每一片的大小是1M)

NSIntegerchunks = (file.fileSize%1024==0)?((int)(file.fileSize/1024*1024)):((int)(file.fileSize/(1024*1024) +1))

NSLog(@"chunks = %ld",(long)chunks)

将文件分片,读取每一片的数据:

NSData* data

NSFileHandle*readHandle = [NSFileHandlefileHandleForReadingAtPath:file.filePath]

[readHandleseekToFileOffset:offset * chunk]

data = [readHandlereadDataOfLength:offset]

}

这样我们就获取了每一片要上传的数据,然后询问服务器,该片是否已经存在

(方法-(void)ifHaveData:(NSData*)data WithChunk:(NSInteger)chunk file:(CNFile*)file)

,如果存在,令chunk+1,重复上面的方法读取下一片,直到服务器不存在该片,那么上传该片数据。在这个方法中注意设置该chunk的上传状态(wait loading finish),这将关系到本地判断该文件是否已全部上传完成。

下一步就是上传的过程:

-(void)uploadData:(NSData*) data WithChunk:(NSInteger) chunk file:(CNFile*)file;

在服务器返回该片上传成功后,我们要做的事有很多:

1)先将已经成功上传的本片的flag置finish

[file.fileArrreplaceObjectAtIndex:chunk withObject:@“finish"]

2)查看是否所有片的flag都已经置finish,如果都已经finishi,说明该文件上传完成,那么删除该文件,上传下一个文件或者结束。

for(NSIntegerj =0j

if(j == chunks || ((j == chunks -1)&&([file.fileArr[j]isEqualToString:@"finish"])))

[medeleteFile:file.filePath]

[mereadNextFile]

}

3)如果没有都finish,那么看本地下一chunk对用的flag是否是wait

NSLog(@"查看第%ld片的状态",chunk+1)

for(NSIntegeri = chunk+1i <chunksi++)

{

NSString* flag = [file.fileArrobjectAtIndex:i]

if([flagisEqualToString:@"wait"]) {

[mereadDataWithChunk:ifileName:fileNamefile:file]

break

}

}

在第2、3步之间可以有一个 2.5)判断是否暂停上传

if(me.isPause ==YES)

{

//将目前读到了第几个文件的第几片保存到本地

[selfsaveProgressWithChunk:chunk file:file]

return

}

这个 *** 作实际上和上传过程中断网是一样的,为了断点续传,在断网或者暂停的时候,我们要将目前的进度保存起来,以便下次上传时略过前面已置finish的片。

然后还有一个问题,如果我们就这样线性的一片一片上传,实际上失去了分片上传的意义,应该结合多线程,使分片上传过程并发执行,同时上传多片,这样就提高了上传效率,并充分利用了网络带宽。

dispatch_async(dispatch_queue_t queue, ^{

[mereadDataWithChunk: chunk]

})

最后注意一下,每上传完一个视频,去设置里看看你的app占用的存储空间有没有增大哦,如果你没有处理那个生成的压缩视频,你会发现你的app的空间占用量是很大的。

站在大牛的肩膀上开发。

javaweb上传文件

上传文件的jsp中的部分

上传文件同样可以使用form表单向后端发请求,也可以使用 ajax向后端发请求

1.通过form表单向后端发送请求

<form id="postForm" action="${pageContext.request.contextPath}/UploadServlet" method="post" enctype="multipart/form-data">

<div class="bbxx wrap">

<inputtype="text" id="side-profile-name" name="username" class="form-control">

<inputtype="file" id="example-file-input" name="avatar">

<button type="submit" class="btn btn-effect-ripple btn-primary">Save</button>

</div>

</form>

改进后的代码不需要form标签,直接由控件来实现。开发人员只需要关注业务逻辑即可。JS中已世扒经帮我们封闭好了

this.post_file = function ()

{

$.each(this.ui.btn, function (i, n) { n.hide()})

this.ui.btn.stop.show()

this.State = this.Config.state.Posting//

this.app.postFile({ id: this.fileSvr.id, pathLoc: this.fileSvr.pathLoc, pathSvr:this.fileSvr.pathSvr,lenSvr: this.fileSvr.lenSvr, fields: this.fields })

}

通过监册尘控工具可以看到控件提交的数据,非常搜姿昌的清晰,调试也非常的简单。

2.通过ajax向后端发送请求

$.ajax({

url : "${pageContext.request.contextPath}/UploadServlet",

type : "POST",

data : $( '#postForm').serialize(),

success : function(data) {

$( '#serverResponse').html(data)

},

error : function(data) {

$( '#serverResponse').html(data.status + " : " + data.statusText + " : " + data.responseText)

}

})

ajax分为两部分,一部分是初始化,文件在上传前通过AJAX请求通知服务端进行初始化 *** 作

this.md5_complete = function (json)

{

this.fileSvr.md5 = json.md5

this.ui.msg.text("MD5计算完毕,开始连接服务器...")

this.event.md5Complete(this, json.md5)//biz event

var loc_path = encodeURIComponent(this.fileSvr.pathLoc)

var loc_len = this.fileSvr.lenLoc

var loc_size = this.fileSvr.sizeLoc

var param = jQuery.extend({}, this.fields, this.Config.bizData, { md5: json.md5, id: this.fileSvr.id, lenLoc: loc_len, sizeLoc: loc_size, pathLoc: loc_path, time: new Date().getTime() })

$.ajax({

type: "GET"

, dataType: 'jsonp'

, jsonp: "callback" //自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名

, url: this.Config["UrlCreate"]

, data: param

, success: function (sv)

{

_this.svr_create(sv)

}

, error: function (req, txt, err)

{

_this.Manager.RemoveQueuePost(_this.fileSvr.id)

alert("向服务器发送MD5信息错误!" + req.responseText)

_this.ui.msg.text("向服务器发送MD5信息错误")

_this.ui.btn.cancel.show()

_this.ui.btn.stop.hide()

}

, complete: function (req, sta) { req = null}

})

}

在文件上传完后向服务器发送通知

this.post_complete = function (json)

{

this.fileSvr.perSvr = "100%"

this.fileSvr.complete = true

$.each(this.ui.btn, function (i, n)

{

n.hide()

})

this.ui.process.css("width", "100%")

this.ui.percent.text("(100%)")

this.ui.msg.text("上传完成")

this.Manager.arrFilesComplete.push(this)

this.State = this.Config.state.Complete

//从上传列表中删除

this.Manager.RemoveQueuePost(this.fileSvr.id)

//从未上传列表中删除

this.Manager.RemoveQueueWait(this.fileSvr.id)

var param = { md5: this.fileSvr.md5, uid: this.uid, id: this.fileSvr.id, time: new Date().getTime() }

$.ajax({

type: "GET"

, dataType: 'jsonp'

, jsonp: "callback" //自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名

, url: _this.Config["UrlComplete"]

, data: param

, success: function (msg)

{

_this.event.fileComplete(_this)//触发事件

_this.post_next()

}

, error: function (req, txt, err) { alert("文件-向服务器发送Complete信息错误!" + req.responseText)}

, complete: function (req, sta) { req = null}

})

}

这里需要处理一个MD5秒传的逻辑,当服务器存在相同文件时,不需要用户再上传,而是直接通知用户秒传

this.post_complete_quick = function ()

{

this.fileSvr.perSvr = "100%"

this.fileSvr.complete = true

this.ui.btn.stop.hide()

this.ui.process.css("width", "100%")

this.ui.percent.text("(100%)")

this.ui.msg.text("服务器存在相同文件,快速上传成功。")

this.Manager.arrFilesComplete.push(this)

this.State = this.Config.state.Complete

//从上传列表中删除

this.Manager.RemoveQueuePost(this.fileSvr.id)

//从未上传列表中删除

this.Manager.RemoveQueueWait(this.fileSvr.id)

//添加到文件列表

this.post_next()

this.event.fileComplete(this)//触发事件

}

这里可以看到秒传的逻辑是非常 简单的,并不是特别的复杂。

var form = new FormData()

form.append("username","zxj")

form.append("avatar",file)

//var form = new FormData($("#postForm")[0])

$.ajax({

url:"${pageContext.request.contextPath}/UploadServlet",

type:"post",

data:form,

processData:false,

contentType:false,

success:function(data){

console.log(data)

}

})

java部分

文件初始化的逻辑,主要代码如下

FileInf fileSvr= new FileInf()

fileSvr.id = id

fileSvr.fdChild = false

fileSvr.uid = Integer.parseInt(uid)

fileSvr.nameLoc = PathTool.getName(pathLoc)

fileSvr.pathLoc = pathLoc

fileSvr.lenLoc = Long.parseLong(lenLoc)

fileSvr.sizeLoc = sizeLoc

fileSvr.deleted = false

fileSvr.md5 = md5

fileSvr.nameSvr = fileSvr.nameLoc

//所有单个文件均以uuid/file方式存储

PathBuilderUuid pb = new PathBuilderUuid()

fileSvr.pathSvr = pb.genFile(fileSvr.uid,fileSvr)

fileSvr.pathSvr = fileSvr.pathSvr.replace("\\","/")

DBConfig cfg = new DBConfig()

DBFile db = cfg.db()

FileInf fileExist = new FileInf()

boolean exist = db.exist_file(md5,fileExist)

//数据库已存在相同文件,且有上传进度,则直接使用此信息

if(exist &&fileExist.lenSvr >1)

{

fileSvr.nameSvr = fileExist.nameSvr

fileSvr.pathSvr= fileExist.pathSvr

fileSvr.perSvr = fileExist.perSvr

fileSvr.lenSvr = fileExist.lenSvr

fileSvr.complete = fileExist.complete

db.Add(fileSvr)

//触发事件

up6_biz_event.file_create_same(fileSvr)

}//此文件不存在

else

{

db.Add(fileSvr)

//触发事件

up6_biz_event.file_create(fileSvr)

FileBlockWriter fr = new FileBlockWriter()

fr.CreateFile(fileSvr.pathSvr,fileSvr.lenLoc)

}

接收文件块数据,在这个逻辑中我们接收文件块数据。控件对数据进行了优化,可以方便调试。如果用监控工具可以看到控件提交的数据。

boolean isMultipart = ServletFileUpload.isMultipartContent(request)

FileItemFactory factory = new DiskFileItemFactory()

ServletFileUpload upload = new ServletFileUpload(factory)

List files = null

try

{

files = upload.parseRequest(request)

}

catch (FileUploadException e)

{// 解析文件数据错误

out.println("read file data error:" + e.toString())

return

}

FileItem rangeFile = null

// 得到所有上传的文件

Iterator fileItr = files.iterator()

// 循环处理所有文件

while (fileItr.hasNext())

{

// 得到当前文件

rangeFile = (FileItem) fileItr.next()

if(StringUtils.equals( rangeFile.getFieldName(),"pathSvr"))

{

pathSvr = rangeFile.getString()

pathSvr = PathTool.url_decode(pathSvr)

}

}

boolean verify = false

String msg = ""

String md5Svr = ""

long blockSizeSvr = rangeFile.getSize()

if(!StringUtils.isBlank(blockMd5))

{

md5Svr = Md5Tool.fileToMD5(rangeFile.getInputStream())

}

verify = Integer.parseInt(blockSize) == blockSizeSvr

if(!verify)

{

msg = "block size error sizeSvr:" + blockSizeSvr + "sizeLoc:" + blockSize

}

if(verify &&!StringUtils.isBlank(blockMd5))

{

verify = md5Svr.equals(blockMd5)

if(!verify) msg = "block md5 error"

}

if(verify)

{

//保存文件块数据

FileBlockWriter res = new FileBlockWriter()

//仅第一块创建

if( Integer.parseInt(blockIndex)==1) res.CreateFile(pathSvr,Long.parseLong(lenLoc))

res.write( Long.parseLong(blockOffset),pathSvr,rangeFile)

up6_biz_event.file_post_block(id,Integer.parseInt(blockIndex))

JSONObject o = new JSONObject()

o.put("msg", "ok")

o.put("md5", md5Svr)

o.put("offset", blockOffset)//基于文件的块偏移位置

msg = o.toString()

}

rangeFile.delete()

out.write(msg)


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/tougao/12233676.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-22
下一篇 2023-05-22

发表评论

登录后才能评论

评论列表(0条)

保存