交通控制示例应用程序模拟高速公路交通控制系统。 其用途是检测超速车辆,并向违规司机发送罚款通知。 这些系统实际上存在于现实生活中,下面是它们的工作原理。 一组摄像头(每个车道上方各一个)被放置在高速公路的起点和终点(假设该路段为 10 公里),没有上匝道或下匝道。 当车辆在摄像头下方经过时,摄像头会拍摄车辆照片。 使用光学字符识别 (OCR) 软件,从照片中提取车辆的车牌号。 系统使用每个车辆的入口和出口时间戳来计算该车辆的平均速度。 如果平均速度高于高速公路的最大速度限制,系统会检索司机信息并自动发送罚款通知。

传统交通目标检测算法 交通目标控制_应用程序

 

 

图 4-1。 示例应用程序中的服务。

  • 摄像头模拟是一种控制台应用程序,它模拟车辆并将消息发送到 TrafficControl 服务。 每个模拟汽车都会调用入口和出口服务终结点。
  • TrafficControl 服务是一种 ASP.NET Core Web API 应用程序,它会公开 /entrycam 和 /exitcam 终结点。 调用的终结点将模拟每个经过入口或出口摄像头的车。 请求消息有效负载仅包含车辆牌照(没有实现实际 OCR)。
  • FineCollection 服务是一种 ASP.NET Core Web API 应用程序,它提供 1 个终结点:/collectfine。 调用此终结点将向超速车辆的司机发送罚款通知。 有效负载包含关于超速违规的所有信息。
  • VehicleRegistration 服务是一种 ASP.NET Core Web API 应用程序,它提供 1 个终结点:/vehicleinfo/{licensenumber}。 它用于根据 URL 中发送的牌照号码(例如 /vehicleinfo/RV-752-S)获取超速车辆的车辆信息和车主信息。

图 4.2 中的序列图展示了此模拟流:

传统交通目标检测算法 交通目标控制_应用程序_02

 

 

使用 Dapr 构建基块

Dapr 的目标之一是为微服务应用程序提供云原生功能。 交通控制应用程序使用 Dapr 构建基块来提高可靠性并缓解上文所述的设计缺陷所带来的影响。 图 4 展示的是启用 Dapr 版本的交通控制应用程序:

传统交通目标检测算法 交通目标控制_传统交通目标检测算法_03

 

 

图 4-3。 具有 Dapr 构建基块的交通控制应用程序。

  1. 服务调用 Dapr 服务调用构建块处理 FineCollectionService 和 VehicleRegistrationService 之间的请求/响应通信。 因为该调用是检索完成操作所需数据的查询,所以此处可以接受同步调用。 服务调用构建基块提供服务发现。 FineCollection 服务不再需要知道 VehicleRegistration 服务所在的位置。 如果 VehicleRegistration 服务脱机,它还会实现自动重试。
  2. 发布 & 订阅 发布和订阅构建基块可处理异步消息传送,以便将 TrafficControl 服务中的超速违规信息发送到 FineCollectionService。 此实现能分离 TrafficControl 和 FineCollection 服务。 如果 FineCollectionService 暂时不可用,数据会在队列中累积,并在稍后恢复处理。 RabbitMQ 是当前的消息代理,用于将消息从生成方传输到使用方。 因为 Dapr 发布/订阅构建基块将消息代理抽象化,所以开发人员无需了解 RabbitMQ 客户端库的详细信息。 切换到另一个消息代理时,不需要更改代码,只需完成配置。
  3. 状态管理 TrafficControl 服务使用状态管理构建基块将车辆状态持久保存服务之外的 Redis 缓存中。 与发布/订阅一样,开发人员无需了解 Redis 特定的 API。 切换到另一个数据存储时,不需要更改代码。
  4. 输出绑定 FineCollection 服务通过电子邮件将罚款信息发送给超速车辆的车主。 SMTP 的 Dapr 输出绑定使用 SMTP 协议将电子邮件传输抽象化。
  5. 输入绑定 CameraSimulation 使用 MQTT 协议将包含模拟车辆信息的消息发送至 TrafficControl 服务。 它使用 .NET MQTT 库将消息发送到 Mosquitto,Mosquitto 是轻量型的 MQTT 代理。 TrafficControl 服务使用 MQTT 的 Dapr 输入绑定来订阅 MQTT 代理并接收消息。
  6. 机密管理 FineCollectionService 需要用于连接到 SMTP 服务器的凭据以及内部使用的罚款计算器组件的许可证密钥。 它使用机密管理构建基块来获取凭据和许可证密钥。
  7. 执行组件 TrafficControlService 具有基于 Dapr 执行组件的替代实现。 在此实现中,TrafficControl 服务会针对入口摄像头记录的每个车辆创建一个新的执行组件。 车辆的牌照号码构成唯一的执行组件 ID。执行组件封装车辆状态,并将其持久保存在 Redis 缓存中。 当出口摄像头记录到车辆时,会调用该执行组件。 执行组件随后会计算平均车速,并可能得出超速违规结果。

图 4.4 展示的是包含所有 Dapr 构建基块的模拟流序列图:

 

传统交通目标检测算法 交通目标控制_应用程序_04

状态管理

 TrafficControllerr控件器,保存和读取车辆状态

/// <summary>
/// 车辆状态库
/// </summary>
public interface IVehicleStateRepository
{
    Task SaveVehicleStateAsync(VehicleState vehicleState);
    /// <summary>
    /// 
    /// </summary>
    /// <param name="licenseNumber">车牌号</param>
    /// <returns></returns>
    Task<VehicleState?> GetVehicleStateAsync(string licenseNumber);
}

  

namespace TrafficControlService.Repositories;

public class DaprVehicleStateRepository : IVehicleStateRepository
{
    private const string DAPR_STORE_NAME = "statestore";
    private readonly DaprClient _daprClient;

    public DaprVehicleStateRepository(DaprClient daprClient)
    {
        _daprClient = daprClient;
    }

    public async Task SaveVehicleStateAsync(VehicleState vehicleState)
    {
        await _daprClient.SaveStateAsync<VehicleState>(
            DAPR_STORE_NAME, vehicleState.LicenseNumber, vehicleState);
    }

    public async Task<VehicleState?> GetVehicleStateAsync(string licenseNumber)
    {
        var stateEntry = await _daprClient.GetStateEntryAsync<VehicleState>(
            DAPR_STORE_NAME, licenseNumber);
        return stateEntry.Value;
    }
}

  

namespace TrafficControlService.Controllers;
//交通控制器
[ApiController]
[Route("")]
public class TrafficController : ControllerBase
{
    private readonly ILogger<TrafficController> _logger;
    private readonly IVehicleStateRepository _vehicleStateRepository;
    private readonly ISpeedingViolationCalculator _speedingViolationCalculator;
    private readonly string _roadId;

    public TrafficController(
        ILogger<TrafficController> logger,
        IVehicleStateRepository vehicleStateRepository,
        ISpeedingViolationCalculator speedingViolationCalculator)
    {
        _logger = logger;
        _vehicleStateRepository = vehicleStateRepository;
        _speedingViolationCalculator = speedingViolationCalculator;
        _roadId = speedingViolationCalculator.GetRoadId();
    }

    [HttpPost("entrycam")]
    public async Task<ActionResult> VehicleEntryAsync(VehicleRegistered msg)
    {
        try
        {
            // log entry
            _logger.LogInformation($"ENTRY detected in lane {msg.Lane} at {msg.Timestamp.ToString("hh:mm:ss")} " +
                $"of vehicle with license-number {msg.LicenseNumber}.");

            // store vehicle state 进入高速,并保存状态到redis
            var vehicleState = new VehicleState(msg.LicenseNumber, msg.Timestamp, null);
            await _vehicleStateRepository.SaveVehicleStateAsync(vehicleState);

            return Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while processing ENTRY");
            return StatusCode(500);
        }
    }

    [HttpPost("exitcam")]
    public async Task<ActionResult> VehicleExitAsync(VehicleRegistered msg, [FromServices] DaprClient daprClient)
    {
        try
        {
            // get vehicle state
            var state = await _vehicleStateRepository.GetVehicleStateAsync(msg.LicenseNumber);
            if (state == default(VehicleState))
            {
                return NotFound();
            }
            // log exit
            _logger.LogInformation($"EXIT detected in lane {msg.Lane} at {msg.Timestamp.ToString("hh:mm:ss")} " +
                $"of vehicle with license-number {msg.LicenseNumber}.");

            // update state
            var exitState = state.Value with { ExitTimestamp = msg.Timestamp };
            await _vehicleStateRepository.SaveVehicleStateAsync(exitState);

            // handle possible speeding violation
            int violation = _speedingViolationCalculator.DetermineSpeedingViolationInKmh(exitState.EntryTimestamp, exitState.ExitTimestamp.Value);
            if (violation > 0)
            {
                _logger.LogInformation($"Speeding violation detected ({violation} KMh) of vehicle" +
                    $"with license-number {state.Value.LicenseNumber}.");

                var speedingViolation = new SpeedingViolation
                {
                    VehicleId = msg.LicenseNumber,
                    RoadId = _roadId,
                    ViolationInKmh = violation,
                    Timestamp = msg.Timestamp
                };

                // publish speedingviolation (Dapr publish / subscribe)
                await daprClient.PublishEventAsync("pubsub", "speedingviolations", speedingViolation);
            }
            return Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while processing EXIT");
            return StatusCode(500);
        }
    }
}

