鴻蒙運動項目開發(fā):封裝超級好用的 RCP 網(wǎng)絡(luò)庫(中)—— 錯誤處理,會話管理與網(wǎng)絡(luò)狀態(tài)檢測篇

鴻蒙核心技術(shù)##運動開發(fā)## Remote Communication Kit(遠場通信服務(wù))

在上篇中,我們介紹了 RCP 網(wǎng)絡(luò)庫的核心功能,包括請求參數(shù)的封裝、響應(yīng)內(nèi)容的轉(zhuǎn)換以及攔截器與日志記錄機制。這些功能為我們的網(wǎng)絡(luò)庫提供了堅實的基礎(chǔ)。在本篇中,我們將繼續(xù)深入探討網(wǎng)絡(luò)庫的高級特性,包括錯誤處理、會話管理以及網(wǎng)絡(luò)狀態(tài)檢測等,進一步提升網(wǎng)絡(luò)庫的健壯性和易用性。

四、網(wǎng)絡(luò)庫的高級特性:錯誤處理與異常管理

(一)自定義異常類

在網(wǎng)絡(luò)請求中,錯誤處理是至關(guān)重要的。為了更好地管理錯誤,我們定義了一個 NetworkException 類,用于封裝各種網(wǎng)絡(luò)相關(guān)的異常。

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]: "網(wǎng)絡(luò)不可用,請檢查網(wǎng)絡(luò)連接",
    [ErrorCodes.REQUEST_TIMEOUT]: "請求超時,請稍后重試",
    [ErrorCodes.SERVER_ERROR]: "服務(wù)器開小差了,請稍后再試",
    [ErrorCodes.INVALID_RESPONSE]: "服務(wù)器返回數(shù)據(jù)格式錯誤",
    [ErrorCodes.UNAUTHORIZED]: "登錄已過期,請重新登錄",
    [ErrorCodes.FORBIDDEN]: "無權(quán)訪問該資源",
    [ErrorCodes.NOT_FOUND]: "請求的資源不存在",
    [ErrorCodes.UNKNOWN_ERROR]: "未知錯誤,請聯(lián)系客服",
    [ErrorCodes.URL_NOT_EXIST_ERROR]: "URL不存在",
    [ErrorCodes.URL_ERROR]: "URL格式不合法",
    [ErrorCodes.BAD_REQUEST]: "客戶端請求的語法錯誤,服務(wù)器無法理解。",
    [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() 方法,區(qū)分是響應(yīng)錯誤還是其他類型的錯誤。
  4. 錯誤信息動態(tài)更新:通過 updateErrorMessages() 方法,允許動態(tài)更新錯誤信息映射表。

(二)錯誤處理邏輯

在網(wǎng)絡(luò)請求中,錯誤處理邏輯需要覆蓋多種場景,包括網(wǎng)絡(luò)不可用、請求超時、服務(wù)器錯誤等。我們通過在 RcpNetworkService 類中捕獲和拋出 NetworkException,實現(xiàn)了統(tǒng)一的錯誤處理。


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 網(wǎng)絡(luò)不可用")
      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));
    // 將請求和會話的映射關(guān)系存儲起來
    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)
    // 當會話被關(guān)閉時,移除與該會話關(guān)聯(lián)的所有請求
    this.requestMap.forEach((entry, key) => {
      if (entry.session === session) {
        this.requestMap.delete(key);
      }
    });
  }
}

核心點解析

  1. 網(wǎng)絡(luò)狀態(tài)檢測:在發(fā)起請求之前,通過 LibNetworkStatus.getInstance().isNetworkAvailable() 檢測網(wǎng)絡(luò)是否可用。
  2. URL 驗證:通過 isValidUrl() 方法驗證 URL 格式是否正確。
  3. 錯誤分類與拋出:根據(jù)不同的錯誤場景,拋出對應(yīng)的 NetworkException。
  4. 統(tǒng)一的錯誤處理:在 catch 塊中,對所有異常進行統(tǒng)一處理,確保錯誤信息的一致性。

