Rpc 与 RMI 服务,java 面试笔试题代码
##前言前面我们曾经深入的了解过 Http 协议,以及 Https 协议的思考,但是在日常开发中,还有这么一种常见的技术--RPC,许多常见的框架库都是基于 RPC 技术进行开发实现的进程通信的技术,例如 RMI,gRpc 以及 dubbo 等,因此在 Java 开发中,RPC 也是常用技术中很重要的一环,本篇开始我们从 RPC 基础开始逐步了解 RPC 技术以及经典 RPC 的实现--RMI 的使用及原理
RPC 概念
RPC 全称 Inter-Process Communication ,即我们常说的进程间通信,指至少两个进程间或者跨线程进行传输数据或者信号的技术。进程是计算机系统分配资源的最小单位,而每一个进程都有着自身独立的系统资源,且彼此隔离。为了让不同的进程相互访问并且能进行协调工作,才有了进程间通信技术,这些进程可以运行在同一个计算机上或者在不同的网络连接的计算机上。进程间通信技术包括信息传递、同步、共享内存以及能实现远程调用,而 IPC 也是 Unix 通信机制 的一种标准。
而 IPC 通信常见的有如下两种:
本地过程调用(LPC):LPC 用在多任务操作系统中,使得同时运行的任务之间可以互相会话进行数据传输,这些任务之间共享内存空间使得任务同步和互相发送信息
远程过程通信(RPC):RPC 与 LPC 类似,其区别在于 RPC 仅进行网络上进程间的传输通信,最开始出现 RPC 是 sun 公司和 HP 公司运行在 UNIX 操作系统的计算机中,一直演化发展至今
简单 RPC 通信过程
需要明白的是 RPC 技术的核心并不在于使用什么协议,RPC 的目的仅仅是实现远程调用,且对用户透明,实现业务解耦。而一个简单的 RPC 至少首先会使用动态代理技术,实现通信层的动态隔离,而传输过程中至少会选择一种协议进行传输,将远程的被调用方的方法的返回结果进行序列化传输,即可完成一个最简单的 RPC 通信,可以参考 spring remoting 和 RMI,而一个复杂的 RPC 则是针对其中的细节进行优化扩展,能支持更多的能力和协议以及序列化等的选择,甚至可以和第三方中间件结合使用,此类 RPC 实现可以参照 dubbo,简单来说:
1.RPC 就是从一台机器上通过参数传输的方式调用到另外一台机器上的某个固定函数或者方法,并且按照固定的传输协议方式接受到返回的结果
2.RPC 会隐藏底层的通信细节,并且 RPC 自身就是一个请求响应模型
3.客户端发起请求后,服务端返回响应,RPC 在使用方式上像调用本地函数一样即可简单实现远程函数的调用
而 RPC 的通信过程简单来说,可以如图所示:
RPC 框架
首先我们来思考一个问题,一个 RPC 框架至少拥有哪些能力?我们可以从下面几点来考虑:
远程通信能力
首先 RPC 得定义就是能实现网络端远程服务器之间的通信,所以 RPC 一定要有的最基础的能力,就是能发起远程的网络传输连接请求,即 Sokect 通讯的维护和协议能力封装
Call ID 映射能力
RPC 框架既然拥有跨服务端进行传输数据交互的能力,那么我们需要告知远程服务器如何调用 Multiply ,要知道在本地调用中,函数体是直接通过函数指针来指定的,而在远程通信中,由于不在一个机器中,使用函数指针的方式是行不通的,所以在 RPC 框架中,几乎所有的函数都会有一个自己的 ID,且保持唯一,服务端和客户端之间都会去维护 ID 在本机器中不同的地址,当有 RPC 请求传递过来的时候,服务端只需要查找本地维护的 ID 和地址的关系,找到对应的函数,进行调用,并且传输对应的结果即可
序列化/反序列化能力
当数据从远程服务器上返回,由于远程网络传输的原因,在 Java 中必须使用序列化标准来实现数据的传输,不能像本地调用一样简单的将内存数据读取,所以 RPC 框架一定要有序列化支持的能力,可以将远程的数据序列化为字节流,接受完毕后反序列化成对应的内存对象
跨平台/夸语言交互能力
由于 RPC 远程交互传输数据,这个时候服务端的系统环境以及所交互的进程使用的语言等并不是固定的,所以 RPC 框架需要拥有一个稳定的可以跨平台的,跨语言环境的传输协议支持
常见的 RPC 框架
了解了 RPC 框架最基础的能力以后,我们来看看 Java 开发中那些常见的经典 RPC 框架吧:
1.Netty:netty 框架严格意义上并不属于 RPC,更多的是作为一种网络协议框架,能够快速的提供高性能的 RPC 或者 HTTP 等的远程通信能力基础
2.bRPC:bRPC 是一个基于 protobuf 接口的 PRC 框架,看到命名就可以知道此框架来自著名的百度团队,此框架囊括了百度内部的所有 RPC 协议,并且支持多种第三方协议,由于基于 protobuf 算法,从性能来看几乎是同类 RPC 框架中的领跑者
3.dubbo:dubbo 框架是业界著名的阿里巴巴团队早期开源的优秀的 RPC 框架,此框架发展较成熟,能独立作为商业化组件使用,且依托于 Spring 使用
4.gRPC:gRPC 是谷歌团队基于 Netty 开发实现的底层网络库,而此框架还有 Go 语言的版本,基于 Net 库开发
5.RMI:rmi 可以说是 java 中最早的 RPC 框架之一,且此 rpc 框架由 sun 团队开发,集成于 JDK 中,可以说完全可以实现开箱即用,不需要任何外部 jar 依赖,但由于早期的 rpc 实现,且设计上并不是为了解决互联网企业类的高并发问题,所以不建议现在互联网开发中作为 rpc 实现
RMI 框架基本使用
RMI 既然是 java 团队设计出来的 rpc 框架,虽然现在已经不适合企业级生产使用,但是其中的思想和规范值得学习,我们就来看看 RMI 框架如何使用吧:
RMI 三大基本类
实现 RMI 所需要的 API 基本都在三大类中,如下:
java.rmi
:提供客户端需要的类、接口和异常;
java.rmi.server
:提供服务端需要的类、接口和异常;
java.rmi.registry
:提供注册表的创建以及查找和命名远程对象的类、接口和异常 ;
构建 RMI 服务端
首先在 RMI 中服务端供客户端调用的实例称之为远程对象,而在 RMI 中实现了 java.rmi.Remote 接口的类或者继承了 java.rmi.Remote 接口的都是 RMI 的远程对象。那么我们来定义一个接口,继承 java.rmi.Remote:
/***用户处理器**/public interface UserHandler extends Remote {String getUserName(int id) throws RemoteException;String getUserPassWord() throws RemoteException;User getUserByName(String name) throws RemoteException;}
这里需要注意的一点是,继承了 Remote 接口的接口中定义的所有的方法必须抛出 RemoteException 异常,并且该接口的实现类必须直接或者间接继承 java.rmi.server.UnicastRemoteObject 类,该类中提供了很多支持 RMI 的方法,可以通过 JRMP 协议导出一个远程对象的引用,生成动态代理构建的 Stub 对象,实现代码如下:
public class UserHandlerImpl extends UnicastRemoteObject implements UserHandler {//这里因为集继承了 UnicastRemoteObject 类,其构造器要抛出 RemoteException,所以申明构造 public UserHandlerImpl() throws RemoteException {super();}
@Overridepublic String getUserName(int id) throws RemoteException {return "pdc";}@Overridepublic String getUserPassWord() throws RemoteException{return 654321;}@Overridepublic User getUserByName(String name) throws RemoteException{return new User(name, 654321);}}
这里我们构造了一个 User 实体,为了能实现远程传输,所以这里我们将其进行序列化:
public class User implements Serializable {private static final long serialVersionUID = 42L;
private String name;private String passWord;
public String getName(){return this.name;}
public String getPassWord(){return this.passWord;}
public void setName(String name){this.name = name;}
public void setPassWord(String passWord){this.passWord = passWord;}
public User(String name, String passWord) {this.name =
name;this.passWord = passWord;}}
需要注意的一点是,如果 jdk 版本低于 1.5,需要手动运行 rmic 命令生成实现类的 Stub 对象,而 1.5 开始使用动态代理技术,已经可以自动生成 Stub 对象了,做完这些就可以启动服务端了:
UserHandler userHandler = null;try {userHandler = new UserHandlerImpl();Naming.rebind("user", userHandler);//将当前的实例与名称为 user 绑定,后面客户端调用查找对应的名称 System.out.println(" RMI 服务端启动成功");} catch (Exception e) {System.err.println(" RMI 服务端启动失败");e.printStackTrace();}
构建 RMI 注册表
其实所谓注册表就是保存了 RMI 服务端启动与绑定的名称的进程,由于 jdk 已经把 RMI 代码集成到了 JDK 中,RMI 的注册表其实不需要写任何代码,在 JDK 的 bin 目录下已经存在一个叫 rmiregistry.exe 的程序,不过我们需要在当前的 class 类路径下启动注册表(所以需要注意 JAVA_HOME 环境变量一定要配置成功) ,来到 class 类路径下,输入命令:
rmiregistry 9999
即可指定 rmi 的注册表在 9999 端口中运行,如果不指定端口,默认使用 1099,当然不想让 RMI 的注册表在前台显示,也可以输入后台运行命令:
start rmiregistry
构建 RMI 客户端
前面服务端和注册表都已经运行起来了,接下来我们需要的就是客户端发起访问的请求了,需要注意的是,User 实例类和 UserHandler 接口在客户端代码中也有一份(企业开发过程中会依赖同一份代码),所以这里的客户端调用代码如下:
try {UserHandler handler = (UserHandler) Naming.lookup("user");//这里使用的 user 是服务端启动的时候绑定的名称 String passWord = handler.getUserPassWord();String name = handler.getUserName(1);System.out.println("name: " + name);System.out.println("passWord: " + passWord);System.out.println("user: " + handler.getUserByName("pdc"));} catch (Exception e) {e.printStackTrace();
评论