.net core 微服务之 gRPC

概念

什么是gRPC

gRPC官网

微软官网

RPC基本理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 RPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。

gRPC 是由谷歌开发的一种语言中立、平台中立、开源的RPC的高性能远程过程调用 (RPC) 框架。

说的白话一点,可以这么理解:现在有两台服务器A和B。部署在A服务器上的应用,想调用部署在B服务器上的另一个应用提供的方法,由于不在一个内存空间,不能直接调用,需要通过网络来达到调用的效果。

现在,我们在A服务的一个本地方法中封装调用B的逻辑,然后只需要在本地使用这个方法,就达到了调用B的效果。

对使用者来说,屏蔽了细节。你只需要知道调用这个方法返回的结果,而无需关注底层逻辑。看到这里是不是想到了HTTP API。下面来比较一下gRPC HTTP API的区别

比较gRPC和HTTP API

gRPC的优点

性能

gRPC 消息使用 Protobuf(一种高效的二进制消息格式)进行序列化。 Protobuf 在服务器和客户端上可以非常快速地序列化。 Protobuf 序列化产生的有效负载较小,这在移动应用等带宽有限的方案中很重要。

gRPC 专为 HTTP/2(HTTP 的主要版本)而设计,与 HTTP 1.x 相比,HTTP/2 具有巨大性能优势:

  • 二进制组帧和压缩。 HTTP/2 协议在发送和接收方面均紧凑且高效。
  • 在单个 TCP 连接上多路复用多个 HTTP/2 调用。 多路复用可消除队头阻塞。

HTTP/2 不是 gRPC 独占的。 许多请求类型(包括具有 JSON 的 HTTP API)都可以使用 HTTP/2,并受益于其性能改进。

严格规范

具有 JSON 的 HTTP API 没有正式规范。 开发人员为 URL、HTTP 谓词和响应代码的最佳格式争论不休。

gRPC 规范对 gRPC 服务必须遵循的格式进行了规定。 gRPC 消除了争论并为开发人员节省了时间,因为 gRPC 在各个平台和实现中都是一致的。

流式处理

HTTP/2 为长期实时通信流提供基础。 gRPC 为通过 HTTP/2 进行流式传输提供一流支持。

gRPC 服务支持所有流式传输组合:

  • 一元(无流式传输)
  • 服务器到客户端流式传输
  • 客户端到服务器流式传输
  • 双向流式传输

一元 RPC:客户端向服务器发送单个请求并返回单个响应,就像普通的函数调用一样,也就是最常见的客户端请求、服务端响应实现方式

服务器流式 RPC:客户端向服务器发送请求并获取流以读取一系列消息。客户端从返回的流中读取,直到没有更多消息。gRPC 保证单个 RPC 调用中的消息排序。

客户端流式 RPC:其中客户端写入一系列消息并将它们发送到服务器,再次使用提供的流。一旦客户端完成写入消息,它等待服务器读取它们并返回其响应。gRPC 再次保证单个 RPC 调用中的消息排序。

双向流式 RPC:其中双方使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务器可以按照他们喜欢的任何顺序进行读写:例如,服务器可以在写入响应之前等待接收所有客户端消息,或者它可以交替读取消息然后写入消息,或其他一些读取和写入的组合。保留每个流中消息的顺序。

截止时间/超时和取消

gRPC 允许客户端指定其愿意等待 RPC 完成的时间期限。 截止时间会发送到服务器,如果超过截止时间,服务器可以决定要执行的操作。 例如,服务器可能会在超时后取消正在进行的 gRPC/HTTP/数据库请求。

通过 gRPC 子调用传播截止时间和取消有助于强制执行资源使用限制。

gRPC 非常适合以下方案:

  • 微服务:gRPC 设计用于低延迟和高吞吐量通信。 gRPC 对于效率至关重要的轻量级微服务非常有用。
  • 点对点实时通信:gRPC 对双向流式传输提供出色的支持。 gRPC 服务可以实时推送消息而无需轮询。
  • 多语言环境:gRPC 工具支持所有常用的开发语言,因此,gRPC 是多语言环境的理想选择。
  • 网络受限环境:gRPC 消息使用 Protobuf(一种轻量级消息格式)进行序列化。 gRPC 消息始终小于等效的 JSON 消息。
  • 进程间通信 (IPC) :IPC 传输(如 Unix 域套接字和命名管道)可与 gRPC 一起用于同一台计算机上的应用间通信。 有关详细信息,请参阅使用 gRPC 进行进程间通信。

 

