DEV Community

zhonghua
zhonghua

Posted on • Edited on

HarmonyOS Sports Project Development: Packaging an Extremely Useful RCP Network Library (Part 2)

Core Technologies of HarmonyOS ## Sports Development ## Remote Communication Kit (Remote Communication Services)

In the previous part, we introduced the core functions of the RCP network library, including request parameter packaging, response content conversion, and the interceptor and log recording mechanism. These functions provide a solid foundation for our network library. In this part, we will continue to delve into the advanced features of the network library, including error handling, session management, and network status detection, to further enhance the robustness and usability of the network library.

IV. Advanced Features of the Network Library: Error Handling and Exception Management

(I) Custom Exception Class

Error handling is crucial in network requests. To better manage errors, we have defined a NetworkException class to encapsulate various network-related exceptions.

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]: "Network is unavailable, please check your network connection",
    [ErrorCodes.REQUEST_TIMEOUT]: "Request timed out, please try again later",
    [ErrorCodes.SERVER_ERROR]: "The server is down, please try again later",
    [ErrorCodes.INVALID_RESPONSE]: "The server returned an invalid data format",
    [ErrorCodes.UNAUTHORIZED]: "Your login has expired, please log in again",
    [ErrorCodes.FORBIDDEN]: "You do not have permission to access this resource",
    [ErrorCodes.NOT_FOUND]: "The requested resource does not exist",
    [ErrorCodes.UNKNOWN_ERROR]: "Unknown error, please contact customer service",
    [ErrorCodes.URL_NOT_EXIST_ERROR]: "URL does not exist",
    [ErrorCodes.URL_ERROR]: "URL format is invalid",
    [ErrorCodes.BAD_REQUEST]: "The client request syntax is incorrect, the server cannot understand it.",
    [ErrorCodes.REQUEST_CANCEL]: "The request was canceled"
  };

  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
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Error Message Mapping: Through the errorMessages object, map error codes to specific error messages.
  2. Custom Error Messages: Allow custom error messages to be passed in to override the default error descriptions.
  3. Error Classification: Through the isResponseError() method, distinguish between response errors and other types of errors.
  4. Dynamic Error Message Updates: Through the updateErrorMessages() method, allow dynamic updates to the error message mapping table.

(II) Error Handling Logic

