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

本篇内容属于非实用性(拿来即用)介绍,如对框架设计没兴趣的朋友,请略过。 快一个月没有写博文了,最近忙着两件事; 一:阅读刘墉先生的《说话的魅力》,以一种微妙的,你我大家都会经常遇见的事物,来建议说话的“艺术和魅力”,对于我们从事软件开发、不太善于沟通和表达的朋友来说,也算是一项软技能了,推荐喜欢阅读的朋友有时间阅读,给你不一样的阅读体验。 二:编写基于Net Core的Rpc框架。之前有朋友说如何将Rpc等整个体系集成到dotnet框架中,我想这篇博文会给你一个答案。 哦,对了,我不建议直接将代码直接复制下来然后去运行的朋友,因为这样你达不到学习的目的,也违背了笔者的初衷。谢谢理解。 一:简单回顾一下之前的介绍 继续贴上之前的一张图片 根据上面图,服务化原理可以分为3步: 服务端启动并且向注册中心发送服务信息,注册中心收到后会定时监控服务状态(常见心跳检测); 客户端需要开始调用服务的时候,首先去注册中心获取服务信息; 客户端创建远程调用连接,连接后服务端返回处理信息;   第3步又可以细分,下面说说远程过程调用的原理: 目标:客户端怎么调用远程机器上的公开方法 服务发现,向注册中心获取服务(这里需要做的有很多:拿到多个服务时需要做负载均衡,同机房过滤、版本过滤、服务路由过滤、统一网关等); 客户端发起调用,将需要调用的服务、方法、参数进行组装; 序列化编码组装的消息,这里可以使用json,也可以使用xml,也可以使用protobuf,也可以使用hessian,几种方案的序列化速度还有序列化后占用字节大小都是选择的重要指标,对内笔者建议使用高效的protobuf,它基于TCP/IP二进制进行序列化,体积小,速度快。 传输协议,可以使用传统的IO阻塞传输,也可以使用高效的nio传输(Netty); 服务端收到后进行反序列化,然后进行相应的处理; 服务端序列化response信息并且返回; 客户端收到response信息并且反序列化;   至于C类和S类之间的通信方式,是采用RPC还是采用RESTful,读者可以参考之前的介绍,根据实际业务进行决定https://www.cnblogs.com/SteveLee/p/service_discovery_and_service_governance.html 二:DotEasy.Rpc框架介绍   单论Rpc框架市场,且不论Java上的Spring Boot和Spring Cloud这样大名鼎鼎的开源框架,目前Net上的Rpc整合性框架确实并不多,我们Net程序员也要混口饭吃,不能总被Java甩掉好几条街吧。   言归正传,一个远程过程调用,会涉及到如下几个方面的技术点(功能): 路由转发:当服务部署在多个节点上时,调用方需要知道自己的目标服务在什么地方。 通信协议:当管道存在,还需要在管道的两端建立处理程序(宿主),以处理管道中的数据包。DotEasy.Rpc基于DotNetty进行通信处理和协议实现。 动态生成:我们知道,基于二进制的RPC传输,每当新增接口,或修改接口,都需要生成相关协议的protobuf文件(或 thrift 文件),本框架基于protobuf-net的传输框架和Rosyln的预生成,动态生成相关的CS文件。 运行时代理:本框架采用一次请求创建一个客户端的模式,进行S端的服务请求,本框架基于Rosyln运行时创建客户端代理。 接口扫描:需要实现多个接口的添加和注册,本框架采用Attribute特性来扫描接口,并添加到相关的注册中心,比如consul或etcd。 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中集成、等等。 功能和特性如下: Apache许可证2协议开放源代码; 统一组件装配和构造; 基于protobuf-net实现字节流序列化; 基于Rosyln的运行时客户端代理生成; 基于Rosyln的预生成的客户端代理; 基于DotNetty的传输信道; 支持客户端以轮询的方式实现负载平衡; Net Core结构及跨平台; 就这么点,不啰嗦,让我们开始做DEMO。 3.1 建立服务接口和服务实现 既然是服务,那么肯定需要以接口interface的方式实现对外暴露,并且,接口的实现不能和接口封装在同一个DLL中,不然还叫什么远程过程调用呢(RPC)呢,如果不了解什么叫接口,去翻一翻C#语言规范。 先定义一个接口,接口方法签名如下: 复制代码 namespace doteasy.rpc.interfaces { [RpcTagBundle] public interface IUserService { Task GetUserName(int id); Task Exists(int id); Task GetUserId(string userName); Task GetUserLastSignInTime(int id); Task> GetDictionary(); Task TryThrowException(); } } 复制代码 很简单,不解释。 其中接口上有个重要特性叫[RpcTagBundle],该特性只允许标记在interface接口上,用于启动时方便框架扫描所有标有该特性的接口。 在看实现类 复制代码 namespace doteasy.rpc.implement { public class UserService : IUserService { public Task GetUserName(int id) { return Task.FromResult($"我传了一个int数字{id}."); } public Task Exists(int id) { return Task.FromResult(true); } public Task GetUserId(string userName) { return Task.FromResult(1); } public Task GetUserLastSignInTime(int id) { return Task.FromResult(DateTime.Now); } public Task> GetDictionary() { return Task.FromResult>(new Dictionary { { "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(); //(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(); 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();使用代理模式动态生成了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]
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信