Dapr 服务调用

在分布式系统中,一项服务通常需要与其他服务进行通信才能完成业务运营。 Dapr 服务调用构建基块可帮助简化服务之间的通信。

它可解决的问题

在分布式应用程序中的服务之间进行调用可能看起来很容易,但其中涉及许多挑战。 例如:

  • 其他服务所在的位置。
  • 在给定服务地址的情况下,如何安全地调用服务。
  • 在发生短暂的暂时性错误时,如何处理重试。

最后,由于分布式应用程序包含许多不同的服务,因此捕获跨服务调用关系图的见解对于诊断生产问题至关重要。

工作原理

假设有两个服务:“服务 A”和“服务 B”。 服务 A 需要调用服务 B 上的 catalog/items API。

传统交通目标检测算法 交通目标控制_传统交通目标检测算法_05

 

图 6-1. Dapr 服务调用的工作原理。

请注意上图中的步骤:

  1. 服务 A 通过调用服务 A 挎斗上的服务调用 API 来调用服务 B 中的 catalog/items 终结点。
  2. 服务 A 挎斗将请求转发到服务 B 挎斗。
  3. 服务 B 挎斗对服务 B API 发出实际 catalog/items 请求。
  4. 服务 B 执行请求,并将响应返回给其挎斗。
  5. 服务 B 挎斗将响应转发回服务 A 挎斗。
  6. 服务 A 挎斗将响应转发回服务 A。

由于调用通过挎斗,Dapr 可以注入一些有用的横切行为:

  • 失败时自动重试调用。
  • 通过相互 (mTLS) 身份验证(包括自动证书滚动更新),确保服务之间的调用安全。
  • 使用访问控制策略控制客户端可以执行的操作。
  • 捕获服务间所有调用的跟踪和指标,以提供见解和诊断。

使用 Dapr .NET SDK

Dapr .NET SDK 为 .NET 开发人员提供了直观的、特定于语言的方法来与 Dapr 交互。 SDK 为开发人员提供了三种方法来进行远程服务调用的调用:

  1. 使用 HttpClient 调用 HTTP 服务
  2. 使用 DaprClient 调用 HTTP 服务
  3. 使用 DaprClient 调用 gRPC 服务

使用 HttpClient 调用 HTTP 服务

 

var httpClient = DaprClient.CreateInvokeHttpClient();
await httpClient.PostAsJsonAsync("http://orderservice/submit", order);

 在此示例中,DaprClient.CreateInvokeHttpClient 返回用于执行 Dapr 服务调用的 HttpClient 实例。 返回的 HttpClient 使用特殊的 Dapr 消息处理程序,该处理程序会重写传出请求的 URI。 主机名称被解释为要调用的服务的应用程序 ID。 实际调用的重写请求为:

 

http://127.0.0.1:3500/v1/invoke/orderservice/method/submit

  

此示例使用 Dapr HTTP 终结点的默认值,即 http://127.0.0.1:<dapr-http-port>/。 dapr-http-port 的值取自 DAPR_HTTP_PORT 环境变量。 如果未设置,则使用默认端口号 3500

或者,你可以在对 DaprClient.CreateInvokeHttpClient 的调用中配置自定义终结点:

 

var httpClient = DaprClient.CreateInvokeHttpClient(daprEndpoint: "localhost:4000");

 还可以通过指定应用程序 ID 来直接设置基址。 这样做可以在调用时启用相对 URI:

 

var httpClient = DaprClient.CreateInvokeHttpClient("orderservice");
await httpClient.PostAsJsonAsync("/submit");

  HttpClient 对象是长期存在的。 在应用程序的生存期内,可以重用单个 HttpClient 实例。 下一个方案演示了 OrderServiceClient 类如何重用 Dapr HttpClient 实例:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IOrderServiceClient, OrderServiceClient>(
    _ => new OrderServiceClient(DaprClient.CreateInvokeHttpClient("orderservice")));

  OrderServiceClient 本身没有特定于 Dapr 的代码。 即使在后台使用 Dapr 服务调用,你也可以像对待任何其他 HttpClient 一样处理 Dapr HttpClient:

public class OrderServiceClient : IOrderServiceClient
{
    private readonly HttpClient _httpClient;

