.NET Core GRPC使用初探

最近需要使用.NET Core搞一个需要和服务器通信的项目。通信协议是protobuf定义的。

我想既然客户端和服务器都是.NET Core,协议都protobuf写好了,那干脆上个gRPC呗。

于是研究了一下。发现.NET Core的gRPC还挺好用的。

以下为我今天研究的内容:


协议库

建立一个 .NET Standard Library 项目,添加如下依赖项:

( .NET Standard Library 可以同时兼容 .NET Core 和 .NET Framework )

NuGet:

  • Google.Protobuf
  • Grpc
  • Grpc.Tools

为该项目添加一个源文件,后缀名为 .proto 。在该文件中描述所需的消息协议以及RPC的接口定义。

.proto 文件中的 package 字段定义了生成库的命名空间。

选中这个 .proto 文件,在 Visual Studio 的 属性 窗格中,为该文件选择编译属性:

Advanced 选项中, Build Action 为: Protobuf compiler

gRPC 选项中, gRPC Stub Classes 为: Client and Server

Protobuf 选项中, Class Access 为: Public ; Compile Protobuf 为: True

直接生成该项目即可得到编译的带有rpc和protobuf协议库的dll文件。


gRPC Server

服务器端需要实现gRPC协议中定义的接口。

新建一个 .NET Core 项目作为gRPC Server。为该项目添加引用上面的协议库项目,或者通过dll、私有NuGet引用该库。

1. 实现RPC接口。

对于 .proto 文件中定义的一个 service 协议块,应在该服务对应的gRPC Server上给出一个实现。

若 命名空间为GrpcTest, service名称为DoSomething。应实现一个class继承自 GrpcTest.DoSomething.DoSomethingBase

请使用IntelliSence快速生成接口中应重载的函数框架。具体的说,为每一个rpc调用都实现一个
public override async Task<返回值消息类型> RPC调用名(参数类型 request, ServerCallContext context) 的函数。

然后在本项目中实现这些函数的具体逻辑。

2. 开启 gRPC 监听服务器。

简单的代码如下:

1
2
3
4
5
6
7
8
Grpc.Core.Server server = new Grpc.Core.Server{
Service = {
GrpcTest.DoSomething.BindServices(new DoSomethingImpl())
},
Ports = {
new ServerPort("0.0.0.0", 30000, ServerCredentials.Insecure)
}
};

然后只需

1
server.Start();

即可启动 gRPC 服务器。

但是注意server.Start()会立即返回,请使用信号量或者其他IO中断让主线程挂起,否则主线程会迅速退出。


gRPC Client

客户端一侧在指定服务器地址和端口号后,像调用普通的函数一样调用rpc接口即可。

新建一个 .NET Core 项目作为gRPC Client。同样通过添加项目引用、添加dll或私有NuGET的方式添加上面协议库的引用。

1. 获得gRPC调用对象

在实例化gRPC服务对象的时候,需要为gRPC服务传入channel对象,该对象定义了gRPC Server的地址和端口号。

示例代码如下:

1
2
Grpc.Core.Channel channel = new Grpc.Core.Channel("127.0.0.1:30000", ChannelCredentials.Insecure);
var client = new DoSomething.DoSomethingClient(channel);

此时client对象即为可用的gRPC服务对象。

2. 进行gRPC调用

像进行本地函数调用一样进行gRPC调用。

每个rpc调用都会生成两个函数,一个原名,为同步版本,在结果返回之前调用函数不会返回。

另一个为异步版本,在原名后追加Async。发送的数据和收到的数据都会按照协议文件中的消息定义自动解析。


备注

代码部分如下

  1. 协议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";

package GrpcTest;

service DoSomething {
rpc Eat (PairNumber) returns (SystemInfo) {}
rpc Cat (PairNumber) returns (PairNumber) {}
}

message PairNumber {
int32 up = 1;
int32 down = 2;
}

message SystemInfo {
string message = 1;
}
  1. gRPC Server
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
using System;
using System.Threading.Tasks;
using Grpc.Core;

namespace GrpcTest
{
public class DoSomethingImpl : DoSomething.DoSomethingBase
{
public override async Task<PairNumber> Cat(PairNumber request, ServerCallContext context)
{
return await Task.Run(() =>
{
var result1 = request.Up + request.Down;
var result2 = request.Up * request.Down;
return new PairNumber { Up = result1, Down = result2 };
});

}

public override async Task<SystemInfo> Eat(PairNumber request, ServerCallContext context)
{
return await Task.Run(() =>
{
var result1 = request.Up + request.Down;
var result2 = request.Up * request.Down;
string result = $"Grpc test num1={request.Up} and num2={request.Down}, result1={result1} but result2={result2}. That's all.";
return new SystemInfo { Message = result };
});
}
}


class Program
{
static void Main(string[] args)
{
Server server = new Server
{
Services = { DoSomething.BindService(new DoSomethingImpl()) },
Ports = { new ServerPort("0.0.0.0", 36667, ServerCredentials.Insecure) }
};
server.Start();

Console.WriteLine("RouteGuide server listening on port 36667");
Console.WriteLine("Press any key to stop the server...");
Console.ReadKey();

server.ShutdownAsync().Wait();
}
}
}
  1. gRPC Client
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
using Grpc.Core;
using System;
using System.Threading.Tasks;

namespace GrpcTest {
class Program
{
static async Task RunRpc()
{
Channel channel = new Channel("127.0.0.1:36667", ChannelCredentials.Insecure);
var client = new DoSomething.DoSomethingClient(channel);

var pn = new PairNumber { Up = 16, Down = 24 };
var res = await client.CatAsync(pn);
Console.WriteLine("Server Response: " + res.ToString());

var res2 = await client.EatAsync(pn);
Console.WriteLine("Server Response: " + res2.Message);

await channel.ShutdownAsync();
}
static void Main(string[] args)
{
RunRpc().Wait();
}
}
}