目录

应用网络重连

概述

网络重连是指设备或应用程序在网络连接中断或出现异常时,重新建立通信链路的过程。对于高度依赖网络的业务与应用而言,网络重连机制能够在网络发生短暂中断后迅速恢复连接,从而显著缩短业务中断时间,提升服务的连续性与可靠性。特别是在在线金融交易、远程医疗及音视频实时播放等对实时性和稳定性要求极高的场景中,网络重连功能发挥着至关重要的作用。根据具体的应用场景,网络重连机制可划分为多种实现方式。

网络超时重连

[h2]场景描述

在网络请求中,经常会遇到网络波动、服务器宕机等情况,从而导致网络不可用、网络超时等问题。为了减少网络超时等带来的影响,在实际应用开发中经常使用超时机制和重试机制。如HTTP请求列表数据时,设置HTTP连接超时和请求重试。

[h2]实现原理

网络超时分为网络连接超时和网络读取超时。

网络重试常用的策略有定时重试、指数退避重试、随机退避重试等。

在设置重试策略时,需要根据实际的场景来进行设置,既要考虑网络超时的时间,还需要关注重试的次数和时间间隔,避免网络资源浪费。

[h2]开发步骤

在HarmonyOS中,在Http、RCP发生错误或者超时后,都可以使用网络超时重连的机制。HTTP超时重连的实现步骤如下所示。

HTTP超时重连的代码如下所示:

async getHttpRequest(url: string, retry: number): Promise<number | undefined> {
  try {
    return await this.httpRequest?.requestInStream(url,
      { method: http.RequestMethod.GET, connectTimeout: 6000, readTimeout: 60000 })
      .then((data: number) => {
        if ((data === 408 || data === 500) && retry > 0) {
          return new Promise((resolve: Function) => {
            setTimeout(() => {
              resolve(this.getHttpRequest(url, retry - 1));
            }, 2000);
          });
        } else {
          return data;
        }
      });
  } catch (err) {
    this.isDownload = false;
    try {
      this.getUIContext().getPromptAction().showToast({ message: $r('app.string.download_error') });
    } catch (error) {
      let err = error as BusinessError;
      hilog.error(0xFF00, 'NetworkReconnection', `showToast fail, code = ${err.code}, message = ${err.message}`);
    }
    return;
  }
}

RCP超时重连的实现与HTTP的实现步骤类似,RCP超时重连的代码如下所示:

createRCPSession(): rcp.Session | null {
  try {
    const customHttpEventsHandler: rcp.HttpEventsHandler = {
      onDownloadProgress: (totalSize: number, transferredSize: number) => {
        this.contentLength = totalSize;
        this.downloadSize = transferredSize;
        this.process = this.contentLength === 0 ? 0 : Math.floor(this.downloadSize / this.contentLength * 100);
      },
      onDataEnd: () => {
        this.contentLength = -1;
        this.downloadSize = 0;
      },
    };
    const sessionConfig: rcp.SessionConfiguration = {
      requestConfiguration: {
        transfer: {
          timeout: {
            connectMs: 6000,
            transferMs: 60000
          }
        },
        tracing: { httpEventsHandler: customHttpEventsHandler }
      }
    }
    return rcp.createSession(sessionConfig);
  } catch (error) {
    return null;
  }
}

// ...

async getRcpRequest(url: string, retry: number): Promise<rcp.Response | undefined> {
  try {
    if (this.session !== null) {
      return await this.session.get(url)
        .then((response) => {
          if ((response.statusCode === 408 || response.statusCode === 500) && retry > 0) {
            return new Promise((resolve: Function) => {
              setTimeout(() => {
                resolve(this.getRcpRequest(url, retry - 1));
              }, 2000)
            })
          } else {
            return response;
          }
        })
    } else {
      return;
    }
  } catch (err) {
    try {
      this.getUIContext().getPromptAction().showToast({ message: $r('app.string.download_error') });
      this.isDownload = false;
    } catch (error) {
      let err = error as BusinessError;
      Logger.error('NetworkReconnection', `showToast fail, code = ${err.code}, message = ${err.message}`);
    }
    return;
  }
}

网络切换重连

[h2]场景描述

在当下这个数字化时代,大部分的应用确实离不开网络,网络已经深度渗透到各类应用场景之中。然而,在网络状态切换后,如何继续保持网络连接是许多应用需要处理的问题。

[h2]实现原理

网络切换主要分为网络类型切换和无网络与有网络之间的切换。针对网络切换的场景,HarmonyOS提供了网络连接管理能力,用于查询网络信息、监听网络连接的变化等。

在实现网络切换重连上,主要包含以下步骤。

[h2]开发步骤

调用connection的register()方法订阅网络变化通知,同时,订阅netCapabilitiesChange网络能力变化事件,订阅netLost网络丢失事件。在设备从有网络到无网络状态会触发netLost事件,从无网络到有网络会触发netCapabilitiesChange事件。而在网络类型切换时,也会触发netLost事件和netCapabilitiesChange事件,开发者可以根据实际场景需要在netCapabilitiesChange事件中,将网络类型及网络状态存储在AppStorage中。

