Skip to content

9. JAVA RMI

9.1. Introdução

Vimos como criar aplicações de rede utilizando ferramentas de comunicação chamadas sockets. Numa aplicação cliente/servidor construída com base nessas ferramentas, a ligação entre o cliente e o servidor é o protocolo de comunicação que adotaram para se comunicarem. As duas aplicações podem ser escritas em linguagens diferentes: Java, por exemplo, para o cliente, Perl para o servidor, ou qualquer outra combinação. Temos, de facto, duas aplicações distintas ligadas por um protocolo de comunicação conhecido por ambas. Além disso, o acesso à rede através de sockets não é transparente para uma aplicação Java: esta deve utilizar a classe Socket, uma classe criada especificamente para gerir estas ferramentas de comunicação conhecidas como sockets.

O Java RMI (Remote Method Invocation) permite criar aplicações de rede com as seguintes características:

  1. As aplicações cliente/servidor são aplicações Java em ambas as extremidades da comunicação
  2. O cliente pode utilizar objetos localizados no servidor como se fossem locais
  3. A camada de rede torna-se transparente: as aplicações não precisam de se preocupar com a forma como a informação é transportada de um ponto para outro.

Este último ponto é um fator de portabilidade: se a camada de rede de uma aplicação RMI fosse alterada, a própria aplicação não precisaria de ser reescrita. Seriam as classes RMI da linguagem Java que teriam de ser adaptadas à nova camada de rede.

O princípio da comunicação RMI é o seguinte:

  1. Uma aplicação Java padrão é escrita na máquina A. Esta atuará como servidor. Para tal, alguns dos seus objetos serão «publicados» na máquina A, onde a aplicação está a ser executada, e tornar-se-ão então serviços.
  2. Uma aplicação Java clássica é escrita na máquina B. Esta atuará como cliente. Terá acesso aos objetos/serviços publicados na máquina A; ou seja, através de uma referência remota, poderá manipulá-los como se fossem locais. Para tal, precisará de conhecer a estrutura do objeto remoto ao qual pretende aceder (métodos e propriedades).

9.2. Vamos aprender através de um exemplo

A teoria subjacente à interface RMI não é simples. Para tornar as coisas mais claras, vamos percorrer passo a passo o processo de escrita de uma aplicação cliente/servidor utilizando o pacote RMI do Java. Vamos utilizar uma aplicação que se encontra em muitos livros sobre RMI: o cliente chama um único método de um objeto remoto, que, por sua vez, devolve uma cadeia de caracteres. Aqui, apresentamos uma ligeira variação: o servidor repete o que o cliente lhe envia. Já apresentámos uma aplicação deste tipo neste livro, uma que se baseia em sockets.

9.2.1. A aplicação do servidor

9.2.1.1. Passo 1: A interface objeto/servidor

Um objeto remoto é uma instância de classe que deve implementar a interface Remote definida no pacote java.rmi. Os métodos do objeto que estarão acessíveis remotamente são aqueles declarados numa interface derivada da interface Remote:

import java.rmi.*;

 // remote interface p
ublic interface interEcho extends Remote{ 
     public String echo(String msg) throws java.rmi.RemoteException
; }

Aqui, declaramos uma interface interEcho que define um método echo como acessível remotamente. Este método pode lançar uma exceção da classe RemoteException, que abrange todos os erros relacionados com a rede.

9.2.1.2. Passo 2: Escrever o objeto servidor

No passo seguinte, definimos a classe que implementa a interface remota anterior. Esta classe deve ser derivada da classe UnicastRemoteObject, que fornece métodos que permitem a invocação remota de métodos.

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

// class implementing remote echo
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // manufacturer
    public srvEcho() throws RemoteException{
        super();
    }// manufacturer end

    // method performing the echo
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fine echo
}// end of class

Na classe anterior, encontramos:

  1. o método que executa o echo
  2. um construtor que não faz nada além de chamar o construtor da classe pai. Ele existe para declarar que pode lançar uma RemoteException.

Vamos criar uma instância desta classe com um método main. Para que um objeto/serviço seja acessível a partir do exterior, deve ser criado e registado no diretório de objetos acessíveis externamente. Um cliente que pretenda aceder a um objeto remoto procede da seguinte forma:

  1. contacta o serviço de diretório na máquina onde o objeto desejado se encontra. Este serviço de diretório opera numa porta que o cliente deve conhecer (1099 por predefinição). O cliente solicita ao diretório uma referência a um objeto/serviço, fornecendo o seu nome. Se este nome corresponder a um objeto/serviço no diretório, o diretório devolve uma referência ao cliente, através da qual o cliente pode comunicar com o objeto/serviço remoto.
  2. A partir desse momento, o cliente pode utilizar este objeto remoto como se fosse local

