鸿蒙核心技术##运动开发## Remote Communication Kit(远场通信服务)

在鸿蒙运动项目开发中,网络通信是不可或缺的一部分。无论是获取运动数据、同步用户信息,还是加载运动视频资源,都需要一个稳定、高效且易于使用的网络库。本文将带你深入探索如何封装一个超级好用的 RCP 网络库,帮助你在鸿蒙开发中轻松应对各种网络请求。本系列文章分为上,中,下三篇,分别介绍网络库的核心功能、高级特性以及实际应用案例。

前言

在移动应用开发中,网络请求是与后端服务交互的基础。一个优秀的网络库不仅需要提供基本的请求功能,还需要具备错误处理、日志记录、缓存管理等高级特性。鸿蒙系统提供了强大的 RCP(Remote Communication Protocol)模块,用于实现高效的网络通信。通过封装 RCP 模块,我们可以构建一个功能完备且易于使用的网络库,提升开发效率和应用性能。

一、网络库的核心功能:请求与响应处理

(一)请求参数的封装

在网络请求中,参数的处理是关键环节之一。我们需要将复杂的请求参数(如表单数据、JSON 对象等)转换为适合传输的格式。为此,我们定义了一个 QueryParamAppender 接口,并实现了 CustomQueryParamAppender 类,用于处理查询参数的拼接。

// 定义一个用于附加查询参数的接口
export interface QueryParamAppender {
append(queryParams?: Map<string, number|string|boolean|Array<number> | Array<string> | Array<boolean> >|Record<string,string>| object |undefined): string|undefined;
}
import { QueryParamAppender } from "./QueryParamAppender";

export class CustomQueryParamAppender implements QueryParamAppender {
append(queryParams?: Map<string, string | number | boolean | number[] | string[] | boolean[]> |Record<string,string> | object |undefined): string|undefined {
if (queryParams === undefined || (queryParams instanceof Map && queryParams.size === 0) || (typeof queryParams === 'object' && Object.keys(queryParams).length === 0)) {
return;
}
const paramsArray: string[] = [];
// 使用 Object.entries() 将对象转换为键值对数组
let values:[string,string|number|boolean|number[]|string[]|boolean[]][] = Object.entries(queryParams)
for (const qp of values) {
let key = qp[0]
let value = qp[1]
let encodedValue = '';
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
encodedValue += `${encodeURIComponent(`${key}[${i}]`)}=${encodeURIComponent(value[i].toString())}&`;
}
if (encodedValue.length > 0) {
encodedValue = encodedValue.slice(0, -1); // 移除最后一个 '&'
}
} else {
encodedValue = encodeURIComponent(key) + '=' + encodeURIComponent(value.toString());
}
paramsArray.push(encodedValue);
}
return paramsArray.join('&');
} }

核心点解析

  1. 参数类型支持:支持多种类型的参数,包括字符串、数字、布尔值以及数组。
  2. 编码处理:使用 encodeURIComponent 对参数进行编码,确保参数在 URL 中的合法性。
  3. 数组处理:对于数组类型的参数,通过索引进行拼接,例如 key[0]=value1&key[1]=value2

(二)响应内容的转换

网络请求的响应内容通常需要根据不同的内容类型(如 JSON、文本等)进行转换。为此,我们定义了 RequestConverterResponseConverter 接口,并实现了多种转换器,例如 FormConverterJsonConverterTextResponseConverterObjectResponseConverter

import { rcp } from "@kit.RemoteCommunicationKit";
import { JSONUtil } from "../../JSONUtil";
import { modelToForm } from "../NetUtils";
import { RcpContentType } from "../RcpService"; export interface RequestConverter {
contentType():RcpContentType;
convert(value: object|undefined): rcp.RequestContent;
} /**
* Form表单转换器
*/
export class FormConverter implements RequestConverter {
contentType(): RcpContentType {
return RcpContentType.FORM_URLENCODED
} convert(value: object|undefined): rcp.RequestContent {
return modelToForm(value);
}
} export class JsonConverter implements RequestConverter { contentType(): RcpContentType {
return RcpContentType.JSON;
}
convert(value: object|undefined): rcp.RequestContent {
if(value){
return JSONUtil.toString(value);
}
return ''
}
} export interface ResponseConverter {
contentType():RcpContentType;
convert(response: rcp.Response): string|object|null;
} /**
* 原始文本转换器
*/
export class TextResponseConverter implements ResponseConverter {
contentType(): RcpContentType {
return RcpContentType.TEXT_PLAIN;
}
convert(response: rcp.Response): string|object|null {
return response.toString();
}
} export class ObjectResponseConverter implements ResponseConverter{
contentType(): RcpContentType {
return RcpContentType.JSON;
}
convert(response: rcp.Response): string|object|null {
return response.toJSON()
}
}

