查看: 1511|回复: 3

HarmonyOS 7 RCP流式上传大文件OOM与表单状态机

[复制链接]
发表于 昨天 09:00 | 显示全部楼层 |阅读模式
在企业级云盘、盘搜和短视频项目中,大文件上传会把内存问题放到放大镜下。一个视频共创项目需要把本地 4K 原始视频(2GB 到 5GB)直接上传到服务器。早期做法是在 ArkTS 侧读取文件,转成 ArrayBuffer,再用传统 HTTP 请求体一次性发送。小文件正常,超过设备可用内存临界值后,App 会因 OOM 无声闪退。即使改为传文件路径让底层读取,只要底层网络引擎仍采用全量加载后发送,内存依然会被瞬间拉爆。根因是磁盘 I/O 读取速度快于网络 I/O 发送速度,没有背压机制时,数据会堆积在应用层或网络层缓冲区。

HarmonyOS 7.0(API 26)在 Remote Communication Kit(RCP)的 C-API 中新增 HMS_Rcp_SetRequestGetDataCallback(),把数据流转从应用层主动 Push 改成网络层按需 Pull。只有 TCP 发送窗口有空余时,底层引擎才触发回调,向应用层索要指定长度的数据块。这样既能解决大文件上传的内存积压,也能为手工构建 multipart/form-data 多片段有序表单提供精确控制面。该方案的目标是:上传 10GB 文件时,进程内存稳定在 2MB 以下,并复用连接池。

一、核心 API 与数据流

RCP C-API 在 API 26 提供细粒度请求生命周期控制。方案主要依赖三类能力:

1. 会话与连接池复用:HMS_Rcp_CreateSession 创建持久化会话,同一会话下发起的所有 Rcp_Request 共享底层连接池。大文件分片上传或伴随状态同步请求时,可复用已建立的 HTTP/2 或 HTTP/3 隧道,降低 TCP 握手和 TLS 协商延迟。

2. 数据拉取回调:HMS_Rcp_SetRequestGetDataCallback 是解决 OOM 的关键。接口原型如下:
  1. uint32_t HMS_Rcp_SetRequestGetDataCallback(
  2.     Rcp_Request* request,
  3.     Rcp_GetDataCallback callback,
  4.     void* clientContext
  5. );
  6. typedef uint32_t (*Rcp_GetDataCallback)(
  7.     void* clientContext,
  8.     uint8_t* buffer,
  9.     uint32_t length
  10. );
复制代码

当网络层发送缓冲区空闲时,RCP 引擎会在内部工作线程触发 Rcp_GetDataCallback。buffer 是引擎预分配的空闲内存指针,应用层把数据直接写入这里,实现用户态到网络层缓冲区的零拷贝,避免中间 malloc。length 是引擎当前能接收的最大字节数,通常取决于 MTU 和 TCP 拥塞窗口状态。回调返回值是实际写入字节数,返回 0 表示 EOF,引擎闭合 HTTP 请求体。

3. 请求终止与内存清理:HMS_Rcp_CancelRequest 可以安全地从底层中断 TCP 传输。取消后,绑定的 clientContext 不能在调用点立即释放,必须等到请求完成或失败回调触发后再释放,否则会产生野指针崩溃。

背压的关键在于:文件读取是被动触发的。弱网环境下 TCP 发送缓冲区积压,RCP 引擎停止触发 GetDataCallback,应用层状态机暂停,不再从磁盘读新数据。应用层与网络层速率由此解耦,内存不会无限制上涨。

二、Native 层流式 multipart 状态机

流式上传中构建 multipart/form-data 不能事先拼一个巨大字符串。必须把 Header、File Data、Tail 分段写入引擎缓冲区。该实践在 Native 层定义了一个上传任务上下文,用 clientContext 贯穿 RCP C-API,并用状态机表示当前发送阶段:
  1. enum class MultipartState {
  2.     SENDING_HEADER,
  3.     SENDING_FILE,
  4.     SENDING_TAIL,
  5.     FINISHED
  6. };
复制代码

