手撕RPC系列(2)—客户端基于stub动态代理的RPC

Nafisa ·
更新时间:2024-11-13
· 967 次阅读

一、前言二、原理三、前置基础四、举例说明五、总结 一、前言

上一节 手撕RPC系列(1)—最原始的RPC通俗理解 中讲了一个最最简单的rpc思想的例子。那种方法的缺陷太多,平常写代码一般不会那样去写,今天我们在之前的基础上稍微进一步演进,引入stub的概念,stub在rpc里面是代理的意思,是个约定俗成的东西,所以不叫proxy,知道是这么个东西就行了。代理是干嘛的?我要做的事丢给别人去做,那个人就叫代理

二、原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ncz2xjSS-1586965963427)(C:\Users\AppData\Roaming\Typora\typora-user-images86874491567.png)]

为了一步一步的理解rpc,我今天不打算实现一个完全标准版的rpc,上图是是标准版,今天要讲的主要区别在红色的部分,只在client端加了stub,如下图

在这里插入图片描述

在上一章节的例子中,客户端要实现一堆通信的逻辑,耦合度太高,我能不能客户端只负责调用接口,中间的网络细节不用去管呢?红色部分的stub就是干这活的。

三、前置基础

反射:这个例子会比上一章节进阶一些的是,我们上一章是写死了只有findStudentByid一个方法的调用,假如Client端要调用的Server端接口有很多个呢?Client端可以通过socket把要调用的接口名传给Server端,Server端再通过接口名反射去调用已实现的接口方法。

动态代理:Client已经把调用Server端的具体细节交给了stub,需要stub动态代理生成了一个Client要调用的接口类,通过这个类去实现跟Server端的交互,让Client端实现只负责接口方法的调用,不用去关心一堆巴拉巴拉的网络细节

不具备上面两个基础的童靴不建议往下读

四、举例说明

代码结构:

common类跟上一章节一样:

Student.java #实体类,作为数据的传输和传入的对象

StudentService.java #接口类,定义一个findStudentByid接口,给客户端调用

StudentServiceImpl.java #接口实现类,实现findStudentByid接口,必须在服务端实现

rpc类:

Client.java #客户端类

Server.java #服务端类

Stub.java #代理类

以下是代码:

Student.java

package rpc2.common; public class Student { int id; String name; public Student(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + '}'; } }

StudentService.java

package rpc2.common; public interface StudentService { public Student findStudentByid(int id); }

StudentServiceImpl.java

package rpc2.common; public class StudentServiceImpl implements StudentService { @Override public Student findStudentByid(int id) { return new Student(id,"zhangsan"); } }

主要的实现逻辑看下面的代码,对应上面图片描述的3大类,Client,Stub,Server

Client.java

package rpc2.rpc; import rpc2.common.StudentService; public class Client { public static void main(String[] args) { // Client这里不用关注一堆网络交互的细节,直接调用Stub产生的代理对象的方法,既可完成整个链路的调用 StudentService service = Stub.getStub(); System.out.println(service.findStudentByid(123)); } }

Stub.java

package rpc2.rpc; import rpc2.common.Student; import rpc2.common.StudentService; import java.io.DataInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.Socket; /** * 但是这里仅仅实现了findStudentByid的方法代理,如果要实现其他方法的代理该怎么做呢? * 这里就要从协议层做出改进 * * 服务器端也要做出对应处理 */ public class Stub { public static StudentService getStub() { InvocationHandler h = new InvocationHandler() { /** * proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0,代理了StudentService类 * method:我们所要调用某个对象真实的方法的Method对象,也就相当于StudentService的findStudentByid方法 * args:指代代理对象方法传递的参数,也就是Client传递的123 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Socket socket = new Socket("127.0.0.1", 8888); ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); // 获取Client要调用的方法名 String methodName = method.getName(); // 获取Client要调用的方法传入的参数类型 Class[] parametersTypes = method.getParameterTypes(); // 把方法名写出去 oos.writeUTF(methodName); // 把参数类型写出去 oos.writeObject(parametersTypes); // 把参数写出去 oos.writeObject(args); oos.flush(); DataInputStream dis = new DataInputStream(socket.getInputStream()); int id = dis.readInt(); String name = dis.readUTF(); Student student = new Student(id, name); oos.close(); socket.close(); return student; } }; // 动态代理产生一个实现了StudentService的接口的代理对象,参数1是类加载器,参数2传入被代理的接口类,参数3是InvocationHandler,被代理时反射调用的方法,也就是Stub给Client端处理的一堆巴拉巴拉的细节 Object o = Proxy.newProxyInstance(StudentService.class.getClassLoader(), new Class[]{StudentService.class}, h); // 返回实现StudentService接口类的代理对象 return (StudentService)o; } }

Server.java

package rpc2.rpc; import rpc2.common.Student; import rpc2.common.StudentService; import rpc2.common.StudentServiceImpl; import java.io.*; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8888); while (true) { Socket socket = serverSocket.accept(); process(socket); socket.close(); } } private static void process(Socket socket) throws Exception { InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); ObjectInputStream oos = new ObjectInputStream(in); DataOutputStream dos = new DataOutputStream(out); // 读取Client端通过socket传来的方法名 String methodName = oos.readUTF(); // 读取Client端通过socket传来的参数类型,考虑到同样的方法名但是不同的入参类型的情况,也就是方法的重载 Class[] parameterTypes = (Class[])oos.readObject(); // 读取Client端通过socket传来的参数值 Object[] args = (Object[])oos.readObject(); // 实例化StudentServiceImpl StudentService service = new StudentServiceImpl(); // 考虑到Client端可能调用Server端的多个方法,不仅仅是findStudentByid一个方法的情况,这时可以根据方法名和参数类型获取Method对象供后面反射调用StudentServiceImpl实现的方法 Method method = service.getClass().getMethod(methodName, parameterTypes); // 反射调用方法查询出结果 Student user = (Student)method.invoke(service, args); // 把结果写回给客户端 dos.writeInt(user.getId()); dos.writeUTF(user.getName()); dos.flush(); } }

最后先运行Server.java,再运行Client.java,输出:

Student{id=123, name='zhangsan'} 五、总结

跟着注释阅读很容易把代码读懂,是不是很简单呢?终于离珠峰又更近了一步,但是这个版本也依旧是个阉割版的rpc。问题一:我们在调用过程中,Student对象里面的所有字段细节,万一增减字段呢?可不可以做到不用关系对象的细节呢?问题二:Server端逻辑依旧很复杂,能不能做到像Client一样也通过Stub去处理细节问题呢?欲知答案,请持续关注本系列。

上一节:手撕RPC系列(1)—最原始的RPC通俗理解


作者:喜剧之皇



客户端 代理 rpc

需要 登录 后方可回复, 如果你还没有账号请 注册新账号