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

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

前言

咱们都知道,API网关是工做在应用层上网关程序,为什么要这样设计呢,而不是将网关程序直接工做在传输层、或者网络层等等更底层的环境呢?让咱们先来简单的了解一下TCP/IP的五层模型。
 
 
 
具体的每一层的工做原理想必你们都已经倒背如流了,笔者也不在重复的复述这内容。回到上面的问题,为什么API网关须要工做在应用层上的问题就变得一目了然,物理层面的网关是交给物理设备进行的,例如物理防火墙,而HTTP是网络通讯中已经彻底规范化和标准化的应用层协议,随处可见的通讯协议,固然,你把网关集成到FTP上面也能够,增长相应的协议转换处理便可。
回过头来,RPC是什么,是一个协议吗?不是。确切的说它只是“远程调用”的一个名称的缩写,并非任何规范化的协议,也不是大众都认知的协议标准,咱们更多时候使用时都是建立的自定义化(例如Socket,Netty)的消息方式进行调用,相比http协议,咱们省掉了很多http中无用的消息内容,例如headers消息头。本一个简单的GET请求,返回一个hello world的请求和响应,元数据就10个字节左右,可是加上headers消息头等等http的标准内容,估计会膨胀到25~30个字节,下面是一个常见的http的headers消息头。
 
1. 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的扩展,html

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

 

手动协议转换

其实到上面这一小节,相信不少朋友均可以实现自定义的下游拦截和处理了,而本节的介绍,只是针对在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 - 1];
                var absPars = predicate[predicate.Length - 2];

                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[0].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思路。
年都过完了,小伙伴们一个一个都拖着疲惫的身体在上班了吧,笔者也深有同感啊,因此本篇文字语义也略显杂乱,包涵一下啦,收心吧,新的一年开始了,你们一块儿加油,一块儿奋斗!
 
感谢阅读!
相关文章
相关标签/搜索