专栏首页圣杰的专栏eShopOnContainers 知多少[11]:服务间通信之gRPC

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协议来实现。流程如下:

这意味着,默认情况下,您需要启用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 的使用和原理

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • eShopOnContainers 知多少[12]:Envoy gateways

    在最新的eShopOnContainers 3.0 中Ocelot 网关被Envoy Proxy 替换。下面就来简要带大家了解下Envoy,并尝试梳理下为什么...

    圣杰
  • .NET Core/.NET5/.NET6 开源项目汇总6:框架与架构设计(DDD、云原生/微服务/容器/DevOps/CICD等)项目

    Furioin 是一款基于.NET5技术开发的功能强大、性能极致、文档完善、示例丰富、极易入门、快速开发、极易维护的Web框架。

    张传宁IT讲堂
  • eShopOnContainers 知多少[1]:总体概览

    在微服务大行其道的今天,Java阵营的Spring Boot、Spring Cloud、Dubbo微服务框架可谓是风水水起,也不得不感慨Java的生态圈的火爆。...

    圣杰
  • eShopOnContainers 知多少[9]:Ocelot gateways

    客户端与微服务的通信问题永远是一个绕不开的问题,对于小型微服务应用,客户端与微服务可以使用直连的方式进行通信,但对于对于大型的微服务应用我们将不得不面对以下问题...

    圣杰
  • .NET微服务最佳实践eShopOnContainers

    微软与社区专家合作,开发了功能齐全的云原生微服务示例应用eShopOnContainers。 该应用旨在展示使用.NET、Docker以及可选的Azure,Ku...

    小码甲
  • 《容器化.NET应用架构指南》脑图学习笔记(1)

    作为.NET程序员,对于微软官方推动的架构示例总是特别关注,从PetShop到MusicStore再到eShopOnContainers,每一次关注,都会了解到...

    Edison Zhou
  • 开篇有益-解析微软微服务架构eShopOnContainers(一)

    为了推广.Net Core,微软为我们提供了一个开源Demo-eShopOnContainers,这是一个使用Net Core框架开发的,跨平台(几乎涵盖了所有...

    脑洞的蜂蜜
  • eShopOnContainers 知多少[5]:EventBus With RabbitMQ

    事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉。事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的...

    圣杰
  • .NET Core微服务系列基础文章索引(目录导航v0.8)

      今年从原来的Team里面被抽出来加入了新的Team,开始做Java微服务的开发工作,接触了Spring Boot, Spring Cloud等技术栈,对微服...

    Edison Zhou
  • eShopOnContainers 知多少[10]:部署到 K8S | AKS

    断断续续,感觉这个系列又要半途而废了。趁着假期,赶紧再更一篇,介绍下如何将eShopOnContainers部署到K8S上,进而实现大家常说的微服务上云。

    圣杰
  • Golang微服务实践

    在之前的文章《漫谈微服务》我已经简单的介绍过微服务,微服务特性是轻量级跨平台和跨语言的服务,也列举了比较了集中微服务通信的手段的利弊,本文将通过RPC通信的方式...

    用户2937493
  • eShopOnContainers 知多少[4]:Catalog microservice

    Catalog microservice(目录微服务)维护着所有产品信息,包括库存、价格。所以该微服务的核心业务为:

    圣杰
  • 云原生|dubbogo 3.0

    自从 2011 年 Dubbo 开源之后,被大量中小公司采用,一直是国内最受欢迎的 RPC 框架。2014 年,由于阿里内部组织架构调整,Dubbo 暂停维护了...

    heidsoft
  • eShopOnContainers 知多少[6]:持久化事件日志

    事件总线解决了微服务间如何基于集成事件进行异步通信的问题。然而只有事件总线正常运行,微服务之间基于事件的通信才得以运转。 而现实情况是,总有这样或那样的问题,...

    圣杰
  • A Kubernetes Service Mesh(第9部分):使用gRPC的乐趣和收益

    原文地址:https://dzone.com/articles/a-service-mesh-for-kubernetes-part-ix-grpc-for-f...

    Techeek
  • 【技术创作101训练营】剖析 gRPC

    各位好,今天的主题是剖析gRPC, 我们在实际工作中大量的应用gRPC做服务之间的调用。经常用写一些proto文件,用protoc把我们的proto文件生成相应...

    lpxxn
  • .NET平台系列31:.NET团队送给.NET开发人员的云原生学习资源汇总

     .NET Core 启动于2016年,跟K8S同年诞生,既拥有着悠久的历史积累,又集成了当下最新的设计理念,加上.NET团队持续对容器技术的官方支持和适配改...

    张传宁IT讲堂
  • gRPC基本使用(一)--java与go之间的相互调用

    gRPC是一个高性能、开源、通用的RPC框架,面向移动和HTTP/2设计。gRPC 默认使用 protocol buffers,这是 Google 开源的一套成...

    lpe234
  • 漫谈gRPC

    本文概括性的介绍gRPC,包括gRPC的起源,核心特性,生态体系,以及一些知名开源软件对gRPC的使用,最后总结gRPC与netty、dubbo等框架的区别,目...

    田守枝

扫码关注云+社区

领取腾讯云代金券