五、網(wǎng)絡(luò)庫的高級特性:會話管理

(一)會話池的實現(xiàn)

為了優(yōu)化網(wǎng)絡(luò)請求的性能,我們實現(xiàn)了會話池機制。通過復用會話,可以減少頻繁創(chuàng)建和銷毀會話的開銷。

import { rcp } from "@kit.RemoteCommunicationKit";
import { HttpConfig } from "./HttpConfig";


// 創(chuàng)建安全配置,跳過證書驗證
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()!;
    }

    // 如果沒有可用的會話,創(chuàng)建一個新的會話
    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 {
    // 如果當前并發(fā)請求小于最大并發(fā)限制,將會話放回連接池
    if (this.currentConcurrentRequests < this._httpConfig.maxConcurrentRequests) {
      this.connectionPool.push(session);
    } else {
      session.close();
    }
  }

  public destroy(): void {
    // 關(guān)閉所有會話
    this.connectionPool.forEach(session => session.close());
    this.connectionPool.length = 0;
  }
}

核心點解析

  1. 會話復用:通過 connectionPool 存儲空閑的會話,避免頻繁創(chuàng)建和銷毀會話。
  2. 并發(fā)限制:根據(jù) HttpConfig 中的 maxConcurrentRequests 配置,限制并發(fā)請求數(shù)量。
  3. 會話釋放:在請求完成后,將會話放回會話池或關(guān)閉會話,以優(yōu)化資源使用。

(二)會話管理的重要性

會話管理在網(wǎng)絡(luò)請求中起著至關(guān)重要的作用。通過合理管理會話,可以顯著提升網(wǎng)絡(luò)請求的性能和穩(wěn)定性。例如:

  • 減少連接開銷:通過復用會話,減少頻繁建立和關(guān)閉連接的開銷。
  • 控制并發(fā)數(shù)量:通過限制并發(fā)請求數(shù)量,避免過多的并發(fā)請求對服務(wù)器造成壓力。
  • 資源回收:在請求完成后及時釋放會話資源,避免資源泄漏。

六、網(wǎng)絡(luò)庫的高級特性:網(wǎng)絡(luò)狀態(tài)檢測

在網(wǎng)絡(luò)請求中,網(wǎng)絡(luò)狀態(tài)的檢測是必不可少的。通過檢測網(wǎng)絡(luò)是否可用,可以提前避免因網(wǎng)絡(luò)問題導致的請求失敗。


import connection from '@ohos.net.connection'
import { appLogger } from '../../app/Application'
import { LibNetworkStatusCallback } from './LibNetworkStatusCallback'

const TAG : string = "LibNetworkStatus"

/**
 * 枚舉:網(wǎng)絡(luò)類型
 */
export enum NetworkType {
  STATE_NULL = 'NULL',//網(wǎng)絡(luò)狀態(tài)標識:未聯(lián)網(wǎng)
  UNKNOWN = 'UNKNOWN',//未知網(wǎng)絡(luò)
  MOBILE = 'MOBILE',
  WIFI = 'WIFI',
  ETHERNET = 'ETHERNET'
}

/**
 * 枚舉:承載類型(內(nèi)部使用,與具體平臺API對接)
 * 注意:這里的枚舉值應(yīng)與平臺API中的實際值保持一致
 */
enum BearerType {
  MOBILE = 0,
  WIFI = 1,
  // ... 可能還有其他承載類型,根據(jù)平臺API添加
  ETHERNET = 3
}

/**
 * 網(wǎng)絡(luò)信息:
 * 1、網(wǎng)絡(luò)連接狀態(tài)管理
 * 2、網(wǎng)絡(luò)事件注冊監(jiān)聽、取消注冊
 */
export class LibNetworkStatus {


