欢迎您的光临,本博所发布之文章皆为作者亲测通过,如有错误,欢迎通过各种方式指正。

文摘  RPC的初步理解和简单实例

其他 网络 1007 0评论

一、什么是RPC


1.RPC概念及分类


RPC全称为Remote Procedure Call,翻译过来为“远程过程调用”。目前,主流的平台中都支持各种远程调用技术,以满足分布式系统架构中不同的系统之间的远程通信和相互调用。远程调用的应用场景极其广泛,实现的方式也各式各样。


从通信协议的层面,大致可以分为:

· 基于HTTP协议的(例如基于文本的SOAP(XML)、Rest(JSON),基于二进制Hessian(Binary))

· 基于TCP协议的(通常会借助Mina、Netty等高性能网络框架)


从不同的开发语言和平台层面,分为:

· 单种语言或平台特定支持的通信技术(例如Java平台的RMI、.NET平台Remoting)

· 支持跨平台通信的技术(例如HTTP Rest、Thrift等)


从调用过程来看,分为:

· 同步通信调用(同步RPC)

· 异步通信调用(MQ、异步RPC)


2.常见的几种通信方式


1)远程数据共享(例如:共享远程文件,共享数据库等实现不同系统通信)

2)消息队列

3)RPC(远程过程调用)


3.序列化/反序列化


只有二进制数据才能在网络中传输,序列化和反序列化的定义是:

将对象转换成二进制流的过程叫做序列化

将二进制流转换成对象的过程叫做反序列化


4.互联网时代常见的RPC技术和框架


1)应用级的服务框架:

· Dubbo/Dubbox

· ZeroICE

· GRpc

· Spring Boot/Spring Cloud


2)基础通信框架:

· Protocol Buffers

· Thrift


3)远程通信协议:

· RMI

· Socket

· SOAP(HTTP XML)

· REST(HTTP JSON)


5.RPC的注意事项


1)性能

影响RPC性能的主要在几个方面:

1.序列化/反序列化的框架

2.网络协议,网络模型,线程模型等


2)安全

RPC安全的主要在于服务接口的鉴权和访问控制支持。


3)跨平台

跨不同的操作系统,不同的编程语言和平台。


跨平台RPC技术和常见框架介绍

· SOAP WebService

· Hessian

· HTTP Rest

· Thrift

· GRpc(Protobuffer)

· Zero ICE

· 消息中间件


二、为什么使用RPC


在学校学编程,我们写一个函数都是在本地调用就行了。但是在互联网公司,服务都是部署在不同服务器上的分布式系统,如何调用呢?

RPC技术简单说就是为了解决远程调用服务的一种技术,使得调用者像调用本地服务一样方便透明。

下图是客户端调用远端服务的过程:

111.png

1、客户端client发起服务调用请求。

2、client stub 可以理解成一个代理,会将调用方法、参数按照一定格式进行封装,通过服务提供的地址,发起网络请求。

3、消息通过网络传输到服务端。

4、server stub接受来自socket的消息

5、server stub将消息进行解包、告诉服务端调用的哪个服务,参数是什么

6、结果返回给server stub。

7、sever stub把结果进行打包交给socket

8、socket通过网络传输消息

9、client slub 从socket拿到消息。

10、client stub解包消息将结果返回给client。


一个RPC框架就是把步骤2到9都封装起来。


1.为什么需要RPC?


1、首先要明确一点:RPC可以用HTTP协议实现,并且用HTTP是建立在 TCP 之上最广泛使用的 RPC,但是互联网公司往往用自己的私有协议,比如鹅厂的JCE协议,私有协议不具备通用性为什么还要用呢?因为相比于HTTP协议,RPC采用二进制字节码传输,更加高效也更加安全。

2、现在业界提倡“微服务“的概念,而服务之间通信目前有两种方式,RPC就是其中一种。RPC可以保证不同服务之间的互相调用。即使是跨语言跨平台也不是问题,让构建分布式系统更加容易。

3、RPC框架都会有服务降级、流量控制的功能,保证服务的高可用。


2.一个简单例子


下面就举一个1+1=2 的远程调用的例子。客户端发送两个参数,服务端返回两个数字的相加结果。RpcConsumer类调用CalculateService中的Calculate方法, 首先通过RpcFramework中的call方法,注册自己想要调用那个服务,返回代理,然后就像本地调用一样去调用Calculate方法,计算People,````People```有两个属性都被赋值成1,返回这两个属性相加后的结果。

public class RpcConsumer {
public static void main(String args[]) {
    CalculateService service=RpcFramework.call(CalculateService.class,"127.0.0.1",8888);      
    People people=new People(1,1);
    String hello=service.Calculate(people);
    System.out.println(hello);
 
     }
}


生成动态代理的代码如下。客户端在调用方法前会先执行invoke方法,建立socket连接,把方法名和参数传递给服务端,然后获取返回结果。 

    //动态代理机制
    public static <T> T  call(final Class<?> interfaceClass,String host,int port){
         if(interfaceClass==null){
             throw new IllegalArgumentException("调用服务为空");
         }
        if(host==null||host.length()==0){
            throw new IllegalArgumentException("主机不能为null");
        }
         if(port<=0||port>65535){
            throw new IllegalArgumentException("端口不合法"+port);
         }
 
         return (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class<?>[]{interfaceClass},new CallerHandler(host,port));
    }
    static class CallerHandler implements InvocationHandler {
        private String host;
        private int port;
        public CallerHandler(String host, int port) {
            this.host = host;
            this.port = port;
            SERVER = new InetSocketAddress(host, port);
        }
 
        public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
 
            Socket socket = new Socket(host, port);
            try {
                ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                try {
                    output.writeUTF(method.getName());
                    output.writeObject(method.getParameterTypes());
                    output.writeObject(arguments);
                    ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                    try {
                        Object result = input.readObject();
                        if (result instanceof Throwable) {
                            throw (Throwable) result;
                        }
                        return result;
 
                    } finally {
                        input.close();
                    }
 
                } finally {
                    output.close();
                }
 
            } finally {
                socket.close();
            }
        }
    }


RpcProvider 类实现具体的Calculate方法。通过RpcFramework中的publish方法,发布自己的服务。

public class RpcProvider{
    public static void main(String[] args) throws Exception {
        CalculateService service =new CalculateServiceImpl();
        RpcFramework.publish(service,8888);
    }
}
 
interface CalculateService{
    String Calculate(People p);
}
 
class CalculateServiceImpl implements CalculateService{
    public String Calculate(People people){
        int res=people.getA()+people.getB();
        return "计算结果 "+res;
    }
 
}

 

发布服务的代码如下。服务端循环监听某个端口,采用java原生的序列化方法,读取客户端需要调用的方法和参数,执行该方法并将结果返回。

public static void publish(final Object service,int port) throws IOException {
        if(service==null)
            throw new IllegalArgumentException("发布服务不能是空");
        if(port<=0 || port >65535)
            throw new IllegalArgumentException("端口不合法"+port);
        ServerSocket server=new ServerSocket(port);
        while (true) {
            try{
                final Socket socket=server.accept();
                new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        try {
                            ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                            try {
                                String methodName = input.readUTF();
                                Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
                                Object[] arguments = (Object[]) input.readObject();
                                ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                                try {
                                    Method method = service.getClass().getMethod(methodName, parameterTypes);
                                    Object result = method.invoke(service, arguments);
                                    output.writeObject(result);
                                } catch (Throwable t) {
                                    output.writeObject(t);
                                } finally {
                                    output.close();
                                }
                            } finally {
                                input.close();
                            }
                        } finally {
                            socket.close();
                        }
                    } catch (Exception e) {
                            e.printStackTrace();
                    }
                }
                }).start();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }

可以看到正确返回计算结果2。

222.png

以上就是一个简单的RPC例子,下面我们看一下如何优化这个例子。


3.序列化和I/O模型的优化


数据序列化:

什么是序列化?序列化就是编码的过程,把对象或者数据结构转化成二进制字节码的过程。而反序列化就是把二进制字节码转化成数据结构或者对象。只有经过序列化后的数据才能在网络中传输。


I/O模型:

客户端和服务端的通信依赖Socket I/O。I/O 模型又可以分为:传统的阻塞 I/O(Blocking I/O)、非阻塞 I/O(Non-blocking I/O)、I/O 多路复用(I/O multiplexing)、异步 I/O(Asynchronous I/O)

下面针对上面的两个技术点进行优化。


4.用Protobuf优化数据序列化


数据序列化方法有很多种方法,常见的有Avro,Thrift,,XML,JSON,Protocol Buffer等工具。本文主要介绍的是Protobuf。Protobuf 全称““Protocol Buffer”” 是google 推出的高性能序列化工具。现已经在Github上开源。Protobuf采用tag来标识字段序号,用varint 和zigzag两种编码方式对整形做特殊的处理,Protobuf序列化后的数据紧凑,而且序列化时间短。下面两张图分别是Uber对不同的序列化框架做的比较结果。

从上面两张看出从数据压缩和时间维度上看,Protobuf 和 Thrift的性能都是非常优秀的。至于如何做到这么好的性能,本文不在这边细究,有兴趣的同学可以参考图解Protobuf编码以及Protocol Buffer 序列化原理大揭秘


5.用Netty优化I/O模型


网络通信中I/O模型可以大致分为以下四种(准确说是5种,这里不讨论信号驱动I/O,因为在真正的编程中,我们很少使用这种模型):

1、阻塞I/O

2、非阻塞I/O

3、I/O多路复用

4、异步I/O

我们知道I/O处理是非常耗时的,CPU的处理速度非常快,如何最大化的利用CPU的性能,就是要避免线程阻塞在I/O处理上。业界目前比较多的采用I/O多路复用和异步I/O提高性能。

如何理解这四种I/O模型,大家可以参照银行业务办理例子。


Netty 正是采用了第三种 I/O多路复用的方法,I/O多路复用对应Reactor模式。Reactor把耗时的网络操作编码交给专门的线程或者线程池去处理。比如下面这张图是Reactor模式示意图。图中mainReactor线程、subReactor线程、work线程这些不同的线程,分别干不同专业的事情,吞吐量自然上去了。

555.jpg

这里要再多说一句异步I/O。前面提到的三种I/O模型都归属于同步I/O,用户发起I/O请求后需要等待或者轮询内核I/O的完成。我现在用的是PHP中的swoole框架, 一款异步网络通信框架。当时我第一次听到异步I/O的感到很奇怪,因为之前看到有些文章里都有说到异步I/O往往对应的是Proactor模式,而Proactor在Linix中没有很好的实现。那么Swoole是如何实现异步I/O。这里就要提协程的概念了。协程可以理解为用户态的线程,他有两个特点:1、占用的资源更少。2、所有切换和调度都发生在用户态。Swoole底层就是借鉴了Go语言的协程,而Go语言之所有能受到关注和部分青睐,也是因为他引入了协程。这里特别要推荐知乎专栏里的协程,高并发IO的终级杀器的文章,通过简单的例子帮你理解协程。


那为什么协程可以提升IO效率?传统的阻塞IO模型中,一个线程在处理IO请求时就被阻塞了,不能再去处理其他IO请求,而服务器创建线程的数量是有限的(线程消耗比较高的内存资源),一个服务器能处理多少个客户端的连接又取决于可以创建多少个线程,这也是造成传统阻塞IO模型不能支持高并发的原因。协程提供了另一种角度去解决高并发问题:把线程占用的资源降下来。所以协程是十分轻量的,只有线程的几十分之一,通过创建更多的协程实现同步的写法。这里多说一句,目前Java对协程的支持可以通过开源的协程库Quasar实现,不过看到消息说Oracle已经在准备把协程引入到新的Java版本中。


6.优化结果的比较


下面给出的是压测代码,parallel变量控制并发的请求数。对阻塞I/O模型+java原生序列化方法压测。

public static void main(String[] args) throws Exception {
 
        //并行度10
        int parallel = 10;
 
        //开始计时
        StopWatch sw = new StopWatch();
        sw.start();
 
        CountDownLatch signal = new CountDownLatch(1);
        CountDownLatch finish = new CountDownLatch(parallel);
 
        for (int index = 0; index < parallel; index++) {
            CalcParallelRequestThread client = new CalcParallelRequestThread(signal, finish, index);
            new Thread(client).start();
        }
 
        //10个并发线程瞬间发起请求操作
        signal.countDown();
        finish.await();
 
        sw.stop();
 
        String tip = String.format("RPC调用总共耗时: [%s] 毫秒", sw.getTime());
        System.out.println(tip);
 
    }


下图是并发请求数是10的时候,返回了10个结果,QPS大致在181。

666.png

下图是将parallel调整到100。可以看到部分请求开始报错了。不能拿到正确结果。

777.jpg

优化后并发10000个请求,QPS高达10214。

888.png

我们用tcpdump抓包工具看一下具体的TCP包

999.jpg

可以看到client的端口是51332,Server端口8090。发送了“beidou”字符串给Server, 对应到代码中Provider属性。代码中的temp1 和temp2都是字符串“1”。Server返回相加结果1。

000.jpg

让我们看一下Server的返回结果

1000.jpg

0200 0000 一共32位表示body 长度为2个字节。 08 表示tag为采用Protobuf Variant 编码, 02表示值为2。


总结

本文只是通过一个简单例子介绍了RPC中的I/O模型以及序列化,其实RPC本身是一个很大的话题,比如如何保证在不可靠的网络中保证RPC的可靠性?如何实现客户端的重试调用、超时控制?如何优雅的起停服务、发现与注册服务?还有很多问题值得大家研究学习,这里不做过多探讨了。


参考网址:

https://blog.csdn.net/wangyunpeng0319/article/details/78651998

https://blog.csdn.net/dinglang_2009/article/details/53453794 

https://blog.csdn.net/KingCat666/article/details/78577079

https://www.cnblogs.com/crazylqy/p/7995395.html


原文地址:https://blog.csdn.net/wangguohe/article/details/81536550

转载请注明: ITTXX.CN--分享互联网 » RPC的初步理解和简单实例

最后更新:2019-01-11 12:39:16

赞 (2) or 分享 ()
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