鸿蒙运动项目开发:封装超级好用的 RCP 网络库(中)—— 错误处理,会话管理与网络状态检测篇

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

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

四、网络库的高级特性:错误处理与异常管理

(一)自定义异常类

在网络请求中,错误处理是至关重要的。为了更好地管理错误,我们定义了一个 NetworkException 类,用于封装各种网络相关的异常。

typescript 复制代码
import { ErrorCodes } from "../NetConstants";
import { BusinessError } from "@kit.BasicServicesKit";
import { appLogger } from "../../../app/Application";


export class NetworkException extends Error {

  private static errorMessages: Record<string, string> = {
    [ErrorCodes.NETWORK_UNAVAILABLE]: "网络不可用,请检查网络连接",
    [ErrorCodes.REQUEST_TIMEOUT]: "请求超时,请稍后重试",
    [ErrorCodes.SERVER_ERROR]: "服务器开小差了,请稍后再试",
    [ErrorCodes.INVALID_RESPONSE]: "服务器返回数据格式错误",
    [ErrorCodes.UNAUTHORIZED]: "登录已过期,请重新登录",
    [ErrorCodes.FORBIDDEN]: "无权访问该资源",
    [ErrorCodes.NOT_FOUND]: "请求的资源不存在",
    [ErrorCodes.UNKNOWN_ERROR]: "未知错误,请联系客服",
    [ErrorCodes.URL_NOT_EXIST_ERROR]: "URL不存在",
    [ErrorCodes.URL_ERROR]: "URL格式不合法",
    [ErrorCodes.BAD_REQUEST]: "客户端请求的语法错误,服务器无法理解。",
    [ErrorCodes.REQUEST_CANCEL]: "请求被取消"
  };

  private responseCode?:number
  private originalError?:Error | BusinessError
  private _code: string;

  public get code(): string {
    return this._code;
  }



  constructor(code : string, originalError?: Error | BusinessError,customMessage?: string,responseCode?:number) {
    super(customMessage || NetworkException.getMessage(code))
    this._code = code
    this.name = "NetworkException";
    this.responseCode = responseCode
    this.originalError = originalError
  }


  public isResponseError(): boolean{
    if (this.responseCode) {
      return true
    }else {
      return false
    }
  }

  private static getMessage(code: string): string {
    return NetworkException.errorMessages[code] || NetworkException.errorMessages[ErrorCodes.UNKNOWN_ERROR];
  }


  static updateErrorMessages(newMessages: Record<string, string>): void {
    const keys = Object.keys(newMessages);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const value = newMessages[key];
      NetworkException.errorMessages[key] = value
    }
  }

}

核心点解析

  1. 错误信息映射 :通过 errorMessages 对象,将错误代码映射到具体的错误信息。
  2. 自定义错误信息:允许传入自定义的错误信息,以覆盖默认的错误描述。
  3. 错误分类 :通过 isResponseError() 方法,区分是响应错误还是其他类型的错误。
  4. 错误信息动态更新 :通过 updateErrorMessages() 方法,允许动态更新错误信息映射表。

(二)错误处理逻辑

在网络请求中,错误处理逻辑需要覆盖多种场景,包括网络不可用、请求超时、服务器错误等。我们通过在 RcpNetworkService 类中捕获和抛出 NetworkException,实现了统一的错误处理。

