Skip to main content

从代码学习如何设计一个健壮的设备应用

分类:  Azure物联网 标签:  #Azure #IoT Hub # #入门 #指南 发布于: 2023-06-13 22:31:50

我们上一章学习了设备SDK连接状态,以及连接状态改变和原因,我们也用一个简单的应用观察和学习了设备SDK在不同的情况下的不同表现。微软发布的Azure IoT Hub .Net Device SDK包含了一个很不错的例子,这个例子里演示了如何创建一个健壮的设备应用。我们本节来学习这个例子。

注意
本章提及的原始代码可以参考:https://github.com/Azure/azure-iot-sdk-csharp/tree/main/iothub/device/samples/how to guides/DeviceReconnectionSample

需要注意的是这部分代码里有这样几个文件:

  • ExceptionHelper.cs: 主要是用于判定当SDK中发生异常时,该异常是网络异常还是安全异常。
  • CustomRetryPolicy.cs: 自定义重试规则,这个重试规则根据异常决定是否会重试,该类实现了接口IRetryPolicy
  • DeviceReconnectionSample.cs: 该文件演示了如何定义一个健壮的设备应用,基本考虑到了各个方面的应用设计技巧。

ExceptionHelper.cs文件浏览

ExceptionHelper首先定义了一个设备SDK中的异常:

  private static readonly HashSet<Type> s_networkExceptions = new()
        {
            typeof(IOException),
            typeof(SocketException),
            typeof(ClosedChannelException),
            typeof(TimeoutException),
            typeof(OperationCanceledException),
            typeof(HttpRequestException),
            typeof(WebException),
            typeof(WebSocketException),
        };

同时还通过如下代码判断运行的平台是否是Windows平台。

private static readonly bool s_isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

还定义了一个非常值得研究的Unwind方法,这个方法中应用了yeild return以及递归,结合这两种方法帮助判断传入的异常是网络异常还是安全异常。

 private static IEnumerable<Exception> Unwind(Exception exception, bool unwindAggregate = false)
{
    while (exception != null)
    {
        yield return exception;

        if (!unwindAggregate)
        {
            exception = exception.InnerException;
            continue;
        }

        if (exception is AggregateException aggEx
            && aggEx.InnerExceptions != null)
        {
            foreach (Exception ex in aggEx.InnerExceptions)
            {
                foreach (Exception innerEx in Unwind(ex, true))
                {
                    yield return innerEx;
                }
            }
        }

        exception = exception.InnerException;
    }
}

CustomRetryPolicy.cs

CustomRetryPolicy实现了接口IRetryPolicy, 该接口必须实现一个方法:ShouldRetry, 在这个方法里,使用辅助类ExceptionHelper判断什么样的异常可以重试,这包括:IotHubException, 以及网络异常,并且判定IotHubException是否瞬时异常,这几种情况可以进入重试。

详细的代码,请查看原始代码。

DeviceReconnectionSample.cs

DeviceReconnectionSample 详细的展示了如何跟踪设备连接状态的变化,以及是否需要重试。

定义了方法:ShouldClientBeInitialized判断是否需要重新初始化客户端:

private bool ShouldClientBeInitialized(ConnectionStatus connectionStatus)
{
    return (connectionStatus == ConnectionStatus.Disconnected || connectionStatus == ConnectionStatus.Disabled)
        && _deviceConnectionStrings.Any();
}

从上述代码可以看到当连接状态为Disconnected以及Disabled时,需要重新构建客户端实例。也需要明白的时,当我们使用方法创建了DeviceCient的实例,并不代表着,该客户端已经通过底层的Transport建立了连接,仅仅当有方法调用到deviceClient.OpenAsync()的时候,才真正建立和服务端的网络连接。当连接的状态变为Disconnected以及Disabled之后,代表底层的Transport的网络连接已经全部关停,对象资源也已经释放,这个时候我们要做的是同时释放DeviceClient的实例,并重新创建一个实例。

代码中定义了一个监控连接状态变化的Handler:
private async void ConnectionStatusChangeHandlerAsync(ConnectionStatus status, ConnectionStatusChangeReason reason)

在这个方法里,针对不同的状态采取了不同的措施:

  • ConnectionStatus.Connected: 表明设备连接成功了。最佳实践是只要设备连接成功后,立即全部拉取设备的Twin然后有针对性的处理。
  • ConnectionStatus.Disconnected_RetryingConnectionStatus.Disabled: 分别提示日志就好了,这里面需要注意的是,针对Disabled是可以重试的。
  • ConnectionStatus.Disconnected: 当时Disconnected状态时,代码设计了检查Reason: 如果原因是ConnectionStatusChangeReason.Bad_Credential, 则重新初始化客户端,因为在代码中传入了多个连接字符串,例如在使用对称密钥连接时,同时传入PrimarySecondly密钥。如果是ConnectionStatusChangeReason.Retry_ExpiredConnectionStatusChangeReason.Communication_Error 则直接重新创建客户端。

其中重要的设计包括:

  • 在类中使用关键字volatile定义DeviceClients_connectionStatus
  • 同时使用了多个连接字符串,当某个连接字符串失效时,启用备用连接字符串。

最重要的几个方法:

  • InitializeAndSetupClientAsync
  • ConnectionStatusChangeHandlerAsync
  • ShouldClientBeInitialized

还有一个技巧是: await Task.WhenAll(SendMessagesAsync(s_appCancellation.Token), ReceiveMessagesAsync(s_appCancellation.Token));

设备连接最佳实践
当设备的连接状态更改为ConnectionStatus.Connected时,第一时间使用设备客户端从Azure IoT Hub取回设备的Twin, 并根据Twin中定义的属性配置设备相关的参数。

当设备的连接状态变为ConnectionStatus.Connected, 第一件要做的事就是从Azure IoT Hub取回完整的Twin, 并根据当时的值设定设备上相关的值:

 case ConnectionStatus.Connected:
    _logger.LogDebug("### The DeviceClient is CONNECTED; all operations will be carried out as normal.");

    // Call GetTwinAndDetectChangesAsync() to retrieve twin values from the server once the connection status changes into Connected.
    // This can get back "lost" twin updates in a device reconnection from status like Disconnected_Retrying or Disconnected.
    //
    // Howevever, considering how a fleet of devices connected to a hub may behave together, one must consider the implication of performing
    // work on a device (e.g., get twin) when it comes online. If all the devices go offline and then come online at the same time (for example,
    // during a servicing event) it could introduce increased latency or even throttling responses.
    // For more information, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-quotas-throttling#traffic-shaping.
    await GetTwinAndDetectChangesAsync(s_appCancellation.Token);
    _logger.LogDebug("The client has retrieved twin values after the connection status changes into CONNECTED.");
    break;

更为详细的说明,大家可以参考一下源代码。

下一节我们依据这个例子改进代码,以便设备应用更加健壮。