上下文中包含 boundary、预先拼好的 headerStr 和 tailStr、headerSentOffset、tailSentOffset、std::ifstream fileStream、当前状态以及 Rcp_Request 句柄。文件以二进制方式打开。Header 中设置 Content-Disposition、filename、Content-Type,并声明 boundary;Tail 用于闭合 boundary。

核心回调 OnDataPullCallback 的逻辑是:把 clientContext 强转为上下文,做空指针和零长度防御;维护 totalWritten、remainingSpace、currentWritePtr;在 while 循环中按状态推进。
  1. static uint32_t OnDataPullCallback(void* clientContext, uint8_t* buffer, uint32_t length) {
  2.     StreamUploadContext* ctx = static_cast<StreamUploadContext*>(clientContext);
  3.     if (!ctx || !buffer || length == 0) return 0;
  4.     uint32_t totalWritten = 0;
  5.     uint32_t remainingSpace = length;
  6.     uint8_t* currentWritePtr = buffer;
  7.     while (remainingSpace > 0 && ctx->state != MultipartState::FINISHED) {
  8.         if (ctx->state == MultipartState::SENDING_HEADER) {
  9.             // 写入 headerStr 剩余内容,更新 headerSentOffset;写完后切到 SENDING_FILE
  10.         } else if (ctx->state == MultipartState::SENDING_FILE) {
  11.             ctx->fileStream.read(reinterpret_cast<char*>(currentWritePtr), remainingSpace);
  12.             std::streamsize bytesRead = ctx->fileStream.gcount();
  13.             // 更新指针、空间和 totalWritten;EOF 时切到 SENDING_TAIL
  14.         } else if (ctx->state == MultipartState::SENDING_TAIL) {
  15.             // 写入 tailStr 剩余内容,更新 tailSentOffset;完成后切到 FINISHED
  16.         }
  17.     }
  18.     return totalWritten;
  19. }
复制代码

文件阶段直接使用 std::ifstream 读取到 RCP 提供的 buffer,减少中间拷贝。每次回调只消耗 length 规定的字节数,通常在 16KB 到 64KB 之间浮动。即使不切片、单请求全量流式推送,内存也维持在这个量级。

三、连接池装配与资源释放

有了拉取回调,还要把上下文装配进 Rcp_Session。实践中用一个全局 Session 复用连接池,配置最大空闲连接数为 10,保活时间为 60000ms。创建请求后设置 POST,并声明 Content-Type 为 multipart/form-data 及 boundary,再通过 HMS_Rcp_SetRequestGetDataCallback 挂载状态机回调。最后用 HMS_Rcp_SessionFetch 异步触发,避免阻塞主线程或 JS 线程。

请求完成回调是最终安全释放点。无论成功、失败还是取消,都在 HMS_Rcp_SessionFetch 的完成回调里销毁 Rcp_Request、Rcp_Response,并 delete 堆上的上下文。该设计把 clientContext 与堆内存强绑定,利用完成回调回收,规避 C++ 内存泄漏和野指针。

四、避坑指南

1. 不要在 GetDataCallback 中做重度阻塞操作。该回调运行在 RCP 内部网络工作线程,通常位于底层 Socket 事件驱动循环所在线程。虽然示例使用同步 fileStream.read,但在企业级高并发场景中,如果磁盘 I/O 瞬时卡顿,会挂起 RCP 事件循环,导致同一 Session 下所有并发网络请求假死。更稳妥的做法是应用层维护异步读取缓冲队列,用单独 I/O 线程预读 1MB 文件切片,回调触发时只做内存 memcpy。这样既保持低内存水位,又保证网络事件循环顺畅。

2. 取消上传时不要立刻释放上下文。调用 HMS_Rcp_CancelRequest 后,底层 C 引擎可能仍在做取消清理。如果调用点立即 delete ctx,后续扫尾动作访问野指针,可能触发致命段错误。资源销毁必须写在 HMS_Rcp_SessionFetch 的完成回调中,遵循生命周期契约。

3. 该方案还以传输单文件 2GB、网络限速 10Mbps 为基准,对内存峰值与响应速度做了多维对比,用于辅助 API 版本选型。核心结论是:拉取式流式上传把磁盘读取与 TCP 拥塞窗口绑定,在低内存占用下获得稳定吞吐。

五、总结

