Skip to content

9. JAVA RMI

9.1. Introdução

Vimos como criar aplicações de rede utilizando as ferramentas de comunicação denominadas 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 comunicar. Ambas as 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 tem de utilizar a classe Socket, criada especificamente para gerir estas ferramentas de comunicação que são as sockets.

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 têm 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 vier a mudar, a própria aplicação não terá de ser reescrita. Serão as classes RMI da linguagem Java que terão de ser adaptadas à nova camada de rede.

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

  1. Uma aplicação Java clássica é escrita numa máquina A. Esta irá desempenhar o papel de servidor. Para tal, alguns dos seus objetos serão «publicados» na máquina A, na qual a aplicação é executada, tornando-se assim serviços.
  2. Uma aplicação Java clássica é escrita numa máquina B. Esta irá desempenhar o papel de 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 com um exemplo

A teoria subjacente à interface RMI não é simples. Para compreender melhor, vamos acompanhar passo a passo a criação de uma aplicação cliente/servidor utilizando o pacote RMI do Java. Tomamos como exemplo uma aplicação que se encontra em muitos livros sobre RMI: o cliente invoca um único método de um objeto remoto, que lhe devolve então uma cadeia de caracteres. Apresentamos aqui uma ligeira variação: o servidor repete o que o cliente lhe envia. Já apresentámos neste livro uma aplicação semelhante, que se baseava em sockets.

9.2.1. A aplicação do servidor

9.2.1.1. Passo 1: a interface do 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.*;

// a interface remota
public interface interEcho extends Remote{
    public String echo(String msg) throws java.rmi.RemoteException;
}

Aqui, declara-se, portanto, uma interface interEcho que declara um método echo como acessível remotamente. Este método pode gerar uma exceção da classe RemoteException, classe que agrupa todos os erros relacionados com a rede.

9.2.1.2. Passo 2: definição do objeto servidor

Na etapa seguinte, define-se a classe que implementa a interface remota anterior. Esta classe deve derivar da classe UnicastRemoteObject, que dispõe dos métodos que permitem a invocação de métodos à distância.

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

// classe que implementa o eco remoto
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // construtor
    public srvEcho() throws RemoteException{
        super();
    }// fim do construtor

    // método que implementa o eco
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fim do eco
}// fim da classe

Na classe anterior, encontramos:

  1. o método que devolve o valor
  2. um construtor que não faz nada além de chamar o construtor da classe pai. Está presente para declarar que pode gerar uma exceção do tipo 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, tem de ser criado e registado no diretório de objetos acessíveis a partir do exterior. Um cliente que pretenda aceder a um objeto remoto procede, de facto, da seguinte forma:

  1. dirige-se ao serviço de diretório da máquina na qual se encontra o objeto que pretende. 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, indicando o seu nome. Se esse nome corresponder a um objeto/serviço do diretório, este devolve ao cliente uma referência através da qual o cliente poderá comunicar com o objeto/serviço remoto.
  2. A partir desse momento, o cliente pode utilizar esse 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 a partir do exterior. Este registo é efetuado com o método da classe rebind da classe Naming:

Naming.rebind(String nom, Remote obj)

com

nome: o nome que será associado ao objeto remoto

obj: o objeto remoto

A nossa classe srvEcho passa, assim, a ter o seguinte aspeto:

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

// classe que implementa o eco remoto
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // construtor
    public srvEcho() throws RemoteException{
        super();
    }// fim do construtor

    // método que implementa o eco
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fim do eco

    // criação do serviço
    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
}// fim da classe

Ao ler o programa anterior, fica-se com a impressão de que ele vai parar imediatamente após criar e registar o serviço de eco. Não é esse o caso. Como a classe srvEcho deriva da classe UnicastRemoteObject, o objeto criado executa-se indefinidamente: fica à escuta de pedidos dos clientes numa porta anónima, ou seja, escolhida pelo sistema de acordo com as circunstâncias. A criação do serviço é assíncrona: no exemplo, o método main cria o serviço e continua a sua execução: irá, de facto, apresentar «Servidor de eco pronto».

