基于 gRPC 和 .NET Core 的服务器流

服务器
gRPC 是一个现代开源的高性能 RPC 框架,可以在任何环境下运行。它可以有效地连接数据中心内和跨数据中心的服务,并对负载平衡、跟踪、健康检查和认证提供可插拔的支持。

[[415137]]

本文转载自微信公众号「精致码农」,作者liamwang。转载本文请联系精致码农公众号。

早在 2019 年,我写过《用 Mapbox 绘制位置数据》一文,详细介绍了我如何通过简单的文件上传,用 Mapbox 绘制约 230 万个位置点。本文介绍我是如何通过使用 gRPC 和 .NET Core 的服务器流来快速获取所有位置历史数据的。

  1. https://chandankkrr.medium.com/mapping-location-data-with-mapbox-9b256f64d569 

什么是 gRPC

gRPC 是一个现代开源的高性能 RPC 框架,可以在任何环境下运行。它可以有效地连接数据中心内和跨数据中心的服务,并对负载平衡、跟踪、健康检查和认证提供可插拔的支持。gRPC 最初是由谷歌创建的,该公司使用一个名为 Stubby 的单一通用 RPC 基础设施来连接其数据中心内和跨数据中心运行的大量微服务,使用已经超过十年。2015 年 3 月,谷歌决定建立 Stubby 的下一个版本,并将其开源,结果就是现在的 gRPC,被许多企业或组织使用。

  1. https://grpc.io/ 

gRPC 服务器流

服务器流式(Server Streaming)RPC,客户端向服务器发送请求,并获得一个流来读取一连串的消息。客户端从返回的流中读取信息,直到没有消息为止。gRPC 保证在单个 RPC 调用中的信息是有序的。

rpc GetLocationData (GetLocationRequest) returns (stream GetLocationResponse);

协议缓冲区(Protobuf)

