• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 文件上传下载性能提升指导
2<!--Kit: Common-->
3<!--Subsystem: Demo&Sample-->
4<!--Owner: @mgy917-->
5<!--Designer: @jiangwensai-->
6<!--Tester: @Lyuxin-->
7<!--Adviser: @huipeizi-->
8
9## 概述
10
11在开发应用时,要实现高效的客户端跟服务器之间数据交换,文件传输的性能是至关重要的。一个数据交换性能较低的应用会导致其在加载过程中耗费较长时间,在很多的场景造成页面卡顿,极大的影响了用户体验。相反,一个数据交换高效的应用,则会让应用变得更加流畅。
12
13本文将介绍两种常见的上传下载传输和网络请求的关键技术:数据压缩和断点续传,可提升上传下载的性能、减少宽带占用,从而提高数据传输效率。
14
15## 上传下载接口
16
17目前系统内提供给文件上传下载可用的模块有[@ohos.net.http](../reference/apis-network-kit/js-apis-http.md)模块和[@ohos.request](../reference/apis-basic-services-kit/js-apis-request.md)模块。[@ohos.net.http](../reference/apis-network-kit/js-apis-http.md)模块提供基础的HTTP数据请求能力,功能较为基础,本文不做介绍。[@ohos.request](../reference/apis-basic-services-kit/js-apis-request.md)模块主要给应用提供上传下载文件、后台传输代理的基础能力。它具备任务管理系统的默认并发功能,简化下载功能的实现和管理,提升数据传输的安全,整合通知机制,新增任务状态与进度查询功能,具有灵活性、高效性、可扩展性、可靠性、一致性和安全性的优势。
18
19具体来说,[@ohos.request](../reference/apis-basic-services-kit/js-apis-request.md)模块包括以下功能:
20
211. 任务管理:任务管理操作包括创建任务、暂停任务、恢复任务、删除任务、文件上传、文件下载、系统通知等。创建的任务分为前端任务和后台任务。前端任务是立即的、模态界面的、同步的,跟随应用的生命周期,通常数据量较小、耗时短,例如发布微信朋友圈、微博,通常优先级高且倾斜带宽资源。后台任务为可等待的、任意界面的、异步的,通常数据量较大、耗时长,例如缓存一部电影、同步数百兆字节乃至若干吉字节的数据,优先级相较于前端任务低且与应用生命周期无关。
22
232. 任务查询管理:系统查询所有任务、过滤上传任务、过滤下载任务、过滤时间段内任务、过滤前端任务、过滤后台任务、用户查询指定任务信息、用户查询指定隐藏任务信息、系统查询指定任务信息、系统清理指定任务等。
24
253. 任务自动恢复:网络条件不满足时任务不启动或者暂停,满足后自动启动或者恢复(需要HTTP服务器支持断点续传)。
26
274. 安全隐私保护:包括网络权限检查、普通接口仅操作自己创建的任务、任务信息加密存储、系统接口检查、系统接口查询隐匿任务敏感字段、普通接口查询隐匿任务敏感字段、遍历攻击、DOS、僵尸任务、恶意的静默后台任务、系统管理接口权限等。
28
295. 日志:包括调试模式和发布模式。调试模式可打印所有内存修改、磁盘、网络读写、逻辑分支等日志。发布模式下除了导致任务失败、服务异常的日志,其余日志都会关闭。
30
316. 任务失败重试:对于不可恢复的原因,直接失败;对于可恢复的原因,网络断开、网络类型不匹配等,不现场重试,任务到等待网络恢复队列;网络超时则就地重试1次,仍网络超时,则立即失败。
32
337. 服务按需启停:上传下载服务不随系统自启。应用主动调用任意接口,上传下载服务自动启动。网络连接事件会触发上传下载服务启动。在任务队列中,没有正在处理的任务,或者等待网络恢复的任务,延迟10秒钟,再check一次,仍旧没有的,则通知系统服务框架(SAMGR)可以停止并卸载上传下载服务。在服务退出过程中,新的接口请求可能失败,在客户端检查服务状态、通过重试按需启动。
34
358. 通知:任务从第一次开始到最终结束都应该有进度通知。目前采用固定时间间隔触发进度通知,前台任务1秒,后台任务3秒。任务状态的每次变化也要触发进度通知。当任务完成和失败,则触发其专用的进度通知。提供了抑制开关,可以在创建任务时打开,以避免频繁通知。
36
37
38### 下载任务的状态迁移流程
39
40使用[@ohos.request](../reference/apis-basic-services-kit/js-apis-request.md)模块执行下载的任务,具有四种运行状态:初始任务、就绪任务、挂起任务、待网任务、成功任务、失败任务。可以通过create创建任务,start开始任务,pause挂起任务,resume恢复任务,remove移除任务,stop停止任务,任务结果有final-failed任务失败,final-completed下载完成,recoverable-failed重试失败,并支持查询任务状态,具体流程如图一所示:
41
42**图一 模块流程图**
43
44![模块流程图](./figures/file-upload-and-download-pic1.png)
45
46## 常见场景与方案
47
48**场景1:低带宽网络上传琐碎文件场景**
49
50在网络连接较差,低带宽的网络环境中,HTTP连接的建立耗时可能会大幅提升。这时候进行[数据压缩](#数据压缩)可以加快页面加载速度,并减少HTTP请求数量和移动数据流量。
51
52**场景2:处理大量资源的场景**
53
54如应用商店、网盘应用等,这类应用通常拥有大体积的文件资源。当用户从暂停或者断网中重新恢复时,如果从头开始上传下载则会额外耗费大量的时间。此时可以采用[断点续传](#断点续传)方法进行上传下载。
55
56### 数据压缩
57
58数据压缩是指在应用中对数据进行压缩,以减少存储空间和数据传输量、节省带宽,提高加载速度。数据压缩通常在网络传输和存储方面发挥着重要作用,特别是在处理大量数据或需要频繁传输数据的场景下。
59
60在应用开发中,常见的数据压缩技术分类如下:
61
62- 有损压缩:仅限图片视频音频等文件适用。通过减少图片视频文件的分辨率,降低音频的音质等手段,以减少文件的大小,来实现减少加载时间和带宽消耗。
63- 无损压缩:对一些零碎文件可以使用 [@ohos.zlib(Zip模块)](../reference/apis-basic-services-kit/js-apis-zlib.md)来进行打包压缩,减少上传请求次数;对一些大文件可以利用缓存技术,服务器将曾经上传过的大文件MD5码缓存起来,本地在上传前预生成MD5码并传输到服务器进行比对,如果相同则说明服务器存在该文件,可以跳过该文件上传,从而省略重复传输时间。
64
65
66以从相册批量上传图片为例,介绍大量文件打包无损压缩上传相关技术,下图为相关示例的界面截图:
67
68**图二 相册批量上传图片示例图**
69
70![相册批量上传图片示例图](./figures/file-upload-and-download-pic2.png)
71
72以批量上传照片(分辨率为480\*640,24位,平均大小50\~120KB)为例,在RK设备上测试的结果如下表所示:
73
74| 上传照片数量 | 优化前耗时(ms) | 优化后耗时(ms) |
75| --- | --- | ---|
76| 10 | 470 | 526 |
77| 20 | 1124 | 1091 |
78| ... | ... | ... |
79| 50 | 2379 | 2138 |
80| 80 | 3950 | 3258 |
81| ... | ... | ... |
82| 100 | 5276 | 3909 |
83
84**图三 上传数量和耗时对比图表**
85
86![上传数量和耗时对比图表](./figures/file-upload-and-download-pic3.png)
87
88由于上传耗时收到网络状态影响偏差较大,结果取的几次测量结果的最小值。但是仍然可以从数据中看出,优化前的耗时基本为线性增长,压缩优化后的耗时在上传文件数量较低时并不明显,还会因为多余的压缩处理影响耗时。不过随着上传的照片数量增多,优化后的耗时和优化之前的耗时差距越来越明显,优化效果越好。
89
90**数据压缩的相关示例代码如下:**
91
921. 导入相关模块:
93
94```ts
95import { common } from '@kit.AbilityKit';
96import { fileIo } from '@kit.CoreFileKit';
97import { zlib } from '@kit.BasicServicesKit';
98```
992. 创建压缩上传相关类:
100
101```ts
102class ZipUpload {
103  // 创建任务前存放的uri
104  private waitList: Array<string> = [];
105  // 需要上传的文件uri
106  private fileUris: Array<string> = [];
107  // ...
108}
109```
1103. 建立用于接收图库图片的临时文件夹,并将整个临时文件夹打包添加到待上传list内:
111
112```ts
113// 文件压缩处理
114async zipUploadFiles(fileUris: Array<string>): Promise<void> {
115  this.context = this.getUIContext().getHostContext() as common.UIAbilityContext;
116  let cacheDir = this.context?.cacheDir;
117  let tempDir = fileIo.mkdtempSync(`${cacheDir}/XXXXXX`);
118  // 将图库图片获取的uri放入fileUris中,遍历复制到临时文件夹
119  for (let i = 0; i < fileUris.length; i++) {
120    let fileName = fileUris[i].split('/').pop();
121    let resourceFile: fileIo.File = fileIo.openSync(fileUris[i], fileIo.OpenMode.READ_ONLY);
122    fileIo.copyFileSync(resourceFile.fd, `${tempDir}/${fileName}`, 0);
123    fileIo.closeSync(resourceFile);
124  }
125  // 文件压缩,将之前生成的临时文件夹内打包到test.zip126  let options: zlib.Options = {
127    level: zlib.CompressLevel.COMPRESS_LEVEL_DEFAULT_COMPRESSION,
128    memLevel: zlib.MemLevel.MEM_LEVEL_DEFAULT,
129    strategy: zlib.CompressStrategy.COMPRESS_STRATEGY_DEFAULT_STRATEGY
130  };
131  let data = await zlib.compressFile(tempDir, `${cacheDir}/test.zip`, options);
132  // 删除临时文件夹
133  fileIo.rmdirSync(tempDir);
134  // 将生成的zip包放到传输队列
135  this.waitList.push(`${cacheDir}/test.zip`);
136}
137```
138### 断点续传
139
140断点续传功能的实现,不管是应用端还是服务器端都需要用到合理的技术来互相协同。在实际开发中,开发者无需亲自实现断点续传功能,只需对SDK进行合理配置。
141
142在应用端需要用到的技术和API:
143
144- [@ohos.file.fs(文件管理)](../reference/apis-core-file-kit/js-apis-file-fs.md):用于处理文件上传操作,提供了读取文件内容,文件分片和组合的功能。
145- [@ohos.file.hash(文件哈希处理)](../reference/apis-core-file-kit/js-apis-file-hash.md):用于实现文件MD5的计算,将计算的MD5值预先传到服务器端进行预处理,实现文件秒传,同时确保传输的准确性和可靠性。
146- [@ohos.request(上传下载)](../reference/apis-basic-services-kit/js-apis-request.md):用于实现文件上传操作,并支持在上传过程中的断点续传功能。
147
148
149在服务器端需要用到的技术:
150
151- 协议需要支持Range:用于在服务器端支持范围请求,方便处理文件上传下载断点续传功能。
152- 文件校验相关逻辑:需要实现校验文件是否有错,确保在传输中断后能够准确恢复并继续传输。
153
154
155通过结合应用端和服务器端的相关技术,可以共同实现高效且可靠的文件断点续传功能,提供更好的用户体验并确保数据传输的稳定性。
156
157本文基于[上传和下载](https://gitcode.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Connectivity/UploadAndDownLoad)中的后台上传场景,给出了部分断点续传的示例代码,具体可以参考该工程。
158
159**文件上传**
160
161对于大文件断点续传上传,本文采用[@ohos.request(上传下载)](../reference/apis-basic-services-kit/js-apis-request.md)模块中的**request.agent**任务托管接口,可以自动实现暂停继续重试等操作,无需手动将文件分片和记录上传分片信息。流程图如图四所示:
162
163**图四 断点续传上传流程图**
164
165![断点续传上传流程图](./figures/file-upload-and-download-pic4.png)
166
167 **断点续传上传示例代码如下:**
168
169具体可以参考[RequestUpload.ets](https://gitcode.com/openharmony/applications_app_samples/blob/master/code/BasicFeature/Connectivity/UploadAndDownLoad/features/uploadanddownload/src/main/ets/upload/RequestUpload.ets)
170
1711. 导入相关模块:
172```ts
173import { common } from '@kit.AbilityKit';
174import { request } from '@kit.BasicServicesKit';
175```
176
1772. 创建相关上传类:
178```ts
179class Upload {
180  // 后台任务
181  private backgroundTask: request.agent.Task | undefined = undefined;
182  // 创建任务前存放的uri
183  private waitList: Array<string> = [];
184  // ...
185}
186```
1873. 生成MD5码,上传到服务器进行校验:
188```ts
189async checkFileExist(fileUri: string): Promise<boolean> {
190  let httpRequest = http.createHttp();
191  // 生成md5码
192  let md5 = await hash.hash(fileUri, 'md5');
193  let requestOption: http.HttpRequestOptions = {
194    method: http.RequestMethod.POST,
195    extraData: {
196      'MD5': md5
197    }
198  }
199  let response = await httpRequest.request('http://XXX.XXX.XXX.XXX/XXXX', requestOption);
200  let result = response.result;
201  let flag = false;
202  // ...
203  // 根据服务器返回对应数据判断是否存在
204  if (flag) {
205    return true;
206  } else {
207    return false;
208  }
209}
210```
2114. 配置Config,创建后台上传任务:
212```ts
213private config: request.agent.Config = {
214  action: request.agent.Action.UPLOAD,
215  headers: HEADER,
216  url: '',
217  mode: request.agent.Mode.BACKGROUND,
218  method: 'POST',
219  title: 'upload',
220  network: request.agent.Network.ANY,
221  data: [],
222  token: 'UPLOAD_TOKEN'
223}
224// ...
225// 转换uri
226private async getFilesAndData(cacheDir: string, fileUris: Array<string>): Promise<Array<request.agent.FormItem>> {
227// ...
228}
229// 创建文件上传后台任务
230async createBackgroundTask(fileUris: Array<string>) {
231 // 获取上传url
232  this.config.url = 'http://XXX.XXX.XXX.XXX';
233  this.config.mode = request.agent.Mode.BACKGROUND;
234  let tempData = await this.getFilesAndData(this.context.cacheDir, fileUris);
235  // 判断每个文件是否为空
236  for (let i = 0; i < tempData.length; i++) {
237    let flag = await this.checkFileExist(`${this.context.cacheDir}/${tempData[i].name}`);
238    if (!flag) {
239      this.config.data.push(tempData[i])
240    }
241  }
242  let isFileExist = await this.checkFileExist(`${this.context.cacheDir}/${this.config.data[0].name}`);
243  if (this.config.data.length === 0) {
244    return;
245  }
246  this.backgroundTask = await request.agent.create(this.context, this.config);
247}
248```
2495. 任务开始:
250```ts
251await this.backgroundTask.start();
252```
2536. 任务暂停:
254```ts
255async pause() {
256  if (this.backgroundTask === undefined) {
257    return;
258  }
259  await this.backgroundTask.pause();
260}
261```
2627. 任务继续:
263```ts
264async resume() {
265  if (this.backgroundTask === undefined) {
266    return;
267  }
268  await this.backgroundTask.resume();
269}
270```
271**文件下载**
272
273对于大文件断点续传下载,也可以直接调用**request.agent**接口,该接口的断点续传是基于HTTP协议Header里的Range字段实现的,在任务暂停重启的时候,会自动设置Header中的Range字段,无需进行额外的配置。
274
275> **Range简介**
276>
277> HTTP协议里面的Range字段,官方名称为范围请求,它允许服务器只发送 HTTP
278> 消息的一部分到客户端,可以用来请求部分数据而不是整个资源。
279>
280> Range的格式通常是Range:
281> `<unit>=<start>-<end>`,其中`<unit>`表示范围所采用的单位,通常是字节(bytes),`<start>` 和 `<end>` 表示请求的起始字节和结束字节的位置。
282>
283> Range语法如下:
284> ```ts
285> // 表示从range-start到文件末尾
286> Range: <unit>=<range-start>-
287> // 表示从range-start到range-end
288> Range: <unit>=<range-start>-<range-end>
289> // 可以同时选择多段,用逗号分隔
290> Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
291>
292> // 示例:表示返回1024btyes之后的文件
293> Range: bytes=1024-
294> ```
295> 服务器收到请求后,正确处理请求会回复206 Partial
296> Content,未正常处理则会回复其他响应码。下表是服务器回复的常见响应码:
297>
298> | 服务器响应码 | 常见的原因 |
299> | ------------------ | -----------------|
300> | 206 Partial Content | 服务器收到正常Range请求的响应码,返回部分内容的响应。|
301> | 416 Range Not Satisfiable | 所请求的范围不合法,表示服务器错误。|
302> |200 OK | 服务器忽略了 Range 首部,返回整个文件。|
303>
304
305**断点续传下载示例代码如下:**
306
307具体可以参考[RequestDownload.ets](https://gitcode.com/openharmony/applications_app_samples/blob/master/code/BasicFeature/Connectivity/UploadAndDownLoad/features/uploadanddownload/src/main/ets/download/RequestDownload.ets)
308
3091. 导入模块:
310```ts
311import { common } from '@kit.AbilityKit';
312import { request } from '@kit.BasicServicesKit';
313```
3142. 创建下载类:
315```ts
316class Download {
317  // 任务存放前的uri
318  private waitList: Array<string[]> = [];
319  // 下载任务
320  private downloadTask: request.agent.Task | undefined = undefined;
321  // 后台任务下载列表
322  private backgroundDownloadTaskList: Array<request.agent.Task> = [];
323  // ...
324}
325```
3263. 配置Config,创建后台下载任务:
327```ts
328async createBackgroundTask(downloadList: Array<string[]>) {
329  let splitUrl = url.split('//')[1].split('/');
330  let context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
331  let downloadConfig: request.agent.Config = {
332    action: request.agent.Action.DOWNLOAD,
333    url: url,
334    method: 'POST',
335    title: 'download',
336    mode: request.agent.Mode.FOREGROUND, // 必须是后台任务才能续传
337    network: request.agent.Network.ANY,
338    saveas: `./${folder}/${splitUrl[splitUrl.length-1]}`,
339    overwrite: true
340  }
341  this.downloadTask = await request.agent.create(context, downloadConfig);
342  if (this.backgroundDownloadTaskList.findIndex(task => task.config.url === downTask.config.url) === -1) {
343    this.backgroundDownloadTaskList.push(downTask);
344  }
345}
346```
3474. 任务开始:
348```ts
349await downTask.start();
350```
3515. 任务暂停:
352```ts
353async pause() {
354  if (this.backgroundDownloadTaskList.length === 0) {
355    return;
356  }
357  this.backgroundDownloadTaskList.forEach(async task => {
358    await task.pause();
359  })
360}
361```
3626. 任务继续:
363```ts
364async resume() {
365  if (this.backgroundDownloadTaskList.length === 0) {
366    return;
367  }
368  this.backgroundDownloadTaskList.forEach(async task => {
369    await task.resume();
370  })
371}
372```
3737. 任务停止:
374```ts
375async deleteAllBackTasks() {
376  if (this.backgroundDownloadTaskList.length > 0) {
377    this.backgroundDownloadTaskList.forEach(async task => {
378      await request.agent.remove(task.tid);
379    })
380    this.backgroundDownloadTaskList = [];
381  }
382}
383```
384
385## 相关实例
386
387针对断点续传,有以下相关实例可以参考:
388
389- [上传和下载](https://gitcode.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Connectivity/UploadAndDownLoad)