typescript 复制代码
async request<T>(requestOption: RequestOptions,requestKeyFun?:(str:string)=>void): Promise<T> {
  const session = this.rcpSessionManager.getSession(requestOption.connectTimeout??this.httpConfig.connectTimeout,requestOption.transferTimeout??this.httpConfig.transferTimeout)

  try {

    let baseUrl = requestOption.baseUrl?requestOption.baseUrl:this.baseUrl
    if(baseUrl === null || baseUrl.trim().length === 0){
      throw new NetworkException(ErrorCodes.URL_NOT_EXIST_ERROR);
    }

    if (!LibNetworkStatus.getInstance().isNetworkAvailable()) {
      appLogger.error("HttpCore 网络不可用")
      throw new NetworkException(ErrorCodes.NETWORK_UNAVAILABLE);
    }


    let url = baseUrl + requestOption.act;

    if (!isValidUrl(url)) {
      appLogger.error("HttpCore url格式不合法")
      throw new NetworkException(ErrorCodes.URL_ERROR);
    }
    const contentType = requestOption.contentType??RcpContentType.JSON

    const headers: rcp.RequestHeaders = {
      'Content-Type': contentType
    };
    const cacheKey = await USystem.getUniqueId()

    if (this.queryParamAppender) {
      let param = this.queryParamAppender.append(requestOption.queryParams);
      if(param){
        url = url + "?" + param
      }
    }


    const requestObj = new rcp.Request(url, requestOption.method??RequestMethod.GET, headers, this.converterManger.selectRequestConverter(requestOption.content,contentType));
    // 将请求和会话的映射关系存储起来
    this.requestMap.set(cacheKey, { session, request: requestObj });
    if(requestKeyFun){
      requestKeyFun(cacheKey)
    }
    let response = await session.fetch(requestObj);

    if (!response.statusCode) {
      throw new NetworkException(ErrorCodes.INVALID_RESPONSE);
    }

    if (response.statusCode >= HttpStatus.SUCCESS && response.statusCode < 300) {
      // 获取 Content-Type
      const responseContentType = response.headers['Content-Type'];
      const responseData = this.converterManger.selectResponseConverter(response, responseContentType)
      const parsedResult = responseData as T

      return parsedResult;

    }

    switch (response.statusCode) {
      case HttpStatus.UNAUTHORIZED:
        throw new NetworkException(ErrorCodes.UNAUTHORIZED,undefined,undefined,response.statusCode);
      case HttpStatus.FORBIDDEN:
        throw new NetworkException(ErrorCodes.FORBIDDEN,undefined,undefined,response.statusCode);
      case HttpStatus.NOT_FOUND:
        throw new NetworkException(ErrorCodes.NOT_FOUND,undefined,undefined,response.statusCode);
      case HttpStatus.REQUEST_TIMEOUT:
        throw new NetworkException(ErrorCodes.REQUEST_TIMEOUT,undefined,undefined,response.statusCode);
      case HttpStatus.BAD_REQUEST:
        throw new NetworkException(ErrorCodes.BAD_REQUEST,undefined,undefined,response.statusCode);
      case HttpStatus.SERVER_ERROR:
      case HttpStatus.BAD_GATEWAY:
      case HttpStatus.SERVICE_UNAVAILABLE:
      case HttpStatus.GATEWAY_TIMEOUT:
        throw new NetworkException(ErrorCodes.SERVER_ERROR,undefined,undefined,response.statusCode);
      default:
        throw new NetworkException(ErrorCodes.UNKNOWN_ERROR,undefined,undefined,response.statusCode);
    }



  }catch (e) {
    if(e instanceof NetworkException){
      throw e
    } else {
      try{
        let err = e as BusinessError;
        appLogger.error(` ${err.code.toString()} ${err.stack} ${err.message} ${err.name}`)
        throw new NetworkException(err.code.toString(),err,err.message)
      }catch  {
        let err = e as Error;
        appLogger.error(`异常: ${err.stack} ${err.message} ${err.name}`)
        throw err
      }
    }
  }finally {
    this.rcpSessionManager?.releaseSession(session)
    // 当会话被关闭时,移除与该会话关联的所有请求
    this.requestMap.forEach((entry, key) => {
      if (entry.session === session) {
        this.requestMap.delete(key);
      }
    });
  }
}

核心点解析

  1. 网络状态检测 :在发起请求之前,通过 LibNetworkStatus.getInstance().isNetworkAvailable() 检测网络是否可用。
  2. URL 验证 :通过 isValidUrl() 方法验证 URL 格式是否正确。
  3. 错误分类与抛出 :根据不同的错误场景,抛出对应的 NetworkException
  4. 统一的错误处理 :在 catch 块中,对所有异常进行统一处理,确保错误信息的一致性。

五、网络库的高级特性:会话管理

(一)会话池的实现

为了优化网络请求的性能,我们实现了会话池机制。通过复用会话,可以减少频繁创建和销毁会话的开销。

typescript 复制代码
import { rcp } from "@kit.RemoteCommunicationKit";
import { HttpConfig } from "./HttpConfig";


