基于 Java 实现简易版的 rpc 框架

原创
2020/03/21 05:00
阅读数 1.8K

开篇

本文会通过 Java 实现一个简单的 rpc 框架,rpc 的概念在此不多赘述。相信看完整个实现过程,会对 rpc 的实现原理有更清晰的,更直观的认识。

目标

最终效果类似 Dubbo 官方 Demo ,先来看几段代码:

  • 定义一个服务接口类
public interface HelloService {

	public void sayHello(String name);
}
  • 服务提供者的接口实现类
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String name) {
        return "hello " + name;
    }
}
  • 服务提供者
public class Provider {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.1l"});
        context.start();
        System.in.read(); // 按任意键退出
    }
}
  • 服务消费者
public class Consumer {
    public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(省略参数);
        context.start();
        DemoService demoService = (DemoService)context.getBean("demoService"); // 获取远程服务代理
        String hello = demoService.sayHello("world"); // 执行远程方法
        System.out.println( hello ); // 显示调用结果
    }

以上是用 Dubbo 仿写的官方 Demo。

具体实现

1. 配置

通过 Dubbo 的 Demo 案例可以看到它是通过加载 Sping 配置文件来读取一些参数, 而我们初步实现的时候,参数配置部分是先直接写死在方法中。

2. 通信

既然是远程过程调用,那么肯定要涉及到通信,先不管 Dubbo 用了什么通信协议, 我们就基于 Socket 实现消费者和服务提供者之间的通信。

如下图所示,有两个进程分别为 Consumer(消费者) 和 Provider(提供者),他们之间通过 Socket 发送和回复给对方一条消息,具体代码不展示了。 在这里插入图片描述

3. 服务接口及其实现类

这部分代码跟上面案例中的一模一样,消费者和提供者方都定义同一个 HelloService 接口,然后在服务提供者具体实现该接口及其方法。

4. 服务消费者

根据上面的案例,看着像是 dubbo 通过 Spring 的配置文件 实例化出来了一个 HelloService 对象,然后调用它的 sayHello 方法。 因此我们的消费者代码就有了如下的雏形:

public class ConsumerApp {
    public static void main(String[] args) {
        // 1. ‘实例化’ HelloService
        HelloService helloService = // doSomething
		// 2. 必要步骤,必须有跟 dubbo 一样的视觉效果
        String result = helloService.sayHello("nimo");
        System.out.println(result);
    }
}

可是我们如何将一个 HelloService 接口实例化? 很明显在消费者端自定义实现类是违背我们的初衷的,因为实现类是在服务提供者方的, 因此实例化是不可行的。

那么怎么办呢?

答案是反射 + 动态代理。

服务提供者

从 dubbo 案例中可以看出,提供者的作用就是把服务暴漏出来,因此我们将服务暴漏过程封装到一个 export 方法中,而在 export 中调用 HelloServiceImpl 的 sayHello 方法,最后在其内部把方法返回结果发送给服务调用方。

把服务暴露出来

public class ProviderApp {
    public static void main(String[] args) throws IOException {
        HelloService helloService = new HelloServiceImpl();
        ServiceManager.export(helloService, 参数);
    }
}

因为服务接口的方法可能有多个,因此服务提供方需要一些参数,诸如要调用的方法名,参数类型,参数值等。而这些参数只能由客户端发送过来。

我们把这些参数封装到如下实体类(一定要实现序列化接口)中:

public class RpcData implements Serializable {
    // 方法名
    String methodName;
    // 参数类型
    Class<?>[] parameterTypes;
    // 参数
    Object[] arguments;
	// 省略get/set方法
}

服务提供方接收到参数后可以通过反射,调用对应的方法,export 方法如下所示:

ServerSocket server = new ServerSocket(port);
        while(true) {
            try {
                final Socket socket = server.accept();
                new Thread(() -> {
                    try {
                        // 读取消费者传来的参数
                        ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                        RpcData rpcData = (RpcData) input.readObject();
                        String methodName = rpcData.getMethodName();
                        Class<?>[] parameterTypes = rpcData.getParameterTypes();
                        Object[] arguments = rpcData.getArguments();
                        ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                        // 反射调用本地方法
                        Method method = service.getClass().getMethod(methodName, parameterTypes);
                        Object result = method.invoke(service, arguments);
                        // 返回结果给服务消费端
                        output.writeObject(result);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        // TODO 关闭流
                    }
                }).start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
服务引用

当服务提供者的需求明确以后,服务消费者的设计也就明朗了,因为消费者需要传指定参数,而这些参数可以通过动态代理获得,所以最后的代码如下所示:

   
     public static  <T> T refer(Class<T> interfaceClass, 潜在参数) {
        // 省略部分代码, 比如参数校验
        
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[]{interfaceClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
               // 开启 socket
                Socket socket = new Socket(host, port);
                try {
                    ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
					// 设定参数
                    RpcData rpcData = new RpcData();
                    rpcData.setMethodName(method.getName());
                    rpcData.setParameterTypes(method.getParameterTypes());
                    rpcData.setArguments(arguments);
				
					// 发送给服务方
                    output.writeObject(rpcData);
					
					// 接收远程调用方法返回值
                    ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                    Object result = input.readObject();
                    return result;
                } finally {
                    // TODO 关闭流
                }
            }
        });
    }
执行流程

启动服务提供者的 main 方法, 等待消费者 socket 连接过来;当连接过来后,服务提供者读取流,然后把参数解析出来;利用反射调用 Service 的具体方法,得到结果后;最后通过 Socket 传到消费者端。

总结

以上就是“乞丐版” dubbo 的简单实现,总结下用到的知识点:

  • 序列化
  • Socket 通信
  • 反射
  • 动态代理

当然 Dubbo 也是基于此实现的,只不过 Dubbo 架构设计中进行了分层,比如:transport 网络传输层,serialize 数据序列化层,proxy 服务代理层等。

上面列出的知识点分别落地实现在对应的层次上,并且每种技术都提供了多种可选方案。

比如:

  • 动态代理有 jdk/javassist;
  • 通信框架可以有Netty,Mina;
  • 协议可以有dubbo协议,rmi,http和hessian协议等。
展开阅读全文
加载中
点击加入讨论🔥(1) 发布并加入讨论🔥
打赏
1 评论
2 收藏
0
分享
返回顶部
顶部