9.2.1.3. Etapa 3: compilação da aplicação de servidor

Nesta fase, já podemos compilar o nosso servidor. Compilamos o ficheiro interEcho.java da interface interEcho, bem como o ficheiro srvEcho.java da classe srvEcho. Obtenemos os ficheiros .class correspondentes: interEcho.class e srvEcho.class.

9.2.1.4. Passo 4: criação do cliente

Escrevemos um cliente ao qual passamos como parâmetro o URL do servidor de eco e que

  1. lê uma linha digitada no teclado
  2. a envia para o servidor de eco
  3. exibe a resposta que este envia
  4. volta ao passo 1 e termina quando a linha digitada for «fim».

O resultado é o seguinte cliente:

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

public class cltEcho {

    public static void main(String arg[]){
        // sintaxe: cltEcho URLService

        // verificação dos argumentos
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // diálogo cliente-servidor
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // abertura do fluxo do teclado
            in=new BufferedReader(new InputStreamReader(System.in));
            // localização do serviço
            serveur=(interEcho) Naming.lookup(urlService);                
            // ciclo de leitura das mensagens a enviar para o servidor de eco
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // envio da mensagem para o servidor e receção da resposta
                reponse=serveur.echo(msg);
                // acompanhamento
                System.out.println("Réponse serveur : " + reponse);
                // mensagem seguinte
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // terminado
            System.exit(0);
        // gestão de erros        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// main
}// classe                

Não há nada de especial neste cliente, a não ser a instrução que solicita uma referência do servidor:

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

Recorde-se que o nosso serviço de eco foi registado no diretório de serviços da máquina onde se encontra com a instrução:


            Naming.rebind("srvEcho",serveurEcho);

O cliente também utiliza, portanto, um método da classe Naming para obter uma referência do servidor que pretende utilizar. O método lookup utilizado aceita como parâmetro o URL do serviço solicitado. Este tem o formato de um URL clássico:

    rmi://máquina:porta/nom_service

com

rmi: opcional — protocolo RMI

machine: nome ou endereço IP da máquina na qual o servidor de eco opera — opcional, por predefinição localhost.

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

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

O que é recuperado é uma instância da interface remota interEcho. Supondo que o cliente e o servidor não se encontrem na mesma máquina, ao compilar o cliente cltEcho.java, é necessário dispor, no mesmo diretório, do ficheiro interEcho.class, resultado da compilação da interface remota interEcho; caso contrário, ocorrerá um erro de compilação nas linhas que fazem referência a essa interface.

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

Para compreender bem o que pertence ao lado do servidor e o que pertence ao lado do cliente, colocaremos o servidor num diretório echo\serveur e o cliente num 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 a compilação destes dois ficheiros fonte, obtêm-se 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, encontra-se 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 a compilação do ficheiro fonte, obtêm-se 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 tentarmos executar o servidor srvEcho, obtemos 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 encontrou a classe srvEcho_stub. De facto, nunca ouvimos falar desta classe. No cliente, a localização do servidor foi efetuada com a seguinte instrução:

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

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

Rmi: protocolo RMI

Localhost: máquina onde o servidor está a funcionar — neste caso, a mesma máquina em que o cliente está a funcionar. A sintaxe é normalmente máquina:porta. Na ausência da porta, será utilizada a porta 1099 por predefinição. A escutar nesta porta encontra-se o serviço de diretório do servidor.

srvEcho: é o nome do serviço específico solicitado

Durante a compilação, não foi sinalizado qualquer erro. Bastava apenas que o ficheiro interEcho.class da interface remota estivesse disponível.

Durante a execução, a máquina virtual exige a presença de um ficheiro srvEcho_stub.class se o serviço solicitado for o serviço srvEcho; de um modo geral, um ficheiro X_stub.class para um serviço X. Este ficheiro só é necessário durante a execução, não durante a compilação do cliente. O mesmo se aplica ao servidor. O que é, então, este ficheiro?

No servidor, encontra-se a classe srvEcho.class, que é o nosso objeto/serviço remoto. O cliente, mesmo que não necessite desta classe, precisa, no entanto, de uma espécie de imagem da mesma para poder comunicar com ela. Na verdade, o cliente não envia as suas solicitações diretamente para o objeto remoto: envia-as 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 da mesma natureza (srvEcho_stub.class), localizada, desta vez, no servidor. Esta imagem é criada a partir do ficheiro .class do servidor com uma ferramenta Java chamada rmic. No Windows, o comando:

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

irá produzir, 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 está, de facto, o ficheiro srvEcho_stub.class de que o cliente e o servidor necessitam para a execução. Existe também um ficheiro srvEcho_Skel.class cuja função, por enquanto, desconhecemos. Fazemos uma cópia do ficheiro srvEcho_stub.class no diretório do cliente e do servidor e eliminamos o ficheiro srvEcho_Skel.class. Ficamos, assim, com 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

do 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: Execução da aplicação cliente-servidor de eco

Estamos prontos para executar a nossa aplicação cliente-servidor. Numa primeira fase, o cliente e o servidor funcionarão na mesma máquina. Em primeiro lugar, é necessário iniciar a nossa aplicação servidor. Recorde-se que esta:

  • cria o serviço
  • o regista no diretório de serviços da máquina em que o servidor de eco está a funcionar

Este último ponto requer a existência de um serviço de diretório. Este é iniciado através do comando:

start j:\jdk12\bin\rmiregistry

rmiregistry é o serviço de diretório. Aqui, é iniciado em segundo plano numa janela do DOS do Windows através do comando start. Com o diretório ativo, podemos criar o serviço de eco e registá-lo no diretório de serviços. Mais uma vez, este é iniciado em segundo plano através do comando start:

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

O servidor de eco é executado numa nova janela DOS e apresenta, tal como solicitado:

Serveur d’écho prêt

Resta-nos 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, tal como anteriormente, na máquina Windows. Na máquina Linux, transferimos os ficheiros .class do cliente:

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 parece estar a solicitar o ficheiro srvEcho_skel.class, que tinha sido gerado pelo utilitário rmic, mas que até agora não tinha sido utilizado. 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

Aparece o mesmo erro de antes... Então, refletimos e relemos a documentação sobre o RMI. Acabamos por concluir que talvez seja o próprio servidor que precise do famoso ficheiro srvEcho_Skel.class. Reiniciamos então, na máquina Windows, o servidor com os dois 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

depois, na máquina Linux, testamos novamente o cliente 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

Conclui-se, portanto, que, do lado do servidor, os dois ficheiros srvEcho_Stub.class e srvEcho_Skel.class devem estar presentes. Do lado do cliente, até agora só foi necessário o ficheiro srvEcho_Stub.class. Este revelou-se indispensável quando o cliente e o servidor se encontravam na mesma máquina Windows. No Linux, vamos removê-lo para ver...

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 aqui um erro interessante que parece indicar que a máquina virtual Java tentou carregar a famosa classe stub, mas não conseguiu devido à ausência de um «security manager». Lembramos-nos de ter visto algo sobre este tema na documentação. Voltamos a consultar a documentação... e descobrimos que o servidor deve criar e instalar um gestor de segurança (security manager) que garanta aos clientes que solicitam o carregamento de classes que estas são seguras. Na ausência desse gestor de segurança, o carregamento das classes é impossível. Parece fazer sentido: o nosso cliente Linux solicitou ao servidor a classe srvEcho_stub.class de que necessita e este recusou, indicando que não tinha sido instalado nenhum gestor de segurança. Por isso, alteramos o código da função main do servidor da seguinte forma:

    // criação do serviço
    public static void main (String arg[]){

        // instalação de um gestor de segurança
        System.setSecurityManager(new RMISecurityManager());

        // inicialização e registo do serviço
        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 ");
        }
    }// principal

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

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 eficaz. Voltamos a consultar a documentação... Percebemos que, quando um gestor de segurança está ativo, é necessário especificar os direitos do programa no momento do seu arranque. Isto faz-se com a seguinte opção:

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

onde

java.security.policy é uma palavra-chave

mypolicy é um ficheiro de texto que define os direitos do programa. Neste caso, é o seguinte:

grant {
    // Permitir tudo por enquanto
    permission java.security.AllPermission;
};

O programa tem aqui todos os direitos.

Vamos recomeçar. Dirijamo-nos ao diretório do servidor e executamos, sucessivamente:

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

E, desta vez, o servidor de eco (o cliente ainda não) inicia corretamente. Agora pode fazer a seguinte experiência:

  • encerrar o servidor de eco e, em seguida, o serviço de diretório
  • reinicie o serviço de diretório estando num diretório diferente do do servidor
  • volte ao diretório do servidor inicie o servidor de eco — obtém 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

Conclui-se que o diretório a partir do qual o serviço de diretório é iniciado é importante. Neste caso, o Java não encontrou a classe 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, é possível especificar em que diretório 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 java.rmi.server.codebase serve para indicar o URL do diretório que contém as classes necessárias ao servidor. Neste caso, este URL especifica o protocolo «file», que é o protocolo de acesso aos ficheiros locais, e o diretório que contém os ficheiros .class do servidor. Portanto, se procedermos da seguinte forma:

  • parar o serviço de diretório
  • reiniciar o serviço de diretório a partir de um diretório diferente do do servidor
  • no diretório do servidor, inicie-o 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 é então iniciado corretamente. Podemos, portanto, passar para o cliente. Testamo-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

Recebe-se o mesmo erro a indicar a ausência de um gestor de segurança. Pensa-se que talvez tenha havido um erro e que seja o cliente que deve criar o seu próprio gestor de segurança. Deixa-se o servidor com o seu gestor de segurança, mas cria-se também um para o cliente. A função main do cliente cltEcho.java passa então a ser:

public static void main(String arg[]){
        // sintaxe: cltEcho porta da máquina
        // máquina: máquina onde o servidor de eco está a funcionar
        // porta: porta em que o diretório de serviços opera na máquina do serviço de eco

        // verificação dos argumentos
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // instalação de um gestor de segurança
        System.setSecurityManager(new RMISecurityManager());

        // comunicação cliente-servidor
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

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

Em seguida, procede-se da seguinte forma:

  • recompila-se o cltEcho.java
  • transferimos 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
  • inicia-se 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 avançar: o erro já não é o mesmo. Verifica-se que o cliente conseguiu solicitar ao servidor a classe srvEcho_Stub.class, mas que este não a encontrou. Portanto, o cliente deve dispor de um gestor de segurança para 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/serveur/ e não foi encontrado. No entanto, é precisamente aí que se encontra. Se analisarmos mais detalhadamente a lista de métodos envolvidos no erro, encontramos este: sun.net.www.protocol.file.FileURLConnection.getInputStream. O cliente parece ter aberto um fluxo com um objeto do tipo FileURLConnection. Concluímos que tudo isto está 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. Ao consultar novamente a documentação, verifica-se que o valor desta palavra-chave, nos exemplos apresentados, é sempre: http://.., c.a.d. O protocolo utilizado é o HTTP. Não fica claro como é que o cliente solicita e obtém as suas classes junto do servidor. Talvez as solicite utilizando o URL da palavra-chave java.rmi.server.codebase, URL especificada ao iniciar o servidor. Decidimos, portanto, iniciar o servidor com 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. É necessário mover os ficheiros .class para um local acessível ao servidor http da máquina na qual as classes serão armazenadas. No nosso exemplo, o servidor está a funcionar numa máquina Windows com um servidor HTTP PWS da Microsoft. A raiz deste servidor é d:\Inetpub\wwwroot. Assim, procedemos da seguinte forma:

  • criamos o diretório d:\Inetpub\wwwroot\rmi\echo
  • colocamos nele os ficheiros .class do servidor, bem como o ficheiro mypolicy
  • iniciamos o servidor Web, caso ainda não o tenhamos feito
  • reinicia-se o serviço de diretório (rmiregistry)
  • reinicia-se 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! Funcionou. O cliente conseguiu recuperar o famoso ficheiro srvEcho_Stub.class.

Tudo isto deu-nos algumas ideias, e perguntamo-nos se o cliente que se encontra na máquina Windows do servidor também funcionaria sem o ficheiro srvEcho_Stub.class. Passamos para o diretório do cliente, eliminamos o ficheiro srvEcho_Stub.class, caso lá se encontre, 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. Résumé

No lado do servidor Windows:

  • o servidor possui um gestor de segurança
  • 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

No lado do cliente (Linux ou Windows)

  • o cliente dispõe de 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 de eco no Linux, clientes no Windows e no Linux

Passamos agora o servidor para uma máquina Linux e testamos clientes Linux e Windows. O procedimento a seguir é o seguinte:

  • transferimos 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
  • uma vez que a classe srvEcho_Stub.class será solicitada pelos clientes, o diretório escolhido para as classes do servidor é um diretório acessível ao servidor HTTP da máquina Linux. Neste caso, a classe URL deste diretório é http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
  • o serviço de diretório é 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/serveur/

srvEcho &

É possível testar os clientes. Primeiro, o cliente Windows.

  • Vamos para o diretório do cliente no computador Windows
  • iniciamos 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

Testamos o 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

De notar que, para o cliente Linux que funciona na mesma máquina que o servidor de eco, não foi necessário especificar a 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 JDBC, vimos como gerir bases de dados relacionais. Nos exemplos apresentados, as aplicações e a base de dados utilizada encontravam-se na mesma máquina Windows. Propomos aqui criar um servidor RMI numa máquina Windows, que permita aos clientes remotos aceder às bases de dados públicas ODBC do computador onde o servidor se encontra.

Image

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

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

O servidor executa as consultas SQL do cliente e envia-lhe os resultados. Esta é a sua função essencial e é por isso que o designaremos como servidor SQL.

Aplicamos os diferentes passos vistos anteriormente com o servidor de eco.

9.3.2. Etapa 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. Utilizaremos a seguinte interface:

import java.rmi.*;

// a interface remota
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;
}

A função dos diferentes métodos é a seguinte:

Connect: o cliente liga-se a uma base de dados remota, indicando o pilote, o url, o JDBC, bem como a sua identidade id e a sua palavra-passe mdp para aceder a essa base de dados. O servidor devolve-lhe 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. Indica o carácter que deve separar os campos nos resultados que lhe são devolvidos. O servidor devolve um conjunto de cadeias de caracteres:

    100 n

para uma consulta de atualização da base de dados, sendo n o número de linhas atualizadas

    500 msg d’erreur

se a consulta tiver gerado um erro

    501 Pas de résultats

se a consulta não tiver gerado nenhum resultado

    101 ligne1
    101 ligne2
    101 ...

se a consulta tiver gerado resultados. As linhas assim devolvidas pelo servidor são as linhas de 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. Etapa 2: Código do servidor

Segue-se o código-fonte Java do servidor SQL. Para o compreender, é necessário ter assimilado a gestão de bases de dados JDBC, bem como a construção de servidores RMI. Os comentários do programa deverão facilitar a sua compreensão.