// 创建安全配置,跳过证书验证
const securityConfig: rcp.SecurityConfiguration = {
  remoteValidation: 'skip'
};


export class RcpSessionManager{

  private currentConcurrentRequests = 0;

  // 定义连接池
  private connectionPool: rcp.Session[] = [];
  private _interceptor: rcp.Interceptor[] = [];

  public set interceptor(value: rcp.Interceptor[]) {
    this._interceptor = value;
  }

  private _httpConfig: HttpConfig = new HttpConfig();

  public set httpConfig(value: HttpConfig) {
    this._httpConfig = value;
  }

  public getSession(connectTimeout:number,transferTimeout:number): rcp.Session {
    // 如果连接池中有可用的会话,直接返回
    if (this.connectionPool.length > 0) {
      return this.connectionPool.pop()!;
    }

    // 如果没有可用的会话,创建一个新的会话
    const session = rcp.createSession({
      interceptors: [...this._interceptor],
      requestConfiguration: {
        transfer: {
          timeout: {
            connectMs: connectTimeout,
            transferMs: transferTimeout
          }
        },
        security: this._httpConfig.security?undefined:securityConfig
      }
    });

    return session;
  }

  public releaseSession(session: rcp.Session): void {
    // 如果当前并发请求小于最大并发限制,将会话放回连接池
    if (this.currentConcurrentRequests < this._httpConfig.maxConcurrentRequests) {
      this.connectionPool.push(session);
    } else {
      session.close();
    }
  }

  public destroy(): void {
    // 关闭所有会话
    this.connectionPool.forEach(session => session.close());
    this.connectionPool.length = 0;
  }
}

核心点解析

  1. 会话复用 :通过 connectionPool 存储空闲的会话,避免频繁创建和销毁会话。
  2. 并发限制 :根据 HttpConfig 中的 maxConcurrentRequests 配置,限制并发请求数量。
  3. 会话释放:在请求完成后,将会话放回会话池或关闭会话,以优化资源使用。

(二)会话管理的重要性

会话管理在网络请求中起着至关重要的作用。通过合理管理会话,可以显著提升网络请求的性能和稳定性。例如:

  • 减少连接开销:通过复用会话,减少频繁建立和关闭连接的开销。
  • 控制并发数量:通过限制并发请求数量,避免过多的并发请求对服务器造成压力。
  • 资源回收:在请求完成后及时释放会话资源,避免资源泄漏。

六、网络库的高级特性:网络状态检测

在网络请求中,网络状态的检测是必不可少的。通过检测网络是否可用,可以提前避免因网络问题导致的请求失败。

typescript 复制代码
import connection from '@ohos.net.connection'
import { appLogger } from '../../app/Application'
import { LibNetworkStatusCallback } from './LibNetworkStatusCallback'

const TAG : string = "LibNetworkStatus"

/**
 * 枚举:网络类型
 */
export enum NetworkType {
  STATE_NULL = 'NULL',//网络状态标识:未联网
  UNKNOWN = 'UNKNOWN',//未知网络
  MOBILE = 'MOBILE',
  WIFI = 'WIFI',
  ETHERNET = 'ETHERNET'
}

/**
 * 枚举:承载类型(内部使用,与具体平台API对接)
 * 注意:这里的枚举值应与平台API中的实际值保持一致
 */
enum BearerType {
  MOBILE = 0,
  WIFI = 1,
  // ... 可能还有其他承载类型,根据平台API添加
  ETHERNET = 3
}

/**
 * 网络信息:
 * 1、网络连接状态管理
 * 2、网络事件注册监听、取消注册
 */
export class LibNetworkStatus {


  /**
   * LibNetworkStatus单例对象
   */
  private static instance: LibNetworkStatus
  /**
   * 当前网络状态
   */
  private currentNetworkStatus:NetworkType = NetworkType.STATE_NULL
  /**
   * 网络是否可用
   */
  private isAvailable = false
  /**
   * 鸿蒙网络连接对象
   */
  private networkConnectio?: connection.NetConnection
  /**
   * 定义回调方法集合,使用WeakSet避免内存泄漏
   */
  private callbacks = new Set<LibNetworkStatusCallback>()

