🐷

HarmonyOS 運動プロジェクト開発:超便利な RCP ネットワークライブラリのカプセル化(中編)

に公開

HarmonyOS 運動プロジェクト開発:超便利な RCP ネットワークライブラリのカプセル化(中編)—— エラー処理、セッション管理、ネットワークステータスの検出編

HarmonyOS 核心技術##運動開発## Remote Communication Kit(遠隔通信サービス)

前編では、RCP ネットワークライブラリのコア機能、リクエストパラメータのカプセル化、レスポンス内容の変換、インターセプターとログ記録のメカニズムについて紹介しました。これらの機能は、ネットワークライブラリの堅牢性と使いやすさを向上させるための基盤を提供します。本編では、エラー処理、セッション管理、ネットワークステータスの検出など、ネットワークライブラリの高度な特性についてさらに詳しく探求し、ネットワークライブラリの堅牢性と使いやすさを向上させます。

  1. ネットワークライブラリの高度な特性:エラー処理と例外管理

(1)カスタム例外クラス

ネットワークリクエストにおいて、エラー処理は非常に重要です。エラーをより効果的に管理するために、NetworkException クラスを定義し、各種ネットワーク関連の例外をカプセル化しました。

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() メソッドを使用して、エラーメッセージのマッピング表を動的に更新することができます。

(2)エラー処理のロジック

ネットワークリクエストにおいて、エラー処理のロジックは複数のシナリオをカバーする必要があります。ネットワークが利用できない、リクエストがタイムアウトする、サーバーにエラーが発生するなどです。RcpNetworkService クラスで NetworkException をキャッチしてスローすることで、統一的なエラー処理を実現しました。


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ブロック内では、すべての例外を統一的に処理し、エラーメッセージの一貫性を保証します。

5. ネットワークライブラリの高度な特性:セッション管理

(1)セッションプールの実装

ネットワーク要求のパフォーマンスを最適化するために、セッションプールメカニズムを実装しました。セッションの再利用により、セッションの頻繁な作成と破棄のオーバーヘッドを減らすことができます。

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. 並行制限:HttpConfigmaxConcurrentRequests設定に従って、並行要求の数を制限する。
  3. セッションの開放:要求が完了した後、セッションをセッションプールに戻すか閉じることで、リソースの使用を最適化する。

(2)セッション管理の重要性

ネットワーク要求において、セッション管理は非常に重要な役割を果たします。セッションを適切に管理することで、ネットワーク要求のパフォーマンスと安定性を大幅に向上させることができます。例えば:

  • 接続オーバーヘッドの削減:セッションの再利用により、接続の頻繁な確立と閉じることによるオーバーヘッドを減らす。
  • 並行数の制御:並行要求の数を制限することで、サーバーに過度な負荷がかからないようにする。
  • リソースの開放:要求が完了した後、セッションリソースをタイムリーに開放することで、リソースリークを避ける。
  1. ネットワークライブラリの高度な特性:ネットワークステータスの検出

ネットワーク要求において、ネットワークステータスの検出は欠かせません。ネットワークが利用可能かどうかを検出することで、ネットワーク問題による要求の失敗を事前に回避することができます。


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,
  // ... 他のキャリアタイプも必要に応じて追加する
  ETHERNET = 3
}

/**
 * ネットワーク情報:
 * 1、ネットワーク接続ステータスの管理
 * 2、ネットワークイベントの登録とリスニング、登録の解除
 */
export class LibNetworkStatus {


  /**
   * LibNetworkStatusシングルトンオブジェクト
   */
  private static instance: LibNetworkStatus
  /**
   * 現在のネットワークステータス
   */
  private currentNetworkStatus:NetworkType = NetworkType.STATE_NULL
  /**
   * ネットワークが利用可能かどうか
   */
  private isAvailable = false
  /**
   * HarmonyOSネットワーク接続オブジェクト
   */
  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 + 'コールバック集合を順番に呼び出し、現在のネットワークステータスをコールバックする')
        this.callbacks.forEach(callback => {
          callback(this.currentNetworkStatus)
        })
      }
      this.debounceTimer = null;
    }, LibNetworkStatus.DEBOUNCE_DELAY);
  }

  /**
   * 現在のネットワークステータスをコールバックする
   */
  callbackNetworkStatus() {
    this.debouncedCallback();
  }

  /**
   * ネットワークステータスのリスナーを登録する:
   * デバイスがネットワークに接続された場合、「netAvailable」、「netCapabilitiesChange」、「netConnectionPropertiesChange」イベントがトリガーされます。
   * デバイスがネットワークから切断された場合、「netLost」イベントがトリガーされます。
   * デバイスがWi-Fiから携帯電話ネットワークに切り替わった場合、「netLost」イベント(Wi-Fiが利用不可)がトリガーされ、その後「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. ネットワークステータスの検出:HarmonyOSが提供するネットワークステータスの検出APIを呼び出して、ネットワークが利用可能かどうかを判断する。

  3. まとめ

本編では、エラー処理、セッション管理、ネットワークステータスの検出など、RCPネットワークライブラリの高度な特性について詳しく探求しました。これらの特性を活用することで、より堅牢で効率的で使いやすいネットワークライブラリを構築することができます。次回の記事では、実際のケースを通じて、このネットワークライブラリを使用して、HarmonyOS運動プロジェクトでさまざまなネットワーク要求機能を実現する方法を示します。乞うご期待!

Discussion