gRPC的弱点

浏览器支持受限

当前无法通过浏览器直接调用 gRPC 服务。 gRPC 大量使用 HTTP/2 功能,且没有浏览器在 Web 请求中提供支持 gRPC 客户端所需的控制级别。 例如,浏览器不允许调用方要求使用 HTTP/2,也不提供对 HTTP/2 基础框架的访问。

ASP.NET Core 上的 gRPC 提供两种兼容浏览器的解决方案:

  • gRPC-Web 允许浏览器应用通过 gRPC-Web 客户端和 Protobuf 调用 gRPC 服务。 gRPC-Web 要求浏览器应用生成 gRPC 客户端。 gRPC-Web 允许浏览器应用从 gRPC 的高性能和低网络使用率获益。 .NET 提供对 gRPC-Web 的内置支持。 gRPC JSON 转码允许浏览器应用调用 gRPC 服务,就像它们是使用 JSON 的 RESTful API 一样。 浏览器应用不需要生成 gRPC 客户端或了解 gRPC 的任何信息。 通过使用 HTTP 元数据注释 .proto 文件,可从 gRPC 服务自动创建 RESTful API。
  • 转码使得应用可以同时支持 gRPC 和 JSON Web API,而无需重复为两者生成单独的服务。 .NET 对从 gRPC 服务创建 JSON Web API 提供了内置支持。gRPC JSON 转码需要 .NET 7 或更高版本。

非人工可读取

HTTP API 请求以文本形式发送,并且可进行人工读取和创建。

默认情况下,gRPC 消息使用 Protobuf 进行编码。 尽管 Protobuf 可以高效地发送和接收,但其二进制格式非人工可读取。 Protobuf 要求在 .proto 文件中指定消息接口描述来正确地反序列化。 需要使用其他工具来分析网络上的 Protobuf 有效负载以及手动撰写请求。服务器反射和 gRPC 命令行工具等功能可帮助使用二进制 Protobuf 消息。 此外,Protobuf 消息支持与 JSON 之间的转换。 内置的 JSON 转换提供在调试时将 Protobuf 消息与人工可读取格式互相转换的高效方法。

备用框架方案

在以下方案中,建议使用其他框架取代 gRPC:

  • 浏览器可访问的 API:gRPC 在浏览器中未受到完全支持。 gRPC-Web 可以提供浏览器支持,但它具有局限性并引入了服务器代理。 广播实时通信:gRPC 支持通过流式传输进行实时通信,但不存在将消息广播到注册连接的概念。 例如,在聊天室方案中,应将新的聊天消息发送到聊天室中的所有客户端,这要求每个 gRPC 调用将新的聊天消息单独流式传输到客户端。
  • SignalR 是适用于此方案的框架。 SignalR 具有持久性连接的概念,并内置对广播消息的支持。

  

 示例

 基本使用

1. 创建服务端

在“创建新项目”对话框中,搜索 gRPC。 选择“ASP.NET Core gRPC 服务”,新建名为GrpcGreeter的gRPC服务项目

 

注意:gRPC 模板配置为使用传输层安全性 (TLS)。 gRPC 客户端需要使用 HTTPS 调用服务器。当然也可以配置为http请求调用,在.net core 5之后好像是默认也支持了,但是之前的版本需要特别配置,这里演示的客户端和服务端都是用的https

GrpcGreeter 项目文件:

  • Protos/greet.proto:定义 Greeter gRPC,并用于生成 gRPC 服务器资产。
  • Services 文件夹:包含 Greeter 服务的实现。

解读一下greet.proto的文件syntax = "proto3"; //protobuf 有2个版本,默认版本是 proto2,如果需要 proto3,则需要在非空非注释第一行使用 syntax = "proto3" 标明版本。

option csharp_namespace = "GrpcGreeter";    //命名空间

package greet;        //即包名声明符是可选的,用来防止不同的消息类型有命名冲突。

//声明客户端需要调用的所有方法
service Greeter {    
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc SendMessage (SendMessageRequest) returns (SendMessageReply);
}

