建议从Dubbo之@SPI开始看。

关键词:Dubbo    RPC


纯手写实现一个简单的RPC调用,帮助更好地学习和理解Dubbo

RPC-远程过程调用,我感觉可以理解成客户端(即消费者)通过TCP加上特定的消息协议访问服务端(即提供者),服务端根据消息协议内容调用本地方法并响应给客户端。就好像浏览器采用http协议,通过TCP传输去调用服务端接口一样,只不过http调用的是服务端的接口,接口其实对应着某个特定的方法。而RPC则直接调用服务端的方法。

关于协议:协议就是双方约定好的一种消息格式,这样发送消息的人发送出去的消息,接收消息的人才能够认识。就好比,两个人通信,发信人用的是中文,收信人呢就去查看新华字典来一个个读取信件中的内容,然后便知道和理解发信人的目的和行为了。你想想,如果此时的收信人拿出一本牛津词典查来查去,他是如论如何都不会理解中文的信件的。这里的新华字典就好比协议。

通过简单的代码来实现简单的RPC调用,这样更有助于理解和使用Dubbo,Dubbo包装了很多功能,理解起来还是蛮困难的。我感觉就像上面说的那样,本质还是TCP调用。TCP是传输层的协议了,比较底层,在JAVA中对应着就是Socket和ServerSocket。


█ 总体认识

  • 我代码都放在一个模块里面了,实际的项目中,客户端代码和服务端代码是分开的,有可能就是放在不同的服务器上。公共代码是客户端和服务端都需要的。
  • 客户端代码就是消费者,也就是服务的调用方;服务端代码就是提供者,也就是服务的提供者

█ 公共代码

package common;/**** 接口,面向接口调用。客户端调用的是接口的代理,* 服务端真正调用接口的实现类(即CatService或DogService)**/
public interface AnimalService {String say();int age(int age);}

协议类,客户端和服务端都需要使用。客户端根据协议的规定格式创建请求信息,服务端根据协议的规定格式解析请求内容。我这里的协议比较简单,就是字符串拼接,包含了请求的接口,方法和参数等信息(这样都是定位到调用的具体方法必不可少的条件)。dubbo-start的作用是用来标识一段请求消息的内容的。当多个客户端同时请求了服务端,服务端获取到的请求内容可能会被放到同一个字节数组里面,这样dubbo-start方面拆分每一条请求。请求的唯一标识的作用差不多,能够方便客户端解析响应消息的时候,对应上自己的哪一次请求。

package common;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 协议类,用于组装、拆解交流的信息** 协议规定:一个请求必须以dubbo-start开头,第二个是请求的唯一标识 第三个是类的全限定名,第四个是方法名 第五个是参数类型列表* 第六个是参数值,(参数类型和参数值,多个用逗号分割)* 值与值中间用一个空行分割**/
public class Protocol {/*** 构建请求信息,即客户端按照协议规定的格式构建请求信息* @param requestId 请求唯一标识* @param targetClass 请求的目标类* @param methodName 请求的目标方法* @param paramTypes 目标方法的参数类型* @param paramValues 传给目标方法的参数* @return*/public byte[] packRequest(String requestId, Class targetClass, String methodName, Class[] paramTypes, Object[] paramValues) {StringBuilder sb = new StringBuilder();// 拼接协议信息sb.append("dubbo-start").append(" ").append(requestId).append(" ").append(targetClass.getName()).append(" ").append(methodName).append(" ");// 处理参数类型if (paramTypes!=null && paramTypes.length>0) {StringBuilder paramType = new StringBuilder();for (Class type : paramTypes) {paramType.append(type.getName()).append(",");}// 去掉结尾的逗号String substring = paramType.toString().substring(0, paramType.toString().length() - 1);sb.append(substring).append(" ");}// 处理参数值if (paramValues!=null && paramValues.length>0) {StringBuilder paramValue = new StringBuilder();for (Object value : paramValues) {paramValue.append(value).append(",");}// 去掉结尾的逗号String substring = paramValue.toString().substring(0, paramValue.toString().length() - 1);sb.append(substring);}return sb.toString().getBytes();}/*** 拆解请求信息,即服务端解析客户端的请求信息* @param bytes* @param len* @return*/public Map<String, Object> unpackRequest(byte[] bytes, int len) {Map<String, Object> map = new HashMap<>();String recv = new String(bytes, 0, len);// 根据协议约定,按照空格分隔各个请求信息String[] split = recv.split(" ");System.out.println("请求信息:"+split);// 请求IDmap.put(Const.REQUEST_ID, split[1]);// 接口名map.put(Const.INTERFACE_NAME, split[2]);// 方法名map.put(Const.METHOD_NAME, split[3]);// 参数类型List<Class> paramTypelist = new ArrayList<>();if (split.length>4 && split[4]!=null) {String[] types = split[4].split(",");for (String type : types) {paramTypelist.add(convertParamType(type));}map.put(Const.PARAM_TYPES, paramTypelist);}// 参数值if (split.length>5 && split[5]!=null) {String[] values = split[5].split(",");List<Object> valueList = new ArrayList<>(values.length);for (int i=0; i<values.length; i++) {valueList.add(convertParamValue(paramTypelist.get(i), values[i]));}map.put(Const.PARAM_VALUES, valueList);}return map;}/*** 参数类型转换* @param type* @return*/private Class convertParamType(String type) {// 简单地举了几个例子if ("int".equals(type)) {return int.class;} else if ("java.util.ArrayList".equals(type) || "ArrayList".equals(type)) {return ArrayList.class;}return String.class;}/*** 参数值转换。解析的时候都转成了字符串,* 这里需要根据参数具体的类型转换* @param type* @param value* @return*/private Object convertParamValue(Class type, String value) {// 简单的举了几个例子if (type==int.class) {return Integer.parseInt(value);} else if (type==double.class) {return Double.parseDouble(value);}return value;}}
package common;/**** 常量类,抽出字符串**/
public class Const {// 请求IDpublic static final String REQUEST_ID = "requestId";// 请求目标接口类型public static final String INTERFACE_NAME = "interfaceName";// 请求目标方法public static final String METHOD_NAME = "methodName";// 请求方法参数类型public static final String PARAM_TYPES = "paramTypes";// 请求方法参数值public static final String PARAM_VALUES = "paramValues";}

█ 服务端

Service-包装了服务发布的功能,即通过创建的ServerSocket来接收和响应客户端的请求。

package server;import common.Const;
import common.Protocol;import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/**** 包装服务端服务发布的功能* */
public class Service<T> {// 用于存放接口和实现类对象的关系,作为服务端服务字典,方便查找private static ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>();// 协议,在服务端,协议的作用就是解析消费端发送过来的消息private Protocol protocol = new Protocol();// 接口的实现类private T obejct;// 接口类型private Class interfaceClass;public Service(Class interfaceClass, T object) {// 前置校验,借实现类是否实现了接口boolean assignableFrom = interfaceClass.isAssignableFrom(object.getClass());if (!assignableFrom) {throw new IllegalArgumentException("object 必须实现interfaceClass接口");}this.interfaceClass = interfaceClass;this.obejct = object;// 缓存起来,方便客户端调用的时候,查找目标类beanMap.put(interfaceClass.getName(), object);}public void export() {try {ServerSocket serverSocket = ServiceServer.getServerSocket();while (true) {// 这里会阻塞,等待客户端的连接Socket socket = serverSocket.accept();// 响应结果byte[] result = new byte[1];// 接收消费者的请求InputStream inputStream = socket.getInputStream();byte[] recv = new byte[1024];// 这里会阻塞,等待客户端的请求信息int len = inputStream.read(recv);// 根据协议,转换请求信息Map<String, Object> requestMap = protocol.unpackRequest(recv, len);// 获取调用的接口Object interfaceName = requestMap.get(Const.INTERFACE_NAME);// 获取接口实现类对象Object object = beanMap.get(interfaceName);if (object==null) {result = "请求的接口不存在".getBytes();} else {// 方法名Object methodName = requestMap.get(Const.METHOD_NAME);// 参数类型Class[] paramTypes = null;List<Class> paramTypeList = (List<Class>)requestMap.get(Const.PARAM_TYPES);if (paramTypeList!=null && paramTypeList.size()>0) {paramTypes = paramTypeList.toArray(new Class[paramTypeList.size()]);}// 参数值Object[] paramValues = null;List<Object> paramValueList = (List<Object>)requestMap.get(Const.PARAM_VALUES);if (paramValueList!=null && paramValueList.size()>0) {paramValues = paramValueList.toArray();}// 获取到调用的方法Method method = object.getClass().getMethod(methodName.toString(), paramTypes);// 方法调用结果Object invoke = method.invoke(object, paramValues);if (invoke!=null) {result = invoke.toString().getBytes();}}// 将结果返回给客户端OutputStream os = socket.getOutputStream();os.write(result);os.flush();os.close();inputStream.close();}} catch (Exception e) {e.printStackTrace();}}}
package server;import java.net.ServerSocket;/*** 创建ServerSocket,暴露服务* 多次服务暴露,都使用一个ServerSocket* 这里的端口号就固定写了8899**/
public class ServiceServer {private static volatile ServerSocket serverSocket;private ServiceServer() {throw new IllegalStateException();}/*** 创建一个ServerSocket,用于暴露服务。* @return*/public static ServerSocket getServerSocket() {if (serverSocket==null) {synchronized (ServiceServer.class) {if (serverSocket==null) {try {// 因为没有注册中心的功能,没法让消费者去感知服务的端口,这里端口就写死了serverSocket = new ServerSocket(8899);} catch (Exception e) {e.printStackTrace();}}}}return serverSocket;}}
package server;import common.AnimalService;public class DogService implements AnimalService {@Overridepublic String say() {return "this is a dog";}@Overridepublic int age(int age) {return age;}}
package server;import common.AnimalService;public class CatService implements AnimalService {@Overridepublic String say() {return "this is a cat";}@Overridepublic int age(int age) {return age + 10;}}

█ 客户端

package client;import common.Protocol;import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.util.UUID;/*** 服务端调用代理类* 客户端并不知道在服务端上接口具体的实现类是哪个,只能通过调用接口来获取。* 创建接口的代理,主要工作就是创建Socket去连接服务端,并按照协议格式发送请求**/
public class Reference implements InvocationHandler {// 协议private Protocol protocol = new Protocol();private Class interfaceClass;public Reference(Class interfaceClass) {this.interfaceClass = interfaceClass;}public Object getReference() {return Proxy.newProxyInstance(Reference.class.getClassLoader(), new Class[]{interfaceClass}, this);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Object result = null;String name = method.getName();// 如果是父类Object中的方法就直接返回方法名,这里举几个例子,不全if ("equals".equals(name) || "toString".equals(name)) {return name;}Socket socket = new Socket("localhost", 8899);// 客户端发送请求OutputStream os = socket.getOutputStream();// 通过约定好的协议格式构建请求信息byte[] requestByte = protocol.packRequest(UUID.randomUUID().toString(), interfaceClass, name, method.getParameterTypes(), args);os.write(requestByte);os.flush();os.close();// 客户端接收响应结果InputStream is = socket.getInputStream();byte[] recv = new byte[1024];int read = is.read(recv);is.close();// 这里应该还有解码器的,我就简单地写了一下Class<?> returnType = method.getReturnType();if (String.class==returnType) {result = new String(recv, 0, read);} else if (int.class==returnType || Integer.class==returnType) {return Integer.parseInt(new String(recv, 0, read));}return result;}
}

█ 运行服务端

package server;import common.AnimalService;public class Main {public static void main(String[] args) {// 暴露DogServiceService service = new Service(AnimalService.class, new DogService());// 启动一个ServerSocket,等待客户端的连接和接收消息service.export();}}

█ 运行客户端

package client;import common.AnimalService;public class Main {public static void main(String[] args) {// 客户端创建代理对象,代理去实现请求服务端的逻辑Reference reference = new Reference(AnimalService.class);AnimalService animalService = (AnimalService)reference.getReference();// 当调用say方法,实际会调用Reference的invoke方法String say = animalService.say();System.out.println(say);System.out.println("获取到的age="+animalService.age(10));// 输出结果:// this is a dog//获取到的age=10}}

█ 总结

上面代码写的粗糙,功能也粗糙,只是简单地模拟了一下RPC调用,实现了服务端和客户端,根据协议构建和解析协议的功能。希望看完能够帮助更好的去理解Dubbo。在Dubbo中还实现了注册中心,路由选择,负载均衡,容错等等强大的功能,这些功能有机会的话会在后面介绍(希望我能坚持写下去)。我想,只有理解了RPC的工作原理,才能更好的去学习和理解Dubbo的功能。

关于Dubbo的使用、服务暴露、服务注册、负载均衡等,官网已经有很详细的介绍了。我就不重复写了,写了也就是分析源码。具体请移步Dubbo官网慢慢品味:我简述一下自己关于Dubbo的理解:

服务端:

  • 服务暴露:将ServiceConfig对应的接口实现类对外提供调用服务,让客户端(消费者)能够请求调用。
  • 服务注册:将ServiceConfig对应的接口实现类注册到注册中心上,这样是对客户端透明,即提供了一个公示栏,让客户端调用者查看其可以调用哪些服务。
  • 一个<dubbo:service />或@Service,就对应一个ServiceConfig实例。ServiceConfig是Dubbo服务暴露和注册的起点。开始于export方法
  • ServiceConfig在暴露和注册服务时,使用了Invoker这样的代理类,能够灵活地调用具体的接口实现类,而不用写死代码。
  • 服务暴露时,一个Invoker又会被封装成一个Exporter,由这个Exporter负责具体的服务暴露和引用工作。
  • 所有被暴露的服务都会缓存在Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap(),exporterMap之中,key是服务的标识信息(端口,版本号,分组名)。这样当消费者待着请求信息过来请求时,Dubbo提取一些参数值,组装成key,去本地缓存中查找对应的Exporter,而Exporter中又包含了Invoker,这样自然就调用了实际的接口实现类方法了。
  • 真正做服务暴露和注册的是协议Protocol,不如DubboProtocol,RegistryProtocol。

Service -----> ServiceConfig -------> Invoker -----> Exporter

消费端:

  • 一个<dubbo:reference />或@Reference对应一个ReferenceConfig。起点是get方法。
  • 一个ReferenceConfig也是对应一个Invoker,通过Invoker代理服务端的调用。
  • Invoker外包装了一层Directory。这个可以理解成消费者端的本地缓存,缓存了服务端的服务调用。当发起远程调用时,要去Directory这个缓存中去查找对应的Invoker,由Invoker完成具体的调用过程。
  • Directory又被Cluster所有。Cluster根据Directory中缓存的Invoker列表完成路由、负载均衡和容错功能

Reference -----> ReferenceConfig ------> Cluster -------> Directory -----> Invoker------->(Exporter ------>Invoker----->Service)

Dubbo之手写RPC框架相关推荐

  1. 手写篇:如何手写RPC框架?

    手写篇:如何手写RPC框架? 首先我们讲下什么是RPC? RPC(Remote Procedure Call)远程过程调用协议,他是一种通过网络从远程计算机程序请求服务.简单的来说,就是通过网络进行远 ...

  2. MyRPCDemo netty+jdk动态代理+反射+序列化,反序列化手写rpc框架

    RPC RPC(remote procedure call)远程过程调用 RPC是为了在分布式应用中,两台主机的Java进程进行通信,当A主机调用B主机的方法时,过程简洁,就像是调用自己进程里的方法一 ...

  3. 第四篇 - 手写RPC框架

    Github源码下载地址:https://github.com/chenxingxing6/myrpc 一.前言 RPC(Remote Procedure Call)-远程过程调用,它是一种通过网络从 ...

  4. 手写RPC框架(六)

    v1.4 小更新 更新事项 暂定目标对启动类进行修改 直接集合 这个就直接看代码吧 不是特别难 难的地方我会点出来 启动引导类直接进行修改 可以传参 可以这样 当然 我想到了可以注解传参 注解构造 注 ...

  5. 手写RPC框架(十六)

    v2.7 更新:实现CGLIB动态代理 实现CGLIB动态代理 实现一下统一调用代理类,创建总调用类,和对应模板接口,调用注解,同时在每个consumerbootstrap进行修改 对应模板接口 pa ...

  6. 手写RPC框架(五)

    v1.3 (启动器依旧使用1.2 1.3版本在启动服务版本上尚未做出大变动 主要是增加了方便学习的功能) 更新事项 以下更新均在非阻塞模块进行更新,阻塞模块可供读者自己尝试 使用注解方式 改造一下启动 ...

  7. 手写RPC框架(八)

    v1.6 热补丁,nio目前来看最后的完善,使用Curator简化zookeeper的操作,优化调用体验 使用Curator创建服务注册和服务发现类(是看快速开始速成的) 服务注册类实现代码 pack ...

  8. Marco's Java【Dubbo 之手写Dubbo框架实现远程调用】

    前言 关于Dubbo入门的网上教程也特别多,因此我没有专门出关于Dubbo的系列博文(主要呢- 也是在忙些工作上的事儿),用Dubbo特别简单,但是想要把Dubbo学好,学精还得花费不少时间的,特别是 ...

  9. 手写RPC(一) 絮絮叨叨

    目录 前言 我的知识库 学会了? 学不动了? 前言 提到RPC(Remote Procedure Call)大家应该都不陌生,特别是像我一样做web开发的,可以说天天和rpc打交道.常见的rpc框架主 ...

  10. c++socket多个客户端通过不同端口与一个服务端通信_手写RPC,深入底层理解整个RPC通信...

    一.前言 RPC,远程过程调用,调用远程方法像调用本地方法一样.RPC交互分为客户端和服务端,客户端调用服务端方法,服务端接收数据并打印到控制台,并response响应给客户端. RPC和HTTP的联 ...

最新文章

  1. html表格筛选排序规则,excel表的排序功能你真的会吗?带你重新认识真正的排序功能...
  2. article.app.php,【求助】修改app\portal\AdminArticle.php二次开发提交数据出错的疑问
  3. android studio升级版本,导入项目出错
  4. bat 发送post请求_get post 请求
  5. 幼儿园视频监控系统设计方案
  6. Linus送出圣诞礼物:发布Linux 4.20,超35万行代码
  7. Python输入和输出
  8. 通用窗口类 Inventory Pro 2.1.2 Demo1(上)
  9. 【Luyten反编译工具】
  10. 网页三剑客的一些序列号
  11. 哈理工OJ—1309入侵检测(字符串处--剪枝)
  12. C语言 —— 回调函数
  13. 怎么恢复优盘里隐藏的数据 u盘隐藏数据恢复教程
  14. 日语入门难?学日语最好用的工具——早道五十音图
  15. LSDB和SPF算法
  16. android studio静态界面设计,2.3 使用Android Studio 简单设计UI界面
  17. matlab中syms x是什么意思,matlab中怎样定义未知数,如x,syms是什么意思?
  18. 初级系列11.个人所得税问题
  19. 一个屌丝程序猿的人生(四十六)
  20. mysql-快速入门

热门文章

  1. Linux系统日志分析与管理
  2. (原创)数字电路设计基础 大一期末 项目 交通灯控制器设计
  3. MyBatis:CRUD操作及配置解析
  4. 阿里云大数据工程师(ACP)认证考试大纲
  5. R plot图片背景设置为透明_万能转换:R图和统计表转成发表级的Word、PPT、Excel、HTML、Latex、矢量图等...
  6. java打开教程,jar文件打开教程
  7. [UPC] 2021秋组队17
  8. 利用百度api接口制作在线语音合成软件
  9. c语言中max的用法。
  10. Tempo - 分布式Loki链路追踪利器