互联网程序设计--RMI程序设计

教学与实践目的:

学会 RMI 软件架构的程序设计技术。

概述:

RMI (Remote Method Invocation) 模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。

Server 端通常会创建一个对象,并使之可以被远程访问。这个对象被称为远程对象。Server 端需要注册这个对象可以被 Client 远程访问。

Client 端调用可以被远程访问的对象上的方法,Client 端就可以和 Server 端进行通信并相互传递信息。

要实现RMI,服务器和客户端必须共享同一个接口。

从图中可以看到,Client 端有一个被称 Stub 的东西,有时也会被成为存根,它是 RMI Client 的代理对象,Stub 的主要功能是请求远程方法时构造一个信息块,RMI 协议会把这个信息块发送给 Server

这个信息块由几个部分组成:

远程对象标识符。

调用的方法描述。

编组后的参数值(RMI协议中使用的是对象序列化)。

既然 Client 端有一个 Stub 可以构造信息块发送给 Server 端,那么 Server 端必定会有一个接收这个信息快的对象,称为 Skeleton,它主要的工作是:

解析信息快中的调用对象标识符和方法描述,在 Server 端调用具体的对象方法。

取得调用的返回值或者异常值。

把返回值进行编组,返回给客户端 Stub.

Server 端主要是构建一个可以被传输的类 User,一个可以被远程访问的类 UserService,同时这个对象要注册到 RMI 开放给客户端使用。

相比 Server 端,Client 端就简单的多。直接引入可远程访问和需要传输的类,通过端口和 Server 端绑定的地址,就可以发起一次调用。

  1. 定义服务器接口(需要继承 Remote 类,方法需要抛出 RemoteException)。
  2. 实现服务器接口(需要继承 UnicastRemoteObject 类,实现定义的接口)。
  3. 定义传输的对象,传输的对象需要实现序列化(Serializable)接口。需要传输的类一定要实现序列化接口,不然传输时会报错。
  4. 注册( rmiregistry)远程对象,并启动服务端程序。服务端绑定了 UserService 对象作为远程访问的对象,启动时端口设置为 1900。

程序设计:

1、创建远程接口

在远程接口中定义的方法就是可以被客 户端远程调用的方法。一个远程接口中可以定义多个远程方法,远程接口本身 也可以定义多个。任何远程接口都要满足以下四个要求:

(1)直接或间接继承 java.rmi.Remote 接口,这是 RMI 规范的要求;

(2)客户进行远程调用,底层用到了网络和 I/O,而网络通信是不可靠 的,比如一旦服务器或者客户端有一方突然断开连接,或者网络出现故障,通 信就会失败。RMI 规范要求远程接口中定义的方法都要声明抛出 RemoteException 异常,在客户端进行远程方法调用时,RMI 框架会把遇到的 网络通信失败转换为 RemoteException,客户端可以捕获这种异常,并进行相 应的处理; (3)由于远程方法的变量必须被打包并通过网络运送,必须靠序列化完 成,所以远程方法的变量和返回值必须属于基本类型、或实现了 Serializable 接 口的引用类型(即可序列化的类型)、或者就是远程接口类型;

(4)服务端和客户端都必须各自拥有一份完全相同的的远程接口,不只是 接口名字、内部定义的远程方法完全一样,远程接口所属的包名称也要一致。 例如服务端的若干远程接口定义在 rmi 包中,那么客户端也需要有定义在 rmi 包中完全相同的若干远程接口。

我们在自己的项目工程中,新建一个包,命名为 rmi,然后在 rmi 包中创 建一个远程接口 HelloService:

package rmi;
......
public interface HelloService extends Remote {
public String echo(String msg) throws RemoteException;
public Date getTime() throws RemoteException;
public ArrayList<Integer> sort(ArrayList<Integer> list) throws 
RemoteException;
}

 由于我们现在是在自己的机器上模拟创建服务端和客户端,所以 rmi 中的 接口就可以看作是通信双方共同拥有的一份远程接口,如果有更多的接口,也 创建在这个包中(当然,如果客户端和服务端是创建在不同机器,就需要两份 相同的远程接口,客户端和服务端各自创建)。