Voltando ao nosso servidor, temos de criar um objeto do tipo srvEcho e registá-lo no diretório de objetos acessíveis externamente. Este registo é efetuado através do método rebind da classe Naming:

Naming.rebind(String nom, Remote obj)

onde

nome: o nome que será associado ao objeto remoto

obj: o objeto remoto

A nossa classe srvEcho fica, portanto, da seguinte forma:

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

// class implementing remote echo
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // manufacturer
    public srvEcho() throws RemoteException{
        super();
    }// manufacturer end

    // method performing the echo
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fine echo

    // service creation
    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 ");
        }
    }// hand
}// end of class

Ao ler o programa anterior, parece que ele irá parar imediatamente após criar e registar o serviço de eco. Não é esse o caso. Como a classe srvEcho é derivada da classe UnicastRemoteObject, o objeto criado executa-se indefinidamente: ele escuta pedidos de clientes numa porta anónima, ou seja, uma escolhida pelo sistema com base nas circunstâncias. A criação do serviço é assíncrona: no exemplo, o método main cria o serviço e continua a execução; irá exibir “Echo server ready.”

9.2.1.3. Passo 3: Compilar a aplicação do servidor

Nesta altura, podemos compilar o nosso servidor. Compilamos o ficheiro interEcho.java a partir da interface interEcho, bem como o ficheiro srvEcho.java a partir da classe srvEcho. Obtemos os ficheiros .class correspondentes: interEcho.class e srvEcho.class.

9.2.1.4. Passo 4: Escrever o cliente

Escrevemos um cliente que recebe a URL do servidor de eco como parâmetro e

  1. lê uma linha digitada no teclado
  2. envia-a para o servidor de eco
  3. exibe a resposta que este envia
  4. regressa ao passo 1 e pára quando a linha digitada for «end».

Isto resulta no seguinte cliente:

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

; public class cltEch

    o { public static void main(String a
        rg[]){ // syntax : cltEcho URLService

        // argument verification
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // client-server dialogue
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // open keyboard flow
            in=new BufferedReader(new InputStreamReader(System.in));
            // service location
            serveur=(interEcho) Naming.lookup(urlService);                
            // loop for reading msg to be sent to echo server
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // send msg to server and receive response
                reponse=serveur.echo(msg);
                // follow-up
                System.out.println("Réponse serveur : " + reponse);
                // next msg
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // it's over
            System.exit(0);
        // error management        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// hand
}// class                

Não há nada de particularmente especial neste cliente, exceto a instrução que recupera uma referência ao servidor:

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

Lembre-se de que o nosso serviço echo foi registado no diretório de serviços da máquina onde reside, utilizando a instrução:


            Naming.rebind("srvEcho",serveurEcho);

O cliente, portanto, também utiliza um método da classe Naming para obter uma referência ao servidor que pretende utilizar. O método de pesquisa utilizado recebe a URL do serviço solicitado como parâmetro. Esta URL tem o formato de uma URL padrão:

    rmi://machine:port/nom_service

onde

rmi: opcional - protocolo RMI

máquina: nome ou endereço IP da máquina na qual o servidor echo é executado - opcional, o padrão é localhost.

port: porta de escuta do serviço de diretório desta máquina — opcional, o padrão é 1099

service_name: nome sob o qual o serviço solicitado foi registado (srvEcho no nosso exemplo)

O que é devolvido é uma instância da interface remota interEcho. Assumindo que o cliente e o servidor não estão na mesma máquina, ao compilar o cliente cltEcho.java, o ficheiro interEcho.class — o resultado da compilação da interface remota interEcho — deve estar presente no mesmo diretório; caso contrário, ocorrerá um erro de compilação nas linhas que referenciam esta interface.

9.2.1.5. Passo 5: Gerar os ficheiros .class necessários para a aplicação cliente-servidor

Para distinguir claramente entre os componentes do lado do servidor e do lado do cliente, colocaremos o servidor no diretório echo\server e o cliente no diretório echo\client.

O diretório do servidor contém os seguintes ficheiros fonte:

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

Após compilar estes dois ficheiros fonte, obtemos os seguintes ficheiros .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

No diretório do cliente, encontramos o seguinte ficheiro fonte:

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

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

bem como o ficheiro interEcho.class que foi gerado durante a compilação do servidor:

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

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

Após compilar o ficheiro fonte, obtemos os seguintes ficheiros .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

Se tentarmos executar o cliente cltEcho, obtemos o seguinte erro:

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

Se tentar executar o servidor srvEcho, obtém o seguinte erro:

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

Em ambos os casos, a Máquina Virtual Java indica que não conseguiu encontrar a classe srvEcho_stub. De facto, nunca ouvimos falar desta classe anteriormente. No cliente, o servidor foi localizado utilizando a seguinte instrução:

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

Aqui, urlservice é a cadeia de caracteres rmi://localhost/srvEcho com

RMI: protocolo RMI

Localhost: a máquina onde o servidor está a ser executado — neste caso, a mesma máquina que o cliente. A sintaxe é normalmente máquina:porta. Se nenhuma porta for especificada, a porta 1099 é utilizada por predefinição. O serviço de diretório do servidor está a escutar nesta porta.

srvEcho: este é o nome do serviço específico que está a ser solicitado

Não foram relatados erros durante a compilação. Bastava que o ficheiro interEcho.class para a interface remota estivesse disponível.

Em tempo de execução, a máquina virtual requer a presença de um ficheiro srvEcho_stub.class se o serviço solicitado for o serviço srvEcho; geralmente, é necessário um ficheiro X_stub.class para um serviço X. Este ficheiro só é necessário em tempo de execução, não durante a compilação do cliente. O mesmo se aplica ao servidor. Então, o que é exatamente este ficheiro?

No servidor, existe a classe srvEcho.class, que é o nosso objeto/serviço remoto. O cliente, mesmo que não precise desta classe, ainda precisa de uma espécie de imagem dela para se comunicar com ela. Na verdade, o cliente não envia as suas solicitações diretamente para o objeto remoto: ele as envia para a sua imagem local srvEcho_stub.class localizada na mesma máquina que ele. Esta imagem local, srvEcho_stub.class, comunica com uma imagem semelhante (srvEcho_stub.class) localizada no servidor. Esta imagem é criada a partir do ficheiro .class do servidor utilizando uma ferramenta Java chamada rmic. No Windows, o comando:

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

irá gerar, a partir do ficheiro srvEcho.class, dois outros ficheiros .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

Aqui temos o ficheiro srvEcho_stub.class de que tanto o cliente como o servidor necessitam em tempo de execução. Existe também um ficheiro srvEcho_Skel.class, cuja finalidade é atualmente desconhecida. Copiamos o ficheiro srvEcho_stub.class para os diretórios do cliente e do servidor e eliminamos o ficheiro srvEcho_Skel.class. Temos agora os seguintes ficheiros:

no lado do servidor:

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

no lado do cliente:

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. Passo 6: Executar a aplicação de eco cliente-servidor

Estamos prontos para executar a nossa aplicação cliente-servidor. Inicialmente, o cliente e o servidor serão executados na mesma máquina. Primeiro, precisamos de iniciar a nossa aplicação de servidor. Recorde-se que isto:

  • cria o serviço
  • regista-o no diretório de serviços da máquina na qual o servidor echo está a ser executado

Este último passo requer um serviço de registo. Este é iniciado com o comando:

start j:\jdk12\bin\rmiregistry

rmiregistry é o serviço de registo. Aqui, é iniciado em segundo plano numa janela do Prompt de Comando do Windows utilizando o comando start. Com o registo ativo, podemos criar o serviço echo e registá-lo no registo de serviços. Mais uma vez, é iniciado em segundo plano utilizando um comando start:

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

O servidor echo é executado numa nova janela do DOS e apresenta a saída solicitada:

Serveur d’écho prêt

Resta apenas iniciar e testar o nosso cliente:

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. O cliente e o servidor em duas máquinas diferentes

No exemplo anterior, o cliente e o servidor estavam na mesma máquina. Agora, colocamo-los em máquinas diferentes:

  • o servidor numa máquina Windows
  • o cliente numa máquina Linux

O servidor é iniciado como antes na máquina Windows. Na máquina Linux, os ficheiros .class do cliente foram transferidos:

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

O cliente é iniciado:

$ 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

Portanto, temos um erro: a Máquina Virtual Java aparentemente requer o ficheiro srvEcho_skel.class, que foi gerado pelo utilitário rmic, mas que não tinha sido utilizado até agora. Recriamo-lo e transferimo-lo também para a máquina 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

Recebemos o mesmo erro de antes... Então, refletimos sobre o assunto e relemos a documentação do RMI. Acabamos por concluir que talvez o próprio servidor precise do famoso ficheiro srvEcho_Skel.class. Em seguida, reiniciamos o servidor na máquina Windows com os ficheiros srvEcho_Stub.class e srvEcho_Skel.class presentes:

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

Em seguida, na máquina Linux, testamos o cliente novamente e, desta vez, funciona:

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

Podemos, portanto, concluir que, no lado do servidor, tanto o ficheiro srvEcho_Stub.class como o srvEcho_Skel.class devem estar presentes. No lado do cliente, até agora, apenas o ficheiro srvEcho_Stub.class tem sido necessário. Este revelou-se essencial quando o cliente e o servidor estavam na mesma máquina Windows. No Linux, vamos removê-lo para ver o que acontece...

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

Temos um erro interessante que parece indicar que a Máquina Virtual Java tentou carregar a famosa classe stub, mas falhou devido à ausência de um «gestor de segurança». Lembramos-nos de ter visto algo sobre isto na documentação. Voltamos a investigar... e descobrimos que o servidor deve criar e instalar um gestor de segurança para garantir aos clientes que solicitam o carregamento de classes que estas são seguras. Sem este gestor de segurança, o carregamento de classes é impossível. Isso parece fazer sentido: o nosso cliente Linux solicitou a srvEcho_stub.class de que necessitava ao servidor, e o servidor recusou, indicando que não tinha sido instalado nenhum gestor de segurança. Assim, modificamos o código da função principal do servidor da seguinte forma:

     // service creation 
    public static void main (String arg[]){

         // installation of a security manager Sy
        stem.setSecurityManager(new RMISecurityManager()); 

        // service launch and registration
        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 ");
        }
    }// hand

Compilamos e geramos os ficheiros srvEcho_stub.class e srvEcho_Skel.class utilizando a ferramenta rmic. Iniciamos o serviço de diretório (rmiregistry) e, em seguida, o servidor, e obtemos um erro que não tínhamos antes!

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

O gestor de segurança parece ter sido demasiado rigoroso. Revemos a documentação novamente... Percebemos que, quando um gestor de segurança está ativo, é necessário especificar as permissões do programa ao iniciá-lo. Isto é feito com a seguinte opção:

<mark style="background-color: #ffff00">start j:\\jdk12\\bin\\java -Djava.security.policy=mypolicy srvEcho</mark>

onde

java.security.policy é uma palavra-chave

mypolicy é um ficheiro de texto que define as permissões do programa. Aqui, é o seguinte:

grant {
    // Allow everything for now
    permission java.security.AllPermission;
};

O programa tem permissões totais aqui.

Vamos recomeçar. Vá para o diretório do servidor e faça o seguinte:

  • inicie o serviço de diretório: start j:\jdk12\bin\rmiregistry
  • Inicie o servidor: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

E, desta vez, o servidor echo (mas ainda não o cliente) inicia corretamente. Agora pode tentar o seguinte:

  • Pare o servidor echo e, em seguida, o serviço de diretório
  • Reinicie o serviço de diretório enquanto estiver num diretório diferente do do servidor
  • volte ao diretório do servidor inicie o servidor echo — receberá o seguinte erro:
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

Podemos concluir que o diretório a partir do qual o serviço de diretório é iniciado é importante. Aqui, o Java não conseguiu encontrar o srvEcho_stub.class porque o serviço de diretório não foi iniciado a partir do diretório do servidor. Ao iniciar o servidor, pode especificar o diretório onde se encontram as classes necessárias ao servidor:

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

O comando está numa única linha. A palavra-chave <mark style="background-color: #ffff00">java.rmi.server.codebase</mark> é utilizada para especificar o URL do diretório que contém as classes necessárias ao servidor. Neste caso, o URL especifica o protocolo file — que é o protocolo para aceder a ficheiros locais — e o diretório que contém os ficheiros .class do servidor. Portanto, se proceder da seguinte forma:

  • parar o serviço de diretório
  • reinicie o serviço de diretório a partir de um diretório diferente do diretório do servidor
  • no diretório do servidor, inicie o servidor com o comando (uma única linha):
 start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

O servidor está agora a funcionar corretamente. Podemos agora passar para o cliente. Vamos testá-lo:

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

Recebemos o mesmo erro indicando a ausência de um gestor de segurança. Pensamos que podemos ter cometido um erro e que é o cliente que precisa de criar o seu próprio gestor de segurança. Deixamos o servidor com o seu gestor de segurança, mas criamos um para o cliente também. A função principal do cliente cltEcho.java passa então a ser:

public static void main(String arg[]){
         // syntax : cltEcho machine po
        rt // machine: machine where the echo server 
        operates // port: port where the service directory operates on the echo servic

        e machine // argument ve
        rification if(ar
            g.length!=1){ System.err.println("Syntaxe : pg u
            rl_service_rmi"
        )

        ; System.exit(1); } // installation
         of a security manager System.setSecurityManager(n

        ew RMISecurityManager()); 
         // client-server dia
        logue String urlServi
        ce=arg[0]; Buf
        feredReader in=null;
         String msg=null; S

        tring reponse=null; interEcho serveur=null; try{
            ....
        } catch (Exception e){
            ....
        }// try
    }// hand

Em seguida, procedemos da seguinte forma:

  • Recompilar cltEcho.java
  • transferir os ficheiros .class para a máquina 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
  • Inicie o cliente
$ 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

Apesar das aparências, estamos a fazer progressos: o erro já não é o mesmo. Podemos ver que o cliente conseguiu solicitar o srvEcho_Stub.class ao servidor, mas o servidor não o conseguiu encontrar. Portanto, o cliente deve ter um gestor de segurança se quiser poder solicitar classes ao servidor.

Se analisarmos o erro anterior, vemos que o ficheiro srvEcho_Stub.class foi procurado no diretório e:/data/java/rmi/echo/server/ e não foi encontrado. No entanto, é exatamente aí que ele se encontra. Se analisarmos mais de perto a lista de métodos envolvidos no erro, encontramos este: sun.net.www.protocol.file.FileURLConnection.getInputStream. O cliente parece ter aberto uma ligação utilizando um objeto FileURLConnection. Suspeitamos que tudo isto esteja relacionado com a forma como iniciámos o nosso servidor:

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

A mensagem de erro parece referir-se ao valor da palavra-chave java.rmi.server.codebase. Quando consultamos novamente a documentação, verificamos que o valor desta palavra-chave é, nos exemplos fornecidos, sempre: http://.., ou seja, o protocolo utilizado é o HTTP. Não é claro como o cliente solicita e obtém as suas classes do servidor. Talvez as solicite utilizando o URL especificado pela palavra-chave *java.rmi.server.codebase*, que é definida quando o servidor é iniciado. Decidimos, portanto, iniciar o servidor utilizando o seguinte novo comando:

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

O protocolo agora é HTTP. Temos de mover os ficheiros .class para um local acessível ao servidor HTTP na máquina onde as classes serão armazenadas. No nosso exemplo, o servidor está a ser executado numa máquina Windows com um servidor HTTP Microsoft PWS. A raiz deste servidor é d:\Inetpub\wwwroot. Por conseguinte, procedemos da seguinte forma:

  • Crie o diretório d:\Inetpub\wwwroot\rmi\echo
  • Coloque os ficheiros .class do servidor e o ficheiro mypolicy nesse local
  • Inicie o servidor web, caso ainda não esteja a funcionar
  • Reinicie o serviço de registo (rmiregistry)
  • Reinicie o servidor com o comando
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
  • Na máquina Linux, inicie o cliente:
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

Ufa! Funciona. O cliente recuperou com sucesso o ficheiro srvEcho_Stub.class.

Tudo isto deu-nos algumas ideias, e estamos a pensar se o cliente a ser executado na máquina Windows do servidor também funcionaria sem o ficheiro srvEcho_Stub.class. Navegamos até ao diretório do cliente, eliminamos o ficheiro srvEcho_Stub.class, caso esteja lá, e iniciamos o cliente da mesma forma que no 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. Resumo

Lado do servidor Windows:

  • O servidor possui um gestor de segurança
  • e foi iniciado com as seguintes opções: start j:\jdk12\bin\java -Djava.security.policy=mypolicy

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

Lado do cliente (Linux ou Windows)

  • O cliente possui um gestor de segurança
  • No Linux, foi iniciado por `java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho`
  • No Windows, foi iniciado por j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

9.2.1.9. Servidor Echo no Linux, clientes no Windows e no Linux

Vamos agora transferir o servidor para uma máquina Linux e testar clientes Linux e Windows. O procedimento é o seguinte:

  • Mova os ficheiros .class do servidor para a máquina 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
  • Como o srvEcho_Stub.class será solicitado pelos clientes, o diretório escolhido para as classes do servidor é aquele acessível ao servidor HTTP da máquina Linux. Aqui, a URL para este diretório é http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
  • o serviço de registo é iniciado em segundo plano: /usr/local/bin/jdk/rmiregistry &
  • O servidor é iniciado em segundo plano: /usr/local/bin/jdk/bin/java

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

srvEcho &

Podemos testar os clientes. Primeiro, o cliente Windows.

  • Navegue até ao diretório do cliente na máquina Windows
  • Inicie o cliente com o comando:
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

Teste do cliente 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

Note que, para o cliente Linux em execução na mesma máquina que o servidor echo, não foi necessário especificar uma máquina no URL do serviço solicitado.

9.3. Segundo exemplo: servidor SQL numa máquina Windows

9.3.1. O problema

No capítulo sobre JDBC, vimos como gerir bases de dados relacionais. Nos exemplos apresentados, as aplicações e a base de dados utilizada estavam na mesma máquina Windows. Aqui, propomos escrever um servidor RMI numa máquina Windows que permita a clientes remotos aceder às bases de dados ODBC públicas na máquina que aloja o servidor.

Image

O cliente RMI poderia realizar três operações:

  • ligar-se à base de dados da sua escolha
  • enviar consultas SQL
  • fechar a ligação

O servidor executa as consultas SQL do cliente e envia os resultados de volta ao cliente. Esta é a sua função principal, razão pela qual nos referiremos a ele como um servidor SQL.

Aplicamos os diferentes passos vistos anteriormente com o servidor echo.

9.3.2. Passo 1: A interface remota

A interface remota é a interface que lista os métodos do servidor RMI que estarão acessíveis aos clientes RMI. Iremos utilizar a seguinte interface:

import java.rmi.*;

 // remote interface p
ublic interface interSQL extends Remote{ 
     public String connect(String pilote, String url, String id, String mdp
        ) throws java.rmi.RemoteExcept
    ion; public String[] executeSQL(String requete, String separa
        teur) throws java.rmi.RemoteE
    xception; public Str
        ing close() throws java.rmi.Re
moteException; }

As funções dos vários métodos são as seguintes:

Connect: o cliente liga-se a uma base de dados remota, fornecendo o controlador, o URL JDBC, bem como o seu ID e palavra-passe para aceder à base de dados. O servidor devolve uma cadeia de caracteres que indica o resultado da ligação:

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

executeSQL: o cliente solicita a execução de uma consulta SQL na base de dados à qual está ligado. Especifica o caractere que deve separar os campos nos resultados que lhe são devolvidos. O servidor devolve uma matriz de cadeias de caracteres:

    100 n
    para uma consulta de atualização da base de dados, em que n é o número de linhas atualizadas
    500 msg d’erreur
    se a consulta gerou um erro
    501 Pas de résultats
    se a consulta não devolveu resultados
    101 ligne1
    101 ligne2
    101 ...
se a consulta devolveu resultados. As linhas devolvidas pelo servidor são os resultados da consulta.

close: o cliente encerra a sua ligação à base de dados remota. O servidor devolve uma cadeia de caracteres que indica o resultado deste encerramento:

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

9.3.3. Passo 2: Escrever o servidor

Segue-se o código-fonte Java para o servidor SQL. A sua compreensão requer familiaridade com a gestão de bases de dados JDBC e com a criação de servidores RMI. Os comentários do programa deverão facilitar a compreensão.

// imported packages
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

// class srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{

    // global class data
    private Connection DB;

    // ------------- manufacturer
    public srvSQL() throws RemoteException{
        super();
    }

    // --------------- connect
    public String connect(String pilote, String url, String id,
        String mdp) throws RemoteException{

        // connection to url database via driver
        // identification with identity id and password mdp

        String resultat=null;            // result of the method
        try{
            // loading the driver
            Class.forName(pilote);
            // connection request
            DB=DriverManager.getConnection(url,id,mdp);
            // ok
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // error
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // end
        return resultat;
    }            

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

        // executes a SQL query on the DB database
        // and puts the results in an array of strings

        // data required to execute the request
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // create query container
            S=DB.createStatement();
            // request execution
            if (! S.execute(requete)){
                // update request
                // returns the number of lines updated
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // it was a query request
            // retrieve results
            RS=S.getResultSet();
            // number of Resultset fields
            int nbChamps=RS.getMetaData().getColumnCount();
            // we exploit them
            while(RS.next()){
                // create results line
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // add to results vector
                resultats.addElement(ligne);
            }// while
            // end of results analysis
            // free up resources
            RS.close();
            S.close();
            // we return the results
            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){
            // error
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- close
    public String close() throws RemoteException {
        // closes database connection
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // return result
        return resultat;
    }

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

        // security manager
        System.setSecurityManager(new RMISecurityManager());

        // service launch
        srvSQL serveurSQL=null;
        try{
            // creation
            serveurSQL=new srvSQL();
            // registration
            Naming.rebind("srvSQL",serveurSQL);
            // follow-up
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // error
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// hand

}// class

9.3.4. Escrever o cliente RMI

O cliente do servidor RMI é chamado com os seguintes parâmetros:

    urlserviceAnnuaire pilote urlBase id mdp separateur

directoryServiceURL: URL RMI do serviço de diretório que registou o servidor SQL

driver: controlador que o servidor SQL deve utilizar para gerir a base de dados

urlBase: URL JDBC da base de dados a gerir

id: ID do cliente ou nulo se não houver ID

password: palavra-passe do cliente ou nulo se não houver palavra-passe

separator: caractere que o servidor SQL deve utilizar para separar os campos nas linhas de resultados de uma consulta

Aqui está um exemplo de parâmetros possíveis:

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

com aqui:

urlserviceAnnuaire    srvSQL
Nome RMI do servidor SQL
pilote     sun.jdbc.odbc.JdbcOdbcDriver
o controlador padrão para bases de dados com uma interface ODBC
urlBase    jdbc:odbc:articles
para utilizar uma base de dados de artigos listada nas bases de dados ODBC públicas do computador Windows
id    null
sem identidade
mdp    null
sem palavra-passe
separateur    , 
os campos nos resultados serão separados por uma vírgula

Uma vez iniciado com os parâmetros acima, o cliente segue estes passos:

  • Liga-se ao servidor RMI srvSQL, que é um servidor RMI na mesma máquina que o cliente
  • solicita uma ligação à base de dados de artigos
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • Solicita ao utilizador que introduza uma consulta SQL
  • envia-a para o servidor SQL
executeSQL(requete, ’’,’’);
  • Exibe os resultados devolvidos pelo servidor no ecrã
  • solicita novamente ao utilizador que introduza uma consulta SQL no teclado. Parará quando a consulta estiver concluída.

Segue-se o código do cliente Java. Os comentários devem ser suficientes para a sua compreensão.

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

public class cltSQL {

    // global class data
    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[]){
        // syntax : cltSQL urlServiceAnnuaire separator driver url id mdp
        // urlServiceAnnuaire : url of the directory of services RMI to contact
        // driver: driver to be used for the database to be processed
        // urlBase: jdbc url of the database to be used
        // id: user identity
        // mdp: password
        // separator: string separating fields in query results

        // check number of arguments
        if(arg.length!=6)
            erreur(syntaxe,1);

        // init database connection parameters
        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];        

        // installation of a security manager
        System.setSecurityManager(new RMISecurityManager());

        // client-server dialogue
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // open keyboard flow
            in=new BufferedReader(new InputStreamReader(System.in));
            // follow-up
            System.out.println("--> Connexion au serveur RMI en cours...");
            // service location
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // follow-up
            System.out.println("--> Connexion à la base de données en cours");
            // initial database connection request
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // follow-up
            System.out.println("<-- "+reponse);
            // response analysis
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // loop for reading requests to be sent to the server SQL
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // send request to server and receive response
                lignes=serveurSQL.executeSQL(requete,separateur);
                // follow-up
                afficheLignes(lignes);
                // following request
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// while
            // follow-up
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // close the connection
            reponse=serveurSQL.close();
            // follow-up
            System.out.println("<-- " + reponse);
            // end
            System.exit(0);
        // error management        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// try
    }// hand

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

    // ------------ error
    private static void erreur(String msg, int exitCode){
        // error msg display
        System.err.println(msg);
        // possible release of resources
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // we leave
        System.exit(exitCode);
    }// error

}// class    

9.3.5. Passo 3: Criação de ficheiros .class

  • O servidor é compilado
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
  • Os ficheiros Stub e Skel são criados
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
  • Mova os ficheiros interSQL.class, srvSQL_Stub.class e srvSQL_Skel.class para o diretório do cliente
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
  • Compilando o cliente
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. Passo 4: Testar com servidor e cliente na mesma máquina Windows

  • O serviço de diretório é iniciado num diretório diferente daquele do servidor e do cliente
F:\>start j:\jdk12\bin\rmiregistry
  • Coloque o seguinte ficheiro mypolicy nos diretórios do cliente e do servidor
grant {
    // Allow everything for now
    permission java.security.AllPermission;
};
  • Inicie o servidor
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
  • Inicie o cliente
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 ,

--> Connection to server RMI in progress...
--> Current database connection
<-- 200 Successful connection
--> Requête : select nom, stock_actu from articles order by stock_actu desc
<-- 101 vélo.31
<-- 101 test3.13
<-- 101 water skis,13
<-- 101 canoeing,13
<-- 101 panther,11
<-- 101 leopard,11
<-- 101 sperm whale,10
<-- 101 rifle,10
<-- 101 arc,10
--> Query: update articles set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Requête : select nom,stock_actu from articles order by stock_actu asc
<-- 101 sperm whale,9
<-- 101 rifle,9
<-- 101 arc.9
<-- 101 panther,10
<-- 101 leopard,10
<-- 101 test3.13
<-- 101 water skis,13
<-- 101 canoeing,13
<-- 101 vélo.31
--> Query: end
--> Closing the remote database connection
<-- 200 Closed base

9.3.7. Passo 5: Testar com um servidor numa máquina Windows e um cliente numa máquina Linux

  • Se necessário, pare o servidor e o serviço de diretório
  • Transfira os ficheiros .class do cliente para uma máquina 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
  • Os ficheiros do servidor são colocados num diretório acessível ao servidor HTTP da máquina 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
  • Reinicie o serviço de diretório
F:\>start j:\jdk12\bin\rmiregistry
  • Reinicie o servidor com parâmetros diferentes dos utilizados no teste anterior
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
  • Inicie o cliente na máquina Linux
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Requête : select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 sperm whale,9,6
<-- 101 canoeing,13.7
<-- 101 test3,13,9
<-- 101 rifle,9,8
<-- 101 leopard,10.7
<-- 101 panther,10.7
<-- 101 water skis,13.8
<-- 101 bicycle,31,8
--> Query: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Requête : select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 sperm whale,6,6
<-- 101 canoeing,7,7
<-- 101 test3,13,9
<-- 101 rifle,9,8
<-- 101 leopard,7,7
<-- 101 panther,7,7
<-- 101 water skis,13.8
<-- 101 bicycle,31,8
--> Query: end
--> Closing the remote database connection
<-- 200 Closed base

9.3.8. Conclusão

Esta é uma aplicação interessante, na medida em que permite o acesso a uma base de dados a partir de qualquer estação de trabalho na rede. Poderíamos muito bem tê-la escrito da forma tradicional, utilizando sockets, que é, na verdade, o que é exigido num exercício do capítulo sobre bases de dados. Se fôssemos escrever esta aplicação da forma tradicional:

  • teríamos um cliente e um servidor que poderiam ser escritos em linguagens diferentes
  • o cliente e o servidor comunicariam através da troca de linhas de texto e teriam um diálogo que poderia ser algo como isto:
client : connect machine port pilote urlBase id mdp

onde os dois primeiros parâmetros especificam onde encontrar o servidor, e os quatro seguintes indicam os parâmetros de ligação para a base de dados a utilizar

O servidor pode responder com algo como:

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

para solicitar ao servidor que execute uma consulta SQL na base de dados ligada ao cliente. separador é o caractere utilizado para separar os campos nas linhas de resposta.

O servidor pode responder com algo como

    100 n
    para uma consulta de atualização da base de dados, em que n é o número de linhas atualizadas
    500 msg d’erreur
    se a consulta gerou um erro
    501 Pas de résultats
    se a consulta não devolveu resultados
    101 ligne1
    101 ligne2
    101 ...
se a consulta devolveu resultados. As linhas devolvidas pelo servidor são os resultados da consulta.
client : close

para fechar a ligação à base de dados remota. O servidor pode devolver uma cadeia de caracteres indicando o resultado deste encerramento:

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

Aqui vemos que, se formos capazes de construir uma aplicação tradicional utilizando um protocolo do tipo descrito acima, podemos deduzir uma possível estrutura para o servidor RMI. No protocolo, temos uma mensagem do cliente para o servidor do tipo:

    commande param1 param2 ... paramq

Podemos ter um método dentro do servidor RMI

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

e este método, que é acessível ao cliente, deve, portanto, fazer parte da interface publicada do servidor.

Para concluir, note que o nosso servidor atualmente lida apenas com um cliente: não consegue lidar com vários clientes tal como está escrito atualmente. De facto, se um cliente se ligar à base de dados B1, o servidor cria um objeto Connection com DB=DB1. Se um segundo cliente solicitar uma ligação à base de dados B2, o servidor estabelece-a com Connection DB=DB2, interrompendo assim a ligação do primeiro cliente à base de dados B1.

9.4. Exercícios

9.4.1. Exercício 1

Amplie o servidor SQL anterior para que possa lidar com vários clientes.

9.4.2. Exercício 2

Escreva o applet de comércio eletrónico em Java apresentado nos exercícios do capítulo sobre JDBC, de modo a que funcione com o servidor RMI do exercício anterior.