最近翻看最新 3.0 eShopOncontainers 源码, 发现其在架构选型中补充了 gRPC 进行服务间通信. 那就索性也写一篇, 作为系列的补充.
gRPC
老规矩, 先来理一下 gRPC 的基本概念. gRPC 是 Google 开源的 RPC 框架, 比肩 dubbo,thrift,brpc. 其优势在于:
基于 proto buffer: 二进制协议, 具有高性能的序列化机制. 相较于 JSON(文本协议)而言, 首先从数据包上就有 60%-80% 的减小, 其次其解包速度仅需要简单的数学运算完成, 无需复杂的词法语法分析, 具有 8 倍以上的性能提升.
支持数据流.
基于 proto 文件: 可以更方便的在客户端和服务端之间进行交互.
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 相关技术选型时有一定的启示和帮助.
参考资料:
HTTP2.0 笔记之连接建立
eShopOnContainers/wiki/gRPC
Google Protocol Buffer 的使用和原理
来源: http://www.jianshu.com/p/75bbdbfdb27a