开篇
本文会通过 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协议等。