  /**
   * 防抖定时器
   */
  private debounceTimer: number | null = null

  /**
   * 防抖时间(毫秒)
   */
  private static readonly DEBOUNCE_DELAY = 300

  /**
   * 获得LibNetworkStatus单例对象
   * @returns LibNetworkStatus单例对象
   */
  static getInstance (): LibNetworkStatus {
    if (!LibNetworkStatus.instance) {
      LibNetworkStatus.instance = new LibNetworkStatus()
    }
    return LibNetworkStatus.instance
  }

  /**
   * 添加回调方法
   * @param callback 回调方法
   * @param isCallBackCurrentNetworkStatus 是否立即返回当前的网络状态
   */
  addCallback (callback: LibNetworkStatusCallback, isCallBackCurrentNetworkStatus: boolean) {
    if (callback && this.callbacks) {
      appLogger.debug(TAG+"添加回调方法")
      if(this.callbacks.has(callback)){
        return
      }
      this.callbacks.add(callback)

      //立即回调当前网络状态
      if (isCallBackCurrentNetworkStatus) {
        appLogger.debug(TAG+'立即回调当前网络状态: ' + this.currentNetworkStatus)
        callback(this.currentNetworkStatus)
      }
    }
  }

  /**
   * 移除回调方法
   * @param callback 回调方法
   */
  removeCallback (callback: LibNetworkStatusCallback) {
    if (callback && this.callbacks && this.callbacks.has(callback)) {
      appLogger.debug(TAG+'移除回调方法')
      this.callbacks.delete(callback)
    }
  }

  /**
   * 防抖处理网络状态回调
   */
  private debouncedCallback() {
    if (this.debounceTimer !== null) {
      clearTimeout(this.debounceTimer);
    }

    this.debounceTimer = setTimeout(() => {
      if (this.callbacks && this.callbacks.size > 0) {
        appLogger.debug(TAG + '遍历callback集合,回调当前网络状态')
        this.callbacks.forEach(callback => {
          callback(this.currentNetworkStatus)
        })
      }
      this.debounceTimer = null;
    }, LibNetworkStatus.DEBOUNCE_DELAY);
  }

  /**
   * 回调当前网络状态
   */
  callbackNetworkStatus() {
    this.debouncedCallback();
  }

  /**
   * 注册网络状态监听:
   * 设备从无网络到有网络会触发"netAvailable"、"netCapabilitiesChange"、"netConnectionPropertiesChange"事件;
   * 设备从有网络到无网络会触发"netLost"事件
   * 设备从wifi到蜂窝网络会触发"netLost"事件(wifi不可用)、之后触发"netAvailable"事件(蜂窝可用)
   */
  registerNetConnectListener () {
    if (this.networkConnectio) {
      appLogger.debug(TAG+'已订阅网络事件,无需再次订阅')
      return
    }

    //创建NetConnection对象
    this.networkConnectio = connection.createNetConnection()

    //判断默认网络状态
    let hasDefaultNet = connection.hasDefaultNetSync()
    if (hasDefaultNet) {
      appLogger.debug(TAG+'hasDefaultNetSync  ' + hasDefaultNet)
      this.isAvailable = true
      //获得默认网络类型
      this.getDefaultNetSync()
    }

    //注册
    this.networkConnectio.register((error) => {
      if (error) {
        appLogger.debug(TAG+'networkConnectio.register failure: ' + JSON.stringify(error))
      } else {
        appLogger.debug(TAG+' networkConnectio.register success')
      }
    })

    //订阅网络可用事件
    appLogger.debug(TAG+'订阅网络可用事件-->')
    this.networkConnectio.on('netAvailable', (data: connection.NetHandle) => {
      appLogger.debug(TAG+'netAvailable:' + JSON.stringify(data))
      this.isAvailable = true

      //获得默认网络类型
      this.getDefaultNetSync()

      //回调网络状态
      this.callbackNetworkStatus()
    })

    //订阅网络丢失事件
    appLogger.debug(TAG+'订阅网络丢失事件-->')
    this.networkConnectio.on('netLost', (data: connection.NetHandle) => {
      appLogger.debug(TAG+'netLost:' + JSON.stringify(data))
      this.isAvailable = false
      this.currentNetworkStatus = NetworkType.STATE_NULL

      //回调网络状态
      this.callbackNetworkStatus()
    })

    //订阅网络不可用事件
    appLogger.debug(TAG+'订阅网络不可用事件-->')
    this.networkConnectio.on('netUnavailable', () => {
      appLogger.debug(TAG+'netUnavailable')
      this.isAvailable = false
      this.currentNetworkStatus = NetworkType.STATE_NULL

      //回调网络状态
      this.callbackNetworkStatus()
    })
  }