HMS_Rcp_SetRequestGetDataCallback() 看似只是新增一个底层 C 函数,实际代表系统处理海量吞吐时向按需计算和背压控制演进。通过 Native 层多片段表单状态机,上传过程中磁盘读取速度被网络层按需拉取节流,10GB 级文件上传也能保持平滑稳定,应用层内存开销被压到 KB 级别。后续如果要获取精准上传进度或结合断点续传,可以在这个状态机基础上引入 Range 头继续扩展。
回复

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: HarmonyOS 7 RCP流式上传大文件OOM与表单状态机

楼主这个思路很清晰,RCP 的 GetDataCallback 确实是把大文件上传从应用层硬 Push 改成了网络层按需 Pull。只有 TCP 发送窗口有空闲时才触发回调,磁盘读取跟着网络节奏走,背压自然就形成了,这比先读成 ArrayBuffer 再发靠谱太多。用状态机分段写 multipart 的 header、file、tail,也避免了拼巨大字符串,方向很对。 几个细节我觉得很关键:一是 CancelRequest 之后 clientContext 不能马上释放,必须等完成或失败回调再清,不然野指针很难查;二是文件读到 EOF 后不能直接返回 0,要先把 tail 和 boundary 收尾发完,再让引擎闭合请求体;三是回调里要处理短读和读取失败,不能假设一次就能填满 length。弱网下状态机暂停后,超时、重试和取消的配合也要提前设计好。 如果 10GB 文件能把进程内存稳定压在 2MB 以下,并且复用连接池跑通,那这套方案对企业级云盘和视频上传场景会很有参考价值。期待楼主把 EOF 切换和收尾逻辑补完。
回复 支持 反对

使用道具 举报

发表于 昨天 19:10 | 显示全部楼层

Re: HarmonyOS 7 RCP流式上传大文件OOM与表单状态机

楼主这个方案把 RCP 的拉取回调和 multipart 状态机结合起来,思路很顺。大文件上传最怕的就是应用层拼命读、网络层发不出去,Pull 模式让 TCP 窗口反过来控制磁盘读取,内存能压在 2MB 以下这个目标就有底气了。我比较关注状态机边界:header 没写完时,headerSentOffset 和 currentWritePtr、remainingSpace 的更新必须同步推进,不然 while 里容易卡住或多写;文件读到 EOF 后不能直接结束,还得保留一次或多次回调把 tail 写完,并且只有 tail 也闭合后才能返回 0 表示 EOF。取消请求后 clientContext 延后释放这点非常关键,实际项目里最容易在这里出野指针。另外如果 multipart 总长度能提前算出来,设置 Content-Length 是否会比 chunked 更稳一些?期待你后续把 tail 和完成回调这部分补完,这个实践对云盘和短视频上传很有参考价值。
回复 支持 反对

使用道具 举报

发表于 昨天 19:20 | 显示全部楼层

Re: HarmonyOS 7 RCP流式上传大文件OOM与表单状态机

这帖信息量很足,把大文件上传 OOM 的根因讲得很透:不是简单把 ArrayBuffer 换成路径就行,只要底层还是全量加载再发,读盘快过发网就迟早堆爆。RCP C-API 的 GetDataCallback 改成网络层按需 Pull,再加上 buffer 直接写、返回实际字节数、返回 0 表示 EOF,这套零拷贝和背压思路确实是对症的。 multipart 状态机那段也很关键,header、file、tail 分步写进引擎缓冲区,避免先拼巨型 form 字符串,这个在企业云盘和短视频场景里很实用。取消后 clientContext 不能立即释放、要等完成或失败回调,这个坑也提醒得及时。 几个想请教的点:1. length 一般量级多大,回调里 while 循环会不会因为单次可写空间很大而频繁跨状态;2. 弱网下 RCP 停止触发回调后,如果用户点取消,HMS_Rcp_CancelRequest 的完成或失败回调是否一定可靠触发,上下文怎么保证只释放一次;3. 同一会话复用连接池时,并发上传和状态同步请求的上下文隔离有没有额外注意;4. tail 闭合 boundary 的 CRLF 细节和最后一块 EOF 切换有没有坑。期待楼主把 tail 和收尾部分补完。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-24 06:11 , Processed in 0.025450 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部