该rpc框架是一个mini版的dubbo。学习rpc之前,建议先了解NIO,Netty和Dubbo等知识。请移步网络编程

前言:(借用阿里大佬的一段话)

为什么要自己写一个RPC框架,我觉得从个人成长上说,如果一个程序员能清楚的了解RPC框架所具备的要素,掌握RPC框架中涉及的服务注册发现、负载均衡、序列化协议、RPC通信协议、Socket通信、异步调用、熔断降级等技术,可以全方位的提升基本素质。虽然也有相关源码,但是只看源码容易眼高手低,动手写一个才是自己真正掌握这门技术的最优路径。

一.概述

什么是RPC?

  • 远程服务调用
  • 官方:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想
  • 通俗一点:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
  • 市面上常见的rpc框架:dobbo,springCloud,gRPC...

那为什么要有 RPC,HTTP 不好么?

  • 因为 RPC 和 HTTP 就不是一个层级的东西,所以严格意义上这两个没有可比性,也不应该来作比较。
  • HTTP 只是传输协议,协议只是规范了一定的交流格式
  • RPC 对比的是本地过程调用,是用来作为分布式系统之间的通信,它可以用 HTTP 来传输,也可以基于 TCP 自定义协议传输。
  • HTTP 协议比较冗余,所以 RPC 大多都是基于 TCP 自定义协议,定制化的才是最适合自己的。

项目总体结构


整体架构

接下来,分别解释上述的过程

 二.自定义注解

服务的提供者和消费者公用一个接口,@ServiceExpose是为了暴露服务,放在生产者的某个实现类上;@ServiceReference是为了引用服务,放在消费者的需要注入的属性上。

  • Target:指定被修饰的Annotation可以放置的位置(被修饰的目标)

    • @Target(ElementType.TYPE)                                //接口、类

    • @Target(ElementType.FIELD)                               //属性

    • @Target(ElementType.METHOD)                           //方法

    • @Target(ElementType.PARAMETER)                   //方法参数
    • @Target(ElementType.CONSTRUCTOR)             //构造函数

    • @Target(ElementType.LOCAL_VARIABLE)          //局部变量

    • @Target(ElementType.ANNOTATION_TYPE)       //注解

    • @Target(ElementType.PACKAGE)                         //包

  • Retention:定义注解的保留策略
    • @Retention(RetentionPolicy.SOURCE)             //注解仅存在于源码中,在class字节码文件中不包含

    • @Retention(RetentionPolicy.CLASS)              //默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得

    • @Retention(RetentionPolicy.RUNTIME)            //注解会在class字节码文件中存在,在运行时可以通过反射获取到

  • Documented:指定被修饰的该Annotation可以被javadoc工具提取成文档
  • Inherited:指定被修饰的Annotation将具有继承性

 二.启动配置

主要是加载一些rpc相关的配置类,使用SpringBoot自动装配。可以使用SPI机制加入一些自定义的类,放到指定文件夹中。

 三.rpc接口注入/rpc服务扫描

这里主要就是通过反射获得对应注解的属性/类,进行服务暴露/服务引用。 这里需要关注的是什么时候进行服务暴露/引用?如下:

  • 客户端:一般有俩种方案
    • 饿汉式:饿汉式是通过实现 Spring 的InitializingBean接口中的 afterPropertiesSet方法,容器通过调用 ReferenceBean的 afterPropertiesSet方法时引入服务。(在Spring启动时,给所有的属性注入实现类,包含远程和本地的实现类)
    • 懒汉式:只有当这个服务被注入到其他类中时启动引入流程,也就是说用到了才会开始服务引入。
      • 在应用的Spring IOC 容器刷新完毕(spring Context初始化)之后,扫描所有的Bean,将Bean中带有@ServiceExpose/@ServiceReference注解的field获取到,然后创建field类型的代理对象,创建完成后,将代理对象set给此field。后续就通过该代理对象创建服务端连接,并发起调用。(dubbo默认)
  • 服务端:与懒汉式一样。

那么怎么知道Spring IOC刷新完成,这里就使用一个Spring提供的监听器,当Spring IOC刷新完成,就会触发监听器。

 四.服务注册到ZK/从Zk获得服务

Zookeeper采用节点树的数据模型,类似linux文件系统,/,/node1,/node2 比较简单。不懂Zookeeper请移步:Zookeeper原理

我们采用的是对每个服务名创建一个持久节点,服务注册时实际上就是在zookeeper中该持久节点下创建了一个临时节点,该临时节点存储了服务的IP、端口、序列化方式等。

客户端获取服务时通过获取持久节点下的临时节点列表,解析服务地址数据:

客户端监听服务变化:

 五.生成代理类对象

