gRPC in ASP.NET Core

开发环境

VSCode

window下需要安装Clang,百度找Clang,去官网自行下载,如果下载速度慢,可以添加以下内容到C:\Windows\System32\drivers\etc\host文件

1
2
3
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中安装两个插件

1
2
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

依赖包:

1
2
3
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,例如:

1
using var channel = GrpcChannel.ForAddress("https://localhost:5001");  

然后假设在.proto文件里有EmployeeService这样一个service,在编译项目之后(需要选择client或client and server),会有一个EmployeeService.EmployeeServiceClient的类,实例化这个类就相当实例化一个client,例如:

1
var client = new EmployeeService.EmployeeServiceClient(channel);  

client里就可以调用.proto服务里定义的那些方法

上代码

服务端

创建名字为RoutingDemoASP.NET Core项目,类型为,通过nuget引入:

1
Grpc.AspNetCore  

创建目录

在项目根目录创建以下三个文件夹

1
2
3
Data  
Protos
Services

编写proto

Protos文件夹中添加文件Order.proto,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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
}
};
}
}

注意:这里的OrdergRPC生成的,命名空间为GrpcDemo.Protos

编写Service

Services文件夹创建DemoOrderService.cs文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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引入以下三个包:

1
2
3
Google.Protobuf  
Grpc.Net.Client
Grpc.Tools

复制proto文件

将服务端GrpcServerDemoProtos文件夹拷贝到项目根目录,在解决方案资源管理器找到Order.proto文件,右键 -> 属性 -> Build Action选择Protobuf compiler -> gRPC Stub Classes选择Client only

修改Program.cs

修改Program.cs文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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内容如下:

1
2
3
4
5
6
7
8
9
10
11
{  
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"grpc": "Debug"
}
},
"AllowedHosts": "\*"
}

运行项目就可以看到控制台打印出gRPC相关日志

.NET Core控制台

在客户端的.NET Core控制台程序,需要自定义一个LoggerFactory,然后在创建Channel的时候指定自定义的LoggerFactory。这里的示例使用Serilog来作为日志组件,需要在引入以下三个包:

1
2
3
Serilog  
Serilog.Extensions.Logging
Serilog.Sinks.Console

创建SerilogLoggerFactory.cs,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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的代码修改如下:

1
2
3
4
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文件
  • 使用submoduleproto文件集成到工程目录中
  • 使用dotnet-grpc命令行添加proto文件及祥光依赖包引用

备注:由proto生成的代码文件会存放在obj目录中,不会被嵌入到Git仓库