仿照dubbo手写一个RPC框架:
- dubbo介绍:
- 框架实现:
- 执行流程:
- 运行验证:
dubbo介绍:
dubbo: Dubbo是一款高性能、轻量级的开源 Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
目的: 实现调用远程服务像调用本地服务一样,将调用过程进行封装。在消费者端只需要一个要调用服务的接口,不需要实现,dubbo对该接口进行动态代理。并且支持多种调用协议/服务器。
框架实现:
- 动态代理接口(核心):动态代理要调用服务的接口,在这里完成了对方法的远程调用,传递一个inovocation。
public static <T> T getProxy(final Class interfaceClass){
/*动态代理,代理接口*/
return (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String mock = System.getProperty("mock");
if(mock != null && mock.startsWith("return:")){
return mock.replace("return",""); // 测试用,不调用远程服务
}
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),method.getParameterTypes(), args);
List<URL> urlList = RemoteMapRegister.get(interfaceClass.getName()); // 从注册中心获取到对应的URLs(可能有多台服务器提供服务)
/*负载均衡*/
URL url = LoadBalance.random(urlList);
Protocol protocol = ProtocolFactory.getProtocol();
return protocol.send(url,invocation);
}
});
}
- invocation类(通信的信息载体):包含要调用的接口名,方法名,参数类型,参数。
public class Invocation implements Serializable {
/*调用接口名*/
private String interfaceName;
/*调用方法名*/
private String methodName;
/*参数类型*/
private Class[] paramType;
/*参数*/
private Object[] params;
- 服务器与通信协议接口(通信的方式):包含两个方法,启动服务器与发送消息
/**
* 将多个协议抽象为一个Protocol
*/
public interface Protocol {
/*启动服务器*/
void start(URL url);
/**
* 发送数据
* @param url 发送的url
* @param invocation 发送的数据,包装为要给invocation
* @return
*/
String send(URL url, Invocation invocation);
}
- 使用工厂模式来获取需要的Protocol:
public class ProtocolFactory {
public static Protocol getProtocol() throws ProtocolNotFoundException {
/*通过命令行参数来决定通信使用dubbo还是http*/
String name = System.getProperty("protocolName");
/*默认为http请求*/
if(name == null || name.equals("")) name = "http";
switch(name) {
case "http":
return new HttpProtocol();
case "dubbo":
return new DubboProtocol();
default:
throw new ProtocolNotFoundException("此协议暂不支持");
}
}
}
- 注册中心:
记录:<接口名,对应提供者的URI>
- 使用一个本地文件模拟
提供者注册时写入文件,消费者使用时读取文件即可 - 使用zookeeper存储
- 使用curator操作zookeeper:
client = CuratorFrameworkFactory.newClient("localhost:2181", new RetryNTimes(3, 1000));
client.start();
- 注册方法:
String result = client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(String.format("/dubbo/service/%s/%s", interfaceName, JSONObject.toJSONString(url)), null);
System.out.println(result);
- 获取方法:
public static List<URL> get(String interfaceName) {
List<URL> urlList = new ArrayList<>();
try {
List<String> result = client.getChildren().forPath(String.format("/dubbo/service/%s", interfaceName));
for (String urlstr : result) {
urlList.add(JSONObject.parseObject(urlstr, URL.class));
}
REGISTER.put(interfaceName, urlList);
} catch (Exception e) {
e.printStackTrace();
}
return urlList;
}
执行流程:
提供者:
- 暴露服务
URL url = new URL("localhost",8080); // 这个url是自己封装的一个对象,只包括ip和端口
/* 将<对应接口,url>注册到远程(redis/zookeeper)*/
RemoteMapRegister.regist(IHelloService.class.getName(),url);
/* 服务器本地注册:<接口,对应实现类> */
LocalRegister.regist(IHelloService.class.getName(), HelloServiceImpl.class);
- 开启服务器,接受请求
// 开启服务器,使用工厂获得服务器
Protocol protocol = ProtocolFactory.getProtocol();
protocol.start(url); // 开启服务器,并且将服务器阻塞接受请求
- 处理接受到的请求( 展示使用 tomcat & Http协议 )
// java 13:获取req中内容,将InputStream转换为Invocation对象
// Invocation invocation = JSONObject.parseObject(req.getInputStream(),Invocation.class);
// java8:使用URL发送请求,获得InputStream,转换为ObjectInputStream,读取对象
InputStream inStream = req.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inStream);
Invocation invocation = (Invocation) objectInputStream.readObject();
String interfaceName = invocation.getInterfaceName();
/*通过接口获得对应实现类*/
Class implClass = LocalRegister.get(interfaceName);
/*通过方法名和参数类型获得唯一的方法*/
Method method = implClass.getMethod(invocation.getMethodName(),invocation.getParamType());
/*反射机制method.invoke进行方法调用*/
String result = (String) method.invoke(implClass.newInstance(),invocation.getParams());
// 打印结果,并且将结果写入到输出流
System.out.println("tomcat:" + result);
IOUtils.write(result,resp.getOutputStream());
消费者:
- 获取代理对象,使用代理对象直接调用方法
public static void main(String[] args) {
IHelloService helloService = ProxyFactory.getProxy(IHelloService.class); // 获取要调用接口的代理对象
String alan = helloService.sayHello("alan"); // 直接调用方法,底层进行方法调用
System.out.println(alan);
}
- 代理对象开启服务器,并且发送请求( 展示使用 tomcat & Http 协议 ):
// 使用JDK13的Java.net发送请求
// var request = HttpRequest.newBuilder()
// .uri(new URI("http", null, hostname, port, "/", null, null))
// .POST(HttpRequest.BodyPublishers.ofString(JSONObject.toJSONString(invocation)))
// .build();
// var client = java.net.http.HttpClient.newHttpClient();
//
// HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
//
// String result = response.body();
// 使用 JDK1.8的url进行发送网络请求
/*创建一个URL对象*/
URL url = new URL("http", hostname, port, "/");
/*建立连接*/
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
/*设置连接参数*/
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
/*输出流*/
OutputStream outputStream = httpURLConnection.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(invocation);
oos.flush();
oos.close();
/*发送请求,获得结果*/
InputStream inputStream = httpURLConnection.getInputStream();
String result = IOUtils.toString(inputStream);
return result;
运行验证:
首先启动服务提供者,然后启动服务消费者:
- 提供者打印:
- 消费者打印:
这里测试的只是一个消费者与一个提供者。
当使用到提供者集群时,也是可以的,本框架消费端提供了负载均衡功能(最简单的 随机负载均衡 )
一个建议的dubbo框架就这样完成了,虽然简单,但是五脏俱全!