Breve Introdução ao RMI - Remote Method Invocation


[Menu]

1. Introdução

O RMI (Remote Method Invocation) é uma tecnologia suportada pela plataforma Java que facilita o desenvolvimento de aplicações distribuídas. Como o próprio nome indica, o RMI permite ao programador invocar  métodos de objectos remotos, ou seja que estão alojados em máquinas virtuais Java distintas, duma forma muito semelhante às invocações a objectos locais. De certa forma, à custa de algum esforço adicional de engenharia de software, o programador pode desenvolver aplicações totalmente distribuídas como se de aplicações locais se tratassem, sendo quase toda a comunicação entre máquinas virtuais Java assegurada transparentemente pelo próprio RMI.
Evidentemente, há algumas diferenças fundamentais entre uma aplicação distribuída e uma aplicação não-distribuída. A principal é a de que numa aplicação distribuída há uma infra-estrutura de comunicação subjacente que impõe um conjunto de condicionalismos à própria aplicação: risco de falhas de comunicação, latências variáveis, limites de banda, falha de servidores, etc... O RMI permite lidar com facilmente algumas destas contingências embora, como iremos ver futuramente, não com todas. Por outro lado, um ambiente distribuído significa normalmente (embora não necessariamente) que existe um suporte heterogéneo de hardware e de plataformas (Unix, Linux, Windows, etc.). Como é implementado usando a tecnologia Java, o RMI torna esta heterogeneidade menos preocupante, resultando numa boa solução global para o programador no desenvolvimento de aplicações distribuídas.

2. Conceitos Básicos

Uma noção central ao RMI é o da separação entre interface e implementação de uma classe, que é aliás compatível com a filosofia OO. A particularidade mais relevante desta separação é que o RMI permite que a interface e a respectiva implementação se localizem em JVM's diferentes. O RMI torna possível que uma determinada aplicação cliente adquira uma interface (que define o comportamento) referente a uma classe (que contém a implementação) que corre numa JVM diferente. Relembre-se que uma interface em Java não possui código de implementação o que significa que neste caso toda a computação subjacente à interface corre numa JVM distinta. A comunicação entre interface e a implementação é assegurada pelo RMI recorrendo a TCP/IP. A próxima figura tenta ilustrar este processo em alto nível:

Arquitectura de Alto Nível RMI
Figura 1

3. As Camadas RMI

O sistema RMI esconde do programador um grande conjunto de operações e recursos que estão involvidos no processo de invocação. Decompondo o sistema RMI é possível identificar 3 camadas que vamos listar por ordem de proximidade ao programador (i.e., de cima para baixo relativamente à figura 1):
  1. a camada de stub/skeleton, responsável por receber as chamadas da aplicação cliente feitas à interface e por reencaminhá-las para o objecto remoto
  2. a camada de Referências Remotas (Remote Reference Layer), que lida com a gestão e com a interpretação das referências remotas. 
  3. a camada de Transporte, que assegura a ligação entre as máquinas virtuais através de TCP/IP.
 Camadas RMI
Figura 2

3.1 Camada Stubs e Skeletons

A camada mais próxima do programador, ou seja da aplicação cliente e do objecto remoto, é a camada Stubs/Skeletons. Os Stubs são classes usadas do lado da aplicação cliente e funcionam como Proxies entre a aplicação cliente e o objecto remoto. Os Stubs recebem os parâmetros dos métodos exportados pelo objecto remota (definidas pela interface da classe remota) e reencaminham-nos para o lado do servidor onde serão interpretados por uma instância de uma classe Skeleton. O Skeleton recebe os parâmetros enviados pelo Stub e executa as respectivas chamadas no objecto remoto. Em sentido inverso, os Skeletons são também responsáveis por receber o valor de retorno do método remoto (local na sua perspectiva) e direcioná-los para os Stubs dos clientes correspondentes.

3.2 Camada de Referências Remotas

Esta camada mantém as referência entre os clientes e os objectos remotos e estabelece a semântica da ligação RMI. As referências mantidas referem-se a ligação unicast, ou seja um proxy para um objecto remoto (i.e. um Stub para um Skeleton). Esta camada funciona como um router entre o cliente e os (eventuais) vários objectos remotos.
Por enquanto, o RMI não suporta directamente outras semânticas de comunicação como por exemplo multi-cast. Numa situação de multi-cast, um único proxy poderia chamar em simultâneo várias instâncias de objectos remotos (localizadas eventualmente em diferentes computadores) e esperar pela primeira resposta retornada.

