引言

最近翻看最新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 的使用和原理

eShopOnContainers 知多少[11]:服务间通信之gRPC的更多相关文章

  1. 浅谈服务间通信【MQ在分布式系统中的使用场景】

    解决的问题 一项技术的产生必然是为了解决问题而生,了解了一项技术解决的问题,就能够很轻松的理解这项技术的设计根本,从而更好地理解与使用这项技术. 消息中间件和RPC从根本上来说都是为了解决分布式系统的 ...

  2. 007. 服务间通信 RPC & REST over HTTP(s) & 消息队列

    服务间通信 服务间通信的几种方式: RPC.REST over HTTP(s).消息队列.  https://www.jianshu.com/p/2a01d4383d0b RPC https://bl ...

  3. SpringCloud使用Feign实现服务间通信

    SpringCloud的服务间通信主要有两种办法,一种是使用Spring自带的RestTemplate,另一种是使用Feign,这里主要介绍后者的通信方式. 整个实例一共用到了四个项目,一个Eurek ...

  4. Spring Cloud netflix feign【服务间通信】

    一.简介 1,进程间通讯的本质是交换消息 2,服务间通信的两种方式 (1)RESTFul风格 (2)RPC风格 (3)两种风格的比较 3.基于RESTFul风格服务调用模型 4.基于Spring Cl ...

  5. gRPC-微服务间通信实践

    微服务间通信常见的两种方式 由于微服务架构慢慢被更多人使用后,迎面而来的问题是如何做好微服务间通信的方案.我们先分析下目前最常用的两种服务间通信方案. gRPC(rpc远程调用) 场景:A服务主动发起 ...

  6. CAP-微服务间通信实践

    微服务间通信常见的两种方式 由于微服务架构慢慢被更多人使用后,迎面而来的问题是如何做好微服务间通信的方案.我们先分析下目前最常用的两种服务间通信方案. gRPC(rpc远程调用) gRPC-微服务间通 ...

  7. .NET Core使用gRPC打造服务间通信基础设施

    一.什么是RPC rpc(远程过程调用)是一个古老而新颖的名词,他几乎与http协议同时或更早诞生,也是互联网数据传输过程中非常重要的传输机制. 利用这种传输机制,不同进程(或服务)间像调用本地进程中 ...

  8. 使用gRPC打造服务间通信基础设施

    一.什么是RPC rpc(远程过程调用)是一个古老而新颖的名词,他几乎与http协议同时或更早诞生,也是互联网数据传输过程中非常重要的传输机制. 利用这种传输机制,不同进程(或服务)间像调用本地进程中 ...

  9. .NET Core微服务开发服务间调用篇-GRPC

    在单体应用中,相互调用都是在一个进程内部调用,也就是说调用发生在本机内部,因此也被叫做本地方法调用:在微服务中,服务之间调用就变得比较复杂,需要跨网络调用,他们之间的调用相对于与本地方法调用,可称为远 ...

随机推荐

  1. Windows下搭建Apache网站

    目录 Apache下载 Apache安装 httpd.conf文件格式说明 启动服务并测试 Apache下载 在Apache官网底部找到APACHE PROJECT LIST里的HTTP Server ...

  2. HashMap(二)之面试题系列

    定义类考题 什么是Hash?什么是HashMap? HashMap 的工作原理是什么 HashMap HashTable的区别 为什么要用HashMap 源码类考题 什么是hash碰撞,怎么减少碰撞, ...

  3. Netty 中的内存分配浅析

    Netty 出发点作为一款高性能的 RPC 框架必然涉及到频繁的内存分配销毁操作,如果是在堆上分配内存空间将会触发频繁的GC,JDK 在1.4之后提供的 NIO 也已经提供了直接直接分配堆外内存空间的 ...

  4. [BZOJ]最长道路

    题目   点这里看题目.    BZOJ 上是权限题目. 分析   这道题可以用点分治,但是我就是喜欢边分治 QAQ .   分治过程中,我们考虑经过分治边的路径的最大痛苦值.一条经过分治边的路径会被 ...

  5. @bzoj - 3148@ 没头脑和不高兴

    目录 @description@ @solution@ @part 1:期望@ @part 2:方差@ @accepted code@ @details@ @description@ 没头脑和不高兴是 ...

  6. 第m大的身份证号码(局部排序代全局、结构体排序)

    第m大的身份证号码(点击) 时间限制: 1 Sec  内存限制: 128 MB                                                             ...

  7. Nirvana【思维+暴力优化】

    Nirvana 题目链接(点击) Kurt reaches nirvana when he finds the product of all the digits of some positive i ...

  8. (二)groupId和artifactId

    groupId一般分为多个段,这里我只说两段,第一段为域,第二段为公司名称. 域又分为org.com.cn等等许多,其中org为非营利组织,com为商业组织. 举个apache公司的tomcat项目例 ...

  9. Elasticsearch去重查询/过滤重复数据(聚合)

    带家好,我是马儿,这次来讲一下最近遇到的一个问题 我司某个环境的es中被导入了重复数据,导致查询的时候会出现一些重复数据,所以要我们几个开发想一些解决方案,我们聊了聊,相出了下面一些方案: 1.从源头 ...

  10. 在tp5.1中获取所有控制器的文件名和所有控制器下的方法名

    我在这块做的是下拉框改变控制器然后ajax去调用获取方法 上代码 private function redController(){//获取当前控制器目录下所有的文件名 $arr=scandir('. ...