eShopOnContainers 知多少[11]:服務間通訊之gRPC
阿新 • • 發佈:2020-07-10
# 引言
最近翻看最新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 _logger;
public OrderingService(IMediator mediator, ILogger logger)
{
_mediator = mediator;
_logger = logger;
}
public override async Task 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 MapBasketItems(RepeatedField 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();
});
```
## 客戶端呼叫
接下來看下客戶端[web.bff.shopping]怎麼消費的:
```
public class OrderingService : IOrderingService
{
private readonly UrlsConfig _urls;
private readonly ILogger _logger;
public readonly HttpClient _httpClient;
public OrderingService(HttpClient httpClient, IOptions config, ILogger logger)
{
_urls = config.Value;
_httpClient = httpClient;
_logger = logger;
}
public async Task 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 協議協商](https://upload-images.jianshu.io/upload_images/2799767-0f9c300f468bd458.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
這意味著,預設情況下,您需要啟用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 CallService(string urlGrpc, Func> 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 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筆記之連線建立](http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html)
>2. [eShopOnContainers/wiki/gRPC](https://github.com/dotnet-architecture/eShopOnContainers/wiki/gRPC)
>3. [Google Protocol Buffer 的使用和原理](https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/inde