在开发应用时,客户端与服务器之间数据交换的效率取决于文件传输的性能。一个数据交换性能较低的应用会导致其在加载过程中耗费较长时间,在很多的场景造成页面卡顿,极大的影响了用户体验。相反,一个数据交换高效的应用,则会让应用变得更加流畅。
本文将介绍两种常见的上传下载传输和网络请求的关键技术:数据压缩和断点续传,可减少宽带占用,提高传输效率,从而达到提升数据交换性能。
目前系统内提供给文件上传下载可用的模块有http模块和request模块。http模块提供基础的HTTP数据请求能力,功能较为基础,本文不做介绍。request模块主要给应用提供上传下载文件、后台传输代理的基础能力。它具备任务管理系统的默认并发功能,可简化下载功能的实现和管理,提升数据传输的安全性,整合通知机制,新增任务状态与进度查询功能,具有灵活性、高效性、可扩展性、可靠性、一致性和安全性的优势。
具体来说,request模块包括以下功能:
使用request模块执行下载的任务,具有四种运行状态:初始任务、就绪任务、挂起任务、待网任务。可以通过create创建任务,start开始任务,pause挂起任务,resume恢复任务,remove移除任务,stop停止任务,任务结果有final-failed任务失败,final-completed下载完成,recoverable-failed重试失败,并支持查询任务状态,具体流程如下图所示:
场景1:低带宽网络上传琐碎文件场景
在网络连接较差,低带宽的网络环境中,HTTP连接的建立耗时可能会大幅提升。这时候进行[数据压缩]可以加快页面加载速度,并减少HTTP请求数量和移动数据流量。
场景2:处理大量资源的场景
如应用商店、网盘应用等,这类应用通常拥有大体积的文件资源。当用户从暂停或者断网中重新恢复时,如果从头开始上传下载则会额外耗费大量的时间。此时可以采用[断点续传]方法进行上传下载。
数据压缩是指在应用中对数据进行压缩,以减少存储空间和数据传输量、节省带宽,提高加载速度。数据压缩通常在网络传输和存储方面发挥着重要作用,特别是在处理大量数据或需要频繁传输数据的场景下。
在应用开发中,常见的数据压缩技术分类如下:
以批量上传照片(分辨率为480*640,24位,平均大小50~120KB)为例,在设备上测试的结果如下表所示:
上传照片数量 | 优化前耗时(ms) | 优化后耗时(ms) |
---|---|---|
10 | 470 | 526 |
20 | 1124 | 1091 |
… | … | … |
50 | 2379 | 2138 |
80 | 3950 | 3258 |
… | … | … |
100 | 5276 | 3906 |
由于上传耗时收到网络状态影响偏差较大,结果取的几次测量结果的最小值。但是仍然可以从数据中看出,优化前的耗时基本为线性增长,压缩优化后的耗时在上传文件数量较低时并不明显,还会因为多余的压缩处理影响耗时。不过随着上传的照片数量增多,优化后的耗时和优化之前的耗时差距越来越明显,优化效果越好。
数据压缩的相关示例代码如下:
import { common } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit'
import { zlib } from '@kit.BasicServicesKit';
class ZipUpload {
// 创建任务前存放的uri
private waitList: Array = [];
// 需要上传的文件uri
private fileUris: Array = [];
...
}
// 文件压缩处理
async zipUploadFiles(fileUris: Array): Promise {
this.context = getContext(this) as common.UIAbilityContext;
let cacheDir = this.context.cacheDir;
let tempDir = fileIo.mkdtempSync(`${cacheDir}/XXXXXX`);
// 将图库图片获取的uri放入fileUris中,遍历复制到临时文件夹
for (let i = 0; i < fileUris.length; i++) {
let fileName = fileUris[i].split('/').pop();
let resourceFile: fileIo.File = fileIo.openSync(fileUris[i], fileIo.OpenMode.READ_ONLY);
fileIo.copyFileSync(resourceFile.fd, `${tempDir}/${fileName}`, 0);
fileIo.closeSync(resourceFile);
}
// 文件压缩,将之前生成的临时文件夹内打包到test.zip内
let options: zlib.Options = {
level: zlib.CompressLevel.COMPRESS_LEVEL_DEFAULT_COMPRESSION,
memLevel: zlib.MemLevel.MEM_LEVEL_DEFAULT,
strategy: zlib.CompressStrategy.COMPRESS_STRATEGY_DEFAULT_STRATEGY
};
let data = await zlib.compressFile(tempDir, `${cacheDir}/test.zip`, options);
// 删除临时文件夹
fileIo.rmdirSync(tempDir);
// 将生成的zip包放到传输队列
this.waitList.push(`${cacheDir}/test.zip`);
}
断点续传功能的实现,不管是应用端还是服务器端都需要用到合理的技术来互相协同。在实际开发中,开发者无需亲自实现断点续传功能,只需对SDK进行合理配置。
在应用端需要用到的技术和API:
在服务器端需要用到的技术:
通过结合应用端和服务器端的相关技术,可以共同实现高效且可靠的文件断点续传功能,提供更好的用户体验并确保数据传输的稳定性。
本文基于[上传和下载]中的后台上传场景,给出了部分断点续传的示例代码,具体可以参考该工程。
对于大文件断点续传上传,本文采用request(上传下载)模块中的request.agent任务托管接口,可以自动实现暂停继续重试等操作,无需手动将文件分片和记录上传分片信息。流程图如下图所示:
class Upload {
// 后台任务
private backgroundTask: request.agent.Task | undefined = undefined;
// 创建任务前存放的uri
private waitList: Array = [];
...
}
class Upload {
// 后台任务
private backgroundTask: request.agent.Task | undefined = undefined;
// 创建任务前存放的uri
private waitList: Array = [];
...
}
private config: request.agent.Config = {
action: request.agent.Action.UPLOAD,
headers: HEADER,
url: '',
mode: request.agent.Mode.BACKGROUND,
method: 'POST',
title: 'upload',
network: request.agent.Network.ANY,
data: [],
token: 'UPLOAD_TOKEN'
}
...
// 转换uri
private async getFilesAndData(cacheDir: string, fileUris: Array): Promise> {
...
}
// 创建文件上传后台任务
async createBackgroundTask(fileUris: Array) {
if (this.context === undefined) {
return;
}
// 获取上传url
this.config.url = await urlUtils.getUrl(this.context);
this.config.data = await this.getFilesAndData(this.context.cacheDir, fileUris);
this.config.mode = request.agent.Mode.BACKGROUND;
try {
this.backgroundTask = await request.agent.create(this.context, this.config);
await this.backgroundTask.start();
let state = AppStorage.get('backTaskState');
if (state === BackgroundTaskState.PAUSE) {
await this.backgroundTask.pause();
}
logger.info(TAG, `createBackgroundTask success`);
} catch (err) {
logger.error(TAG, `task err, err = ${JSON.stringify(err)}`);
}
}
await this.backgroundTask.start();
async pause() {
...
if (this.backgroundTask === undefined) {
return;
}
await this.backgroundTask.pause();
}
async resume() {
...
if (this.backgroundTask === undefined) {
return;
}
await this.backgroundTask.resume();
}
对于大文件断点续传下载,也可以直接调用request.agent接口,该接口的断点续传是基于HTTP协议Header里的Range字段实现的,在任务暂停重启的时候,会自动设置Header中的Range字段,无需进行额外的配置。
Range简介
HTTP协议里面的Range字段,官方名称为范围请求,它允许服务器只发送 HTTP 消息的一部分到客户端,可以用来请求部分数据而不是整个资源。
Range的格式通常是Range: =-,其中表示范围所采用的单位,通常是字节(bytes), 和 表示请求的起始字节和结束字节的位置。
Range语法如下:
// 表示从range-start到文件末尾
Range: =-
// 表示从range-start到range-end
Range: =-
// 可以同时选择多段,用逗号分隔
Range: =-, -
// 示例:表示返回1024btyes之后的文件
Range: bytes=1024-
服务器收到请求后,正确处理请求会回复206 Partial Content,未正常处理则会回复其他响应码。下表是服务器回复的常见响应码:
服务器响应码 | 常见的原因 |
---|---|
206 Partial Content | 服务器收到正常Range请求的响应码,返回部分内容的响应。 |
416 Range Not Satisfiable | 所请求的范围不合法,表示服务器错误。 |
200 OK | 服务器忽略了 Range 首部,返回整个文件。 |
断点续传下载示例代码如下:
import { common } from '@kit.AbilityKit';
import { request } from '@kit.BasicServicesKit';
class RequestDownload {
// 任务存放前的uri
private waitList: Array = [];
// 下载任务
private downloadTask: request.agent.Task | undefined = undefined;
...
}
async createBackgroundTask(downloadList: Array) {
if (this.context === undefined) {
return;
}
for (let i = 0; i < downloadList.length; i++) {
try {
let splitUrl = downloadList[i][1].split('//')[1].split('/');
let downloadConfig: request.agent.Config = {
action: request.agent.Action.DOWNLOAD,
url: downloadList[i][1],
method: 'POST',
title: 'download',
mode: request.agent.Mode.BACKGROUND, // 必须是后台任务才能续传
network: request.agent.Network.ANY,
saveas: `./${downloadList[i][0]}/${splitUrl[splitUrl.length-1]}`,
overwrite: true,
gauge: true
}
let downTask = await request.agent.create(this.context, downloadConfig);
if (this.backgroundDownloadTaskList.findIndex(task => task.config.url === downTask.config.url) === -1) {
this.backgroundDownloadTaskList.push(downTask);
}
await downTask.start();
} catch (err) {
logger.error(TAG, `task err, err = ${JSON.stringify(err)}`);
this.waitList.push(downloadList[i]);
}
}
}
...
await downTask.start();
...
文件下载监听是指在单文件下载的功能基础上,同时进行多个文件下载进度和状态的监听管理。实际开发中,需要使用request上传下载模块实现,包括监听每个文件下载任务的进度,任务是否暂停,下载是否完成等状态情况。
以具体场景为例,下图是常见的多文件下载列表:
进入页面后,点击“全部开始”,启动所有文件的下载任务。点击“全部暂停”,暂停所有文件下载任务。再次点击“全部开始”,可重新启动未完成的下载任务。下载完成的文件会保存在应用缓存路径下。如果出现下载失败,一般是网络不稳定,点击“全部开始”可重新下载。
实现思路
let config: request.agent.Config = {
action: request.agent.Action.DOWNLOAD, // 配置任务选项,这里配置为下载任务
url: downloadUrl, // 配置下载任务url
overwrite: true, // 下载过程中路径已存在时的解决方案选择。true表示覆盖已存在的文件
method: 'GET', // HTTP标准方法。下载时,使用GET或POST。
saveas: './', // 这里'./'表示下载至应用当前缓存路径下。
mode: request.agent.Mode.BACKGROUND, // 任务模式设置后台任务。
gauge: true // 后台任务的过程进度通知策略,仅应用于后台任务。true表示发出每个进度已完成或失败的通知。
};
ForEach(this.downloadConfigArray, (item: request.agent.Config) => {
ListItem() {
// 创建文件下载监听实例
FileDownloadItem({
downloadConfig: item, // 文件下载配置
isStartAllDownload: this.isStartAllDownload, // 是否全部开始下载
downloadCount: this.downloadCount // 待下载任务数量
})
}
}, (item: request.agent.Config) => JSON.stringify(item))
request.agent.create(context, this.downloadConfig).then((task: request.agent.Task) => {
// 注册下载任务相关回调
task.on('completed', this.completedCallback); // 完成回调
task.on('failed', this.failedCallback); // 失败回调
task.on('pause', this.pauseCallback); // 暂停回调
task.on('resume', this.resumeCallback); // 重新启动回调
task.on('progress', this.progressCallback); // 进度更新回调
task.on('response', this.progressCallback); // response响应头数据回调
}).catch((err: BusinessError) => {
logger.error(TAG, `Failed to task create with error message: ${err.message}, error code: ${err.code}`);
});
private completedCallback = (progress: request.agent.Progress) => {
// 下载状态设置为下载完成
this.state = "下载完成";
if (this.sFileSize === '未知大小') {
this.nCurrentDownloadSize = 1;
}
// 文件下载完成,待下载任务数量减1
this.downloadCount--;
this.isShow = false;
}
task.start().then(() => {
this.downloadTask = task;
}).catch((err: Error) => {
logger.error(TAG, 'task start error:', err);
})
// 判断当前下载任务状态是否满足暂停条件。
if (this.downloadTask && (taskInfo.progress.state === request.agent.State.WAITING || taskInfo.progress.state
=== request.agent.State.RUNNING || taskInfo.progress.state === request.agent.State.RETRYING)) {
this.downloadTask.pause().then(() => {
}).catch((err: Error) => {
logger.error(TAG, 'task pause error:', err);
});
}
// 判断如果任务是暂停状态,则重新启动下载任务
if (this.downloadTask && taskInfo.progress.state === request.agent.State.PAUSED) {
this.downloadTask.resume().then(() => {
}).catch((err: Error) => {
logger.error(TAG, 'task resume error:', err);
});
}