仿照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>
  1. 使用一个本地文件模拟
    提供者注册时写入文件,消费者使用时读取文件即可
  2. 使用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;
    }

执行流程:

提供者:

  1. 暴露服务
URL url = new URL("localhost",8080);  // 这个url是自己封装的一个对象,只包括ip和端口

/* 将<对应接口,url>注册到远程(redis/zookeeper)*/
RemoteMapRegister.regist(IHelloService.class.getName(),url);

/* 服务器本地注册:<接口,对应实现类> */
LocalRegister.regist(IHelloService.class.getName(), HelloServiceImpl.class);
  1. 开启服务器,接受请求
// 开启服务器,使用工厂获得服务器
Protocol protocol = ProtocolFactory.getProtocol();
protocol.start(url);    // 开启服务器,并且将服务器阻塞接受请求
  1. 处理接受到的请求( 展示使用 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());

消费者:

  1. 获取代理对象,使用代理对象直接调用方法
public static void main(String[] args) {
    IHelloService helloService = ProxyFactory.getProxy(IHelloService.class);    // 获取要调用接口的代理对象

    String alan = helloService.sayHello("alan");     // 直接调用方法,底层进行方法调用
    System.out.println(alan);
}
  1. 代理对象开启服务器,并且发送请求( 展示使用 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;

运行验证:

首先启动服务提供者,然后启动服务消费者:

  • 提供者打印:
  • java开源OCR 手写识别文字_zookeeper

  • 消费者打印:

java开源OCR 手写识别文字_rpc_02

这里测试的只是一个消费者与一个提供者。

当使用到提供者集群时,也是可以的,本框架消费端提供了负载均衡功能(最简单的 随机负载均衡 )

一个建议的dubbo框架就这样完成了,虽然简单,但是五脏俱全!