RPC
1. 简介
RPC:远程过程调用,指计算机 A 上的进程,调用另外一台计算机 B 上的进程(基于某种传输协议通过网络),其中 A 上的调用进程被挂起,而 B 上的被调用进程开始执行,当值返回给 A 时,A 进程继续执行。调用方可以通过使用参数将信息传送给被调用方,而后可以通过传回的结果得到信息。而这一过程,对于开发人员来说是透明的。开发人员不需要了解具体底层网络传输协议。
与之对应的是本地过程调用,在不同的操作系统中叫法不同,使用方式也不太一样。在Windows编程中,称为LPC;在linux编程中,更习惯称之为IPC,即进程间通信。进程间通信通常有以下几种方式:
-
管道:一种半双工的通信方式,数据只能单向流动,而且只能在父子进程间使用
-
命名管道:半双工的通信方式,允许无亲缘关系进程间的通信。
-
信号量:信号量是一个计数器,可用来控制多个进程对共享资源的访问。
-
消息队列:消息队列是消息的链接表,具有写权限的进程可以按照一定的规则向消息队列中添加新信息;对消息队列有读权限的进程则可以从消息队列中读取信息。
-
共享内存:映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式。
2. 示例
以 count = read(fd, buf, nbytes); 为例:
本地调用的实现逻辑:fd 为一个整型数,表示一个文件。buf 为一个字符数组,用于存储读入的数据。nbytes 为另一个整型数,用于记录实际读入的字节数。如果该调用位于主程序中,那么在调用之前堆栈的状态如图2(a)所示。为了进行调用,调用方首先把参数反序压入堆栈,即为最后一个参数先压入,如图2(b)所示。在 read 操作运行完毕后,它将返回值放在某个寄存器中,移出返回地址,并将控制权交回给调用方。调用方随后将参数从堆栈中移出,使堆栈还原到最初的状态。
RPC 背后的思想是尽量使远程过程调用具有与本地调用相同的形式。假设程序需要从某个文件读取数据,程序员在代码中执行 read 调用来取得数据。在传统的系统中, read 例程由链接器从库中提取出来,然后链接器再将它插入目标程序中。read 过程是一个短过程,一般通过执行一个等效的 read 系统调用来实现。即,read 过程是一个位于用户代码与本地操作系统之间的接口。
虽然 read 中执行了系统调用,但它本身依然是通过将参数压入堆栈的常规方式调用的。如图2(b)所示,程序员并不知道 read 干了啥。
RPC 是通过类似的途径来获得透明性。当 read 实际上是一个远程过程时(比如在文件服务器所在的机器上运行的过程),库中就放入 read 的另外一个版本,称为客户存根(client stub)。这种版本的 read 过程同样遵循图2(b)的调用次序,这点与原来的 read 过程相同。另一个相同点是其中也执行了本地操作系统调用。唯一不同点是它不要求操作系统提供数据,而是将参数打包成消息,而后请求此消息发送到服务器,如图3所示。在对 send 的调用后,客户存根调用 receive 过程,随即阻塞自己,直到收到响应消息。
3. 背景
3.1. 系统架构演进
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构势在必行,这样各个服务的治理以及服务之间的如何调用就需要额外的手段进行控制。
-
单一应用架构
当网站流量很小时,只需一个应用将所有功能都部署在一起,以减少部署节点和成本。
此时,用于简化增删改查工作量的数据访问框架(ORM) 是关键。 -
垂直应用架构
当访问量逐渐增大,单一应用多服务器部署带来的收益越来越小,将应用拆成互不相干的几个应用,以提升效率。
此时,用于加速前端页面开发的 Web框架(MVC) 是关键。 -
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。
此时,用于提高业务复用及整合的分布式服务框架提供的统一服务是关键。
分布式服务架构下调用服务的方式也有不同。SOA一般使用SOAP或者REST方式来提供服务,这样外部业务系统可以使用通用网络协议来处理请求和响应,而微服务还可以有一些私有的协议方式来提供服务,例如基于自定义协议的RPC框架。RPC使得调用服务简单,但是需要一些其他耗时间的交流协调工作,不一定适合SOA场景
3.2. 分布式架构调用方法
以web服务为例:
web service被W3C设立规范之初,SOAP方案已经提出。但随着服务化技术和架构的发展,SOAP多少显得过于复杂,因此出现了简化版的REST方案。此后由于分布式服务应用越来越大,对性能和易用性的要求越来越大,就出现了RPC框架。
-
SOAP ( Simple Object Access Protocol)
SOAP简单对象访问协议,是基于XML数据格式来交换数据;其内部定义了一套复杂完善的XML标签,标签中包含了调用的远程过程、参数、返回值和出错信息等等,通信双方根据这套标签来解析数据或者请求服务。与SOAP相关的配套协议是WSDL (Web Service Description Language),用来描述哪个服务器提供什么服务,怎样找到它,以及该服务使用怎样的接口规范。因此,SOAP服务整体流程是:首先获得该服务的WSDL描述,根据WSDL构造一条格式化的SOAP请求发送给服务器,然后接收一条同样SOAP格式的应答,最后根据先前的WSDL解码数据。请求和应答通常使用HTTP协议传输,发送请求就使用HTTP的POST方法。
-
REST(Representational State Transfort)
REST表示性状态转移,由于SOAP方案过于庞大复杂,在很多简单的web服务应用场景中,轻量级的REST就替代SOAP方案了。和SOAP相比,REST只是对URI做了一些规范,数据通常采用JSON格式,底层传输使用HTTP/HTTPS,因此,所有web服务器都可以快速支持该方案;开发人员也可以快速学习和使用。
-
SOAP & REST
从命名来看,SOAP是一种协议,而REST只是一种方案。协议的实现通常需要设计开发专门的工具支持,而方案相对基于目前的工具来做一些设计和约束,因此可用性更高。REST特点:
由于数据返回格式是自定义的,绝大部分使用JSON,这种数据结构节省带宽,并且前端JavaScript能天生支持。
无状态,基于HTTP协议,所以只能适应无状态场景。SOAP特点:
协议有安全性的一些规范。
基于xml的标签约束,不要求一定是HTTP传输,所以可支持有状态的场景。
3.3. RPC 实现方式
RPC 方案具有几下几种实现:
- RMI
RMI是Java制定的远程通信协议,是Java的标准RPC组件,其他编程语言无法使用。 - Thrift
Thrift 是基于IDL来跨语言的RPC组件,Thrift的使用者只需要按照Thrift官方规定的方式来写API结构,然后生成对应语言的API接口,继而就可以跨语言完成远程过程调用。 - Dubbo
作为服务化的组件,如果没有服务治理来完成大规模应用集群中服务调用管理工作,则运维工作非常繁重的,因此类似dubbo这种包含服务治理的RPC组件出现了。
4. RPC框架实现
4.1. RMI 框架
4.1.1. 框架结构
RMI全称是Remote Method Invocation,也就是远程方法调用。当应用比较小性能要求不高的情况下使用RMI方便快捷。
概念说明
1.Stub(桩):
stub实际上就是远程过程在客户端上面的一个代理proxy。当客户端代码调用API接口提供的方法的时候,RMI生成
的stub代码块会将请求数据序列化,交给远程服务端处理,然后将结果反序列化之后返回给客户端。这些处理过程
对于客户端来说,基本是透明无感知的。
2.Remote(远程交互):
底层网络处理,RMI对用户屏蔽了这层细节。stub通过remote来和远程服务端进行通信。
3.Skeleton(骨架):
和stub相似,skeleton是服务端生成的一个代理proxy。当客户端通过stub发送请求到服务端,交给skeleton来处
理,它根据指定的服务方法来反序列化请求,然后调用具体方法执行,最后将结果返回给客户端。
4.Registry(服务注册):
RMI服务注册中心,在服务端实现之后需要注册到rmi server上,然后客户端从指定的rmi地址上lookup服务,调用
该服务对应的方法完成远程方法调用。registry是个很重要的功能,当服务端开发完服务之后,要对外暴露,如果
没有服务注册,则客户端是无从调用的。
4.1.2. 使用示例
-
声明服务接口
/* 接口须继承RMI的Remote **/ public interface RmiService extends Remote { // 必须有RemoteException,才是RMI方法 String hello(String name) throws RemoteException; }
-
实现接口
/* UnicastRemoteObject生成一个代理proxy **/ public class RmiServiceImpl extends UnicastRemoteObject implements RmiService { public RmiServiceImpl() throws RemoteException { } public String hello(String name) throws RemoteException { System.out.println("RmiService invoke hello"); return "Hello " + name; } }
-
启动服务并发布
/* 服务端server启动 **/ public class RmiServer { public static void main(String[] args) { try { RmiService service = new RmiServiceImpl(); //在本地创建注册中心实例,端口为10086 LocateRegistry.createRegistry(10086); //注册service服务到创建的注册中心 Naming.rebind("rmi://127.0.0.1:10086/service1", service); } catch (Exception e) { e.printStackTrace(); } System.out.println("------------server start-----------------"); } }
-
客户端调用
/* 客户端调用rmi服务 **/ public class RmiClient { public static void main(String[] args) { try { // 根据服务地址查找服务,然后调用API对应方法 RmiService service = (RmiService) Naming.lookup("rmi://localhost:10086/service1"); System.out.println(service.hello("RMI")); } catch (Exception e) { e.printStackTrace(); } } }
4.2. 通用rpc框架
4.2.1. 框架结构
1. ClientService:
这个模块主要是封装服务端对外提供的API,让客户端像使用本地API接口一样调用远程服务。一般使用动态代理机
制,当客户端调用api的方法时,serviceClient会走代理逻辑,去远程服务器请求真正的执行方法,然后将响应结
果作为本地的api方法执行结果返回给客户端应用。
2.Processor:
在服务端存在很多方法,当客户端请求过来,服务端需要定位到具体对象的具体方法,然后执行该方法,这个功能就
由processor模块来完成。一般这个操作需要使用反射机制来获取用来执行真实处理逻辑的方法,有的RPC直
接在server初始化的时候,将一定规则写进Map映射中,这样直接获取对象即可。
3.Protocol:
一般协议层包括编码/解码,或者说序列化和反序列化工作;当然,有时候编解码不仅仅是对象序列化的工作,还有
一些通信相关的字节流的额外解析部分。
4.Transport:
主要是服务端和客户端网络通信相关的功能。这里和下面的IO层区分开,主要是因为传输层处理
server/client的网络通信交互,而不涉及具体底层处理连接请求和响应相关的逻辑。
5.I/O:
这个模块主要是为了提高性能可能采用不同的IO模型和线程模型,当然,一般我们可能和上面的transport层联系
的比较紧密,统一称为remote模块。
4.2.2. 简单实现
实现代码大致可分为 三个部分:
- Common:服务接口和数据序列化部分
- Server:服务实现,请求处理,以及服务发布
- Client:客户端调用的远程端,代理客户端服务的实现
-
common包:存放公共资源,比如服务接口文件及参数传输模型
/* 服务接口 **/ public interface RpcService { String sayHi(String name); }
/*协议层 对传输通信的远程调用请求接口和方法参数等数据按照规定的格式进行处理 *此处使用 jdk 自带的方式进行序列化 **/ public class ServiceProtocol { private static volatile ServiceProtocol instance; private ServiceProtocol() { } public static ServiceProtocol getInstance() { if (null == instance) { synchronized (ServiceProtocol.class) { if (null == instance) { instance = new ServiceProtocol(); } } } return instance; } /** * 参数传递的模型 */ public static class ProtocolModel implements Serializable { private static final long serialVersionUID = 1L; //jdk 序列化 private String clazz; // 接口名 private String method; // 方法名 private String[] argTypes; // 参数类型数组 private Object[] args; // 参数数组 public String getClazz() { return clazz; } public void setClazz(String clazz) { this.clazz = clazz; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String[] getArgTypes() { return argTypes; } public void setArgTypes(String[] argTypes) { this.argTypes = argTypes; } public Object[] getArgs() { return args; } public void setArgs(Object[] args) { this.args = args; } }
-
server包:服务端实现服务
/* 服务实现 **/ public class RpcServiceImpl implements RpcService { @Override public String sayHi(String name) { return "Hello," + name; } }
/** 服务端处理层,负责发布管理服务,并定位处理客户端的调用 **/ public class ServiceProcessor { private static volatile ServiceProcessor instance; private ServiceProcessor() { } public static ServiceProcessor getInstance() { if (null == instance) { synchronized (ServiceProcessor.class) { if (null == instance) { instance = new ServiceProcessor(); } } } return instance; } private static final ConcurrentMap<String, Object> PROCESSOR_INSTANCE_MAP = new ConcurrentHashMap<String, Object>(); // 存储服务实例的 Map,实际起到注册中心的作用 // 发布服务则将其加入Map,以接口名为key,实例为value,生产环境下使用第三方工具注册服务 public boolean publish(Class clazz, Object obj) { return PROCESSOR_INSTANCE_MAP.putIfAbsent(clazz.getName(), obj) != null; } // 定位远程调用服务方法,并返回执行结果 public Object process(ServiceProtocol.ProtocolModel model) { try { // 定位调用的接口 Class clazz = Class.forName(model.getClazz()); Class[] types = new Class[model.getArgTypes().length]; for (int i = 0; i < types.length; i++) { types[i] = Class.forName(model.getArgTypes()[i]); } // 根据参数类型定位远程调用的方法 Method method = clazz.getMethod(model.getMethod(), types); Object obj = PROCESSOR_INSTANCE_MAP.get(model.getClazz()); if (obj == null) { return null; } // 返回执行结果 return method.invoke(obj, model.getArgs()); } catch (Exception e) { e.printStackTrace(); return null; } } }
/*服务端 remote 层,负责监听Socket,接收客户端远程调用 **/ public class ServerRemoter { private static final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); public void startServer(int port) throws Exception { final ServerSocket server = new ServerSocket(); // 绑定服务地址 server.bind(new InetSocketAddress(port)); System.out.println("-----------start server----------------"); try { while (true) { final Socket socket = server.accept(); // 线程池处理请求 executor.execute(new MyRunnable(socket)); } } finally { server.close(); } } class MyRunnable implements Runnable { private Socket socket; public MyRunnable(Socket socket) { this.socket = socket; } public void run() { try { //1.接收参数 ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream()); ServiceProtocol.ProtocolModel model = (ServiceProtocol.ProtocolModel) inputStream.readObject(); //服务端通过processor执行实现类方法 Object object = ServiceProcessor.getInstance().process(model); //返回结果给客户端 ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream()); outputStream.writeObject(object); outputStream.flush(); //5.关闭连接 outputStream.close(); inputStream.close(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
/* 服务端发布接口,启动服务 **/ public class Server { public static void main(String[] args) throws Exception { // 注册服务 ServiceProcessor.getInstance().publish(RpcService.class, new RpcServiceImpl()); // 启动server,开始监听 socket ServerRemoter remoter = new ServerRemoter(); remoter.startServer(10086); } }
-
client包:实现调用步骤
/* 客户端测试调用, 用户调用远程方法 **/ public class Client { public static void main(String[] args) { System.out.println("----------start invoke----------------"); // 使用代理客户端创建服务接口实例 RpcService service = ServiceProxyClient.getInstance(RpcService.class); // 调用远程方法 System.out.println(service.sayHi("RPC World")); System.out.println("----------end invoke----------------"); } }
/* 客户端服务代理,实际是将服务调用通过动态代理的方式返回远程服务执行结果. **/ public class ServiceProxyClient { // 创建代理对象 public static <T> T getInstance(Class<T> clazz) { return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ServiceProxy(clazz)); } // 动态代理类 public static class ServiceProxy implements InvocationHandler { private Class clazz; public ServiceProxy(Class clazz) { this.clazz = clazz; } @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { //执行服务接口方法时将远程调用的参数组装为model ServiceProtocol.ProtocolModel model = new ServiceProtocol.ProtocolModel(); model.setClazz(clazz.getName()); model.setMethod(method.getName()); model.setArgs(objects); String[] argType = new String[method.getParameterTypes().length]; for (int i = 0; i < argType.length; i++) { argType[i] = method.getParameterTypes()[i].getName(); } model.setArgTypes(argType); //通过ClientRemoter发起远程调用, 返回远程服务执行结果 Object object = ClientRemoter.getInstance().getDataRemote(model); return object; } } }
/* 客户端 remote 层,与服务端通过 Socket 远程交互,生产环境需从第三方工具注册中心获 *取远程服务地址,之后才能发起调用 **/ public class ClientRemoter { private static volatile ClientRemoter instance; private ClientRemoter() { } public static ClientRemoter getInstance() { if (null == instance) { synchronized (ClientRemoter.class) { if (null == instance) { instance = new ClientRemoter(); } } } return instance; } public Object getDataRemote(ServiceProtocol.ProtocolModel model) { try (Socket socket = new Socket()) { // socket远程连接 socket.connect(new InetSocketAddress("127.0.0.1", 10086)); ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream()); // 传输参数,请求远程执行 outputStream.writeObject(model); outputStream.flush(); //接收返回结果 ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream()); Object object = inputStream.readObject(); return object; } catch (Exception e) { e.printStackTrace(); return null; } } }
4.3. 总结
RPC 的主要流程可分为以下几个步骤,通常经过这些步骤之后,一次完整的RPC调用算是完成了,另外可能因为网络波动等原因需要重试
4.3.1. 建立通信
首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。
4.3.2. 服务寻址
要实现服务寻址,A 服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称是什么。
1、从服务提供者的角度看:
- 当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找;
- 当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务;
- 服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉。
2、从服务调用者的角度看:
- 服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息.
- 当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者.
- 服务调用者下线的时候,则取消订阅.
4.3.3. 网络传输
- 序列化
当 A 机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输,然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。 - 反序列化
当 B 机器接收到 A 机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用,通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。
4.3.4. 服务调用
B 机器进行本地调用(通过代理Proxy和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。
疑问
既有 HTTP为何还要用 RPC 进行服务调用?
- 应用层
与其它计算机进行通讯的一个应用,它是对应应用程序的通信服务的。例如,一个没有通信功能的字处理程序就不能执行通信的 代码,从事字 处理工作的程序员也不关心OSI的第7层。但是,如果添加了一个传输文件的选项,那么字 处理器的程序就需要实现OSI的第7层。示例: TELNET, HTTP, FTP, NFS, SMTP等。 - 表示层
这一层的主要功能是定义数据格式及加密。例如,FTP允许你选择以二进制或ASCII格式传输。如果选择二进制,那么发送方和接收方不改变文件的内容。如果选择ASCII格式,发送方将把文本从发送方的 字符集转换成标准的ASCII后发送数据。在接收方将标准的ASCII转换成接收方计算机的字符集。示例:加密,ASCII等。 - 会话层
它定义了如何开始、控制和结束一个会话,包括对多个双向消息的控制和管理,以便在只完成连续消息的一部分时可以通知应用,从而使表示层看到的数据是连续的,在某些情况下,如果表示层收到了所有的 数据,则用数据代表表示层。示例:RPC,SQL等。 - 传输层
这层的功能包括是否选择差错恢复协议还是无差错恢复协议,及在同一 主机上对不同应用的 数据流的输入进行复用,还包括对收到的顺序不对的 数据包的重新排序功能。示例: TCP, UDP, SPX。 - 网络层
这层对端到端的包传输进行定义,它定义了能够标识所有结点的 逻辑地址,还定义了 路由实现的方式和学习的方式。为了适应 最大传输单元长度小于包长度的 传输介质,网络层还定义了如何将一个包分解成更小的包的分段方法。示例:IP,IPX等。 - 数据链路层
它定义了在单个链路上如何传输数据。这些协议与被讨论的各种介质有关。示例: ATM, FDDI等。 - 物理层
OSI的物理层规范是有关 传输介质的特性,这些规范通常也参考了其他组织制定的标准。连接头、帧、帧的使用、电流、编码及光调制等都属于各种物理层规范中的内容。物理层常用多个规范完成对所有细节的定义。示例: Rj45, 802.3等。
通过以上七层网络模型就可以看出,只要是网络调用从传输层开始基本都一样,TCP/UDP+IP,不同的只是传输层之上,传输层之上可以叫做交互协议,传输层之下可以叫传输协议,http和rpc可以理解为不同的交互协议,不同的交互方式会产生不同的优缺点
二者区别
- 传输协议:http使用http协议;rpc既可以使用http协议也可以直接使用tcp协议或者自定义传输协议,减少冗余信息,性能会更高
- 数据格式:使用MIME协议的数据格式,文本、图片、录音等;rpc基本使用json或者xml
- 连接方式:http是半双工短连接,无法复用,通信完毕即销毁;rpc是全双工长连接,通信期间持续连接(使用传输id确定交互信息的归属),可以复用。
- 附属功能:RPC包含了重试机制、路由策略、负载均衡策略、高可用策略、流量控制策略等,http只有交互功能。
- 使用:HTTP需要显式拼接url,url变更则需要修改代码;rpc可以解耦,使用起来就是本地方法。
使用选择:由于RPC直接通过自定义TCP协议实现通信,而HTTP服务通过Http协议,相当于多了一层,所以RPC的效率高于Http。http是超文本传输协议,包含的信息比较臃肿,网关之前一般使用http,服务之间的调用采用rpc。 一方面是因为RPC框架的效率比较高,还有一个原因是RPC包含了重试机制、路由策略、负载均衡策略、高可用策略、流量控制策略,使用http无法完成上述功能 我们通常采用的原则为:向系统外部暴露采用HTTP,向系统内部暴露调用采用RPC方式。 也就是说前后端之间(网关之前)用http,各个服务之间用rpc。