In network requests, error handling logic needs to cover a variety of scenarios, including network unavailability, request timeouts, server errors, etc. We have implemented unified error handling by catching and throwing NetworkException in the RcpNetworkService class.


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 network is unavailable")
      throw new NetworkException(ErrorCodes.NETWORK_UNAVAILABLE);
    }


    let url = baseUrl + requestOption.act;

    if (!isValidUrl(url)) {
      appLogger.error("HttpCore URL format is invalid")
      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));
    // Store the mapping relationship between the request and the session
    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) {
      // Get 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(`Exception: ${err.stack} ${err.message} ${err.name}`)
        throw err
      }
    }
  }finally {
    this.rcpSessionManager?.releaseSession(session)
    // When the session is closed, remove all requests associated with this session
    this.requestMap.forEach((entry, key) => {
      if (entry.session === session) {
        this.requestMap.delete(key);
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Network Status Detection: Before initiating a request, detect whether the network is available through LibNetworkStatus.getInstance().isNetworkAvailable().
  2. URL Validation: Validate the URL format through the isValidUrl() method.
  3. Error Classification and Throwing: Throw the corresponding NetworkException based on different error scenarios.
  4. Unified Error Handling: In the catch block, handle all exceptions uniformly to ensure consistency of error messages.

V. Advanced Features of the Network Library: Session Management

(I) Implementation of the Session Pool

To optimize the performance of network requests, we have implemented a session pool mechanism. By reusing sessions, we can reduce the overhead of frequently creating and destroying sessions.

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


// Create a security configuration to skip certificate validation
const securityConfig: rcp.SecurityConfiguration = {
  remoteValidation: 'skip'
};


export class RcpSessionManager{

  private currentConcurrentRequests = 0;

  // Define a connection pool
  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 there is an available session in the connection pool, return it directly
    if (this.connectionPool.length > 0) {
      return this.connectionPool.pop()!;
    }

    // If there is no available session, create a new one
    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 the current concurrent requests are less than the maximum concurrent limit, put the session back into the connection pool
    if (this.currentConcurrentRequests < this._httpConfig.maxConcurrentRequests) {
      this.connectionPool.push(session);
    } else {
      session.close();
    }
  }

  public destroy(): void {
    // Close all sessions
    this.connectionPool.forEach(session => session.close());
    this.connectionPool.length = 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Session Reuse: Store idle sessions in the connectionPool to avoid the overhead of frequently creating and destroying sessions.
  2. Concurrency Limit: Limit the number of concurrent requests based on the maxConcurrentRequests configuration in HttpConfig.
  3. Session Release: After the request is completed, put the session back into the session pool or close the session to optimize resource usage.

(II) The Importance of Session Management

Session management plays a crucial role in network requests. By managing sessions properly, we can significantly improve the performance and stability of network requests. For example:

  • Reduce Connection Overhead: By reusing sessions, reduce the overhead of frequently establishing and closing connections.
  • Control Concurrency: Limit the number of concurrent requests to avoid overwhelming the server with too many concurrent requests.
  • Resource Recycling: Timely release session resources after the request is completed to avoid resource leaks.

VI. Advanced Features of the Network Library: Network Status Detection

Network status detection is essential in network requests. By detecting whether the network is available, we can avoid request failures caused by network issues in advance.


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

const TAG : string = "LibNetworkStatus"

/**
 * Enum: Network Type
 */
export enum NetworkType {
  STATE_NULL = 'NULL',// Network status indicator: Not connected to the network
  UNKNOWN = 'UNKNOWN',// Unknown network
  MOBILE = 'MOBILE',
  WIFI = 'WIFI',
  ETHERNET = 'ETHERNET'
}

/**
 * Enum: Bearer Type (Internal use,对接具体平台API)
 * Note: The values of this enumeration should be consistent with the actual values in the platform API
 */
enum BearerType {
  MOBILE = 0,
  WIFI = 1,
  // ... There may be other bearer types, add according to the platform API
  ETHERNET = 3
}

/**
 * Network Information:
 * 1. Network connection status management
 * 2. Network event registration and cancellation
 */
export class LibNetworkStatus {


  /**
   * LibNetworkStatus singleton instance
   */
  private static instance: LibNetworkStatus
  /**
   * Current network status
   */
  private currentNetworkStatus:NetworkType = NetworkType.STATE_NULL
  /**
   * Whether the network is available
   */
  private isAvailable = false
  /**
   * HarmonyOS network connection object
   */
  private networkConnectio?: connection.NetConnection
  /**
   * Define a set of callback methods, use WeakSet to avoid memory leaks
   */
  private callbacks = new Set<LibNetworkStatusCallback>()

  /**
   * Debounce timer
   */
  private debounceTimer: number | null = null

  /**
   * Debounce delay (milliseconds)
   */
  private static readonly DEBOUNCE_DELAY = 300

  /**
   * Obtain the LibNetworkStatus singleton instance
   * @returns LibNetworkStatus singleton instance
   */
  static getInstance (): LibNetworkStatus {
    if (!LibNetworkStatus.instance) {
      LibNetworkStatus.instance = new LibNetworkStatus()
    }
    return LibNetworkStatus.instance
  }

  /**
   * Add a callback method
   * @param callback Callback method
   * @param isCallBackCurrentNetworkStatus Whether to immediately return the current network status
   */
  addCallback (callback: LibNetworkStatusCallback, isCallBackCurrentNetworkStatus: boolean) {
    if (callback && this.callbacks) {
      appLogger.debug(TAG+"Add callback method")
      if(this.callbacks.has(callback)){
        return
      }
      this.callbacks.add(callback)

      // Immediately callback the current network status
      if (isCallBackCurrentNetworkStatus) {
        appLogger.debug(TAG+'Immediately callback the current network status: ' + this.currentNetworkStatus)
        callback(this.currentNetworkStatus)
      }
    }
  }

  /**
   * Remove a callback method
   * @param callback Callback method
   */
  removeCallback (callback: LibNetworkStatusCallback) {
    if (callback && this.callbacks && this.callbacks.has(callback)) {
      appLogger.debug(TAG+'Remove callback method')
      this.callbacks.delete(callback)
    }
  }

  /**
   * Debounce processing of network status callback
   */
  private debouncedCallback() {
    if (this.debounceTimer !== null) {
      clearTimeout(this.debounceTimer);
    }

    this.debounceTimer = setTimeout(()    () => {
      if (this.callbacks && this.callbacks.size > 0) {
        appLogger.debug(TAG + 'Iterate through the callback set and callback the current network status')
        this.callbacks.forEach(callback => {
          callback(this.currentNetworkStatus)
        })
      }
      this.debounceTimer = null;
    }, LibNetworkStatus.DEBOUNCE_DELAY);
  }

  /**
   * Callback the current network status
   */
  callbackNetworkStatus() {
    this.debouncedCallback();
  }

  /**
   * Register network status listener:
   * The device will trigger "netAvailable", "netCapabilitiesChange", and "netConnectionPropertiesChange" events when switching from no network to a network;
   * The device will trigger the "netLost" event when switching from a network to no network
   * The device will trigger the "netLost" event when switching from Wi-Fi to cellular network (Wi-Fi is unavailable), and then trigger the "netAvailable" event (cellular is available)
   */
  registerNetConnectListener () {
    if (this.networkConnectio) {
      appLogger.debug(TAG+'Already subscribed to network events, no need to subscribe again')
      return
    }

    // Create a NetConnection object
    this.networkConnectio = connection.createNetConnection()

    // Determine the default network status
    let hasDefaultNet = connection.hasDefaultNetSync()
    if (hasDefaultNet) {
      appLogger.debug(TAG+'hasDefaultNetSync  ' + hasDefaultNet)
      this.isAvailable = true
      // Get the default network type
      this.getDefaultNetSync()
    }

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

    // Subscribe to network available event
    appLogger.debug(TAG+'Subscribe to network available event-->')
    this.networkConnectio.on('netAvailable', (data: connection.NetHandle) => {
      appLogger.debug(TAG+'netAvailable:' + JSON.stringify(data))
      this.isAvailable = true

      // Get the default network type
      this.getDefaultNetSync()

      // Callback network status
      this.callbackNetworkStatus()
    })

    // Subscribe to network lost event
    appLogger.debug(TAG+'Subscribe to network lost event-->')
    this.networkConnectio.on('netLost', (data: connection.NetHandle) => {
      appLogger.debug(TAG+'netLost:' + JSON.stringify(data))
      this.isAvailable = false
      this.currentNetworkStatus = NetworkType.STATE_NULL

      // Callback network status
      this.callbackNetworkStatus()
    })

    // Subscribe to network unavailable event
    appLogger.debug(TAG+'Subscribe to network unavailable event-->')
    this.networkConnectio.on('netUnavailable', () => {
      appLogger.debug(TAG+'netUnavailable')
      this.isAvailable = false
      this.currentNetworkStatus = NetworkType.STATE_NULL

      // Callback network status
      this.callbackNetworkStatus()
    })
  }

  /**
   * Get the default network type
   */
  getDefaultNetSync () {
    // Get the current network status
    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) {

        // Get the first bearer type
        const bearerType = capabilities.bearerTypes[0];
        // Determine the network type based on the bearer type
        switch (bearerType) {
          case BearerType.MOBILE.valueOf():
          // Cellular network
            appLogger.debug(TAG+'currentNetworkState:Cellular network')
            this.currentNetworkStatus =  NetworkType.MOBILE;
                break;
          case BearerType.WIFI.valueOf():
          // Wi-Fi network
            appLogger.debug(TAG+'currentNetworkState:Wi-Fi network')
            this.currentNetworkStatus =  NetworkType.WIFI;
            break;
          case BearerType.ETHERNET.valueOf():
          // Ethernet network (usually not supported on mobile devices, but retained for completeness)
            appLogger.debug(TAG+'currentNetworkState:Ethernet network')
            this.currentNetworkStatus =  NetworkType.ETHERNET;
            break;
          default:
          // Unknown network type
            appLogger.debug(TAG+'currentNetworkState:Unknown network type')
            this.currentNetworkStatus =  NetworkType.UNKNOWN;
            break;
        }

      }
    }
  }

  /**
   * Is the current network available
   */
  isNetworkAvailable () {
    return this.isAvailable
  }

  /**
   * Get the current network status
   * @returns
   */
  getCurrentNetworkStatus () {
    return this.currentNetworkStatus
  }
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Singleton Pattern: Ensure that the LibNetworkStatus instance is unique throughout the application using the singleton pattern.
  2. Network Status Detection: Determine whether the network is available by calling the HarmonyOS-provided network status detection API.

VII. Summary

In this part, we have delved into the advanced features of the RCP network library, including error handling, session management, and network status detection. With these features, we can build a more robust, efficient, and easy-to-use network library. In the next part, we will demonstrate how to use this network library in HarmonyOS sports projects through practical cases to implement various network request functions. Stay tuned!

Top comments (0)