// pacotes importados
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

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

    // dados globais da classe
    private Connection DB;

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

    // --------------- ligação
    public String connect(String pilote, String url, String id,
        String mdp) throws RemoteException{

        // ligação à base de dados url através do controlador
        // identificação com o ID e a palavra-passe

        String resultat=null;            // resultado do método
        try{
            // carregamento do controlador
            Class.forName(pilote);
            // pedido de ligação
            DB=DriverManager.getConnection(url,id,mdp);
            // ok
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // erro
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // fim
        return resultat;
    }            

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

        // executa uma consulta SQL na base de dados DB
        // e coloca os resultados numa tabela de cadeias

        // dados necessários para a execução da consulta
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // criação do contentor da consulta
            S=DB.createStatement();
            // execução da consulta
            if (! S.execute(requete)){
                // consulta de atualização
                // é devolvido o número de linhas atualizadas
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // era uma consulta
            // recuperam-se os resultados
            RS=S.getResultSet();
            // número de campos do Resultset
            int nbChamps=RS.getMetaData().getColumnCount();
            // processa-se os resultados
            while(RS.next()){
                // criação da linha de resultados
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // adição ao vetor de resultados
                resultats.addElement(ligne);
            }// while
            // fim da análise dos resultados
            // liberamos os recursos
            RS.close();
            S.close();
            // retornam-se os resultados
            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){
            // erro
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- fechar
    public String close() throws RemoteException {
        // encerra a ligação à base de dados
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // retorno do resultado
        return resultat;
    }

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

        // gestor de segurança
        System.setSecurityManager(new RMISecurityManager());

        // inicialização do serviço
        srvSQL serveurSQL=null;
        try{
            // criação
            serveurSQL=new srvSQL();
            // registo
            Naming.rebind("srvSQL",serveurSQL);
            // acompanhamento
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // erro
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// main

}// classe

9.3.4. Código do cliente RMI

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

    urlserviceAnnuaire pilote urlBase id mdp separateur

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

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

urlBase: URL JDBC da base de dados a gerir

id: identidade do cliente ou null se não houver identidade

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

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

Eis um exemplo de parâmetros possíveis:

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

onde:

urlserviceAnnuaire    srvSQL

nome RMI do servidor SQL

pilote     sun.jdbc.odbc.JdbcOdbcDriver

o controlador habitual das bases de dados com interface ODBC

urlBase    jdbc:odbc:articles

para utilizar uma base de dados de artigos declarada na lista de bases de dados públicas ODBC do computador Windows

id    null

sem identificação

mdp    null

sem palavra-passe

separateur    , 

os campos dos resultados serão separados por uma vírgula

Depois de iniciado com os parâmetros anteriores, o cliente segue os seguintes passos:

  • liga-se ao servidor RMI srvSQL, ou seja, um servidor RMI na mesma máquina que o cliente
  • solicita a ligação à base de dados de artigos
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • pede ao utilizador que introduza uma consulta SQL através do teclado
  • envia-a para o servidor SQL
executeSQL(requete, ’’,’’);
  • exibe no ecrã os resultados devolvidos pelo servidor
  • solicita novamente ao utilizador que introduza uma consulta SQL através do teclado. O programa terminará quando a consulta terminar.

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

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

public class cltSQL {

    // dados globais da classe
    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[]){
        // sintaxe: cltSQL urlServiceAnnuaire separador, driver, URL, ID, palavra-passe
        // urlServiceAnnuaire: URL do diretório de serviços RMI a contactar
        // driver: driver a utilizar para a base de dados a explorar
        // urlBase: URL JDBC da base de dados a utilizar
        // id: identificação do utilizador
        // mdp: a sua palavra-passe
        // separador: cadeia de caracteres que separa os campos nos resultados de uma consulta

        // verificação do número de argumentos
        if(arg.length!=6)
            erreur(syntaxe,1);

        // inicialização dos parâmetros de ligação à base de dados
        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];        

        // instalação de um gestor de segurança
        System.setSecurityManager(new RMISecurityManager());

        // comunicação cliente-servidor
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // abertura do fluxo do teclado
            in=new BufferedReader(new InputStreamReader(System.in));
            // acompanhamento
            System.out.println("--> Connexion au serveur RMI en cours...");
            // localização do serviço
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // acompanhamento
            System.out.println("--> Connexion à la base de données en cours");
            // pedido de ligação inicial à base de dados
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // acompanhamento
            System.out.println("<-- "+reponse);
            // análise da resposta
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // ciclo de leitura das solicitações a enviar ao servidor SQL
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // envio da solicitação ao servidor e receção da resposta
                lignes=serveurSQL.executeSQL(requete,separateur);
                // acompanhamento
                afficheLignes(lignes);
                // próxima solicitação
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// enquanto
            // acompanhamento
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // encerrar a ligação
            reponse=serveurSQL.close();
            // seguimento
            System.out.println("<-- " + reponse);
            // fim
            System.exit(0);
        // gestão de erros        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// tentar
    }// main

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

    // ------------ erro
    private static void erreur(String msg, int exitCode){
        // exibição da mensagem de erro
        System.err.println(msg);
        // eventual libertação de recursos
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // sair
        System.exit(exitCode);
    }// erro

}// classe    

