• .NET Core微服务之路:让我们对上一个Demo通讯进行修改,完成RPC通讯


      最近一段时间有些事情耽搁了更新,抱歉各位了。
      上一篇我们简单的介绍了DotNetty通信框架,并简单的介绍了基于DotNetty实现了回路(Echo)通信过程。
      我们来回忆一下上一个项目的整个流程:
    1. 当服务端启动后,绑定并监听(READ)设定的端口,比如1889。
    2. 当客户端启动后,绑定指定端口,等待用户输入。
    3. 当用户输入任意字符串数据后,客户端将这组数据进行转码为byte格式进行传输到服务端。
    4. 当服务端收到客户端传来的数据,进行转码后输出控制台,并将这组数据再次回传到客户端。
    5. 客户端收到数据,也打印出来。

      很简单的实现了一个点对点的通信例子。接下来我们将对这个DEMO进行简单的修改,模拟最简单的gRPC通信的一个构造过程。
     
      本篇很简单,只要实现了上一个demo,稍作修改,就能实现gRPC了(当然实际构建gRPC根本不会这么简单),本篇也是顺带一下这几天搞出来的一个轻量级RPC框架,先接上一个例子。
     

    服务端

    增加两个静态方法SayHello和SayByebye,用于提供远程调用,超级简单,不解释。

    public static class Say
    {
        public static string SayHello(string content)
        {
            return $"hello {content}";
        }
    
        public static string SayByebye(string content)
        {
            return $"byebye {content}";
        }
    }

    在我们原来的ChannelRead函数中,将原有的Echo回路传输,直接替换成如下内容。

     1 public override void ChannelRead(IChannelHandlerContext context, object message)
     2 {
     3     if (message is IByteBuffer buffer)
     4     {
     5         Console.WriteLine($"message length is {buffer.Capacity}");
     6         var obj = JsonConvert.DeserializeObject<Dictionary<string, string>>(buffer.ToString(Encoding.UTF8).Replace(")", "")); // (1)
     7 
     8         byte[] msg = null;
     9         if (obj["func"].Contains("sayHello"))  // (2)
    10         {
    11             msg = Encoding.UTF8.GetBytes(Say.SayHello(json["username"]));
    12         }
    13 
    14         if (obj["func"].Contains("sayByebye")) // (2)
    15         {
    16             msg = Encoding.UTF8.GetBytes(Say.SayByebye(json["username"]));
    17         }
    18 
    19         if (msg == null) return;
    20         // 设置Buffer大小
    21         var b = Unpooled.Buffer(msg.Length, msg.Length); // (3)
    22         IByteBuffer byteBuffer = b.WriteBytes(msg); // (4)
    23         context.WriteAsync(byteBuffer); // (5)
    24     }
    25 }
    (1):有这样一句话Replace(")", ""),笔者不知为何每次传送过来从buffer里转义出来的字符串,始终会有一个左括号在里面,也许是消息头,也许是protobuf-net的标记头,因为都是byte格式,在服务端偷懒就没有再进行一次protobuf的反序列化了。
    为何要用Dictionary来作为中间对象转换,因为序列化需要实体对象作为类型,为了简单的介绍RPC,目前也就这么干了,例如上面代码所示。
    (2):通过判断“func”字段中的内容进行方法调用,并将调用过程的返回结果转为BYTE格式。
    (3):设置本次传输中的Buffer大小。
    (4):将消息(数据)写入到DotNetty的Buffer。
    (5):最终将Buffer写入到当前上下文(包含通道,传输对象,连接对象等等)。
     

    客户端

    我们将上一个demo中的EchoClientHandler做如下修改,以完成一个简单的请求

     1 public EchoClientHandler()
     2 {
     3     var hello = new Dictionary<string, string> // (1)
     4     {
     5         {"func", "sayHello"},
     6         {"username", "stevelee"}
     7     };
     8     SendMessage(ToStream(JsonConvert.SerializeObject(hello)));
     9 }
    10 
    11 private byte[] ToStream(string msg)
    12 {
    13     Console.WriteLine($"string length is {msg.Length}");
    14     using (var stream = new MemoryStream()) // (2)
    15     {
    16         Serializer.Serialize(stream, msg);
    17         return stream.ToArray();
    18     }
    19 }
    20 
    21 private void SendMessage(byte[] msg)
    22 {
    23     Console.WriteLine($"byte length is {msg.Length}");
    24     _initialMessage = Unpooled.Buffer(msg.Length, msg.Length);
    25     _initialMessage.WriteBytes(msg); // (3)
    26 }
    (1):建立与服务端相关的通信数据。
    (2):将数据序列化为二进制流。
    (3):将数据写入到ByteBuffer中。
     

    启动一下

    由于在客户端明文标注了使用sayHello这个方法,客户端会收到服务端返回的"hello stevelee"。

      这样一个最简单的RPC远程调用就完成了(其实上一篇就也属于RPC,只是这里用方法和过滤来指定调用)。

     问题

    1. 服务端不可能都通过这样笨拙的过滤方式来调用方法吧?是的,这只是DEMO,为了演示和理解基础概念而已,而是要动过动态代理来实现方法Invoke。
    2. 这个DEMO只是一个点对点的远程调用,不会涉及到任何服务路由和转发等高级特性。
    3. 有新的接口的时候时候,需要重新编译和暴露,如果有上万个新的接口,这样的重复工作岂不是疯了。
    4. ...etc
      这里推荐一下最近构建的一个小框架:Easy.Rpc(连接点我),实现了路由,转发,代理,动态编译的特性。这里也帮朋友们推荐一个同样基于DotNetty的RPC框架(连接点我)张队推荐我加入他们,可我不知道怎么加入他们的团队,悲催啊...
     
      简单介绍一下使用方法,本篇不详细介绍这个框架是如何实现的,估计会好几十万字,单独拧出来做个系列会更好,框架设计需要哪些原则,需要考虑到的问题,包含设计模式、依赖注入、动态代理、动态编译、路由转发等等特性。
     

    Esay.Rpc

      正如上面提到问题,需要解决这些问题,就需要修改诸多内容,
     
      例如把函数改为接口,把接口的定义放置服务端并对外开放相应端口,把接口的实现同样放置服务端,提供接口的调用,客户端通过类似API的方式进行远程接口调用,因此这个接口的定义必须单列的一个项目;
    如何将接口自动部署(暴露)出来,可以通过中间协调器(也叫服务注册中心,如ETCD,consul,zookeeper),如何将这些接口自动注册到服务中心呢,需要实现反射自动扫描并添加到注册中心。
     
      我们添加一个Rpc.Common的中间通用库,当然Easy.Rpc的框架源码也在这个里面(框架目前不探讨),添加IUserService接口,UserModel实体类,UserServiceImpl实现类。其实通用类库只需要接口和实体就行,接口实现完全放置服务端,这样这个库也能完全分离出来。(不过笔者偷懒都写到Rpc.Common库中去了,实际生产决不能这么膜,分离,分离,分离,这也是微服务的主要概念之一)
     
      DEMO结构如下(Easy.Rpc源码目前也包含在这个里面,过两天单独拎出来做成框架,方便调用)

     

    先看看接口定义了些什么:

     1 /// <summary>
     2 /// 接口UserService的定义
     3 /// </summary>
     4 [RpcTagBundle]
     5 public interface IUserService
     6 {
     7     Task<string> GetUserName(int id);
     8 
     9     Task<int> GetUserId(string userName);
    10 
    11     Task<DateTime> GetUserLastSignInTime(int id);
    12 
    13     Task<UserModel> GetUser(int id);
    14 
    15     Task<bool> Update(int id, UserModel model);
    16 
    17     Task<IDictionary<string, string>> GetDictionary();
    18 
    19     Task Try();
    20 
    21     Task TryThrowException();
    22 }
    8个接口,几乎囊括了目前RPC调用测试的所有方法场景。接口实现就不贴了,你完全可以自定义接口的任何实现,或者就一句Console.Write("哇凉哇凉完啦")都可以。
    接口参数中有个UserModel的实体对象,这里也贴上来。
    1 [ProtoContract]
    2 public class UserModel
    3 {
    4     [ProtoMember(1)] public string Name { get; set; }
    5 
    6     [ProtoMember(2)] public int Age { get; set; }
    7 }

    上面有两个不一样的标记,也是protobuf-net中独有的特性。

    ProtoContract标记:该类是参与序列化内容的数据类。
    ProtoMember标题:该类需要序列化的字段和顺序。

    protobuf-net的坑

    1. 默认例子中该类没有任何继承,因此不会存在一个妖孽问题,但如果UserModel是一个子类,他继承于一个父类,而这个父类也同样拥有多个子类,直接ProtoContract参与序列化将会报错,需要在特性上增加DataMemberOffset = x,此处的x不是字母,而是这个子类的一个序列化顺序。比如有3个子类继承同一个父类,前面两个子类的偏移量分别是1和2,那么这个类的偏移量将设置为3,以此类推。
    2. 默认的数据类型中,系统定义的标准类型没问题,但有个妖孽的int[]这样的数组类型,那也将是个噩梦,官网团队没有解释为何不支持数组的序列化,我猜测估计是因为数组的不规则性(比如多维数组、甚至不规则的多维数组)而放弃了这个类型的序列化,毕竟序列化是不能影响性能的。

    接下来继续服务端的代码

     1 static void Main()
     2 {
     3     var bTime = DateTime.Now;
     4 
     5     // 实现自动装配
     6     var serviceCollection = new ServiceCollection();
     7     {
     8         serviceCollection
     9             .AddLogging()
    10             .AddRpcCore()
    11             .AddService()
    12             .UseSharedFileRouteManager("d:\routes.txt")
    13             .UseDotNettyTransport();
    14 
    15         // ** 注入本地测试类
    16         serviceCollection.AddSingleton<IUserService, UserServiceImpl>();
    17     }
    18 
    19     // 构建当前容器
    20     var buildServiceProvider = serviceCollection.BuildServiceProvider();
    21 
    22     // 获取服务管理实体类
    23     var serviceEntryManager = buildServiceProvider.GetRequiredService<IServiceEntryManager>();
    24     var addressDescriptors = serviceEntryManager.GetEntries().Select(i => new ServiceRoute
    25     {
    26         Address = new[]
    27         {
    28             new IpAddressModel {Ip = "127.0.0.1", Port = 9881}
    29         },
    30         ServiceDescriptor = i.Descriptor
    31     });
    32     var serviceRouteManager = buildServiceProvider.GetRequiredService<IServiceRouteManager>();
    33     serviceRouteManager.SetRoutesAsync(addressDescriptors).Wait();
    34 
    35     // 构建内部日志处理
    36     buildServiceProvider.GetRequiredService<ILoggerFactory>().AddConsole((console, logLevel) => (int) logLevel >= 0);
    37 
    38     // 获取服务宿主
    39     var serviceHost = buildServiceProvider.GetRequiredService<IServiceHost>();
    40 
    41     Task.Factory.StartNew(async () =>
    42     {
    43         //启动主机
    44         await serviceHost.StartAsync(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9881));
    45     });
    46 
    47     Console.ReadLine();
    48 }
    全程基于serviceCollection实现自动装配和构造,相信用过Ioc容器都能明白这上面几条依赖注入和自动构建服务的含义。
    再添加客户端代码:
     1 static void Main()
     2 {
     3     var serviceCollection = new ServiceCollection();
     4     {
     5         serviceCollection
     6             .AddLogging()                                // 添加日志
     7             .AddClient()                                 // 添加客户端
     8             .UseSharedFileRouteManager(@"d:
    outes.txt") // 添加共享路由
     9             .UseDotNettyTransport();                     // 添加DotNetty通信传输
    10     }
    11 
    12     var serviceProvider = serviceCollection.BuildServiceProvider();
    13 
    14     serviceProvider.GetRequiredService<ILoggerFactory>().AddConsole((console, logLevel) => (int) logLevel >= 0);
    15 
    16     var services = serviceProvider.GetRequiredService<IServiceProxyGenerater>()
    17         .GenerateProxys(new[] {typeof(IUserService)}).ToArray();
    18 
    19     var userService = serviceProvider.GetRequiredService<IServiceProxyFactory>().CreateProxy<IUserService>(
    20         services.Single(typeof(IUserService).GetTypeInfo().IsAssignableFrom)
    21     );
    22 
    23     while (true)
    24     {
    25         Task.Run(async () =>
    26         {
    27             Console.WriteLine($"userService.GetUserName:{await userService.GetUserName(1)}");
    28             Console.WriteLine($"userService.GetUserId:{await userService.GetUserId("rabbit")}");
    29             Console.WriteLine($"userService.GetUserLastSignInTime:{await userService.GetUserLastSignInTime(1)}");
    30             var user = await userService.GetUser(1);
    31             Console.WriteLine($"userService.GetUser:name={user.Name},age={user.Age}");
    32             Console.WriteLine($"userService.Update:{await userService.Update(1, user)}");
    33             Console.WriteLine($"userService.GetDictionary:{(await userService.GetDictionary())["key"]}");
    34             await userService.Try();
    35             Console.WriteLine("client function completed!");
    36         }).Wait();
    37         Console.ReadKey();
    38     }
    39 }
      我想看到这里,明白上面代码的作用,也就明白了这个框架的作用,客户端能像调用本地方法一样去调用远程方法,并且中间过程是完全透明的,分离,分离,分离。
      微服务的作用不再介绍,呵呵。
     
     
    感谢阅读!

    作者:李知洲

    出处:http://stevelee.cnblogs.com

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。

  • 相关阅读:
    Spring事务管理
    Java GC算法
    内连接,左连接,右连接
    ThreadLocal相关
    @Autowired 与 @Resource的区别
    spring注解
    BZOJ 1040 ZJOI 2008 骑士 树形DP
    HDU 5575 Discover Water Tank 并查集 树形DP
    BZOJ 3571 画框 KM算法 最小乘积最大权匹配
    ZOJ 3256 Tour in the Castle 插头DP 矩阵乘法
  • 原文地址:https://www.cnblogs.com/SteveLee/p/Simple_Rpc_Demo.html
Copyright © 2020-2023  润新知