很多时候,我们应用程序所有的组件都在同一台主机的同一个 JVM 中运行。
然而,有的时候,基于一些原因(例如:性能、安全),我们也会把程序的某些部分分别部署在不同的机器上。
这时候,我们就不得不考虑这些分布在不同机器上的组件(或是子系统)应如何通信,来共同实现业务功能。
在 Java 中,组件间要通信,可以基于消息的方式实现,也可以基于远程调用的方式实现。
前面学习过的 TCP/IP 、UDP/IP 编程,就可以实现不同主机间的通信。
但是,这样一来,我们就需要处理网络连接、数据收发 以及 Java 对象的序列化等一系列的问题。想想就头大。
如果能够取得远程对象的引用就好了,因为这样我们就可以直接调用它的方法,就好像大家都在同一个 JVM 中。
这就是 RMI(远程方法调用,Remote Method Invocation)提供给我们的功能。
RMI
基本原理
首先,我们要理解两个概念。
- 这里说的“远程”,并不仅仅指的是不同的主机之间,在同一台主机的不同虚拟机之间,也属于远程。
- 如果一个对象的方法能够在不同的 JVM 之间被调用,那么这个对象就可以称为远程对象(Remote Object)。
基于 RMI 的应用,通常由服务端程序和客户端程序组成。
服务端程序负责创建一些远程对象,并使得对这些远程对象的引用能够被访问。
客户端程序则会获取远程对象的引用,并调用这些远程对象所提供的方法。
其基本原理大致如下:
编程模型
基于 RMI 开发一个分布式应用主要遵循以下几个步骤:
- 定义远程接口:也就是指定哪些方法能够被客户端远程调用。
- 实现远程接口:编写远程接口具体的实现类。
- 编写服务器端代码:创建并暴露远程对象,启动远程对象注册表,监听请求。
- 编写客户端代码:通过远程注册表的引用,查询并获取远程对象,并调用其方法。
参考代码
定义远程接口:
1
2
3
4
5
6
7
8
9
10
11import java.rmi.Remote;
import java.rmi.RemoteException;
// 服务器端对外提供的接口,必须继承自 Remote
public interface TimeService extends Remote {
// 方法抛出 RemoteException
// 远程调用底层涉及网络、IO 操作,因此总是有风险
public String now() throws RemoteException;
}实现远程接口:
1
2
3
4
5
6
7
8
9
10
11
12
13import java.rmi.RemoteException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeServiceImpl implements TimeService {
@Override
public String now() throws RemoteException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return sdf.format(new Date());
}
}编写服务器端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
public static void main(String[] args) throws RemoteException,
AlreadyBoundException {
// 远程对象接收调用的端口
final int REMOTE_PORT = 54321;
// 远程对象在注册表中绑定的名称
final String REMOTE_NAME = "TimeService";
// 注册表接受请求的端口
final int REGISTRY_PORT = 1099;
// 创建远程对象
TimeService timeService = new TimeServiceImpl();
// 暴露该远程对象,以便能够接收传入的调用
// 这会通过动态代理,自动生成 stub 类
// 如果端口设置为 0 ,则会使用匿名端口
UnicastRemoteObject.exportObject(timeService, REMOTE_PORT);
// 创建并启动远程对象注册表
Registry registry = LocateRegistry.createRegistry(REGISTRY_PORT);
// 将远程对象绑定到注册表,使得客户端可以查找到
registry.bind(REMOTE_NAME, timeService);
}
}编写客户端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException,
NotBoundException {
// 在默认注册表端口 1099 获取远程注册表的引用
Registry registry = LocateRegistry.getRegistry("localhost");
final String REMOTE_NAME = "TimeService";
// 根据名称获取远程对象的引用
// 实际上是动态生成的代理对象(Stub)
TimeService timeService = (TimeService) registry.lookup(REMOTE_NAME);
System.out.println(timeService.getClass().getName()); // com.sun.proxy.$Proxy0
System.out.println(timeService.toString()); // TimeServiceImpl_Stub[...]
// 调用其方法
String now = timeService.now();
System.out.println("现在是北京时间:" + now);
}
}
要点
- 远程对象除了
Remote
接口,也可以实现其它的接口,但其它接口里声明的方法只在本地 JVM 可用,不能被远程调用。 - 远程接口声明的方法,必须抛出
RemoteException
,原因是远程调用的底层会涉及网络、IO 等操作,因此总是有风险。 - 远程方法的参数和返回值,只允许是基本数据类型、可序列化的对象。原因也很简单,因为要进行序列化操作。
- 在 JDK 1.5 之前,对于每个远程接口,我们都需要使用 RMI 编译器(rmic)手动生成 Stub 和 Skeleton 类,并打包到客户端。但是 JDK 1.5 之后,RMI 工具使用了动态代理,会在运行时动态生成代理对象,使用起来更简单。实际上,如果你现在还调用 rmic 的话,会出现以下警告信息:
警告: 为 JRMP 生成和使用骨架及静态存根已过时。骨架不再必要, 而静态存根已由动态生成的存根取代。建议用户不再使用rmic来生成骨架和静态存根。
- RMI 注册机制是一个简单的远程对象命名服务,就好像是一个共享的电话本,客户端通过远程对象的名字可以查到远程对象(实际上是 Stub)的引用。
- RMI 框架采用分布式垃圾收集机制(DGC,Distributed Garbage Collection)来管理远程对象的生命周期。其主要规则为:当一个远程对象没有任何本地变量引用和远程引用时,这个远程对象才可以被垃圾回收器回收。如果远程对象希望 GC 回收前做一些安全退出的操作(例如:释放资源),可以实现
java.rmi.server.Unreferenced
接口,并在unreferenced()
方法中执行相关操作。当 RMI 框架监测到一个远程对象没有任何远程引用时,这个方法。
WebService
除了 RMI ,JDK 1.6 之后,也集成了 WebService 。因此,我们也可以通过这种方式实现远程调用。
基本原理
WebService 基于 XML 进行信息交换,相对 RMI 而言,它具有跨平台、跨语言的优势。
其基本原理大致如下:
编程模型
基于 WebService 开发应用,主要有以下几个步骤:
- 定义远程接口:也就是指定哪些方法能够被客户端远程调用。
- 实现远程接口:编写远程接口具体的实现类,并通过
@WebService
注解,配置对外暴露的 WebService 的名称,以及生成的 stub 代码的类名和包名等信息。 - 编写服务器端代码:发布 WebService 。
- 生成客户端辅助代码:使用 wsimport 工具,解析 WSDL ,并生成辅助代码。
- 编写客户端代码:导入辅助代码,并调用其方法。
参考代码
定义远程接口:
1
2
3
4
5public interface TimeService {
// 获取远程服务器的时间
public String now();
}实现远程接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import java.text.SimpleDateFormat;
import java.util.Date;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
// 标记为 WebService 的实现类
// 可以配置生成的 stub 代码的类名和包名等信息
@WebService(name = "TimeService", serviceName = "RemoteTime", targetNamespace = "http://stub.webservice.codinglike.com")
@SOAPBinding(style = SOAPBinding.Style.RPC)
public class TimeServiceImpl implements TimeService {
@Override
public String now() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return sdf.format(new Date());
}
}编写服务器端代码:
1
2
3
4
5
6
7
8
9
10
11
12import javax.xml.ws.Endpoint;
public class WebServiceServer {
public static void main(String[] args) {
// 发布 WebService
// 客户端可以通过 http://localhost:54321/timeService?wsdl 访问 WSDL
Endpoint.publish("http://localhost:54321/timeService",
new TimeServiceImpl());
}
}生成客户端辅助代码:
1
wsimport -keep http://localhost:54321/timeService?wsdl
编写客户端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import com.codinglike.webservice.stub.RemoteTime;
import com.codinglike.webservice.stub.TimeService;
public class WebServiceClient {
public static void main(String[] args) {
// 先使用 wsimport -keep http://localhost:54321/timeService?wsdl 生成辅助代码
// 再基于辅助代码实现业务功能
RemoteTime stub = new RemoteTime();
TimeService timeService = stub.getTimeServicePort();
String now = timeService.now();
System.out.println("现在是北京时间:" + now);
}
}
要点
- WebService 使用 SOAP(简单对象访问协议,Simple Object Access Protocol)来传输的数据。
- WSDL (网络服务描述语言,Web Services Description Language)用来描述如何访问具体的接口。
总结
以上内容介绍了基于 Java 自身技术实现远程通信的两种方式: RMI 和 WebService 。它们的共同点在于,都封装底层网络通信的细节,使用起来比较简单。但是,RMI 是 Java 独有的,而 WebService 是跨语言的。
除了这两种方式,还有一些第三方提供的技术和框架可以使用,例如:Spring Remoting 、JBoss Remoting 等。
但是,从目前的趋势来看,现在的应用架构更倾向于,让客户端直接通过 HTTP 与服务器进行通信。在 API 的设计风格上,RESTful 的架构越来越流行。而就传输的数据格式而言,相对于 XML 也更喜欢使用 JSON 。