什么是gRPC
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
开始实战
现在有这样一个场景,在我们的数据库设计中,有User表和Goods表,还有一个订单表,订单表里的存的是User和Goods的Id,如下图。
当然真实的业务场景是不会这样设计的,这里只是举例。用户想获取订单信息,就会去掉订单列表接口,但是订单里需要展示商品信息,如下图。
那这就相当于,我的订单服务,需要去查询商品服务,如果需要个人信息的,还需要查询用户服务。这时候就我们的gRPC
就改派上用场了。
首先我们需要在订单服务EasyShop.OrderService
里需要引入gRPC的客户端。
<PackageReference Include="Google.Protobuf" Version="3.15.6" />
<PackageReference Include="Grpc.AspNetCore" Version="2.36.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.36.0" />
<PackageReference Include="Grpc.Tools" Version="2.36.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
以及设置proto
文件为客户端。
<ItemGroup>
<Protobuf Include="Protos\pgoods.proto" GrpcServices="Client" />
<Protobuf Include="Protos\puser.proto" GrpcServices="Client" />
</ItemGroup>
接着在订单服务里新增一个名为Protos
的文件夹,用来存放proto
文件,接着新增pgoods.proto
文件和puser.proto
文件.
pgoods.proto
// 语法结构,使用pb3
syntax = "proto3";
// 定义命名空间,一般是项目名或者解决方案名
option csharp_namespace = "EasyShop.OrderService";
// 定义服务的包
package easyshop;
// 定义具体的服务
service gGoods {
// 定义某一个方法API,格式是:rpc 方法名(请求参数对象名) returns(返回参数对象名)
rpc Get (GoodsRequest) returns (GoodsReply);
}
// 定义请求的对象名
message GoodsRequest {
string Id = 1;
}
// 定义返回的对象名
message GoodsReply {
repeated Goods Goods = 1;
}
message Goods{
string Id=1;
string Name=2;
double Price=3;
string Description =4;
string Poster=5;
}
puser.proto
// 语法结构,使用pb3
syntax = "proto3";
// 定义命名空间,一般是项目名或者解决方案名
option csharp_namespace = "EasyShop.OrderService";
// 定义服务的包
package easyshop;
// 定义具体的服务
service gUser {
// 定义某一个方法API,格式是:rpc 方法名(请求参数对象名) returns(返回参数对象名)
rpc Get (UserRequest) returns (UserReply);
}
// 定义请求的对象名
message UserRequest {
// 有一个属性字段是name
string Id = 1;
}
// 定义返回的对象名
message UserReply {
// 有一个返回的字段是message
string Id = 1;
string UserName = 2;
}
接着右键选择项目,选择重新生成,正常情况下,会在obj目录下生成一个文件夹,包含这几个文件。
这是Google.Protobuf
这个dll根据项目下的proto
文件,自动生成的类文件。
客户端配置好了 ,接下来要配置服务端了,我们以商品服务举例,首先需要在打开商品服务项目EasyShop.GoodsService
,添加nuget包。
<PackageReference Include="Google.Protobuf" Version="3.15.6" />
<PackageReference Include="Grpc.AspNetCore" Version="2.36.0" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.36.0" />
<PackageReference Include="Grpc.Tools" Version="2.36.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
同样需要添加Protos
文件夹和pgoods.proto
文件,但是GrpcServices
需要设置为Server
<Protobuf Include="Protos\pgoods.proto" GrpcServices="Server" />
这里注意下,商品服务下的pgoods.proto
文件和订单服务下的pgoods.proto
的文件,除了命名空间的参数不一致以外,其他部分都是相同的。
option csharp_namespace = "EasyShop.GoodsService";
接着右键重新生成项目,若在obj目录下成功生成了两个类文件,说明gRPC正常。
接下来需要写具体的实现代码了,在订单服务里,会获取多个商品的ID,然后会调用商品服务的方法,通过Id获取对应的商品信息并返回。
在商品服务下,新增一个Services
文件夹,接着新增一个GGoodsService.cs
文件,代码如下。
using EasyShop.GoodsService.Domain;
using Grpc.Core;
using Infrastructure;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace EasyShop.GoodsService.Services
{
public class GGoodsService:gGoods.gGoodsBase
{
// 和ASP.NETCore一样,可以使用依赖注入和服务
private readonly ILogger<GGoodsService> _logger;
private readonly MySqlDBContext _mySqlDBContext;
public GGoodsService(ILogger<GGoodsService> logger,MySqlDBContext mySqlDBContext)
{
_logger = logger;
_mySqlDBContext = mySqlDBContext;
}
/// <summary>
/// 重写 设计对应的多个接口
/// 一般都是异步处理
/// </summary>
/// <param name="request"></param>
/// <param name="context"></param>
/// <returns></returns>
public override Task<GoodsReply> Get(GoodsRequest request, ServerCallContext context)
{
var goodsIds = request.Id.Split(",");
var goods = _mySqlDBContext.Goods.Where(c => goodsIds.Contains(c.Id)).ToList();
var goodsReplyList =new GoodsReply();
foreach (var item in goods)
{
var goodsReply = item.MapTo<Goods>();
goodsReplyList.Goods.Add(goodsReply);
}
return Task.FromResult(goodsReplyList);
}
}
}
这里继承的 gGoods.gGoodsBase
就是通过proto
文件自动生成的类文件中的方法。
商品服务的实现写完了,接下来需要在订单服务中去调用这个方法了。
//gRPC:调用商品服务,查询商品详情
var orderServiceUrl = ServiceUrL.GetServiceUrlByName("GoodsService");
var goodsIds = string.Join(",", orders.Select(c => c.GoodsId).ToList());
using var channel_goods = GrpcChannel.ForAddress(orderServiceUrl);
var client_goods = new gGoods.gGoodsClient(channel_goods);
var reply_goods = client_goods.Get(new GoodsRequest { Id = goodsIds });
这里简单说明一下orderServiceUrl
参数,正常GrpcChannel.ForAddress
参数应该是一个地址,如下面这样。
using var channel_goods = GrpcChannel.ForAddress("https://localhost:5070");
但是由于我们的微服务是可以做集群的,总不能一直逮着一个地址调用吧,万一商品服务挂了一个节点,那订单服务也挂了,这明显是不合适的,还记得我们的服务都是做了服务发现的吗,对了就是Consul,我写了一个公共的类,凡是需要地址的,全都通过服务名称就Consul中查询,代码如下。
using Consul;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Infrastructure
{
public class ServiceUrL
{
public static int number = 1;
//通过调用Consul查询对应的服务地址,方便gRPC调用
public static string GetServiceUrlByName(string name)
{
var consulClient = new ConsulClient(x => x.Address = new Uri($"http://localhost:8500"));
var agent = consulClient.Agent.Services().Result.Response.Values.ToList();
var services = agent.Where(c => c.Service == name).ToList();
if (services != null)
{
var index = number++ % services.Count;
var result = services[index];
return string.Format("https://{0}:{1}", result.Address, result.Port);
}
else
{
throw new Exception("没有找到地址");
}
}
}
}
上面是我们通过服务名称,获取Consul
中的服务地址,然后用轮询的方法返回一个地址,供gRPC调用。
到目前位置,gRPC的使用就算完成了。
项目地址:https://gitee.com/limeng66/easy-shop