    public OrderServiceClient(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task SubmitOrder(Order order)
    {
        var response = await _httpClient.PostAsJsonAsync("submit", order);
        response.EnsureSuccessStatusCode();
    }
}

  

将 HttpClient 类用于 Dapr 服务调用有很多好处:

  • HttpClient 是一个众所周知的类,许多开发人员已经在其代码中使用了它。 通过将 HttpClient 用于 Dapr 服务调用,开发人员可重复使用其现有技能。
  • HttpClient 支持高级方案,如自定义标头,以及对请求和响应消息的完全控制。
  • 在 .NET 5 中,HttpClient 支持使用 System.Text.Json 的自动序列化和反序列化。
  • HttpClient 集成了许多现有框架和库,如 RefitRestSharp 和 Polly

 

 

使用 DaprClient 调用 HTTP 服务

尽管 HttpClient 是使用 HTTP 语义调用服务的首选方法,但也可以使用 DaprClient.InvokeMethodAsync 方法系列。 下面的示例通过调用 orderservice 应用程序的 submit 方法来提交订单:

var daprClient = new DaprClientBuilder().Build();
try
{
    var confirmation =
        await daprClient.InvokeMethodAsync<Order, OrderConfirmation>(
            "orderservice", "submit", order);
}
catch (InvocationException ex)
{
    // Handle error
}

  

第三个参数是 order 对象,在内部序列化(使用 System.Text.JsonSerializer)并作为请求有效负载发送。 .NET SDK 负责调用挎斗。 它还反序列化对 OrderConfirmation 对象的响应。 由于未指定 HTTP 方法,因此请求作为 HTTP POST 执行。

下一个示例演示如何通过指定 HttpMethod 来发出 HTTP GET 请求:

var catalogItems = await daprClient.InvokeMethodAsync<IEnumerable<CatalogItem>>(HttpMethod.Get, "catalogservice", "items");

  在某些情况下,你可能需要对请求消息进行更多的控制。 例如,当你需要指定请求标头,或你想要将自定义序列化程序用于有效负载时。 DaprClient.CreateInvokeMethodRequest 创建 HttpRequestMessage。 下面的示例演示如何将 HTTP 授权标头添加到请求消息:

var request = daprClient.CreateInvokeMethodRequest("orderservice", "submit", order);
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);

  

HttpRequestMessage 现在具有以下属性集:

  • Url = http://127.0.0.1:3500/v1.0/invoke/orderservice/method/submit
  • HttpMethod = POST
  • Content = 包含 JSON 序列化 order 的 JsonContent 对象
  • Headers.Authorization = "bearer <token>"

按所需方式设置请求后,请使用 DaprClient.InvokeMethodAsync 发送它:

var orderConfirmation = await daprClient.InvokeMethodAsync<OrderConfirmation>(request);

  如果请求成功,DaprClient.InvokeMethodAsync 反序列化对 OrderConfirmation 对象的响应。 或者,可以使用 DaprClient.InvokeMethodWithResponseAsync 获取对基础 HttpResponseMessage 的完全访问权限:

var response = await daprClient.InvokeMethodWithResponseAsync(request);
response.EnsureSuccessStatusCode();

var orderConfirmation = response.Content.ReadFromJsonAsync<OrderConfirmation>();

  

使用 DaprClient 调用 gRPC 服务

 DaprClient 提供了一系列用于调用 gRPC 终结点的 InvokeMethodGrpcAsync 方法。 与 HTTP 方法的主要区别是使用了 Protobuf 序列化程序而不是 JSON。 下面的示例通过 gRPC 调用 orderservice 的 submitOrder 方法。

var daprClient = new DaprClientBuilder().Build();
try
{
    var confirmation = await daprClient.InvokeMethodGrpcAsync<Order, OrderConfirmation>("orderservice", "submitOrder", order);
}
catch (InvocationException ex)
{
    // Handle error
}

  在以上示例中,DaprClient 使用 Protobuf 序列化给定的 order 对象,并使用结果作为 gRPC 请求正文。 同样,响应正文被 Protobuf 反序列化并返回给调用方。 Protobuf 通常可提供比 HTTP 服务调用中使用的 JSON 有效负载更好的性能。

 

示例应用程序:Dapr 流量控制

在流量控制示例应用程序中使用 Dapr 服务调用具有多个优势:

  1. 分离目标服务的位置。
  2. 通过自动重试功能添加复原能力。
  3. 能够重用现有的基于 HttpClient 的代理(由 ASP.NET Core 集成提供)。

在 Dapr 流量控制示例应用中,FineCollection 服务使用 Dapr 服务调用构建基块从 VehicleRegistration 服务检索车辆和所有者信息。 图 6-2 显示了 Dapr 流量控制示例应用程序的概念体系结构。 Dapr 服务调用构建基块在图中标记为数字 1 的流中使用:

传统交通目标检测算法 交通目标控制_传统交通目标检测算法_06

 

 

finecollectionservice服务调用VehicleRegistrationService.VehicleInfoController.GetVehicleInfo

namespace FineCollectionService.Proxies;

public class VehicleRegistrationService
{
    private HttpClient _httpClient;
    public VehicleRegistrationService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    /// <summary>
    /// 获取车辆信息,调用服务: vehicleregistrationservice.VehicleInfoController.
    /// </summary>
    /// <param name="licenseNumber"></param>
    /// <returns></returns>
    public async Task<VehicleInfo> GetVehicleInfo(string licenseNumber)
    {
        return await _httpClient.GetFromJsonAsync<VehicleInfo>(
            $"vehicleinfo/{licenseNumber}");
    }
}

 注册服务:

builder.Services.AddSingleton<VehicleRegistrationService>(_ =>
    new VehicleRegistrationService(DaprClient.CreateInvokeHttpClient(
        "vehicleregistrationservice", $"http://localhost:{daprHttpPort}")));

  

namespace FineCollectionService.Controllers;

[ApiController]
[Route("")]
public class CollectionController : ControllerBase
{
    private static string? _fineCalculatorLicenseKey = null;
    private readonly ILogger<CollectionController> _logger;
    private readonly IFineCalculator _fineCalculator;
    private readonly VehicleRegistrationService _vehicleRegistrationService;

    public CollectionController(ILogger<CollectionController> logger,
        IFineCalculator fineCalculator, VehicleRegistrationService vehicleRegistrationService,
        DaprClient daprClient)
    {
        _logger = logger;
        _fineCalculator = fineCalculator;
        _vehicleRegistrationService = vehicleRegistrationService;

        // set finecalculator component license-key
        if (_fineCalculatorLicenseKey == null)
        {
            bool useKubernetesSecrets = Convert.ToBoolean(Environment.GetEnvironmentVariable("USE_KUBERNETES_SECRETS") ?? "false");
            string secretName = Environment.GetEnvironmentVariable("FINE_CALCULATOR_LICENSE_SECRET_NAME") ?? "finecalculator.licensekey";
            var metadata = new Dictionary<string, string> { { "namespace", "dapr-trafficcontrol" } };
            if (useKubernetesSecrets)
            {
                var k8sSecrets = daprClient.GetSecretAsync(
                    "kubernetes", "trafficcontrol-secrets", metadata).Result;
                _fineCalculatorLicenseKey = k8sSecrets[secretName];
            }
            else
            {
                var secrets = daprClient.GetSecretAsync(
                    "trafficcontrol-secrets", secretName, metadata).Result;
                _fineCalculatorLicenseKey = secrets[secretName];
            }
        }
    }

    [Topic("pubsub", "speedingviolations")]
    [Route("collectfine")]
    [HttpPost()]
    public async Task<ActionResult> CollectFine(SpeedingViolation speedingViolation, [FromServices] DaprClient daprClient)
    {
        decimal fine = _fineCalculator.CalculateFine(_fineCalculatorLicenseKey!, speedingViolation.ViolationInKmh);

        // get owner info (Dapr service invocation)
        //调用微服务
        var vehicleInfo = _vehicleRegistrationService.GetVehicleInfo(speedingViolation.VehicleId).Result;

        // log fine
        string fineString = fine == 0 ? "tbd by the prosecutor" : $"{fine} Euro";
        _logger.LogInformation($"Sent speeding ticket to {vehicleInfo.OwnerName}. " +
            $"Road: {speedingViolation.RoadId}, Licensenumber: {speedingViolation.VehicleId}, " +
            $"Vehicle: {vehicleInfo.Brand} {vehicleInfo.Model}, " +
            $"Violation: {speedingViolation.ViolationInKmh} Km/h, Fine: {fineString}, " +
            $"On: {speedingViolation.Timestamp.ToString("dd-MM-yyyy")} " +
            $"at {speedingViolation.Timestamp.ToString("hh:mm:ss")}.");

        // send fine by email (Dapr output binding)
        var body = EmailUtils.CreateEmailBody(speedingViolation, vehicleInfo, fineString);
        var metadata = new Dictionary<string, string>
        {
            ["emailFrom"] = "noreply@cfca.gov",
            ["emailTo"] = vehicleInfo.OwnerEmail,
            ["subject"] = $"Speeding violation on the {speedingViolation.RoadId}"
        };
        await daprClient.InvokeBindingAsync("sendmail", "create", body, metadata);

        return Ok();
    }
}