gRpc服务在.NET Core中的应用

什么是gRpc

gRPC是Google开发的高性能开源RPC(远程过程调用)框架,因其构建高效、健壮的 API 而受到开发人员的广泛欢迎。本文将探讨如何在.NET Core中使用gRPC服务。

gRPC可促进分布式系统之间高效、稳健的通信。它基于远程过程调用 (RPC) 协议,其中程序可以使过程(子例程)在另一个地址空间(通常在另一台服务器上)执行,而无需程序员显式编码远程通信的详细信息。gRPC(Google Remote Procedure Calls)即:Google远程过程调用,使用HTTP/2协议传输二进制消息,依靠Protocol buffers(又名Protobuf)来定义端点之间的通信协议。
最常见的应用场景:

  1. 微服务框架下,多种语言服务之间的高效交互。
  2. 将手机服务、浏览器连接至后台
  3. 产生高效的客户端库

为什么需要gRpc

gRPC的创建是为了满足对高性能、与语言无关且与平台无关的框架的需求,以构建高效的API和微服务。它旨在取代传统的REST API并克服其局限性。gRpc有如下的优点:

  1. 高效率:gRPC使用协议缓冲区(protobufs)作为其接口定义语言(IDL)。Protocol Buffers 是一种与语言无关的二进制序列化格式,紧凑且高效。与REST API中使用的JSON或XML相比,这可以实现更小的消息大小和更快的序列化/反序列化过程。
  2. 高性能:gRPC 支持多路复用,可以通过单个TCP连接同时发送多个请求。该特性显着提升了性能,尤其是在高延迟连接的场景下,非常适合微服务架构。
  3. 多语言兼容:gRPC 支持多种编程语言,可以轻松构建多语言系统,其中不同的服务可以用其他语言实现,但仍然可以无缝通信。
  4. 流式传输:gRPC 支持一元(单个请求/响应)和流式RPC。双向流允许客户端和服务器相互发送消息流,从而实现实时通信模式。
  5. 通用性:gRPC 使用Protocol Buffers来定义服务方法和消息类型。此定义可用于生成多种编程语言的客户端和服务器代码,确保类型安全并减少出错的机会。

gRpc关键的概念

  1. 服务定义:gRPC服务是使用 Protocol Buffers 定义的。文件.proto定义服务方法和服务中使用的消息类型。
  2. RPC 方法:gRPC服务公开客户端可以调用的远程方法。这些方法在文件中定义.proto,可以是一元的或流式的。
  3. Protocol Buffers (protobufs): Protocol Buffers用于序列化结构化数据。它们提供与语言无关的接口定义,允许您定义数据模型和服务方法。
  4. 流式传输:gRPC支持流式传输,使客户端和服务器能够发送和接收消息流,使其适合实时应用程序。
  5. 代码生成:协议缓冲区被编译以生成各种编程语言的客户端和服务器代码。生成的代码处理序列化/反序列化和网络通信,使开发人员可以轻松使用gRPC。

项目实战

Nuget包

  1. Grpc.Tools:提供在 .NET 应用程序中使用 gRPC 的工具和实用程序。它包括用于 C# 代码生成的 Protocol Buffers 编译器 ( protoc) 插件。该插件对于从文件(协议缓冲区文件)生成 C# 客户端和服务器代码至关重要.proto,这些文件定义了 gRPC 服务方法和消息类型。
  2. Microsoft.AspNetCore.Grpc.JsonTranscoding:为 gRPC 服务创建 RESTful JSON API。

安装命令:

1
2
dotnet add package grpc.tools
dotnet add package microsoft.aspnetcore.grpc.jsontranscoding

定义.proto文件

在 gRPC 中定义服务涉及创建 Protocol Buffers ( .proto) 文件,我们在其中指定用于通信的服务方法和消息类型。Protocol Buffers 提供了一种与语言无关的方式来定义我们的 API,允许我们用多种编程语言生成客户端和服务器代码

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
syntax = "proto3";

option csharp_namespace = "ToDoGrpc";

package todoit;