这里使用JDK的动态代理,也可以使用cglib或者Javassist(dobbo使用)。

  1. public class ClientProxyFactory {
  2. /**
  3. * 获取代理对象,绑定 invoke 行为
  4. *
  5. * @param clazz 接口 class 对象
  6. * @param <T> 类型
  7. * @return 代理对象
  8. */public <T> T getProxyInstance(Class<T> clazz) {
  9. return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
  10. final Random random = new Random();
  11.  
  12. @Override
  13. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  14. // 第一步:通过服务发现机制选择一个服务提供者暴露的服务
  15. String serviceName = clazz.getName();
  16. final List<ServiceInfo> serviceInfos = serviceDiscovery.listServices(serviceName);
  17. logger.info("Rpc server instance list: {}", serviceInfos);
  18. if (CollectionUtils.isEmpty(serviceInfos)) {
  19. throw new RpcException("No rpc servers found.");
  20. }
  21.  
  22. // TODO: 这里模拟负载均衡,从多个服务提供者暴露的服务中随机挑选一个,后期写方法实现负载均衡
  23. final ServiceInfo serviceInfo = serviceInfos.get(random.nextInt(serviceInfos.size()));
  24.  
  25. // 第二步:构造 rpc 请求对象
  26. final RpcRequest rpcRequest = new RpcRequest();
  27. rpcRequest.setServiceName(serviceName);
  28. rpcRequest.setMethod(method.getName());
  29. rpcRequest.setParameterTypes(method.getParameterTypes());
  30. rpcRequest.setParameters(args);
  31.  
  32. // 第三步:编码请求消息, TODO: 这里可以配置多种编码方式
  33. byte[] data = messageProtocol.marshallingReqMessage(rpcRequest);
  34.  
  35. // 第四步:调用 rpc client 开始发送消息
  36. byte[] byteResponse = rpcClient.sendMessage(data, serviceInfo);
  37.  
  38. // 第五步:解码响应消息
  39. final RpcResponse rpcResponse = messageProtocol.unmarshallingRespMessage(byteResponse);
  40.  
  41. // 第六步:解析返回结果进行处理
  42. if (rpcResponse.getException() != null) {
  43. throw rpcResponse.getException();
  44. }
  45. return rpcResponse.getRetValue();
  46. }
  47. });
  48. }
  49. }

六.负载均衡

本实现支持两种主要负载均衡策略,随机和轮询,其中他们都支持带权重的随机和轮询,其实也就是四种策略。

七.Netty通信

服务端和客户端基本一样,这里只展示服务端的代码。代理对象在Spring启动的时候就生成了,但是没有调用,每一个调用(请求)都会生成一个Netty的连接。

  1. public class NettyRpcServer extends RpcServer {
  2.  
  3. @Override
  4. public void start() {
  5. // 创建两个线程组
  6. EventLoopGroup bossGroup = new NioEventLoopGroup();
  7. EventLoopGroup workerGroup = new NioEventLoopGroup();
  8. try {
  9. // 创建服务端的启动对象
  10. ServerBootstrap serverBootstrap = new ServerBootstrap()
  11. // 设置两个线程组
  12. .group(bossGroup, workerGroup)
  13. // 设置服务端通道实现类型
  14. .channel(NioServerSocketChannel.class)
  15. // 服务端用于接收进来的连接,也就是boosGroup线程, 线程队列大小
  16. .option(ChannelOption.SO_BACKLOG, 100)
  17. .childOption(ChannelOption.SO_KEEPALIVE, true)
  18. // child 通道,worker 线程处理器
  19. .childHandler(new ChannelInitializer<SocketChannel>() {
  20. // 给 pipeline 管道设置自定义的处理器
  21. @Override
  22. public void initChannel(SocketChannel channel) {
  23. ChannelPipeline pipeline = channel.pipeline();
  24. pipeline.addLast(new NettyServerHandler());
  25. }
  26. });
  27.  
  28. // 绑定端口号,同步启动服务
  29. ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
  30. channel = channelFuture.channel();
  31. // 对关闭通道进行监听,变为同步
  32. channelFuture.channel().closeFuture().sync();
  33. } catch (Exception e) {
  34. logger.error("server error.", e);
  35. } finally {
  36. // 释放线程组资源
  37. bossGroup.shutdownGracefully();
  38. workerGroup.shutdownGracefully();
  39. }
  40. }

实现具体handler

  1. public class NettyServerHandler extends ChannelInboundHandlerAdapter {
  2. //当通道就绪就会触发该方法
  3. @Override
  4. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  5. //进行记录
  6. logger.info("channel active: {}", ctx);
  7. }
  8.  
  9. //读取数据实际(这里我们可以读取客户端发送的消息)
  10. @Override
  11. public void channelRead(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg) throws Exception {
  12. //将数据读到buffer中
  13. final ByteBuf msgBuf = (ByteBuf) msg;
  14. final byte[] reqBytes = new byte[msgBuf.readableBytes()];
  15. msgBuf.readBytes(reqBytes);
  16. }
  17.  
  18. //数据读取完毕
  19. @Override
  20. public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
  21. //使用反射获找到目标方法进行返回
  22. final byte[] respBytes = requestHandler.handleRequest(reqBytes);
  23. ctx.writeAndFlush(respBytes);
  24. }
  25.  
  26. //处理异常, 一般是需要关闭通道
  27. @Override
  28. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  29. ctx.close();
  30. }
  31. }

