模拟RPC远程调用

前言: 相信做开发的小伙伴们应该都使用过类似Dubbo,Feign的远程方法调用框架,那么有人了解其中的原理吗?本文就用通俗易懂的demo来实现RPC。

前置知识:
动态代理,socket(TCP/IP协议,也可以使用http等其他协议),反射

1.代码实现

  • 服务端:
/**
 * 服务端
 *
 * @author wrx
 * @date 2022/5/13 22:30
 */
public class Server {
    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        SpringSinglePool.initInstance();
        ServerSocket serverSocket = new ServerSocket(8888);
        while (running) {
            Socket socket = serverSocket.accept();
            System.out.println("链接进来了" + JSONUtil.toJsonStr(socket));
            process(socket);
        }
        System.out.println("服务端关闭");
        serverSocket.close();
    }

    private static void process(Socket socket) {
        try {
            ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
            //客户端按顺序传了类名,方法名,参数类型,参数值。服务端这边顺序读取
            String clazzName = inputStream.readUTF();
            String methodName = inputStream.readUTF();
            Class<?>[] parameterTypes = (Class<?>[]) inputStream.readObject();
            Object[] args = (Object[]) inputStream.readObject();

            //从服务注册表获取实例,例如spring的ContextLoader.getCurrentWebApplicationContext().getBean(
            Object implObj = SpringSinglePool.singletonObjects.get(clazzName);
            Method method = implObj.getClass().getMethod(methodName, parameterTypes);
            Object returnObj = method.invoke(implObj, args);
            outputStream.writeObject(returnObj);
            outputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 用户服务实现类
 *
 * @author wrx
 * @date 2022/5/13 0:31
 */
public class UserServiceImpl implements IUserService {
    @Override
    public User findUserById(Integer id) {
        //实际是从db查
        return new User(id, "Alice");
    }
}
/**
 * 假装我是spring池 ☺
 * @author wrx
 * @date 2022/5/14 23:30
 */
public class SpringSinglePool {
    /**
     * key:beanName  value:实例
     */
    public static Map<String, Object> singletonObjects = new HashMap<>();

    /**
     * 初始化实例
     */
    public static void initInstance(){
        singletonObjects.put("IUserService",new UserServiceImpl());
    }

}
  • 服务端暴露的接口
/**
 * 服务接口 (服务端提供了实现,客户端只要通过stub调用即可)
 *
 * @author wrx
 * @date 2022/5/13 0:31
 */
public interface IUserService {
    User findUserById(Integer id);
}
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private Integer id;
    private String name;

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
  • 客户端:
/**
 * 远程对象在客户端的代理
 * 为屏蔽客户调用远程主机上的对象,必须提供某种方式来模拟本地对象,这种本地对象称为存根(stub),存根负责接收本地方法调用,并将它们委派给各自的具体实现对象
 *
 * @author wrx
 * @date 2022/5/13 22:54
 */
public class Stub {

    /**
     * 生成代理类
     *
     * @param: clazz 服务端暴露出来的接口(服务端提供了实现)
     * @return: 返回代理后的实例
     * @author: wrx
     * @date: 2022/5/13 23:15
     */
    public static <T> T getStub(Class<T> clazz) {
        InvocationHandler handler = new InvocationHandler() {
            /*
             * 生成的代理实例调用任意方法都会进入此逻辑
             *
             * @param: proxy 代理对象
             * @param: method 调用的方法
             * @param: args 方法参数
             * @return: 调用代理方法后返回的对象
             * @author: wrx
             * @date: 2022/5/13 22:55
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
                Socket socket = new Socket("127.0.0.1", 8888);
                ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
                //将类名,方法名,参数类型(方法可能重载,需要根据类型判断),参数值传给服务端
                //若服务端有多个实现类,这边还要传版本号过去version
                String clazzName = clazz.getSimpleName();
                String methodName = method.getName();
                Class<?>[] parameterTypes = method.getParameterTypes();
                outputStream.writeUTF(clazzName);
                outputStream.writeUTF(methodName);
                outputStream.writeObject(parameterTypes);
                outputStream.writeObject(args);
                outputStream.flush();

                //接收服务端的响应
                ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
                Object readObject = inputStream.readObject();
                System.out.println("服务端响应的对象:" + JSONUtil.toJsonStr(readObject));
                //关闭逻辑应在finally中,此处为代码简洁直接在方法抛异常
                socket.close();
                outputStream.close();
                inputStream.close();
                return readObject;
            }
        };
        Object proxyInstance = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, handler);
        //将代理实例转换为class对应的类
        return clazz.cast(proxyInstance);
    }
}
/**
 * 测试远程调用
 *
 * @author wrx
 * @date 2022/5/13 23:02
 */
public class Test {
    public static void main(String[] args) {
        IUserService stub = Stub.getStub(IUserService.class);
        //服务端不需要知道接口的实现,就能获取返回值,实现了远程调用
        System.out.println(stub.findUserById(66));
    }
}

2.说明

以Feign举例: 当我们需要远程调用时,通过服务端暴露一个接口 IUserService,并提供它的实现 IUserServiceImpl。客户端只要引入 接口 IUserService,不需要考虑实现,直接调用 IUserService中的方法即可。

流程: 通过动态代理生成一个代理类,并在代理类调用任意方法时,执行代理后的方法(即 invoke 中的逻辑),此时会建立socket连接至服务端,并将类元信息发送给服务端(服务端才能根据信息判断要调用哪个接口服务)。服务端解析后,将方法实现的返回值写回给客户端,实现了远程调用。

3.对序列化的改进

序列化: 网络传输中都需要序列化;把对象转为二进制字节数组称为序列化。
目前demo中使用的是ObjectOutputStream,即JDK自带的 Serializable,这种方式只支持java,而且性能较差(序列化后的字节长度比较长,无用信息多)

解决: 使用序列化框架 Hessian

   <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.38</version>
        </dependency>
/**
 * 序列化工具
 *
 * @author wrx
 * @date 2022/5/15 0:48
 */
public class HessianUtil {
    /**
     * 序列化
     *
     * @param: o 要转为字节数组的对象
     * @return: 字节数组
     * @author: wrx
     * @date: 2022/5/15 0:23
     */
    public static byte[] serialize(Object o) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(o);
        hessian2Output.flush();
        byte[] bytes = byteArrayOutputStream.toByteArray();
        byteArrayOutputStream.close();
        hessian2Output.close();
        return bytes;
    }

    /**
     * 反序列化
     *
     * @param: bytes 字节数组
     * @return: 反序列化为对象
     * @author: wrx
     * @date: 2022/5/15 0:23
     */
    public static Object deserialize(byte[] bytes) throws IOException {
        ByteInputStream byteInputStream = new ByteInputStream();
        Hessian2Input hessian2Input = new Hessian2Input(byteInputStream);
        Object o = hessian2Input.readObject();
        byteInputStream.close();
        hessian2Input.close();
        return o;
    }
}
public class Server {
    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        SpringSinglePool.initInstance();
        ServerSocket serverSocket = new ServerSocket(8888);
        while (running) {
            Socket socket = serverSocket.accept();
            System.out.println("链接进来了" + JSONUtil.toJsonStr(socket));
            process(socket);
        }
        System.out.println("服务端关闭");
        serverSocket.close();
    }

    private static void process(Socket socket) {
        try {
//            ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
            Hessian2Input inputStream = new Hessian2Input(socket.getInputStream());
            //客户端按顺序传了类名,方法名,参数类型,参数值。服务端这边顺序读取
            String clazzName = inputStream.readString();
            String methodName = inputStream.readString();
            Class<?>[] parameterTypes = (Class<?>[]) inputStream.readObject();
            Object[] args = (Object[]) inputStream.readObject();

            //从服务注册表获取实例,例如spring的ContextLoader.getCurrentWebApplicationContext().getBean(
            Object implObj = SpringSinglePool.singletonObjects.get(clazzName);
            Method method = implObj.getClass().getMethod(methodName, parameterTypes);
            Object returnObj = method.invoke(implObj, args);
//            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
            Hessian2Output outputStream = new Hessian2Output(socket.getOutputStream());
            outputStream.writeObject(returnObj);
            outputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Stub {

    /**
     * 生成代理类
     *
     * @param: clazz 服务端暴露出来的接口(服务端提供了实现)
     * @return: 返回代理后的实例
     * @author: wrx
     * @date: 2022/5/13 23:15
     */
    public static <T> T getStub(Class<T> clazz) {
        InvocationHandler handler = new InvocationHandler() {
            /*
             * 生成的代理实例调用任意方法都会进入此逻辑
             *
             * @param: proxy 代理对象
             * @param: method 调用的方法
             * @param: args 方法参数
             * @return: 调用代理方法后返回的对象
             * @author: wrx
             * @date: 2022/5/13 22:55
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
                Socket socket = new Socket("127.0.0.1", 8888);
//                ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
                Hessian2Output outputStream = new Hessian2Output(socket.getOutputStream());
                //将类名,方法名,参数类型(方法可能重载,需要根据类型判断),参数值传给服务端
                String clazzName = clazz.getSimpleName();
                String methodName = method.getName();
                Class<?>[] parameterTypes = method.getParameterTypes();
//                outputStream.writeUTF(clazzName);
//                outputStream.writeUTF(methodName);
                outputStream.writeString(clazzName);
                outputStream.writeString(methodName);
                outputStream.writeObject(parameterTypes);
                outputStream.writeObject(args);
                outputStream.flush();

                //接收服务端的响应
//                ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
                Hessian2Input inputStream = new Hessian2Input(socket.getInputStream());
                Object readObject = inputStream.readObject();
                System.out.println("服务端响应的对象:" + JSONUtil.toJsonStr(readObject));
                //关闭逻辑应在finally中,此处为代码简洁直接在方法抛异常
                socket.close();
                outputStream.close();
                inputStream.close();
                return readObject;
            }
        };
        Object proxyInstance = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, handler);
        //将代理实例转换为class对应的类
        return clazz.cast(proxyInstance);
    }
}

4.一些细节优化思路

在这里插入图片描述
1.注册中心注册的思路: 服务端搞个Map<接口名,List<ip+端口+协议>>,启动时将需要的服务提供类及ip端口注册(list是因为服务端可能是集群)。 客户端通过接口名从注册中心,随便搞个负载均衡算法取出ip+端口+协议,作为socket的参数发送请求。

要求:

  • 共享数据: 可以利用redis这种单独的应用做Map共享数据,给予客户端及服务端共享访问(nacos,zk也行)。但是这样性能比较低,可以在本地搞个Map做本地缓存(查到redis则加入本地的Map)
  • 监听数据的变化: 如果服务提供者变多了等咋办?所以我们需要引入一些机制 - > 【redis的消费订阅】
  • 心跳检测: 如果服务提供者挂了咋办?定时心跳检测,无心跳则过期掉。

2.容错降级思路:
直接把 Stub 类中的代理实现 invoke方法try catch起来,异常捕获中简单重试几次,重试后还是失败,则读取配置想要展示的降级信息。


3.假设服务端还在开发中:
在这里插入图片描述

客户端无法改动到服务端代码,所以改动Stub,响应mock数据,Stub 类中的代理实现 invoke方法直接取mock的内容响应。

 String mock = System.getProperty("mock");
 if (mock!=null && mock.startsWith("return:")){
    return mock.replace("return:","");
 }

4.支持不同协议
新增接口 Protocol,例socket协议的A类实现Protocol的 send方法,dubbo协议的B类实现Protocol的seng方法。(传参;读配置,工厂模式判断要调用A类还是B类的实现)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值