Dapper 交通控制示例应用程序模拟高速公路交通控制系统
交通控制示例应用程序模拟高速公路交通控制系统。 其用途是检测超速车辆,并向违规司机发送罚款通知。 这些系统实际上存在于现实生活中,下面是它们的工作原理。 一组摄像头(每个车道上方各一个)被放置在高速公路的起点和终点(假设该路段为 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 中的序列图展示了此模拟流:
使用 Dapr 构建基块
Dapr 的目标之一是为微服务应用程序提供云原生功能。 交通控制应用程序使用 Dapr 构建基块来提高可靠性并缓解上文所述的设计缺陷所带来的影响。 图 4 展示的是启用 Dapr 版本的交通控制应用程序:
图 4-3。 具有 Dapr 构建基块的交通控制应用程序。
- 服务调用 Dapr 服务调用构建块处理 FineCollectionService 和 VehicleRegistrationService 之间的请求/响应通信。 因为该调用是检索完成操作所需数据的查询,所以此处可以接受同步调用。 服务调用构建基块提供服务发现。 FineCollection 服务不再需要知道 VehicleRegistration 服务所在的位置。 如果 VehicleRegistration 服务脱机,它还会实现自动重试。
- 发布 & 订阅 发布和订阅构建基块可处理异步消息传送,以便将 TrafficControl 服务中的超速违规信息发送到 FineCollectionService。 此实现能分离 TrafficControl 和 FineCollection 服务。 如果 FineCollectionService 暂时不可用,数据会在队列中累积,并在稍后恢复处理。 RabbitMQ 是当前的消息代理,用于将消息从生成方传输到使用方。 因为 Dapr 发布/订阅构建基块将消息代理抽象化,所以开发人员无需了解 RabbitMQ 客户端库的详细信息。 切换到另一个消息代理时,不需要更改代码,只需完成配置。
- 状态管理 TrafficControl 服务使用状态管理构建基块将车辆状态持久保存服务之外的 Redis 缓存中。 与发布/订阅一样,开发人员无需了解 Redis 特定的 API。 切换到另一个数据存储时,不需要更改代码。
- 输出绑定 FineCollection 服务通过电子邮件将罚款信息发送给超速车辆的车主。 SMTP 的 Dapr 输出绑定使用 SMTP 协议将电子邮件传输抽象化。
- 输入绑定 CameraSimulation 使用 MQTT 协议将包含模拟车辆信息的消息发送至 TrafficControl 服务。 它使用 .NET MQTT 库将消息发送到 Mosquitto,Mosquitto 是轻量型的 MQTT 代理。 TrafficControl 服务使用 MQTT 的 Dapr 输入绑定来订阅 MQTT 代理并接收消息。
- 机密管理 FineCollectionService 需要用于连接到 SMTP 服务器的凭据以及内部使用的罚款计算器组件的许可证密钥。 它使用机密管理构建基块来获取凭据和许可证密钥。
- 执行组件 TrafficControlService 具有基于 Dapr 执行组件的替代实现。 在此实现中,TrafficControl 服务会针对入口摄像头记录的每个车辆创建一个新的执行组件。 车辆的牌照号码构成唯一的执行组件 ID。执行组件封装车辆状态,并将其持久保存在 Redis 缓存中。 当出口摄像头记录到车辆时,会调用该执行组件。 执行组件随后会计算平均车速,并可能得出超速违规结果。
图 4.4 展示的是包含所有 Dapr 构建基块的模拟流序列图:
状态管理
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。
图 6-1. Dapr 服务调用的工作原理。
请注意上图中的步骤:
-
服务 A 通过调用服务 A 挎斗上的服务调用 API 来调用服务 B 中的
catalog/items
终结点。
-
服务 A 挎斗将请求转发到服务 B 挎斗。
-
服务 B 挎斗对服务 B API 发出实际
catalog/items
请求。 -
服务 B 执行请求,并将响应返回给其挎斗。
-
服务 B 挎斗将响应转发回服务 A 挎斗。
-
服务 A 挎斗将响应转发回服务 A。
由于调用通过挎斗,Dapr 可以注入一些有用的横切行为:
- 失败时自动重试调用。
- 通过相互 (mTLS) 身份验证(包括自动证书滚动更新),确保服务之间的调用安全。
- 使用访问控制策略控制客户端可以执行的操作。
- 捕获服务间所有调用的跟踪和指标,以提供见解和诊断。
使用 Dapr .NET SDK
Dapr .NET SDK 为 .NET 开发人员提供了直观的、特定于语言的方法来与 Dapr 交互。 SDK 为开发人员提供了三种方法来进行远程服务调用的调用:
- 使用 HttpClient 调用 HTTP 服务
- 使用 DaprClient 调用 HTTP 服务
- 使用 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 集成了许多现有框架和库,如 Refit、RestSharp 和 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 服务调用具有多个优势:
- 分离目标服务的位置。
- 通过自动重试功能添加复原能力。
- 能够重用现有的基于
HttpClient
的代理(由 ASP.NET Core 集成提供)。
在 Dapr 流量控制示例应用中,FineCollection 服务使用 Dapr 服务调用构建基块从 VehicleRegistration 服务检索车辆和所有者信息。 图 6-2 显示了 Dapr 流量控制示例应用程序的概念体系结构。 Dapr 服务调用构建基块在图中标记为数字 1 的流中使用:
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(); } }