八.序列化协议

对计算机网络稍微有一点了解的同学都知道,数据在网络中传输是二进制的:01010101010101010,类似这种,只有二进制数据才能在网络中传输。但是在编码之前我们一般先进行序列化,目的是为了优化传输的数据量。因为有的数据太大,需要进行空间优化。

那么我们来区分一下序列化和编码:我画一张图大家都全明白了


定义一个序列化协议,放入作为一个handler放入pipeline中。

Netty支持多种序列化,比如jdk,Json,ProtoBuf 等,这里使用ProtoBuf,其序列化后码流小性能高,非常适合RPC调用。接下来看怎么使用ProtoBuf?

  • 1.编写需要序列化的类xxx.proto:ProtoBuf有自己的语法规则(自行百度)

  • 2.通过官网提供的protoc.exe生成对应的Java代码
  • 3.前面通过工具生成的代码(AnimalProto)已经帮我们封装好了序列化和反序列化的方法,我们只需要调用对应方法即可

引入Protobuf的依赖

  1. <dependency>
  2. <groupId>com.google.protobuf</groupId>
  3. <artifactId>protobuf-java</artifactId>
  4. <version>2.4.1</version>
  5. </dependency>

序列化:

  1. /**
  2. * 调用对象构造好的Builder,完成属性赋值和序列化操作
  3. * @return
  4. */
  5. public static byte[] protobufSerializer(){
  6. AnimalProto.Animal.Builder builder = AnimalProto.Animal.newBuilder();
  7. builder.setId(1L);
  8. builder.setName("小猪");
  9. List<String> actions = new ArrayList<>();
  10. actions.add("eat");
  11. actions.add("run");
  12. builder.addAllActions(actions);
  13. return builder.build().toByteArray();
  14. }

反序列化:

  1. /**
  2. * 通过调用parseFrom则完成反序列化
  3. * @param bytes
  4. * @return
  5. * @throws InvalidProtocolBufferException
  6. */
  7. public static Animal deserialize(byte[] bytes) throws Exception {
  8. AnimalProto.Animal pAnimal = AnimalProto.Animal.parseFrom(bytes);
  9. Animal animal = new Animal();
  10. animal.setId(pAnimal.getId());
  11. animal.setName(pAnimal.getName());
  12. animal.setActions(pAnimal.getActionsList());
  13. return animal;
  14. }

测试:

  1. public static void main(String[] args) throws Exception {
  2. byte[] bytes = serializer();
  3. Animal animal = deserialize(bytes);
  4. System.out.println(animal);
  5. }

以下看到是能正常序列化和反序列化的:

 九.通信协议

通信协议主要是解决网络传输问题,比如TCP拆包粘包问题。

TCP问题:

  • TCP拆包粘包主要就是把一些数据合并或者分割开进行发送,这时候有的数据就不完整,有的数据就多出一部分,就会造成问题。一般使用TCP协议都需要考虑拆包粘包问题
  • tcp粘包和半包问题就是因为滑动窗口。 因为不管你的数据是多少长度,怎么分割每一条数据。但是tcp只按照我滑动窗口的长度发送。
  • 本质是因为TCP是流式协议,消息无边界。

解决方案:业界的主流协议的解决方案可以归纳如下

  • 消息定长:例如每个报文的大小为固定长度100字节,如果不够用空格补足。(定长解码器)
  • 在包尾加特殊结束符进行分割。(分隔符编码器)

  • 消息长度+消息:将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。

    • Netty自带:

    • 自定义编解码器

这里只是列举出来编码过程,解码是逆过程。(说白了,编码就是找着固定的格式进行写入,解码就是照着固定的格式读)


恭喜你,已经学会写RPC框架了,想深入了解的朋友可以参照源码。进行学习,升级。

寄语:生命只有一次,你要活得畅快淋漓

参考文章:

https://mp.weixin.qq.com/s/yaIOCfEigkQMm2kt6I7Orw

https://mp.weixin.qq.com/s/ltos1nEgktec5pn47xAgMw

