自己寫了個Java RMI(遠端方法呼叫)的實現案例

淵渟嶽發表於2022-03-05

自己簡單寫了個Java RMI(遠端方法呼叫)的實現案例。

為了更好理解RMI(遠端方法呼叫)、序列化的意義等等,花費三天多的時間肝了一個Java RMI的實現案例。

!!!高能預警!!!

程式碼量有點大,先附上了簡圖用於理解

整個過程分為兩大步

  • 第一步--註冊過程:客戶端通過指定路由獲取註冊中心指定的遠端客戶端物件
  • 第二部--服務呼叫過程:客戶端通過遠端客戶端物件訪問遠端服務端(代理服務)從而訪問到真實服務的實現

調整為舒適的姿勢,慢慢看…… 廢話少說,上程式碼!!!

1.定義遠端標記介面

面向介面程式設計,具體作用看後面的程式碼怎麼使用

// 標記介面:直接或間接實現MyRMI介面將獲得遠端呼叫的能力
public interface MyRMI{
}

2.編寫RMI 服務註冊中心

註冊中心類:用於註冊服務和獲取服務,核心是hashMap路由表物件

/**
 * 註冊中心:維護服務釋出的登錄檔
 */
public class MyRMIRegistry {
    // 預設埠
    public final int REGISTRY_PORT = 10099;
    private String host;
    private int port;
    private Map<String, MyRMI> bindings;

    public MyRMIRegistry(int port){
        this.port = port;
    }
    public MyRMIRegistry(String host, int port){
        this.host=host;
        this.port=port;
    }

    public void createRegistry(String serverName,MyRMI myRMI){
        // 註冊服務,並開啟服務
        this.bindings = new HashMap<>();
        String host = null;
        try {
            host = Inet4Address.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        // 路由規則可自行定義,只要能確保Key唯一即可
        String binding = "myrmi://"+host+":"+port+"/"+serverName;
        this.bindings.put("myrmi://"+host+":"+port+"/"+serverName,myRMI);
        System.out.println("註冊的服務有:"+bindings.keySet().toString());
        MyRMIRegistryServer myRMIRegistryServer = new MyRMIRegistryServer(this.port, this.bindings);
        Executors.newCachedThreadPool().submit(myRMIRegistryServer); // 執行緒池啟動服務

    }

    public MyRMI getRegistry(String serverName){
        Socket socket = null;
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        MyRMI myRMI = null;
        // 通過
        try {
            socket = new Socket(host, port);
            out = new ObjectOutputStream(socket.getOutputStream());
            out.writeObject("myrmi://"+host+":"+port+"/"+serverName);
            in = new ObjectInputStream(socket.getInputStream());
            myRMI = (MyRMI)in.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        }catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return myRMI;
    }
}

RMI 註冊中心獲取服務的執行緒:啟動註冊中心服務,等待客戶端來獲取路由表中的遠端客戶端

/**
 * RMI註冊中心獲取服務執行緒
 */
public class MyRMIRegistryServer implements Runnable {
    private int port;
    private Map<String, MyRMI> bindings;
    public MyRMIRegistryServer(Integer port,Map<String, MyRMI> bindings){
        this.port = port;
        this.bindings = bindings;
    }