核心点解析

  1. 内容类型标识:通过 contentType() 方法明确指定转换器支持的内容类型。
  2. JSON 转换:将 JavaScript 对象转换为 JSON 字符串。
  3. 文本响应处理:直接将响应内容转换为字符串。

(三)请求与响应的统一管理

为了更好地管理请求与响应,我们创建了 ConverterManager 类,用于注册和选择合适的转换器。


// src/main/ets/net/converter/ConverterManager.ts
import { rcp } from '@kit.RemoteCommunicationKit';
import { CustomErrorCode, LibError } from '../../LibError';
import { RcpContentType } from '../RcpService';
import { RequestConverter, ResponseConverter } from './RcpConverter'; /**
* 内容类型与转换器映射
*/
export class ConverterManager { private requestConverters: RequestConverter[] = [];
private responseConverters: ResponseConverter[] = [];
private _requestFunc?: ((data: object|undefined, dataType: RcpContentType) => RequestConverter) | undefined;
private _responseFunc?: ((response: rcp.Response) => ResponseConverter) | undefined; public set responseFunc(value: ((response: rcp.Response) => ResponseConverter) | undefined) {
this._responseFunc = value;
} public set requestFunc(value: ((data: object|undefined, dataType: string) => RequestConverter) | undefined) {
this._requestFunc = value;
} // 注册请求转换器
public registerRequestConverter(converter: RequestConverter): void {
this.requestConverters.push(converter);
} // 注册响应转换器
public registerResponseConverter(converter: ResponseConverter): void {
this.responseConverters.push(converter);
} /**
* 根据请求数据自动选择转换器
*/
selectRequestConverter(data: object|undefined,dataType: RcpContentType): rcp.RequestContent {
if(this._requestFunc){
return this._requestFunc(data , dataType).convert(data)
}
for(const request of this.requestConverters){
if(request.contentType() == dataType){
return request.convert(data);
}
}
throw LibError.makeBusinessError(CustomErrorCode.NOT_FIND_REQUEST_CONVERTER_ERROR,"NOT_FIND_REQUEST_CONVERTER_ERROR")
} /**
* 根据响应头选择转换器
*/
selectResponseConverter(response: rcp.Response,contentType: string | string[] | undefined): string|object|null {
if(this._responseFunc){
return this._responseFunc(response).convert(response)
}
let dataType = RcpContentType.TEXT_PLAIN
if(contentType){
if (contentType.includes('application/json')) {
dataType = RcpContentType.JSON
} else if (contentType.includes('text/plain')) {
dataType = RcpContentType.TEXT_PLAIN
} else if (contentType.includes('text/html')) {
dataType = RcpContentType.TEXT_PLAIN
}
}
for(const responseConver of this.responseConverters){
if(responseConver.contentType() == dataType){
return responseConver.convert(response);
}
}
throw LibError.makeBusinessError(CustomErrorCode.NOT_FIND_RESPONSE_CONVERTER_ERROR,"NOT_FIND_RESPONSE_CONVERTER_ERROR")
} }

核心点解析

  1. 转换器注册:通过 registerRequestConverterregisterResponseConverter 方法,将转换器注册到管理器中。
  2. 转换器选择:根据请求或响应的内容类型,自动选择合适的转换器进行处理。
  3. 错误处理:如果找不到合适的转换器,抛出错误提示。

二、网络库的高级特性:拦截器与日志记录

(一)拦截器机制

拦截器是网络库中的一个重要特性,用于在请求发送和响应返回时插入自定义逻辑。我们实现了 LoggingInterceptor,用于记录请求和响应的详细信息。