3.3 Camada de Transporte

Esta camada lida directamente com a comunicação entre as várias JVM's, usando TCP/IP. É importante referir que mesmo que as JVM's sejam executadas no mesmo computador, o RMI recorre sempre à comunicação TCP/IP. Isto significa que é sempre é necessário possuir uma interface de rede funcional para se poder utilizar RMI (mesmo tendo a aplicação cliente e a aplicação servidora a executar no mesmo computador).
Sobre a pilha TCP/IP o RMI possui um protocolo denominado JRMP, Java Remote Method Protocol e que permite ultrapassar alguns obstáculos que podem surgir na comunicação de rede via TCP/IP. Por exemplo, o JRMP permite multiplexar várias ligações TCP/IP numa única ligação TCP/IP ultrapassando imposições de utilização de apenas uma ligação em alguns ambientes (ex.: certos browsers a correr Applets RMI)

4. Serviço de Registo / Naming

Para que seja possível a uma aplicação cliente a utilização de um determinado serviço remoto é necessário que o esse mesmo serviço seja de alguma forma encontrado na rede. O RMI utiliza um sistema relativamente simples para isso, recorrendo a um Serviço de Registo para esse efeito. Todas as aplicações servidores que desejem tornar alguns dos seus objectos acessíveis remotamente deverão registar esses mesmoa objectoa (que implementa uma determinada interface pública) no serviço de registo com um nome conhecido das aplicações clientes.
O Serviço de Registo deverá estar disponível numa localização  preestabelecida (i.e. endereço da máquina e porta de rede) de forma ser conhecido por todos os eventuais clientes. Qualquer aplicação cliente pretenda aceder a um determinado objecto remoto (já registado) terá de consultar um ou mais Serviços de Registos em busca do nome com o qual o serviço remoto foi registado. No caso de esse serviço (i.e. nome) ser encontrado o Serviço de Registos retorna um objecto Stub através do qual se torna possível chamar os métodos do objecto remoto.
O RMI inclui no conjunto de aplicações que lhe está associado uma implementação simples de um Serviço de Registos, o rmiregistry, que corre na porta 1099 de todas as máquinas que disponibilizam serviços remotos. Na aplicação exemplo que se segue iremos utilizar o rmiregistry como Serviço de Registos

5. Um primeiro exemplo

Vamos agora exemplificar o uso de RMI criando um servidor remoto de operações aritméticas e a correspondente aplicação cliente.  
De uma forma muito resumida, os principais passos para a criação de uma aplicação RMI são os seguinte:
  1. Definição das Interface do objecto remoto
  2. Implementação da classe do objecto remoto
  3. Compilação do código fonte utilizando javac
  4. Criação do Stub e do Skeleton a partir das classes java anteriormente compiladas recorrendo ao rmic (RMI Compiler)
  5. Criação de Aplicação Servidora que instancia e regista o objecto remoto num Serviço de Registos
  6. Criação da Aplicação Cliente que consulta o Serviço de Registos e recolhe Stub de acesso ao objecto remoto
Para poder correr a aplicação RMI é, no caso mais simples, necessário:
  1. garantir que a interface de rede dos computadores que alojam as JVM está funcional.
  2. garantir que o Serviço de Registos (ex.: rmiregistry na porta 1099) está a correr nas máquinas que alojam objectos remotos
  3. garantir que o Serviço de Registos é acessível às máquinas onde correm as aplicações clientes.
  4. garantir que aplicação que exporta os serviços está a correr!...

5.1 Definição da interface do objecto remoto

O objecto remoto que vamos construir irá ser capaz de realizar 4 operações aritméticas básicas. Iremos começar por criar um serviço que seja capaz de realizar as seguintes 4 operações: adição, subtracção, multiplicação e divisão. As interfaces RMI a desenvolver deverão estender a classes java.rmi.remote e cada método declarado tem de indicar o envio excepções do tipo RemoteException. As excepções do tipo RemoteException estão relacionadas com todo o tipo de problemas que poderão surgir durante a invocação dos métodos remotos: sempre que algum desses problemas surja, e há vários, o cliente recebe do sistema RMI uma dessas excepções.
A interface terá então o seguinte código, onde se dclaram 4 operações:

import java.rmi.*;

public interface InterfaceServidorMat extends Remote
{
    public double soma(double a, double b) throws RemoteException;
    public double subtrai(double a, double b) throws RemoteException;
    public double multiplica(double a, double b) throws RemoteException;
   public double divide(double a, double b) throws RemoteException;
}

5.2 Implementação da classe do objecto remoto

A implementação da classe do serviço remoto possui como única particularidade o facto de estender a classe UnicastRemoteObject que realiza a ligação com o sistema RMI. Como consequência, a instanciação da classe poderá também originar problemas de RMI pelo que o construtor da classe terá necessariamente de declarar o envio de excepções do tipo RemoteException. Para além destas características, a classe decorre muito naturalmente da interface previamente definida (nota: tal como qualquer classe em Java, a classe do serviço remoto poderá ter métodos não declarados na interface).
O código da classe é o seguinte:
import java.rmi.*;
import java.rmi.server.*;
public class ServidorMat extends UnicastRemoteObject implements InterfaceServidorMat
{
    public ServidorMat() throws RemoteException
    {
        System.out.println("Novo Servidor instanciado...");
    }
    public double soma(double a, double b) throws RemoteException
    {
        return a+b;
    }
    public double subtrai(double a, double b) throws RemoteException
    {
        return a-b;
    }
    public double multiplica(double a, double b) throws RemoteException
{
        return a*b;
    }
    public double divide(double a, double b) throws RemoteException
    {
        return a/b;
    }
} 

5.3 Compilação do código fonte utilizando javac

Este passo é simples e consiste na compilação das duas fontes Java usando o compilador de Java javac. A partir da linha de comando:

javac *.java

Não deverão surgir quaisquer erros.

5.4 Criação do Stub e do Skeleton

O próximo passo é exclusivo do RMI e consiste na geração automática das classes de Stub e de Skeleton usando um compilador dedicado denominado rmic. O rmic gera Stubs e Skeletons a partir da classe remota compilada e não a partir do seu código fonte pelo que este passo só poderá se executado depois da compilação do referido código fonte. A partir da linha de comando corre-se:

rmic ServidorMat

Deverão ter sido criados dois novos ficheiro:

ServidorMat_Stub.class
ServidorMat_Skel.class

Sempre que a interface do serviço remoto for alterada deverão ser gerados novos ficheiros Stub e Skeleton.

5.5 Criação de Aplicação Servidora

Como vimos anteriormente, para que um determinado objecto remoto fique disponível aos clientes é necessário que este seja criado e registado no sistema RMI. Assim, vamos criar uma pequena aplicação servidora com o único objectivo de construir um objecto remoto da classe ServidorMat e registá-lo no Serviço de Registos do sistema RMI sob um determinado nome. O registo é feito recorrendo ao método estático RMI Naming.rebind() que recebe dois parâmetros: (i) o nome pelo qual o objecto remoto deverá ficar conhecido e (ii) a referência do próprio objecto remoto. A partir do momento em que o registo é efectuado com sucesso, o objecto encontra-se disponível para acesso remoto.
O código fonte é o seguinte:

import java.rmi.*;

public class ArrancaServidor
{
    public static void main(String argv[])
    {
        try
        {
            System.out.println("Arrancando servidor...");
            Naming.rebind("ServidorMat_1", new ServidorMat());
        }
        catch (Exception e)
        {
            System.out.println("Ocorreu um problema no arranque do servidor.\n"+e.toString());
        }
    }
}
Mais uma vez, o facto de se estar a lidar com RMI arrasta consigo a possibilidade da ocorrência de alguns problemas pelo que é possivel receber vários tipos de excepções que deverão ser convenientemente tratadas.
Não esquecer a compilação do código fonte usando o javac.

5.6 Criação da Aplicação Cliente

A aplicação cliente que vamos criar calcula a área e o perímetro de um rectângulo de lados A e B recorrendo às primitivas aritméticas disponibilizadas pelo objecto remoto ServidorMat. Para poder invocar os métodos remotos do objecto ServidorMat, o cliente deverá primeiro consultar o Serviço de Registo do RMI de modo a obter o stub para o objecto remoto. Isso é feito recorrendo ao método estático Naming.lookup(), que executa uma pesquisa num Serviço de Registos. O objecto remoto é identificado por um URL RMI que tem a seguinte forma:

rmi://<Servidor onde Corre o Serviço de Registos>[:<porta de rede (opcional)>]/<nome do serviço remoto>

Neste caso, iremos assumir que o Serviço de Registos é o rmiregistry (porta predefinida 1099) corre na máquina local pelo que o URL será simplesmente:

rmi://127.0.0.1/ServidorMat_1

import java.rmi.*;


public class Cliente
{
public Cliente()
{

System.out.println("Arrancando o Cliente...");
// Vamos tentar ir aceder ao Servidor de Registos para recolher a interface
try
{
msi = (InterfaceServidorMat) Naming.lookup("rmi://127.0.0.1/ServidorMat_1");
}
catch (Exception e)
{
System.out.println("Falhou o arranque do Cliente.\n"+e);
System.out.println("Certifique-se que tanto o Servidor de Registos como a Aplicação Servidora estão a correr correctamente.\n");
System.exit(0);
}

}


public double area(double a, double b) throws RemoteException
{
 return msi.multiplica(a,b);
}

public double perimetro(double a, double b) throws RemoteException
{
double metade = msi.soma(a,b);
return msi.multiplica(2.0,metade);
}

public static void main (String[] argv)
{
Cliente c = new Cliente();
try
{
System.out.println("Area: " + c.area(20.0,40.0));
System.out.println("Perimetro: " + c.perimetro(20.0,40.0));
}
catch (Exception e)
{
System.out.println("Excepção durante chamadas remotas:" +e);
}
}

private InterfaceServidorMat msi; // A interface para o objecto remoto
}

Não esquecer a compilação do código fonte usando o javac.

5.7 Executar os Programas de Exemplo

Em primeiro lugar é necessário verificar que todas as fontes foram correctamente compiladas. Podes correr da linha de comando:

javac *.java

Depois de tudo compilar devidamente é necessário assegurar que o Stub e o Skeleton foram devidamente gerados:

rmic ServidorMat

Deverão existir na directoria os dois ficheiros:

ServidorMat_Stub.class
ServidorMat_Skel.class

Em seguida é necessário arrancar o Servidor de Registos rmiregistry a partir da directoria onde se encontram as classes Java:

rmiregistry

Em seguida arranca-se a aplicação servidora:

java ArrancaServidor
Arrancando servidor...
Novo Servidor instanciado...

Finalmente e se tudo até aqui tiver corrido bem, lança-se a aplicação cliente:

java Cliente
Arrancando o Cliente...
Área: 800.0
Perímetro: 120.0

5.8 Alguns Problemas Frequentes

1. Ao arrancar o servidor obtive a seguinte excepção:

java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
        java.net.ConnectException: Connection refused: connect


Resposta: Provavelmente o Servidor de Registos não estava a correr. É necessário arrancar o rmiregistry a partir da directoria onde se encontram as classes Java.

2. Eu tenho o rmiregistry a correr, mas ainda assim ao arrancar o servidor porque obtive a seguinte excepção:

java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
        java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
        java.lang.ClassNotFoundException: ServidorMat_Stub

Resposta: O servidor está de facto a correr. Por isso a excepção é diferente da anterior. Neste caso é o próprio RMI que não encontra o Stub porque o rmiregistry foi colocado a correr a partir de uma directoria diferente daquela onde se encontra as classes java, incluindo o Stub.

3.  Ao arrancar o cliente obtive a seguinte excepção:

java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
        java.net.ConnectException: Connection refused: connect


Resposta: Provavelmente o Servidor de Registos não estava a correr. É necessário arrancar o rmiregistry a partir da directoria onde se encontram as classes Java.

4.  Eu tenho o rmiregistry a correr mas ao arrancar o cliente obtive a seguinte excepção:


java.rmi.NotBoundException: ServidorMat_1

Resposta: O cliente consultou o Serviço de Registos mas recebeu uma excepção indicando que o serviço remoto (neste caso ServidorMat_1) não estava registado. A aplicação servidora provavelmente não estará a correr.

[Menu]

Luís Sarmento 2003