NET Core微服务之路:自己动手实现Rpc服务框架,基于DotEasy.Rpc服务框架的介绍和集成...
本篇內容屬于非實用性(拿來即用)介紹,如對框架設計沒興趣的朋友,請略過。
?
快一個月沒有寫博文了,最近忙著兩件事; ?? 一:閱讀劉墉先生的《說話的魅力》,以一種微妙的,你我大家都會經常遇見的事物,來建議說話的“藝術和魅力”,對于我們從事軟件開發、不太善于溝通和表達的朋友來說,也算是一項軟技能了,推薦喜歡閱讀的朋友有時間閱讀,給你不一樣的閱讀體驗。 二:編寫基于Net?Core的Rpc框架。之前有朋友說如何將Rpc等整個體系集成到dotnet框架中,我想這篇博文會給你一個答案。 哦,對了,我不建議直接將代碼直接復制下來然后去運行的朋友,因為這樣你達不到學習的目的,也違背了筆者的初衷。謝謝理解。?
一:簡單回顧一下之前的介紹
繼續貼上之前的一張圖片
?
?
根據上面圖,服務化原理可以分為3步:二:DotEasy.Rpc框架介紹
單論Rpc框架市場,且不論Java上的Spring?Boot和Spring?Cloud這樣大名鼎鼎的開源框架,目前Net上的Rpc整合性框架確實并不多,我們Net程序員也要混口飯吃,不能總被Java甩掉好幾條街吧。 言歸正傳,一個遠程過程調用,會涉及到如下幾個方面的技術點(功能):?
2.1 解決方案介紹:
整體解決方案不做過多介紹,相信單詞的詞義已經表達了這個項目的作用。 小插曲:筆者曾用Easy.Rpc做為項目的主庫名稱,但上傳到nuget后才發現,原來也有名為easy.rpc的包,不過筆者并沒找到這個開源作者的網站,無賴之下,想到“點”這個詞,也是dotnet的dot開頭部分,索性干脆也叫DotEasy了吧。 筆者的目的,是想通過這個框架(或類庫集),來簡化微服務的部署和開發,讓希望從事微服務NET開發的朋友不再找不到從何入手的窘境。 在Nuget.org上能直接下載編譯過的包,地址:https://www.nuget.org/packages/DotEasy.Rpc/,推薦使用這種方式進行安裝。 如果喜歡研究源碼,筆者同樣也貼上地址:https://github.com/steveleeCN87/doteasy.rpc,不過源碼不包含任何依賴,如果編譯中出現版本問題,可聯系筆者。目前Github和Nuget上就放上了DotEasy.Rpc核心庫和DotEasy.Rpc.Consul擴展包,etcd和entry還在測試階段,就不方便拿出來獻丑了。o(∩_∩)o 哈哈 當然,也歡迎廣大愛好開源的朋友加入,共同為NET開源項目做貢獻。?
2.2 主項目介紹:
Attributes:用于標注該接口為某一特性的方法體,當系統通過Microsoft.Extensions.DependencyInjection自動構建成功后,框架將自動掃描接口上標注過[RpcTagBundle]的接口,形成目標接口列表,以供下面的方法調用。
Core:該核心分為Server和Client以及核心通用三個部分組成:
? Client:主要用于通過Ip地址遠程調用的方法Invoke實現,以及遠程服務端的檢查檢查實現。
? Communally:通用方法庫,包括唯一ID生成器(用于標識每次請求所產生的唯一客戶端)、通用對象轉換器(將復雜對象轉換為喜歡默認對象)、異常處理器、序列化生成器。
? Server:提供服務宿主,服務執行者、服務入口處理、服務管理、服務定位、服務管理工廠的接口及默認實現。
Proxy:運行時預生成及客戶端動態代理模組的接口和方法實現。
Routing:提供地址定位、服務描述、服務路由的接口和方法實現。
Transport:基于protobuf-net和dotnettey構建的二進制序列化、和通信管道和宿主的接口和實現。
所有主要類均已接口的形式提供接口名稱,和已默認實現的具體方法體(虛方法),方便在這個基礎上進行擴展和重寫。
?
2.3 主項目接口
本節簡單介紹一下DotEasy.Rpc主框架的接口的作用。
Attribute: |---RpcTagBundleAttribute.cs Core: |---Client: |-------Address: |-----------IAddressResolver.cs |-------IRemoteInvokeService.cs |---Server: |-------IServiceEntryFactory.cs |-------IServiceEntryLocate.cs |-------IServiceEntryManager.cs |-------IServiceEntryProvider.cs |-------IServiceExecutor.cs |-------IServiceHost.cs Proxy: |---IServiceProxyFactory.cs |---IServiceProxyGenerater.cs Routing: |---IServiceRouteFactory.cs |---IServiceRouteManager.cs Transport: |-------Codec: |-----------ITransportMessageCodecFactory.cs |-----------ITransportMessageDecoder.cs |-----------ITransportMessageEncoder.cs |---IMessageListener.cs |---IMessageSender.cs RpcTagBundleAttribute.cs:所有標記過[RpcTagBundle]特性的接口均會被掃描至doteasy.rpc框架中; IAddressResolver.cs:地址解析器,提供IPv4地址解析作用,用于IServiceRouteFactory和IRemoteInvokeService定位操作; IRemoteInvokeService.cs:遠程調用服務接口,提供遠程服務調用的關鍵接口,通過IServiceProxyFactory接口代理調用; IServiceEntryFactory.cs:服務入口工廠接口,對全局服務入口的統一的工廠操作,例如添加,監聽,移除,修改服務入口等; IServiceEntryLocate.cs:服務入口定位接口,通過IAddressResolver過濾和解析,實現服務入口的定位; IServiceEntryManager.cs:服務入口管理全局管理接口,功能同IServiceEntryFactory相似,但提供更多的服務管理操作,比如負載均衡(采用輪詢實現); IServiceEntryProvider.cs:服務入口提供者接口,一個簡單的服務入口提供者程序; IServiceExecutor.cs:執行服務方法接口,執行遠程服務的IRemoteInvokeService; IServiceHost.cs:服務宿主接口,DotNetty的服務宿主,類似own框架的自宿主程序,提供請求和響應操作; IServiceProxyFactory.cs:服務代理工廠接口,客戶端代理(預編譯)的所有操作實現; IServiceProxyGenerater.cs:服務代理生成接口,客戶端代理(預編譯)生成器; IServiceRouteFactory.cs:服務路由工廠接口; IServiceRouteManager.cs:服務路由全局管理接口; ITransportMessageCodecFactory.cs:管道消息傳輸工廠接口; ITransportMessageDecoder.cs:管道消息解碼器接口; ITransportMessageEncoder.cs:管道消息編碼器接口; IMessageListener.cs:管道消息監聽接口,可實現一個消息的接受者和處理程序; IMessageSender.cs:管道消息發送接口,可實現一個消息的發送者; 通過上面的框架和?就能實現客戶端到服務端的RPC通信了嗎?當然不是,還需要服務注冊中心(例如Consul,etcd,zookeeper)來實現。上面只是提供了路由轉發,服務定位,客戶端預編譯的實現等等功能而已,而服務的注冊并沒提供,因為它不屬于基礎框架的范疇,筆者對zookeeper的笨重太反感(當然不是說它不好),而consul和etcd十分輕量級,特此又專門新增了兩個項目:DotEasy.Rpc.Consul和DotEasy.Rpc.Etcd,用于實現不同注冊中心的注冊(獲取)方法,和健康檢測機制。 當然,介紹Consul和Etcd如何實現不是本節的重點,DotEasy.Rpc這個框架的完全剖析也將在日后新開篇章中專門介紹如何去實現一個框架,想必大部分朋友關心的是這個框架能做什么,有什么樣的功能,那么,接下來開始吧。?
三:如何使用
本系列一直重復的那張圖片,噼里啪啦噼里啪啦......此處省略三百字。繞來繞去,難以入手,正如上一篇有朋友推薦如何在Asp.net core中集成、等等。 功能和特性如下:3.1?建立服務接口和服務實現
既然是服務,那么肯定需要以接口interface的方式實現對外暴露,并且,接口的實現不能和接口封裝在同一個DLL中,不然還叫什么遠程過程調用呢(RPC)呢,如果不了解什么叫接口,去翻一翻C#語言規范。 先定義一個接口,接口方法簽名如下: namespace doteasy.rpc.interfaces {[RpcTagBundle]public interface IUserService{Task<string> GetUserName(int id);Task<bool> Exists(int id);Task<int> GetUserId(string userName);Task<DateTime> GetUserLastSignInTime(int id);Task<IDictionary<string, string>> GetDictionary();Task TryThrowException();} }?
很簡單,不解釋。 其中接口上有個重要特性叫[RpcTagBundle],該特性只允許標記在interface接口上,用于啟動時方便框架掃描所有標有該特性的接口。 在看實現類 namespace doteasy.rpc.implement {public class UserService : IUserService{public Task<string> GetUserName(int id){return Task.FromResult($"我傳了一個int數字{id}.");}public Task<bool> Exists(int id){return Task.FromResult(true);}public Task<int> GetUserId(string userName){return Task.FromResult(1);}public Task<DateTime> GetUserLastSignInTime(int id){return Task.FromResult(DateTime.Now);}public Task<IDictionary<string, string>> GetDictionary(){return Task.FromResult<IDictionary<string, string>>(new Dictionary<string, string> { { "key", "value" } });}public Task TryThrowException(){throw new Exception("嘗試拋出異常!");}} }?
再次重申:注意兩篇代碼中的命名空間,就是兩個不同的程序集(兩個項目),千萬不能以為筆者僅僅是為了區分而已。3.2?建立asp.net core?mvc應用程序
新建一個asp.net core mvc應用程序,模版默認webapi,添加一個控制器HeathController,當然,控制器名稱你也可以自由發揮,鍵入如下代碼: namespace doteasy.rpc.webserver.Controllers {[Produces("application/json")][Route("api/Health")]public class HealthController : Controller{[HttpGet]public IActionResult Get() => Ok("ok");} }?
也十分簡單,對外暴露一個路由地址為"api/Health"的API接口,提供GET方法,返回OK信息。 這個接口的作用是用于Consul對服務的健康狀態檢查回調地址,Consul可以基于HTTP做健康檢查,也可以通過gRPC驗證服務健康狀態。 至此,一個WebApi就建立完成,對外不在通過HTTP做任何接口暴露。 接下來我們添加一個IApplicationBuilder的擴展,用于啟動Rpc服務端,代碼如下: using System; using doteasy.rpc.implement; using doteasy.rpc.interfaces; using DotEasy.Rpc.Entry; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;namespace doteasy.rpc.webserver {public static class ConsulServerExtensions{public static IApplicationBuilder UseConsulServerExtensions(this IApplicationBuilder app, IConfiguration configuration){if (app == null) throw new ArgumentNullException(nameof(app));BaseServer baseServer = new BaseServer(configuration); //(1)baseServer.RegisterEvent += collection => collection.AddTransient<IUserService, UserService>(); //(2)baseServer.Start(); //(3)return app;}} } (1):實例化一個BaseServer的服務類型,并使用IConfiguration作為參數。該BaseServer類是封裝太DotEasy.Rpc.Entry中的一個實現,主要是簡化調用者的代碼構建能力,不然,截圖一個部分源碼瞧瞧 框架的目的是簡化編碼工作,構建RPC實例也不例外,100多行的構建代碼只用3行實現,偷懶者必備。 (2):調用一次RegisterEvent委托事件,用于將接口和實現注冊到ServiceCollection容器中。 (3):啟動這個RPC服務,其實方法內還有構建ServiceCollection容器等等一大堆方法,你可以自己實現,也可以問筆者要源碼。 配置文件在哪兒,難道這樣就可以了? 當然不是,我們還需要一個appsettings.json的默認配置文件,代碼如下: {"Hosting.urls": "http://127.0.0.1:5000","Hosting.And.Rpc.Health.Check": "http://127.0.0.1:5000/api/health","Rpc": {"IP": "127.0.0.1","Port": 9881},"ServiceDescriptor": {"Name": "LZZ.DEV.ServerService"},"ConsulRegister": {"IP": "127.0.0.1","Port": 8500,"Timeout": 5} } 這篇配置文件很容易理解,這里不再重復啰嗦的解釋。 至此,一個寄宿于Asp.net core的rpc服務就這樣搭建完成,可以啟動隨Consul啟動看看。 友情提示:建議將Asp.net core的默認日志功能關閉,否則Consul會5秒發送一個健康檢查請求過來,日志會慢慢的變得十分臃腫,ConfigureLogging((context, logging) => { logging.ClearProviders(); })即可移除Logging日志功能。當然,你也可以做其他修改,畢竟日志在項目中非常重要。3.3?測試啟動Consul和Asp.net core
consul如何啟動這個,筆者就不再復述了吧,想必看過之前的文章,應該都會啟動和使用consul了,截個圖,喜悅一下 3.1中的六個接口全被注冊到consul服務中,不信,我們訪問一個具體接口,看看meta信息。 路由和服務均通過這個信息進行定位,我們可以知道,在127.0.0.1的9881端口上,可以訪問名為“doteasy.rpc.interfaces.IUserService.Exists_id”的接口。當然,目前是沒有任何驗證機制的,下一篇會介紹RPC中的統一驗證機制。3.4?建立一個客戶端
廢話不多說,直接上代碼: using System; using System.Threading.Tasks; using doteasy.rpc.interfaces; using DotEasy.Rpc.Entry;namespace DotEasy.Client {class Program : BaseClient{static void Main(){new TestClient();}}public class TestClient : BaseClient{public TestClient(){Task.Run(async () =>{var userService = Proxy<IUserService>();Console.WriteLine($"UserService.GetUserName:{await userService.GetUserName(1)}");Console.WriteLine($"UserService.GetUserId:{await userService.GetUserId("rabbit")}");Console.WriteLine($"UserService.GetUserLastSignInTime:{await userService.GetUserLastSignInTime(1)}");Console.WriteLine($"UserService.Exists:{await userService.Exists(1)}");Console.WriteLine($"UserService.GetDictionary:{(await userService.GetDictionary())["key"]}");}).Wait();}} }?
如你所見,我們就像在調用接口一樣的去調用了RPC遠程服務,中間的所有操作都是透明的,不需要關心的,唯一多了一句是Proxy<IUserService>();使用代理模式動態生成了RPC遠程客戶端,該操作又被筆者封裝在了BaseClient中,一切都為了使用者簡單。3.5?跑跑客戶端看看結果
info: DotEasy.Rpc.Core.Communally.Convertibles.Impl.DefaultTypeConvertibleService[0]發現了以下類型轉換提供程序:DotEasy.Rpc.Core.Communally.Convertibles.Impl.DefaultTypeConvertibleProvider info: DotEasy.Rpc.Core.Communally.IdGenerator.Impl.DefaultServiceIdGenerator[0]方法:System.Threading.Tasks.Task`1[System.String] GetUserName(Int32) 生成服務Id:doteasy.rpc.interfaces.IUserService.GetUserName_id info: DotEasy.Rpc.Core.Communally.IdGenerator.Impl.DefaultServiceIdGenerator[0]方法:System.Threading.Tasks.Task`1[System.Boolean] Exists(Int32) 生成服務Id:doteasy.rpc.interfaces.IUserService.Exists_id info: DotEasy.Rpc.Core.Communally.IdGenerator.Impl.DefaultServiceIdGenerator[0]方法:System.Threading.Tasks.Task`1[System.Int32] GetUserId(System.String) 生成服務Id:doteasy.rpc.interfaces.IUserService.GetUserId_userName info: DotEasy.Rpc.Core.Communally.IdGenerator.Impl.DefaultServiceIdGenerator[0]方法:System.Threading.Tasks.Task`1[System.DateTime] GetUserLastSignInTime(Int32) 生成服務Id:doteasy.rpc.interfaces.IUserService.GetUserLastSignInTime_id info: DotEasy.Rpc.Core.Communally.IdGenerator.Impl.DefaultServiceIdGenerator[0]方法:System.Threading.Tasks.Task`1[System.Collections.Generic.IDictionary`2[System.String,System.String]] GetDictionary() 生成服務Id:doteasy.rpc.interfaces.IUserService.GetDictionary info: DotEasy.Rpc.Core.Communally.IdGenerator.Impl.DefaultServiceIdGenerator[0]方法:System.Threading.Tasks.Task TryThrowException() 生成服務Id:doteasy.rpc.interfaces.IUserService.TryThrowException info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]準備為服務id:doteasy.rpc.interfaces.IUserService.GetUserName_id,解析可用地址 info: DotEasy.Rpc.Consul.ConsulServiceRouteManager[0]準備獲取所有路由配置。 info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]根據服務id:doteasy.rpc.interfaces.IUserService.GetUserName_id,找到以下可用地址:127.0.0.1:9881 info: DotEasy.Rpc.Core.Client.Implementation.RemoteInvokeService[0]使用地址:'127.0.0.1:9881'進行調用 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備為服務端地址:127.0.0.1:9881創建客戶端。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備發送消息。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備獲取Id為:0b720018feda4e4192937dfbb76eeb66的響應內容。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]消息發送成功。 UserService.GetUserName:我傳了一個int數字1. info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]準備為服務id:doteasy.rpc.interfaces.IUserService.GetUserId_userName,解析可用地址 info: DotEasy.Rpc.Consul.ConsulServiceRouteManager[0]準備獲取所有路由配置。 info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]根據服務id:doteasy.rpc.interfaces.IUserService.GetUserId_userName,找到以下可用地址:127.0.0.1:9881 info: DotEasy.Rpc.Core.Client.Implementation.RemoteInvokeService[0]使用地址:'127.0.0.1:9881'進行調用 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備為服務端地址:127.0.0.1:9881創建客戶端。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備發送消息。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備獲取Id為:e14b7606b4d54a66af81bfe3c7df46d4的響應內容。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]消息發送成功。 info: DotEasy.Rpc.Core.Communally.Convertibles.Impl.DefaultTypeConvertibleService[0]準備將 System.Int64 轉換為:System.Int32 UserService.GetUserId:1 info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]準備為服務id:doteasy.rpc.interfaces.IUserService.GetUserLastSignInTime_id,解析可用地址 info: DotEasy.Rpc.Consul.ConsulServiceRouteManager[0]準備獲取所有路由配置。 info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]根據服務id:doteasy.rpc.interfaces.IUserService.GetUserLastSignInTime_id,找到以下可用地址:127.0.0.1:9881 info: DotEasy.Rpc.Core.Client.Implementation.RemoteInvokeService[0]使用地址:'127.0.0.1:9881'進行調用 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備為服務端地址:127.0.0.1:9881創建客戶端。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備發送消息。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備獲取Id為:d0452b16caeb48ba877da5f69a31b2f8的響應內容。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]消息發送成功。 UserService.GetUserLastSignInTime:2018/12/11 22:31:41 info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]準備為服務id:doteasy.rpc.interfaces.IUserService.Exists_id,解析可用地址 info: DotEasy.Rpc.Consul.ConsulServiceRouteManager[0]準備獲取所有路由配置。 info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]根據服務id:doteasy.rpc.interfaces.IUserService.Exists_id,找到以下可用地址:127.0.0.1:9881 info: DotEasy.Rpc.Core.Client.Implementation.RemoteInvokeService[0]使用地址:'127.0.0.1:9881'進行調用 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備為服務端地址:127.0.0.1:9881創建客戶端。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備發送消息。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備獲取Id為:4e9a218c4abd4551845008d9bc23c31f的響應內容。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]消息發送成功。 UserService.Exists:True info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]準備為服務id:doteasy.rpc.interfaces.IUserService.GetDictionary,解析可用地址 info: DotEasy.Rpc.Consul.ConsulServiceRouteManager[0]準備獲取所有路由配置。 info: DotEasy.Rpc.Core.Client.Address.Resolvers.Implementation.DefaultAddressResolver[0]根據服務id:doteasy.rpc.interfaces.IUserService.GetDictionary,找到以下可用地址:127.0.0.1:9881 info: DotEasy.Rpc.Core.Client.Implementation.RemoteInvokeService[0]使用地址:'127.0.0.1:9881'進行調用 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備為服務端地址:127.0.0.1:9881創建客戶端。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備發送消息。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]準備獲取Id為:a625fd4a6bd24e6b82983272b8894562的響應內容。 info: DotEasy.Rpc.Transport.Impl.DefaultDotNettyTransportClientFactory[0]消息發送成功。 info: DotEasy.Rpc.Core.Communally.Convertibles.Impl.DefaultTypeConvertibleService[0]準備將 Newtonsoft.Json.Linq.JObject 轉換為:System.Collections.Generic.IDictionary`2[System.String,System.String] UserService.GetDictionary:value?
注意加粗的文字,是否跟調用接口一樣的結果呢,其他的調試日志目前請忽略。四:總結
這樣,通過asp.net core + consul +?doteasy.rpc便實現了一個簡單的遠程服務調用,你可以嘗試部署到外網,看看是否是遠程調用,當然,目前并沒有任何的統一網關驗證,所以,任何人的機器都可以調用。 碼字不易,你的推薦是我最大的動力,謝謝。 感謝閱讀!總結
以上是生活随笔為你收集整理的NET Core微服务之路:自己动手实现Rpc服务框架,基于DotEasy.Rpc服务框架的介绍和集成...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Error APICloud iOS n
- 下一篇: 【Apache】Apache的安装和配置