service ToDoIt{
//create
rpc CreateToDo(CreateToDoRequest) returns (CreateToDoResponse){}

//read single
rpc ReadToDo(ReadToDoRequest) returns(ReadToDoResponse){}

//read list
rpc ListToDo(GetAllRequest) returns(GetAllResponse){}

//update
rpc UpdateToDo(UpdateToDoRequest) returns(UpdateToDoResponse){}

//delete
rpc DeleteToDo(DeleteToDoRequest) returns(DeleteToDoResponse){}
}

message CreateToDoRequest{
string title=1;
string description=2;
}

message CreateToDoResponse{
int32 id=1;
}

message ReadToDoRequest{
int32 id =1;
}

message ReadToDoResponse{
int32 id =1;
string title=2;
string description = 3;
string to_do_status=4;
}

message GetAllRequest{

}

message GetAllResponse{
repeated ReadToDoResponse to_do =1;
}

message UpdateToDoRequest{
int32 id =1;
string title=2;
string description = 3;
string to_do_status=4;
}

message UpdateToDoResponse{
int32 id =1;
}

message DeleteToDoRequest{
int32 id =1;
}

message DeleteToDoResponse{
int32 id =1;
}

todo.proto文件创建了增上改查RPC服务,每个request和response是消息类型,消息类型定义了可以在gRpc方法中发送和接收的数据结构。

构筑服务

在实现服务之前,必须把todo.proto文件中添加到csproj项目文件并构建项目。
添加服务

构建项目后,将生成一个包含抽象类的文件,该抽象类具有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
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
public class ToDoService : ToDoIt.ToDoItBase
{
private readonly AppDBContext _dbContext;

public ToDoService(AppDBContext dbContext)
{
_dbContext = dbContext;
}

public override async Task<CreateToDoResponse> CreateToDo(CreateToDoRequest request, ServerCallContext serverCallContext)
{
if (string.IsNullOrEmpty(request.Title) || string.IsNullOrEmpty(request.Description))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Data Empty"));

var todo_item = new ToDoItem
{
Title = request.Title,
Description = request.Description
};

await _dbContext.AddAsync(todo_item);
await _dbContext.SaveChangesAsync();

return await Task.FromResult(new CreateToDoResponse
{
Id = todo_item.Id
});
}

public override async Task<ReadToDoResponse> ReadToDo(ReadToDoRequest request, ServerCallContext serverCallContext)
{
if (request.Id <= 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "id error"));

var todo_item = await _dbContext.TodoItems.FirstOrDefaultAsync(a => a.Id == request.Id);
if (todo_item == null)
throw new RpcException(new Status(StatusCode.NotFound, $"No Task with id {request.Id}"));

return await Task.FromResult(new ReadToDoResponse
{
Id = todo_item.Id,
Title = todo_item.Title,
Description = todo_item.Description,
ToDoStatus = todo_item.TodoStatus
});
}

public override async Task<GetAllResponse> ListToDo(GetAllRequest request, ServerCallContext context)
{
var response = new GetAllResponse();
var todo_items = await _dbContext.TodoItems.ToListAsync();

foreach (var todo in todo_items)
{
response.ToDo.Add(new ReadToDoResponse
{
Id = todo.Id,
Title = todo.Title,
Description = todo.Description,
ToDoStatus = todo.TodoStatus
});
}

return await Task.FromResult(response);
}

public override async Task<UpdateToDoResponse> UpdateToDo(UpdateToDoRequest request, ServerCallContext context)
{
if (request.Id <= 0 || string.IsNullOrEmpty(request.Title) || string.IsNullOrEmpty(request.Description))
throw new RpcException(new Status(StatusCode.InvalidArgument, "param error"));

var todo = await _dbContext.TodoItems.FirstOrDefaultAsync(a => a.Id == request.Id);

if (todo == null)
throw new RpcException(new Status(StatusCode.NotFound, $"No Task with id {request.Id}"));

todo.Title = request.Title;
todo.Description = request.Description;
todo.TodoStatus = request.ToDoStatus;

await _dbContext.SaveChangesAsync();

return await Task.FromResult(new UpdateToDoResponse
{
Id = todo.Id
});
}