  /**
   * LibNetworkStatus單例對象
   */
  private static instance: LibNetworkStatus
  /**
   * 當前網(wǎng)絡(luò)狀態(tài)
   */
  private currentNetworkStatus:NetworkType = NetworkType.STATE_NULL
  /**
   * 網(wǎng)絡(luò)是否可用
   */
  private isAvailable = false
  /**
   * 鴻蒙網(wǎng)絡(luò)連接對象
   */
  private networkConnectio?: connection.NetConnection
  /**
   * 定義回調(diào)方法集合,使用WeakSet避免內(nèi)存泄漏
   */
  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
  }

  /**
   * 添加回調(diào)方法
   * @param callback 回調(diào)方法
   * @param isCallBackCurrentNetworkStatus 是否立即返回當前的網(wǎng)絡(luò)狀態(tài)
   */
  addCallback (callback: LibNetworkStatusCallback, isCallBackCurrentNetworkStatus: boolean) {
    if (callback && this.callbacks) {
      appLogger.debug(TAG+"添加回調(diào)方法")
      if(this.callbacks.has(callback)){
        return
      }
      this.callbacks.add(callback)

      //立即回調(diào)當前網(wǎng)絡(luò)狀態(tài)
      if (isCallBackCurrentNetworkStatus) {
        appLogger.debug(TAG+'立即回調(diào)當前網(wǎng)絡(luò)狀態(tài): ' + this.currentNetworkStatus)
        callback(this.currentNetworkStatus)
      }
    }
  }

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

  /**
   * 防抖處理網(wǎng)絡(luò)狀態(tài)回調(diào)
   */
  private debouncedCallback() {
    if (this.debounceTimer !== null) {
      clearTimeout(this.debounceTimer);
    }

    this.debounceTimer = setTimeout(() => {
      if (this.callbacks && this.callbacks.size > 0) {
        appLogger.debug(TAG + '遍歷callback集合,回調(diào)當前網(wǎng)絡(luò)狀態(tài)')
        this.callbacks.forEach(callback => {
          callback(this.currentNetworkStatus)
        })
      }
      this.debounceTimer = null;
    }, LibNetworkStatus.DEBOUNCE_DELAY);
  }

  /**
   * 回調(diào)當前網(wǎng)絡(luò)狀態(tài)
   */
  callbackNetworkStatus() {
    this.debouncedCallback();
  }

  /**
   * 注冊網(wǎng)絡(luò)狀態(tài)監(jiān)聽:
   * 設(shè)備從無網(wǎng)絡(luò)到有網(wǎng)絡(luò)會觸發(fā)"netAvailable"、"netCapabilitiesChange"、"netConnectionPropertiesChange"事件;
   * 設(shè)備從有網(wǎng)絡(luò)到無網(wǎng)絡(luò)會觸發(fā)"netLost"事件
   * 設(shè)備從wifi到蜂窩網(wǎng)絡(luò)會觸發(fā)"netLost"事件(wifi不可用)、之后觸發(fā)"netAvailable"事件(蜂窩可用)
   */
  registerNetConnectListener () {
    if (this.networkConnectio) {
      appLogger.debug(TAG+'已訂閱網(wǎng)絡(luò)事件,無需再次訂閱')
      return
    }

    //創(chuàng)建NetConnection對象
    this.networkConnectio = connection.createNetConnection()

    //判斷默認網(wǎng)絡(luò)狀態(tài)
    let hasDefaultNet = connection.hasDefaultNetSync()
    if (hasDefaultNet) {
      appLogger.debug(TAG+'hasDefaultNetSync  ' + hasDefaultNet)
      this.isAvailable = true
      //獲得默認網(wǎng)絡(luò)類型
      this.getDefaultNetSync()
    }

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

    //訂閱網(wǎng)絡(luò)可用事件
    appLogger.debug(TAG+'訂閱網(wǎng)絡(luò)可用事件-->')
    this.networkConnectio.on('netAvailable', (data: connection.NetHandle) => {
      appLogger.debug(TAG+'netAvailable:' + JSON.stringify(data))
      this.isAvailable = true

      //獲得默認網(wǎng)絡(luò)類型
      this.getDefaultNetSync()

      //回調(diào)網(wǎng)絡(luò)狀態(tài)
      this.callbackNetworkStatus()
    })

    //訂閱網(wǎng)絡(luò)丟失事件
    appLogger.debug(TAG+'訂閱網(wǎng)絡(luò)丟失事件-->')
    this.networkConnectio.on('netLost', (data: connection.NetHandle) => {
      appLogger.debug(TAG+'netLost:' + JSON.stringify(data))
      this.isAvailable = false
      this.currentNetworkStatus = NetworkType.STATE_NULL

      //回調(diào)網(wǎng)絡(luò)狀態(tài)
      this.callbackNetworkStatus()
    })

    //訂閱網(wǎng)絡(luò)不可用事件
    appLogger.debug(TAG+'訂閱網(wǎng)絡(luò)不可用事件-->')
    this.networkConnectio.on('netUnavailable', () => {
      appLogger.debug(TAG+'netUnavailable')
      this.isAvailable = false
      this.currentNetworkStatus = NetworkType.STATE_NULL

      //回調(diào)網(wǎng)絡(luò)狀態(tài)
      this.callbackNetworkStatus()
    })
  }

  /**
   * 獲得默認網(wǎng)絡(luò)類型
   */
  getDefaultNetSync () {
    //獲得當前網(wǎng)絡(luò)狀態(tài)
    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];
        // 根據(jù)承載類型判斷網(wǎng)絡(luò)類型
        switch (bearerType) {
          case BearerType.MOBILE.valueOf():
          // 蜂窩網(wǎng)絡(luò)
            appLogger.debug(TAG+'currentNetworkState:蜂窩網(wǎng)絡(luò)')
            this.currentNetworkStatus =  NetworkType.MOBILE;
                break;
          case BearerType.WIFI.valueOf():
          // Wi-Fi網(wǎng)絡(luò)
            appLogger.debug(TAG+'currentNetworkState:WIFI網(wǎng)絡(luò)')
            this.currentNetworkStatus =  NetworkType.WIFI;
            break;
          case BearerType.ETHERNET.valueOf():
          // 以太網(wǎng)網(wǎng)絡(luò)(通常移動設(shè)備不支持,但為完整性保留)
            appLogger.debug(TAG+'currentNetworkState:以太網(wǎng)網(wǎng)絡(luò)')
            this.currentNetworkStatus =  NetworkType.ETHERNET;
            break;
          default:
          // 未知網(wǎng)絡(luò)類型
            appLogger.debug(TAG+'currentNetworkState:未知網(wǎng)絡(luò)類型')
            this.currentNetworkStatus =  NetworkType.UNKNOWN;
            break;
        }

      }
    }
  }

  /**
   * 當前網(wǎng)絡(luò)是否可用
   */
  isNetworkAvailable () {
    return this.isAvailable
  }

  /**
   * 獲得當前網(wǎng)絡(luò)狀態(tài)
   * @returns
   */
  getCurrentNetworkStatus () {
    return this.currentNetworkStatus
  }
}

核心點解析

  1. 單例模式:通過單例模式,確保 LibNetworkStatus 的實例在整個應(yīng)用中唯一。
  2. 網(wǎng)絡(luò)狀態(tài)檢測:通過調(diào)用鴻蒙提供的網(wǎng)絡(luò)狀態(tài)檢測 API,判斷網(wǎng)絡(luò)是否可用。

七、總結(jié)

在本篇中,我們深入探討了 RCP 網(wǎng)絡(luò)庫的高級特性,包括錯誤處理、會話管理以及網(wǎng)絡(luò)狀態(tài)檢測等。通過這些特性,我們可以構(gòu)建一個更加健壯、高效且易于使用的網(wǎng)絡(luò)庫。在下篇中,我們將通過實際案例,展示如何在鴻蒙運動項目中使用這個網(wǎng)絡(luò)庫,實現(xiàn)各種網(wǎng)絡(luò)請求功能。敬請期待!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容