环境:
- window10x64 企业版
- vs2019 16.7.7
- .net core3.1
参照:
《ASP.NET Core 3.0 使用gRPC》
《Asp.Net Core Grpc使用C#对象取代Proto定义》
实验代码下载:
https://download.csdn.net/download/u010476739/13090566
一、什么是grpc?
首先,RPC(Remote Procedure Call)是用来实现不同服务之间的调用的。像之前的microsoft .net remoting、WCF、WebService都是这类功能。
然后,随着http和web前端的迅速发展,也越来越多的服务接口采用restful设计。
现在,微服务概念盛行,伴随着而来的就是微服务之间的互相调用,那么google就自己开发了一套微服务间通信的标准,称之为:GRPC。
二、grpc有什么特点?
- 现代高性能轻量级 RPC 框架。
- 使用 HTTP/2 进行传输
- 通过Protocol Buffers二进制序列化减少网络使用
- 约定优先的 API 开发,默认使用 Protocol Buffers 作为描述语言,允许与语言无关的实现。
三、创建一个grpc服务
新建一个空白解决方案:grpc-trial
,然后新建一个空的web工程grpc-server1
,如下:
修改grpc-server1
工程:
添加nuget包Grpc.AspNetCore
:
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.27.0" />
</ItemGroup>
我们在解决方案目录下新建文件夹Protos
:
在这个文件夹下,新建文件TestServer.proto
,内容如下:
syntax = "proto3";
// 命名空间
option csharp_namespace = "Protos";
package Protos;
// 服务接口
service TestServer {
// 接口内的方法
rpc SayHello (HelloRequest) returns (HelloReply);
}
// 请求参数模型
message HelloRequest {
string name = 1;
}
// 响应参数模型
message HelloReply {
string message = 1;
}
将文件夹Protos
下的内容全部包含到grpc-server1
工程下:
<ItemGroup>
<Protobuf Include="..\Protos\**\*.proto" GrpcServices="Server" Link="Protos\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
效果如下图所示:
在工程上新建服务类TestServer
,如下图所示:
服务类的代码如下:
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Protos;
using System.Threading.Tasks;
namespace grpc_server1.GrpcServices
{
public class TestServer : Protos.TestServer.TestServerBase
{
private readonly ILogger<TestServer> _logger;
public TestServer(ILogger<TestServer> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
_logger.LogInformation($"接受到了请求:{request.Name}");
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
}
Protos.TestServer.TestServerBase
这个类是怎么来的?vs根据TestServer.proto
文件自动生成的。
由于grpc是用的http2,所以我们在appsettings.json
中配置Kestrel使用http2:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}
现在,让我们在web程序中引入grpc
,Startup.cs
的ConfigureServices
中注入grpc服务:
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
}
在Configure
方法中,注入TestServer服务
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<TestServer>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
好了,现在让我们编译并启动它吧!
四、创建一个客户端,调用grpc服务
在解决方案上新建.net core控制台项目TestClient1
。
修改工程TestClient1
:
引入nuget包Grpc.Net.Client
、Google.Protobuf
以及Grpc.Tools
:
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.13.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.33.1" />
<PackageReference Include="Grpc.Tools" Version="2.33.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
然后也将Protos
文件夹下的*.proto文件包含进来(注意GrpcServices属性的值):
<ItemGroup>
<Protobuf Include="..\Protos\**\*.proto" GrpcServices="Client" Link="Protos\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
最终如下:
在Program.cs中编写代码:
using Grpc.Net.Client;
using Protos;
using System;
using System.Threading.Tasks;
namespace TestClient1
{
class Program
{
static async Task Main(string[] args)
{
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new TestServer.TestServerClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "jackletter" });
Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
Console.WriteLine("ok");
Console.ReadLine();
}
}
}
五、测试效果
编译并运行工程TestClient1
如下:
而grpc服务端输出如下:
到此,一个简单的grpc调用就完成了。
六、给客户端和服务端加上拦截器
拦截器就像MVC的过滤器或者是ASP.NET Core middleware 一样,具有面向切面的思想,可以在调用服务的时候进行一些统一处理, 很适合在这里处理验证、日志等流程。
Interceptor
类是gRPC服务拦截器的基类,是一个抽象类,它定了几个虚方法,分别如下:
//拦截阻塞调用
public virtual TResponse BlockingUnaryCall<TRequest, TResponse>();
//拦截异步调用
public virtual AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>();
//拦截异步服务端流调用
public virtual AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>();
//拦截异步客户端流调用
public virtual AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>();
//拦截异步双向流调用
public virtual AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>();
//用于拦截和传入普通调用服务器端处理程序
public virtual Task<TResponse> UnaryServerHandler<TRequest, TResponse>();
//用于拦截客户端流调用的服务器端处理程序
public virtual Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>();
//用于拦截服务端流调用的服务器端处理程序
public virtual Task ServerStreamingServerHandler<TRequest, TResponse>();
//用于拦截双向流调用的服务器端处理程序
public virtual Task DuplexStreamingServerHandler<TRequest, TResponse>();
6.1 客户端拦截器
在TestClient1
工程上新建类:ClientLoggerInterceptor
:
public class ClientLoggerInterceptor : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
LogCall(context.Method);
return continuation(request, context);
}
private void LogCall<TRequest, TResponse>(Method<TRequest, TResponse> method)
where TRequest : class
where TResponse : class
{
var initialColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Starting call. Type: {method.Type}. Request: {typeof(TRequest)}. Response: {typeof(TResponse)}");
Console.ForegroundColor = initialColor;
}
}
在客户端上注册拦截器,修改Program
代码:
class Program
{
static async Task Main(string[] args)
{
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());
var client = new TestServer.TestServerClient(invoker);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "jackletter" });
Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
Console.WriteLine("ok");
}
}
6.2 服务端拦截器
在grpc-server1
工程上新建类ServerLoggerInterceptor
,如下:
public class ServerLoggerInterceptor:Interceptor
{
private readonly ILogger<ServerLoggerInterceptor> logger;
public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
{
this.logger = logger;
}
public override Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
{
LogCall<TRequest, TResponse>(MethodType.Unary, context);
return continuation(request, context);
}
private void LogCall<TRequest, TResponse>(MethodType methodType, ServerCallContext context)
where TRequest : class
where TResponse : class
{
logger.LogWarning($"Starting call. Type: {methodType}. Request: {typeof(TRequest)}. Response: {typeof(TResponse)}");
}
}
在服务端上注册拦截器,修改Startup
的ConfigureServices
方法代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.Interceptors.Add<ServerLoggerInterceptor>();
});
}
然后,编译并先后运行服务端和客户端,输出如下:
服务端:
客户端:
七、grpc流式调用
7.1 什么是grpc流?
gRPC 有四种服务类型,分别是:简单 RPC(Unary RPC)、服务端流式 RPC (Server streaming RPC)、客户端流式 RPC (Client streaming RPC)、双向流式 RPC(Bi-directional streaming RPC)。它们主要有以下特点:
- 简单 RPC
一般的rpc调用,传入一个请求对象,返回一个返回对象
- 服务端流式 RPC
传入一个请求对象,服务端可以返回多个结果对象
- 客户端流式 RPC
客户端传入多个请求对象,服务端返回一个结果对象
- 双向流式 RPC
结合客户端流式RPC和服务端流式RPC,可以传入多个请求对象,返回多个结果对象
gRPC 通信是基于 HTTP/2 实现的,它的双向流映射到 HTTP/2 流。HTTP/2 具有流的概念,流是为了实现HTTP/2的多路复用。关于http2的流概念参考:《HTTP/2笔记之流和多路复用》
7.2 写一个双向流功能
注:一开始我们写的属于简单调用。
下面我们要写一个支持双向流调用的方法。当我们调用这个方法后,我们就打开了一个双向流管道,我们可以向这个管道中写入东西,同时也可以从这个管道中读取东西(同时读写肯定需要两个线程的!)。
我们新建一个proto
文件:TestStream.proto
,将它放到TestServer.proto的同目录,内容如下:
syntax = "proto3";
// 命名空间
option csharp_namespace = "Protos";
package Protos;
// 服务接口
service TestStream {
// 接口内的方法
rpc StreamRequest (stream StreamReq) returns (stream StreamRes);
}
// 请求参数模型
message StreamReq {
string name = 1;
}
// 响应参数模型
message StreamRes {
string message = 1;
}
然后,我们在服务端新建类TestStream
,位置如下:
服务类的代码如下:
namespace grpc_server1.GrpcServices
{
public class TestStream : Protos.TestStream.TestStreamBase
{
private static int count = 0;
private readonly ILogger<TestStream> logger;
public TestStream(ILogger<TestStream> logger)
{
this.logger = logger;
}
public override async Task StreamRequest(IAsyncStreamReader<StreamReq> req, IServerStreamWriter<StreamRes> res, ServerCallContext context)
{
var cur = Interlocked.Increment(ref count);
logger.LogInformation($"开始接受流式请求:{cur}");
while (await req.MoveNext())
{
StreamReq currentReq = req.Current;
logger.LogInformation($"在流式请求{cur}中,接受到了请求:{currentReq.Name}...");
await res.WriteAsync(new StreamRes()
{
Message = $"请求序列:{cur},请求参数:{currentReq.Name}"
});
}
logger.LogInformation($"处理流请求{cur}完毕!");
}
}
}
然后,我们将这个grpc服务像TestServer
一样注册进去(Startup.cs):
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<TestServer>();
endpoints.MapGrpcService<TestStream>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
现在,我们修改客户端的调用代码(main方法),修改后如下:
class Program
{
static async Task Main(string[] args)
{
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());
//rpc简单调用=======================开始============================
var client = new TestServer.TestServerClient(invoker);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "jackletter" });
Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
//rpc简单调用========================结束============================
//rpc双向流调用=======================开始===========================
var streamClient = new TestStream.TestStreamClient(invoker);
var streamCall = streamClient.StreamRequest();
//定义流式响应处理逻辑
var task = Task.Run(async () =>
{
await foreach (var resp in streamCall.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"收到了消息:{resp.Message}");
}
});
//发送几段流式数据
Console.WriteLine("客户端开始发送5段数据...");
var names = new string[] { "name1", "name2", "name3", "name4", "name5" };
for (int i = 0; i < 5; i++)
{
await streamCall.RequestStream.WriteAsync(new StreamReq() { Name = names[i] });
Console.WriteLine($"客户端发送请求段完毕:{names[i]}");
//查看控制台接受的返回
await Task.Delay(800);
}
//发送完毕
await streamCall.RequestStream.CompleteAsync();
Console.WriteLine("客户端已发送完5段数据!");
//等待响应结束
await task;
Console.WriteLine("双向流调用完成!");
//rpc双向流调用===============================结束========================
Console.WriteLine("ok");
Console.ReadLine();
}
}
7.3 测试双向流效果
编译上面两个工程,先运行服务端,然后运行客户端,效果如下图:
八、使用c#代码
代替*.proto
文件
上面的例子都是使用*.proto
文件定义服务接口,这样,当我们给别人使用的时候,我们需要将*.proto
文件共享给别人,然而当彼此双方都是.Neter
的时候,是否直接提供给别人*.dll
文件更方便呢?下面就来实验使用*.dll
文件…
先准备一个空白解决方案GrpcDemo
。
8.1 准备接口
新建.net standard 2.1
工程ShareDllInterface
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.ServiceModel.Primitives" Version="4.8.1" />
</ItemGroup>
</Project>
定义接口如下(IHelloService.cs):
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
namespace ShareDllInterface
{
[ServiceContract]
public interface IHelloService
{
[OperationContract]
ResponseMessage SayHello(RequestMessage req);
[OperationContract]
IAsyncEnumerable<ResponseMessage> StreamDemo(IAsyncEnumerable<RequestMessage> stream);
}
[DataContract]
public class RequestMessage
{
[DataMember(Order = 1)]
public string Name { get; set; }
}
[DataContract]
public class ResponseMessage
{
[DataMember(Order = 1)]
public string Message { set; get; }
}
}
8.2 准备grpc服务
新建asp.net core空web工程ShareDllGrpcServer
,并添加到工程ShareDllInterface
的引用,然后添加protobuf-net.Grpc.AspNetCore
包引用,最终*.csproj文件如下:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.123" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ShareDllInterface\ShareDllInterface.csproj" />
</ItemGroup>
</Project>
新建类HelloService
实现IHelloService
服务:
using Microsoft.Extensions.Logging;
using ShareDllInterface;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace ShareDllGrpcServer
{
public class HelloService : IHelloService
{
private readonly ILogger<HelloService> logger;
public HelloService(ILogger<HelloService> logger)
{
this.logger = logger;
}
public ResponseMessage SayHello(RequestMessage req)
{
logger.LogInformation($"接收到了SayHello调用:{req?.Name}");
return new ResponseMessage()
{
Message = $"hello,{req?.Name}!"
};
}
private int count = 0;
public async IAsyncEnumerable<ResponseMessage> StreamDemo(IAsyncEnumerable<RequestMessage> stream)
{
int cur = Interlocked.Increment(ref count);
logger.LogInformation($"接收到了StreamDemo调用:{cur}...");
await foreach (var req in stream)
{
logger.LogInformation($"收到了消息:P{req.Name}");
await Task.Delay(800);
var res = new ResponseMessage()
{
Message = $"流式调用{cur}:{req?.Name}"
};
yield return res;
}
logger.LogInformation($"客户端关闭流式调用:{cur}");
}
}
}
将grpc服务注入到容器中,并开启到HelloService
的映射,修改Startup.cs
即可,最终如下:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ProtoBuf.Grpc.Server;
namespace ShareDllGrpcServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
//添加Grpc服务
services.AddCodeFirstGrpc();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<HelloService>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
}
最后,不要忘了grpc采用的是http2,所以我们在appsettings.json
文件中配置Kestrel
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}
好了,现在grpc服务就准备完了。
8.3 准备grpc客户端
新建.net core控制台工程ShareDllClient
,添加如下包引用:
<ItemGroup>
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.23.2" />
<PackageReference Include="protobuf-net.Grpc" Version="1.0.123" />
</ItemGroup>
添加到ShareDllInterface
工程的引用:
<ItemGroup>
<ProjectReference Include="..\ShareDllInterface\ShareDllInterface.csproj" />
</ItemGroup>
我们也可以通过
nuget
包或直接引用dll文件。
写客户端代码如下:
using Grpc.Net.Client;
using ProtoBuf.Grpc.Client;
using ShareDllInterface;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ShareDllClient
{
class Program
{
static async Task Main(string[] args)
{
ProtoBuf.Grpc.Client.GrpcClientFactory.AllowUnencryptedHttp2 = true;
using var http = GrpcChannel.ForAddress("http://localhost:5000");
var client = http.CreateGrpcService<IHelloService>();
//简单测试
var req = new RequestMessage()
{
Name = "小明"
};
Console.WriteLine($"发送简单消息:{req.Name}");
var res = client.SayHello(req);
Console.WriteLine($"收到了响应:{res.Message}");
//流式调用
await foreach (var reply in client.StreamDemo(SendPackage()))
{
Console.WriteLine($"收到流式响应:{reply.Message}");
}
Console.WriteLine("流式调用已结束!");
Console.ReadLine();
}
static int count = 1;
private static async IAsyncEnumerable<RequestMessage> SendPackage()
{
for (int i = 0; i < 5; i++)
{
var request = new RequestMessage
{
Name = "name" + count
};
Console.WriteLine($"发送流式消息:{request.Name}");
yield return request;
}
}
}
}
8.4 测试效果
让我们编译并先后运行ShareDllGrpcServer
和ShareDllClient
工程吧:
至此,我们的初体验完成!