注:本文为笔者阅读各个大佬的博客后的理解,读者如果有时间可以直接去看原文。
引用:
什么是RPC
RPC(Remote Procedure Call,远程过程调用)是一种允许运行在一台计算机上的程序调用另一台计算机上子程序的技术。这种技术屏蔽了底层的网络通信细节,使得程序间的远程通信如同本地调用一样简单。RPC机制使得开发者能够构建分布式计算系统,其中不同的组件可以分布在不同的计算机上,但它们之间可以像在同一台机器上一样相互调用。
一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
挖掘出几个要点:
- RPC是协议:既然是协议就只是一套规范,那么就需要有人遵循这套规范来进行实现。目前典型的RPC实现包括:Dubbo、Thrift、GRPC、Hetty等。
- 网络协议和网络IO模型对其透明:既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。
- 信息格式对其透明:我们知道在本地应用程序中,对于某个对象的调用需要传递一些参数,并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数,并计算出处理结果的,调用方是不需要关心的。那么对于远程调用来说,这些参数会以某种信息格式传递给网络上的另外一台计算机,这个信息格式是怎样构成的,调用方是不需要关心的。
- 应该有跨语言能力:为什么这样说呢?因为调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。

新兴RPC框架:随着分布式系统和微服务架构的普及,出现了许多新的RPC框架,如Apache Thrift、gRPC、Dubbo等。这些框架通常具有更高的性能、更好的可扩展性和更丰富的功能特性。
RPC技术从最初的简单过程调用协议发展到如今的现代化RPC框架,经历了多个阶段和不断的改进与创新。随着分布式计算和微服务架构的不断发展,RPC技术将继续在分布式系统中发挥重要作用。
常见RPC框架的对比
1. gRPC
开发者:由Google开发。
协议基础:基于HTTP/2协议,并使用Protocol Buffers(ProtoBuf)作为序列化协议。
支持语言:支持多语言,包括C++、Java、Python、Go、Ruby、C#、Node.js等。
特点:
提供强大的IDL(接口定义语言)和自动代码生成工具。
支持双向流、流式传输等特性。
适用于大规模分布式系统,要求高性能和跨语言支持的场景。
适用于需要使用Protocol Buffers进行高效数据序列化的场景。2. Apache Dubbo
开发者:由阿里巴巴开发。
协议:支持多种协议,包括Dubbo自定义协议、REST、HTTP等。
支持语言:主要基于Java,但可以通过扩展支持其他语言。
特点:
提供高性能、透明化的远程方法调用。
支持负载均衡、服务发现、集群容错等特性。
提供了REST风格的远程调用。
适用于Java生态系统中的分布式应用,尤其是基于Spring的应用。
适用于需要提供多协议支持和高度可扩展性的场景。3. Apache Thrift
开发者:由Apache开发。
协议:支持多种传输协议和序列化协议,如TBinaryProtocol、TCompactProtocol等。
支持语言:支持多语言,包括C++、Java、Python、Go、Ruby、C#、Node.js等。
特点:
使用IDL进行接口定义,提供代码生成工具。
支持异步和同步的通信方式。
可以在不同语言之间进行跨语言通信。
适用于异构系统中不同语言之间的远程调用。
适用于需要高度定制和支持多种传输协议的场景。4. Motan
开发者:新浪微博开源。
特点:
是一个Java框架,具有高性能和可扩展性。
在微博平台中已经广泛应用,每天为数百个服务完成近千亿次的调用。
提供了丰富的功能和良好的性能表现。5. 其他RPC框架
其他框架:如Tars(腾讯内部使用并开源)、ZeroMQ(高性能异步消息传递库,非专门RPC框架)、Akka(并发编程框架,提供Actor模型实现)等。
特点:
这些框架各有特色,如Tars特别支持C++语言,适合高性能要求的应用场景。
ZeroMQ适用于构建高度异步、消息驱动的系统。
Akka适用于构建高并发、分布式、容错性强的系统。
为什么要用RPC
其实这是应用开发到一定的阶段的强烈需求驱动的。
业务越来越多、应用也越来越多时,自然的,我们会发现有些功能已经不能简单划分开来或者划分不出来。此时,可以将公共业务逻辑抽离出来,将之组成独立的服务Service应用 。而原有的、新增的应用都可以与那些独立的Service应用 交互,以此来完成完整的业务功能。
RPC原理
RPC调用流程
客户端(Client)调用:客户端应用程序调用本地的一个存根(Stub)函数,该函数是一个本地函数,但其实现会触发远程调用。
存根(Stub)处理:存根函数负责将调用参数打包成一种可以在网络上传输的格式(如序列化(其中JSON格式的序列化和反序列化可以看笔者的另一个文章:Java中的JSON序列化和反序列化)),并通过网络发送给服务器。
网络传输:打包后的数据通过网络发送到服务器。
服务器端接收:服务器端接收并解包这些数据,调用实际的服务端程序或函数,处理请求。
结果返回:服务端将处理结果打包,通过网络发送回客户端。
客户端接收结果:客户端的存根函数接收并解包结果,然后返回给原始的调用者。
flowchart TD
A[RPC调用] --> B[ClientStub(客户端存根)]
B --> C[参数校验]
C --> D[序列化参数]
D --> E[连接管理(连接池/重连)]
E --> F[网络发送]
F --> G[网络接收(服务端)]
G --> H[反序列化参数]
H --> I[权限/认证校验]
I --> J[调用服务(服务端存根)]
J --> K[业务处理]
K --> L[生成结果]
L --> M[序列化结果]
M --> N[网络发送(服务端返回)]
N --> O[网络接收(客户端)]
O --> P[反序列化结果]
P --> Q[异常处理(超时/网络/服务异常)]
Q --> R[绑定返回值]
R --> S[返回结果]
RPC 框架提供了一系列的功能来支持上述过程,包括但不限于:
接口定义:定义服务端和客户端之间的接口,确保双方能够正确理解和调用。
数据序列化与反序列化:将调用信息和结果转换为网络可传输的格式,并在接收时进行还原。
网络通信:封装底层的网络通信逻辑,使得开发者无需关心具体的网络细节。
负载均衡:在多个服务实例之间分配请求,提高系统的可扩展性和可用性。
服务注册与发现:在分布式系统中,自动发现可用的服务实例。
要让网络通信细节对使用者透明,我们需要对通信细节进行封装,我们先看下一个RPC调用的流程涉及到哪些通信细节:
- 服务消费方(client)调用以本地调用方式调用服务;
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- client stub找到服务地址,并将消息发送到服务端;
- server stub收到消息后进行解码;
- server stub根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给server stub;
- server stub将返回结果打包成消息并发送至消费方;
- client stub接收到消息,并进行解码;
- 服务消费方得到最终结果。
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。
下面是网上的另外一幅图,感觉一目了然:

如何做到透明化远程服务调用
怎么封装通信细节才能让用户像以本地调用方式调用远程服务呢?对java来说就是使用代理!java代理有两种方式:1) jdk 动态代理;2)字节码生成。尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,大部分公司实现RPC框架时还是选择动态代理方式。
下面简单介绍下动态代理怎么实现我们的需求。我们需要实现RPCProxyClient代理类,代理类的invoke方法中封装了与远端服务通信的细节,消费方首先从RPCProxyClient获得服务提供方的接口,当执行helloWorldService.sayHello("test")方法时就会调用invoke方法。
public class RPCProxyClient implements java.lang.reflect.InvocationHandler{ private Object obj; public RPCProxyClient(Object obj){ this.obj=obj; } /** * 得到被代理对象; */ public static Object getProxy(Object obj){ return java.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new RPCProxyClient(obj)); } /** * 调用此方法执行 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //结果参数; Object result = new Object(); // ...执行通信相关逻辑 // ... return result; } }public class Test { public static void main(String[] args) { HelloWorldService helloWorldService = (HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.class); helloWorldService.sayHello("test"); } }其实就是通过动态代理模式,在执行该方法的前后对数据进行封装和解码等,让用于感觉就像是直接调用该方法一样,殊不知,我们对方法前后都经过了复杂的处理。
如何对消息进行编码和解码
确定消息数据结构
客户端的请求消息结构一般需要包括以下内容:
- 接口名称:在我们的例子里接口名是“HelloWorldService”,如果不传,服务端就不知道调用哪个接口了;
- 方法名:一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;
- 参数类型&参数值:参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct等,以及相应的参数值;
- 超时时间 + requestID(标识唯一请求id)
服务端返回的消息结构一般包括以下内容:
- 状态code + 返回值
- requestID
序列化
一旦确定了消息的数据结构后,下一步就是要考虑序列化与反序列化了。
什么是序列化?序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程。
什么是反序列化?将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
为什么需要序列化?转换为二进制串后才好进行网络传输嘛!
为什么需要反序列化?将二进制转换为对象才好进行后续处理!
现如今序列化的方案越来越多,每种序列化方案都有优点和缺点,它们在设计之初有自己独特的应用场景,那到底选择哪种呢?从RPC的角度上看,主要看三点:
- 通用性:比如是否能支持Map等复杂的数据结构;
- 性能:包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;
- 可扩展性:对互联网公司而言,业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。
目前互联网公司广泛使用Protobuf、Thrift、Avro等成熟的序列化解决方案来搭建RPC框架,这些都是久经考验的解决方案。


377

被折叠的 条评论
为什么被折叠?



