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