2. 创建服务端的远程接口实现类

远程接口实现类(以下都简称远程服务类)就是远程服务对象所属的类, 其实现远程接口中定义的远程方法。其对象实例就是客户真正想要调用的方法 所属的对象。这个类除了必须实现远程接口,且为了成为远程服务对象,我们 的对象需要某些“远程的”功能,最简单的方式是扩展 java.rmi.server.UnicastRemoteObject,让超类帮我们完成这些工作 (UnicastRemoteObject 可以把实现了远程接口的类“导出”为远程对象,使它 具有相应的服务端 stub,并使它能够监听远程客户的方法调用请求,这些细节 都被 RMI 框架很好地隐藏了),并且远程接口实现类的构造方法必须声明抛出 RemoteException。

在项目工程中新建包,命名为 chapter12,然后在 chapter12 包中再创建一 个 server 子包,在 chapter12.server 包中创建类 HelloServerImpl:

package chapter12.server;
//远程服务类
import chapter12.rmi.HelloService;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Date;


public class HelloServerImpl extends UnicastRemoteObject implements HelloService {
    private String name;

    public HelloServerImpl() throws RemoteException {

    }

    public HelloServerImpl(String name) throws RemoteException {
        this.name = name;
    }

    @Override
    public String echo(String msg) throws RemoteException {
        System.out.println("服务端完成一些echo方法相关任务......");
        return "echo: " + msg + " from " + name;
    }

    @Override
    public Date getTime() throws RemoteException {
        System.out.println("服务端完成一些getTime方法相关任务......");
        return new Date();
    }
}

3. 创建远程服务发布程序

远程服务类创建后,如何为客户端服务呢?不是简单地写个服务发布程 序,仅仅实例化远程服务类就完事了,还要解决客户端如何寻找服务的问题。

RMI 采用一种命名服务机制来使得客户程序可以找到服务器上的一个远程 对象。RMI 注册器会提供这种命名服务。不妨把 RMI 注册器想象成日常生活中 的 114 电话查询系统,那些希望对外公开联系方式的单位先到 114 查询系统登 记,使得 114 查询系统记录该单位的名字和联系方式信息。当客户想知道某个 单位的联系方式时,只需向 114 查询系统提供单位的名字,114 查询系统就会 返回该单位的联系方式。

远程服务发布程序的一大任务就是向 RMI 注册器注册(绑定)远程对象。 远程对象注册到注册器后,就可以被客户端检索到。

在 chapter12.server 包中新建类 HelloServer:

package chapter12.server;