    @Override
    public void run() {
        ServerSocket serverSocket = null;
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        try {
            serverSocket = new ServerSocket(this.port);
            while(true){
                Socket socket = serverSocket.accept();
                in = new ObjectInputStream(socket.getInputStream());
                out = new ObjectOutputStream(socket.getOutputStream());
                // 看看客戶端想要什麼服務
                String serverName = (String)in.readObject();
                Iterator iterator = bindings.keySet().iterator();
                while (iterator.hasNext()){
                    String key = (String) iterator.next();
                    if(serverName.equals(key)){
                        // 給客戶端響應服務物件
                        MyRMI myRMI = bindings.get(key);
                        out.writeObject(myRMI);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            // 異常後進入
            try {
                if (out!=null)  out.close();
                if (in!=null)   in.close();
                if (serverSocket!=null) serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

}

3.定義要釋出的服務介面

需要提供RMI服務的介面,必須繼承自定義的MyRMI標記介面

/**
 * 服務介面
 */
public interface Hello extends MyRMI {
    public String sayHello(String name);
}

4.服務用到的實體類

/**
 * 物件資料類:Person
 */
public class Person implements Serializable {
    // 序列化版本UID
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private String sex;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    @Override
    public String toString() {
        return "Person{" + "name='" + name + ", age=" + age + ", sex='" + sex + '}';
    }
    public Person() {
    }
    public Person(String name, Integer age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

5.實現要釋出的服務介面

/**
 * 對外提供的服務實現
 */
public class HelloImpl implements Hello {
    private static File file = new File("D:/HelloRMI.txt");
    private static List<Person> list = new ArrayList<>();

    @Override
    public String sayHello(String name) {
        String result = "沒有獲取到"+name+"的資訊";
        try {
            List<Person> personList = readList();
            for(Person person:personList){
                if (person.getName().equals(name)){
                    result = "Hello , welcome to the RMI! "
                            + "姓名:"+name + " 年齡:"+person.getAge()+" 性別:"+person.getSex();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return result;
    }


    /**
     * 生成資料,為測試做準備
     * @param args
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //資料準備:集合類都實現了序列化介面Serializable
        list.add(new Person("張三", 38, "男"));
        list.add(new Person("李四", 38, "男"));
        list.add(new Person("如花", 18, "女"));
        // 持久化物件資料
        writerList(list);
        // 查詢持久化物件資料
        List<Person> personList = readList();
        System.out.println("遍歷持久化物件資料>");
        for (Person person : personList) {
            System.out.println(person);
            if (person.getAge() == 38) {
                person.setAge(18);
            }
        }

    }

    public static void writerList(List<Person> list) throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
        objectOutputStream.writeObject(list);
        objectOutputStream.close();
    }

    public static List<Person> readList() throws IOException, ClassNotFoundException {
        // 讀取普通檔案反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        List<Person> personList = (List<Person>) objectInputStream.readObject();
        objectInputStream.close();
        return personList;
    }
}

6.遠端客戶端的執行緒類

用於自動生成服務介面(繼承了MyRMI標記介面)的遠端客戶端類:這個類原本是通用類實現,為了方便實現,就直接實現Hello介面了

/**
 * 遠端客戶端的執行緒類的生成:
 *      為了方便實現,這邊直接實現服務介面編寫
 */
public class HelloClientThread implements Hello,Serializable {
    // 序列化版本UID
    private static final long serialVersionUID = 1L;
    private Map<String,Object> map = new HashMap<>(); // 報文物件:方法名和引數物件
    private String ip;
    private int port;

    public HelloClientThread(String ip, int port){
        this.ip = ip;
        this.port = port;
    }

    @Override
    public String sayHello(String name) {
        map.put("sayHello",name);
        String result = (String)send();
        return result;
    }

    private Object send(){
        Object o =null;
        Socket socket = null;
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        try {
            socket = new Socket(ip, port);
            out = new ObjectOutputStream(socket.getOutputStream());
            in = new ObjectInputStream(socket.getInputStream());
            // 告訴服務端我要呼叫什麼服務
            out.writeObject(map);
            // 獲取服務實現物件
            o = in.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            try {
                if (out!=null)  out.close();
                if (in!=null)   in.close();
                if (socket!=null) socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return o;
    }
}

7.遠端服務端的執行緒類

用於自動生成服務介面(繼承了MyRMI標記介面)的遠端服務端類:這個類原本也是通用類實現,為了方便實現,部分程式碼尚未做到解耦通用

/**
 * 遠端服務端的執行緒類的生成:
 *      為了方便實現,這邊直接實現服務執行緒類
 */
public class HelloServerThread implements Runnable {
    private Integer port;
    private MyRMI myRMI;
    public HelloServerThread(Integer port, MyRMI myRMI){
        this.port = port;
        this.myRMI = myRMI;
    }

    @Override
    public void run() {
        ServerSocket serverSocket = null;
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        try {
            serverSocket = new ServerSocket(this.port);
            while(true){
                Socket socket = serverSocket.accept();
                in = new ObjectInputStream(socket.getInputStream());
                out = new ObjectOutputStream(socket.getOutputStream());
                // 看看客戶端想要什麼服務
                Map map = (Map)in.readObject();
                Iterator iterator = map.keySet().iterator();
                while (iterator.hasNext()){
                    String key = (String) iterator.next();
                    if("sayHello".equals(key)){
                        // 給客戶端響應服務物件
                        Hello hello = (Hello)myRMI;
                        String result = hello.sayHello((String) map.get(key));
                        out.writeObject(result);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            // 異常後進入
            try {
                if (out!=null)  out.close();
                if (in!=null)   in.close();
                if (serverSocket!=null) serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
    
}

8.遠端客戶端生成和遠端服務端生成和啟動的類

/**
 * 遠端客戶端生成和遠端服務端生成和啟動的類
 */
public class RemoteSocketObject{
    // 預設埠
    private int port=18999;

    // 指定遠端通訊埠和代理服務
    public MyRMI createRemoteClient(MyRMI myRMI,int port){
        if (port > 0)
            this.port=port;

        MyRMI myRMIClient = null;
        try {
            // 生成底層通訊服務端,並啟動
            HelloServerThread helloServerThread = new HelloServerThread(this.port, myRMI);
            Executors.newCachedThreadPool().submit(helloServerThread); // 執行緒池啟動服務
            // 生成底層通訊客戶端
            String localHost = Inet4Address.getLocalHost().getHostAddress();
            System.out.println("host="+localHost+",port="+this.port);
            myRMIClient= new HelloClientThread(localHost, this.port);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return myRMIClient;
    }
    
}

9.服務釋出類

/**
 * RMI 服務釋出類
 */
public class HelloServer {
    public static void main(String[] args) {

        System.out.println("Create Hello Remote Method Invocation...");
        // 例項化一個Hello
        Hello hello = new HelloImpl();
        // 轉換成遠端服務,並提供遠端客戶端
        Hello remoteClient = (Hello)new RemoteSocketObject().createRemoteClient(hello, 0);
        // 將服務實現託管到Socket服務
        MyRMIRegistry myRMIRegistry = new MyRMIRegistry(16000);
        // 開啟執行緒服務
        myRMIRegistry.createRegistry("Hello",remoteClient);

    }
}

10.客戶端測試類

/**
 * 客戶端測試類
 *      客戶端只知道服務介面、服務釋出的地址和服務釋出的名稱
 */
public class TestHello {
    public static void main(String[] args) {
        // 注意不是127.0.0.1,不知道host的看server端啟動後列印的資訊
        // 埠16000是註冊中心的埠,底層代理服務的埠客戶端無需知道
        MyRMIRegistry client = new MyRMIRegistry("192.168.233.1", 16000);
        Hello hello = (Hello) client.getRegistry("Hello");
        System.out.println(hello.sayHello("張三"));
    }
}

11.總結

所有程式碼整下來,在真正的場景中:

客戶端只知道:TestHello類、Hello介面定義、MyRMI標記介面、MyRMIRegistry註冊類程式碼(路由表中只知道Key,不知道具體值);

服務端只知道:Hello介面、HelloImpl服務實現類、MyRMI標記介面、MyRMIRegistry註冊類程式碼(路由表中知道Key和具體值);

關於其他的程式碼實現都是無感的,為了簡單實現遠端客戶端和遠端服務端,將服務介面耦合到兩者上了,未做到解耦通用。

自己寫了個Java RMI(遠端方法呼叫)的實現案例

Java往期文章

Java全棧學習路線、學習資源和麵試題一條龍

我心裡優秀架構師是怎樣的?

免費下載經典程式設計書籍

更多優質文章和資源?

自己寫了個Java RMI(遠端方法呼叫)的實現案例

原創不易、三聯支援:分享,點贊,在看?

相關文章