Dubbo系列 之 浅谈RPC

本文将带你抽丝剥茧,揭开RPC神秘面纱的一角

一、什么是RPC?

RPC(Remote Procedure Call)远程过程调用。
与之对应的是 LPC(Local Procedure Call)本地方法调用。

举个例子:

public class CommonDTO implements Serializable {

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "CommonDTO{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class CommonService {
    public String sayHi(CommonDTO param) {
        return "hi " + param.getName() + " ! You're " + param.getAge() + " years old!";
    }
    
	public String sayHi() {
        return "hello world!";
    }
}

现在项目A有一个CommonServie类,包含两个个sayHi方法,那么在项目A中,我们就能通过以下方式调用某一方法:

public class Test {
    public static void main(String[] args) {
        CommonService commonService = new CommonService();
        CommonDTO param = new CommonDTO();
        param.setName("zhangsan");
        param.setAge(18);
        commonService.sayHi(param);
    }
}

这就是本地方法调用。

如果另一个项目B需要通过网络请求的方式来调用项目A中CommonServie类中的sayHi方法,就叫做远程过程调用。
如果项目B直接把项目A当做依赖引入,就是通过本地方法调用的方式调用该方法。

二、如何实现一个RPC框架?

通过RPC框架,我们可以像调用本地方法一样调用远程机器上的方法。
那么,问题来了,让我们一步一步的看。

2.1 什么是像调用本地方法一样调用远程机器上的方法?

先看看本地方法的实现:

CommonService commonService = new CommonService();
CommonDTO param = new CommonDTO();
param.setName("zhangsan");
param.setAge(18);
commonService.sayHi(param);

项目B中,我们肯定也希望通过commonService.sayHi(param);的方式去调用项目A的这个方法。

我们很明显就能发现两个问题:

  1. 怎么定位到服务的提供端?
  2. CommonService类从哪来?
  3. 怎么让服务提供端知道,需要调用的类是 CommonService,调用的方法是sayHi方法,并且该方法有一个实体类型的参数?
  4. 怎么能在执行sayHi方法的时候,去调用服务提供端的这个方法?

带着疑问,我们继续。

2.2 如何准确的调用到远程服务器上的目标方法?

这个时候,必须要知道,远程调用的服务器地址、端口号、调用的类名、方法名,以及参数类型、参数值,才能准确的进行方法的调用。

需要参数类型,是因为方法存在重载的情况,比如本例当中,CommonService类中就存在两个sayHi方法,必须加上参数类型,才能准确的找到调用的方法。

2.3 如何保证服务消费端、提供端的类名、方法名与参数类型的一致性?

首先可以确定,调用的方法,肯定是一个抽象方法,因为我们并不知道该方法内部的实现逻辑,具体的实现肯定是在远程调用的另一端中。

我们常采用的一种方式,就是将远程调用的方法提取成公共的接口,将这些接口以及传参用的实体抽取到一个模块中,然后让服务提供端和消费端都引入该模块。

服务提供端、消费端共同引用的模块:

public interface CommonService {
    /**
     * 有参方法
     * @param param
     * @return
     */
    String sayHi(CommonDTO param);

    /**
     * 无参方法
     * @return
     */
    String sayHi();
}

服务提供端的真实业务逻辑:

public class CommonServiceImpl implements CommonService {
    @Override
    public String sayHi(CommonDTO param) {
        return "hi " + param.getName() + " ! You're " + param.getAge() + " years old!";
    }

    @Override
    public String sayHi() {
        return "hello world!";
    }
}

这样,远程调用的时候,类名、方法名、参数类型就不存在不一致的情况了。

2.4 服务消费端与提供端之间,如何进行网络传输?

这就要求我们约定好网络传输使用的协议,是http协议还是tcp传输协议。

由于http在应用层中完成,整个通信的代价较高,基于tcp进行远程调用,数据传输在传输层完成,更适合对效率要求比较高的场景,本文以建立socket链接举例。

数据传输时,无法直接传递参数,因此需要消费端把参数转换成字节流,传给服务提供端,然后服务提供端将字节流转换成自身能读取的格式,是一个序列化反序列化的过程。

同时我们要约定好,序列化时各个参数的顺序。

服务消费端序列化方法如下:

Socket socket = new Socket(host, port);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
// 类名
objectOutputStream.writeUTF(clazz.getName());
// 方法名
objectOutputStream.writeUTF(method.getName());
// 参数类型
objectOutputStream.writeObject(method.getParameterTypes());
// 参数值
objectOutputStream.writeObject(args);

ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object result = objectInputStream.readObject();
socket.close();

所以对应的服务提供端反序列方法如下:

ServerSocket serverSocket = new ServerSocket(port);
Socket socket = null;
try {
    socket = serverSocket.accept();

    ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
    // 类名
    String className = objectInputStream.readUTF();
    // 方法名
    String methodName = objectInputStream.readUTF();
    // 参数类型
    Class<?>[] parameterType = (Class<?>[]) objectInputStream.readObject();
    // 参数
    Object[] arguments = (Object[]) objectInputStream.readObject();

   	......

    // 返回结果
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
    objectOutputStream.writeObject(result);
} catch (Exception e){
    System.out.println("处理失败!" + e.getMessage());
} finally {
    if (socket != null){
        try {
            socket.close();
        } catch (IOException e) {
            socket = null;
            System.out.println("关闭异常!" + e.getMessage());
        }
    }
}

这时又出现了两个新问题:

  1. 服务消费端,怎么将序列化的逻辑封装起来,真正做到像调用本地方法一样,去调用远程方法?
  2. 服务提供端,怎么根据传输的数据,找到真正执行的方法?

2.5 服务消费端,如何做到像调用本地方法一样,去调用远程方法?

在执行方法的时候,去进行远程调用,我们应该能联想到需要对方法进行增强。这个时候大家应该能联想到动态代理吧!

public class Center {
	public static <T> T getProxy (Class<T> clazz, String host, int port){
	        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[] {clazz}, (proxy, method, args) -> {
	
			// 采用反射,像调用本地方法一样调用远程方法
            Socket socket = new Socket(host, port);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeUTF(clazz.getName());
            objectOutputStream.writeUTF(method.getName());
            objectOutputStream.writeObject(method.getParameterTypes());
            objectOutputStream.writeObject(args);

            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object result = objectInputStream.readObject();
            socket.close();
            return result;
       });
    }
}

目前为止,服务消费端进行远程调用的逻辑已经完整了:

  1. 设置服务提供端的url地址以及端口
  2. 生成远程调用的类的一个代理类
  3. 执行方法的时候,通过代理类,去进行网络请求,进行远程调用
public static void main(String[] args) {
    CommonService commonService = Center.getProxy(CommonService.class, "127.0.0.1", 8081);
    CommonDTO param = new CommonDTO();
    param.setName("zhangsan");
    param.setAge(18);
    System.out.println(commonService.sayHi(param));
}

2.6 服务提供端,怎么根据传输的数据,找到真正执行的方法?

服务提供端,真正执行的是接口的实现类,这就要求服务提供端,将接口的实现类暴露出来,通过CommonService类映射到真正的实现类,去执行具体的方法。

public class Center {

	// 接口和实现类的映射
    public static ConcurrentHashMap<String, Class> service = new ConcurrentHashMap<>();

    /**
     * 注册
     *
     *
     * @param commonService
     * @param port
     * @throws Exception
     */
    public static void regist(HashMap<String, Class> commonService, int port) throws Exception {
        service.putAll(commonService);
        ServerSocket serverSocket = new ServerSocket(port);
        Socket socket = null;
        try {
            socket = serverSocket.accept();

            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            // 类名
            String className = objectInputStream.readUTF();
            // 方法名
            String methodName = objectInputStream.readUTF();
            // 参数类型
            Class<?>[] parameterType = (Class<?>[]) objectInputStream.readObject();
            // 参数
            Object[] arguments = (Object[]) objectInputStream.readObject();

            Class target = service.get(className);
            // 反射执行
            Method method = target.getMethod(methodName, parameterType);
            Object result = method.invoke(target.newInstance(), arguments);

            // 返回结果
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(result);
        } catch (Exception e){
            System.out.println("处理失败!" + e.getMessage());
        } finally {
            if (socket != null){
                try {
                    socket.close();
                } catch (IOException e) {
                    socket = null;
                    System.out.println("关闭异常!" + e.getMessage());
                }
            }
        }
    }
}

服务提供端的启动:

public static void main(String[] args) {
    HashMap<String, Class> service = new HashMap<>();
    service.put(CommonService.class.getName(), CommonServiceImpl.class);
    try {
        Center.regist(service, 8081);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2.3 一个简易RPC框架结构

将服务消费端、提供端的Center类提取到公共模块中。
整个项目的结构如下:
在这里插入图片描述
api模块,存放远程调用的接口及参数:
在这里插入图片描述
center模块,服务提供者的注册与启动,以及服务消费者的真正调用:
在这里插入图片描述
consumer模块,消费者进行远程调用:
在这里插入图片描述
producer模块,其实叫provider更合适,进行服务的暴露:
在这里插入图片描述
码云仓库地址.

三、总结

大致上一个 RPC 框架需要一个注册中心,需要约定通信协议、序列化的格式,此外还需要负载均衡策略、容错机制、和监控运维。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值