gRPC 使用协议缓冲区(protocol buffers)作为接口定义语言(IDL)来定义客户端和服务器之间的契约。在下面的 proto 文件中,定义了一个 RPC 方法 GetLocations,它接收 GetLocationsRequest 消息类型并返回 GetLocationsResponse 消息类型。响应消息类型前面的 stream 关键字表示响应是流类型,而不是单个响应。

  1. syntax = "proto3"
  2.  
  3. option csharp_namespace = "GPRCStreaming"
  4.  
  5. package location_data; 
  6.  
  7. service LocationData { 
  8.   rpc GetLocations (GetLocationsRequest) returns (stream GetLocationsResponse); 
  9.  
  10. message GetLocationsRequest { 
  11.   int32 dataLimit = 1; 
  12.  
  13. message GetLocationsResponse { 
  14.   int32 latitudeE7 = 1; 
  15.   int32 longitudeE7 = 2; 

创建 gRPC 服务

我们可以使用 dotnet new grpc -n threemillion 命令轻松创建一个 .NET gRPC 服务。更多关于在 ASP.NET Core 中创建 gRPC 服务器和客户端的信息可在微软文档中找到。

 

  1. Create a gRPC client and server in ASP.NET Core 
  2. https://docs.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-5.0&tabs=visual-studio-code 

 

在添加了 proto 文件并生成了 gRPC 服务资源文件后,接下来我添加了 LocationService 类。在下面的代码片段中,我有一个 LocationService 类,它继承了从 Location.proto 文件中生成的 LocationDataBase 类型。客户端可以通过 Startup.cs 文件中 Configure 方法中的 endpoints.MapGrpcService() 来访问 LocationService。当服务器收到 GetLocations 请求时,它首先通过 GetLocationData 方法调用读取 Data 文件夹中 LocationHistory.json 文件中的所有数据(未包含在源代码库)。该方法返回 RootLocation 类型,其中包含 List 类型的 Location 属性。Location 类由两个内部属性 Longitude 和 Latitude 组成。接下来,我循环浏览每个位置,然后将它们写入 responseStream 中,返回给客户端。服务器将消息写入流中,直到客户在 GetLocationRequest 对象中指定的 dataLimit。

 

  1. using System.Threading.Tasks; 
  2. using Grpc.Core; 
  3. using Microsoft.Extensions.Logging; 
  4. using System.IO; 
  5. using System; 
  6. using System.Linq; 
  7.  
  8. namespace GPRCStreaming 
  9.     public class LocationService : LocationData.LocationDataBase 
  10.     { 
  11.         private readonly FileReader _fileReader; 
  12.         private readonly ILogger<LocationService> _logger; 
  13.  
  14.         public LocationService(FileReader fileReader, ILogger<LocationService> logger) 
  15.         { 
  16.             _fileReader = fileReader; 
  17.             _logger = logger; 
  18.         } 
  19.  
  20.         public override async Task GetLocations( 
  21.           GetLocationsRequest request, 
  22.           IServerStreamWriter<GetLocationsResponse> responseStream, 
  23.           ServerCallContext context) 
  24.         { 
  25.             try 
  26.             { 
  27.                 _logger.LogInformation("Incoming request for GetLocationData"); 
  28.  
  29.                 var locationData = await GetLocationData(); 
  30.                 var locationDataCount = locationData.Locations.Count
  31.  
  32.                 var dataLimit = request.DataLimit > locationDataCount ? locationDataCount : request.DataLimit; 
  33.  
  34.                 for (var i = 0; i <= dataLimit - 1; i++) 
  35.                 { 
  36.                    var item = locationData.Locations[i]; 
  37.  
  38.                     await responseStream.WriteAsync(new GetLocationsResponse 
  39.                     { 
  40.                         LatitudeE7 = item.LatitudeE7, 
  41.                         LongitudeE7 = item.LongitudeE7 
  42.                     }); 
  43.                 } 
  44.             } 
  45.             catch (Exception exception) 
  46.             { 
  47.                 _logger.LogError(exception, "Error occurred"); 
  48.                 throw; 
  49.             } 
  50.         } 
  51.  
  52.         private async Task<RootLocation> GetLocationData() 
  53.         { 
  54.             var currentDirectory = Directory.GetCurrentDirectory(); 
  55.             var filePath = $"{currentDirectory}/Data/Location_History.json"
  56.  
  57.             var locationData = await _fileReader.ReadAllLinesAsync(filePath); 
  58.  
  59.             return locationData; 
  60.         } 
  61.     } 

 

现在,让我们运行该服务并发送一个请求。我将使用一个叫 grpcurl 的命令行工具,它可以让你与 gRPC 服务器交互。它基本上是针对 gRPC 服务器的 curl。

 

  1. https://github.com/fullstorydev/grpcurl 

通过 grpcurl 与 gRPC 端点(endpoint)交互只有在 gRPC 反射服务被启用时才可用。这允许服务可以被查询,以发现服务器上的 gRPC 服务。扩展方法 MapGrpcReflectionService 需要引入 Microsoft.AspNetCore.Builder 的命名空间:

 

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
  2.     app.UseEndpoints(endpoints => 
  3.     { 
  4.         endpoints.MapGrpcService<LocationService>(); 
  5.  
  6.         if (env.IsDevelopment()) 
  7.         { 
  8.             endpoints.MapGrpcReflectionService(); 
  9.         } 
  10.  
  11.         endpoints.MapGet("/", async context => 
  12.         { 
  13.             await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); 
  14.         }); 
  15.     }); 

 

 

  1. grpcurl -plaintext -d '{"dataLimit": "100000"}' localhost:80 location_data.LocationData/GetLocations 

一旦服务器收到请求,它就会读取文件,然后在位置列表中循环,直到达到 dataLimit 计数,并将位置数据返回给客户端。

接下来,让我们创建一个 Blazor 客户端来调用 gRPC 服务。我们可以使用 IServiceCollection 接口上的 AddGrpcClient 扩展方法设置一个 gRPC 客户端:

 

  1. public void ConfigureServices(IServiceCollection services) 
  2.     services.AddRazorPages(); 
  3.     services.AddServerSideBlazor(); 
  4.     services.AddSingleton<WeatherForecastService>(); 
  5.  
  6.     services.AddGrpcClient<LocationData.LocationDataClient>(client => 
  7.     { 
  8.         client.Address = new Uri("http://localhost:80"); 
  9.     }); 

 

我使用 Virtualize Blazor 组件来渲染这些位置。Virtualize 组件不是一次性渲染列表中的每个项目,只有当前可见的项目才会被渲染。

 

  1. ASP.NET Core Blazor component virtualization 
  2. https://docs.microsoft.com/en-us/aspnet/core/blazor/components/virtualization?view=aspnetcore-5.0 

 

相关代码:

 

  1. @page "/locationdata" 
  2.  
  3. @using Grpc.Core 
  4. @using GPRCStreaming 
  5. @using threemillion.Data 
  6. @using System.Diagnostics 
  7. @using Microsoft.AspNetCore.Components.Web.Virtualization 
  8.  
  9. @inject IJSRuntime JSRuntime; 
  10. @inject System.Net.Http.IHttpClientFactory _clientFactory 
  11. @inject GPRCStreaming.LocationData.LocationDataClient _locationDataClient 
  12.  
  13. <table class="tableAction"
  14.     <tbody> 
  15.         <tr> 
  16.             <td> 
  17.                 <div class="data-input"
  18.                     <label for="dataLimit">No of records to fetch</label> 
  19.                     <input id="dataLimit" type="number" @bind="_dataLimit" /> 
  20.                     <button @onclick="FetchData" class="btn-submit">Call gRPC</button> 
  21.                 </div> 
  22.             </td> 
  23.             <td> 
  24.                 <p class="info"
  25.                     Total records: <span class="count">@_locations.Count</span> 
  26.                 </p> 
  27.                 <p class="info"
  28.                     Time taken: <span class="time">@_stopWatch.ElapsedMilliseconds</span> milliseconds 
  29.                 </p> 
  30.             </td> 
  31.         </tr> 
  32.     </tbody> 
  33. </table
  34.  
  35. <div class="tableFixHead"
  36.     <table class="table"
  37.         <thead> 
  38.             <tr> 
  39.                 <th>Longitude</th> 
  40.                 <th>Latitude</th> 
  41.             </tr> 
  42.         </thead> 
  43.         <tbody> 
  44.             <Virtualize Items="@_locations" Context="locations"
  45.                 <tr> 
  46.                     <td>@locations.LongitudeE7</td> 
  47.                     <td>@locations.LatitudeE7</td> 
  48.                 </tr> 
  49.             </Virtualize> 
  50.         </tbody> 
  51.     </table
  52. </div> 
  53.  
  54. @code { 
  55.     private int _dataLimit = 1000; 
  56.  
  57.     private List<Location> _locations = new List<Location>(); 
  58.  
  59.     private Stopwatch _stopWatch = new Stopwatch(); 
  60.  
  61.     protected override async Task OnInitializedAsync() 
  62.     { 
  63.         await FetchData(); 
  64.     } 
  65.  
  66.     private async Task FetchData() 
  67.     { 
  68.         ResetState(); 
  69.  
  70.         _stopWatch.Start(); 
  71.  
  72.         using (var call = _locationDataClient.GetLocations(new GetLocationsRequest { DataLimit = _dataLimit })) 
  73.         { 
  74.             await foreach (var response in call.ResponseStream.ReadAllAsync()) 
  75.             { 
  76.                 _locations.Add(new Location { LongitudeE7 = response.LongitudeE7, LatitudeE7 = response.LatitudeE7 }); 
  77.  
  78.                 StateHasChanged(); 
  79.             } 
  80.         } 
  81.  
  82.         _stopWatch.Stop(); 
  83.     } 
  84.  
  85.     private void ResetState() 
  86.     { 
  87.         _locations.Clear(); 
  88.  
  89.         _stopWatch.Reset(); 
  90.  
  91.         StateHasChanged(); 
  92.     } 

 

通过在本地运行的流调用,从 gRPC 服务器接收 2,876,679 个单独的响应大约需要 8 秒钟。让我们也在 Mapbox 中加载数据:

 

  1. @page "/mapbox" 
  2.  
  3. @using Grpc.Core 
  4. @using GPRCStreaming 
  5. @using System.Diagnostics 
  6.  
  7. @inject IJSRuntime JSRuntime; 
  8. @inject System.Net.Http.IHttpClientFactory _clientFactory 
  9. @inject GPRCStreaming.LocationData.LocationDataClient LocationDataClient 
  10.  
  11. <table class="tableAction"
  12.     <tbody> 
  13.         <tr> 
  14.             <td> 
  15.                 <div class="data-input"
  16.                     <label for="dataLimit">No of records to fetch</label> 
  17.                     <input id="dataLimit" type="number" @bind="_dataLimit" /> 
  18.                     <button @onclick="LoadMap" class="btn-submit">Load data</button> 
  19.                 </div> 
  20.             </td> 
  21.             <td> 
  22.                 <p class="info"
  23.                     Total records: <span class="count">@_locations.Count</span> 
  24.                 </p> 
  25.                 <p class="info"
  26.                     Time taken: <span class="time">@_stopWatch.ElapsedMilliseconds</span> milliseconds 
  27.                 </p> 
  28.             </td> 
  29.         </tr> 
  30.     </tbody> 
  31. </table
  32.  
  33. <div id='map' style="width: 100%; height: 90vh;"></div> 
  34.  
  35. @code { 
  36.     private int _dataLimit = 100; 
  37.  
  38.     private List<object> _locations = new List<object>(); 
  39.  
  40.     private Stopwatch _stopWatch = new Stopwatch(); 
  41.  
  42.     protected override async Task OnAfterRenderAsync(bool firstRender) 
  43.     { 
  44.         if (!firstRender) 
  45.         { 
  46.             return
  47.         } 
  48.  
  49.         await JSRuntime.InvokeVoidAsync("mapBoxFunctions.initMapBox"); 
  50.     } 
  51.  
  52.     private async Task LoadMap() 
  53.     { 
  54.         ResetState(); 
  55.  
  56.         _stopWatch.Start(); 
  57.  
  58.         using (var call = LocationDataClient.GetLocations(new GetLocationsRequest { DataLimit = _dataLimit })) 
  59.         { 
  60.  
  61.             await foreach (var response in call.ResponseStream.ReadAllAsync()) 
  62.             { 
  63.                 var pow = Math.Pow(10, 7); 
  64.                 var longitude = response.LongitudeE7 / pow; 
  65.                 var latitude = response.LatitudeE7 / pow; 
  66.  
  67.                 _locations.Add(new 
  68.                 { 
  69.                     type = "Feature"
  70.                     geometry = new 
  71.                     { 
  72.                         type = "Point"
  73.                         coordinates = new double[] { longitude, latitude } 
  74.                     } 
  75.                 }); 
  76.  
  77.                 StateHasChanged(); 
  78.             } 
  79.  
  80.             _stopWatch.Stop(); 
  81.  
  82.             await JSRuntime.InvokeVoidAsync("mapBoxFunctions.addClusterData", _locations); 
  83.         } 
  84.     } 
  85.  
  86.     private void ResetState() 
  87.     { 
  88.         JSRuntime.InvokeVoidAsync("mapBoxFunctions.clearClusterData"); 
  89.  
  90.         _locations.Clear(); 
  91.  
  92.         _stopWatch.Reset(); 
  93.  
  94.         StateHasChanged(); 
  95.     } 

源代码在我的 GitHub 上 :

 

 

 

 

  1. https://github.com/Chandankkrr/threemillion 

 

责任编辑:武晓燕 来源: 精致码农
相关推荐

2024-04-07 09:41:18

SignalR实时通信开发

2024-03-14 10:51:13

服务器技术.NET Core

2021-04-26 09:49:46

服务设计消息

2009-07-28 10:43:23

Web服务器ASP.NET

2023-01-11 15:17:01

gRPC.NET 7

2010-05-21 11:50:54

IIS服务器

2015-08-18 10:29:47

2009-04-26 22:27:54

触发器密码修改数据库

2010-10-12 10:35:15

2010-05-13 17:57:44

IIS服务器

2022-02-20 22:10:20

微服务框架gRPC

2009-08-01 10:07:58

ASP.NET服务器控ASP.NET

2009-02-27 11:15:00

多线程服务器MTS专用服务器

2011-12-08 13:04:06

JavaNIO

2019-02-26 16:20:52

FTP服务器

2010-05-17 16:50:38

IIS服务器

2022-03-14 15:10:20

云服务器物理服务器性能

2009-12-21 14:51:44

Fedora Core

2011-02-22 10:01:13

2018-02-23 15:18:22

云服务器独立服务器差异
点赞
收藏

51CTO技术栈公众号