//下面都是上面方法需要的消息类型以及类型具体的属性,每个属性后面有一个数字,这个数字称为标识符,每个属性都需要提供一个唯一的标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变,标识符的取值范围为 [1, 2^29 - 1]。
message HelloRequest { string name = 1; } // The response message containing the greetings. message HelloReply { string message = 1; } message SendMessageRequest { string message = 1; } message SendMessageReply { string message = 1; }

注意:消息类型前面都需要 message 关键字定义,但是每个属性的类型(int,string,bool)在不同的语言定义不一样。

 

GreeterService类中的代码就是服务的具体实现,我们的业务就写在这里

 

在Program.cs生成的代码中已经帮我们注入了Grpc的服务,但是都是一些默认的配置,如果我们想自己设置消息的大小等属性,需要自己进行设置。更多服务端和客户端的配置可以跳转看看

//配置服务端
builder.Services.AddGrpc(options =>
{
    options.EnableDetailedErrors = true;
    options.MaxReceiveMessageSize = 2 * 1024 * 1024; // 2 MB   //默认Null ,消息大小不限制
    options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB   //默认Null ,消息大小不限制
});
//单个服务配置会覆盖全局选项
//builder.Services.AddGrpc().AddServiceOptions<GreeterService>(options =>
//{
//    options.MaxReceiveMessageSize = 2 * 1024 * 1024; // 2 MB
//    options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB
//});

对实现类GreeterService也添加了注入,如果自己新增了实现类也需要在这里注入

app.MapGrpcService<GreeterService>();

  

 

2. 创建客户端

2.1 我这里创建了一个.net core web api的项目作为客户端,取名为GrpcClient。

2.2 引入nuget包

Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools

2.3 创建 Protos 文件夹

2.4 从 gRPC 服务端 将 Protos\greet.proto 文件复制到 gRPC 客户端项目中的 Protos 文件夹 。

2.5 将 客户端 greet.proto 文件中的命名空间更新为项目的命名空间,其他东西都不改变:

 

option csharp_namespace = "GrpcClient";

 效果:

syntax = "proto3";

option csharp_namespace = "GrpcClient";

package greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc SendMessage (SendMessageRequest) returns (SendMessageReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}


message SendMessageRequest {
  string message = 1;
}

message SendMessageReply {
  string message = 1;
}

  

2.6 编辑 GrpcGreeterClient.csproj 项目文件,添加具有引用 greet.proto 文件的 <Protobuf> 元素的项组:

<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

3. 上面环境大部分都是自动生成的,这里看一下正题,客户端如何调用,测试一下调用一下服务端的SendMessage方法很简单

  [HttpGet("clientsendmessage")]
        public async Task<IActionResult> ClientSendMessage()
        {
            var channel = GrpcChannel.ForAddress("https://localhost:5166", new GrpcChannelOptions
            {
                MaxReceiveMessageSize = 5 * 1024 * 1024, // 5 MB
                MaxSendMessageSize = 2 * 1024 * 1024 // 2 MB
            });

            var client = new Greeter.GreeterClient(channel);
            var reply = await client.SendMessageAsync(
                              new SendMessageRequest { Message = "我是GrpcClient" });
            return Ok(reply.Message);
        }

注意:
GreeterClient类型是由生成进程自动生成的。 工具包 Grpc.Tools 基于 greet.proto 文件生成以下文件: GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs:用于填充、序列化和检索请求和响应消息类型的协议缓冲区代码。 GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs:包含生成的客户端类。

具有身份认证和授权的gRPC

服务端

这里演示是手动创建基于jwt认证,如果有三方的认证服务,直接调用三方的认证就行了

1.  引入nuget包

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

2. 创建简易JWT帮助类

    public static class JwtTicketHelper
    {
        public static JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
        public static SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());
        public static string CreateToken(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new InvalidOperationException("Name is not specified.");
            }

            var claims = new[] { new Claim(ClaimTypes.Name, name) }; //这里可以自定义写入其他想要的claim
            var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken("ExampleServer", "ExampleClients", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials);
            return JwtTokenHandler.WriteToken(token);
        }
    }

3. Program.cs配置:

  3.1 引入认证授权中间件

app.UseAuthentication();
app.UseAuthorization();

  3.2 创建自定义路由。以便客户端通过httpclient 获取token

app.MapGet("/generateJwtToken", context =>
{
    return context.Response.WriteAsync(JwtTicketHelper.CreateToken(context.Request.Query["name"]));
});

  注意:当然获取token这里也可以通过grpc的方式获取,下面两种方式都演示了

4. 在Protos文件夹中创建新的proto文件演示,这里命名为 ticketer.proto

    注意:
1. 右键=》新建文件=》可以添加后缀为proto的“协议缓冲区文件”,但是一直提示“必须指定语言”的错误我就放弃了
2. 不要复制现有的greet.proto来创建文件,直接导致(GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos)生成不了相应的文件,直接报错,这个坑了我好久。不知道是不是bug会不会修复
3. 我这里是手动创建了一个文本文件,然后修改其后缀为proto

  

syntax = "proto3";

option csharp_namespace = "GrpcTicketServer";
import "google/protobuf/empty.proto";

package Ticketer;

service Ticketer {
  rpc TestEmpty (google.protobuf.Empty) returns (google.protobuf.Empty);
  rpc SendMessage (Ticketer_SendMessageRequest) returns (Ticketer_SMessageReply);
  rpc GetToken (TokenRequest) returns (TokenReply);
}

message TokenRequest {
  string name =1;
}
message TokenReply {
  string token =1;
}

message Ticketer_SendMessageRequest {
  string message = 1;
}

message Ticketer_SMessageReply{
  string message = 1;
}
ticketer.proto

5. 在Services文件夹中创建实现类TicketerService。

 public class TicketerService : Ticketer.TicketerBase
    {
        private readonly ILogger<TicketerService> _logger;
        public TicketerService(ILogger<TicketerService> logger)
        {
            _logger = logger;
        }
        public override Task<Empty> TestEmpty(Empty request, ServerCallContext context)
        {
           return Task.FromResult(new Empty());
        }
        public override Task<TokenReply> GetToken(TokenRequest request, ServerCallContext context)
        {
            var token = JwtTicketHelper.CreateToken(request.Name);
            return Task.FromResult(new TokenReply
            {
                Token = token
            }) ;
        }
        [Authorize]
        public override Task<Ticketer_SMessageReply> SendMessage(Ticketer_SendMessageRequest request, ServerCallContext context)
        {
            var httpContext = context.GetHttpContext();
            return Task.FromResult(new Ticketer_SMessageReply
            {
                Message =$"这里是Ticketer,已经接受到您的消息:{request.Message}。您的名字是{httpContext.User.Identity.Name}",
            });
        }
    }

注意:这里添加了两个方法:

GetToken:用于未开启授权的方法,方便获取token,也就是上面 3.2 中提到的第二种获取token方式
SendMessage:开启授权,方便演示效果

 客户端

 1. 创建一个公共类GrpcCommon.cs,以便获取

 public class GrpcCommon
    {
        public static string hostUrl = "https://localhost:5166";
        /// <summary>
        /// 通过调用httpclient获取token
        /// </summary>
        /// <returns></returns>
        public static async Task<string> GetToken_Http()
        {
            var httpClient = new HttpClient();
            var request = new HttpRequestMessage
            {
                RequestUri = new Uri($"{hostUrl}/generateJwtToken?name=靓仔"),
                Method = HttpMethod.Get,
                Version = new Version(2, 0)
            };
            var tokenResponse = await httpClient.SendAsync(request);
            var _token = await tokenResponse.Content.ReadAsStringAsync();
            return _token;
        }
        /// <summary>
        /// 通过调用grpc获取token
        /// </summary>
        /// <returns></returns>
        public static async Task<string> GetToken_Grpc()
        {
            var client = new Ticketer.TicketerClient(GrpcChannel.ForAddress(hostUrl));
            var reply = await client.GetTokenAsync(
                              new TokenRequest { Name = "靓仔" });
            var _token = reply.Token;
            return _token;
        }

        public static async Task<GrpcChannel> GetGrpcInstance()
        {
            #region 获取token,设置每次请求metadata 中都会带token

            var _token = await GetToken_Grpc();
            var credentials = CallCredentials.FromInterceptor((context, metadata) =>
            {
                if (!string.IsNullOrEmpty(_token))
                {
                    metadata.Add("Authorization", $"Bearer {_token}");
                }
                return Task.CompletedTask;
            });
            #endregion
            var channel = GrpcChannel.ForAddress(hostUrl, new GrpcChannelOptions
            {
                Credentials = ChannelCredentials.Create(ChannelCredentials.SecureSsl, credentials)  //ChannelCredentials.Insecure : 不安全连接
            });
            return channel;
        }
    }

  2. 添加演示接口

 [HttpGet("ticketer_sendmessage")]
        public async Task<IActionResult> Ticketer_SendMessage()
        {
            var channel = await GrpcCommon.GetGrpcInstance();

            var client = new Ticketer.TicketerClient(channel);
            var reply = await client.SendMessageAsync(
                             new Ticketer_SendMessageRequest { Message = "我是GrpcClient" });
            return Ok(reply.Message);
        }

  效果:

 

