本文系油管上一个系列教程的学习记录第九章,原链接是.NET Microservices – Full Course。本文分成五部分:gRPC介绍、配置集群内gRPC通信服务、平台服务集成gRPC、指令服务集成gRPC和调用平台服务、本地测试和重新部署到集群内。
Google Remoe Procedure Call (gRPC) 介绍
gRPC 的主要优点是:
- 现代高性能轻量级 RPC 框架
- 协定优先 API 开发,默认使用协议缓冲区,允许与语言无关的实现。
- 可用于多种语言的工具,以生成强类型服务器和客户端。
- 支持客户端、服务器和双向流式处理调用。
- 使用 Protobuf 二进制序列化减少对网络的使用。
配置集群内gRPC通信服务
在整个架构图中提到,平台服务和指令服务会有两类、共三种通信方式。其中一类是同步信息通讯,包括HTTP协议通讯以及gRPC通讯。在生产环境中,为了区分二者,则需要平台服务开放额外的端口,用于指令服务通过gRPC调用。
在Kubernetes架构中,预先为platformservice再配置一个ClusterIP的Port以及TargetPort对(这里两个端口都采用了2333)。
apiVersion: v1
kind: Service
metadata:
name: platforms-clusterip-svc
spec:
selector:
app: platformservice
ports:
- name: platformservice
protocol: TCP
port: 80
targetPort: 80
- name: platformgrps
protocol: TCP
port: 2333
targetPort: 2333
重新部署Service
>>kubectl apply -f deployement platforms-depl.yaml
deployment.apps/platforms-depl unchanged
service/platform-clusterip-svc configured
>>kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
commands-clusterip-svc ClusterIP 10.97.40.210 <none> 80/TCP 30d
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 30d
mssql-clusterip-svc ClusterIP 10.107.148.45 <none> 1433/TCP 30d
mssql-loadbalancer LoadBalancer 10.104.109.162 localhost 1433:31137/TCP 30d
platforms-clusterip-svc ClusterIP 10.96.108.221 <none> 80/TCP,2333/TCP 30d
platforms-np-svc NodePort 10.101.93.173 <none> 80:30001/TCP 30d
rabbitmq-clusterip-svc ClusterIP 10.98.53.151 <none> 15672/TCP,5672/TCP 14d
rabbitmq-loadbalancer LoadBalancer 10.102.90.146 localhost 15672:32088/TCP,5672:30621/TCP 14d
看到对于platforms-clusterip-svc,除了原有的80端口、还有2333端口。
平台服务集成gRPC
这一部分将在平台服务中集成gRPC。暴露出基于gRPC的Endpoint,用于调用平台服务。细分内容包括,依赖包安装、配置文件、配置协议、以及实现gRPC服务。
依赖包安装
作为服务端,平台服务需要安装Nuget包:Grpc.AspNetCore
<PackageReference Include="Grpc.AspNetCore" Version="2.50.0" />
配置文件
上文提到生产环境中,需要在平台服务中开设额外的端口,用于指令服务调用。所以在appsettings.Production.json文件中添加如下的配置:
"Kestrel": {
"Endpoints": {
"Grpc": {
"Protocols": "Http2",
"Url": "http://platforms-clusterip-svc:2333"
},
"WebApi": {
"Protocols": "Http1",
"Url": "http://platforms-clusterip-svc:80"
}
}
}
其中Kestrel是AspNet.Core的默认Web服务器,其默认端口配置在Properties/launchSetting.json中(本地),而在容器化的过程中,默认采用了80端口作为HTTP/1协议的API端口。现在还多出了gRPC框架,另外开设了2333端口,协议采用HTTP/2协议。
配置协议
创建协议文件
在平台服务主项目目录下创建Protos文件夹
│
├─Controllers
├─Data
├─PlatformDomain
├─Properties
├─Protos
└─Utils
在其中创建.proto文件(可以右键文件夹选择新建项,搜索“协议缓冲区文件”),创建后进行如下配置:
syntax = "proto3";
option csharp_namespace = "PlatformService.Protos";
service PlatformGrpc {
rpc GetAllPlatforms (GetAllRequest) returns (GetAllResponse);
}
message GetAllRequest {
}
message PlatformGrpcDto {
int32 platformId = 1;
string name = 2;
string publisher = 3;
}
message GetAllResponse {
repeated PlatformGrpcDto platforms = 1;
}
协议说明:
-
采用的是
proto3的语法结构 -
命名空间是
PlatformService.Protos -
整个协议用C#语法等价表示如下:
public class PlatformGrpc { public GetAllResponse GetAllPlatforms(GetAllRequest request) { } public class GetAllResponse { public IEnumerable<PlatformGrpcDto> platforms { get; set; } } public class PlatformGrpcDto { public int PlatformId { get; set; } public string Name { get; set; } public string Publisher { get; set; } } }其中
service相当于class,rpc相当于method,message即数据结构。 -
在
PlatformGrpcDto中属性后复制的数字,代表属性在传输的二进制文件中的顺序。 -
由于属性最终以二进制文件的形式进行值传递,所以属性的类型定义将遵循二进制中的基本类型。对于值类型,这里采用的是
int32。
设定协议类型
在PlatformService的项目文件.csproj中配置.proto文件
<ItemGroup>
<Protobuf Include="Protos\Platforms.proto" GrpcServices="Server" />
</ItemGroup>
编译整个项目工程!!!
实现gRPC服务
配置映射关系
在Util文件夹下的MappingProfile里,
└─Utils
│ MappingProfile.cs
├─CommandService
└─MessageBusService
添加在.proto文件中创建的PlatformGrpcDto和Platform的映射关系:
CreateMap<Platform, PlatformGrpcDto>();
实现PlatformGrpcServie
在PlatformDomain文件夹下,创建PlatformGrpcService。该类继承PlatformGrpc.PlatformGrpcBase
using PlatformService.Protos;
namespace PlatformService.PlatformDomain
{
public class PlatformGrpcService: PlatformGrpc.PlatformGrpcBase
{
}
}
其中PlatformGrpc.PlatformGrpcBase的命名空间就是早先定义的
option csharp_namespace = "PlatformService.Protos";
而这个类是C#编译器根据.proto文件创建的。
接下来为PlatformGrpcService创建构造器、并且注入必要的服务:
private readonly IPlatformRepository _repo;
private readonly IMapper _mapper;
public PlatformGrpcService(IPlatformRepository repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
再重写它的GetAllPlatforms:
public override async Task<GetAllResponse> GetAllPlatforms(GetAllRequest request, ServerCallContext context)
{
var response = new GetAllResponse();
var platforms = await _repo.GetAllPlatformsAsync();
foreach (var plat in platforms)
{
var dto = _mapper.Map<PlatformGrpcDto>(plat);
response.Platforms.Add(dto);
}
return response;
}
默认情况下,未重写的GetAllPlatforms方法为(在PlatformGrpc.PlatformGrpcBase中):
/// <summary>Base class for server-side implementations of PlatformGrpc</summary>
[grpc::BindServiceMethod(typeof(PlatformGrpc), "BindService")]
public abstract partial class PlatformGrpcBase
{
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::System.Threading.Tasks.Task<global::PlatformService.Protos.GetAllResponse> GetAllPlatforms(global::PlatformService.Protos.GetAllRequest request, grpc::ServerCallContext context)
{
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
}
}
注意到
- 尽管在
.proto文件中GetAllResponse里的platforms是以驼峰法命名的;但是经过编译之后,定义的response(var response = new GetAllResponse();)访问Platform却是以Pascal法命名的。
注册并使用PlatformGrpcServie
在Startup.cs文件中注册和使用gRPC
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<PlatformGrpcService>();
});
}
指令服务集成gRPC和调用平台服务
这一部分将在指令服务中集成gRPC,调用平台服务暴露的Endpoint。细分内容包括,依赖包安装、配置文件、配置协议、调用平台服务以及。
依赖包安装
作为客户端,指令服务需要安装Nuget包如下:
- Google.Protobuf
- Grpc.Net.Client
- Grpc.Tools
<PackageReference Include="Google.Protobuf" Version="3.17.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.50.0" />
<PackageReference Include="Grpc.Tools" Version="2.51.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
配置文件
由于本地和生产环境,平台服务gRPC服务的Endpoint不同,因此需要分配配置。在本地(开发)环境下,需要在appsettings.Development.json文件中进行配置:
"PlatformGrpc": "http://localhost:5000"
这个端口号是平台服务在Kestrel服务器中的默认HTTP协议端口号。
在生产环境中,需要在appsettings.Production.json文件中添加如下的配置:
"PlatformGrpc": "http://platforms-clusterip-svc:2333"
IP地址和端口号是早先配置ClusterIP时设置的。
配置协议
复制协议文件
将平台服务中创建的.proto文件复制到如下目录
│
├─Controllers
├─Data
├─Domains
├─Properties
├─Protos
└─Util
在其中创建.proto文件(可以右键文件夹选择新建项,搜索“协议缓冲区文件”),创建后进行如下配置
设定协议类型
在CommandService的项目文件.csproj中配置.proto文件
<ItemGroup>
<Protobuf Include="Protos\Platforms.proto" GrpcServices="Client" />
</ItemGroup>
编译整个项目工程!!!
调用平台服务
配置映射关系
在Util文件夹下的MappingProfile里,
└─Util
│ IEntityRepository.cs
│ MappingProfile.cs
├─MessageBusService
└─SubscriberService
添加在.proto文件中创建的PlatformGrpcDto和Platform的映射关系:
CreateMap<PlatformGrpcDto, Platform>()
.ForMember(dest => dest.ExternalId, src => src.MapFrom(x => x.PlatformId));
接口定义
在Util目录下创建PlatformGrpcService文件夹,继续在其中创建IPlatoformClient以及实现PlatformClient
using CommandService.Domains.Platforms;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CommandService.Util.PlatformGrpcService
{
public interface IPlatformClient
{
Task<IEnumerable<Platform>> GetAllPlatformsAsync();
}
}
接口实现
using CommandService.Domains.Platforms;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CommandService.Util.PlatformGrpcService
{
public class PlatformClient : IPlatformClient
{
public async Task<IEnumerable<Platform>> GetAllPlatformsAsync()
{
throw new System.NotImplementedException();
}
}
}
首先创建构造器、注入必要的服务
private readonly IConfiguration _config;
private readonly IMapper _mapper;
public PlatformClient(IConfiguration config, IMapper mapper)
{
_config = config;
_mapper = mapper;
}
添加包引用:
using AutoMapper;
using Microsoft.Extensions.Configuration;
然后继承的GetAllPlatformsAsync方法中,创建gRPC客户端:
var channel = GrpcChannel.ForAddress(_config["PlatformGrpc"]);
var client = new PlatformGrpc.PlatformGrpcClient(channel);
其中PlatformGrpc.PlatformGrpcClient是C#编译器根据.proto文件创建的。
获取client之后,就可以调用GetAllPlatforms方法获取全部的平台数据:
public async Task<IEnumerable<Platform>> GetAllPlatformsAsync()
{
Console.WriteLine($">>> Calling GRPC Server on the path: {_config["PlatformGrpc"]}");
var channel = GrpcChannel.ForAddress(_config["PlatformGrpc"]);
var client = new PlatformGrpc.PlatformGrpcClient(channel);
var request = new GetAllRequest();
try
{
var response = client.GetAllPlatforms(request);
return response.Platforms.Select(x => _mapper.Map<Platform>(x));
}
catch (Exception ex)
{
Console.WriteLine($">>> Fail to call GRPC Server {ex.Message}");
return null;
}
}
注册并使用IPlatformClient
在Startup.cs文件中注册IPlatformClient
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IPlatformClient, PlatformClient>();
}
业务整合
指令服务获取全部的平台数据,是为了在指令服务初始化时,就提供全部的平台信息。因此,需要App启动(重启)时调用数据灌入的方法、其从平台服务中获取全部平台数据,存到指令服务的内存中。
数据灌入定义与调用
在Data目录下创建PrepInMemoryDatabase.cs
├─Data
│ ApplicationDbContext.cs
│ CommandRepository.cs
│ EntityRepository.cs
│ PlatformRepository.cs
└─ PrepInMemoryDatabase.cs
再在其中创建PrepPopulation方法:
using CommandService.Domains.Platforms;
using Microsoft.AspNetCore.Builder;
using System.Collections.Generic;
namespace CommandService.Data
{
public class PrepInMemoryDatabase
{
public static void PrepPopulation(IApplicationBuilder app)
{
}
}
}
在Startup.cs中调用该方法:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
PrepInMemoryDatabase.PrepPopulation(app);
}
数据灌入实现
public static void PrepPopulation(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.CreateScope())
{
var grpcClient = serviceScope.ServiceProvider.GetService<IPlatformClient>();
var platforms = grpcClient.GetAllPlatformsAsync().Result;
SeedData(serviceScope.ServiceProvider.GetService<IPlatformRepository>(), platforms);
}
}
private static void SeedData(IPlatformRepository repo, IEnumerable<Platform> platforms)
{
Console.WriteLine(">>>Seeding new platforms...");
if(platforms is not null)
{
foreach (var platform in platforms)
{
if (!repo.JudgePlatformExistencyAsync(platform.ExternalId).Result)
repo.CreatePlatform(platform);
}
Task.FromResult(repo.SaveChangesAsync());
}
}
由于IPlatformClient的生命周期定义为Scope,所以需要从Scope类的构造注入容器中取服务:
var serviceScope = app.ApplicationServices.CreateScope();
var grpcClient = serviceScope.ServiceProvider.GetService<IPlatformClient>();
取出服务后,即可调用远程调用平台服务获取全部平台数据:
var platforms = grpcClient.GetAllPlatformsAsync().Result;
然后将数据映射转化、判断去重后,存入指令服务的内存中。
本地测试
分别启动平台服务与指令服务,注意其中平台服务需要以Kestrel服务器形式(指令服务调用的是Kestrel服务器的默认端口,如果用的是IIS服务器端口、则应该以IIS服务器形式)启动。
当指令服务启动后,可以看到控制台的消息
>>> Listening on the MessageBus...
>>> Calling GRPC Server on the path: http://localhost:5000
>>>Seeding new platforms...
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\03Codes\HaitaoCodes\self-practise\dotnet-microservice-practice\Service\CommandService\CommandService
通过Postman调用获取指令服务的全部平台数据:
以上正是平台服务本地其中时,存放在内存中的平台数据。
重新部署到集群内
参考之前的内容
看到平台服务的容器内控制台消息:
>>> Using SQL Server
>>> Attempting to apply migration...
>>> Seed data exist...
>>> CommandService Endpoint http://commands-clusterip-svc:80
warn: Microsoft.AspNetCore.Server.Kestrel[0]
Overriding address(es) 'http://+:80'. Binding to endpoints defined in UseKestrel() instead.
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:2333
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
Failed to determine the https port for redirect.
>>> Connect to MessageBus
看到指令服务的容器内控制台消息:
>>> Listening on the MessageBus...
>>> Calling GRPC Server on the path: http://platforms-clusterip-svc:2333
>>>Seeding new platforms...
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
Failed to determine the https port for redirect.
通过Postman调用获取指令服务的全部平台数据: