前言
我們都知道,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的擴展,
總結下來,當我們需要擴展下游協議時,我們需要手動配置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消息頭
首先需要明白這樣做的一個目的
-
在Doteasy.RPC單次調用中,為了減少眾多接口生成代理所帶來的耗時,每次調用前都會檢查相關接口的動態代理是否已經生成,確保每次只生成一個片段的代理。然而,作為一個網關中的中繼器,這樣一次生成一個代碼片段顯得非常無力,需要將所有的接口全部生成代理,以方便在Relay中查找和調用。
-
再看一個RESTful風格中的URL: http://127.0.0.1:8080/api/1/Sync,一般我們將謂詞放置最后,而參數一般放置在謂詞的前面,在手動轉換RPC的過程中,就可以利用謂詞來假設我們需要調用的RPC服務名稱(但實際不一定就是Sync)。
-
基於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; } } }
筆者的轉換方式是將謂詞作為服務名稱和參數值進行調用,雖然這種方式目前來看十分拙劣,但為自定義轉換提供了一組思路,還可以不斷的優化和調整,目前缺點如下:
-
當http中多個參數時,無法進行協議轉換,因為不知道代理目標方法的參數集合是多少,只有全局假設一對一的參數目標。
-
RPC客戶端在網關中集成了大量的代理生成,無法實現動態更新,例如原來手動替換DLL,接口自動更新動態代理。
-
每一次調用都需要從大量的代理中查找指定(或模糊匹配)的方法名稱,如果存在1KW+的接口名稱,這個查找將是一個非常嚴重的瓶頸。
總結
世上沒有100%完美的事物,所以才有各種各樣的手段,這里筆者在Doteasy.RPC和Ocelot的基礎上做了一個簡單下游協議轉換,有興趣的朋友可以自行實現自己想要的協議轉換。再次感謝張隊提供的Ocelot手動轉RPC思路。
年都過完了,小伙伴們一個一個都拖着疲憊的身體在上班了吧,筆者也深有同感啊,所以本篇文字語義也略顯雜亂,包涵一下啦,收心吧,新的一年開始了,大家一起加油,一起奮斗!
感謝閱讀!
