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思路。
年都過完了,小夥伴們一個一個都拖着疲憊的身體在上班了吧,筆者也深有同感啊,因此本篇文字語義也略顯雜亂,包涵一下啦,收心吧,新的一年開始了,你們一塊兒加油,一塊兒奮鬥!
 
感謝閱讀!
相關文章
相關標籤/搜索