关于RPC框架,首先我们要了解什么叫RPC,为什么要用RPC。
RPC是只远程过程调用,也就是说两台服务器A,B, 一个应用部署在A服务器上,另一个应用部署在B服务器上,A服务器上的应用想要调用B服务器上的应用提供的方法/函数,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语意和传递调用的参数。
比如说,一个方法可能这样定义:
Student getStudentByName(String name)
那么:
首先,要解决通讯的问题,主要是通过在客户端和服务器之间建立TCP连接,远程调用过程中所有交换的数据都在这个连接里传输,连接可以是按需连接,调用结束后就关闭,也
可以是长连接,多个远程调用共享一个连接。
第二,要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口号,方法的名称是什么,这样才能完成调用,比如基于WEB服务协议的RPC,就要提供一个endpoint URI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。
第三,当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于 二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给B服务器。
第四,B服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后找到对应的方法(寻址的一部分)进行本地调用,然后得到返回值。
第五,返回值还要发送给A服务器上的应用,也要经过序列化的方式发送,服务器A接到后,在反序列化,恢复为内存中的表达方式,交给A服务器上的应用。
RMI与Web Service的同和异:
RMI与Web Service各有各的优势与特点。在RMI中,客户端可以传递一个包含方法的对象给服务端,服务端可以执行该对象的方法,Web Service是通过HTTP协议进行数据传输,只能传递值,而不能传递方法。由于 Web Service是通过HTTP协议进行数据传输,因此,Web Service可以实现跨平台的调用,而RMI的客户端和服务端都必须是Java平台
1 RMI简介
RMI是Remote Method Invocation的简称,即远程方法调用。通过RMI,Client可以调用Server的方法。
首先,Server生成Stub和Skeleton,用于描述方法和解析方法调用。
然后,Client获取Stub,即获取Server提供的方法的描述,从而知道Server提供了哪些方法及相应的参数(如图:步骤1)。
接着,Client通过Stub调用Server的方法(如图:步骤2),调用信息被传递到Server(如图:步骤3),由Server的Skeleton解析。
解析后,Server根据信息,调用相应的方法(如图:步骤4),最后,将方法返回的数据传递给Client(如图:步骤5、6、7)。
单看Client方面,在不考虑从Server获取Stub的步骤前提下,Client的操作就是调用一个方法,然后获得返回值,与调用本地方法一样。
同样,单看Server方面,就是某个方法被调用,Server运行该方法,然后将结果返回给调用者。
但是,对于步骤1来讲,Client如何知道在哪里下载Server的Stub?或者说,Client如何知道这个Server位置?
这里,要用到的就是Registry。首先,Server将方法绑定在Registry上。接着,Client可以通过Registry来获取相关信息。然后,Client就可以调用该Server的方法了。
类似于以前学校里的公告板(互联网还不发达的时候),学校各部门的通知会张贴(注册)在公告板上,学生通过公告板来获取相关的信息。
若对Web Service有了解,会发现RMI与Web Service有一些相似。在Web Service中,客户端是通过获取Service Description来知道服务端提供了哪些方法及相关信息,并根据获取的信息,传递相应的数据至服务端,服务端解析并调用相应方法后,将结果返回至客户端。
但是,RMI与Web Service各有各的优势与特点。在RMI中,客户端可以传递一个包含方法的对象给服务端,服务端可以执行该对象的方法,Web Service是通过HTTP协议进行数据传输,只能传递值,而不能传递方法。由于 Web Service是通过HTTP协议进行数据传输,因此,Web Service可以实现跨平台的调用,而RMI的客户端和服务端都必须是Java平台。
2 实现步骤:Server端
2.1 定义接口(Interface)
在Server定义一个接口,该接口描述了Server提供的方法。接口仅定义抽象方法(仅方法名、参数及返回值等信息),不需要给出具体的逻辑实现。这个接口被作为Stub来使用。
Client通过获取该接口(Stub),从而知道Server提供了哪些方法,包括方法名、参数及返回值等信息,但并不知道这些方法的具体实现。
注意:由于该接口将用于远程调用,因此,接口需要继承Remote类,且接口方法需要声明可能会抛出RemoteException。
-
Public interface Compute extends Remote {
-
public int multiply(int a, int b) throws RemoteException;
-
}
若参数为某类的对象,该类需要继承Serializable(可序列化)。
-
public interface Compute extends Remote {
-
public Object executeTask(Task task) throws RemoteException;
-
}
-
public interface Task extends Serializable {
-
public Object execute();
-
}
2.2 实现接口
当Client调用Server提供的方法时,Server需要执行该方法并返回结果至Client。因此,Server需要提供方法的具体逻辑实现,即Server需要实现上述定义的接口。
-
public class ComputeEngine implements Compute {
-
public ComputeEngine() throws RemoteException {
-
super();
-
}
-
public int multiply(int a, int b) throws RemoteException {
-
return a * b;
-
}
-
}
2.3 公开接口
当Server完成接口的定义和实现后,需要将该接口公开,以表示该接口是用于远程调用的接口。有两种方法来完成这一操作。
方法一:使实现该接口的类继承UnicastRemoteObject类
-
public class ComputeEngine extends UnicastRemoteObject implements Compute {
-
public ComputeEngine() throws RemoteException {
-
super();
-
}
-
public int multiply(int a, int b) throws RemoteException {
-
return a * b;
-
}
-
}
方法二:通过UnicastRemoteObject.exportObject()方法将某个对象设置为公开接口对象
-
public static void main(String[] args) {
-
// 省略其它代码
-
ComputeEngine computeEngine = new ComputeEngine();
-
UnicastRemoteObject.exportObject(computeEngine);
-
// 省略其它代码
-
}
2.4 绑定接口
2.4.1 绑定对象
由于Server对外公开的是接口,或者说,向Client提供的是Stub,因此,在绑定接口时,应该将Stub对象绑定至Registry。做法是,创建实现接口的类的对象,赋值给接口对象。(多态技术)
-
// 通过 2.3中方法一 公开接口
-
Compute compute = new ComputeEngine();
-
// 通过 2.3中方法二 公开接口
-
ComputeEngine computeEngine = new ComputeEngine();
-
Compute compute = (Compute) UnicastRemoteObject.exportObject(computeEngine);
2.4.2 获取Registry
LocateRegistry类相关方法:
-
// 在本地创建一个Registry并指定端口
-
Registry registry = LocateRegistry.createRegistry(int port);
-
// 获得一个本地Registry对象并使用默认端口(1099)
-
Registry registry = LocateRegistry.getRegistry();
-
// 获得一个本地Registry对象并指定端口
-
Registry registry = LocateRegistry.getRegistry(int port);
-
// 获得一个指定服务器的Registry对象并使用默认端口(1099)
-
Registry registry = LocateRegistry.getRegistry(String host);
-
// 获得一个指定服务器的Registry对象并指定端口
-
Registry registry = LocateRegistry.getRegistry(String host, int port);
2.4.3 绑定Registry
Registry相关方法:
-
// 在registry上将name与obj绑定
-
registry.bind(String name, Remote obj);
-
// 在registry上将name与obj重新绑定(替换原name的绑定)
-
registry.rebind(String name, Remote obj);
-
// 在registry上将name解绑(删除name的绑定)
-
registry.unbind(String name);
-
// 在registry上查找指定name并返回相应的obj对象
-
registry.lookup(String name);
2.4.4 Naming
有的时候,直接使用Naming类,而不使用LocateRegistry和Registry类。但是,Naming类实际上就是对LocateRegistry和Registry的再一次封装,其内部还是通过这两个类的方法实现绑定操作的。
例如,我们通过LocateRegistry.getRegistry方法来确定Registry的位置,然后通过registry.bind方法绑定name和obj。上述两步骤,可以通过Naming.bind方法一步直接完成,代码如下:
Naming.bind(“rmi://localhost:1099/compute”, stub);
即,通过Naming类方法,在指定name时,需要加上host地址。
Naming类相关方法:
-
// 将name与obj绑定
-
Naming.bind(String name, Remote obj);
-
// 将name与obj重新绑定(替换原name的绑定)
-
Naming.rebind(String name, Remote obj);
-
// 将name解绑(删除name的绑定)
-
Naming.unbind(String name);
-
// 查找指定name并返回相应的obj对象
-
Naming.lookup(String name);
3 实现步骤:Client端
3.1 添加接口
将Server的接口文件添加至Client项目。
-
Public interface Compute extends Remote {
-
public int multiply(int a, int b) throws RemoteException;
-
}
3.2 调用接口
-
public static void main(String args[]) {
-
try {
-
String name = "rmi://localhost:1099/compute";
-
Compute compute = (Compute) Naming.lookup(name);
-
int result = compute.multiply(3, 5);
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
4 执行步骤
4.1 系统参数
java.rmi.server.codebase=”…”
指定类文件路径,远程调用时可能会从该路径加载类文件。
java.rmi.server.useCodebaseOnly=true/false
如果设置为true,将禁用自动加载类文件,仅从CLASSPATH和java.rmi.server.codebase指定路径加载类文件。
java.security.policy=”…”
指定附加或不同的安全策略文件。
4.2 启动Registry
指定系统参数:
-J-D(name=value)
Windows终端:
start rmiregistry –J-Djava.rmi.server.useCodebaseOnly=false
Linux/Mac终端:
rmiregistry –J-Djava.rmi.server.useCodebaseOnly=false
4.3 启动Server
指定系统参数:
-D(name=value)
启动Server:
java -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy="policy.permission" Server
4.4 启动Client
指定系统参数:
-D(name=value)
启动Client:
java -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy="policy.permission" Client
RML执行过程的逻辑归纳:
1.server在远程机器上监听一个端口,这个端口是jvm或者os在运行时随机选择的一个端口。可以说server在远程机器上在这个端口上导出自己。
2.client并不知道server在哪,以及sever监听哪个端口,但是他有stub,stub知道所有这些东西,这样client可以调用stub上他想调用的任何方法。
3.client调用给你stub上的方法
4.stub链接server监听的端口并发送参数,详细过程如下:
a.client连接server监听的端口
b.server收到请求并创建一个socket来处理这个链接
c.server继续监听到来的请求
d.使用双方协定的歇息,传送参数和结果
e.协议可以是JRMP或者 iiop
5.方法在远程server上执行,并发执行结果返回给stub
6.stub返回结果给client,就好像是stub执行了这个方法一样