若何实现一个简朴的RPC

若何实现一个简朴的RPC

局域网基础,MAC地址,MAC地址表作用及老化,一分钟了解下

RPC的实现原理

正如上一讲所说,RPC主要是为领会决的两个问题:

  • 解决分布式系统中,服务之间的挪用问题。
  • 远程挪用时,要能够像内陆挪用一样利便,让挪用者感知不到远程挪用的逻辑。

还是以计算器Calculator为例,若是实现类CalculatorImpl是放在内陆的,那么直接挪用即可:

若何实现一个简朴的RPC

 

 

现在系统酿成分布式了,CalculatorImpl和挪用方不在同一个地址空间,那么就必须要举行远程过程挪用:

若何实现一个简朴的RPC

 

 

那么若何实现远程过程挪用,也就是RPC呢,一个完整的RPC流程,可以用下面这张图来形貌:

若何实现一个简朴的RPC

 

 

其中左边的Client,对应的就是前面的Service A,而右边的Server,对应的则是Service B。 下面一步一步详细解释一下。

  1. Service A的应用层代码中,挪用了Calculator的一个实现类的add方式,希望执行一个加法运算;
  2. 这个Calculator实现类,内部并不是直接实现计算器的加减乘除逻辑,而是通过远程挪用Service B的RPC接口,来获取运算效果,因此称之为Stub
  3. Stub怎么和Service B确立远程通讯呢?这时刻就要用到远程通讯工具了,也就是图中的Run-time Library,这个工具将帮你实现远程通讯的功效,好比JAVA的Socket,就是这样一个库,固然,你也可以用基于Http协议的HttpClient,或者其他通讯工具类,都可以,RPC并没有规定说你要用何种协议举行通讯
  4. Stub通过挪用通讯工具提供的方式,和Service B确立起了通讯,然后将请求数据发给Service B。需要注重的是,由于底层的网络通讯是基于二进制花样的,因此这里Stub传给通讯工具类的数据也必须是二进制,好比calculator.add(1,2),你必须把参数值1和2放到一个Request工具里头(这个Request工具固然不只这些信息,还包罗要挪用哪个服务的哪个RPC接口等其他信息),然后序列化为二进制,再传给通讯工具类,这一点也将在下面的代码实现中体现;
  5. 二进制的数据传到Service B这一边了,Service B固然也有自己的通讯工具,通过这个通讯工具吸收二进制的请求;
  6. 既然数据是二进制的,那么自然要举行反序列化了,将二进制的数据反序列化为请求工具,然后将这个请求工具交给Service B的Stub处置;
  7. 和之前的Service A的Stub一样,这里的Stub也同样是个“假玩意”,它所卖力的,只是去剖析请求工具,知道挪用方要调的是哪个RPC接口,传进来的参数又是什么,然后再把这些参数传给对应的RPC接口,也就是Calculator的现实实现类去执行。很明显,若是是Java,那这里一定用到了反射
  8. RPC接口执行完毕,返回执行效果,现在轮到Service B要把数据发给Service A了,怎么发?一样的原理,一样的流程,只是现在Service B酿成了Client,Service A酿成了Server而已:Service B反序列化执行效果->传输给Service A->Service A反序列化执行效果 -> 将效果返回给Application,完毕。

理论的讲完了,是时刻把理论酿成实践了。

把理论酿成实践

本文的示例代码,可到Github下载。

首先是Client端的应用层怎么提议RPC,ComsumerApp:

public class ComsumerApp {
    public static void main(String[] args) {
        Calculator calculator = new CalculatorRemoteImpl();
        int result = calculator.add(1, 2);
    }
}

通过一个CalculatorRemoteImpl,我们把RPC的逻辑封装进去了,客户端挪用时感知不到远程挪用的贫苦。下面再来看看CalculatorRemoteImpl,代码有些多,然则实在就是把上面的2、3、4几个步骤用代码实现了而已,CalculatorRemoteImpl:

public class CalculatorRemoteImpl implements Calculator {
    public int add(int a, int b) {
        List<String> addressList = lookupProviders("Calculator.add");
        String address = chooseTarget(addressList);
        try {
            Socket socket = new Socket(address, PORT);

            // 将请求序列化
            CalculateRpcRequest calculateRpcRequest = generateRequest(a, b);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());

            // 将请求发给服务提供方
            objectOutputStream.writeObject(calculateRpcRequest);

            // 将响应体反序列化
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object response = objectInputStream.readObject();

            if (response instanceof Integer) {
                return (Integer) response;
            } else {
                throw new InternalError();
            }

        } catch (Exception e) {
            log.error("fail", e);
            throw new InternalError();
        }
    }
}

add方式的前面两行,lookupProviders和chooseTarget,可能人人会以为不明觉厉。

分布式应用下,一个服务可能有多个实例,好比Service B,可能有ip地址为198.168.1.11和198.168.1.13两个实例,lookupProviders,实在就是在寻找要挪用的服务的实例列表。在分布式应用下,通常会有一个服务注册中央,来提供查询实例列表的功效。

查到实例列表之后要挪用哪一个实例呢,只时刻就需要chooseTarget了,实在内部就是一个负载平衡计谋。

由于我们这里只是想实现一个简朴的RPC,以是暂时不思量服务注册中央和负载平衡,因此代码里写死了返回ip地址为127.0.0.1。

代码继续往下走,我们这里用到了Socket来举行远程通讯,同时行使ObjectOutputStream的writeObject和ObjectInputStream的readObject,来实现序列化和反序列化。

6个好用的网络监视工具,值得收藏

最后再来看看Server端的实现,和Client端异常类似,ProviderApp:

public class ProviderApp {
    private Calculator calculator = new CalculatorImpl();

    public static void main(String[] args) throws IOException {
        new ProviderApp().run();
    }

    private void run() throws IOException {
        ServerSocket listener = new ServerSocket(9090);
        try {
            while (true) {
                Socket socket = listener.accept();
                try {
                    // 将请求反序列化
                    ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                    Object object = objectInputStream.readObject();

                    log.info("request is {}", object);

                    // 挪用服务
                    int result = 0;
                    if (object instanceof CalculateRpcRequest) {
                        CalculateRpcRequest calculateRpcRequest = (CalculateRpcRequest) object;
                        if ("add".equals(calculateRpcRequest.getMethod())) {
                            result = calculator.add(calculateRpcRequest.getA(), calculateRpcRequest.getB());
                        } else {
                            throw new UnsupportedOperationException();
                        }
                    }

                    // 返回效果
                    ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                    objectOutputStream.writeObject(new Integer(result));
                } catch (Exception e) {
                    log.error("fail", e);
                } finally {
                    socket.close();
                }
            }
        } finally {
            listener.close();
        }
    }

}

Server端主要是通过ServerSocket的accept方式,来吸收Client端的请求,接着就是反序列化请求->执行->序列化执行效果,最后将二进制花样的执行效果返回给Client。

就这样我们实现了一个简陋而又详细的RPC。 说它简陋,是因为这个实现确实对照挫,在下一小节会说它为什么挫。 说它详细,是因为它一步一步的演示了一个RPC的执行流程,利便人人领会RPC的内部机制。

为什么说这个RPC实现很挫

这个RPC实现只是为了给人人演示一下RPC的原理,要是想放到生产环境去用,那是绝对不行的。

1、缺乏通用性 我通过给Calculator接口写了一个CalculatorRemoteImpl,来实现计算器的远程挪用,下一次要是有其余接口需要远程挪用,是不是又得再写对应的远程挪用实现类?这一定是很不利便的。

那该若何解决呢?先来看看使用Dubbo时是若何实现RPC挪用的:

@Reference
private Calculator calculator;

...

calculator.add(1,2);

...

Dubbo通过和Spring的集成,在Spring容器初始化的时刻,若是扫描到工具加了@Reference注解,那么就给这个工具天生一个署理工具,这个署理工具会卖力远程通讯,然后将署理工具放进容器中。以是代码运行期用到的calculator就是谁人署理工具了。

我们可以先反面Spring集成,也就是先不接纳依赖注入,然则我们要做到像Dubbo一样,无需自己手动写署理工具,怎么做呢?那自然是要求所有的远程挪用都遵照一套模板,把远程挪用的信息放到一个RpcRequest工具内里,发给Server端,Server端剖析之后就知道你要挪用的是哪个RPC接口、以及入参是什么类型、入参的值又是什么,就像Dubbo的RpcInvocation:

public class RpcInvocation implements Invocation, Serializable {

    private static final long serialVersionUID = -4355285085441097045L;

    private String methodName;

    private Class<?>[] parameterTypes;

    private Object[] arguments;

    private Map<String, String> attachments;

    private transient Invoker<?> invoker;

2、集成Spring 在实现了署理工具通用化之后,下一步就可以思量集成Spring的IOC功效了,通过Spring来建立署理工具,这一点就需要对Spring的bean初始化有一定掌握了。

3、长毗邻or短毗邻 总不能每次要挪用RPC接口时都去开启一个Socket确立毗邻吧?是不是可以保持若干个长毗邻,然后每次有rpc请求时,把请求放到义务行列中,然后由线程池去消费执行?只是一个思绪,后续可以参考一下Dubbo是若何实现的。

4、 服务端线程池 我们现在的Server端,是单线程的,每次都要等一个请求处置完,才能去accept另一个socket的毗邻,这样性能一定很差,是不是可以通过一个线程池,来实现同时处置多个RPC请求?同样只是一个思绪。

5、服务注册中央 正如之前提到的,要挪用服务,首先你需要一个服务注册中央,告诉你对方服务都有哪些实例。Dubbo的服务注册中央是可以设置的,官方推荐使用Zookeeper。若是使用Zookeeper的话,要怎样往上面注册实例,又要怎样获取实例,这些都是要实现的。

6、负载平衡 若何从多个实例里挑选一个出来,举行挪用,这就要用到负载平衡了。负载平衡的计谋一定不只一种,要怎样把计谋做成可设置的?又要若何实现这些计谋?同样可以参考Dubbo,Dubbo - 负载平衡

7、效果缓存 每次挪用查询接口时都要真的去Server端查询吗?是不是要思量一下支持缓存?

8、多版本控制 服务端接口修改了,旧的接口怎么办?

9、异步挪用 客户端挪用完接口之后,不想守候服务端返回,想去干点其余事,可以支持不?

10、优雅停机 服务端要停机了,还没处置完的请求,怎么办?

......

诸如此类的优化点另有许多,这也是为什么实现一个高性能高可用的RPC框架那么难的缘故原由。

固然,我们现在已经有许多很不错的RPC框架可以参考了,我们完全可以借鉴一下前人的智慧。

后面若是有(dian)机(zan)会(duo)的话,也将和人人分享一下若何一步一步优化现有的这块RPC代码,把它做成一个小型RPC框架!

参考

  • 一本很棒的分布式书籍:《大型网站系统与Java中间件实践》
  • Dubbo 使用文档
  • Dubbo 源码开发手册

思科路由重发布配置小案例,新手入门级,值得学习

分享到 :
相关推荐

发表评论

登录... 后才能评论