zoukankan      html  css  js  c++  java
  • eShopOnContainers 知多少[11]:服务间通信之gRPC

    引言

    最近翻看最新3.0 eShopOncontainers源码,发现其在架构选型中补充了 gRPC 进行服务间通信。那就索性也写一篇,作为系列的补充。

    gRPC

    老规矩,先来理一下gRPC的基本概念。gRPC是Google开源的RPC框架,比肩dubbo、thrift、brpc。其优势在于:
    1. 基于proto buffer:二进制协议,具有高性能的序列化机制。相较于JSON(文本协议)而言,首先从数据包上就有60%-80%的减小,其次其解包速度仅需要简单的数学运算完成,无需复杂的词法语法分析,具有8倍以上的性能提升。
    2. 支持数据流。
    3. 基于proto 文件:可以更方便的在客户端和服务端之间进行交互。
    4. gRPC语言无关性: 所有服务都是使用原型文件定义的。这些文件基于protobuffer语言,并定义服务的接口。基于原型文件,可以为每种语言生成用于创建服务端和客户端的代码。其中protoc编译工具就支持将其生成C #代码。从.NET Core 3 中,gRPC在工具和框架中深度集成,开发者会有更好的开发体验。

    gRPC 在 eShopOncontainers 的应用

    首先来理一下eShopOncontainers 中服务间同步通信的技术选型,主要还是是基于HTTP/REST,gRPC作为补充。

    在eShopOncontainers中Ordering API、Catalog API、Basket API微服务通过gRPC端点暴露服务。其中Mobile Shopping、Web Shopping BFFs使用gRPC客户端访问服务。以下以Ordering API gRPC 服务举例说明。

    订单微服务中定义了一个gRPC服务,用于从购物车创建订单。

    服务端实现

    proto文件定义如下:

    syntax = "proto3";
    option csharp_namespace = "GrpcOrdering";
    package OrderingApi;
    service OrderingGrpc {
      rpc CreateOrderDraftFromBasketData(CreateOrderDraftCommand) returns (OrderDraftDTO) {}
    }
    message CreateOrderDraftCommand {
      string buyerId = 1;
        repeated BasketItem items = 2;
    }
    message BasketItem {
        string id = 1;
        int32 productId = 2;
        string productName = 3;
        double unitPrice = 4;
        double oldUnitPrice = 5;
        int32 quantity = 6;
        string pictureUrl = 7;
    }
    message OrderDraftDTO {
        double total = 1;
        repeated OrderItemDTO orderItems = 2;
    }
    message OrderItemDTO {
        int32 productId = 1;
        string productName = 2;
        double unitPrice = 3;
        double discount = 4;
        int32 units = 5;
        string pictureUrl = 6;
    }
    

    服务实现,主要是借助Mediator充当CommandBus进行命令分发,具体实现如下:

    namespace GrpcOrdering
    {
        public class OrderingService : OrderingGrpc.OrderingGrpcBase
        {
            private readonly IMediator _mediator;
            private readonly ILogger<OrderingService> _logger;
    
            public OrderingService(IMediator mediator, ILogger<OrderingService> logger)
            {
                _mediator = mediator;
                _logger = logger;
            }
    
            public override async Task<OrderDraftDTO> CreateOrderDraftFromBasketData(CreateOrderDraftCommand createOrderDraftCommand, ServerCallContext context)
            {
                _logger.LogInformation("Begin gRPC call from method {Method} for ordering get order draft {CreateOrderDraftCommand}", context.Method, createOrderDraftCommand);
                _logger.LogTrace(
                    "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",
                    createOrderDraftCommand.GetGenericTypeName(),
                    nameof(createOrderDraftCommand.BuyerId),
                    createOrderDraftCommand.BuyerId,
                    createOrderDraftCommand);
    
                var command = new AppCommand.CreateOrderDraftCommand(
                                createOrderDraftCommand.BuyerId,
                                this.MapBasketItems(createOrderDraftCommand.Items));
                var data = await _mediator.Send(command);
    
                if (data != null)
                {
                    context.Status = new Status(StatusCode.OK, $" ordering get order draft {createOrderDraftCommand} do exist");
    
                    return this.MapResponse(data);
                }
                else
                {
                    context.Status = new Status(StatusCode.NotFound, $" ordering get order draft {createOrderDraftCommand} do not exist");
                }
    
                return new OrderDraftDTO();
            }
    
            public OrderDraftDTO MapResponse(AppCommand.OrderDraftDTO order)
            {
                var result = new OrderDraftDTO()
                {
                    Total = (double)order.Total,
                };
    
                order.OrderItems.ToList().ForEach(i => result.OrderItems.Add(new OrderItemDTO()
                {
                    Discount = (double)i.Discount,
                    PictureUrl = i.PictureUrl,
                    ProductId = i.ProductId,
                    ProductName = i.ProductName,
                    UnitPrice = (double)i.UnitPrice,
                    Units = i.Units,
                }));
    
                return result;
            }
    
            public IEnumerable<ApiModels.BasketItem> MapBasketItems(RepeatedField<BasketItem> items)
            {
                return items.Select(x => new ApiModels.BasketItem()
                {
                    Id = x.Id,
                    ProductId = x.ProductId,
                    ProductName = x.ProductName,
                    UnitPrice = (decimal)x.UnitPrice,
                    OldUnitPrice = (decimal)x.OldUnitPrice,
                    Quantity = x.Quantity,
                    PictureUrl = x.PictureUrl,
                });
            }
        }
    }
    

    同时,服务端还要注册gRPC的请求处理管道:

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
        endpoints.MapControllers();
        endpoints.MapGrpcService<OrderingService>();
    });
    

    客户端调用

    接下来看下客户端[web.bff.shopping]怎么消费的:

    public class OrderingService : IOrderingService
        {
            private readonly UrlsConfig _urls;
            private readonly ILogger<OrderingService> _logger;
            public readonly HttpClient _httpClient;
    
            public OrderingService(HttpClient httpClient, IOptions<UrlsConfig> config, ILogger<OrderingService> logger)
            {
                _urls = config.Value;
                _httpClient = httpClient;
                _logger = logger;
            }
            public async Task<OrderData> GetOrderDraftAsync(BasketData basketData)
            {
                return await GrpcCallerService.CallService(_urls.GrpcOrdering, async channel =>
                {
                    var client = new OrderingGrpc.OrderingGrpcClient(channel);
                    _logger.LogDebug(" gRPC client created, basketData={@basketData}", basketData);
                    var command = MapToOrderDraftCommand(basketData);
                    var response = await client.CreateOrderDraftFromBasketDataAsync(command);
                    _logger.LogDebug(" gRPC response: {@response}", response);
    
                    return MapToResponse(response, basketData);
                });
            }
            private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData)
            {
                if (orderDraft == null)
                {
                    return null;
                }
                var data = new OrderData
                {
                    Buyer = basketData.BuyerId,
                    Total = (decimal)orderDraft.Total,
                };
    
                orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData
                {
                    Discount = (decimal)o.Discount,
                    PictureUrl = o.PictureUrl,
                    ProductId = o.ProductId,
                    ProductName = o.ProductName,
                    UnitPrice = (decimal)o.UnitPrice,
                    Units = o.Units,
                }));
                return data;
            }
    
            private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData)
            {
                var command = new CreateOrderDraftCommand
                {
                    BuyerId = basketData.BuyerId,
                };
    
                basketData.Items.ForEach(i => command.Items.Add(new BasketItem
                {
                    Id = i.Id,
                    OldUnitPrice = (double)i.OldUnitPrice,
                    PictureUrl = i.PictureUrl,
                    ProductId = i.ProductId,
                    ProductName = i.ProductName,
                    Quantity = i.Quantity,
                    UnitPrice = (double)i.UnitPrice,
                }));
    
                return command;
            }
        }
    

    其中,GrpcCallerService是对gRPC Client的一层封装,主要是为了解决未启用TLS无法使用gRPC的问题。

    不启用TLS使用gRPC

    我们已经知道gRpc 是基于HTTP2.0 协议。然而,连接的建立,默认并不是一步到位直接基于HTTP2.0建立连接的。客户端是先基于HTTP1.1进行协议协商,协商成功后,确认服务端支持HTTP2.0后,才会建立HTT2.0连接,协议协商需要TLS的ALPN协议来实现。流程如下:
    HTTP2.0 协议协商

    这意味着,默认情况下,您需要启用TLS协议才能完成HTTP2.0协议协商,进而才能使用gRPC。

    然而,在微服务架构中,并不是所有服务都需要启用安全传输层协议,尤其是微服务间的内部调用。那么在微服务内部如何使用gRPC进行通信呢?

    客户端绕过协议协商,直连HTTP2.0(前提是:服务端必须支持HTTP2.0)

    服务端配置如下:

    WebHost.CreateDefaultBuilder(args)
        .ConfigureKestrel(options =>
        {
            options.Listen(IPAddress.Any, ports.httpPort, listenOptions =>
            {
                listenOptions.Protocols = HttpProtocols.Http1AndHttp2; //同时监听协议HTTP1,HTTP2
            });
            options.Listen(IPAddress.Any, ports.gRPCPort, listenOptions =>
            {
                listenOptions.Protocols = HttpProtocols.Http2; // gRPC端口仅监听HTTP2.0
            });
    
        })
    

    客户端需要添加以下设置,这些设置只能在客户端开始时设置一次:

    AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
    AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);
    

    知道了这些,再回过来看GrpcCallerService的实现,就一目了然了。

    public static class GrpcCallerService
    {
        public static async Task<TResponse> CallService<TResponse>(string urlGrpc, Func<GrpcChannel, Task<TResponse>> func)
        {
            AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
            AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);
    
            var channel = GrpcChannel.ForAddress(urlGrpc);
    
            /*
            using var httpClientHandler = new HttpClientHandler
            {
                ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }
            };
            */
    
            Log.Information(@"Creating gRPC client base address urlGrpc ={@urlGrpc}, 
                              BaseAddress={@BaseAddress} ", urlGrpc, channel.Target);
    
            try
            {
                return await func(channel);
            }
            catch (RpcException e)
            {
                Log.Error("Error calling via gRPC: {Status} - {Message}", e.Status, e.Message);
                return default;
            }
            finally
            {
                AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false);
                AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", false);
            }
        }
    
        public static async Task CallService(string urlGrpc, Func<GrpcChannel, Task> func)
        {
            AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
            AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);
    
            /*
            using var httpClientHandler = new HttpClientHandler
            {
                ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }
            };
            */
    
            var channel = GrpcChannel.ForAddress(urlGrpc);
    
            Log.Debug("Creating gRPC client base address {@httpClient.BaseAddress} ", channel.Target);
    
            try
            {
                await func(channel);
            }
            catch (RpcException e)
            {
                Log.Error("Error calling via gRPC: {Status} - {Message}", e.Status, e.Message);
            }
            finally
            {
                AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false);
                AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", false);
            }
        }
    }
    

    最后

    本文简要介绍了 eShopOnContainers 如何通过集成 gRPC 来完善服务间同步通信机制,希望对你在对微服务进行RPC相关技术选型时有一定的启示和帮助。

    参考资料:

    1. HTTP2.0笔记之连接建立
    2. eShopOnContainers/wiki/gRPC
    3. Google Protocol Buffer 的使用和原理
  • 相关阅读:
    JQUERY获取text,areatext,radio,checkbox,select值(转)
    如何查看oracle sql执行计划
    Silverlight资源整理
    ORACLE分页SQL语句
    百科名片SEO
    转 JQuery FlexiGrid的asp.net完美解决方案:dotNetFlexGrid 源码、文档、范例程序下载。
    清除SQLServer2005的LOG文件
    GridView EmptyDataText Border
    Uploading Files in ASP.NET 2.0
    Invalid postback or callback argument.
  • 原文地址:https://www.cnblogs.com/sheng-jie/p/13274580.html
Copyright © 2011-2022 走看看