开发RMI服务器
1)新建接口,需继承java.rmi.Remote接口
2)远程服务提供类,需继承java.rmi.server.UnicastRemoteObject对象,实现上面新建的接口。
public static void main(String[] args) throws Exception{
Server imp=new ServerImpl();
//LocateRegistry.createRegistry(1099);
Naming.rebind(“rmi://:1099/it”,imp);
}
直接运行上面的程序会抛出 java.rmi.ConnectException 异常,表明该服务类无法启动,这是因为我们还需要在服务器上注册 RMI 服务, JDK 提供了工具 rmiregistry 工具。
格式:rmiregistry <port>
默认使用1099端口,也可以手工指定端口。
应该在RMI服务器程序所在的路径下运行该命令,对于上面的程序,应该进入ServerImpl类所在的位置去运行rmiregistry命令。
之后重新执行,即可运行正常。
开发RMI客户端
1)通过JNDI查找远程服务的对象,并对其执行强制类型转换。
2)调用远程方法
public static void main(String[] args) throws Exception{
Server ser=(Server)Naming.lookup(“rmi://:1099/it”);
System.out.println(ser.helloWorld(“king”));
System.out.println(ser.getPerson(“king”,27));
}
从客户端来看服务端是透明的,与平常调用方法没有区别。
远程方法的返回值必须有一个要求,实现Serializable接口。因为远程方法的参数、返回值都必须在网络上传输,网络只能传输字节流。
注意:个人测试的时候,如果带包名的话会有问题,建议不要使用package
RMI的基本原理
可以认为RMI是一种封装过的Socket通信机制
原理示意图
客户端调用远程方法之后,接下来要经过几个步骤
1)本地客户端调用远程服务对象的方法——实际上是调用Stub对象的方法。
2)Stub对象其实就是远程服务对象在客户端的代理。Stub对象会调用请求进行编码,保证远程调用请求可以在网络上传输。所以这一步要求调用远程方法的所有参数都是可序列化的。
3)通过底层网络传输将请求传递到Skeleton。
4)Skeleton收到Stub通过网络传输过来的调用请求后,Skeleton对请求进行解码,将请求转换为满足远程服务对象要求的请求。
5)Skeleton将解码后的请求发送到远程服务对象,让远程服务对象来负责处理调用请求。
6)Skeleton收到远程服务对象的执行结果后(就是方法返回值),再次对执行结果进行编码,因此这一步要求RMI中的方法返回值是可序列化的。
7)通过底层网络传输将处理结果送到Stub。
8)Stub解码处理结果。
9)Stub将解码后的符合本地客户端要求的结果送给本地客户端。
10)本地客户端收到执行结果。假象就是:本地客户端成功地调用了远程Java方法。
对于一个实际的网络程序,是同时作为客户端和服务器端的。
服务器端程序的特征:
1)有固定的IP地址
2)可以执行JNDI绑定,将远程服务暴露出来
3)服务器总处于等待状态,随时准备接受远程调用
客户端则与之相反
1)客户端往往没有固定IP地址
2)客户端不会执行JNDI绑定,不会主动暴露服务
3)客户端不允许外界直接调用
因此服务器不大可能直接调用客户端的方法——只可能先由客户端调用服务器端的远程方法,这样服务器才可以回过来调用远程客户端的方法,因此这种调用也被成为回调Callback。
既然客户端并未执行JNDI绑定,那么服务器如何获得对客户端的引用呢?答案是方法调用的参数,当客户端调用远程服务器端的远程方法时,客户端应该将自身作为参数传入远程方法。
public interface Client extends Remote {
void showDialog(String msg) throws RemoteException;
}
public class RMIClient implements Client {
@Override
public void showDialog(String msg) throws RemoteException {
System.out.println(msg);
}
public static void main(String[] args) throws Exception {
Client client = new RMIClient();
UnicastRemoteObject.exportObject(client);
Server stub = (Server) Naming.lookup("rmi://127.0.0.1:1099/it");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while ((line = br.readLine()) != null) {
stub.hello(client, line);
}
}
}
public interface Server extends Remote {
void hello(Client client, String saying) throws Exception;
}
public class ServerImpl extends UnicastRemoteObject implements Server {
protected ServerImpl() throws RemoteException {
super();
}
private static final long serialVersionUID = 1L;
static List<Client> users = new ArrayList<Client>();
@Override
public void hello(Client cm, String saying) throws Exception {
if (!users.contains(cm)) {
users.add(cm);
}
try {
Date now = new Date();
String msg = now + saying;
for (Client c : users) {
c.showDialog(msg);
}
} catch (RemoteException e) {
users.remove(cm);
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
Server remote = new ServerImpl();
Naming.rebind("rmi://127.0.0.1:1099/it", remote);
}
}
上面的程序写好之后,按如下步骤开始编译
1)编译客户端接口Client,生成Client.class
2)将客户端接口的Client.class文件复制到服务器端
3)编译服务器端接口Server,生成Server.class
4)编译服务器端实现类ServerImpl,生成ServerImpl.class
5)将服务器端远程接口Client.class复制到客户端
6)编译客户端实现类RMIClient,生成RMIClient.class
7)在客户端用rmic编译RMIClient类(运行rmic RMIClient即可),生成RMIClient_Stub.class,并将该class文件复制到客户端。
运行服务器端程序,再启动多个客户端程序即可实现网络聊天功能。
除了RMI之外,CORBA(Common Object Request Broker Architecture,通用对象请求代理架构)、Web Service等也能实现类似的功能。
这些技术也存在不足:
1)同步通信:客户端调用服务器端对象的方法之后,在服务器端方法返回之前,客户端程序无法向下执行。
2)客户端和服务器端的生命周期耦合:客户端的调用代码和服务器端的被调方法必须同步执行,而且这两段代码都必须可以正常执行。如果服务器端的被调方法出现异常或网络通讯出现异常,客户端代码都将受到异常。
点对点通信:客户端每次只能调用一个服务器的方法。