Skip to content

9. JAVA RMI

9.1. Вступ

Ми розглянули, як створювати мережеві програми за допомогою засобів зв’язку, що називаються sockets. У клієнт-серверній програмі, побудованій на основі цих засобів, зв’язком між клієнтом і сервером є протокол зв’язку, який вони використовують для взаємодії. Обидва додатки можуть бути написані на різних мовах: наприклад, Java для клієнта, Perl для сервера або будь-яка інша комбінація. Дійсно, маємо два окремі додатки, з’єднані протоколом зв’язку, відомим обом. Крім того, доступ до мережі через сокети не є прозорим для Java-додатка: він повинен використовувати клас Socket, створений спеціально для управління цими засобами зв’язку — sockets.

JAVA RMI (Remote Method Invocation) дозволяє створювати мережеві додатки з такими характеристиками:

  1. Клієнт-серверні додатки — це Java-додатки, розташовані на обох кінцях каналу зв’язку
  2. Клієнт може використовувати об’єкти, розташовані на сервері, так, ніби вони знаходяться локально
  3. Мережевий рівень стає прозорим: додаткам не потрібно турбуватися про те, як інформація передається з однієї точки в іншу.

Останній пункт є фактором переносимості: якщо мережевий рівень додатка RMI зміниться, сам додаток не доведеться переписувати. До нового мережевого рівня потрібно буде адаптувати лише класи RMI мови Java.

Принцип комунікації RMI полягає в наступному:

  1. Класичний Java-додаток написано на машині A. Він виконуватиме роль сервера. Для цього деякі його об’єкти будуть «опубліковані» на машині A, на якій виконується додаток, і таким чином стануть службами.
  2. Класичний Java-додаток написано на машині B. Він виконуватиме роль клієнта. Він матиме доступ до об’єктів/сервісів, опублікованих на машині A, тобто через віддалене посилання зможе оперувати ними так, ніби вони є локальними. Для цього йому потрібно знати структуру віддаленого об’єкта, до якого він хоче отримати доступ (методи та властивості).

9.2. Розглянемо приклад

Теорія, що лежить в основі інтерфейсу RMI, не є простою. Щоб краще розібратися в цьому, ми крок за кроком простежимо процес написання клієнт-серверного додатка з використанням пакета RMI для Java. Візьмемо приклад програми, який можна знайти в багатьох посібниках з RMI: клієнт викликає єдиний метод віддаленого об’єкта, який у відповідь повертає йому рядок символів. Тут ми розглянемо невеликий варіант: сервер повторює те, що йому надсилає клієнт. У цій книзі ми вже розглядали подібний приклад програми, що базується на сокетах.

9.2.1. Серверна програма

9.2.1.1. Крок 1: інтерфейс об’єкта/сервера

Віддалений об’єкт — це екземпляр класу, який повинен реалізовувати інтерфейс Remote, визначений у пакеті java.rmi. Методи об’єкта, доступні віддалено, — це ті, що оголошені в інтерфейсі, похідному від інтерфейсу Remote:

import java.rmi.*;

// віддалений інтерфейс
public interface interEcho extends Remote{
    public String echo(String msg) throws java.rmi.RemoteException;
}

Тут оголошується інтерфейс interEcho, який визначає метод echo як доступний віддалено. Цей метод може генерувати виняток класу RemoteException, який об’єднує всі помилки, пов’язані з мережею.

9.2.1.2. Крок 2: написання об’єкта-сервера

На наступному етапі ми визначаємо клас, який реалізує попередній віддалений інтерфейс. Цей клас повинен бути похідним від класу UnicastRemoteObject, який має методи, що дозволяють віддалено викликати методи.

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

// клас, що реалізує віддалене відлуння
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // конструктор
    public srvEcho() throws RemoteException{
        super();
    }// кінець конструктора

    // метод, що реалізує відгук
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// кінець методу «echo»
}// кінець класу

У цьому класі ми бачимо:

  1. метод, що виконує ехо
  2. конструктор, який нічого не робить, окрім виклику конструктора батьківського класу. Він призначений для того, щоб заявити, що може генерувати виняток типу RemoteException.

Ми створимо екземпляр цього класу за допомогою методу main. Щоб об’єкт/сервіс був доступний ззовні, його потрібно створити та зареєструвати в каталозі об’єктів, доступних ззовні. Клієнт, який бажає отримати доступ до віддаленого об’єкта, діє наступним чином:

  1. він звертається до служби каталогу машини, на якій знаходиться потрібний йому об’єкт. Ця служба каталогу працює на порту, який клієнт повинен знати (за замовчуванням — 1099). Клієнт запитує у каталозі посилання на об’єкт/службу, вказавши її ім’я. Якщо це ім’я відповідає об’єкту/службі в каталозі, каталог повертає клієнту посилання, за допомогою якого клієнт зможе взаємодіяти з віддаленим об’єктом/службою.
  2. Відтепер клієнт може використовувати цей віддалений об’єкт так, ніби він є локальним

Повертаючись до нашого сервера, ми повинні створити об’єкт типу srvEcho і зареєструвати його в каталозі об’єктів, доступних ззовні. Ця реєстрація здійснюється за допомогою методу класу rebind класу Naming:

Naming.rebind(String nom, Remote obj)

з

ім’ям: ім’я, яке буде пов’язане з віддаленим об’єктом

obj: віддалений об’єкт

Отже, наш клас srvEcho виглядає наступним чином:

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

// клас, що реалізує віддалене відлуння
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // конструктор
    public srvEcho() throws RemoteException{
        super();
    }// кінець конструктора

    // метод, що реалізує відлуння
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// кінець функції «echo»

    // створення сервісу
    public static void main (String arg[]){
        try{
            srvEcho serveurEcho=new srvEcho();
            Naming.rebind("srvEcho",serveurEcho);
            System.out.println("Serveur d’écho prêt");
        } catch (Exception e){
            System.err.println(" Erreur "  + e + "  lors du lancement du serveur d’écho ");
        }
    }// main
}// кінець класу

Коли читаєш попередню програму, складається враження, що вона зупиниться одразу після створення та збереження служби відлуння. Це не так. Оскільки клас srvEcho є похідним від класу UnicastRemoteObject, створений об’єкт виконується нескінченно: він очікує запитів клієнтів на анонімному порту, тобто такому, який система обирає залежно від обставин. Створення служби відбувається асинхронно: у цьому прикладі метод main створює службу та продовжує своє виконання: він дійсно виведе повідомлення «Сервер ехо готовий».

9.2.1.3. Крок 3: компіляція серверного додатка

На цьому етапі ми можемо скомпілювати наш сервер. Ми компілюємо файл interEcho.java з інтерфейсу interEcho, а також файл srvEcho.java з класу srvEcho. У результаті отримуємо відповідні файли .class: interEcho.class та srvEcho.class.

9.2.1.4. Крок 4: написання клієнта

Ми пишемо клієнт, якому передаємо як параметр файл URL з сервера-відлуння і який

  1. зчитує рядок, введений з клавіатури
  2. надсилає її на сервер відлуння
  3. виводить відповідь, яку той надсилає
  4. повертається до кроку 1 і зупиняється, коли введено рядок «fin».

У результаті отримуємо такий клієнт:

import java.rmi.*;
import java.io.*;

public class cltEcho {

    public static void main(String arg[]){
        // синтаксис: cltEcho URLService

        // перевірка аргументів
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // діалог «клієнт-сервер»
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // відкриття потоку клавіатури
            in=new BufferedReader(new InputStreamReader(System.in));
            // локалізація служби
            serveur=(interEcho) Naming.lookup(urlService);                
            // цикл зчитування повідомлень для відправки на сервер відлуння
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // відправлення повідомлення на сервер та отримання відповіді
                reponse=serveur.echo(msg);
                // відстеження
                System.out.println("Réponse serveur : " + reponse);
                // наступне повідомлення
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // завершено
            System.exit(0);
        // обробка помилок        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// main
}// клас                

У цьому клієнті немає нічого особливого, крім команди, яка запитує посилання на сервер:

            serveur=(interEcho) Naming.lookup(urlService);

Нагадаємо, що наш сервіс-відлуння було зареєстровано в каталозі сервісів машини, на якій він розташований, за допомогою команди:


            Naming.rebind("srvEcho",serveurEcho);

Отже, клієнт також використовує метод класу Naming, щоб отримати посилання на сервер, яким він хоче скористатися. Використаний метод lookup приймає як параметр URL-адресу запитуваної служби. Вона має вигляд звичайної URL-адреси:

    rmi://машина:порт/nom_service

з

rmi: необов’язковий — протокол RMI

machine: ім’я або адреса IP машини, на якій працює сервер-відлуння — необов’язковий параметр, за замовчуванням localhost.

port: порт, на якому слухає служба каталогу цієї машини — необов’язковий, за замовчуванням 1099

nom_service: ім’я, під яким було зареєстровано запитувану службу (у нашому прикладі — srvEcho)

Отримується екземпляр віддаленого інтерфейсу interEcho. Якщо припустити, що клієнт і сервер знаходяться не на одній машині, то під час компіляції клієнта cltEcho.java у тому ж каталозі повинен бути файл interEcho.class, отриманий у результаті компіляції віддаленого інтерфейсу interEcho; інакше у рядках, що посилаються на цей інтерфейс, виникне помилка компіляції.

9.2.1.5. Крок 5: створення файлів .class, необхідних для клієнт-серверної програми

Щоб краще зрозуміти, що знаходиться на стороні сервера, а що — на стороні клієнта, розмістимо сервер у каталозі echo\serveur, а клієнт — у каталозі echo\client.

Каталог сервера містить такі вихідні файли:

E:\data\java\RMI\echo\serveur>dir *.java

INTERE~1 JAV           158  09/03/99  15:06 interEcho.java
SRVECH~1 JAV           759  09/03/99  15:07 srvEcho.java

Після компіляції цих двох вихідних файлів отримуємо такі файли .class:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class

У каталозі клієнта знаходиться такий вихідний файл:

E:\data\java\RMI\echo\client>dir *.java

CLTECH~1 JAV         1 427  09/03/99  16:08 cltEcho.java

а також файл interEcho.class, який було згенеровано під час компіляції сервера:

E:\data\java\RMI\echo\client>dir *.class

INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

Після компіляції вихідного файлу отримано такі файли .class:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 506  09/03/99  16:08 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

Якщо спробувати запустити клієнт cltEcho, з’явиться така помилка:

E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Erreur : java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
        java.lang.ClassNotFoundException: srvEcho_Stub

Якщо спробувати запустити сервер srvEcho, з’являється така помилка:

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\java srvEcho
Erreur java.rmi.StubNotFoundException: Stub class not found: srvEcho_Stub; nested exception is:
        java.lang.ClassNotFoundException: srvEcho_Stub  lors du lancement du serveur d’écho

В обох випадках віртуальна машина Java повідомляє, що не знайшла клас srvEcho_stub. Дійсно, ми ще ніколи не чули про цей клас. У клієнті локалізація сервера відбувалася за допомогою такої інструкції:

            serveur=(interEcho) Naming.lookup(urlService);                

Тут urlservice — це рядок rmi://localhost/srvEcho із

Rmi — протокол RMI

Localhost — машина, на якій працює сервер — у даному випадку та сама машина, на якій працює клієнт. Зазвичай синтаксис має вигляд «машина:порт». Якщо порт не вказано, за замовчуванням використовується порт 1099. На цьому порту працює служба каталогу сервера.

srvEcho — це ім’я конкретної служби, до якої звертається запит

Під час компіляції помилок не було виявлено. Потрібно було лише, щоб файл interEcho.class віддаленого інтерфейсу був доступний.

Під час виконання віртуальна машина вимагає наявності файлу srvEcho_stub.class, якщо запитується служба srvEcho, а для служби X — як правило, файлу X_stub.class. Цей файл необхідний лише під час виконання, а не під час компіляції клієнта. Те саме стосується і сервера. Що ж це за файл?

На сервері знаходиться клас srvEcho.class, який є нашим віддаленим об’єктом/сервісом. Клієнт, навіть якщо йому не потрібен цей клас, все одно потребує свого роду його «образу», щоб мати змогу з ним спілкуватися. Насправді клієнт не надсилає свої запити безпосередньо до віддаленого об’єкта: він надсилає їх до свого локального образу srvEcho_stub.class, розташованого на тій самій машині, що й він сам. Ця локальна копія srvEcho_stub.class взаємодіє з аналогічною копією (srvEcho_stub.class), яка цього разу розміщена на сервері. Цей образ створюється на основі файлу .class із сервера за допомогою Java-інструменту під назвою rmic. У Windows використовується команда:

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\rmic srvEcho

створить на основі файлу srvEcho.class ще два файли .class:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~2 CLA         3 264  09/03/99  16:57 srvEcho_Stub.class
SRVECH~3 CLA         1 736  09/03/99  16:57 srvEcho_Skel.class

Серед них є файл srvEcho_stub.class, який необхідний клієнту та серверу для виконання. Також є файл srvEcho_Skel.class, роль якого наразі невідома. Створюємо копію файлу srvEcho_stub.class у каталозі клієнта та сервера і видаляємо файл srvEcho_Skel.class. Отже, маємо такі файли:

на стороні сервера:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class
SRVECH~1 CLA         3 264  09/03/99  16:01 srvEcho_Stub.class

на стороні клієнта:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 506  09/03/99  16:08 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class
SRVECH~1 CLA         3 264  09/03/99  16:01 srvEcho_Stub.class

9.2.1.6. Крок 6: Запуск клієнт-серверного додатка «echo»

Ми готові до запуску нашого клієнт-серверного додатка. Спочатку клієнт і сервер працюватимуть на одній машині. Спочатку потрібно запустити наш серверний додаток. Нагадаємо, що він:

  • створює службу
  • реєструє його в каталозі служб комп’ютера, на якому працює сервер «ехо»

Останній пункт вимагає наявності служби каталогу. Її запускають за допомогою команди:

start j:\jdk12\bin\rmiregistry

rmiregistry — це служба каталогу. Тут вона запускається у фоновому режимі у вікні командного рядка Windows за допомогою команди start. Коли каталог активний, можна створити службу «echo» та зареєструвати її в каталозі служб. І тут вона запускається у фоновому режимі за допомогою команди start:

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

Сервер «echo» працює в новому вікні DOS і відображає, як було задано:

Serveur d’écho prêt

Залишилося лише запустити та протестувати наш клієнт:

E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

9.2.1.7. Клієнт і сервер на двох різних машинах

У попередньому прикладі клієнт і сервер знаходилися на одній машині. Тепер розмістимо їх на різних машинах:

  • сервер — на комп’ютері з ОС Windows
  • клієнт — на комп’ютері з ОС Linux

Сервер запускається, як і раніше, на комп’ютері з Windows. На комп’ютер з Linux було перенесено файли .class клієнта:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 9
drwxr-xr-x   2 serge    admin        1024 Mar 10 10:02 .
drwxr-xr-x   4 serge    admin        1024 Mar 10 10:01 ..
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

Клієнт запущено:

$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Erreur : java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
      java.rmi.UnmarshalException: error unmarshalling call header; nested exception is: 
      java.rmi.UnmarshalException: skeleton class not found but required for client version

Отже, маємо помилку: віртуальна машина Java, судячи з усього, вимагає файл srvEcho_skel.class, який був створений утилітою rmic, але до цього моменту не використовувався. Ми створюємо його заново та також переносимо на машину під управлінням Linux:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 11
drwxr-xr-x   2 serge    admin        1024 Mar 10 10:17 .
drwxr-xr-x   4 serge    admin        1024 Mar 10 10:01 ..
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        1736 Mar 10 10:17 srvEcho_Skel.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

З’являється та сама помилка, що й раніше... Тож ми замислюємося і перечитуємо документацію щодо RMI. Зрештою доходимо висновку, що, можливо, саме серверу потрібен той самий файл srvEcho_Skel.class. Тож на комп’ютері з Windows ми перезапускаємо сервер із двома файлами srvEcho_Stub.class та srvEcho_Skel.class, а також :

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class
SRVECH~2 CLA         3 264  10/03/99   9:05 srvEcho_Stub.class
SRVECH~3 CLA         1 736  10/03/99   9:05 srvEcho_Skel.class

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

потім на комп’ютері з Linux ми знову тестуємо клієнт, і цього разу все працює:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

Отже, з цього випливає, що на стороні сервера повинні бути присутні обидва файли: srvEcho_Stub.class та srvEcho_Skel.class. На стороні клієнта до цього часу був необхідний лише файл srvEcho_Stub.class. Він виявився необхідним, коли клієнт і сервер знаходилися на одній машині під управлінням Windows. У Linux його видаляємо, щоб перевірити...

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
        at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:28)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is: 
        java.rmi.RMISecurityException: security.No security manager, stub class loader disabled

Ми маємо цікаву помилку, яка, схоже, вказує на те, що віртуальна машина Java намагалася завантажити відомий клас stub, але не змогла цього зробити через відсутність «менеджера безпеки» (security manager). Здається, ми бачили щось на цю тему в документації. Повертаємося до документації... і виявляємо, що сервер повинен створити та встановити менеджер безпеки (security manager), який гарантує клієнтам, що запитують завантаження класів, їхню безпеку. За відсутності цього менеджера безпеки завантаження класів неможливе. Це здається логічним: наш клієнт під Linux звернувся до сервера з запитом на клас srvEcho_stub.class, який йому потрібен, а сервер відмовив, повідомивши, що менеджер безпеки не встановлено. Тому ми змінюємо код функції main на сервері наступним чином:

    // створення служби
    public static void main (String arg[]){

        // встановлення менеджера безпеки
        System.setSecurityManager(new RMISecurityManager());

        // запуск та реєстрація служби
        try{
            srvEcho serveurEcho=new srvEcho();
            Naming.rebind("srvEcho",serveurEcho);
            System.out.println("Serveur d’écho prêt");
        } catch (Exception e){
            System.err.println(" Erreur "  + e + "  lors du lancement du serveur d’écho ");
        }
    }// головна

Компілюємо та створюємо файли srvEcho_stub.class та srvEcho_Skel.class за допомогою інструменту rmic. Запускаємо службу каталогу (rmiregistry), а потім сервер — і отримуємо помилку, якої раніше не було!

Erreur java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve)  lors du lancement du serveur d’écho

Схоже, менеджер безпеки виявився надто ефективним. Знову переглядаємо документацію... Виявляється, що коли менеджер безпеки активний, під час запуску програми потрібно вказати її права. Це робиться за допомогою такого параметра:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

де

java.security.policy — це ключове слово

mypolicy — це текстовий файл, що визначає права програми. У даному випадку це такий файл:

grant {
    // Наразі дозволити все
    permission java.security.AllPermission;
};

У цьому випадку програма має всі права.

Почнемо спочатку. Перейдіть до каталогу сервера та виконайте послідовно:

  • запуск служби каталогу: start j:\jdk12\bin\rmiregistry
  • запуск сервера: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

І цього разу сервер echo (а клієнт — ще ні) запускається правильно. Тепер ви можете провести такий експеримент:

  • зупиніть сервер echo, а потім службу каталогу
  • перезапустіть службу каталогу, перебуваючи в іншому каталозі, ніж той, де розташований сервер
  • поверніться до каталогу сервера запустіть сервер ехо — ви отримаєте таку помилку:
Erreur java.rmi.ServerException: RemoteException occurred in server thread; nes
ted exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub  lors du lancement du serveur d’écho

З цього випливає, що каталог, з якого запускається служба каталогу, має значення. У даному випадку Java не знайшла клас srvEcho_stub.class, оскільки служба каталогу не була запущена з каталогу сервера. Під час запуску сервера можна вказати, в якому каталозі знаходяться класи, необхідні для роботи сервера:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

Команда складається з одного рядка. Ключове слово java.rmi.server.codebase використовується для вказівки URL каталогу, що містить класи, необхідні для роботи сервера. У цьому випадку цей URL визначає протокол file, який є протоколом доступу до локальних файлів, та каталог, що містить файли .class сервера. Отже, якщо виконати наступні дії:

  • зупинити службу каталогу
  • перезапуск служби каталогів з іншого каталогу, відмінного від каталогу сервера
  • у каталозі сервера запустити його за допомогою команди (один рядок):
 start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

Сервер запущено успішно. Тепер можна перейти до клієнта. Перевіряємо його:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 14:28 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
        at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:31)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is: 
        java.rmi.RMISecurityException: security.No security manager, stub class loader disabled

З’являється та сама помилка, що вказує на відсутність менеджера безпеки. Ми припускаємо, що, можливо, помилилися і що саме клієнт повинен створити свій менеджер безпеки. Ми залишаємо сервер із його менеджером безпеки, але створюємо менеджера безпеки також і для клієнта. Функція main клієнта cltEcho.java тоді виглядає так:

public static void main(String arg[]){
        // синтаксис: cltEcho порт машини
        // машина: машина, на якій працює сервер відлуння
        // порт: порт, на якому працює каталог служб на машині служби ехо

        // перевірка аргументів
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // встановлення менеджера безпеки
        System.setSecurityManager(new RMISecurityManager());

        // взаємодія «клієнт-сервер»
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            ....
        } catch (Exception e){
            ....
        }// try
    }// main

Далі виконуємо такі дії:

  • перекомпілюємо cltEcho.java
  • переносимо файли .class на комп’ютер під управлінням Linux
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 14:28 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
  • запускаємо клієнт
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

java.io.FileNotFoundException: /e:/data/java/rmi/echo/serveur/srvEcho_Stub.class
        at java.io.FileInputStream.<init>(FileInputStream.java)
        at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:150)
        at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:170)
        at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:119)
        at sun.applet.AppletClassLoader.findClass(AppletClassLoader.java:496)
        at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:199)
        at sun.rmi.server.RMIClassLoader.loadClass(RMIClassLoader.java:159)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:97)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:31)
File not found when looking for: srvEcho_Stub
Erreur : java.rmi.UnmarshalException: Return value class not found; nested exception is: 
        java.lang.ClassNotFoundException: srvEcho_Stub

Незважаючи на зовнішні ознаки, ми просуваємося вперед: помилка вже інша. Ми бачимо, що клієнт зміг запросити у сервера клас srvEcho_Stub.class, але сервер його не знайшов. Отже, клієнт повинен мати менеджер безпеки, якщо хоче мати можливість запитувати класи у сервера.

Якщо розглянути попередню помилку, то бачимо, що файл srvEcho_Stub.class шукали в каталозі e:/data/java/rmi/echo/serveur/, але його не знайшли. Проте він саме там і знаходиться. Якщо уважніше придивитися до списку методів, пов’язаних із цією помилкою, можна знайти такий: sun.net.www.protocol.file.FileURLConnection.getInputStream. Схоже, клієнт відкрив потік з об’єктом типу FileURLConnection. Можна припустити, що все це пов’язано зі способом запуску нашого сервера:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

Повідомлення про помилку, схоже, посилається на значення ключового слова java.rmi.server.codebase. Якщо ще раз переглянути документацію, то можна побачити, що значення цього ключового слова у наведених прикладах завжди таке: http://.., c.a.d, а використовуваний протокол — http. Незрозуміло, як саме клієнт запитує та отримує свої класи від сервера. Можливо, він запитує їх за допомогою ключового слова URL, яке було вказано під час запуску сервера разом із ключовими словами java.rmi.server.codebase та URL. Тому ми вирішили запустити сервер за допомогою такої нової команди:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

Тепер протоколом є http. Необхідно перемістити файли .class у місце, доступне для http-сервера на комп’ютері, на якому будуть зберігатися класи. У нашому прикладі сервер працює на комп’ютері з ОС Windows із сервером HTTP PWS від Microsoft. Кореневий каталог цього сервера — d:\Inetpub\wwwroot. Тому діємо наступним чином:

  • створюємо каталог d:\Inetpub\wwwroot\rmi\echo
  • поміщаємо туди файли .class із сервера, а також файл mypolicy
  • запускаємо веб-сервер, якщо це ще не зроблено
  • перезапускаємо службу каталогу (rmiregistry)
  • перезапускаємо сервер за допомогою команди
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
  • на комп’ютері з ОС Linux запускаємо клієнт:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1622 Mar 10 14:37 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

Уф! Все працює. Клієнту вдалося завантажити той самий файл srvEcho_Stub.class.

Все це надихнуло нас на певні ідеї, і ми замислилися, чи клієнт, який працює на сервері під Windows, також функціонуватиме без файлу srvEcho_Stub.class. Переходимо до каталогу клієнта, видаляємо файл srvEcho_Stub.class, якщо він там є, і запускаємо клієнт так само, як і в Linux:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 622  10/03/99  14:12 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

Message : nouveau message
Réponse serveur : [nouveau message]
Message : fin

9.2.1.8. Résumé

Сторона сервера Windows:

  • сервер має менеджер безпеки
  • він був запущений із такими параметрами: start j:\jdk12\bin\java -Djava.security.policy=mypolicy

-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

На стороні клієнта (Linux або Windows)

  • клієнт має менеджер безпеки
  • на Linux він був запущений командою java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
  • у Windows його запущено за допомогою j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

9.2.1.9. Сервер-відлуння на Linux, клієнти на Windows та Linux

Тепер переносимо сервер на комп’ютер під управлінням Linux і тестуємо клієнти під Linux та Windows. Послідовність дій така:

  • переносимо файли .class із сервера на машину під управлінням Linux
shiva[serge]:/home/admin/serge/WWW/rmi/echo/serveur#
$ dir
total 11
drwxr-xr-x   2 serge    admin        1024 Mar 10 16:15 .
drwxr-xr-x   3 serge    admin        1024 Mar 10 16:09 ..
-rw-r--r--   1 serge    admin         256 Mar 10 16:09 interEcho.class
-rw-r--r--   1 serge    admin        1245 Mar 10 16:09 srvEcho.class
-rw-r--r--   1 serge    admin        1736 Mar 10 16:09 srvEcho_Skel.class
-rw-r--r--   1 serge    admin        3264 Mar 10 16:09 srvEcho_Stub.class
  • оскільки клас srvEcho_Stub.class буде запитуватися клієнтами, для класів сервера обрано каталог, доступний для HTTP-сервера на машині під управлінням Linux. Тут URL у цьому каталозі — це http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
  • Служба реєстру запускається у фоновому режимі: /usr/local/bin/jdk/rmiregistry &
  • сервер запускається у фоновому режимі: /usr/local/bin/jdk/bin/java

-Djava.rmi.server.codebase=http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur/

srvEcho &

Можна протестувати клієнти. Спочатку клієнт для Windows.

  • Переходимо до каталогу клієнта на комп'ютері з Windows
  • запускаємо клієнт за допомогою команди:
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://shiva.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : fin

Перевіряємо роботу клієнта під Linux:

shiva[serge]:/home/admin/serge/java/rmi/echo/client#
$ java cltEcho srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

Слід зазначити, що для клієнта Linux, який працює на тій самій машині, що й сервер echo, не було потреби вказувати машину в URL запитуваної служби.

9.3. Другий приклад: сервер SQL на комп’ютері з ОС Windows

9.3.1. Проблема

У розділі JDBC ми розглянули, як працювати з реляційними базами даних. У наведених прикладах програми та використовувана база даних знаходилися на одному комп’ютері з ОС Windows. Тут ми пропонуємо створити сервер RMI на комп’ютері з ОС Windows, який дозволить віддаленим клієнтам користуватися публічними базами даних ODBC того комп’ютера, на якому розташований сервер.

Image

Клієнт RMI може виконувати 3 операції:

  • підключитися до обраної бази даних
  • надсилати запити SQL
  • закрити з'єднання

Сервер виконує запити SQL від клієнта та надсилає йому результати. Це його основна функція, тому ми називатимемо його сервером SQL.

Ми застосовуємо різні етапи, розглянуті раніше на прикладі сервера-відлуння.

9.3.2. Етап 1: віддалений інтерфейс

Віддалений інтерфейс — це інтерфейс, який містить перелік методів сервера RMI, доступних для клієнтів RMI. Ми будемо використовувати такий інтерфейс:

import java.rmi.*;

// віддалений інтерфейс
public interface interSQL extends Remote{
    public String connect(String pilote, String url, String id, String mdp)
        throws java.rmi.RemoteException;
    public String[] executeSQL(String requete, String separateur) 
        throws java.rmi.RemoteException;
    public String close()
        throws java.rmi.RemoteException;
}

Роль різних методів така:

Connect: клієнт підключається до віддаленої бази даних, вказуючи її pilote, url, JDBC, а також свій ідентифікатор id та пароль mdp для доступу до цієї бази. Сервер повертає йому рядок символів, що вказує на результат підключення:

    200 - Connexion réussie
    500 - Echec de la connexion

executeSQL: клієнт запитує виконання запиту SQL у базі даних, до якої він підключений. Він вказує символ, який має розділяти поля у результатах, що йому повертаються. Сервер повертає масив рядків:

    100 n

для запиту на оновлення бази даних, де n — кількість оновлених рядків

    500 msg d’erreur

якщо запит призвів до помилки

    501 Pas de résultats

якщо запит не дав жодних результатів

    101 ligne1
    101 ligne2
    101 ...

якщо запит дав результати. Рядки, які сервер повертає таким чином, є результатами запиту.

close: клієнт закриває з’єднання з віддаленою базою даних. Сервер повертає рядок, що вказує на результат цього закриття:

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

9.3.3. Крок 2: Код сервера

Далі наведено вихідний код Java для сервера SQL. Для його розуміння необхідно засвоїти принципи управління базами даних JDBC, а також побудову серверів RMI. Коментарі до програми повинні полегшити її розуміння.

// імпортовані пакети
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

// клас srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{

    // глобальні дані класу
    private Connection DB;

    // ------------- конструктор
    public srvSQL() throws RemoteException{
        super();
    }

    // --------------- підключення
    public String connect(String pilote, String url, String id,
        String mdp) throws RemoteException{

        // підключення до бази даних за URL-адресою за допомогою драйвера
        // аутентифікація за допомогою ідентифікатора id та пароля mdp

        String resultat=null;            // результат виконання методу
        try{
            // завантаження драйвера
            Class.forName(pilote);
            // запит на підключення
            DB=DriverManager.getConnection(url,id,mdp);
            // ok
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // помилка
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // кінець
        return resultat;
    }            

    // ------------- executeSQL
    public String[] executeSQL(String requete, String separateur)
        throws RemoteException{

        // виконує запит SQL до бази даних DB
        // та заносить результати в масив рядків

        // дані, необхідні для виконання запиту
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // створення контейнера запиту
            S=DB.createStatement();
            // виконання запиту
            if (! S.execute(requete)){
                // запит на оновлення
                // повертається кількість оновлених рядків
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // це був запит на пошук
            // отримано результати
            RS=S.getResultSet();
            // кількість полів у наборі результатів
            int nbChamps=RS.getMetaData().getColumnCount();
            // їх обробка
            while(RS.next()){
                // створення рядка результатів
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // додавання до масиву результатів
                resultats.addElement(ligne);
            }// while
            // завершення обробки результатів
            // звільняємо ресурси
            RS.close();
            S.close();
            // повернення результатів
            int nbLignes=resultats.size();
            if (nbLignes==0){
                lignes=new String[1];
                lignes[0]="501 Pas de résultats";
            } else {
                lignes=new String[resultats.size()];
                for(int i=0;i<lignes.length;i++)
                    lignes[i]=(String) resultats.elementAt(i);
            }//if
            return lignes;
        } catch (Exception e){
            // помилка
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- закрити
    public String close() throws RemoteException {
        // закриває з'єднання з базою даних
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // повернення результату
        return resultat;
    }

    // ----------- main
    public static void main (String[] args){

        // менеджер безпеки
        System.setSecurityManager(new RMISecurityManager());

        // запуск служби
        srvSQL serveurSQL=null;
        try{
            // створення
            serveurSQL=new srvSQL();
            // реєстрація
            Naming.rebind("srvSQL",serveurSQL);
            // відстеження
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // помилка
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// main

}// клас

9.3.4. Запуск клієнта RMI

Клієнт сервера RMI викликається з такими параметрами:

    urlserviceAnnuaire pilote urlBase id mdp separateur

urlserviceAnnuaire: URL-адреса RMI служби каталогів, яка зареєструвала сервер SQL

драйвер: драйвер, який повинен використовувати сервер SQL для управління базою даних

urlBase: URL-адреса JDBC бази даних, яку потрібно керувати

id: ідентифікатор клієнта або null, якщо ідентифікатор відсутній

mdp: пароль клієнта або null, якщо пароль відсутній

separateur: символ, який сервер SQL повинен використовувати для розділення полів у рядках результатів запиту

Ось приклад можливих параметрів:

    srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

де:

urlserviceAnnuaire    srvSQL

RMI-ім'я сервера SQL

pilote     sun.jdbc.odbc.JdbcOdbcDriver

стандартний драйвер баз даних з інтерфейсом ODBC

urlBase    jdbc:odbc:articles

для використання бази даних товарів, заявленої у списку загальнодоступних баз даних ODBC на комп’ютері з ОС Windows

id    null

немає ідентифікатора

mdp    null

без пароля

separateur    , 

поля результатів будуть розділені комою

Після запуску з наведеними вище параметрами клієнт виконує такі кроки:

  • він підключається до сервера RMI srvSQL, тобто до сервера RMI, розташованого на тій самій машині, що й клієнт
  • він запитує підключення до бази даних товарів
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • він просить користувача ввести запит SQL з клавіатури
  • він надсилає його на сервер SQL
executeSQL(requete, ’’,’’);
  • він відображає на екрані результати, отримані від сервера
  • він знову просить користувача ввести запит SQL з клавіатури. Програма зупиниться, коли запит буде завершено.

Далі наведено Java-код клієнта. Коментарів має вистачити для його розуміння.

import java.rmi.*;
import java.io.*;

public class cltSQL {

    // глобальні дані класу
    private static String syntaxe =
        "syntaxe : cltSQL urlServiceAnnuaire pilote urlBase id mdp separateur";
    private static BufferedReader in=null;
    private static interSQL serveurSQL=null;

    public static void main(String arg[]){
        // синтаксис: cltSQL urlServiceAnnuaire роздільник драйвер url id пароль
        // urlServiceAnnuaire: URL-адреса каталогу служб RMI, до яких слід звернутися
        // драйвер: драйвер, який слід використовувати для роботи з базою даних
        // urlBase : URL-адреса бази даних (JDBC) для обробки
        // id: ідентифікатор користувача
        // mdp: його пароль
        // separateur: рядок, що розділяє поля у результатах запиту

        // перевірка кількості аргументів
        if(arg.length!=6)
            erreur(syntaxe,1);

        // ініціалізація параметрів підключення до бази даних
        String urlService=arg[0];        
        String pilote=arg[1];
        String urlBase=arg[2];
        String id, mdp, separateur;
        if(arg[3].equals("null")) id=""; else id=arg[3];
        if(arg[4].equals("null")) mdp=""; else mdp=arg[4];        
        if(arg[5].equals("null")) separateur=" "; else separateur=arg[5];        

        // встановлення менеджера безпеки
        System.setSecurityManager(new RMISecurityManager());

        // діалог «клієнт-сервер»
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // відкриття потоку клавіатури
            in=new BufferedReader(new InputStreamReader(System.in));
            // моніторинг
            System.out.println("--> Connexion au serveur RMI en cours...");
            // локалізація служби
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // відстеження
            System.out.println("--> Connexion à la base de données en cours");
            // запит на початкове підключення до бази даних
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // відстеження
            System.out.println("<-- "+reponse);
            // аналіз відповіді
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // цикл зчитування запитів, що надсилаються на сервер SQL
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // відправлення запиту на сервер та отримання відповіді
                lignes=serveurSQL.executeSQL(requete,separateur);
                // відстеження
                afficheLignes(lignes);
                // наступний запит
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// while
            // відстеження
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // завершуємо з'єднання
            reponse=serveurSQL.close();
            // продовження
            System.out.println("<-- " + reponse);
            // кінець
            System.exit(0);
        // обробка помилок        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// спроба
    }// main

    // ----------- AfficheLignes
    private static void afficheLignes(String[] lignes){
        for (int i=0;i<lignes.length;i++)
            System.out.println("<-- " + lignes[i]);
    }// afficheLignes

    // ------------ помилка
    private static void erreur(String msg, int exitCode){
        // відображення повідомлення про помилку
        System.err.println(msg);
        // можливе звільнення ресурсів
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // вихід
        System.exit(exitCode);
    }// помилка

}// клас    

9.3.5. Крок 3: створення файлів .class

  • сервер скомпільовано
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac interSQL.java

E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac srvSQL.java

E:\data\java\RMI\sql\serveur>dir *.class

INTERS~1 CLA           451  12/03/99  17:54 interSQL.class
SRVSQL~1 CLA         3 238  12/03/99  17:54 srvSQL.class
  • створено файли Stub та Skel
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\rmic srvSQL

E:\data\java\RMI\sql\serveur>dir *.class


INTERS~1 CLA           451  12/03/99  17:54 interSQL.class
SRVSQL~1 CLA         3 238  12/03/99  17:54 srvSQL.class
SRVSQL~2 CLA         4 491  12/03/99  17:56 srvSQL_Stub.class
SRVSQL~3 CLA         2 414  12/03/99  17:56 srvSQL_Skel.class
  • файли interSQL.class, srvSQL_Stub.class, srvSQL_Skel.class переносяться до каталогу клієнта
E:\data\java\RMI\sql\client>dir

CLTSQL~1 JAV         3 486  11/03/99  11:39 cltSQL.java
INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
SRVSQL~1 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~2 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class
  • компілюємо клієнт
E:\data\java\RMI\sql\client>j:\jdk12\bin\javac cltSQL.java

E:\data\java\RMI\sql\client>dir *.class

INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
CLTSQL~1 CLA         2 839  12/03/99  18:00 cltSQL.class
SRVSQL~1 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~2 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class

9.3.6. Крок 4: Тестування з сервером і клієнтом на одній машині під Windows

  • Служба каталогу запускається в каталозі, відмінному від каталогу сервера та клієнта
F:\>start j:\jdk12\bin\rmiregistry
  • файл mypolicy розміщується у каталогах клієнта та сервера
grant {
    // Наразі дозволити все
    permission java.security.AllPermission;
};
  • запускаємо сервер
E:\data\java\RMI\sql\serveur>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/sql/serveur/ srvSQL
  • запускаємо клієнт
E:\data\java\RMI\sql\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltSQL srvSQL 
sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Connexion au serveur RMI en cours...
--> Connexion à la base de données en cours
<-- 200 Connexion réussie
--> Запит: select name, stock_actu from articles order by stock_actu desc
<-- 101 vélo,31
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 panthère,11
<-- 101 léopard,11
<-- 101 cachalot,10
<-- 101 fusil,10
<-- 101 arc,10
--> Запит: update articles set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Запит: select nom, stock_actu from articles order by stock_actu asc
<-- 101 cachalot,9
<-- 101 fusil,9
<-- 101 arc,9
<-- 101 panthère,10
<-- 101 léopard,10
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 vélo,31
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée

9.3.7. Крок 5: Тестування з сервером на комп’ютері з ОС Windows та клієнтом на комп’ютері з ОС Linux

  • за необхідності зупиняємо сервер та службу каталогу
  • переносимо файли .class клієнта на комп’ютер під управлінням Linux
shiva[serge]:/home/admin/serge/java/rmi/sql/client#
$ dir *.class
-rw-r--r--   1 serge    admin        2839 Mar 11 14:37 cltSQL.class
-rw-r--r--   1 serge    admin         451 Mar 11 14:37 interSQL.class
  • файли з сервера розміщуються в каталозі, доступному для HTTP-сервера на комп'ютері з ОС Windows
D:\Inetpub\wwwroot\rmi\sql>dir

INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
SRVSQL~1 CLA         3 238  11/03/99  13:19 srvSQL.class
SRVSQL~2 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~3 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class
MYPOLICY                81  08/06/98  15:01 mypolicy
  • відновлюємо роботу довідкової служби
F:\>start j:\jdk12\bin\rmiregistry
  • перезапускаємо сервер з параметрами, відмінними від тих, що використовувалися в попередньому тесті
D:\Inetpub\wwwroot\rmi\sql>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/sql/ srvSQL
  • запускаємо клієнт на комп'ютері під управлінням Linux
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Запит: select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 cachalot,9,6
<-- 101 canoé,13,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,10,7
<-- 101 panthère,10,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Запит: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Запит: select ім'я, stock_actu, stock_mini from articles, упорядковано за ім'ям
<-- 101 arc,9,8
<-- 101 cachalot,6,6
<-- 101 canoé,7,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,7,7
<-- 101 panthère,7,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée

9.3.8. Висновок

Перед нами цікава програма, оскільки вона дозволяє отримати доступ до бази даних з будь-якого комп’ютера в мережі. Цю програму цілком можна було б написати традиційним способом із використанням сокетів, що, до речі, і вимагається в одному із завдань розділу про бази даних. Якби ми написали цю програму традиційним способом:

  • ми б отримали клієнт і сервер, які могли б бути написані на різних мовах
  • клієнт і сервер спілкувалися б шляхом обміну рядками тексту, і їхній діалог міг би виглядати приблизно так:
client : connect machine port pilote urlBase id mdp

де перші два параметри вказують, де знайти сервер, а наступні чотири — параметри підключення до бази даних, з якою потрібно працювати

Сервер міг би відповісти чимось на кшталт:

200 - Connexion réussie
500 - Echec de la connexion
client : executeSQL requete, separateur

щоб попросити сервер виконати запит SQL до бази даних, до якої підключений клієнт. separateur — це символ, що розділяє поля у рядках відповіді.

Сервер може відповісти приблизно так

    100 n

на запит щодо оновлення бази даних, де n — кількість оновлених рядків

    500 msg d’erreur

якщо запит призвів до помилки

    501 Pas de résultats

якщо запит не дав жодних результатів

    101 ligne1
    101 ligne2
    101 ...

якщо запит дав результати. Рядки, які сервер повернув у такий спосіб, є результатами запиту.

client : close

для закриття з’єднання з віддаленою базою даних. Сервер може повернути рядок, що вказує на результат цього закриття:

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

Звідси видно, що якщо ми здатні створити традиційний додаток із протоколом, подібним до наведеного вище, то можемо вивести можливу структуру сервера RMI. Там, де в протоколі є фраза від клієнта до сервера такого типу:

    commande param1 param2 ... paramq

то на сервері RMI може існувати метод

    String commande(param1,param2,..., paramq)

, і цей метод, доступний для клієнта, повинен бути частиною опублікованого інтерфейсу сервера.

На завершення зауважимо, що наш сервер наразі обслуговує лише одного клієнта: у поточній реалізації він не може обслуговувати декількох клієнтів. Дійсно, якщо клієнт підключається до бази даних B1, сервер створює об’єкт Connection DB=DB1. Якщо другий клієнт запитує підключення до бази B2, сервер фіксує це, створюючи Connection DB=DB2, тим самим розриваючи з’єднання першого клієнта з базою B1.

9.4. Вправи

9.4.1. Вправа 1

Розширте попередній сервер SQL так, щоб він міг обслуговувати декількох клієнтів.

9.4.2. Вправа 2

Напишіть Java-аплет для електронної комерції, представлений у вправах розділу JDBC, так, щоб він працював із сервером RMI із попередньої вправи.