import { rcp } from "@kit.RemoteCommunicationKit";
import { util } from "@kit.ArkTS";
import { appLogger } from "../../../app/Application"; export class LoggingInterceptor implements rcp.Interceptor {
async intercept(context: rcp.RequestContext, next: rcp.RequestHandler): Promise<rcp.Response> {
// 记录请求信息
this.logRequest(context.request); // 调用下一个请求处理器
const response = await next.handle(context); // 记录响应信息
this.logResponse(response); return response;
} private logRequest(request: rcp.Request) {
const method = request.method;
const url = request.url.href;
const headers = request.headers;
const body = request.content; appLogger.info(`[请求] ${method} ${url}`);
appLogger.info(`[请求头] ${JSON.stringify(headers, null, 2)}`); if (body instanceof rcp.Form) {
appLogger.info(`[请求体] ${JSON.stringify(body, null, 2)}`);
} else {
appLogger.info(`[请求体] ${body}`);
}
} private logResponse(response: rcp.Response) {
const statusCode = response.statusCode;
const headers = response.headers;
const body = response.body; appLogger.info(`[响应] 状态码: ${statusCode}`);
appLogger.info(`[响应头] ${JSON.stringify(headers, null, 2)}`); if (body) {
try {
const uint8Array = new Uint8Array(body);
// 将 ArrayBuffer 转换为字符串
const decoder = new util.TextDecoder();
const bodyString = decoder.decodeToString(uint8Array);
// 尝试解析为 JSON
appLogger.logMultiLine(`[响应头] ${JSON.stringify(JSON.parse(bodyString), null, 2)}`) } catch (error) {
appLogger.logMultiLine(`[响应体] ${body}`);
}
}
}
}

核心点解析

  1. 请求记录:记录请求的 URL、方法、头信息和请求体。
  2. 响应记录:记录响应的状态码、头信息和响应体。
  3. 日志格式化:使用 JSON.stringify 格式化日志输出,便于阅读。

(二)日志记录的重要性

日志记录在网络请求调试中至关重要。通过记录请求和响应的详细信息,我们可以快速定位问题,例如:

  • 请求参数是否正确传递。
  • 响应数据是否符合预期。
  • 网络请求是否超时或失败。

在实际开发中,建议在开发阶段启用详细的日志记录,而在生产环境中关闭或仅记录关键信息,以避免性能损耗。

三、网络库的配置与初始化

为了使网络库更加灵活,我们提供了 HttpConfig 类,用于配置连接超时时间、传输超时时间、并发请求限制等参数。

import { Timeout } from "./NetConstants";

// 定义明确的配置接口
export interface IHttpConfigOptions {
connectTimeout?: number;
transferTimeout?: number;
maxConcurrentRequests?: number;
security?: boolean; } export class HttpConfig {
/**
* 连接超时时间(毫秒)
*/
connectTimeout: number; /**
* 传输超时时间(毫秒)
*/
transferTimeout: number; /**
* 最大并发请求数量
*/
maxConcurrentRequests: number; constructor(config?: IHttpConfigOptions) {
this.connectTimeout = config?.connectTimeout ?? Timeout.CONNECT_TIME_OUT;
this.transferTimeout = config?.transferTimeout ?? Timeout.TRANS_TIME_OUT;
this.maxConcurrentRequests = config?.maxConcurrentRequests ?? 5;
this.security = config?.security ?? true
}
}

核心点解析

  1. 默认值设置:通过 ?? 操作符,为配置项提供默认值。
  2. 灵活配置:允许开发者根据需求自定义超时时间、并发限制等参数。

在本篇中,我们详细介绍了网络库的核心功能,包括请求参数的封装、响应内容的转换以及拦截器与日志记录机制。这些功能为我们的网络库提供了坚实的基础。在接下来的中篇中,我们将继续深入探讨网络库的高级特性,包括错误处理、会话管理以及网络状态检测等,进一步提升网络库的健壮性和易用性。敬请期待!

