HarmonyOS —— Remote Communication Kit 请求暂停 恢复 & 断点续传实战笔记
HarmonyOS —— Remote Communication Kit 请求暂停 / 恢复 & 断点续传实战笔记
这篇可以当成我给自己留的“网络大文件场景小抄”:
网络慢、需要暂停下载、想实现断点续传,这一节 Remote Communication Kit 都给了现成的能力。
一、能力概览 & 设备支持
Remote Communication Kit 在网络传输这块,不只是“发请求、收响应”这么简单,它还内置了:
- 请求发送 / 接收的暂停与恢复
- 基于传输范围(TransferRange)的断点续传
典型使用场景:
- 上传 / 下载大文件时,用户切到 4G / Wi-Fi,想先暂停;
- 断网 / 应用被打断后,希望下次从上次进度继续下载,而不是重头来;
- 做自定义下载器(分块、拼接、支持暂停/继续)。
设备支持和之前几节一样:
- 支持设备:Phone / 2in1 / Tablet / Wearable
- 自 5.1.1(19) 起:新增支持 TV
一句囫囵记:
请求暂停、恢复与断点续传能力支持 Phone / 2in1 / Tablet / Wearable,5.1.1(19) 起支持 TV。
二、请求暂停 / 恢复:SendingPausePolicy & PausePolicy
1. 代码里用到的基础工具
import { rcp } from '@kit.RemoteCommunicationKit';
import { util } from '@kit.ArkTS';
const HTTP_SERVER_POST: string = "https://example.org/anything";
// 定义调试信息接口
interface StringifiedDebugInfo {
type: rcp.DebugEvent;
data: string;
}
// 调试信息源类型:可能来自 response 或单独的 DebugInfo 数组
type DebugInfoSource = undefined | rcp.DebugInfo[] | rcp.Response;
这里的 Debug 相关逻辑,是用来从调试信息里筛出“暂停发送 / 恢复发送”的事件,方便验证是否真的发生了暂停/恢复。
2. 调试信息序列化:debugInfoStringify
function debugInfoStringify(infoSource: DebugInfoSource): StringifiedDebugInfo[] {
const debugInfo = Array.isArray(infoSource)
? (infoSource as rcp.DebugInfo[])
: (infoSource as rcp.Response).debugInfo;
if (!debugInfo) {
return [];
}
const decoder = util.TextDecoder.create('utf-8');
return debugInfo.map((i: rcp.DebugInfo): StringifiedDebugInfo => {
return {
type: i.type,
data: decoder.decodeToString(new Uint8Array(i.data)).trim(),
};
});
}
response.debugInfo里包含了 RCP 的“内部调试事件”;- 用
TextDecoder解出人类可读的字符串,方便我们按前缀匹配。
3. 提取“发送暂停 / 恢复”事件
function getSendPausedEvents(debugInfo: DebugInfoSource) {
return debugInfoStringify(debugInfo).filter((i) =>
i.data.startsWith('[[RCP]]: Pause sending')
);
}
function getSendResumedEvents(debugInfo: DebugInfoSource) {
return debugInfoStringify(debugInfo).filter((i) =>
i.data.startsWith('[[RCP]]: Resume sending')
);
}
这两个函数就是在调试信息里找:
[[RCP]]: Pause sending...[[RCP]]: Resume sending...
这样我们就能知道:发送过程到底有没有被暂停 / 恢复过。
4. 发送暂停策略:SendingPausePolicy + PausePolicy
核心示例函数:
const SendingPauseByTimeout = async (done: Function): Promise<void> => {
const session = rcp.createSession();
const request = new rcp.Request(HTTP_SERVER_POST);
// 1. 定义发送暂停策略:kind 为 'timeout',timeoutMs 为 1ms
const sendPolicy: rcp.SendingPausePolicy = {
kind: 'timeout',
timeoutMs: 1,
};
// 2. 定义总的暂停策略,把发送策略挂上去
const pausePolicy: rcp.PausePolicy = {
sending: sendPolicy,
};
// 3. 把暂停策略 & 调试跟踪配置挂到 request 上
request.configuration = {
transfer: {
pausePolicy: pausePolicy,
},
tracing: {
infoToCollect: {
textual: true,
},
},
};
// 4. 准备请求体数据
const data = 'TestData';
request.headers = {
'Content-Length': data.length.toString(),
};
let isReadCompleted = false;
request.method = 'POST';
// 5. 自定义请求体生成逻辑
request.content = (maxSize) => {
if (isReadCompleted) {
return new ArrayBuffer(0);
}
isReadCompleted = true;
const buffer = new ArrayBuffer(data.length);
util.TextEncoder.create('utf-8').encodeIntoUint8Array(data, new Uint8Array(buffer));
return buffer;
};
// 6. 发起请求
const response = await session.fetch(request);
// 7. 从响应的 debugInfo 里,查看发送暂停 / 恢复事件
const pausedEvents = getSendPausedEvents(response);
const resumedEvents = getSendResumedEvents(response);
console.info(`pausedEvents: ${JSON.stringify(pausedEvents)}`);
console.info(`resumedEvents: ${JSON.stringify(resumedEvents)}`);
session.close();
done();
}
关键点拆解:
-
SendingPausePolicykind: 'timeout':表示使用“超时触发暂停”策略;timeoutMs: 1:超时时间非常短(1ms),示例里用来模拟“很容易触发暂停”。
-
PausePolicy-
把
sending字段设为上面的sendPolicy,也就是说:发送过程遵循发送暂停策略;
-
文档里提到“接收暂停”同样支持,只是示例展示的是 sending 这一侧。
-
-
request.configuration.transfer.pausePolicy- 把这一套策略挂上去之后,请求发送过程中就会根据策略自动触发“发送暂停 / 恢复”机制。
-
tracing.infoToCollect.textual = true- 打开文本形式的跟踪信息,方便你在
response.debugInfo里看到暂停/恢复日志。
- 打开文本形式的跟踪信息,方便你在
-
调试验证
- 用
getSendPausedEvents(response)/getSendResumedEvents(response)就能检验 RCP 到底有没有按策略暂停/恢复。
- 用
这段示例主要是教你:怎么配置暂停策略 + 怎么在 Debug 里看见暂停/恢复事件。
真正业务里,你可以把kind换成其他策略(比如手动控制),也可以把timeoutMs改成合适的值。
三、断点续传:基于 transferRange 的分段下载
1. 基本思想
断点续传本质上就是:
- 每次只请求文件的一部分(即设置传输范围),例如:
- 第一次:下载 [0, 100)
- 第二次:下载 [100, 200)
- …
- 每段下载完之后,记录本次“下载截止位置”;
- 下次继续时,从上次截止的位置开始,再请求下一段。
在 Remote Communication Kit 里,这个“传输范围”由 request.transferRange 控制:
request.transferRange = { from: number, to: number };
你可以把它理解为 HTTP Range 头的语义封装。
2. 会话 & 请求初始化
import { rcp } from '@kit.RemoteCommunicationKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 创建会话
let session: rcp.Session | null = rcp.createSession();
// 定义服务器地址
const kHttpServerAddress = "http://www.example.com/fetch";
// 创建请求
const request = new rcp.Request(kHttpServerAddress, "GET");
// 定义变量记录下载文件的大小
let totalSize = 0;
// 定义一个存储上次传输位置的变量
let lastTransferPosition = 0;
totalSize:整个文件的总大小(字节数)lastTransferPosition:当前已经成功下载到的位置(相当于断点)
3. 先获取文件总大小:getTotalSize
示例是通过响应头里的 content-range 来拿总大小:
async function getTotalSize(): Promise<number> {
request.transferRange = { from: 0, to: 1 };
let totalSize = 0;
try {
let rep = await session?.fetch(request);
if (rep) {
// 从 header 的 content-range 字段中提取文件大小
let contentRange = rep.headers['content-range'];
let sizeStr = contentRange
? contentRange.substring(contentRange.indexOf('\/') + 1, contentRange.length)
: '0';
totalSize = Number(sizeStr);
}
} catch (err: any) {
console.error(`getTotalSize err: code is ${err.code}, message is ${JSON.stringify(err)}`);
}
console.info(`getTotalSize totalSize: ${totalSize.toString()}`);
return totalSize;
}
典型的 Content-Range 形态类似:
bytes 0-1/1024
这里是简单粗暴地:
- 找到
/后面的那一段(例如1024) - 转成
Number,当成文件总大小
⚠ 注意:这种做法依赖服务端正确返回
Content-Range头,实际使用时要做好兜底逻辑(比如换 HEAD 请求、用Content-Length等其他方式)。
4. 根据传输范围下载一段:downloadTransfer
function downloadTransfer(from: number, to: number) {
// 设置请求的数据传输范围
request.transferRange = { from: from, to: to };
session?.fetch(request).then((rep) => {
if (rep.body) {
// 这里应该把 rep.body 写入本地文件,对应 from ~ to 的内容
console.info(`Response succeeded: ${JSON.stringify(rep.headers)}`);
// 下一次传输的起始位置 = 上次的位置 + 本次传输数据的长度
lastTransferPosition += rep.body.byteLength;
if (lastTransferPosition < totalSize) {
// 计算下一次传输范围的结束位置,这里每次传 100 字节为例
const nextTo = Math.min(lastTransferPosition + 100, totalSize);
// 递归调用继续下载下一段数据
downloadTransfer(lastTransferPosition, nextTo);
} else {
console.info("Response succeeded, completed.");
}
}
}).catch((err: BusinessError) => {
console.error(`Continue transfer error: code is ${err.code}, message is ${err.message}`);
});
}
这里的逻辑可以理解为:
- 设置当前这段要下载的范围
[from, to); session.fetch(request)拿到这一段数据;- 把
rep.body写到本地文件对应区域(示例里没写这一步,但真实场景需要); - 把
lastTransferPosition往前推进rep.body.byteLength; - 如果还没到
totalSize,继续算下一段[lastTransferPosition, nextTo); - 递归调用直到下载完成。
示例中用了固定 chunk size = 100,可以抽成一个常量,比如
const CHUNK_SIZE = 1024 * 64;。
5. 开始下载:startDownload
async function startDownload() {
if (!session) {
session = rcp.createSession();
}
// 传输位置归零
lastTransferPosition = 0;
// 获取要下载文件的总大小
totalSize = await getTotalSize();
// 计算第一段的结束位置
const nextTo = Math.min(lastTransferPosition + 100, totalSize);
// 开始下载第一段
downloadTransfer(lastTransferPosition, nextTo);
}
- 重置
lastTransferPosition; - 通过
getTotalSize()拿到总长度; - 再从 0 开始下载第一段。
6. 暂停、继续、停止下载
暂停下载
function pauseDownload() {
// 取消当前下载请求
session?.cancel(request);
}
session.cancel(request):取消当前这个请求;- 由于我们每段是一次
fetch,暂停时就会中断当前这一次分段请求; - 但已经下载成功的部分我们通过
lastTransferPosition记录下来了。
继续下载
function resumeDownload() {
// 计算下一段的结束位置
const nextTo = Math.min(lastTransferPosition + 100, totalSize);
// 从 lastTransferPosition 继续下载下一段
downloadTransfer(lastTransferPosition, nextTo);
}
- 关键是:不重头来,而是从上一次
lastTransferPosition继续。
停止下载
function stopDownload() {
// 取消当前请求
session?.cancel(request);
// 关闭 session
session?.close();
session = null;
}
- 停止和暂停的区别在于:
- 暂停:只取消当前请求,但保留 session & 状态,可以 resume;
- 停止:直接 cancel + close,后续想下载需要重新建 session / request。
四、这一节的实战总结(给自己的一句话提示)
- 暂停 / 恢复:
- 用
PausePolicy控制发送 / 接收; - 发送端示例:
SendingPausePolicy(kind: 'timeout', timeoutMs: ...); - 配合
tracing.infoToCollect.textual+response.debugInfo可以检查暂停 / 恢复事件。
- 用
- 断点续传:
- 核心是
request.transferRange = { from, to }; - 先通过
Content-Range拿总大小; - 再用
lastTransferPosition滚动推进,分块下载; pause=cancel(request),resume= 从lastTransferPosition继续拉下一块。
- 核心是
- 设备支持:
- Phone / 2in1 / Tablet / Wearable
- 5.1.1(19) 起新增 TV
如果你接下来想把:

浙公网安备 33010602011771号