Loading

手动造轮子——基于.NetCore的RPC框架DotNetCoreRpc

前言#

    一直以来对内部服务间使用RPC的方式调用都比较赞同,因为内部间没有这么多限制,最简单明了的方式就是最合适的方式。个人比较喜欢类似Dubbo的那种使用方式,采用和本地方法相同的方式,把接口层独立出来作为服务契约,为服务端提供服务,客户端也通过此契约调用服务。.Net平台上类似Dubbo这种相对比较完善的RPC框架还是比较少的,GRPC确实是一款非常优秀的RPC框架,能跨语言调用,但是每次还得编写proto文件,个人感觉还是比较麻烦的。如今服务拆分,微服务架构比较盛行的潮流下,一个简单实用的RPC框架确实可以提升很多开发效率。

简介#

    随着.Net Core逐渐成熟稳定,为我一直以来想实现的这个目标提供了便利的方式。于是利用闲暇时间本人手写了一套基于Asp.Net Core的RPC框架,算是实现了一个自己的小目标。大致的实现方式,Server端依赖Asp.Net Core,采用的是中间件的方式拦截处理请求比较方便。Client端可以是任何可承载.Net Core的宿主程序。通信方式是HTTP协议,使用的是HttpClientFactory。至于为什么使用HttpClientFactory,因为HttpClientFactory可以更轻松的实现服务发现,而且可以很好的集成Polly,很方便的实现,超时重试,熔断降级这些,给开发过程中提供了很多便利。由于本人能力有限,基于这些便利,站在巨人的肩膀上,简单的实现了一个RPC框架,项目托管在GitHub上https://github.com/softlgl/DotNetCoreRpc有兴趣的可以自行查阅。

开发环境#

  • Visual Studio 2019
  • .Net Standard 2.1
  • Asp.Net Core 3.1.x

使用方式#

    打开Visual Studio先新建一个RPC契约接口层,这里我起的名字叫IRpcService。然后新建一个Client层(可以是任何可承载.Net Core的宿主程序)叫ClientDemo,然后建立一个Server层(必须是Asp.Net Core项目)叫WebDemo,文末附本文Demo连接,建完这些之后项目结构如下:

Client端配置#

Client端引入DotNetCoreRpc.Client包,并引入自定义的契约接口层

<PackageReference Include="DotNetCoreRpc.Client" Version="1.0.2" />

然后可以愉快的编码了,大致编码如下

class Program
{
    static void Main(string[] args)
    {
        IServiceCollection services = new ServiceCollection();
        //*注册DotNetCoreRpcClient核心服务
        services.AddDotNetCoreRpcClient()
        //*通信是基于HTTP的,内部使用的HttpClientFactory,自行注册即可
        .AddHttpClient("WebDemo", client => { client.BaseAddress = new Uri("http://localhost:13285/"); });
    IServiceProvider serviceProvider = services.BuildServiceProvider();
    <span class="hljs-comment">//*获取RpcClient使用这个类创建具体服务代理对象</span>
    RpcClient rpcClient = serviceProvider.GetRequiredService&lt;RpcClient&gt;();

    <span class="hljs-comment">//IPersonService是我引入的服务包interface,需要提供ServiceName,即AddHttpClient的名称</span>
    IPersonService personService = rpcClient.CreateClient&lt;IPersonService&gt;(<span class="hljs-string">"WebDemo"</span>);

    PersonDto personDto = <span class="hljs-keyword">new</span> PersonDto
    {
        Id = <span class="hljs-number">1</span>,
        Name = <span class="hljs-string">"yi念之间"</span>,
        Address = <span class="hljs-string">"中国"</span>,
        BirthDay = <span class="hljs-keyword">new</span> DateTime(<span class="hljs-number">2000</span>,<span class="hljs-number">12</span>,<span class="hljs-number">12</span>),
        IsMarried = <span class="hljs-literal">true</span>,
        Tel = <span class="hljs-number">88888888888</span>
    };

    <span class="hljs-built_in">bool</span> addFlag = personService.Add(personDto);
    Console.WriteLine(<span class="hljs-string">$"添加结果=[<span class="hljs-subst">{addFlag}</span>]"</span>);

    <span class="hljs-keyword">var</span> person = personService.Get(personDto.Id);
    Console.WriteLine(<span class="hljs-string">$"获取person结果=[<span class="hljs-subst">{person.ToJson()}</span>]"</span>);

    <span class="hljs-keyword">var</span> persons = personService.GetAll();
    Console.WriteLine(<span class="hljs-string">$"获取persons结果=[<span class="hljs-subst">{persons.ToList().ToJson()}</span>]"</span>);

    personService.Delete(person.Id);
    Console.WriteLine(<span class="hljs-string">$"删除完成"</span>);

    Console.ReadLine();
}

}

折叠

到这里Client端的代码就编写完成了

Server端配置#

Client端引入DotNetCoreRpc.Client包,并引入自定义的契约接口层

<PackageReference Include="DotNetCoreRpc.Server" Version="1.0.2" />

然后编写契约接口实现类,比如我的叫PersonService

//实现契约接口IPersonService
public class PersonService:IPersonService
{
    private readonly ConcurrentDictionary<int, PersonDto> persons = new ConcurrentDictionary<int, PersonDto>();
    public bool Add(PersonDto person)
    {
        return persons.TryAdd(person.Id, person);
    }
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Delete</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> id</span>)</span>
{
    persons.Remove(id,<span class="hljs-keyword">out</span> PersonDto person);
}

<span class="hljs-comment">//自定义Filter</span>
[<span class="hljs-meta">CacheFilter(CacheTime = 500)</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> PersonDto <span class="hljs-title">Get</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> id</span>)</span>
{
    <span class="hljs-keyword">return</span> persons.GetValueOrDefault(id);
}

<span class="hljs-comment">//自定义Filter</span>
[<span class="hljs-meta">CacheFilter(CacheTime = 300)</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerable&lt;PersonDto&gt; <span class="hljs-title">GetAll</span>()</span>
{
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> persons)
    {
        <span class="hljs-keyword">yield</span> <span class="hljs-keyword">return</span> item.Value;
    }
}

}

通过上面的代码可以看出,我自定义了Filter,这里的Filter并非Asp.Net Core框架定义的Filter,而是DotNetCoreRpc框架定义的Filter,自定义Filter的方式如下

//*要继承自抽象类RpcFilterAttribute
public class CacheFilterAttribute: RpcFilterAttribute
{
    public int CacheTime { get; set; }
<span class="hljs-comment">//*支持属性注入,可以是public或者private</span>
<span class="hljs-comment">//*这里的FromServices并非Asp.Net Core命名空间下的,而是来自DotNetCoreRpc.Core命名空间</span>
[<span class="hljs-meta">FromServices</span>]
<span class="hljs-keyword">private</span> RedisConfigOptions RedisConfig { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

[<span class="hljs-meta">FromServices</span>]
<span class="hljs-keyword">public</span> ILogger&lt;CacheFilterAttribute&gt; Logger { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">InvokeAsync</span>(<span class="hljs-params">RpcContext context, RpcRequestDelegate next</span>)</span>
{
    Logger.LogInformation(<span class="hljs-string">$"CacheFilterAttribute Begin,CacheTime=[<span class="hljs-subst">{CacheTime}</span>],Class=[<span class="hljs-subst">{context.TargetType.FullName}</span>],Method=[<span class="hljs-subst">{context.Method.Name}</span>],Params=[<span class="hljs-subst">{JsonConvert.SerializeObject(context.Parameters)}</span>]"</span>);
    <span class="hljs-keyword">await</span> next(context);
    Logger.LogInformation(<span class="hljs-string">$"CacheFilterAttribute End,ReturnValue=[<span class="hljs-subst">{JsonConvert.SerializeObject(context.ReturnValue)}</span>]"</span>);
}

}

以上代码基本上完成了对服务端业务代码的操作,接下来我们来看如何在Asp.Net Core中配置使用DotNetCoreRpc。打开Startup,配置如下代码既可

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IPersonService, PersonService>()
        .AddSingleton(new RedisConfigOptions { Address = "127.0.0.1:6379", Db = 10 })
        //*注册DotNetCoreRpcServer
        .AddDotNetCoreRpcServer(options => {
            //*确保添加的契约服务接口事先已经被注册到DI容器中
        <span class="hljs-comment">//添加契约接口</span>
        <span class="hljs-comment">//options.AddService&lt;IPersonService&gt;();</span>

        <span class="hljs-comment">//或添加契约接口名称以xxx为结尾的</span>
        <span class="hljs-comment">//options.AddService("*Service");</span>

        <span class="hljs-comment">//或添加具体名称为xxx的契约接口</span>
        <span class="hljs-comment">//options.AddService("IPersonService");</span>

        <span class="hljs-comment">//或扫描具体命名空间下的契约接口</span>
        options.AddNameSpace(<span class="hljs-string">"IRpcService"</span>);

        <span class="hljs-comment">//可以添加全局过滤器,实现方式和CacheFilterAttribute一致</span>
        options.AddFilter&lt;LoggerFilterAttribute&gt;();
    });
}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Configure</span>(<span class="hljs-params">IApplicationBuilder app, IWebHostEnvironment env</span>)</span>
{
    <span class="hljs-comment">//这一堆可以不要+1</span>
    <span class="hljs-keyword">if</span> (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    <span class="hljs-comment">//添加DotNetCoreRpc中间件既可</span>
    app.UseDotNetCoreRpc();

    <span class="hljs-comment">//这一堆可以不要+2</span>
    app.UseRouting();

    <span class="hljs-comment">//这一堆可以不要+3</span>
    app.UseEndpoints(endpoints =&gt;
    {
        endpoints.MapGet(<span class="hljs-string">"/"</span>, <span class="hljs-keyword">async</span> context =&gt;
        {
            <span class="hljs-keyword">await</span> context.Response.WriteAsync(<span class="hljs-string">"Server Start!"</span>);
        });
    });
}

}

折叠

以上就是Server端简单的使用和配置,是不是感觉非常的Easy。附上可运行的Demo地址,具体编码可查看Demo.

总结#

    能自己实现一套RPC框架是我近期以来的一个愿望,现在可以说实现了。虽然看起来没这么高大上,但是整体还是符合RPC的思想。主要还是想自身实地的实践一下,顺便也希望能给大家提供一些简单的思路。不是说我说得一定是对的,我讲得可能很多是不对的,但是我说的东西都是我自身的体验和思考,也许能给你带来一秒钟、半秒钟的思考,亦或是哪怕你觉得我哪一句话说的有点道理,能引发你内心的感触,这就是我做这件事的意义。最后,欢迎大家评论区或本项目GitHub下批评指导。

posted @ 2022-07-28 11:01  直争朝夕  阅读(231)  评论(0编辑  收藏  举报