鸿蒙运动项目开发:封装超级好用的 RCP 网络库(上)—— 请求参数封装,类型转化器与日志记录篇的更多相关文章

  1. 基于c++11新标准开发一个支持多线程高并发的网络库

    背景 新的c++11标准出后,c++语法得到了非常多的扩展,比起以往不论什么时候都要灵活和高效,提高了程序编码的效率,为软件开发者节省了不少的时间. 之前我也写过基于ACE的网络server框架,但A ...

  2. Spring 将请求参数封装成对象

    简单描述:最近手里的模块,前后台之间需要传递很多的参数,使用封装的PageData,来获取请求参数的,作微服务迁移的时候,就涉及到需要把参数从pagedata里取出来,一个一个的放到对象的属性中.就很 ...

  3. HFun.快速开发平台(二)=》自定义列表实例(请求参数的处理)

    上编描述了自定义列表的基本实现功能,本此记录列表的请求过程. 个人比较喜欢对参数进行对象化,方便后续人维护及查看,先上代码: /************************************ ...

  4. HFun.快速开发平台(四)=》自定义列表实例(请求参数的处理)

    上编自定义列表描述了自定义列表的基本实现功能,本此记录列表的请求过程. 个人比较喜欢对参数进行对象化,方便后续人维护及查看,先上代码: /******************************* ...

  5. 项目:《ssh框架综合项目开发视频》-视频目录和第六天的EasyUI简单讲解

    4 练习使用技术: Struts2 + hibernate5.x + spring4.x + mysql数据库 1 crm:customer relational manager,客户关系管理 2 c ...

  6. 第十章 企业项目开发--分布式缓存Redis(2)

    注意:本章代码是在上一章的基础上进行添加修改,上一章链接<第九章 企业项目开发--分布式缓存Redis(1)> 上一章说了ShardedJedisPool的创建过程,以及redis五种数据 ...

  7. Spring Boot 整合JDBC 实现后端项目开发

    一.前言 二.新建Spring Boot 项目 三.Spring Boot 整合JDBC 与MySQL 交互 3.1 新建数据表skr_user 3.2 Jdbcproject 项目结构如下 3.3 ...

  8. [Openwrt 项目开发笔记]:Openwrt必要设置(二)

    [Openwrt项目开发笔记]系列文章传送门:http://www.cnblogs.com/double-win/p/3888399.html 正文: 前面的两篇blog中,我将如何搭建Openwrt ...

  9. 在jsp提交表单的参数封装到一个方法里

    建议去看一下孤傲苍狼写的Servlet+JSP+JavaBean开发模式(http://www.cnblogs.com/xdp-gacl/p/3902537.html), 最好把他JavaWeb学习总 ...

  10. 十一、Struts2封装请求参数的方式

    十一.Struts2封装请求参数的方式 方式一.Action 本身作为model对象,通过成员setter封装(一个名字为params的拦截器干的) 注意:表单中的名称要和动作类中的名称一致(这是必须 ...

随机推荐

  1. 『Plotly实战指南』--饼图绘制基础篇

    在数据可视化的世界里,饼图是一种直观且广泛使用的图表类型. 它能够将数据各个部分占整体的比例关系清晰地展现出来,适用于诸如市场占有率分析.调查结果分布.预算分配等多个领域. 饼图以扇形面积比例直观展示 ...

  2. 使用Python+SymPy求解微分方程

    引言 在学习微积分或者物理.工程相关的学科时,微分方程常常是我们需要解决的一个重要问题.微分方程是包含未知函数及其导数的方程,广泛应用于描述变化过程中的规律,如物理中的运动方程.化学中的反应速率.经济 ...

  3. 详细介绍Mybatis的缓存机制

    一.缓存机制 1.缓存概述 缓存:缓存就是一块内存空间,保存临时数据 作用:将数据源(数据库或者文件)中的数据读取出来存放到缓存中,再次获取时直接从缓存中获取,可以减少和数据库交互的次数,提升程序的性 ...

  4. 通过引用实现php无限极分类

    /** * 递归加引用实现无限极分类 * @param $items * @return array */ public function getTree2($items) {   $array = ...

  5. TreeSet练习 根据字符串长度排序

    String类已经实现了Comparable接口,我们可以根据TreeSet提供的构造器传入自己的比较器. public class Set4 { public static void main(St ...

  6. 【HUST】网络攻防实践|6_物联网设备固件安全实验|flag2~5速通指南

    写在最前:最近没空写报告,实验原理虽然已经摸清了但是没空写.flag2到4是一些大胆的想法的通关方式,flag5是正经通关的.之后写报告的时候会补发正经的实验原理,和flag2到4正常的通关方式. 记 ...

  7. Linux 常识和操作(常用命令)

    1. 存放用户账号的文件在哪里? /etc/passwd 2. 如何删除一个非空的目录? rm -rf 目录名 3. 查看当前的工作目录用什么命令? pwd 4. 创建一个文件夹用什么命令? mkdi ...

  8. 操作系统综合题之“采用记录型信号量机制实现进程INPUT、PROCESS和OUTPUT的同步算法(代码补充)”

    1.问题:系统中有有三个进程INPUT.PROCESS和OUTPUT,共用两个缓冲区BUF1和BUF2.假期设BUF1中最多可放10个数据,现已放入了2个数据:BUF2最多可放5个数据.INPUT进程 ...

  9. HarmonyOS NEXT开发实战教程—淘宝搜索页

    今天忙里偷闲,分享一个淘宝搜索页实现过程,先上效果图: 界面部分比较简单,大体分为导航栏.历史搜索.猜你想搜和热搜榜几个部分,历史搜索采用用户首选项进行存储数据. 导航栏部分相关代码如下: Flex( ...

  10. vue3 学习-初识体验-常见指令v-on和v-if

    继续来体验一波数据驱动结合绑定方法的实践案例. 这里以最常见的反转字符串为栗子来体验面向数据编程. v-on 用来绑定事件的, 然后将方法名写在 methods 中即可. <!DOCTYPE h ...