最近一段时间有些事情耽搁了更新,抱歉各位了。
  上一篇我们简单的介绍了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回路传输,直接替换成如下内容。

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

客户端

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

 public EchoClientHandler()
{
var hello = new Dictionary<string, string> // (1)
{
{"func", "sayHello"},
{"username", "stevelee"}
};
SendMessage(ToStream(JsonConvert.SerializeObject(hello)));
} private byte[] ToStream(string msg)
{
Console.WriteLine($"string length is {msg.Length}");
using (var stream = new MemoryStream()) // (2)
{
Serializer.Serialize(stream, msg);
return stream.ToArray();
}
} private void SendMessage(byte[] msg)
{
Console.WriteLine($"byte length is {msg.Length}");
_initialMessage = Unpooled.Buffer(msg.Length, msg.Length);
_initialMessage.WriteBytes(msg); // (3)
}
(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源码目前也包含在这个里面,过两天单独拎出来做成框架,方便调用)

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

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

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

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

protobuf-net的坑

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

接下来继续服务端的代码

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

.NET Core微服务之路:让我们对上一个Demo通讯进行修改,完成RPC通讯的更多相关文章

  1. .NET Core微服务之路:文章系列和内容索引汇总 (v0.52)

    微服务架构,对于从事JAVA架构的童鞋来说,早已不是什么新鲜的事儿,他们有鼎鼎大名的Spring Cloud这样的全家桶框架支撑,包含微服务核心组件如 1. Eureka:实现服务注册与发现. 2. ...

  2. NET Core微服务之路:实战SkyWalking+Exceptionless体验生产环境下的追踪系统

    前言 当一个APM或一个日志中心实际部署在生产环境中时,是有点力不从心的. 比如如下场景分析的问题: 从APM上说,知道某个节点出现异常,或延迟过过高,却不能及时知道日志反馈情况,总不可能去相应的节点 ...

  3. NET Core微服务之路:实战SkyWalking+Exceptionless体验生产下追踪系统

    原文:NET Core微服务之路:实战SkyWalking+Exceptionless体验生产下追踪系统 前言 当一个APM或一个日志中心实际部署在生产环境中时,是有点力不从心的. 比如如下场景分析的 ...

  4. .NET Core微服务之路:不断更新中的目录 (v0.43)

    原文:.NET Core微服务之路:不断更新中的目录 (v0.43) 微服务架构,对于从事JAVA架构的童鞋来说,早已不是什么新鲜的事儿,他们有鼎鼎大名的Spring Cloud这样的全家桶框架支撑, ...

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

    本篇内容属于非实用性(拿来即用)介绍,如对框架设计没兴趣的朋友,请略过. 快一个月没有写博文了,最近忙着两件事;    一:阅读刘墉先生的<说话的魅力>,以一种微妙的,你我大家都会经常遇见 ...

  6. NET Core微服务之路:简单谈谈对ELK,Splunk,Exceptionless统一日志收集中心的心得体会

    前言 日志,一直以来都是开发人员和运维人员最关心的问题.开发人员可通过日志记录来协助问题定位,运维人员可通过日志发现系统隐患,故障等定位问题.如果你的系统中没有日志,就像一个断了线的风筝,你永远不知道 ...

  7. .NET Core微服务之路:基于gRPC服务发现与服务治理的方案

    重温最少化集群搭建,我相信很多朋友都已经搭建出来,基于Watch机制也实现了出来,相信也有很多朋友有了自己的实现思路,但是,很多朋友有个疑问,我API和服务分离好了,怎么通过服务中心进行发现呢,这个过 ...

  8. .NET Core微服务之路:基于Consul最少集群实现服务的注册与发现(二)

    重温Consul最少化集群的搭建  

  9. NET Core微服务之路:基于Ocelot的API网关Relay实现--RPC篇

    前言 我们都知道,API网关是工作在应用层上网关程序,为何要这样设计呢,而不是将网关程序直接工作在传输层.或者网络层等等更底层的环境呢?让我们先来简单的了解一下TCP/IP的五层模型.     (图片 ...

随机推荐

  1. 2101244 - FAQ: SAP HANA Multitenant Database Containers (MDC)

    Symptom You face issues or have questions related to multitenant database containers in SAP HANA env ...

  2. mac os High Sierra 升级错误

    升级mac OS High Sierra错误 已经成功从10.10升级到10.12.8 mac OS  Sierra了.就是升级到10.13报错. you may not install to thi ...

  3. Selenium + Chrome headless 报ERROR:gpu_process_transport_factory.cc(1007)] Lost UI shared context 可忽略并配置不输出日志

    Selenium不再推荐使用PhantomJS,会报如下警告 UserWarning: Selenium support for PhantomJS has been deprecated, plea ...

  4. cxGrid类似pagecontrol的效果

    1.对TcxGrid创建多个Level 2.对TcxGrid的TcxGridLevelOptions的TabsForEmptyDetail设置为True 3.再设置DetailTabsPosition ...

  5. 使用jQuery+huandlebars循环中索引(@index)使用技巧(访问父级索引)

    兼容ie8(很实用,复制过来,仅供技术参考,更详细内容请看源地址:http://www.cnblogs.com/iyangyuan/archive/2013/12/12/3471227.html) & ...

  6. MFC笔记10

    1. CDC MemDC1; MemDC1.SetBkMode(OPAQUE); 背景模式,VC6下面有三种:/* Background Modes */#define TRANSPARENT 1// ...

  7. cdnbest自定义错误显示节点名教程

    在自定义错误里选择js选项,输入: document.write("error!" + hostname); 这是最简单的写法,只显示节点名,如果要显示其他效果,可自已修改js

  8. mybatis 根据参数映射对应模型

    ORM 框架的优势在于能让我们利用面向对象的思维去操作数据库, hibernate 作为重量级的 ORM 框架对面向对象的支持很强大.作为半自动化的 mybatis ,对面向对象的支持也是很完备的.这 ...

  9. APIcloud微信支付和支付宝支付(方案2,主要在后台进行)

    支付宝代码 var aliPay = api.require('aliPay'); api.ajax({ url: yuming+'index.php/api/Alipay/getOrder', me ...

  10. xmlhttprequest 1.0和2.0的区别,from qq前端哥

    阮一峰好文:http://www.ruanyifeng.com/blog/2012/09/xmlhttprequest_level_2.html