前言

我们都知道,API网关是工作在应用层上网关程序,为何要这样设计呢,而不是将网关程序直接工作在传输层、或者网络层等等更底层的环境呢?让我们先来简单的了解一下TCP/IP的五层模型。
 
 
 
具体的每一层的工作原理想必大家都已经滚瓜烂熟了,笔者也不在重复的复述这内容。回到上面的问题,为何API网关需要工作在应用层上的问题就变得一目了然,物理层面的网关是交给物理设备进行的,例如物理防火墙,而HTTP是网络通信中已经完全规范化和标准化的应用层协议,随处可见的通信协议,当然,你把网关集成到FTP上面也可以,增加相应的协议转换处理即可。
回过头来,RPC是什么,是一个协议吗?不是。确切的说它只是“远程调用”的一个名称的缩写,并不是任何规范化的协议,也不是大众都认知的协议标准,我们更多时候使用时都是创建的自定义化(例如Socket,Netty)的消息方式进行调用,相比http协议,我们省掉了不少http中无用的消息内容,例如headers消息头。本一个简单的GET请求,返回一个hello world的请求和响应,元数据就10个字节左右,但是加上headers消息头等等http的标准内容,估计会膨胀到25~30个字节,下面是一个常见的http的headers消息头。
 
. Accept:*/*
2. Accept-Encoding:gzip, deflate
3. Accept-Language:zh-CN,zh;q=0.9,en;q=0.8
4. Cache-Control:no-cache
5. Connection:keep-alive
6. Cookie:.AUTH=A24BADC9D552CF1157B7842F2A6C159A681CA330DBB449568896FAC839CFEE51F42973C9A5B9F632418FB82C128A8BF612D27C2EE7DABDE985E9A79DF19A955FFED9E8219853FB90574B0990DD29B2B7ED23A7C26B8AD1934870B8C0FCB4F577636E267003E6D214D9B319A4739D3716E2A8299C35E228F96EC12A29CCDE83A7D2D3B24EE6A84CF2D69D81A44E0F46EC9B112BDAA9FC0E0943DB36C1449FD79E6D5A123E5D182D2C3A03D4049CBD76947D33EB5DCCE82CB1C91ACACD83B6D07F19A6629732FA16D08443450DC2937C7CEF6A2FAE941760C79064C7A5A67E844ABDA2DE89E5B10F3B30B8A89CEDE9C00A3C79711D
7. Host:erp-dev.holderzone.cn:90
8. Pragma:no-cache
9. Referer:http://erp-dev.holderzone.cn:90/
10. User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
 
因此很多系统内部调用仍然采用自定义化的RPC调用模式进行通信,毕竟速度和性能是内网的关键指标之一,而标准化和语义无关性在外网中举足轻重。所以,为何API网关无法工作在RPC上,因为它没有一个像HTTP/HTTPS那样的通用标准,需要我们将标准化的协议转为为自定义协议的处理,通常称为Relay,如图所示。
 
 

 
上一篇中,我们已经简单的介绍了Ocelot在Http中的网关实现,无需任何修改,全都可以在配置文件中完成,相当方便。但是,我们需要实现自定义的RPC协议时,应该怎么办呢?
这里,感谢张队在上一篇中提供建议和思路https://www.cnblogs.com/SteveLee/p/Ocelot_Api_http_and_https.html#4171964,可以通过增加(或扩展)OcelotMiddleware来处理下游协议的转换。
 
 
 

Ocelot下游中间件扩展

我们知道,在Ocelot网关中,所有操作均通过中间件来进行过滤和处理,而多个中间件之间的相互迭代通信便形成了Ocelot的通信管道,源码中使用OcelotPipelineConfiguration来扩展和配置更多的Ocelot中间件,见源码所示:

 
在源码中,我们可以看到,所有的中间件对应操作对象的均是DownstreamContext下游上下文对象。而MapWhenOcelotPipeline正好可以满足我们扩展中间件的需求,它提供List<Func<IOcelotPipelineBuilder, Func<DownstreamContext, bool>>>委托以供我们配置多个下游处理中间件并映射到Ocelot管道构建器中。我们查看DownstreamContext的源码,可以看到,构建下游上下文的时候,默认就传递了HttpContext对象,而通过DownstreamRequest和DownstreamResponse完成对下游的请求和响应接收。
 
 
这样,我们便可以通过对OcelotPipelineConfiguration的扩展来添加自定义中间件,我们把它扩展名称定义为OcelotPipelineConfigurationExtensions吧。
 
namespace DotEasy.Rpc.ApiGateway
{
public static class OcelotPipelineConfigurationExtensions
{
public static void AddRpcMiddleware(this OcelotPipelineConfiguration config)
{
config.MapWhenOcelotPipeline.Add(builder => builder.AddRpcMiddleware());
}
private static Func<DownstreamContext, bool> AddRpcMiddleware(this IOcelotPipelineBuilder builder)
{
builder.UseHttpHeadersTransformationMiddleware();
builder.UseDownstreamRequestInitialiser();
builder.UseRateLimiting();
builder.UseRequestIdMiddleware();
builder.UseDownstreamUrlCreatorMiddleware();
builder.UseRpcRequesterMiddleware();
return context => context.DownstreamReRoute.DownstreamScheme.Equals("tcp", StringComparison.OrdinalIgnoreCase);
}
}
}
 
当有了DownstreamContext的扩展定义,而且在下游配置中,我们需要指定的配置协议是tcp,那么我们便可以开始实现这个扩展的中间件了,我们把中间件的名称定义为RelayRequesterMiddleware。
 
using Ocelot.Middleware.Pipeline;

namespace DotEasy.Rpc.ApiGateway
{
public static class RpcRequesterMiddlewareExtensions
{
public static void UseRpcRequesterMiddleware(this IOcelotPipelineBuilder builder)
{
builder.UseMiddleware<RelayRequesterMiddleware>();
}
}
}
using System;
using System.Net;
using System.Threading.Tasks;
using Ocelot.Logging;
using Ocelot.Middleware; namespace DotEasy.Rpc.ApiGateway
{
public class RelayRequesterMiddleware : OcelotMiddleware
{
private readonly OcelotRequestDelegate _next;
private readonly IOcelotLogger _logger; public RelayRequesterMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory) : base(loggerFactory .CreateLogger<RelayRequesterMiddleware>())
{
_next = next;
_logger = loggerFactory.CreateLogger<RelayRequesterMiddleware>();
} public async Task Invoke(DownstreamContext context)
{
var httpContent = ... // TODO:协议转换处理等操作
context.DownstreamResponse = new DownstreamResponse(httpContent, HttpStatusCode.OK, context.DownstreamRequest.Headers);
await
_next.Invoke(context);
}
}
}
 
上面加粗的代码便是下游拦截的主要处理地方,在这里我们便可以使用http转rpc的协议转换处理。当然,在Ocelot的使用配置中,我们需要对该Middleware中间件进行添加。

app.UseOcelot(pipelineConfiguration => pipelineConfiguration.AddRpcMiddleware()).Wait();

以上便完成了对Ocelot中DownstreamContext的扩展,

总结下来,当我们需要扩展下游协议时,我们需要手动配置OcelotPipelineConfiguration并添加到IOcelotPipelineBuilder中,然后通过扩展IOcelotPipelineBuilder实现下游中间件的自定义处理。

手动协议转换

其实到上面这一小节,相信很多朋友都可以实现自定义的下游拦截和处理了,而本节的介绍,只是针对在Doteasy.RPC中如何进行协议转换的一个参考。
 
我们先看一组http中的URL:http://127.0.0.1:8080/api/values,然后再看看tcp中的URL:tcp://127.0.0.1:8080/api/values。有什么区别吗?没有任何区别,唯一的区别是scheme从http转为tcp。而在rpc过程调用中,一般我们是没有“绝对路径+谓词”的方式去识别服务节点的,一般都是tcp://127.0.0.1:8080,而具体指定的服务节点交给注册中心去完成,也就是通常所说的服务发现。
由于Doteasy.RPC内部并未实现如“<scheme>://<username>:<password>@<host>:<port>/<path>......”这样标准化的统一定位,所以笔者的做法是将RPC的客户端集成到Ocelot宿主中,让它替代DownstreamConext下游的请求和响应,通过扩展反射的方式实现所有代理的生成,并根据谓词和参数进行方法的调用,这样,代码就不需要做太多的修改。
 
var httpContent = relayHttpRouteRpc.HttpRouteRpc(ClientProxy.GenerateAll(new Uri("http://127.0.0.1:8500")),
new Uri(context.DownstreamRequest.ToUri()),
context.DownstreamRequest.Headers); // 目前尚未处理Headers消息头
 
首先需要明白这样做的一个目的
  1. 在Doteasy.RPC单次调用中,为了减少众多接口生成代理所带来的耗时,每次调用前都会检查相关接口的动态代理是否已经生成,确保每次只生成一个片段的代理。然而,作为一个网关中的中继器,这样一次生成一个代码片段显得非常无力,需要将所有的接口全部生成代理,以方便在Relay中查找和调用。
  2. 再看一个RESTful风格中的URL:http://127.0.0.1:8080/api/1/Sync,一般我们将谓词放置最后,而参数一般放置在谓词的前面,在手动转换RPC的过程中,就可以利用谓词来假设我们需要调用的RPC服务名称(但实际不一定就是Sync)。
  3. 基于Doteasy.RPC中的服务容器,可以很方便的实现参数类型转换以及后期的Headers处理。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using DotEasy.Rpc.Core.Runtime.Client;
using DotEasy.Rpc.Core.Runtime.Communally.Convertibles;
using Newtonsoft.Json; namespace DotEasy.Rpc.Core.ApiGateway.Impl
{
public class DefaultRelayHttpRouteRpc : IRelayHttpRouteRpc
{
private IRemoteInvokeService _remoteInvokeService;
private ITypeConvertibleService _typeConvertibleService; public DefaultRelayHttpRouteRpc(IRemoteInvokeService remoteInvokeService, ITypeConvertibleService typeConvertibleService)
{
_remoteInvokeService = remoteInvokeService;
_typeConvertibleService = typeConvertibleService;
} public StringContent HttpRouteRpc(List<dynamic> proxys, Uri urlPath, HttpRequestHeaders headers)
{
foreach (var proxy in proxys)
{
Type type = proxy.GetType();
if (!urlPath.Query.Contains("scheme=rpc")) continue; var predicate = urlPath.AbsolutePath.Split('/');
var absName = predicate[predicate.Length - ];
var absPars = predicate[predicate.Length - ]; if (!type.GetMethods().Any(methodInfo => methodInfo.Name.Contains(absName))) continue; var method = type.GetMethod(absName);
if (method != null)
{
var parameters = method.GetParameters();
var parType = parameters[].ParameterType; // only one parameter
var par = _typeConvertibleService.Convert(absPars, parType); var relayScriptor = new RelayScriptor {InvokeType = type, InvokeParameter = new dynamic[] {par}}; var result = method.Invoke(
Activator.CreateInstance(relayScriptor.InvokeType, _remoteInvokeService, _typeConvertibleService),
relayScriptor.InvokeParameter); var strResult = JsonConvert.SerializeObject(result);
return new StringContent(strResult);
}
} return null;
}
}
}
 
笔者的转换方式是将谓词作为服务名称和参数值进行调用,虽然这种方式目前来看十分拙劣,但为自定义转换提供了一组思路,还可以不断的优化和调整,目前缺点如下:
  1. 当http中多个参数时,无法进行协议转换,因为不知道代理目标方法的参数集合是多少,只有全局假设一对一的参数目标。
  2. RPC客户端在网关中集成了大量的代理生成,无法实现动态更新,例如原来手动替换DLL,接口自动更新动态代理。
  3. 每一次调用都需要从大量的代理中查找指定(或模糊匹配)的方法名称,如果存在1KW+的接口名称,这个查找将是一个非常严重的瓶颈。

总结

世上没有100%完美的事物,所以才有各种各样的手段,这里笔者在Doteasy.RPC和Ocelot的基础上做了一个简单下游协议转换,有兴趣的朋友可以自行实现自己想要的协议转换。再次感谢张队提供的Ocelot手动转RPC思路。
年都过完了,小伙伴们一个一个都拖着疲惫的身体在上班了吧,笔者也深有同感啊,所以本篇文字语义也略显杂乱,包涵一下啦,收心吧,新的一年开始了,大家一起加油,一起奋斗!
 
感谢阅读!

NET Core微服务之路:基于Ocelot的API网关Relay实现--RPC篇的更多相关文章

  1. .NET Core微服务之路:文章系列和内容索引汇总 (v0.52)

    微服务架构,对于从事JAVA架构的童鞋来说,早已不是什么新鲜的事儿,他们有鼎鼎大名的Spring Cloud这样的全家桶框架支撑,包含微服务核心组件如 1. Eureka:实现服务注册与发现. 2. ...

  2. .NET Core微服务之路:不断更新中的目录 (v0.43)

    原文:.NET Core微服务之路:不断更新中的目录 (v0.43) 微服务架构,对于从事JAVA架构的童鞋来说,早已不是什么新鲜的事儿,他们有鼎鼎大名的Spring Cloud这样的全家桶框架支撑, ...

  3. NET Core微服务之路:实战SkyWalking+Exceptionless体验生产环境下的追踪系统

    前言 当一个APM或一个日志中心实际部署在生产环境中时,是有点力不从心的. 比如如下场景分析的问题: 从APM上说,知道某个节点出现异常,或延迟过过高,却不能及时知道日志反馈情况,总不可能去相应的节点 ...

  4. NET Core微服务之路:实战SkyWalking+Exceptionless体验生产下追踪系统

    原文:NET Core微服务之路:实战SkyWalking+Exceptionless体验生产下追踪系统 前言 当一个APM或一个日志中心实际部署在生产环境中时,是有点力不从心的. 比如如下场景分析的 ...

  5. .NET Core微服务之基于Ocelot实现API网关服务

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.啥是API网关? API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口.这样就可以明显的简化客户端 ...

  6. .NET Core微服务之基于Ocelot实现API网关服务(续)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.负载均衡与请求缓存 1.1 负载均衡 为了验证负载均衡,这里我们配置了两个Consul Client节点,其中ClientServic ...

  7. 基于.net core微服务(Consul、Ocelot、Docker、App.Metrics+InfluxDB+Grafana、Exceptionless、数据一致性、Jenkins)

    1.微服务简介 一种架构模式,提倡将单一应用程序划分成一组小的服务,服务之间互相协调.互相配合,为用户提供最终价值.每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(RESTfu ...

  8. NET Core微服务之路:自己动手实现Rpc服务框架,基于DotEasy.Rpc服务框架的介绍和集成

    本篇内容属于非实用性(拿来即用)介绍,如对框架设计没兴趣的朋友,请略过. 快一个月没有写博文了,最近忙着两件事;    一:阅读刘墉先生的<说话的魅力>,以一种微妙的,你我大家都会经常遇见 ...

  9. .NET Core微服务之路:基于gRPC服务发现与服务治理的方案

    重温最少化集群搭建,我相信很多朋友都已经搭建出来,基于Watch机制也实现了出来,相信也有很多朋友有了自己的实现思路,但是,很多朋友有个疑问,我API和服务分离好了,怎么通过服务中心进行发现呢,这个过 ...

随机推荐

  1. Xshell 连接虚拟机出现 "The remote SSH server rejected X11 forwarding request"

    1. 描述 虚拟机:VirtualBox Linux: centOS7 解决了 centOS7在VirtualBox中装好后的网络连接问题 后,用 Xshell 连接服务器时出现下面情况: 2. ss ...

  2. 初学ASP.NET 知识点

    1.C# 中的虚方法 和 C++中的作用一致,能让指向子类的父类指针优先到子类中寻找方法,而不是直接调用父类中的方法. 看一段例子回忆下: namespace ConsoleApplication1 ...

  3. 洛谷P4770 [NOI2018]你的名字 [后缀自动机,线段树合并]

    传送门 思路 按照套路,直接上后缀自动机. 部分分:\(l=1,r=|S|\) 首先把\(S\)和\(T\)的后缀自动机都建出来. 考虑枚举\(T\)中的右端点\(r\),查询以\(r\)结尾的串最长 ...

  4. Linux-SFTP/SSH免密码,钥匙登录

    1.原理 登录的客户端要有私钥,被登录的服务器要保存客户端生成的公钥. 2.修改服务器/etc/ssh/sshd_config设置,如下: sudo vi /etc/ssh/sshd_config P ...

  5. bootstrap学习(一)栅格、布局

    栅格: 栅格将整个屏幕分为12份,当使用单一的一组 .col-md-* 栅格类,就可以创建一个基本的栅格系统,在手机和平板设备上一开始是堆叠在一起的(超小屏幕到小屏幕这一范围),在桌面(中等)屏幕设备 ...

  6. 2018-2019-2 网络对抗技术 20165323 Exp6 信息搜集与漏洞扫描

    一.实验内容 二.实验步骤 1.各种搜索技巧的应用 2.DNS IP注册信息的查询 3.基本的扫描技术 主机发现 端口扫描 OS及服务版本探测 具体服务的查点 4.漏洞扫描 三.实验中遇到的问题 四. ...

  7. MySQL5.7 关键字和保留关键字

    来源:https://dev.mysql.com/doc/refman/5.7/en/keywords.html 标有(R)的为保留关键字. A ACCESSIBLE (R) ACCOUNT; add ...

  8. C#学习-类型转换

    类型转换的方式主要有以下几种: 隐式类型转换,由低级别类型向高级类型的转换过程.例如派生类可以隐式地转换为它的父类,装箱过程就属于这种隐式类型转换. 显式类型转换,也叫强制类型转换, 通过is和as运 ...

  9. (转载)C# 枚举 FlagsAttribute用法

    这是读过几篇文章后发现整理的最完整的一篇文章 转载地址:枚举特性FlagsAttribute的用法 先看官方的解释:指示可以将枚举作为位域(即一组标志)处理. 看起来并不好理解,到底什么是作为位域处理 ...

  10. python+adb实现自动化获取手机信息

    首先我们先看一下使用adb查看Android手机信息的指令 #获取手机名称NAME = 'adb shell getprop ro.product.model'#获取手机版本VERSION = 'ad ...