最近翻看最新3.0 eShopOncontainers源碼,發現其在架構選型中補充了 gRPC 進行服務間通訊。那就索性也寫一篇,做爲系列的補充。html
老規矩,先來理一下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在工具和框架中深度集成,開發者會有更好的開發體驗。java
首先來理一下eShopOncontainers 中服務間同步通訊的技術選型,主要仍是是基於HTTP/REST,gRPC做爲補充。linux
在eShopOncontainers中Ordering API、Catalog API、Basket API微服務經過gRPC端點暴露服務。其中Mobile Shopping、Web Shopping BFFs使用gRPC客戶端訪問服務。如下以Ordering API gRPC 服務舉例說明。git
訂單微服務中定義了一個gRPC服務,用於從購物車建立訂單。github
proto文件定義以下:web
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]怎麼消費的:app
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的問題。框架
咱們已經知道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相關技術選型時有必定的啓示和幫助。
參考資料: