RMI基本原理與實現

RMI基本原理與應用

1. RMI基本原理

RMI的目的是使運行在不同計算機上的對象之間的調用表現得像本地方法調用一樣

RMI由3個部分構成,第一個是RMIService即JDK提供的一個可以獨立運行的程序(bin目錄下的rmiregistry),第二個是RMIServer即我們自己編寫的一個java項目,這個項目對外提供服務。第三個是RMIClient即我們自己編寫的另外一個java項目,這個項目遠程使用RMIServer提供的服務

RMI應用程序通常包括兩個獨立的程序:服務端程序和客戶端程序

RMI需要將行爲的定義和行爲的實現分別定義,並允許將行爲定義與行爲實現存放並運行在不同的jvm上

在RMI中,遠程服務的定義是存放在繼承了Remote的接口中,遠程服務的實現是存放在實現該定義接口的類中

RMI支持兩個類實現一個相同的遠程服務接口:一個類實現行爲並運行在服務器端,另一個類作爲一個遠程服務代理運行在客戶端

客戶程序發出關於代理對象的調用方法,RMI將該調用請求發送到遠程的JVM上,並且進一步發送到實現的方法中,實現方法將結果返回給代理,在通過代理返回給調用者

RMI構建三個抽象層,高層覆蓋底層,分別負責socket通信,參數和結果的序列化和反序列化等工作

存根(stub)和骨架(skeleton)合在一起形成了RMI框架協議
在這裏插入圖片描述
當客戶端調用遠程對象方法時,存根負責把要調用的遠程對象方法的方法名及其參數編組打包,並將該包向下經遠程引用層、傳輸層轉發給遠程對象所在服務器。通過RMI系統的RMI註冊表實現簡單服務器名稱服務,可定位遠程對象所在的服務器

該包到達服務器後,向上經遠程引用層,被遠程對象的skeleton接收,此skeleton解析客戶包中的方法名及編組的參數後,在服務端執行客戶要調用的遠程對象方法,然後將該方法的返回值打包後通過相反路線返回給客戶端,客戶端stub將返回結果解析後傳遞給客戶程序

事實上, 不僅客戶端程序可以通過存根調用服務器端的遠程對象的方法, 而服務器端的程序亦可通過由客戶端傳遞的遠程接口回調客戶端的遠程對象方法。在分佈式系統中, 所有的計算機可以是服務器, 同時又可以是客戶機。
在這裏插入圖片描述
Remote接口用於標識其方法可以從非本地虛擬機上調用的接口。任何遠程對象都必須直接或間接實現此接口。只有在“遠程接口”(擴展 java.rmi.Remote 的接口)中指定的這些方法纔可遠程使用。 也就是說需要遠程調用的方法必須在擴展Remote接口的接口中聲名並且要拋出RemoteException異常才能被遠程調用。

遠程對象必須實現java.rmi.server.UniCastRemoteObject類,這樣才能保證客戶端訪問獲得遠程對象時,該遠程對象將會把自身的一個拷貝序列化後以Socket的形式傳輸給客戶端,此時客戶端所獲得的這個拷貝稱爲“存根”,而服務器端本身已存在的遠程對象則稱之爲“骨架”。其實此時的存根是客戶端的一個代理,用於與服務器端的通信,而骨架也可認爲是服務器端的一個代理,用於接收客戶端的請求之後調用遠程方法來響應客戶端的請求。 遠程對象的接口和實現必須在客戶端和服務器端同時存在並且保持一致才行。

RMI時序圖
在這裏插入圖片描述

2. 直接使用Registry實現rmi

2.1 發佈 RMI 服務

發佈一個 RMI 服務,我們只需做三件事情:

  1. 定義一個 RMI 接口
  2. 編寫 RMI 接口的實現類
  3. 發佈 RMI 服務

2.1.1 定義一個 RMI 接口

RMI 接口實際上還是一個普通的 Java 接口,只是 RMI 接口必須繼承 java.rmi.Remote,此外,每個 RMI 接口的方法必須聲明拋出一個 java.rmi.RemoteException 異常

package com.huawei.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloRegistryFacade extends Remote {
    String helloWorld(String name) throws RemoteException;
}

2.1.2 編寫 RMI 接口的實現類

我們必須讓實現類繼承 java.rmi.server.UnicastRemoteObject 類,此外,必須提供一個構造器,並且構造器必須拋出 java.rmi.RemoteException 異常。我們既然使用 JVM 提供的這套 RMI 框架,那麼就必須按照這個要求來實現,否則是無法成功發佈 RMI 服務的

package com.huawei.rmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class HelloRegistryFacadeImpl extends UnicastRemoteObject implements  HelloRegistryFacade{
    public HelloRegistryFacadeImpl() throws RemoteException {
        super();
    }
    @Override
    public String helloWorld(String name) {
        return "[Registry] 你好! " + name;
    }
}

2.1.3 發佈 RMI 服務

通過LocateRegistry生成註冊表registry ,然後通過rebind把遠程對象註冊到註冊表上

package com.huawei.rmi;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RegistryService {
    public static void main(String[] args) {
        try {
            // 本地主機上的遠程對象註冊表Registry的實例,默認端口1099
            Registry registry = LocateRegistry.createRegistry(1098);
            // 創建一個遠程對象
            HelloRegistryFacade hello = new HelloRegistryFacadeImpl();
            // 把遠程對象註冊到RMI註冊服務器上,並命名爲HelloRegistry
            registry.rebind("HelloRegistry", hello);
            System.out.println("======= 啓動RMI服務成功! =======");
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

2.2 調用 RMI 服務

首先通過LocateRegistry根據註冊服務監聽端口號獲取到註冊表,然後獲取對應的對象引用,執行所需的方法

package com.huawei.rmi;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RegistryClient {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry(1098);
            HelloRegistryFacade hello = (HelloRegistryFacade) registry.lookup("HelloRegistry");
            String response = hello.helloWorld("ZhenJin");
            System.out.println("=======> " + response + " <=======");
        } catch (NotBoundException | RemoteException e) {
            e.printStackTrace();
        }
    }
}

3. 使用zookeeper實現rmi

3.1 定義一個 RMI 接口

package com.huawei.openvector.zk;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloService extends Remote {
    String sayHello(String name) throws RemoteException;
}

3.2 編寫 RMI 接口的實現類

package com.huawei.openvector.zk;

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

public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
    private static final long serialVersionUID = 4834983262416126666L;

    protected HelloServiceImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String name) throws RemoteException {
        return String.format("Hello %s", name);
    }
}

3.3 服務提供者

需要編寫一個 ServiceProvider 類,來發布 RMI 服務,並將 RMI 地址註冊到 ZooKeeper 中

package com.huawei.openvector.zk;
import java.io.IOException;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;

public class ServiceProvider {

    // 用於等待 SyncConnected 事件觸發後繼續執行當前線程
    private CountDownLatch latch = new CountDownLatch(1);

    // 發佈 RMI 服務並註冊 RMI 地址到 ZooKeeper 中
    public void publish(Remote remote, String host, int port) {
        String url = publishService(remote, host, port); // 發佈 RMI 服務並返回 RMI 地址
        if (url != null) {
            ZooKeeper zk = connectServer(); // 連接 ZooKeeper 服務器並獲取 ZooKeeper 對象
            if (zk != null) {
                createNode(zk, url); // 創建 ZNode 並將 RMI 地址放入 ZNode 上
            }
        }
    }

    // 發佈 RMI 服務
    private String publishService(Remote remote, String host, int port) {
        String url = null;
        try {
            url = String.format("rmi://%s:%d/%s", host, port, remote.getClass().getName());
            LocateRegistry.createRegistry(port);
            Naming.rebind(url, remote);
            System.out.println("publish rmi service url:" + url);
        } catch (RemoteException | MalformedURLException e) {
            e.printStackTrace();
        }
        return url;
    }

    // 連接 ZooKeeper 服務器
    private ZooKeeper connectServer() {
        ZooKeeper zk = null;
        try {
            zk = new ZooKeeper(Constant.ZK_CONNECTION_STRING, Constant.ZK_SESSION_TIMEOUT, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
                        latch.countDown(); // 喚醒當前正在執行的線程
                    }
                }
            });
            latch.await(); // 使當前線程處於等待狀態
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        return zk;
    }

    // 創建 ZNode
    private void createNode(ZooKeeper zk, String url) {
        try {
            byte[] data = url.getBytes();
            String path = zk
                .create(Constant.ZK_PROVIDER_PATH, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 創建一個臨時性且有序的
                                                                                                                        // ZNode
            System.out.println("create zookeeper node path =" + path + " ,url = " + url);
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.4 發佈服務

我們需要調用 ServiceProvider 的 publish() 方法來發布 RMI 服務,發佈成功後也會自動在 ZooKeeper 中註冊 RMI 地址

package com.huawei.openvector.zk;

public class Server {
    public static void main(String[] args) throws Exception {
        ServiceProvider provider = new ServiceProvider();
        HelloService helloService = new HelloServiceImpl();
        provider.publish(helloService, "localhost", 1099);
        Thread.sleep(Long.MAX_VALUE);
    }
}

3.5 服務消費者

服務消費者需要在創建的時候連接 ZooKeeper,同時監聽 /registry 節點的 NodeChildrenChanged 事件,也就是說,一旦該節點的子節點有變化,就需要重新獲取最新的子節點。這裏提到的子節點,就是存放服務提供者發佈的 RMI 地址。需要強調的是,這些子節點都是臨時性的,當服務提供者與 ZooKeeper 服務註冊表的 Session 中斷後,該臨時性節會被自動刪除

package com.huawei.openvector.zk;
import java.io.IOException;
import java.net.MalformedURLException;
import java.rmi.ConnectException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

public class ServiceConsumer {

    // 用於等待 SyncConnected 事件觸發後繼續執行當前線程
    private CountDownLatch latch = new CountDownLatch(1);

    // 定義一個 volatile 成員變量,用於保存最新的 RMI 地址(考慮到該變量或許會被其它線程所修改,一旦修改後,該變量的值會影響到所有線程)
    private volatile List<String> urlList = new ArrayList<>();

    // 構造器
    public ServiceConsumer() {
        ZooKeeper zk = connectServer(); // 連接 ZooKeeper 服務器並獲取 ZooKeeper 對象
        if (zk != null) {
            watchNode(zk); // 觀察 /registry 節點的所有子節點並更新 urlList 成員變量
        }
    }

    // 查找 RMI 服務
    public <T extends Remote> T lookup() {
        T service = null;
        int size = urlList.size();
        if (size > 0) {
            String url;
            if (size == 1) {
                url = urlList.get(0); // 若 urlList 中只有一個元素,則直接獲取該元素
                System.out.println("using only url: " + url);
            } else {
                url = urlList.get(ThreadLocalRandom.current().nextInt(size)); // 若 urlList 中存在多個元素,則隨機獲取一個元素
                System.out.println("using random url:" + url);
            }
            service = lookupService(url); // 從 JNDI 中查找 RMI 服務
        }
        return service;
    }

    // 連接 ZooKeeper 服務器
    private ZooKeeper connectServer() {
        ZooKeeper zk = null;
        try {
            zk = new ZooKeeper(Constant.ZK_CONNECTION_STRING, Constant.ZK_SESSION_TIMEOUT, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        latch.countDown(); // 喚醒當前正在執行的線程
                    }
                }
            });
            latch.await(); // 使當前線程處於等待狀態
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        return zk;
    }

    // 觀察 /registry 節點下所有子節點是否有變化
    private void watchNode(final ZooKeeper zk) {
        try {
            List<String> nodeList = zk.getChildren(Constant.ZK_REGISTRY_PATH, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getType() == Event.EventType.NodeChildrenChanged) {
                        watchNode(zk); // 若子節點有變化,則重新調用該方法(爲了獲取最新子節點中的數據)
                    }
                }
            });
            List<String> dataList = new ArrayList<>(); // 用於存放 /registry 所有子節點中的數據
            for (String node : nodeList) {
                byte[] data = zk.getData(Constant.ZK_REGISTRY_PATH + "/" + node, false, null); // 獲取 /registry 的子節點中的數據
                dataList.add(new String(data));
            }
            System.out.println("node data: {}" + dataList);
            urlList = dataList; // 更新最新的 RMI 地址
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 在 JNDI 中查找 RMI 遠程服務對象
    private <T> T lookupService(String url) {
        T remote = null;
        try {
            remote = (T) Naming.lookup(url);
        } catch (NotBoundException | MalformedURLException | RemoteException e) {
            if (e instanceof ConnectException) {
                // 若連接中斷,則使用 urlList 中第一個 RMI 地址來查找(這是一種簡單的重試方式,確保不會拋出異常)
                if (urlList.size() != 0) {
                    url = urlList.get(0);
                    return lookupService(url);
                }
            }
            e.printStackTrace();
        }
        return remote;
    }
}

3.6 調用服務

通過調用 ServiceConsumer 的 lookup() 方法來查找 RMI 遠程服務對象。我們使用一個“死循環”來模擬每隔 3 秒鐘調用一次遠程方法。

package com.huawei.openvector.zk;

public class Client {
    public static void main(String[] args) throws Exception {
        ServiceConsumer consumer = new ServiceConsumer();

        while (true) {
            HelloService helloService = consumer.lookup();
            String result = helloService.sayHello("Jack");
            System.out.println(result);
            Thread.sleep(3000);
        }
    }
}

3.7 常量定義

樣例中所需的常量通過單獨類進行定義

package com.huawei.openvector.zk;

public interface Constant {
    String ZK_CONNECTION_STRING = "10.31.20.171:2181";
    int ZK_SESSION_TIMEOUT = 5000;
    String ZK_REGISTRY_PATH = "/registry";
    String ZK_PROVIDER_PATH = ZK_REGISTRY_PATH + "/provider";
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章