import chapter12.rmi.HelloService;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class HelloServer {
    public static void main(String[] args) {
        try {
            System.setProperty("java.rmi.server.hostname","192.168.233.151");

            Registry registry = LocateRegistry.createRegistry(1099);
            HelloService helloService = new HelloServerImpl("远程服务");
            registry.rebind("HelloService", helloService);

            System.out.println("发布了一个HelloService RMI服务");
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

4. 创建客户端程序

客户端程序首先要获得 RMI 注册器,然后在注册器中用助记符(别名)来 查找远程服务,并获得远程服务在本地的 stub,之后就可以像使用本地方法一 样调用远程服务方法。 在 chapter12 包下新建 client 包,在 chapter12.client 中创建 JavaFX 程序 HelloClientFX,其中 rmi 操作相关的核心代码封装在 rmiInit()方法中,程序运 行参考图 :

5、创建两个远程接口

在 rmi 包下,创建两个远程接口 RmiMsgService 和 RmiKitService。

6、创建 RmiKitService 接口的实现类

在 chapter12 包下,新建 RmiKitService 接口的远程服务类 RmiKitServiceImpl,在该类中,请 override(重写)实现全部四个方法。

7、创建学生端的远程服务发布程序

在 chapter12 包中,新建类 RmiStudentServer,该类负责在 RMI 注册器 中注册和启动学生端的远程服务对象(绑定助记符名称使用 RmiKitService, 供教师端检索),供教师端进行远程回调。注意:发布了 RmiKitService 远程 服务后,自己先在本地写一段测试代码模拟调用者访问该远程服务的四个方 法,看是否能正常工作,后续教师端将调用你的远程服务。

该程序框架如下:

package chapter12;
...... 
/**
* 该服务器程序负责在 RMI 注册器中注册和启动远程服务对象
*/
public class RmiStudentServer {
 ......
//助记符(别名)使用 RmiKitService,供教师端检索
registry.rebind("RmiKitService",rmiKitService);
......
}

8、创建学生端客户程序

在 chapter12 包中,新建 JavaFX 应用程序 RmiStudentClientFX.java(可参 考 HelloClientFX 结构,按需添加控件),参考界面如图所示

“发送信息”按钮和“发送学号和姓名”按钮分别用于调用教师端的两个 远程服务方法(即定义在远程接口中的两个不同的 send 方法),这个两个远程 方法的返回值就是教师端返回给学生的相关信息。

总结+总代码:

RMI程序示意图

代码框架

HelloClientFX

package chapter12.client;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import chapter12.rmi.HelloService;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class HelloClientFX extends Application {
    private TextArea taDisplay = new TextArea();
    private TextField tfMessage = new TextField();
    Button btnEcho = new Button("调用echo方法");
    Button btnGetTime = new Button("调用getTime方法");
    //客户端也有一份和服务端相同的远程接口
    private HelloService helloService;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        //main区域
        VBox vBoxMain = new VBox();
        vBoxMain.setSpacing(10);//各控件之间的间隔
        //VBoxMain面板中的内容距离四周的留空区域
        vBoxMain.setPadding(new Insets(10, 20, 10, 20));
        HBox hBox = new HBox();
        hBox.setSpacing(10);//各控件之间的间隔
        //HBox面板中的内容距离四周的留空区域
        hBox.setPadding(new Insets(10, 20, 10, 20));
        hBox.setAlignment(Pos.CENTER);
        hBox.getChildren().addAll(new Label("输入信息:"), tfMessage,
                btnEcho, btnGetTime);

        vBoxMain.getChildren().addAll(new Label("信息显示区:"),
                taDisplay, hBox);
        Scene scene = new Scene(vBoxMain);
        primaryStage.setScene(scene);
        primaryStage.show();

        // TODO 启动时需要先启动服务器
        //初始化rmi相关操作
        new Thread(() -> {
            rmiInit();
        }).start();
        btnEcho.setOnAction(event -> {
            try {
                String msg = tfMessage.getText();
                // 调用service中的方法,此处service虽然只有接口(Interface)但是实现在服务器端
                taDisplay.appendText(helloService.echo(msg) + "\n");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        });

        btnGetTime.setOnAction(event -> {
            try {
                String msg = helloService.getTime().toString();
                taDisplay.appendText(msg + "\n");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 初始化rmi相关操作,rmi操作相关的核心代码封装在rmiInit()中
     */
    public void rmiInit() {
        try {
            //(1)获取RMI注册器
            Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
            System.out.println("RMI远程服务别名列表:");
            for (String name : registry.list()) {
                System.out.println(name);
            }

            //(2)客户端(调用端)到注册器中使用助记符寻找并创建远程服务对象的客户端(调用端)stub,之后本地调用helloService的方法,实质就是调用了远程服务器上同名的远程接口下的同名方法
            helloService = (HelloService) registry.lookup("HelloService");
            //另外一种创建stub的方式
            //helloService = (HelloService)Naming.lookup("chapter12.rmi://127.0.0.1:1099/" + "HelloService");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

RmiMsgService

package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RmiMsgService extends Remote {
    //声明远程方法一,用于学生发送信息给教师端,该方法由教师端实现,学生端调用
    public String send(String msg) throws RemoteException;

    //声明远程方法二 用于学生发送学号和姓名给教师端,该方法由教师端实现,学生端调用
    public String send(String yourNo, String yourName) throws RemoteException;

}

 RmiKitService

package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RmiKitService extends Remote {
    //以下远程方法全部由学生端实现,教师端进行回调

    //远程方法一 将ipv4格式字符串转为长整型
    public long ipToLong(String ip) throws RemoteException;

    //远程方法二 将长整型转为ipv4字符串格式
    public String longToIp(long ipNum) throws RemoteException;

    //远程方法三 将":"或“-”格式的MAC地址转为Jpcap可用的字节数组
    public byte[] macStringToBytes(String macStr) throws RemoteException;

    //远程方法四 将Jpcap的byte[]格式的MAC地址转为可读的常见格式MAC字符串
    public String bytesToMACString(byte[] macBytes) throws RemoteException;

}

RmiKitServiceImpl 

package chapter12;

import rmi.RmiKitService;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RmiKitServiceImpl extends UnicastRemoteObject implements RmiKitService {
    public RmiKitServiceImpl() throws RemoteException {

    }

    /**
     * 将字符串类型的ip转换成长整型(long)
     * 方便进行遍历(+1即可)
     * @param ip String
     * @return long
     * @throws RemoteException
     */
    @Override
    public long ipToLong(String ip) throws RemoteException {
        String[] s = ip.split("\\.");
        long lIp = (Long.parseLong(s[0]) << 24)
                + (Long.parseLong(s[1]) << 16) +
                (Long.parseLong(s[2]) << 8)
                + (Long.parseLong(s[3]));
        return lIp;
    }

    /**
     * 将long类型的ip转换成字符串
     * @param ipNum long
     * @return String
     * @throws RemoteException
     */
    @Override
    public String longToIp(long ipNum) throws RemoteException {
        // 由于作为服务端,因此使用线程安全的StringBuffer而不是StringBuilder,两者区别和使用详见https://blog.csdn.net/itchuxuezhe_yang/article/details/89966303
        StringBuffer sb = new StringBuffer("");
        sb.append(String.valueOf(ipNum >> 24)).append(".").
                append(String.valueOf((ipNum & 0x00ffffff) >> 16)).append(".").
                append(String.valueOf((ipNum & 0x0000ffff) >> 8)).append(".").
                append(String.valueOf(ipNum & 0x000000ff));
        return sb.toString();
    }

    /**
     * 将MAC地址转换成Bytes
     * @param macStr String
     * @return byte[]
     * @throws RemoteException
     */
    @Override
    public byte[] macStringToBytes(String macStr) throws RemoteException {
        String[] macs = new String[6];
        // 可接受-和:分隔的MAC字符串
        if (macStr.contains("-")) {
            macs = doSplit(macStr, "-");
        } else if (macStr.contains(":")) {
            macs = doSplit(macStr, ":");
        } else {
            return null;
        }
        byte[] result = new byte[6];
        for (int i = 0; i < macs.length; i++) {
            result[i] = (byte) Integer.parseInt(macs[i], 16);
        }
        return result;
    }

    /**
     * 将byte转换成MAC字符串
     * @param macBytes byte[]
     * @return String
     * @throws RemoteException
     */
    @Override
    public String bytesToMACString(byte[] macBytes) throws RemoteException {
        StringBuffer sb = new StringBuffer("");
        for (int i = 0; i < macBytes.length; i++) {
            if (i != 0) {
                sb.append("-");
            }
            int temp = macBytes[i] & 0xff;
            String str = Integer.toHexString(temp);
            if (str.length() == 1) {
                sb.append("0" + str);
            } else {
                sb.append(str);
            }
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 切分MAC字符串工厂方法(伪)
     * @param MAC String MAC地址
     * @param splitter String 分隔符
     * @return
     */
    private static String[] doSplit(String MAC, String splitter) {
        return MAC.split(splitter);
    }
}

RmiStudentServer

package chapter12;
//该类负责在 RMI 注册器中注册和启动学生端的远程服务对象(绑定助记符名称使用 RmiKitService,供教师端检索),供教师端进行远程回调。
// 注意:发布了 RmiKitService 远程服务后,自己先在本地写一段测试代码模拟调用者访问该远程服务的四个方法,看是否能正常工作,后续教师端将调用你的远程服务。
import rmi.RmiKitService;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
 * 该服务器程序负责在 RMI 注册器中注册和启动远程服务对象
 */

public class RmiStudentServer {
    public static void main(String[] args) {
        try {
            System.setProperty("java.rmi.server.hostname","192.168.233.151");
            Registry registry = LocateRegistry.createRegistry(1099);
            RmiKitService rmiKitService = new RmiKitServiceImpl();
     //助记符(别名)使用 RmiKitService,供教师端检索registry.rebind("RmiKitService",rmiKitService)
            registry.rebind("RmiKitService", rmiKitService);

            System.out.println("发布了一个RmiKitService RMI服务");
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

RmiStudentClientFX

package chapter12;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import rmi.RmiMsgService;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

//“发送信息”按钮和“发送学号和姓名”按钮分别用于调用教师端的两个远程服务方法(即定义在远程接口中的两个不同的 send 方法),这个两个远程方法的返回值就是教师端返回给学生的相关信息。

public class RmiStudentClientFX extends Application {
    private TextArea taDisplay = new TextArea();
    private TextField tfMessage = new TextField();
    private TextField tfStunum = new TextField();
    private TextField tfName = new TextField();
    Button btnSendMsg = new Button("发送消息");
    Button btnSendStu = new Button("发送学号和姓名");

    private RmiMsgService rmiMsgService;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        VBox vBoxMain = new VBox();
        vBoxMain.setSpacing(10);
        vBoxMain.setPadding(new Insets(10, 20, 10, 20));
        HBox hBox = new HBox();
        hBox.setSpacing(10);
        hBox.setPadding(new Insets(10, 20, 10, 20));
        hBox.setAlignment(Pos.CENTER);
        hBox.getChildren().addAll(new Label("输入信息:"), tfMessage,
                btnSendMsg, new Label("学号:"), tfStunum, new Label("姓名:"), tfName, btnSendStu);

        vBoxMain.getChildren().addAll(new Label("信息显示区:"),
                taDisplay, hBox);
        Scene scene = new Scene(vBoxMain);
        primaryStage.setScene(scene);
        primaryStage.show();

        // TODO 启动时需要先启动服务器
        //初始化rmi相关操作
        new Thread(() -> {
            rmiInit();
        }).start();
        btnSendMsg.setOnAction(event -> {
            try {
                taDisplay.appendText(rmiMsgService.send(tfMessage.getText().trim()) + "\n");
                tfMessage.clear();

            } catch (RemoteException e) {
                e.printStackTrace();
            }
        });

        btnSendStu.setOnAction(event -> {
            try {
                taDisplay.appendText(rmiMsgService.send(tfStunum.getText(), tfName.getText()) + "\n");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        });

    }

    /**
     * 初始化rmi相关操作
     */
    public void rmiInit() {
        try {
            //(1)获取RMI注册器
            Registry registry = LocateRegistry.getRegistry("202.116.195.71", 1099);
            System.out.println("RMI远程服务别名列表:");
            for (String name : registry.list()) {
                System.out.println(name);
            }

            //(2)客户端(调用端)到注册器中使用助记符寻找并创建远程服务对象的客户端(调用端)stub,之后本地调用helloService的方法,实质就是调用了远程服务器上同名的远程接口下的同名方法
            rmiMsgService = (RmiMsgService) registry.lookup("RmiMsgService");
//            rmiMsgService = (RmiMsgService) Naming.lookup("chapter12.rmi://202.116.195.71:1099/" + "rmiMsgService");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tsuyt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值