@wrlqwe
2016-06-05T15:27:19.000000Z
字数 2499
阅读 934
iOS
开发
由于移动网络的不稳定性,为了节省用户的流量,提升用户体验,在做文件下载时,经常遇到断点续传的需求。
项目在以前其实有断点续传的实现,只不过并不完善,这次在重构过程中,iOS客户端放弃了之前被修改的千疮百孔的ASIHTTPRequest,转用iOS7开始提供的NSURLSession,在使用过程中,发现以前服务端配套的断点逻辑并不完善,两端配合完成了改造。
当前的断点续传方案大多基于HTTP1.1,客户端上的大多数网络请求的第三方框架,都有良好的兼容性。而服务端在自定义文件传输时,往往要注意的比客户端要多,下面介绍断点续传的理论基础。
HTTP1.1 使用 Range 和 Content-Range Header来确定传输的资源的部分,它们分别在Request-Header和Response-Header里,指定要请求和要提供的部分。
Range 指定要请求实体(entity)的范围,和多数的程序规范一样,它也是从 0 开始计数的。比如前面已经下载了 500 bytes 的内容,要请求 500 bytes 以后的内容,只要在 HTTP 请求的头部加上 Range: bytes=500- 。
Range 有几种不同的方式来限定范围:
500-900:指定开始到结束这一段的长度,Range 是从 0 计数 的,所以这个是要求服务器从 501 字节开始传,一直到 901 字节结束。
500-:从 501 bytes 之后开始传,一直传到最后。
-500:如果范围没有指定开始位置,就是要服务器端传递倒数 500 字节的内容。而不是从 0 开始的 500 字节。
500-900, 1000-2000:也可以同时指定多个范围的内容。
服务端通过 ETag 和 Last-Modified 两个header,为该资源确定版本,以防下载文件不一致。
ETag: 用来唯一标识一个文件,它是一串字符,ETag机制同时支持强校验和弱校验。它们通过ETag标识符的开头是否存在“W/”来区分,在断点续传里都是强校验。
Last-Modified:就是这个文件上次修改的时间。
如果没有版本控制的需求,这两个字段服务端可以不加,这也是之前我们的服务端的实现方式。
如果要控制版本,提供一个就可以完成断点续传,自定义服务时,需要读取客户端提供的 ETag 或 Last-Modified 字段进行比较,通过 200 状态码告知客户端文件变更重新下载,206 告知客户端断点续传继续传输。每次服务端的返回里都要有 ETag 或 Last-Modified 字段。
iOS7 开始,Apple 废弃了之前的NSURLConnection,改而推荐用 NSURLSession 实现网络请求,NSURLSession,NSURLSession 是一个强大的系统工具,原生支持断点续传。
NSURLSession 在开始下载时,提供了NSURLSessionDownloadTask,用来处理下载中用户的行为:
public func downloadTaskWithRequest(request: NSURLRequest, completionHandler: (NSURL?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDownloadTask
public func downloadTaskWithURL(url: NSURL, completionHandler: (NSURL?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDownloadTask
public class NSURLSessionDownloadTask : NSURLSessionTask {
/* Cancel the download (and calls the superclass -cancel). If
* conditions will allow for resuming the download in the future, the
* callback will be called with an opaque data blob, which may be used
* with -downloadTaskWithResumeData: to attempt to resume the download.
* If resume data cannot be created, the completion handler will be
* called with nil resumeData.
*/
public func cancelByProducingResumeData(completionHandler: (NSData?) -> Void)
}
我们可以在下载中,调用 cancelByProducingResumeData 方法主动停止下载,并且保存下载中途的数据。
在合适的时候, Resume Download:
public func downloadTaskWithResumeData(resumeData: NSData, completionHandler: (NSURL?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDownloadTask
NSURLSession只有这一个方法可以断点续传。而在非用户暂停,因为网络原因失败的下载,要如何获取下载中断的数据呢?
NSURLSession的API定义里,声明了一个 NSURLSessionDownloadTaskResumeData 字段,在失败的NSError的UserInfo里,提供了失败的请求data,允许我们在请求意外中断时保存数据:
/* Key in the userInfo dictionary of an NSError received during a failed download. */
public let NSURLSessionDownloadTaskResumeData: String
在这两个地方处理断点数据,无论是意外终端,还是用户暂停,都能很好的处理断点数据,辅助断点续传。