一、背景说明

HarmonyOS 提供了两套网络请求 API:基础的 @ohos.net.http 和更强大的 Remote Communication Kit(rcp)。相比 @ohos.net.http,rcp 提供了会话(Session)级别的连接复用、拦截器、DNS 配置、缓存控制和 HTTP 版本协商等企业级能力。随着 HarmonyOS 7(API 26)在 rcp 中引入 QUIC 支持,HttpVersionSelectCallback 允许开发者在 ALPN 协商阶段动态决策 HTTP 版本,弱网环境下的传输体验进一步提升。本文实现一个基于 rcp 的通用网络请求封装,统一处理 GET/POST、请求头、超时、重试与响应解析,可直接用于业务工程。

二、技术原理

rcp 的核心是「会话—请求—响应」三层模型:

  1. Session(会话):通过 rcp.createSession(config) 创建,持有连接池、DNS 缓存、Cookie 管理等配置。一次创建、多次请求复用底层 TCP/TLS 连接,避免重复握手。会话使用完应调用 close() 释放资源(自 5.1.0(18) 起最多可创建 1024 个 session 实例)。
  2. Request(请求):描述 URL、方法、请求头、请求体、超时等;也可直接用 session.get()/post() 便捷方法。
  3. Response(响应):包含 statusCode、headers、响应体(可按字符串、JSON、ArrayBuffer 等形式取出)。

数据流:createSession → session.fetch(request) → Promise<Response>。权限上需要 ohos.permission.INTERNET;若把请求路径偏好设为蜂窝网络(pathPreference: 'cellular'),还需要 ohos.permission.GET_NETWORK_INFO。

与 @ohos.net.http 相比的关键差异:rcp 的会话配置(拦截器、超时、Cookie、缓存)在 Session 级别统一生效,不必每个请求重复设置;这是封装成单例网络层的天然基础。

三、实现步骤
1. 权限配置

在 entry/src/main/module.json5 的 module 节点下添加:

{
  "module": {
    // ...其余配置
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}
2. 请求封装类 HttpRequestUtil

新建 entry/src/main/ets/utils/HttpRequestUtil.ets,完整代码如下:

import { rcp } from '@kit.RemoteCommunicationKit';
import { BusinessError } from '@kit.BasicServicesKit';

/** 统一响应结构 */
export interface HttpResult<T> {
  success: boolean;
  code: number;          // HTTP 状态码,失败时为 -1
  data?: T;              // 解析后的业务数据
  message: string;       // 错误或状态描述
}

export class HttpRequestUtil {
  private static session: rcp.Session | null = null;

  /** 懒加载全局会话:拦截器、超时、请求头在会话级统一生效 */
  private static getSession(): rcp.Session {
    if (!HttpRequestUtil.session) {
      HttpRequestUtil.session = rcp.createSession({
        // 请求拦截器:统一注入时间戳与 token 占位
        interceptors: [{
          intercept: (context: rcp.RequestContext) => {
            context.request.headers['X-Timestamp'] = Date.now().toString();
            return context.next.handle(context);
          }
        }],
        // 会话级请求头
        headers: {
          'content-type': 'application/json',
          'user-agent': 'HarmonyOS-RCP-Demo/1.0'
        },
        // 会话级超时(毫秒)
        configuration: {
          timeout: {
            connect: 10000,
            transfer: 15000
          }
        },
        // HTTP 版本协商回调(API 26+):可对问题域名强制降级
        httpVersionSelectCallback: (incomingVersion: rcp.HttpVersion) => {
          // 示例:已知某老网关 HTTP/2 有兼容问题,降级到 HTTP/1.1
          // 其余情况返回 undefined,尊重系统协商结果
          return undefined;
        }
      });
    }
    return HttpRequestUtil.session;
  }

  /** GET 请求 */
  static async get<T>(url: string, params?: Record<string, string>): Promise<HttpResult<T>> {
    let fullUrl = url;
    if (params) {
      const query = Object.keys(params)
        .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
        .join('&');
      fullUrl = url.includes('?') ? `${url}&${query}` : `${url}?${query}`;
    }
    return HttpRequestUtil.request<T>(fullUrl, 'GET');
  }

  /** POST 请求(JSON 体) */
  static async post<T>(url: string, body: object): Promise<HttpResult<T>> {
    return HttpRequestUtil.request<T>(url, 'POST', body);
  }

  /** 通用请求:发请求 → 校验状态码 → 解析 JSON */
  private static async request<T>(url: string, method: 'GET' | 'POST', body?: object): Promise<HttpResult<T>> {
    const session = HttpRequestUtil.getSession();
    try {
      const request: rcp.Request = {
        url: url,
        method: method,
        content: body ? JSON.stringify(body) : undefined
      };
      const response = await session.fetch(request);
      if (response.statusCode >= 200 && response.statusCode < 300) {
        const data = response.toJSON() as T;
        return { success: true, code: response.statusCode, data: data, message: 'ok' };
      }
      return {
        success: false,
        code: response.statusCode,
        message: `服务器返回异常状态码: ${response.statusCode}`
      };
    } catch (err) {
      const e = err as BusinessError;
      console.error(`rcp request failed, code=${e.code}, msg=${e.message}`);
      return { success: false, code: -1, message: `网络请求失败: ${e.message}` };
    }
  }

  /** 应用退出或页面销毁时释放会话 */
  static closeSession(): void {
    if (HttpRequestUtil.session) {
      HttpRequestUtil.session.close();
      HttpRequestUtil.session = null;
    }
  }
}
3. UI 调用示例

新建 entry/src/main/ets/pages/Index.ets:

import { HttpRequestUtil, HttpResult } from '../utils/HttpRequestUtil';

interface TodoItem {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

@Entry
@Component
struct Index {
  @State resultText: string = '点击按钮发起请求';

  private async doGet(): Promise<void> {
    const result: HttpResult<TodoItem> = await HttpRequestUtil.get<TodoItem>(
      'https://jsonplaceholder.typicode.com/todos/1'
    );
    if (result.success && result.data) {
      this.resultText = `GET 成功\n标题: ${result.data.title}\n完成: ${result.data.completed}`;
    } else {
      this.resultText = `GET 失败: ${result.message}`;
    }
  }

  private async doPost(): Promise<void> {
    const result: HttpResult<TodoItem> = await HttpRequestUtil.post<TodoItem>(
      'https://jsonplaceholder.typicode.com/todos',
      { userId: 1, title: '学习 rcp 网络请求', completed: false }
    );
    this.resultText = result.success
      ? `POST 成功,新建记录 id=${result.data?.id}`
      : `POST 失败: ${result.message}`;
  }

  build() {
    Column({ space: 16 }) {
      Text('Remote Communication Kit(rcp)网络请求')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      Button('发起 GET 请求').onClick(() => {
        this.doGet();
      })
      Button('发起 POST 请求').onClick(() => {
        this.doPost();
      })
      Text(this.resultText)
        .fontSize(14)
        .padding(12)
        .backgroundColor('#F1F3F5')
        .borderRadius(8)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(16)
  }
}
四、应用场景示例

结合按钮触发,在业务代码里直接复用封装(带查询参数的搜索场景):

import { HttpRequestUtil } from '../utils/HttpRequestUtil';

interface SearchResponse {
  total: number;
  list: object[];
}

async function searchKeyword(keyword: string): Promise<void> {
  const result = await HttpRequestUtil.get<SearchResponse>(
    'https://api.example.com/search',
    { q: keyword, page: '1', size: '20' }
  );
  if (result.success && result.data) {
    console.info(`搜索到 ${result.data.total} 条结果`);
  } else {
    console.warn(`搜索失败: ${result.message}`);
  }
}
五、技术要点
  1. 权限:ohos.permission.INTERNET 为普通权限,module.json5 声明即可;pathPreference: 'cellular' 模式还需 ohos.permission.GET_NETWORK_INFO。
  2. 会话生命周期:Session 底层持有连接池,getSession() 用单例懒加载避免频繁握手;页面级或应用退出时调用 closeSession() 释放,防止连接泄漏。系统上限 1024 个 session 实例。
  3. 超时分层:configuration.timeout 里 connect 控制建连超时、transfer 控制传输超时,弱网场景建议两者分开设置,并在请求级 configuration 里覆盖会话级默认值。
  4. 错误处理:session.fetch 抛出的 BusinessError 携带 code 与 message,按业务需要区分网络不可达、超时、SSL 错误并给出不同提示,避免把原始异常直接抛给 UI。
  5. HTTP 版本协商:httpVersionSelectCallback 运行在网络 I/O 线程,回调里禁止阻塞操作(读文件、再发请求),否则 ALPN 协商挂起导致全部请求超时。
  6. JSON 解析:response.toJSON() 直接反序列化;响应体较大时改用 response.arrayBuffer() 分段处理,避免主线程解析长字符串。
六、扩展建议

可以进一步给封装加上自动重试与请求缓存。重试示例(在 request 外包一层):

static async getWithRetry<T>(url: string, retries: number = 2): Promise<HttpResult<T>> {
  let last: HttpResult<T> = { success: false, code: -1, message: '未执行' };
  for (let i = 0; i <= retries; i++) {
    last = await HttpRequestUtil.get<T>(url);
    if (last.success) {
      return last;
    }
  }
  return last;
}

进阶方向:用 rcp.Interceptor 实现统一日志埋点;通过 SessionConfiguration 的 Cookie 管理维持登录态;对大文件下载改用 destination 指定响应体落盘路径,边下边写不占内存。

结语

本文基于 Remote Communication Kit 实现了鸿蒙 ArkTS 侧的通用网络请求封装,核心代码约 130 行,覆盖会话复用、拦截器、超时配置、统一错误处理与 JSON 解析。相比逐请求创建 @ohos.net.http 实例,rcp 的会话模型让网络层更易维护,配合 API 26 的 HTTP 版本协商与 QUIC 能力,可以平滑应对弱网与高延迟场景。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