从零开始实现一个分布式RPC框架的更多相关文章

  1. 设计一个分布式RPC框架

    0 前言 提前先祝大家春节快乐!好了,先简单聊聊. 我从事的是大数据开发相关的工作,主要负责的是大数据计算这块的内容.最近Hive集群跑任务总是会出现Thrift连接HS2相关问题,研究了解了下内部原 ...

  2. 一个轻量级分布式RPC框架--NettyRpc

    1.背景 最近在搜索Netty和Zookeeper方面的文章时,看到了这篇文章<轻量级分布式 RPC 框架>,作者用Zookeeper.Netty和Spring写了一个轻量级的分布式RPC ...

  3. 一个轻量级分布式 RPC 框架 — NettyRpc

    原文出处: 阿凡卢 1.背景 最近在搜索Netty和Zookeeper方面的文章时,看到了这篇文章<轻量级分布式 RPC 框架>,作者用Zookeeper.Netty和Spring写了一个 ...

  4. 轻量级分布式 RPC 框架

    @import url(/css/cuteeditor.css); 源码地址:http://git.oschina.net/huangyong/rpc RPC,即 Remote Procedure C ...

  5. 【转】轻量级分布式 RPC 框架

    第一步:编写服务接口 第二步:编写服务接口的实现类 第三步:配置服务端 第四步:启动服务器并发布服务 第五步:实现服务注册 第六步:实现 RPC 服务器 第七步:配置客户端 第八步:实现服务发现 第九 ...

  6. Dubbo[一个分布式服务框架

    http://alibaba.github.io/dubbo-doc-static/User+Guide-zh.htm#UserGuide-zh-API%E9%85%8D%E7%BD%AE http: ...

  7. 轻量级分布式RPC框架

    随笔- 139  文章- 0  评论- 387  一个轻量级分布式RPC框架--NettyRpc   1.背景 最近在搜索Netty和Zookeeper方面的文章时,看到了这篇文章<轻量级分布式 ...

  8. 一个入门rpc框架的学习

    一个入门rpc框架的学习 参考 huangyong-rpc 轻量级分布式RPC框架 该程序是一个短连接的rpc实现 简介 RPC,即 Remote Procedure Call(远程过程调用),说得通 ...

  9. 轻量级分布式 RPC 框架(转)

    RPC,即 Remote Procedure Call(远程过程调用),说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样. RPC 可基于 HTTP 或 TCP 协议,Web Servi ...

随机推荐

  1. [ vue ] quasar框架踩坑:在vue文件外导入路由,执行router.push('/')没有效果

    问题描述: 1. 如图所示的项目结构目录, axios.js 文件负责拦截全局请求和回复,我在拦截回复的代码中写了:如果服务器回复了一个401错误,则执行Router.push('/'),但是该方法失 ...

  2. spring security 登出操作 详细说明

    1.前言 这里专门 做 spring security 登出操作 的  详细记录 2.操作 (1)目录结构 (2)在security 拦截规则配置文件添加退出登录支持 源码 package com.e ...

  3. 539. Minimum Time Difference

    Given a list of 24-hour clock time points in "Hour:Minutes" format, find the minimum minut ...

  4. Redis作缓存

    缓存策略三要素:缓存命中率   缓存更新策略  最大缓存容量.衡量一个缓存方案的好坏标准是:缓存命中率.缓存命中率越高,缓存方法设计的越好. 三者之间的关系为:当缓存到达最大的缓存容量时,会触发缓存更 ...

  5. javascript 获取<td>标签内的值。

    当网页被加载时,浏览器会创建页面的文档对象模型(Document Object Model). HTML DOM 模型被构造为对象的树. 通过可编程的对象模型,JavaScript 获得了足够的能力来 ...

  6. javaObject类-equals方法及覆盖

    1 package face_object; 2 /* 3 * Object:所有类的根类. 4 * Object是不断抽取而来的,具备所有对象都具备的共性内容. 5 * 常用的共性功能: 6 * 7 ...

  7. 【转载】Systemd 入门教程:实战篇

    作者: 阮一峰 日期: 2016年3月 8日 上一篇文章,我介绍了 Systemd 的主要命令,今天介绍如何使用它完成一些基本的任务. 一.开机启动 对于那些支持 Systemd 的软件,安装的时候, ...

  8. iptables规则管理

    查看规则 iptables -t filter -L INPUT -n -v --line  省略-t选项时,表示默认操作filter表中的规则 添加规则 注意点:添加规则时,规则的顺序非常重要  - ...

  9. 幸运转轮(Cakra)

    题目描述 lxx参加了某卫视举办的一场选秀节目,凭借曼妙的舞姿和动人的歌声,他在众多idol中脱颖而出.现在在他的面前,有四个大转轮,这四个转轮将决定他能否赢得最终大奖--出道,机会只有一次!   每 ...

  10. vue之Better-Scroll组件 将滚动条滚到最底部

    首先我们需要使用scrollTo这个方法: scrollTo(x, y, time, easing) 参数: {Number} x 横轴坐标(单位 px) {Number} y 纵轴坐标(单位 px) ...