private netCon: connection.NetConnection = connection.createNetConnection();

register() {
  this.netCon.register((error: BusinessError) => {
    Logger.error('net register' + JSON.stringify(error));
  });
}

netCapabilitiesChange() {
  this.netCon.on('netCapabilitiesChange', (data: connection.NetCapabilityInfo) => {
    let netAvailable = false;
    data.netCap.networkCap?.forEach((value) => {
      if (value === connection.NetCap.NET_CAPABILITY_INTERNET) {
        netAvailable = true;
      }
    })
    Logger.info('ConnectionUtil.netAvailable:' + netAvailable);
    AppStorage.setOrCreate('netAvailable', netAvailable);
  })

  this.netCon.on('netLost', (data: connection.NetHandle) => {
    AppStorage.setOrCreate('netAvailable', false);
    Logger.info("WifiChangeListen-- Succeeded to get data: " + JSON.stringify(data));
  });
}

使用AppStorage存储应用全局网络状态。

@StorageProp('netAvailable') @Watch('onSocketUpdated') netAvailable: boolean = true;

使用@Watch监听状态变量的变化,并根据对应的变化和实际应用场景重新连接网络。

onSocketUpdated() {
  this.netAvailable ? this.tcpSocketConnect() : this.tcpSocketDisconnect();
  Logger.info('netAvailable:' + this.netAvailable);
}

// ...

tcpSocketConnect() {
  if (!this.netAvailable) {
    try {
      this.getUIContext().getPromptAction().showToast({ message: $r('app.string.connect_error') });
    } catch (error) {
      let err = error as BusinessError;
      Logger.error('NetworkReconnection', `showToast fail, code = ${err.code}, message = ${err.message}`);
    }
    return;
  }

  this.tcp = socket.constructTCPSocketInstance();
  this.tcp.on('connect', () => {
    Logger.info('on connect');
  });
  this.tcp.on('close', () => {
    Logger.info('on close');
  });
  this.tcp.on('error', (error: BusinessError) => {
    Logger.error('error:' + error.code + error.message
    );
  });
  const clientIpAddress: socket.NetAddress = {
    address: CommonConstants.IP_ADDRESS,
    port: CommonConstants.CLIENT_IP_PORT
  } as socket.NetAddress;

  this.tcp.bind(clientIpAddress, (err: BusinessError) => {
    if (err) {
      Logger.error('bind fail' + JSON.stringify(err));
      this.getUIContext().getPromptAction().showToast({ message: $r('app.string.connect_error') });
      return;
    }

    let tcpConnect: socket.TCPConnectOptions = {} as socket.TCPConnectOptions;
    const serverIpAddress: socket.NetAddress = {
      address: CommonConstants.IP_ADDRESS,
      port: CommonConstants.IP_PORT
    } as socket.NetAddress;
    tcpConnect.address = serverIpAddress;
    tcpConnect.timeout = 3000;
    this.tcp?.connect(tcpConnect, (err: BusinessError) => {
      if (err) {
        Logger.error('connect fail');
        this.getUIContext().getPromptAction().showToast({ message: $r('app.string.connect_error') });
        return;
      }
    });
  });
}

应用前后台切换后重连

[h2]场景描述

应用在使用TCPSocket、UDPSocket等通信时,如果未申请长时任务或短时任务,当应用退到后台一段时间后,可能遇到网络不可用或网络资源异常的情况,并且将应用切回前台后,继续使用之前的TCPSocket、UDPSocket等连接对象继续和服务器通信也可能出现网络异常。

[h2]实现原理

在HarmonyOS中,应用切换到后台2秒后,应用的网络资源会被冻结,并且在12秒后进行释放。此时,再继续使用网络资源,就会出现网络不可用的情况。如果应用有后台使用网络资源的场景,可以使用短时任务或长时任务。

由于Socket通信是基于IP和端口进行通信的,在应用退到后台后,网络资源被冻结时会清空TCP、UDP连接对象的IP和端口,但是不会释放连接对象。在应用切换到前台后,系统会给连接对象重新分配新的IP和端口,继续使用之前的连接对象与服务器进行通信时,服务器会认为同一个连接对象前后IP和端口不一致,从而导致通信不可信、网络异常。

应用前后台切换后网络重连在实现上有以下两个关键部分。

[h2]开发步骤

前后台切换时,在UIAbility组件生命周期中存储前后台状态。

export default class EntryAbility extends UIAbility {
  // ...

  onForeground(): void {
    // ...
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // ...
    AppStorage.setOrCreate('onForeground', false);
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

使用@StorageProp同步前后台状态,并使用@Watch监听状态变化。

@StorageProp('onForeground') @Watch('onForegroundChange') onForeground: boolean = true;
// ...

onForegroundChange(): void {
  this.onForeground ? this.tcpSocketConnect() : this.tcpSocketDisconnect();
}

示例代码