侦查器

侦听器是一个 gRPC 概念,允许应用与传入或传出的 gRPC 调用进行交互。 它们提供了一种方法来扩充请求处理管道,侦听器针对通道或服务进行配置,并针对每个 gRPC 调用自动执行。 由于侦听器对用户的应用程序逻辑是透明的,因此它们是适用于常见情况(例如日志记录、监视、身份验证和验证)的极佳解决方案。

 客户端侦查器

Interceptor 方法为客户端重写以下项:

  • BlockingUnaryCall:截获一元 RPC 的阻塞调用。
  • AsyncUnaryCall:截获一元 RPC 的异步调用。
  • AsyncClientStreamingCall:截获客户端流式处理 RPC 的异步调用。
  • AsyncServerStreamingCall:截获服务器流式处理 RPC 的异步调用。
  • AsyncDuplexStreamingCall:截获双向流式处理 RPC 的异步调用。
示例
public class ClientLoggingInterceptor : Interceptor
    {
        public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
        {
            Console.WriteLine($"Starting call. Type: {context.Method.Type}. " +
                $"Method: {context.Method.Name}.");
            return continuation(request, context);
        }
    }
public class ErrorHandlerInterceptor : Interceptor
    {
        public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
        {
            var call = continuation(request, context);

            return new AsyncUnaryCall<TResponse>(
                HandleResponse(call.ResponseAsync),
                call.ResponseHeadersAsync,
                call.GetStatus,
                call.GetTrailers,
                call.Dispose);
        }

        private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> inner)
        {
            try
            {
                return await inner;
            }
            catch (Exception ex)
            {
                Console.Write("Custom error:"+ex.Message);
                throw new InvalidOperationException("Custom error", ex);
            }
        }
    }
 [HttpGet("ticketer_sendmessage")]
        public async Task<IActionResult> Ticketer_SendMessage()
        {
            var channel = await GrpcCommon.GetGrpcInstance();

            //添加侦听器
            var invoker = channel.Intercept(new ClientLoggingInterceptor())
                                               .Intercept(new ErrorHandlerInterceptor());

            var client = new Ticketer.TicketerClient(invoker);
            var reply = await client.SendMessageAsync(
                             new Ticketer_SendMessageRequest { Message = "我是GrpcClient" });
            return Ok(reply.Message);
        }

 服务端侦查器

Interceptor 方法为服务器重写以下项:

  • UnaryServerHandler:截获一元 RPC。
  • ClientStreamingServerHandler:截获客户端流式处理 RPC。
  • ServerStreamingServerHandler:截获服务器流式处理 RPC。
  • DuplexStreamingServerHandler:截获双向流式处理 RPC。
示例
public class ServerLoggerInterceptor : Interceptor
{
    private readonly ILogger _logger;

    public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        _logger.LogInformation($"Starting receiving call. Type: {MethodType.Unary}. " +
            $"Method: {context.Method}.");
        try
        {
            return await continuation(request, context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Error thrown by {context.Method}.");
            throw;
        }
    }
}
builder.Services.AddGrpc(options =>
{
    options.EnableDetailedErrors = true;
    options.MaxReceiveMessageSize = 2 * 1024 * 1024; // 2 MB   //默认Null ,消息大小不限制
    options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB   //默认Null ,消息大小不限制
    options.Interceptors.Add<ServerLoggerInterceptor>(); //将 ServerLoggerInterceptor 添加到服务选项的 Interceptors 集合中,为所有服务配置它。
});

 项目源码:

链接:https://pan.baidu.com/s/1MIvTod7LHogwDIXcvrgXaA?pwd=nd5c
提取码:nd5c

posted @ 2023-07-07 15:42  Joni是只狗  阅读(1201)  评论(0编辑  收藏  举报