鴻蒙核心技術(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
}
}
}
核心點解析:
-
錯誤信息映射:通過
errorMessages對象,將錯誤代碼映射到具體的錯誤信息。 - 自定義錯誤信息:允許傳入自定義的錯誤信息,以覆蓋默認的錯誤描述。
-
錯誤分類:通過
isResponseError()方法,區(qū)分是響應(yīng)錯誤還是其他類型的錯誤。 -
錯誤信息動態(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);
}
});
}
}
核心點解析:
-
網(wǎng)絡(luò)狀態(tài)檢測:在發(fā)起請求之前,通過
LibNetworkStatus.getInstance().isNetworkAvailable()檢測網(wǎng)絡(luò)是否可用。 -
URL 驗證:通過
isValidUrl()方法驗證 URL 格式是否正確。 -
錯誤分類與拋出:根據(jù)不同的錯誤場景,拋出對應(yīng)的
NetworkException。 -
統(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;
}
}
核心點解析:
-
會話復用:通過
connectionPool存儲空閑的會話,避免頻繁創(chuàng)建和銷毀會話。 -
并發(fā)限制:根據(jù)
HttpConfig中的maxConcurrentRequests配置,限制并發(fā)請求數(shù)量。 - 會話釋放:在請求完成后,將會話放回會話池或關(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
}
}
核心點解析:
-
單例模式:通過單例模式,確保
LibNetworkStatus的實例在整個應(yīng)用中唯一。 - 網(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ò)請求功能。敬請期待!