9.3.5. Etapa 3: criação dos 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
  • Os ficheiros interSQL.class, srvSQL_Stub.class e srvSQL_Skel.class são transferidos 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
  • compilamos 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: Testes com servidor e cliente na mesma máquina Windows

  • O serviço de diretório é iniciado num diretório diferente do do servidor e do cliente
F:\>start j:\jdk12\bin\rmiregistry
  • Coloca-se o seguinte ficheiro «mypolicy» nos diretórios do cliente e do servidor
grant {
    // Permitir tudo por enquanto
    permission java.security.AllPermission;
};
  • inicia-se 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
  • inicia-se 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 ,

--> Connexion au serveur RMI en cours...
--> Connexion à la base de données en cours
<-- 200 Connexion réussie
--> Consulta: select nome, stock_actu from artigos 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
--> Consulta: update artigos set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Consulta: select nome, stock_actu from artigos 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. Passo 5: Testes com o servidor numa máquina Windows e o cliente numa máquina Linux

  • Se necessário, desligue o servidor e o serviço de diretório
  • transferimos os ficheiros .class do cliente para um computador 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
  • Reativamos o serviço de lista telefónica
F:\>start j:\jdk12\bin\rmiregistry
  • reiniciamos 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
  • inicia-se 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 ,

--> Consulta: select nome,stock_actu,stock_mini from artigos order by nome
<-- 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
--> Consulta: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Consulta: selecionar nome, stock_actu, stock_mini dos artigos, ordenados por nome
<-- 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. Conclusão

Temos aqui uma aplicação interessante, na medida em que permite o acesso a uma base de dados a partir de qualquer computador da rede. Teríamos muito bem podido escrevê-la de forma tradicional com sockets, o que, aliás, é solicitado num exercício do capítulo sobre bases de dados. Se escrevêssemos esta aplicação de 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 do tipo:
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 à base de dados a utilizar

O servidor poderia responder com algo do género:

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 carácter que separa os campos nas linhas da resposta.

O servidor poderá responder com algo do género

    100 n

para uma consulta de atualização da base de dados, sendo n o número de linhas atualizadas

    500 msg d’erreur

se a solicitação tiver gerado um erro

    501 Pas de résultats

se a consulta não tiver gerado nenhum resultado

    101 ligne1
    101 ligne2
    101 ...

se a consulta tiver gerado resultados. As linhas assim devolvidas pelo servidor são as linhas de resultados da consulta.

client : close

para encerrar a ligação com a base de dados remota. O servidor poderá 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)

Vemos aqui que, se for possível construir uma aplicação tradicional com um protocolo do tipo anterior, podemos deduzir uma possível estrutura do servidor RMI. Nos casos em que, no protocolo, temos uma frase do cliente para o servidor do tipo:

    commande param1 param2 ... paramq

poderemos ter, no servidor RMI, um método

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

e esse método, acessível ao cliente, deverá, portanto, fazer parte da interface publicada do servidor.

Para concluir, note-se que o nosso servidor, atualmente, só gere um cliente: não consegue gerir vários, tal como está escrito neste momento. Com efeito, se um cliente se ligar a uma base de dados B1, o servidor cria um objeto Connection DB=DB1. Se um segundo cliente solicitar uma ligação a uma base de dados B2, o servidor regista-o criando 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 gerir vários clientes.

9.4.2. Exercício 2

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