RPC远程过程调用可以说是分布式系统的基础,本文将通过Java演示一次普通的rpc调用到底发生了什么。

我曾经在网上看到有人提问,为什么RPC要叫作远程过程调用,而不叫作RMC远程方法调用。个人认为RPC的叫法才是合理的,远程调用的是某个过程,不一定是一个具体的方法。(你只要看过后面第一个版本的代码就能懂了)

这整个过程可以用一句话概括:机器A通过网络与机器B建立连接,A发送一些参数给B,B执行某个过程,并把结果返回给A。

在写代码之前,先说一个前置背景,假设我们有一个商品类

public class Product implements Serializable {

    private Integer id;
private String name; public Product(Integer id, String name) {
this.id = id;
this.name = name;
} //toString() //get set 方法
}

有一个商品服务接口

public interface IProductService {

    Product getProductById(Integer id);
}

服务端有商品服务接口的实现类

public class ProductServiceImpl implements IProductService {
@Override
public Product getProductById(Integer id) {
//实际上这里应该去查询数据库获得数据,下面简化了
return new Product(id, "手机");
}
}

下面我们通过客户端发送一个商品id到服务端,服务端获得id后通过商品服务类获取商品信息,返回给客户端

public class Client {

    public static void main(String[] args) throws Exception {
//建立Socket
Socket socket = new Socket("127.0.0.1", 8888);
//获取输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
//把商品Id通过网络传到服务端
dos.writeInt(123); socket.getOutputStream().write(baos.toByteArray());
socket.getOutputStream().flush(); //读取服务端返回的商品信息
DataInputStream dis = new DataInputStream(socket.getInputStream());
Integer id = dis.readInt(); //商品id
String name = dis.readUTF(); //商品名称
Product product = new Product(id, name);//通过服务端返回的商品信息生成商品 System.out.println(product); //关闭流资源为了方便阅读,没有做try-catch处理
dos.close();
baos.close();
socket.close();
}
} public class Server {
private static boolean running = true; public static void main(String[] args) throws Exception {
//建立服务端Socket
ServerSocket ss = new ServerSocket(8888);
//不断监听,处理客户端请求
while (running) {
Socket socket = ss.accept();
process(socket);
socket.close();
}
ss.close();
} private static void process(Socket socket) throws Exception {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
DataInputStream dis = new DataInputStream(is);
DataOutputStream dos = new DataOutputStream(os); //读取客户端发过来的id
Integer id = dis.readInt();
//调用服务类生成商品
IProductService service = new ProductServiceImpl();
Product product = service.getProductById(id);
//把商品的信息写回给客户端
dos.writeInt(id);
dos.writeUTF(product.getName());
dos.flush(); dos.close();
dis.close();
os.close();
is.close();
}
}

上面的是RPC远程调用的原始简易版本,可以看到联网的代码写死在了客户端中,网络部分代码和getProductById()耦合在了一起,如果想把别的方法改为远程调用还要重新写一遍联网的代码,非常麻烦。

在实际的使用中,我们会编写各种各样的远程调用,打个比方,IProductService接口以后可能会扩展成这样:

public interface IProductService {

    Product getProductById(Integer id);

    Product getProductByName(String name);

    Product getMostExpensiveProduct();
}

我们总不可能为每个方法都编写一段网络连接的代码吧,我们得想到一种办法为所有的方法都嵌入一段共用的网络连接代码。

那具体应该怎样嵌入呢?这里我们可以用到代理模式。

在Java中许多优秀的框架都用到了代理模式做代码嵌入,比如说Mybatis。它把JDBC连接部分的代码通过代理模式嵌入到sql语句的周围,让我们专注于写sql。

首先,服务端的代码要进行修改,由于多个方法共用一套联网的代码,我们要在服务端识别调用的是哪一个方法:

public class Server {

    private static boolean running = true;

    public static void main(String[] args) throws Exception {
//......
} private static void process(Socket socket) throws Exception {
//获取输入流,输出流
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
ObjectInputStream ois = new ObjectInputStream(is);
ObjectOutputStream oos = new ObjectOutputStream(os);
//获取本次远程调用的方法名
String methodName = ois.readUTF();
//获取本次远程调用方法的参数类型
Class[] parameterTypes = (Class[]) ois.readObject();
//获取具体的参数对象
Object[] args = (Object[]) ois.readObject(); //创建商品服务类实例,(往后这里可以继续优化)
IProductService service = new ProductServiceImpl();
//根据远程获取的方法名和参数,调用相应的方法
Method method = service.getClass().getMethod(methodName, parameterTypes);
Product product = (Product) method.invoke(service, args);
//把结果写回给客户端
oos.writeObject(product); oos.close();
ois.close();
socket.close();
}
}

然后在客户端,我们创建一个新的代理类,对外提供一个getStub获取代理类的方法。使用JDK的动态代理需要三个参数,一个是类加载器,一个是接口的class类,最后一个是InvocationHandler实例。

JDK动态代理背后的逻辑是这样的:JVM会根据接口的class类动态创建一个代理类对象,这个代理对象实现了传入的接口,也就是说它拥有了接口中所有方法的实现。方法具体的实现可以由用户指定,也就是调用InvocationHandlerinvoke方法。

invoke方法中有三个参数,分别是proxy代理类,method调用的方法,args调用方法的参数。我们可以在invoke方法中对具体的实现方法进行增强,在本案例中就是进行网络调用。

public class Stub {

    public static IProductService getStub() {

        InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//和服务端建立Socket连接
Socket socket = new Socket("127.0.0.1", 8888);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//拿到远程调用的方法名
String methodName = method.getName();
//拿到远程调用方法的参数类型
Class[] parametersTypes = method.getParameterTypes();
//把方法名传递给服务端
oos.writeUTF(methodName);
//把方法参数类型传递给服务端
oos.writeObject(parametersTypes);
//把方法参数传递给服务端
oos.writeObject(args);
oos.flush();
//获取远程调用的返回结果
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Product product = (Product) ois.readObject(); ois.close();
oos.close();
socket.close();
return product;
}
};
Object o = Proxy.newProxyInstance(IProductService.class.getClassLoader(), new Class[]{IProductService.class}, h);
return (IProductService) o;
}
}

这个新版本比第一个版本又美好了一些,但是其实还可以继续优化。现在我们的代理只能够返回IProductService的实现类,得想办法让它返回任意类型的服务实现类。

思路和远程调用方法相似,在远程调用方法时,我们把方法的名称,参数类型,参数传递给服务端;现在要动态创建服务类,我们可以把服务接口的名字传给服务端。服务端拿到远程接口的名字后,就可以从服务注册表中找到对应服务实现类。

至于服务实现类如何注册到服务注册表,这里提供一个思路:可以考虑使用Spring的注解注入。这和我们平时写spring代码是相似的,在创建完服务实现类后我们会加上注解@Service,这样我们就可以在收到远程调用后,遍历使用了@Service的Bean,找到对应的实现类。

总结

如果想要搭建一个简易的RPC框架,我认为有4个点:

  1. 代理问题
  2. 服务实例化问题
  3. 序列化问题
  4. 通信问题

本篇文章的侧重点是前两个,代理问题和服务实例化问题,我们回顾一下:

  1. 首先通过代理模式解决了每个远程方法联网的问题
  2. 通过传递方法名,方法参数类型,参数,以及后面提及的直接传接口名字的形式解决服务实例化问题,这背后的原理是反射,传这些参数也是给反射用的。

关于另外两个点的延伸

在网络上传输数据避免不了序列化问题。Java自带的序列化方式效率比较低,如果要造一个RPC框架的话,我们可以考虑引入其他第三方的序列化框架。

通信问题也很重要,现有的RPC框架使用的通信协议也不相同,各有各的优缺点。但这部分手动实现可能比较困难,可以根据语言自身封装好的API进行选择。


参考:马士兵rpc的演化过程公开课

https://www.bilibili.com/video/BV1Ug4y1875i?p=2

Java讲解RPC的基本实现的更多相关文章

  1. 【Other】最近在研究的, Java/Springboot/RPC/JPA等

    我的Springboot框架,欢迎关注: https://github.com/junneyang/common-web-starter Dubbo-大波-服务化框架 dubbo_百度搜索 Dubbo ...

  2. 利用jmeter+JAVA对RPC的单接口(dubbo接口等)进行性能测试

    建立JAVA项目 建立maven项目,加入Jmeter所需要的JAR包依赖. POM.xml  加入如下: <dependency> <groupId>org.apache.j ...

  3. java之rpc/orm

    Netty线程模型 其中ChannelPiepline的设计模型采用的是Handler组成的责任链模型 blocking I/O 阻塞nonblocking I/O 非阻塞I/O multiplexi ...

  4. java实现RPC

    一,服务提供者 工程为battercake-provider,项目结构图如下图所示 1.1 先创建一个“卖煎饼”微服务的接口和实现类 package com.jp.service; public in ...

  5. 简单的RPC java实现

    RPC的名声大噪之时是在2003年,那一个“冲击波”病毒(Blaster Worm virus)袭卷全球的一年.而“冲击波”正是用着RPC这把刀来敲开了远程电脑的大门.当然RPC 有更多正面的应用,比 ...

  6. Hadoop中RPC协议小例子报错java.lang.reflect.UndeclaredThrowableException解决方法

    最近在学习传智播客吴超老师的Hadoop视频,里面他在讲解RPC通信原理的过程中给了一个RPC的小例子,但是自己编写的过程中遇到一个小错误,整理如下: log4j:WARN No appenders ...

  7. (转)简单的RPC java实现 .

    转:http://blog.csdn.net/jackliang55/article/details/7580563 我也承认,RPC的名声大噪之时是在2003年,那一个“冲击波”病毒(Blaster ...

  8. Java程序员的现代RPC指南

    Java程序员的现代RPC指南 1.前言 1.1 RPC框架简介 最早接触RPC还是初学Java时,直接用Socket API传东西好麻烦.于是发现了JDK直接支持的RMI,然后就用得不亦乐乎,各种大 ...

  9. Java RMI与RPC的区别

    转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6542811.html  一:RPC 远程过程调用 RPC(Remote Procedure Call Prot ...

随机推荐

  1. SpringCloud服务的注册发现--------Eureka实现高可用

    1,Eureka作为注册中心,掌管者服务治理的功能,十分重要,如果注册中心的服务一旦宕机,所有的服务就会挂了,为此,实现注册中心的集群(高可用)就显得十分必要了 2,Eureka 搭建集群 实现原理就 ...

  2. Prism 源码解读6-事件聚合

    0 介绍 事件提供的是1对多的绑定,通过委托链实现对订阅者的调用,事件必须要通过发布者调用.同时事件订阅是强引用,事件订阅者的生命周期总是大于等于事件发布者.如果代码中事件很多就会充斥着各种事件的订阅 ...

  3. java fork/join简单实践

    我们知道,java8中有并行流,而并行流在后台的实现是通过fork/join池来完成的,例如: List<Integer> a = buildList(); List<Integer ...

  4. python opencv 图片缺陷检测(讲解直方图以及相关系数对比法)

    一.利用直方图的方式进行批量的图片缺陷检测(方法简单) 二.步骤(完整代码见最后) 2.1灰度转换(将原图和要检测对比的图分开灰度化) 灰度化的作用是因为后面的直方图比较需要以像素256为基准进行相关 ...

  5. E - E CodeForces - 1100E(拓扑排序 + 二分)

    E - E CodeForces - 1100E 一个n个节点的有向图,节点标号从1到n,存在m条单向边.每条单向边有一个权值,代表翻转其方向所需的代价.求使图变成无环图,其中翻转的最大边权值最小的方 ...

  6. 【tensorflow2.0】处理文本数据

    一,准备数据 imdb数据集的目标是根据电影评论的文本内容预测评论的情感标签. 训练集有20000条电影评论文本,测试集有5000条电影评论文本,其中正面评论和负面评论都各占一半. 文本数据预处理较为 ...

  7. PTA数据结构与算法题目集(中文) 7-18

    PTA数据结构与算法题目集(中文)  7-18 7-18 银行业务队列简单模拟 (25 分)   设某银行有A.B两个业务窗口,且处理业务的速度不一样,其中A窗口处理速度是B窗口的2倍 —— 即当A窗 ...

  8. 从JDK源码学习Hashmap

    这篇文章记录一下hashmap的学习过程,文章并没有涉及hashmap整个源码,只学习一些重要部分,如有表述错误还请在评论区指出~ 1.基本概念 Hashmap采用key算hash映射到具体的valu ...

  9. Spring 中使用 ActiveMQ 笔记

    首先需要在 pom.xml 中添加如下两个 jar 包:spring-jms 与 activemq-core,其依赖的 jar 包会自动下载 接着进行相关配置 @Configuration publi ...

  10. 数据库学习 day2 检索数据

    上一节我们介绍了什么是数据库,以及一些基本的数据库术语 这一课介绍使用SELECT语句从表中检索一个或多个数据列. 关键字(Keyword) 作为SQL组成部分的保留字.关键字不能用作表和列的名字(类 ...