  /**
   * 获得默认网络类型
   */
  getDefaultNetSync () {
    //获得当前网络状态
    let netHandle = connection.getDefaultNetSync()
    if (netHandle) {
      let capabilities = connection.getNetCapabilitiesSync(netHandle)
      appLogger.debug(TAG+'getNetCapabilitiesSync:' + JSON.stringify(capabilities))
      if (capabilities && capabilities.bearerTypes && capabilities.bearerTypes.length > 0) {

        // 获取第一个承载类型
        const bearerType = capabilities.bearerTypes[0];
        // 根据承载类型判断网络类型
        switch (bearerType) {
          case BearerType.MOBILE.valueOf():
          // 蜂窝网络
            appLogger.debug(TAG+'currentNetworkState:蜂窝网络')
            this.currentNetworkStatus =  NetworkType.MOBILE;
                break;
          case BearerType.WIFI.valueOf():
          // Wi-Fi网络
            appLogger.debug(TAG+'currentNetworkState:WIFI网络')
            this.currentNetworkStatus =  NetworkType.WIFI;
            break;
          case BearerType.ETHERNET.valueOf():
          // 以太网网络(通常移动设备不支持,但为完整性保留)
            appLogger.debug(TAG+'currentNetworkState:以太网网络')
            this.currentNetworkStatus =  NetworkType.ETHERNET;
            break;
          default:
          // 未知网络类型
            appLogger.debug(TAG+'currentNetworkState:未知网络类型')
            this.currentNetworkStatus =  NetworkType.UNKNOWN;
            break;
        }

      }
    }
  }

  /**
   * 当前网络是否可用
   */
  isNetworkAvailable () {
    return this.isAvailable
  }

  /**
   * 获得当前网络状态
   * @returns
   */
  getCurrentNetworkStatus () {
    return this.currentNetworkStatus
  }
}

核心点解析

  1. 单例模式 :通过单例模式,确保 LibNetworkStatus 的实例在整个应用中唯一。
  2. 网络状态检测:通过调用鸿蒙提供的网络状态检测 API,判断网络是否可用。

七、总结

在本篇中,我们深入探讨了 RCP 网络库的高级特性,包括错误处理、会话管理以及网络状态检测等。通过这些特性,我们可以构建一个更加健壮、高效且易于使用的网络库。在下篇中,我们将通过实际案例,展示如何在鸿蒙运动项目中使用这个网络库,实现各种网络请求功能。敬请期待!

相关推荐
Aisanyi3 小时前
【鸿蒙开发】PC实现开局沉浸式全屏
前端·华为·harmonyos
我睡醒再说6 小时前
以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
harmonyos
我睡醒再说6 小时前
ArkUI-X跨平台开发能力解析:优势与限制场景
harmonyos
我睡醒再说6 小时前
纯血Harmony NETX 5小游戏实践:趣味三消游戏(附源文件)
harmonyos
我睡醒再说6 小时前
HarmonyOS NETX 5ArkUI-X打造数字猜谜游戏:(附源文件)
harmonyos
我睡醒再说6 小时前
纯血Harmony NETX 5小游戏实践:电子木鱼(附源文件)
harmonyos
shenshizhong6 小时前
鸿蒙列表新的实现方式
harmonyos
程序员小刘6 小时前
鸿蒙跨平台开发:打通安卓、iOS生态
android·ios·harmonyos
王二蛋与他的张大花7 小时前
鸿蒙运动项目开发:封装超级好用的 RCP 网络库(上)—— 请求参数封装,类型转化器与日志记录篇
harmonyos