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相关技术选型时有一定的启示和帮助。
参考资料:
eShopOnContainers 知多少[11]:服务间通信之gRPC的更多相关文章
- 浅谈服务间通信【MQ在分布式系统中的使用场景】
解决的问题 一项技术的产生必然是为了解决问题而生,了解了一项技术解决的问题,就能够很轻松的理解这项技术的设计根本,从而更好地理解与使用这项技术. 消息中间件和RPC从根本上来说都是为了解决分布式系统的 ...
- 007. 服务间通信 RPC & REST over HTTP(s) & 消息队列
服务间通信 服务间通信的几种方式: RPC.REST over HTTP(s).消息队列. https://www.jianshu.com/p/2a01d4383d0b RPC https://bl ...
- SpringCloud使用Feign实现服务间通信
SpringCloud的服务间通信主要有两种办法,一种是使用Spring自带的RestTemplate,另一种是使用Feign,这里主要介绍后者的通信方式. 整个实例一共用到了四个项目,一个Eurek ...
- Spring Cloud netflix feign【服务间通信】
一.简介 1,进程间通讯的本质是交换消息 2,服务间通信的两种方式 (1)RESTFul风格 (2)RPC风格 (3)两种风格的比较 3.基于RESTFul风格服务调用模型 4.基于Spring Cl ...
- gRPC-微服务间通信实践
微服务间通信常见的两种方式 由于微服务架构慢慢被更多人使用后,迎面而来的问题是如何做好微服务间通信的方案.我们先分析下目前最常用的两种服务间通信方案. gRPC(rpc远程调用) 场景:A服务主动发起 ...
- CAP-微服务间通信实践
微服务间通信常见的两种方式 由于微服务架构慢慢被更多人使用后,迎面而来的问题是如何做好微服务间通信的方案.我们先分析下目前最常用的两种服务间通信方案. gRPC(rpc远程调用) gRPC-微服务间通 ...
- .NET Core使用gRPC打造服务间通信基础设施
一.什么是RPC rpc(远程过程调用)是一个古老而新颖的名词,他几乎与http协议同时或更早诞生,也是互联网数据传输过程中非常重要的传输机制. 利用这种传输机制,不同进程(或服务)间像调用本地进程中 ...
- 使用gRPC打造服务间通信基础设施
一.什么是RPC rpc(远程过程调用)是一个古老而新颖的名词,他几乎与http协议同时或更早诞生,也是互联网数据传输过程中非常重要的传输机制. 利用这种传输机制,不同进程(或服务)间像调用本地进程中 ...
- .NET Core微服务开发服务间调用篇-GRPC
在单体应用中,相互调用都是在一个进程内部调用,也就是说调用发生在本机内部,因此也被叫做本地方法调用:在微服务中,服务之间调用就变得比较复杂,需要跨网络调用,他们之间的调用相对于与本地方法调用,可称为远 ...
随机推荐
- PHP配合JS导出Excel大量数据
一般使用PHP导出Excel表格都会用PHPExcel,但是当遇到要导出大量数据时,就会导致超时,内存溢出等问题.因此在项目中放弃使用这种方式,决定采用前段生成Excel的方式来解决问题. 步骤如下: ...
- 【实战】基于OpenCV的水表字符识别(OCR)
目录 1. USB摄像头取图 2. 图像预处理:获取屏幕ROI 2.1. 分离提取屏幕区域 2.2. 计算屏幕区域的旋转角度 2.3. 裁剪屏幕区域 2.4. 旋转图像至正向视角 2.5. 提取文字图 ...
- Linux使用手册
一.开关机 sync :把内存中的数据写到磁盘中(关机.重启前都需先执行sync) shutdown -rnow或reboot :立刻重启 shutdown -hnow :立刻关机 shutdown ...
- C# 9.0 新特性之目标类型推导 new 表达式
阅读本文大概需要 2 分钟. 呼~~,每次过完一个周末,写作就失去了动力,一两天才能缓过来.尽管如此,还是要坚持写好每一篇文章的.宁缺毋滥嘛,宁愿发文的频率低一点,也要保证文章的质量,至少排版不能差, ...
- 深入理解 EF Core:EF Core 读取数据时发生了什么?
阅读本文大概需要 11 分钟. 原文:https://bit.ly/2UMiDLb 作者:Jon P Smith 翻译:王亮 声明:我翻译技术文章不是逐句翻译的,而是根据我自己的理解来表述的.其中可能 ...
- 记linux vsftpd配置遇到的错误
环境:centos 7 yum安装 yum install -y vsftpd 增加用户 # 家目录为/www 并设置nologin useradd -d /www -s /sbin/nologin ...
- 【Azure SQL】数据库性能分析
前置条件 用户有查询数据统计权限 GRANT VIEW DATABASE STATE TO database_user; CPU性能问题 正在发生 查看前X个CPU消耗查询 (汇总) SELECT T ...
- cookie的介绍和使用
一.什么是cookie 是由服务器端生成,发送给客户端(一般指浏览器),浏览器将cookie以键值对的形式保存到某个目录下的文本文件内.下次请求该网站时就把cookie发送回服务器.(cookie就是 ...
- selenium(5)-解读强制等待,隐式等待,显式等待的区别
背景 为什么要设置元素等待 因为,目前大多数Web应用程序都是使用Ajax和Javascript开发的:每次加载一个网页,就会加载各种HTML标签.JS文件 但是,加载肯定有加载顺序,大型网站很难说一 ...
- RocksDB事务的隔离性分析【原创】
Rocksdb事务隔离性指的是多线程并发事务使用时候,事务与事务之间的隔离性,通过加锁机制来实现,本文重点剖析Read Commited隔离级别下,Rocksdb的加锁机制. Rocksdb事务相关类 ...