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

一、前言

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

二、原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ncz2xjSS-1586965963427)(C:\Users\1\AppData\Roaming\Typora\typora-user-images\1586874491567.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通俗理解

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值