DotNet Core 中使用 gRPC

gRPC 简介

gRPC(gRPC Remote Procedure Calls)是一个由 Google 开源的,跨语言的,高性能的远程过程调用(RPC)框架。 gRPC 使客户端和服务端应用程序可以透明地进行通信,并简化了连接系统的构建。它使用 HTTP/2 作为通信协议,使用 Protocol Buffers 作为序列化协议。

官网:https://grpc.io/

Github:https://github.com/grpc/grpc

DotNet Core 官方示例:https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/grpc

gRPC 的主要优点
  • 现代高性能轻量级 RPC 框架。
  • 约定优先的 API 开发,默认使用 Protocol Buffers 作为描述语言,允许与语言无关的实现。
  • 可用于多种语言的工具,以生成强类型的服务器和客户端。
  • 支持双向流式的请求和响应,对批量处理、低延时场景友好。
  • 通过 Protocol Buffers 二进制序列化减少网络使用。
  • 使用 HTTP/2 进行传输
gRPC 适用的场景
  • 高性能的轻量级微服务。
  • 多语言混合开发的 Polyglot 系统。
  • 需要处理流式处理请求或响应的点对点实时通信服务。
gRPC 不适用的场景
  • 浏览器可访问的 API:浏览器不完全支持 gRPC。虽然 gRPC-Web 可以提供浏览器支持,但是它有局限性,引入了服务器代理。
  • 广播实时通信:gRPC 支持通过流进行实时通信,但不存在向已注册连接广播消息的概念。
  • 进程间通信:进程必须承载 HTTP/2 才能接受传入的 gRPC 调用,对于 Windows,进程间通信管道是一种更快速的方法。
gRPC 支持的语言

目前 gRPC 已经实现了对主流语言的支持,以下语言在 gRPC 的 Github 中都提供了实现。

支持的语言

在 DotNet Core 中使用 gRPC

创建服务端

Visual Studio 2019 中已经集成了 gRPC 项目的模版,我们可以通过这个模版快速的创建一个基于 DotNet Core 的 gRPC 项目。

创建好的项目结构如下:

这时候项目不用做任何修改就可以运行了,那么这个项目和普通的 DotNet Core 项目有什么不同呢?
首先项目文件 GrpcService.csproj 中引入了 Grpc.AspNetCore 包。

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.27.0" />
  </ItemGroup>

appsettings.json 文件中多出了一个 Kestrel 节点,配置 Protocols 使用 Http2 协议。

  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http2"
    }
  }

服务端 GreeterService 类的实现如下:

    public class GreeterService : Greeter.GreeterBase
    {
        private readonly ILogger<GreeterService> _logger;
        public GreeterService(ILogger<GreeterService> logger)
        {
            _logger = logger;
        }

        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = "Hello " + request.Name
            });
        }
    }

服务端 Startup 类中注入了 gRPC 服务:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<GreeterService>();

            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
            });
        });
    }

gRPC 工具会根据 proto 文件自动生成需要使用的类,生成的类会存放在项目的 obj\Debug\netcoreapp3.1 目录下:

创建客户端

客户端项目需要手动的创建,创建方法也很简单,直接在解决方案中添加一个新的项目即可,这里我创建了一个空的 Web 项目。

项目创建好了以后首先要把 proto 文件添加到项目中,这里需要用到 dotnet-grpc 这个工具。
在命令行下安装 gRPC 工具:

dotnet tool install dotnet-grpc -g

安装完以后从命令行进入 GrpcClient 项目的目录后,添加服务端的 proto 文件到客户端。

 dotnet grpc add-file ..\GrpcService\Protos\greet.proto 

也可以使用远程路径的 proto 文件:

dotnet grpc add-url 
https://raw.githubusercontent.com/grpc/grpc/master/examples/protos/keyvaluestore.proto -o /Protos/keyvaluesrore.proto

导入 proto 文件以后,GrpcClient 项目文件中会增加如下代码:

  <ItemGroup>
    <Protobuf Include="..\GrpcService\Protos\greet.proto" 
Link="Protos\greet.proto" />
    <Protobuf Include="XXXX/Protos/keyvaluesrore.proto" 
Link="Protos\keyvaluesrore.proto">
<SourceUrl>https://raw.githubusercontent.com/grpc/grpc/master/examples/protos/keyvaluestore.proto</SourceUrl>
    </Protobuf>
  </ItemGroup>

dotnet-grpc 除了以上用法还支持以下命令,详细用法可以查阅 Microsoft Docs

  • dotnet grpc add-file
  • dotnet grpc add-url
  • dotnet grpc remove
  • dotnet grpc refresh

接下来修改 Startup 中的代码,在 ConfigureServices(IServiceCollection services) 方法注入 gRPC 客户端代码:

services.AddGrpcClient<GreeterClient>(options => options.Address = new 
Uri("https://localhost:5001"));

注:Uri("https://localhost:5001") 中使用服务端的端口和地址。

