开发环境
VSCode
window下需要安装Clang
,百度找Clang
,去官网自行下载,如果下载速度慢,可以添加以下内容到C:\Windows\System32\drivers\etc\host
文件
54.231.82.146 vagrantcloud-files-production.s3.amazonaws.com
219.76.4.4 s3.amazonaws.com
219.76.4.4 github-cloud.s3.amazonaws.com
然后再VSCode
中安装两个插件
vscode-proto3
Clang-Format
消息类型的演进
- 向前兼容变更:使用新的
.proto
文件来写数据 – 从旧的.proto
文件读取数据 - 向后兼容变更:使用旧的
.proto
文件来写数据 – 从新的.proto
文件读取数据
更新消息类型的规则
- 不要修改任何现有字段的数字(
tag
) - 可以添加新的字段,旧的代码会忽略掉新字段的解析,所以要注意新字段的默认值
- 字段可以被删除,只要它们的数字
(tag)
在更新后的消息类型中不再使用即可,也可以把字段名使用OBSOLETE_
前缀而不是删除字段,或者把这些字段的数字(tag)
进行保留(reserved)
,以免未来其他开发者不小心使用这些字段 - 尽量不要修改原有的字符数据类型
默认值
默认值在更新Protocol Buffer
消息定义的时候有很重要的作用,它可以防止对现有代码/新代码造成破坏性影响。它们也可以保证字段永远不会有null
值
但是,默认值还是非常危险的:你无法区分这个默认值到底是来自一个丢失的字段还是字段的实际值正好等于默认值
所以,需要保证这个默认值对于业务来说是一个毫无意义的值,例如int32 pop
人口这个字段的默认值可以设置为-1
,再就是可能需要再代码里对默认值进行判断处理
枚举
enum
同样可以进化,就和消息的字段一样,可以添加、删除值,也可以保留值
但是如果代码不知道它接收到的值对应哪个enum
值,那么enum
的默认值将会被采用
在.NET Core
中使用gRPC
ASP.NET Core
依赖包:
Grpc.AspNetCore
.NET Core
依赖包:
Google.Protobuf
Grpc.Net.Client
Grpc.Tools
引包之后的操作
按照项目类型引入上面的包之后,直接编译是不会得到gRPC
框架生成的代码,需要做以下操作:
右键.proto
文件 -> 属性 -> 将Build Action
选择为Protobuf compiler
-> gRPC Stub Classes
按照需求选择Client and Server/Client only/Server only/Do not generate
进行完上面的操作之后,编译项目会在obj\Debug\netcoreapp3.1
目录里自动生成RPC
代码
作为服务端
怎么实现rpc
定义的方法:假设在.proto
文件里有EmployeeService
这样一个service
,在编译项目之后,会有一个EmployeeService.EmployeeServiceBase
的类,自己编写一个类继承自EmployeeService.EmployeeServiceBase
这个类,然后override
去重载.proto
服务里定义的那些rpc
方法即可
作为客户端
怎么调用rpc
定义的方法:需要先创建Channel
,例如:
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
然后假设在.proto
文件里有EmployeeService
这样一个service
,在编译项目之后(需要选择client
或client and server
),会有一个EmployeeService.EmployeeServiceClient
的类,实例化这个类就相当实例化一个client
,例如:
var client = new EmployeeService.EmployeeServiceClient(channel);
在client
里就可以调用.proto
服务里定义的那些方法
上代码
服务端
创建名字为RoutingDemo
的ASP.NET Core
项目,类型为空
,通过nuget
引入:
Grpc.AspNetCore
创建目录
在项目根目录创建以下三个文件夹
Data
Protos
Services
编写proto
在Protos
文件夹中添加文件Order.proto
,具体内容如下:
syntax = "proto3";
option csharp_namespace = "GrpcDemo.Protos";
message Order{
int32 Id = 1;
string OrderNo = 2;
int32 Status = 3;
float Payment = 4;
repeated OrderProduct Products = 5;
OrderAddress Address = 6;
int32 OrderOwner = 7;
message OrderProduct{
string ProductTitle = 1;
string SkuTitle = 2;
int32 Num = 3;
float UnitPrice = 4;
}
message OrderAddress{
string Province = 1;
string City = 2;
string Districe = 3;
string Detail = 4;
string Name = 5;
string Mobile = 6;
}
}
message GetByOrderNoRequest{
string OrderNo = 1;
}
message GetByOwnerRequest{
int32 OrderOwner = 1;
}
message BatchAddOrderNoReturnResponse{
bool IsAllSuccess = 1;
repeated string FailOrderNo = 2;
}
service OrderService{
rpc GetByOrderNo(GetByOrderNoRequest) returns(Order);
rpc GetByOwner(GetByOwnerRequest) returns(stream Order);
rpc AddOrder(Order) returns(Order);
rpc BatchAddOrder(stream Order) returns(stream Order);
rpc BatchAddOrderNoReturn(stream Order) returns(BatchAddOrderNoReturnResponse);
}
在解决方案资源管理器
找到Order.proto
文件,右键 -> 属性 -> Build Action
选择Protobuf compiler
-> gRPC Stub Classes
选择Server only
编译一次项目
编写测试数据
在Data
文件夹创建InMemoryData.cs
文件,内容如下:
using System.Collections.Generic;
using GrpcDemo.Protos;
namespace GrpcServerDemo.Data
{
public class InMemoryData
{
public static List<Order> Orders = new List<Order>()
{
new Order()
{
Id = 1,
OrderNo = "2020042201",
Status = 1,
Payment = 43141.98f,
Products =
{
new Order.Types.OrderProduct()
{
ProductTitle = "Apple iPhone11",
SkuTitle = "256GB 黑色",
Num = 2,
UnitPrice = 9999.99f
},
new Order.Types.OrderProduct()
{
ProductTitle = "Apple MacBook Pro",
SkuTitle = "i7 512GB 灰色",
Num = 1,
UnitPrice = 23142
}
},
Address = new Order.Types.OrderAddress()
{
Province = "广东省",
City = "深圳市",
Districe = "南山区",
Detail = "Nanshan Road 1234",
Name = "Jiamiao.x",
Mobile = "13500000000"
},
OrderOwner = 100,
},
new Order()
{
Id = 2,
OrderNo = "2020042202",
Status = 2,
Payment = 56.00f,
Products =
{
new Order.Types.OrderProduct()
{
ProductTitle = "ASP.NET Core微服务实战",
SkuTitle = "1本",
Num = 1,
UnitPrice = 56.00f
}
},
Address = new Order.Types.OrderAddress()
{
Province = "广东省",
City = "深圳市",
Districe = "南山区",
Detail = "Nanshan Road 1234",
Name = "Jiamiao.x",
Mobile = "13500000000"
},
OrderOwner = 100
}
};
}
}
注意:这里的Order
是gRPC
生成的,命名空间为GrpcDemo.Protos
编写Service
在Services
文件夹创建DemoOrderService.cs
文件,内容如下:
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
using GrpcDemo.Protos;
using GrpcServerDemo.Data;
using Microsoft.Extensions.Logging;
namespace GrpcServerDemo.Services
{
public class DemoOrderService : OrderService.OrderServiceBase
{
private readonly ILogger<DemoOrderService> _logger;
public DemoOrderService(ILogger<DemoOrderService> logger)
{
_logger = logger;
}
public override async Task<Order> GetByOrderNo(GetByOrderNoRequest request, ServerCallContext context)
{
_logger.LogInformation("有人请求接口 -> GetByOrderNo");
var metaData = context.RequestHeaders;
foreach (var item in metaData)
{
_logger.LogInformation($"{item.Key}: {item.Value}");
}
await Task.CompletedTask;
var dbValue = InMemoryData.Orders.FirstOrDefault(x => x.OrderNo == request.OrderNo);
if (dbValue != null)
{
return dbValue;
}
else
{
throw new Exception("订单号错误");
}
}
public override async Task GetByOwner(GetByOwnerRequest request, IServerStreamWriter<Order> responseStream, ServerCallContext context)
{
_logger.LogInformation("有人请求接口 -> GetByOwner");
var dbValue = InMemoryData.Orders.Where(x => x.OrderOwner == request.OrderOwner);
foreach (var item in dbValue)
{
Thread.Sleep(2000);
_logger.LogInformation($"发送数据:{item}");
await responseStream.WriteAsync(item);
}
}
public override async Task<Order> AddOrder(Order request, ServerCallContext context)
{
_logger.LogInformation("有人请求接口 -> AddOrder");
await Task.CompletedTask;
request.Id = InMemoryData.Orders.Max(x => x.Id) + 1;
InMemoryData.Orders.Add(request);
return request;
}
public override async Task BatchAddOrder(IAsyncStreamReader<Order> requestStream, IServerStreamWriter<Order> responseStream, ServerCallContext context)
{
_logger.LogInformation("有人请求接口 -> BatchAddOrder");
while (await requestStream.MoveNext())
{
var inputOrder = requestStream.Current;
lock (this)
{
_logger.LogInformation($"接受数据:{inputOrder}");
inputOrder.Id = InMemoryData.Orders.Max(x => x.Id) + 1;
InMemoryData.Orders.Add(inputOrder);
}
await responseStream.WriteAsync(inputOrder);
Thread.Sleep(5000);
}
}
}
}
注意:这里的OrderService.OrderServiceBase
一样是gRPC
生成的,命名空间为GrpcDemo.Protos
修改Startup
修改Startup.cs
,内容如下:
using GrpcServerDemo.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace GrpcServerDemo
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<DemoOrderService>();
});
}
}
}
运行项目
在Powershell
中进入到项目根目录,直接dotnet run
运行目录即可
客户端
创建项目
创建名字为GrpcClientDemo
的控制台应用
,通过nuget
引入以下三个包:
Google.Protobuf
Grpc.Net.Client
Grpc.Tools
复制proto
文件
将服务端GrpcServerDemo
的Protos
文件夹拷贝到项目根目录,在解决方案资源管理器
找到Order.proto
文件,右键 -> 属性 -> Build Action
选择Protobuf compiler
-> gRPC Stub Classes
选择Client only
修改Program.cs
修改Program.cs
文件,内容如下:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using GrpcDemo.Protos;
namespace GrpcClientDemo
{
class Program
{
static async Task Main(string[] args)
{
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new OrderService.OrderServiceClient(channel);
var option = int.Parse(args[0]);
switch (option)
{
case 0:
await GetByOrderNoAsync(client);
break;
case 1:
await GetByOwner(client);
break;
case 2:
await AddOrder(client);
break;
case 3:
await BatchAddOrder(client);
break;
}
Console.WriteLine("==========END==========");
}
public static async Task GetByOrderNoAsync(OrderService.OrderServiceClient client)
{
var metaData = new Metadata()
{
{"userName", "jiamiao.x"},
{"clientName", "GrpcClientDemo"}
};
var response = await client.GetByOrderNoAsync(new GetByOrderNoRequest() {OrderNo = "2020042201"},metaData);
Console.WriteLine($"接收到数据:{response}");
}
public static async Task GetByOwner(OrderService.OrderServiceClient client)
{
var response = client.GetByOwner(new GetByOwnerRequest() {OrderOwner = 100});
var responseStream = response.ResponseStream;
while (await responseStream.MoveNext())
{
Console.WriteLine($"接收到数据:{responseStream.Current}");
}
Console.WriteLine($"数据接收完毕");
}
public static async Task AddOrder(OrderService.OrderServiceClient client)
{
var order = new Order()
{
OrderNo = "2020042301",
Status = 1,
Payment = 43141.98f,
Products =
{
new Order.Types.OrderProduct()
{
ProductTitle = "OnePlus 7T",
SkuTitle = "256GB 蓝色",
Num = 1,
UnitPrice = 3600f
}
},
Address = new Order.Types.OrderAddress()
{
Province = "广东省",
City = "深圳市",
Districe = "南山区",
Detail = "北科大厦7003",
Name = "Jiamiao.x",
Mobile = "13822113366"
},
OrderOwner = 100,
};
var response = await client.AddOrderAsync(order);
Console.WriteLine($"接收到数据:{response}");
}
public static async Task BatchAddOrder(OrderService.OrderServiceClient client)
{
var orders = new List<Order>()
{
new Order()
{
OrderNo = "2020042301",
Status = 1,
Payment = 3600f,
Products =
{
new Order.Types.OrderProduct()
{
ProductTitle = "OnePlus 7T",
SkuTitle = "256GB 蓝色",
Num = 1,
UnitPrice = 3600f
}
},
Address = new Order.Types.OrderAddress()
{
Province = "广东省",
City = "深圳市",
Districe = "南山区",
Detail = "北科大厦7003",
Name = "Jiamiao.x",
Mobile = "13822113366"
},
OrderOwner = 100,
},
new Order()
{
OrderNo = "2020042302",
Status = 1,
Payment = 13999.99f,
Products =
{
new Order.Types.OrderProduct()
{
ProductTitle = "SONY PS4 Pro",
SkuTitle = "1TB 黑色",
Num = 1,
UnitPrice = 3999.99f
},
new Order.Types.OrderProduct()
{
ProductTitle = "Surface Desktop Pro",
SkuTitle = "1TB 白色",
Num = 1,
UnitPrice = 13999.99f
}
},
Address = new Order.Types.OrderAddress()
{
Province = "广东省",
City = "深圳市",
Districe = "南山区",
Detail = "北科大厦7003",
Name = "Jiamiao.x",
Mobile = "13822113366"
},
OrderOwner = 100,
}
};
var call = client.BatchAddOrder();
foreach (var order in orders)
{
await call.RequestStream.WriteAsync(order);
}
await call.RequestStream.CompleteAsync();
Console.WriteLine("----数据发送完毕----");
await Task.Run(async () =>
{
while (await call.ResponseStream.MoveNext())
{
Console.WriteLine($"接收到消息:{call.ResponseStream.Current}");
}
});
}
}
}
运行项目
在Powershell
进入到项目根目录,使用dotnet run [arg]
运行项目既可以看到效果,[arg]
是对应switch
里的参数
日志和异常
日志
ASP.NET Core
作为服务端在ASP.NET Core
中开启gRPC
日志只需要在appsettings.json
中配置grpc
的日志等级即可,修改appsettings.json
内容如下:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"grpc": "Debug"
}
},
"AllowedHosts": "*"
}
运行项目就可以看到控制台打印出gRPC
相关日志
.NET Core控制台
在客户端的.NET Core控制台程序
,需要自定义一个LoggerFactory
,然后在创建Channel
的时候指定自定义的LoggerFactory
。这里的示例使用Serilog
来作为日志组件,需要在引入以下三个包:
Serilog
Serilog.Extensions.Logging
Serilog.Sinks.Console
创建SerilogLoggerFactory.cs
,内容如下:
using Microsoft.Extensions.Logging;
using Serilog.Debugging;
using Serilog.Extensions.Logging;
namespace Jiamiao.x.GrpcClient
{
public class SerilogLoggerFactory:ILoggerFactory
{
private readonly SerilogLoggerProvider _provider;
public SerilogLoggerFactory(Serilog.ILogger logger=null,bool dispose = false)
{
_provider = new SerilogLoggerProvider(logger, dispose);
}
public void Dispose() => _provider.Dispose();
public ILogger CreateLogger(string categoryName)
{
return _provider.CreateLogger(categoryName);
}
public void AddProvider(ILoggerProvider provider)
{
SelfLog.WriteLine("Ignore added logger provider {0}", provider);
}
}
}
回到gRPC
服务调用的地方,将创建GrpcChannel
的代码修改如下:
using var channel = GrpcChannel.ForAddress("https://localhost:5001",new GrpcChannelOptions()
{
LoggerFactory = new SerilogLoggerFactory()
});
运行项目即可以看到gRPC
日志内容
异常
服务端在gRPC
抛出异常的时候,可以抛出RpcException
来指定异常类型,RpcException
示例里的trailer
是一个Metadata
,可以携带自定义的键值对,客户端捕获异常也可以捕获指定的RpcException
,一样可以拿到trailer
来获取自定义的键值对信息
关于JWT授权
在通过授权接口获取到JWT Token
之后,与普通HTTP
请求类似,JWT Token
也是放在头部与请求一起发送出去,只不过在RPC
换了个名词,编程MetaData
,其实是一样道理,用Authorization:Bearer {JWT Token}
来进行发送即可
多项目之间共享proto
文件
- 使用单独的
Git
仓库管理proto
文件 - 使用
submodule
将proto
文件集成到工程目录中 - 使用
dotnet-grpc
命令行添加proto
文件及祥光依赖包引用
备注:由proto
生成的代码文件会存放在obj
目录中,不会被嵌入到Git
仓库