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:\routes.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://www.mzph.cn/news/450294.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Centos7防火墙设置

查看防火墙状态 or rootlocalhost ~]# systemctl status firewalld / firewall-cmd --state 启动防火墙 [rootlocalhost ~]# systemctl start firewalld 关闭防火墙 [rootlocalhost ~]# systemctl stop firewalld 设置开机启动 [rootlocalhost ~]# systemctl enable fi…

HTTP协议中POST、GET、HEAD、PUT等请求方法及相应值得含义

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 请求方法是请求一定的Web页面的程序或用于特定的URL。可选用下列几种&#xff1a; GET&#xff1a; 请求指定的页面信息&#xff0c;并…

java面试题文档(QA)

– 基础篇 1、 Java语言有哪些特点2、面向对象和面向过程的区别3 、八种基本数据类型的大小&#xff0c;以及他们的封装类4、标识符的命名规则。5、instanceof 关键字的作用6、Java自动装箱与拆箱7、 重载和重写的区别8、 equals与的区别9、 Hashcode的作用10、String、String …

第四次软件工程作业

关于 石墨文档客户端 的案例分析 作业地址&#xff1a; https://edu.cnblogs.com/campus/nenu/2016CS/homework/2505 第一部分 调研&#xff0c; 评测 1.下载并使用&#xff0c;按照描述的bug定义&#xff0c;找3~5个功能性的比较严重的bug。请用专业的语言描述&#xff08;每个…

深入剖析C++中的string类

一&#xff0c;C语言的字符串 在C语言里&#xff0c;对字符串的处理一项都是一件比较痛苦的事情&#xff0c;因为通常在实现字符串的操作的时候都会用到最不容易驾驭的类型——指针。 比如下面这个例子&#xff1a; //example 1: char str[12] "Hello"; char *…

Apple System: Error: ENFILE: file table overflow

2019独角兽企业重金招聘Python工程师标准>>> 在MAC上跑nodejs&#xff0c;遇到了一个问题&#xff1a;file table overflow 主要意思就是说文件打开太多了&#xff0c;超过了限制&#xff0c;产生这个问题主要是苹果操作系统的限制。 echo kern.maxfiles65536 | sud…

springboot的缓存技术

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 我门知道一个程序的瓶颈在于数据库&#xff0c;我门也知道内存的速度是大大快于硬盘的速度的。当我门需要重复的获取相同的数据的时候&a…

深度优先遍历解决连通域求解问题-python实现

问题描述 在一个矩形网格中每一个格子的颜色或者为白色或者为黑色。任意或上、或下、或左、或右相邻同为黑色的格子组成一个家族。家族中所有格子的数量反映家族的大小。要求找出最大家族的家族大小&#xff08;组成最大家族的格子的数量&#xff09;并统计出哪些点属于哪一族。…

字符串进阶

C风格字符串 1、字符串是用字符型数组存储的&#xff0c;字符串要求其尾部以’\0’作为结束标志。如&#xff1a; char string[ ]”C programming language”; 用sizeof来测string长度为25个字节&#xff0c;而实际串本身长度(含空格)为24个字节&#xff0c;多出来的一个就是…

flask上传excel文件,无须存储,直接读取内容

运行环境python3.6 import xlrd from flask import Flask, requestapp Flask(__name__)app.route("/", methods[POST, GET]) def filelist1():print(request.files)file request.files[file]print(file, type(file), file)print(file.filename) # 打印文件名f …

分布式 ID的 9 种生成方式

一、为什么要用分布式 ID&#xff1f; 在说分布式 ID 的具体实现之前&#xff0c;我们来简单分析一下为什么用分布式 ID&#xff1f;分布式 ID 应该满足哪些特征&#xff1f; 1、什么是分布式 ID&#xff1f; 拿 MySQL 数据库举个栗子&#xff1a; 在我们业务数据量不大的时…

spring boot Redis集成—RedisTemplate

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Spring boot 基于Spring, Redis集成与Spring大同小异。 文章示例代码均以前篇笔记为基础增加修改&#xff0c;直接上代码&#xff1a;…

QtCreator无法编辑源文件

在Qt Creator中新建工程&#xff0c;添加现有C源文件&#xff0c;有的源文件可以编辑&#xff0c;有的源文件编辑不了&#xff0c;发现无法编辑的源文件有一个共同特点&#xff0c;即其中都包含中文&#xff0c;且中文出现乱码&#xff0c;于是&#xff0c;点击Qt Creator菜单栏…

Unicode简介和使用

一、Unicode简介 在第一章中&#xff0c;我已经预告&#xff0c;C语言中在Microsoft Windows程序设计中扮演着重要角色的任何部分都会讲述到&#xff0c;您也许在传统文字模式程序设计中还尚未遇到过这些问题。宽字符集和Unicode差不多就是这样的问题。 简单地说&#xff0c;…

webpack4.x 模块化浅析-CommonJS

先看下webpack官方文档中对模块的描述&#xff1a; 在模块化编程中&#xff0c;开发者将程序分解成离散功能块(discrete chunks of functionality)&#xff0c;并称之为模块。每个模块具有比完整程序更小的接触面&#xff0c;使得校验、调试、测试轻而易举。 精心编写的模块提供…

设计模式--抽象工厂(个人笔记)

一、抽象工厂的应用场景以及优缺点 1 应用场景&#xff1a; 如果系统需要多套的代码解决方案&#xff0c;并且每套的代码解决方案中又有很多相互关联的产品类型&#xff0c;并且在系统中我们可以相互替换的使用一套产品的时候可以使用该模式&#xff0c;客户端不需要依赖具体的…

利用阿里云OSS对文件进行存储,上传等操作

--pom.xml加入阿里OSS存储依赖 <!--阿里云OSS存储--> <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>2.8.3</version> </dependency> --配置阿里云oss相关常量参数 /…

Java并发编程之ThreadGroup

ThreadGroup是Java提供的一种对线程进行分组管理的手段&#xff0c;可以对所有线程以组为单位进行操作&#xff0c;如设置优先级、守护线程等。 线程组也有父子的概念&#xff0c;如下图&#xff1a; 线程组的创建 1 public class ThreadGroupCreator {2 3 public static v…

springboot 缓存ehcache的简单使用

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 步骤&#xff1a; 1. pom文件中加 maven jar包&#xff1a; <!-- ehcache 缓存 --><dependency><groupId>net.sf.eh…

Spring boot + mybatis plus 快速构建项目,生成基本业务操作代码。

---进行业务建表&#xff0c;这边根据个人业务分析&#xff0c;不具体操作 --加入mybatis plus pom依赖 <!-- mybatis-plus 3.0.5--> <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId>&l…