public override async Task<DeleteToDoResponse> DeleteToDo(DeleteToDoRequest request, ServerCallContext context)
{
if (request.Id <= 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "id error"));

var todo = await _dbContext.TodoItems.FirstOrDefaultAsync(a => a.Id == request.Id);
if (todo == null)
throw new RpcException(new Status(StatusCode.NotFound, $"No Task with id {request.Id}"));

_dbContext.Remove(todo);
await _dbContext.SaveChangesAsync();

return await Task.FromResult(new DeleteToDoResponse
{
Id = todo.Id
});
}
}

注册服务

在应用程序中配置服务

1
app.MapGrpcService<ToDoService>();

测试服务

  1. 启动服务
1
2
dotnet build
dotnet run
  1. 启动Postman,新建gRpc请求
    grpc

  2. 导入todo.proto文件
    proto

  3. 选择测试服务
    service

  4. 测试结果
    result

gRPC JSON转码

gRPC 有一个限制,即不是所有平台都可以使用它。 浏览器并不完全支持 HTTP/2,这使得 REST API 和 JSON 成为将数据引入浏览器应用的主要方式。 尽管 gRPC 带来了很多好处,REST API 和 JSON 在新式应用中仍发挥着重要作用。 构建 gRPC 和 JSON Web API 给应用开发增加了不必要的开销。

gRPC JSON 转码是为 gRPC 服务创建 RESTful JSON API 的 ASP.NET Core 的扩展。 配置转码后,应用可以使用熟悉的HTTP概念调用gRPC服务。

  1. 添加包引用:Microsoft.AspNetCore.Grpc.JsonTranscoding;
  2. 在 Program.cs 文件中,将builder.Services.AddGrpc(); 更改为builder.Services.AddGrpc().AddJsonTranscoding();
  3. 在包含 .csproj 文件的项目目录中创建目录结构 /google/api;
  4. 将 google/api/http.proto 和 google/api/annotations.proto 文件添加到 /google/api 目录中;
  5. 用HTTP绑定和路由在todo.proto 文件中注释 gRPC方法
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
syntax = "proto3";

option csharp_namespace = "ToDoGrpc";

import "google/api/annotations.proto";

package todoit;


service ToDoIt{
//create
rpc CreateToDo(CreateToDoRequest) returns (CreateToDoResponse){
option (google.api.http) = {
post: "/todo/v1",
body:"*"
};
}
//read single
rpc ReadToDo(ReadToDoRequest) returns(ReadToDoResponse){
option (google.api.http) = {
get: "/todo/v1/{id}"
};
}
//read list
rpc ListToDo(GetAllRequest) returns(GetAllResponse){
option (google.api.http) = {
get: "/todo/v1"
};
}
//update
rpc UpdateToDo(UpdateToDoRequest) returns(UpdateToDoResponse){
option (google.api.http) = {
put: "/todo/v1",
body: "*"
};
}
//delete
rpc DeleteToDo(DeleteToDoRequest) returns(DeleteToDoResponse){
option (google.api.http) = {
delete: "/todo/v1/{id}"
};
}
}

message CreateToDoRequest{
string title=1;
string description=2;
}

message CreateToDoResponse{
int32 id=1;
}

message ReadToDoRequest{
int32 id =1;
}

message ReadToDoResponse{
int32 id =1;
string title=2;
string description = 3;
string to_do_status=4;
}

message GetAllRequest{

}

message GetAllResponse{
repeated ReadToDoResponse to_do =1;
}

message UpdateToDoRequest{
int32 id =1;
string title=2;
string description = 3;
string to_do_status=4;
}

message UpdateToDoResponse{
int32 id =1;
}

message DeleteToDoRequest{
int32 id =1;
}

message DeleteToDoResponse{
int32 id =1;
}
  1. 更新appsettings.json中的默认协议:
1
2
3
4
5
6
7
{
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http1AndHttp2"
}
}
}
  1. 测试结果
    result

参考链接

代码实现
ASP.NET Core中的gRPC JSON转码