Configure 方法中添加响应代码:

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            GreeterClient client = context.RequestServices.GetService<GreeterClient>();
            HelloRequest request = new HelloRequest();
            request.Name = "Charles";
            var reply = await client.SayHelloAsync(request);

            await context.Response.WriteAsync(reply.Message);
        });
    });

如果不想使用注入的方式也可以直接调用:

    var httpClientHandler = new HttpClientHandler();
    var httpClient = new HttpClient(httpClientHandler);

    var channel = GrpcChannel.ForAddress(Address);
    var client = new GreeterClient(channel);

    HelloRequest request = new HelloRequest
    {
        Name = "Charles"
    };
    var reply = await client.SayHelloAsync(request);

    await context.Response.WriteAsync(reply.Message);

启动项目后在客户端可以看到输出了服务端返回的内容:

Hello Charles
限制消息大小

消息大小限制是一种有助于防止 gRPC 消耗过多资源的机制。gRPC 使用每个消息的大小限制来管理传入和传出消息。 默认情况下,gRPC 将传入消息限制为 4 MB。 传出消息没有限制。
在服务端上,可以使用 AddGrpc 为应用中的所有服务配置 gRPC 消息限制:

    services.AddGrpc(options =>
    {
        options.MaxReceiveMessageSize = 1 * 1024 * 1024; // 1 MB
        options.MaxSendMessageSize = 1 * 1024 * 1024; // 1 MB
    });
调用不安全的 gRPC 服务:

若要使客户端调用不安全的 gRPC 服务,需要修改客户端的配置。

    AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
调用不受信任、无效证书调用 gRPC 服务

DotNet gRPC 客户端要求服务具有受信任的证书,若要调用不受信任、无效证书调用 gRPC 服务,需要修改客户端请求的代码:

    services.AddGrpcClient<GreeterClient>(options => options.Address = new Uri(Address)).
    ConfigurePrimaryHttpMessageHandler(provider =>
    {
        var handler = new SocketsHttpHandler();
        handler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; // 允许不受信任、无效证书
        return handler;
    });

非注入方式:

    var httpClientHandler = new HttpClientHandler
    {
        ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    };

    var httpClient = new HttpClient(httpClientHandler);

    var channel = GrpcChannel.ForAddress(Address);
    var client = new GreeterClient(channel);

    HelloRequest request = new HelloRequest
    {
        Name = "Charles"
    };
    var reply = await client.SayHelloAsync(request);

身份验证和授权

服务端

为服务端加入身份验证也很简单,首先需要为项目引入 Microsoft.AspNetCore.Authentication.JwtBearer 这个包。

    <ItemGroup>
        <PackageReference Include="Grpc.AspNetCore" Version="2.27.0" />
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
    </ItemGroup>

修改服务端 Startup 类的代码,分别在 ConfigureConfigureServices 方法中加入如下代码:

    services.AddAuthorization(options =>
    {
        options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
        {
            policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
            policy.RequireClaim(ClaimTypes.Name);
        });
    });
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters =
                new TokenValidationParameters
                {
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidateActor = false,
                    ValidateLifetime = true,
                    IssuerSigningKey = SecurityKey
                };
        });
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<GreeterService>();

        endpoints.MapGet("/getToken", context =>
        {
            return context.Response.WriteAsync(GenerateJwtToken(context.Request.Query["name"]));
        });
    });

生成 Token 的方法:

    private string GenerateJwtToken(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            throw new InvalidOperationException("Name is not specified.");
        }

        var claims = new[] { new Claim(ClaimTypes.Name, name) };
        var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
        var token = new JwtSecurityToken("JwtSecurityIssuer", "JwtSecurityClients", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials);
        return JwtTokenHandler.WriteToken(token);
    }
客户端

客户端的实现逻辑是在请求服务端之前先从服务端获取到 Token,在之后调用响应服务的时候将 Token 放入请求头中传入服务端,如果不传入直接请求服务端会返回 401。

    var httpClient = new HttpClient();
    var httpRequest = new HttpRequestMessage
    {
        RequestUri = new Uri($"{Address}/getToken?name=Charles"),
        Method = HttpMethod.Get,
        Version = new Version(2, 0)
    };
    var tokenResponse = await httpClient.SendAsync(httpRequest);
    tokenResponse.EnsureSuccessStatusCode();

    var token = await tokenResponse.Content.ReadAsStringAsync();
    Metadata headers = null;
    if (token != null)
    {
        headers = new Metadata
        {
            { "Authorization", $"Bearer {token}" }
        };
    }

    var channel = GrpcChannel.ForAddress(Address);
    var client = new GreeterClient(channel);

    HelloRequest request = new HelloRequest
    {
        Name = "Charles"
    };
    var reply = await client.SayHelloAsync(request, headers);

总结

以上就是在 DotNet Core 中使用 gRPC 的常用方法了,想深入学习可以去查看 gRPC DotNet 项目的 Github,里面有很多实例可以参考:https://github.com/grpc/grpc-dotnet/tree/master/examples

posted @ 2020-05-25 12:02  Charles Zhang  阅读(1752)  评论(0编辑  收藏  举报