RPC 原理与实现

您所在的位置:网站首页 rpc框架实现原理 RPC 原理与实现

RPC 原理与实现

2023-08-24 01:37| 来源: 网络整理| 查看: 265

文章目录 1. RPC 简介1.1 什么是RPCReference: LPC & IPC (本地过程调用) 1.2 为什么要用RPC1.2.1 系统架构的演进1.2.2 Web 服务的技术结构1.2.2 SOAP ( Simple Object Access Protocol)1.2.2 REST(Representational State Transfort)SOAP & REST1.2.3 RPC 实现方式 2. RPC 框架实现2.1 RMI 框架2.1.1 RMI 框架结构2.1.2 RMI 使用示例 2.2 通用RPC框架2.2.1 通用RPC框架结构2.2.2 通用RPC框架简单实现1. Common 包2. Server 包3. Client 包 2.3 总结2.3.1. 建立通信2.3.2. 服务寻址1. 从服务提供者的角度看:2.从调用者的角度看: 2.3.3. 网络传输1. 序列化2. 反序列化 2.3.4. 服务调用

1. RPC 简介 1.1 什么是RPC

远程过程调用(RPC),就是客户端基于某种传输协议通过网络向服务提供端请求服务处理,然后获取返回的数据。这种调用对于客户端而言和调用本地服务一样方便,开发人员不需要了解具体底层网络传输协议。简单讲,就是本地调用的逻辑处理过程放在的远程的机器上,而不是本地服务代理来处理。 在这里插入图片描述

其基本流程如上图所示, 如果有过Android 系统的开发经验,很容易就能想到该架构和 Android 系统中的 ServiceManager 结构极其相似。 不同点在于 Android 系统中的 ServiceManager 其实是基于 IPC机制,也就是 Linux 系统中的本地过程调用。

Reference: LPC & IPC (本地过程调用)

本地过程调用在不同的操作系统中叫法不同,使用方式也不太一样。在Windows编程中,称为LPC;在linux编程中,更习惯称之为IPC,即进程间通信。进程间通信通常有以下几种方式:

管道:一种半双工的通信方式,数据只能单向流动,而且只能在父子进程间使用。命名管道:半双工的通信方式,允许无亲缘关系进程间的通信。信号量:信号量是一个计数器,可用来控制多个进程对共享资源的访问。Socket 套接字:可用于网络中不同机器之间的进程间通信,应用广泛。消息队列:消息队列是消息的链接表,具有写权限的进程可以按照一定的规则向消息队列中添加新信息;对消息队列有读权限的进程则可以从消息队列中读取信息。共享内存:映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式。 1.2 为什么要用RPC 1.2.1 系统架构的演进

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构势在必行,这样各个服务的治理以及服务之间的如何调用就需要额外的手段进行控制。

单一应用架构 当网站流量很小时,只需一个应用将所有功能都部署在一起,以减少部署节点和成本。 此时,用于简化增删改查工作量的数据访问框架(ORM) 是关键。

垂直应用架构 当访问量逐渐增大,单一应用多服务器部署带来的收益越来越小,将应用拆成互不相干的几个应用,以提升效率。 此时,用于加速前端页面开发的 Web框架(MVC) 是关键。

分布式服务架构 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。 此时,用于提高业务复用及整合的分布式服务框架提供的统一服务是关键。

分布式服务架构下调用服务的方式也有不同。SOA一般使用SOAP或者REST方式来提供服务,这样外部业务系统可以使用通用网络协议来处理请求和响应,而微服务还可以有一些私有的协议方式来提供服务,例如基于自定义协议的RPC框架。RPC使得调用服务简单,但是需要一些其他耗时间的交流协调工作,不一定适合SOA场景

1.2.2 Web 服务的技术结构

系统架构的改变必然需要相应的服务调用方案来实现,以下为Web 服务技术结构。

在这里插入图片描述

web service被W3C设立规范之初,SOAP方案已经提出。但随着服务化技术和架构的发展,SOAP多少显得过于复杂,因此出现了简化版的REST方案。此后由于分布式服务应用越来越大,对性能和易用性的要求越来越大,就出现了RPC框架。

1.2.2 SOAP ( Simple Object Access Protocol)

SOAP简单对象访问协议,是基于XML数据格式来交换数据;其内部定义了一套复杂完善的XML标签,标签中包含了调用的远程过程、参数、返回值和出错信息等等,通信双方根据这套标签来解析数据或者请求服务。与SOAP相关的配套协议是WSDL (Web Service Description Language),用来描述哪个服务器提供什么服务,怎样找到它,以及该服务使用怎样的接口规范。

因此,SOAP服务整体流程是:首先获得该服务的WSDL描述,根据WSDL构造一条格式化的SOAP请求发送给服务器,然后接收一条同样SOAP格式的应答,最后根据先前的WSDL解码数据。请求和应答通常使用HTTP协议传输,发送请求就使用HTTP的POST方法。

1.2.2 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传输,所以可支持有状态的场景。 1.2.3 RPC 实现方式

RPC 方案具有几下几种实现:

RMI RMI是Java制定的远程通信协议,是Java的标准RPC组件,其他编程语言无法使用。Thrift Thrift 是基于IDL来跨语言的RPC组件,Thrift的使用者只需要按照Thrift官方规定的方式来写API结构,然后生成对应语言的API接口,继而就可以跨语言完成远程过程调用。Dubbo 作为服务化的组件,如果没有服务治理来完成大规模应用集群中服务调用管理工作,则运维工作非常繁重的,因此类似dubbo这种包含服务治理的RPC组件出现了。 2. RPC 框架实现 2.1 RMI 框架 2.1.1 RMI 框架结构

RMI全称是Remote Method Invocation,也就是远程方法调用。当应用比较小性能要求不高的情况下使用RMI方便快捷。

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是个很重要的功能,当服务端开发完服务之后,要对外暴露,如果 没有服务注册,则客户端是无从调用的。 2.1.2 RMI 使用示例

首先声明服务接口

/* 接口须继承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(); } } } 2.2 通用RPC框架 2.2.1 通用RPC框架结构

一个通用的RPC框架组成如下图所示:

在这里插入图片描述

通用的RPC组件一般包括以下一些模块:

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模块。 2.2.2 通用RPC框架简单实现

实现代码大致可分为 三个部分:

Common 服务接口和数据序列化部分Server 服务实现,请求处理,以及服务发布Client 客户端调用的远程端,代理客户端服务的实现 1. 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; } } 2. 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 PROCESSOR_INSTANCE_MAP = new ConcurrentHashMap(); // 存储服务实例的 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 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); } } 3. 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 getInstance(Class 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 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; } } } 2.3 总结

RPC 的主要流程可分为以下几个步骤,通常经过这些步骤之后,一次完整的RPC调用算是完成了,另外可能因为网络波动等原因需要重试

2.3.1. 建立通信

首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。

2.3.2. 服务寻址

要实现服务寻址,A 服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称是什么。

1. 从服务提供者的角度看: 当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找;当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务;服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉。 2.从调用者的角度看: 服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息.当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者.服务调用者下线的时候,则取消订阅. 2.3.3. 网络传输 1. 序列化

当 A 机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输,然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。

2. 反序列化

当 B 机器接收到 A 机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用, 通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。

2.3.4. 服务调用

B 机器进行本地调用(通过代理Proxy和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3