大多数浏览器和
Developer App 均支持流媒体播放。
-
构建稳健、可恢复的文件传输
了解 URLSession 如何帮助你的 App 传输大文件并从网络中断中实现恢复。学习如何暂停并恢复 HTTP 文件传输以及如何支持可恢复上传,探索使用 URLSession 传输文件的最佳方法,即便你的 App 在后台挂起时也同样适用。
章节
- 0:00 - Welcome
- 2:42 - Explore the resumable downloads protocol in HTTP
- 4:23 - Pause and resume downloads with URLSession
- 7:50 - Pause and resume uploads with URLSession
- 9:45 - Explore the resumable uploads protocol in HTTP
- 12:46 - Add resumable uploads to SwiftNIO
- 16:11 - Use background URLSession
资源
相关视频
WWDC19
-
下载
♪ ♪
Jonathan:大家好 我是 Jonathan 一名 Internet Technologies 团队的工程师 今天 我们来谈谈如何使用 URLSession 构建稳健且可恢复的文件传输 大文件传输一直都极具挑战性 因为即便是个小小的中断 也会导致用户丢失所有的进度 迫使他们从头开始 并且 传输文件越大 耗费的时间就越长 而耗费的时间越长 出错的概率就会增加
并且在传输过程中 用户可能会离开你的 App 离开 Wi-Fi 所覆盖范围 或碰到许多脱离掌控的网络问题 在本次讲座中 我们将深入探讨应对这些挑战的方法 并为你的用户提供稳健的网络服务 首先 我们谈谈 可恢复的 HTTP 协议 该协议允许用户 在连接中断时保留进度 由于可以避免时间和带宽的浪费 该协议因此成为 传输大量数据过程中一件强大工具 接下来 我将演示如何在 URLSession 恢复下载和上传 其中 我会使用 用于恢复上传任务的全新 API 了解该 API 背后的机制十分有用 因为这不仅可以帮你调试 App 甚至能帮你构建自己的服务器支持 这就是为什么接下来 我们还会介绍可恢复协议 因为只有这样你才能清楚了解 App 和 服务器是如何通过 HTTP 实现可恢复的
接着 谈到服务器 你还将了解如何在使用 SwiftNIO 的 服务器中引入可恢复上传支持
最后 我们还将回顾后台 URLSessions 是如何 妥善处理用户和网络的中断的同时 实现系统资源的高效利用
我们来进一步了解 URLSession 中下载和上传的恢复 现在我在下载最新版 Xcode 且 7 千兆字节的下载即将完成 但就在最后时刻 我的 Wi-Fi 断了 但请看 下载已经暂停了 在 Wi-Fi 恢复后 我又可以从中断处恢复下载了
这样我就节省了大量的时间 和几千兆的带宽 可恢复下载真是太棒了 但其工作原理是什么呢? 首先 客户端会发送 GET 请求 并在服务器中检索下载内容
接着 服务器会在响应中使用 Accept-Ranges 标头 通告其支持可恢复下载 Accept-Ranges 标头中的字节 表示服务器支持 对其资源中特定字节的范围请求 服务器响应还包含 名为 ETag 的内容 该内容可以唯一标识当前的资源 如果服务器上的内容发生了变化 那么 ETag 也随之发生变化
那么 假如下载出现了中断会发生什么呢?
由于客户端已经存储部分下载数据 因此其只需要下载最后的部分 为了实现这一点 客户端会发送范围请求 检索下载中缺失的字节 该请求会使用范围字段 指示具体的字节 但客户端同时还要确保 资源没有发生任何改变 否则已存储的旧资源 便会附加来自新资源的数据 为了防止这种情况的发生 If-Range 字段会包含 先前响应中收到的 ETag 用来告知服务器仅在 ETag 保持不变的 情况下 才可发送部分数据
如果 ETag 保持不变 服务器便会响应 206 Partial Content 这里的内容范围字段表示 该响应中的字节范围 从而实现该部分内容的下载
URLSession 自出现以来 便一直利用范围请求提供 暂停和恢复下载任务的 API 此外 你还能暂停和恢复上传任务 使用 URLSession 你不仅能手动暂停正在进行的任务 还可以执行错误处理 以从意外连接问题中实现恢复 并从中断处恢复传输 我们先来回顾一下 该技术在下载中的工作原理 假设你创建了一个 UI 允许用户手动暂停及恢复下载 与 Safari 浏览器类似 你的 App 拥有该 UI 但是由于位于底层 因此你便可以使用 URLSession 来处理所有细节 例如跟踪部分下载数据、ETag 及请求标头 如果需要开始下载 你可以照常创建一个下载任务 并通过调用 resume 来进行启动 如果需要暂停下载 例如用户点击了暂停按钮 你则可以调用 cancelByProducingResumeData 为了之后可以恢复下载 URLSession 将需要关于部分下载的信息 例如 ETag、 当前大小以及其所处磁盘位置 这些信息与其他元数据一起 方便存储在该函数返回的 resumeData对象中 需要强调的是 该 resumeData 并非部分下载的数据 如果 resumeData 为 nil 这就意味着一个或多个 可恢复下载请求无法得到满足 这一点我们接下来会进行介绍 而如果 resumeData 不为 nil 那么你需要将其进行存储以备后用 因为要想恢复下载 例如用户点击了恢复按钮 你就需要将存储的数据传递给 downloadTask 中的 withResumeData 方法 整个过程就是这么简单! 这种模式十分适合手动暂停下载 但 URLSession 还提供了 从意外连接中断中恢复的方法
如果你的下载任务因网络问题失败 你可以在 error 中 查看是否存在 resumeData 如果下载可以恢复 error 中 userInfo 字典 将包含该 resumeData 你可以使用 URLError 的 downloadTaskResumeData 属性 实现对该数据的轻松访问 在 URLSession 中 可恢复下载需要满足以下几个条件 下载的本质就是获取数据 并且这一过程可以安全地重复执行 所以 URLSession 要求下载任务 使用 HTTP GET 请求 其他方案或方法都不予支持 其次 服务器必须支持字节范围请求 并需要使用 Accept-Ranges 标头 进行通告 服务器必须为响应中的资源 提供 ETag 或 Last-Modified 字段 但 ETag 是首选 最后 临时下载文件不可被系统删除 以释放磁盘空间
满足了这些要求 你便可以手动暂停并恢复下载 甚至可以从连接中断处进行恢复 但如果没有恢复协议 即便是小小的中断 你也需要从头开始重新传输 这点对于上传而言 问题会尤为严重 由于上传速度 通常比下载速度慢得多 因此 重新启动意味着 会浪费更多的时间和资源 iOS 17 针对可恢复上传任务 引入了全新支持 对此我感到十分激动 现在 只要服务器支持最新的协议草案 上传任务就可以实现自动恢复 我们先来看看全新的 API 再来深入了解 可恢复上传协议的细节 和下载任务类似 你只需要创建一个上传任务 然后调用 resume 即可实现启动 如果你需要暂停 上传任务 现在也支持和下载任务一样的 cancelByProducingResumeData 方法 任务会自动检测服务器是否支持 最新版的可恢复上传协议 如果服务器支持的话 你就可以对 resumeData 进行存储以备后用
最后 若你需要恢复被暂停的上传 使用 newuploadTask 中的 withResumeData 方法即可 你可以注意到 这里展示的模式 和下载任务完全相同 这就意味着 如果你已经在 App 中 创造了暂停和恢复下载的出色体验 那么你也可以为用户轻松实现 可恢复上传 如果只是出现暂时的网络中断 但服务器仍可进行访问 URLSession 就会自动尝试恢复上传 而无需额外的代码 但如果出现其他更广泛的连接问题 如你的网络或服务器完全陷入瘫痪 你就需要像在下载任务中那样 在 error 中查看 是否存在 resumeData 我非常高兴在 URLSession 中 可以看到可恢复上传 希望你也会爱上该功能 但如果你想充分利用该功能 你的服务器还必须支持 最新版的可恢复上传传输协议
该协议目前正在开发中 整个行业都在努力让其 成为 IETF 标准 该协议中 客户端可以自动发现服务器支持 这就意味着 URLSession 在接收第一次请求时 便开始让所有上传具备可恢复性 如果服务器不支持可恢复上传 那么请求便只能 作为常规上传持续下去 我们来看看该协议 在通信中是如何工作的
首先 客户端向上传端点发送请求 Upload-Incomplete 字段 表示该客户端支持可恢复上传 问号零是所谓的结构化字段布尔值 表示值为 false 这就表明所有的上传数据都 包含在该请求的正文中
如果服务器支持可恢复上传 它将检测客户端的标头 并使用 104 信息响应 通告其支持可恢复上传 104 响应中的 位置字段使用可恢复 URL 该可恢复 URL 可以唯一识别上传 因此当连接中断时 客户端知道从何处恢复上传 服务器将接收到的上传数据 与其唯一可恢复 URL 进行关联
如果上传过程中没有出现中断 那就很好 服务器发送 201 响应后 该过程便结束了 但如果上传过程中出现了中断 客户端和服务器 便需要执行可恢复上传过程
虽然服务器已从可恢复 URL 中 存储了部分上传 但客户端仍然需要确认 服务器实际接收了多少数据 为此 客户端会向可恢复 URL 发送 HEAD 请求 向服务器询问上传偏移量 该偏移量指服务器实际接收字节数
接着 服务器就会根据客户端 特定的上传响应上传偏移量
最后 客户端需要对服务器的 偏移量进行确认并发送剩余数据 为此 客户端会使用匹配的上传偏移量 向可恢复 URL 发送 PATCH 请求 该请求的正文包含了 从给定偏移量开始的所有上传数据
这一步完成之后 客户端才最终将 所有数据发送至服务器 从而完成上传 并且 你的 App 可以免费使用 URLSession 中的上传任务 接着 我们快速了解一下服务器端 并深入探讨如何使用 SwiftNIO 构建属于你的可恢复上传服务器
如果你已经在服务器上 使用了 SwiftNIO 那么该部分便非常符合你当前需要 任何服务器都可以实现可恢复上传 但如果你的服务器 已经使用了 SwiftNIO 那么这个新软件包便可让你 轻松添加支持 我们来看一个简单的例子 如果你还不熟悉 SwiftNIO 就是一个可用于 App 和服务器的 异步网络应用框架 在这段示例代码中 我们设置了 一个 HTTP/2 服务器 并在服务器上添加了两个处理器 编解码器会将 HTTP/2 帧 转换为示例处理器 可以理解的请求 反过来 该编解码器也会 接收示例处理器中的响应 并将其编码为 HTTP/2 帧 ExampleChannelHandler 会执行服务器的 基本路由和逻辑 一开始 我们只支持常规上传 让我们一起来看看将服务器转换为 支持可恢复上传是多么轻松吧
首先 我们下载 NIOResumableUpload 项目 将其添加为依赖项 并导入我们的代码中 接着 我们定义一个可恢复上传的上下文 这一步是为了告诉处理器 在生成可恢复 URL 时 使用哪个上传端点
最后 我们将当前处理器封装进 HTTPResumableUploadHandler 中 从而该处理器便可在当前逻辑的 基础上执行可恢复上传过程 在每次上传中 该处理器都会生成 一个随机且安全的可恢复 URL 并将其与上传数据进行关联 一旦连接发生中断 处理器便会保存部分数据 并为我们响应所有可恢复上传过程 哇! 只需几行代码 我们就可以增强服务器的功能 并支持可恢复上传 如果你已经在服务器上使用了 Swift 不妨试试这个! 但不管怎样 请每个人都务必 看看描述文件的链接所提供的 开源示例代码 该示例代码还使用了新的 HTTP 类型 从而你可以在 App 和 Swift on Server 中 使用相同的类型 我们已将该数据类型作为开源软件包 与 SwiftNIO 一起共同发布 所以 你可以查看 Swift 博客进一步 了解更多信息并提供相关反馈
你可能已经注意到 可恢复上传协议 使用的是 104 状态代码信息响应 新的 HTTP 类型可以在服务器端 轻松支持该响应 在 App 中 URLSession 可以 自动处理用于 可恢复上传的 104 响应 除此之外 URLSession 现在还提供了一个委托方法 didReceiveInformationalResponse 该方法让你的 App 也可以处理 其他中间响应 例如 102 Processing 及 103 Early Hints
可恢复协议可以 有效缓解网络中断问题并节省带宽 后台 URLSession 在处理 大文件传输时也发挥着重要作用 假如你的用户希望上传最近 滑雪旅行中的大型 4K 视频 如果他们的连接发生中断 而你希望尽可能保证恢复上传 你可以自己处理所有错误 或者 你也可以让 background session 替你处理
事实上 如果服务器支持 URLSession background session 便可以自动 对恢复下载和上传任务进行处理
如果任务发生了中断 系统便会不断尝试恢复该任务 并且尝试的间隔时间不断增加
如果任务无法恢复 系统便会自动从头开始重试该任务
如果你的用户在滑雪山上 超出了手机信号覆盖范围 又或是 暴风雪影响了他们的 Wi-Fi background session 会始终等待连接 从而设备再次连接到互联网后 任务便可以得到调度
你的用户在上传视频时 他们可能会离开你的 App 或放下设备 也许他们正准备去滑雪 但他们仍然希望上传可以继续进行
这时 background session 便尤为重要 由于后台任务受系统调度 因此该任务便会 脱离你的 App 进程运行 这就意味着 即便你的 App 被系统挂起或终止 你的网络任务依然能保持可靠运行 如果你需要传输耗时很久的大文件 或需要在用户离开 App 后继续保持传输 你便可以使用 background session
最后 如果你希望为用户 提供最佳的 App 体验 你便可以将不太紧急的任务 安排至稍后的时间 使用 background session 你可以以多种方式高效安排网络活动 并为用户节约资源
如果任务不需要立刻发生 那么你便可以考虑在后台配置上 将 isDiscretionary 的 属性设置为 true 这就使得系统在对你的任务 进行智能调度时 同时考虑到“用户是否连接到 Wi-Fi” “用户的设备是否连接到电源” 以及“网络是否受限”等因素 如果你需要下载资源以备后用 上传夜间备份 或分析数据 background session 会是一个不错的选择
如果你希望在低数据模式下 避免使用过多带宽 你便可以考虑将 allowsConstrainedNetworkAccess 的 属性设置为 false 想要了解更多有关 App 中支持低数据模式的提示 请观看网络会议中的最新技术
你还可以将后台任务 安排在稍晚时间开始 因为此时你不太可能使用系统资源 深夜通常是安排 类似大型备份任务的最佳时机
为了进一步协助系统调度 我们可以对 countOfBytesClientExpectsToSend 和 Receive 的属性进行设置 充分利用这些属性 你便能够增强系统能力 更好地实现资源分配 同时将这些优势传递给用户 background session 作为一件绝佳工具 不仅可以用于 无需即刻发生的大文件传输 同时还可在 App 挂起后 实现持续传输 而对于小型任务 及需要尽快完成的任务 你可以使用标准 URLSession 将恢复功能添加至你的 App 让你的用户享受更可靠的网络连接 查看 SwiftNIO 及 HTTP 类型 让我们携手在 Swift 中 共同创造一流的 HTTP 体验 最后 在大型文件传输或自主文件 传输中尝试使用 background session 你将会发现多种节省资源的方法 用于应对用户需要紧急传输的情况 感谢你的观看 请不要错过下方其他精彩网络讲座 本次讲座到此结束! ♪ ♪
-
-
4:53 - Pausing and resuming a URLSessionDownloadTask
let downloadTask = session.downloadTask(with: request) downloadTask.resume()
-
5:21 - Pausing and resuming a URLSessionDownloadTask
let downloadTask = session.downloadTask(with: request) downloadTask.resume() guard let resumeData = await downloadTask.cancelByProducingResumeData() else { // Download cannot be resumed return }
-
6:11 - Pausing and resuming a URLSessionDownloadTask
let downloadTask = session.downloadTask(with: request) downloadTask.resume() guard let resumeData = await downloadTask.cancelByProducingResumeData() else { // Download cannot be resumed return } let newDownloadTask = session.downloadTask(withResumeData: resumeData) newDownloadTask.resume()
-
6:34 - Retrieving resume data on error
do { let (url, response) = try await session.download(for: request) } catch let error as URLError { guard let resumeData = error.downloadTaskResumeData else { // Download cannot be resumed return } }
-
8:29 - Pausing and resuming a URLSessionUploadTask
let uploadTask = session.uploadTask(with: request, fromFile: fileURL) uploadTask.resume()
-
8:37 - Pausing and resuming a URLSessionUploadTask
let uploadTask = session.uploadTask(with: request, fromFile: fileURL) uploadTask.resume() guard let resumeData = await uploadTask.cancelByProducingResumeData() else { // Upload cannot be resumed return }
-
8:57 - Pausing and resuming a URLSessionUploadTask
let uploadTask = session.uploadTask(with: request, fromFile: fileURL) uploadTask.resume() guard let resumeData = await uploadTask.cancelByProducingResumeData() else { // Upload cannot be resumed return } let newUploadTask = session.uploadTask(withResumeData: resumeData) newUploadTask.resume()
-
9:22 - Retrieving resume data on error
do { let (data, response) = try await session.upload(for: request, fromFile: fileURL) } catch let error as URLError { guard let resumeData = error.uploadTaskResumeData else { // Upload cannot be resumed return } }
-
13:15 - Before resumable uploads in Swift NIO
NIOTSListenerBootstrap(group: NIOTSEventLoopGroup()) .childChannelInitializer { channel in channel.configureHTTP2Pipeline(mode: .server) { channel in channel.pipeline.addHandlers([ HTTP2FramePayloadToHTTPServerCodec(), ExampleChannelHandler() ]) }.map { _ in () } } .tlsOptions(tlsOptions)
-
14:06 - Add resumable uploads in Swift NIO
import NIOResumableUpload let uploadContext = HTTPResumableUploadContext(origin: "https://example.com") NIOTSListenerBootstrap(group: NIOTSEventLoopGroup()) .childChannelInitializer { channel in channel.configureHTTP2Pipeline(mode: .server) { channel in channel.pipeline.addHandlers([ HTTP2FramePayloadToHTTPServerCodec(), HTTPResumableUploadHandler(context: uploadContext, handlers: [ ExampleChannelHandler() ]) ]) }.map { _ in () } } .tlsOptions(tlsOptions)
-
15:48 - Informational responses in URLSession
protocol URLSessionTaskDelegate : URLSessionDelegate { optional func urlSession(_ session: URLSession, task: URLSessionTask, didReceiveInformationalResponse response: HTTPURLResponse) }
-
18:19 - Using background URLSession
// Configuring your background session let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app") configuration.isDiscretionary = true configuration.allowsConstrainedNetworkAccess = false let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) // Configuring your background task let backgroundTask = session.uploadTask(with: url, fromFile: fileURL) backgroundTask.earliestBeginDate = .now.addingTimeInterval(60 * 60) backgroundTask.countOfBytesClientExpectsToSend = 500 * 1024 backgroundTask.countOfBytesClientExpectsToReceive = 200
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。