top of page
java distrib.jpg

5.1 Programação com objetos distribuídos

 

Na programação distribuída usando a arquitetura cliente-servidor, clientes e servidores podem ser implementados usando qualquer paradigma de programação. Assim, é possível que um serviço específico seja executado por um método de algum objeto. No entanto, mesmo que o cliente também tenha sido desenvolvido orientação a objetos, na comunicação entre o cliente e o servidor esse paradigma deve ser esquecido, devendo ser utilizado algum protocolo pré-estabelecido de troca de mensagens para a solicitação e resposta ao serviço.

Um sistema de objetos distribuídos é aquele que permite a operação com objetos remotos. Dessa forma é possível, a partir de uma aplicação cliente orientada a objetos, obter uma referência para um objeto que oferece o serviço desejado e, através dessa referência, invocar métodos desse objeto — mesmo que a instância desse objeto esteja em uma máquina diferente daquela do objeto cliente.

O conceito básico que suporta plataformas de objetos distribuídos é o conceito de arquiteturas de objetos. Essencialmente, uma arquitetura orientada a objetos estabelece as regras, diretrizes e convenções definindo como as aplicações podem se comunicar e inter-operar. Dessa forma, o foco da arquitetura não é em como a implementação é realizada, mas sim na infra-estrutura e na interface entre os componentes da arquitetura.

Na plataforma Java, dois mecanismos são oferecidos para o desenvolvimento de aplicações usan- do o conceito de objetos distribuídos: Java RMI e Java IDL. RMI (invocação remota de métodos) é um mecanismo para desenvolver aplicações com objetos distribuídos que opera exclusivamente com objetos Java. Java IDL utiliza a arquitetura padrão CORBA para integração de aplicações Java a aplicações desenvolvidas em outras linguagens.

 

5.1.1Arquiteturas de objetos distribuídos

No paradigma de arquiteturas de objetos, há três elementos principais. A arquitetura OO forne- ce uma descrição abstrata do software — que categorias de objetos serão utilizadas, como estarão particionados e como interagirão. As interfaces distribuídas são as descrições detalhadas das fun- cionalidades do software. Finalmente, a implementação é composta por módulos de software que suportam as funcionalidades especificadas nas interfaces distribuídas.

O uso de interfaces distribuídas permite isolar a arquitetura de um sistema de sua implementa- ção. Dessa forma, o sistema pode ser construído com um alto grau de independência em relação às implementações específicas de suas funcionalidades, ou seja, é possível substituir implementações específicas com pequeno impacto sobre o sistema como um todo.

A adoção do paradigma de arquitetura de objetos permite também atingir um alto grau de inter- operabilidade através da adoção de uma infra-estrutura padronizada de comunicação entre objetos através das interfaces. Assim, cada componente da arquitetura deve se preocupar apenas em como se dará sua comunicação com a infra-estrutura de comunicação, estabelecida através de um objeto wrapper. Sem essa padronização, seria necessário estabelecer os mecanismos de comunicação com todos os demais componentes do sistema.

Em uma arquitetura de objetos, alguma forma deve ser estabelecida para que clientes possam localizar serviços que estão sendo oferecidos. Isso é usualmente oferecido na forma de um serviço básico da plataforma de objetos distribuídos. Do ponto de vista de quem oferece o serviço, é preciso habilitar o objeto para que seus métodos possam ser invocados remotamente. Isto é realizado através das seguintes atividades:

 

Descrever o serviço. Na arquitetura de objetos, a descrição ou especificação de serviços é determi- nada através das interfaces. Em Java, isto é realizado através da especificação oferecida por uma interface.

Implementar o serviço. Isto é realizado através do desenvolvimento de uma classe Java que imple- mente a interface especificada.

Anunciar o serviço. Quando um objeto que implementa o serviço torna-se ativo, é preciso que ele seja registrado em um “diretório de serviços” de forma que potenciais clientes possam localizá- lo.

Sob o ponto de vista do objeto cliente, que vai usar o serviço do objeto remoto, é preciso localizar o serviço, o que é feito acessando o registro de serviços. Portanto, é necessário que servidores e clientes estejam de acordo com o local (a máquina) onde o registro é realizado. Como resultado dessa tarefa, obtém-se uma referência ao objeto remoto que pode ser utilizada como se fosse uma referência para o objeto local.

A utilização desse mecanismo de localizar o serviço através de um diretório ou registro permite que as máquinas clientes ignorem totalmente em que máquinas os serviços solicitados estão operando

— uma facilidade conhecida como transparência de localização.

Um dos principais objetivos em uma plataforma de objetos distribuídos é atingir transparência de localização, tornando uniforme a forma de utilização de objetos independentemente desses objetos estarem na máquina local da aplicação ou em máquinas distintas.

A fim de que se atinja transparência de localização, as seguintes funcionalidades devem ser ofe- recidas:

  1. Localizar e carregar classes remotas;

  2. Localizar e obter referências a objetos remotos; e

  3. Habilitar a invocação de métodos de objetos remotos.

A primeira funcionalidade, não muito diferente do que ocorre em sistemas com objetos locais, é necessária para que a aplicação conheça as facilidades oferecidas pelo objeto remoto.

Referências a objetos também são utilizadas em sistemas com objetos locais, porém com diferen- ças significativas. Em um sistema local, as referências a objetos são tipicamente manipuladores com especificação de endereços de memória. No caso de objetos remotos, esses endereços da memória de outra máquina não têm validade na máquina local. Assim, é preciso oferecer mecanismos que traduzam essas referências entre máquinas de forma transparente para o programador.

Para a invocação de métodos de um objeto remoto, além da necessidade de se localizar a refe- rência ao método é preciso oferecer mecanismos para tornar transparente a passagem de argumentos para e o retorno de valores desde o método.

Além dessas funcionalidades, a comunicação de falhas no oferecimento da transparência de loca- lização ao programador é essencial. Assim, funcionalidades para comunicar exceções entre máquinas também dever ser suportadas pela plataforma de objetos distribuídos.

 

5.1.2Java RMI

RMI (Remote Method Invocation) é uma das abordagens da tecnologia Java para prover as funci- onalidades de uma plataforma de objetos distribuídos. Esse sistema de objetos distribuídos faz parte do núcleo básico de Java desde a versão JDK 1.1, com sua API sendo especificada através do pacote java.rmi e seus subpacotes.

Através da utilização da arquitetura RMI, é possível que um objeto ativo em uma máquina virtual

Java possa interagir com objetos de outras máquinas virtuais Java, independentemente da localização dessas máquinas virtuais.

A arquitetura RMI oferece a transparência de localização através da organização de três camadas entre os objetos cliente e servidor:

  1. A camada de stub/skeleton oferece as interfaces que os objetos da aplicação usam para interagir entre si;

  2. A camada de referência remota é o middleware entre a camada de stub/skeleton e o protocolo de transporte. É nesta camada que são criadas e gerenciadas as referências remotas aos objetos;

  3. A camada do protocolo de transporte oferece o protocolo de dados binários que envia as soli- citações aos objetos remotos pela rede.

 

Desenvolvimento da aplicação RMI

No desenvolvimento de uma aplicação cliente-servidor usando Java RMI, como para qualquer plataforma de objetos distribuídos, é essencial que seja definida a interface de serviços que serão oferecidos pelo objeto servidor.

A especificação de uma interface remota é equivalente à definição de qualquer interface em Java, a não ser pelos seguintes detalhes: a interface deverá, direta ou indiretamente, estender a interface Remote; e todo método da interface deverá declarar que a exceção RemoteException (ou uma de suas superclasses) pode ser gerada na execução do método.

Esse exemplo ilustra a definição de uma interface remota para um objeto que contém um contador inteiro:

  1. import java.rmi.*;

  2. public interface Count extends Remote {

  3. void set(int val) throws RemoteException;

  4. void reset() throws RemoteException;

  5. int get() throws RemoteException;

  6. int increment() throws RemoteException;

7       }

Esse contador é manipulado por quatro métodos: set(), para definir um valor inicial para o conta- dor; reset(), para reiniciar o contador com o valor 0; get(), para consultar o valor do contador sem alterá-lo; e increment(), que lê o valor atual do contador e incrementa-o.

Os serviços especificados pela interface RMI deverão ser implementados através de uma clas- se Java. Nessa implementação dos serviços é preciso indicar que objetos dessa classe poderão ser acessados remotamente.

 

A implementação do serviço se dá através da definição de uma classe que implementa a inter- face especificada. No entanto, além de implementar a interface especificada, é preciso incluir as funcionalidades para que um objeto dessa classe possa ser acessado remotamente como um servidor. A implementação da interface remota se dá da mesma forma que para qualquer classe imple- mentando uma interface Java, ou seja, a classe fornece implementação para cada um dos métodos

especificados na interface.

As funcionalidades de um servidor remoto são especificadas na classe abstrata RemoteServer, do pacote java.rmi.server. Um objeto servidor RMI deverá estender essa classe ou, mais especificamente, uma de suas subclasses. Uma subclasse concreta de RemoteServer oferecida no mesmo pacote é UnicastRemoteObject, que permite representar um objeto que tem uma única implementação em um servidor (ou seja, não é replicado em vários servidores) e mantém uma conexão ponto-a-ponto com cada cliente que o referencia.

Tipicamente, a declaração de uma classe que implementa um servidor remoto RMI terá a forma

 

public class ... extends UnicastRemoteObject implements ... {

...

}

Esse exemplo oferece uma possível implementação para a interface remota previamente especi- ficada:

 

1

import java.rmi.*;

 

2

import java.rmi.server.UnicastRemoteObject;

3

public class CountImpl extends UnicastRemoteObject

4

implements Count {

5

private int sum;

6

public CountImpl() throws RemoteException  {

7

}

8

public  void set(int val) throws RemoteException

{

9

sum = val;

 

10

}

 

11

public  void reset() throws RemoteException {

 

12

sum = 0;

 

13

}

 

14

public int get() throws RemoteException {

 

15

return sum;

 

16

}

 

17

public int increment() throws RemoteException {

 

18

return sum++;

 

19

}

 

20

}

 

 

Clientes e servidores RMI

Uma vez que a interface remota esteja definida e a classe que implementa o serviço remoto tenha sido criada, o próximo passo no desenvolvimento da aplicação distribuída é desenvolver o servidor

 

RMI, uma classe que crie o objeto que implementa o serviço e cadastre esse serviço na plataforma de objetos distribuídos.

Um objeto servidor RMI simples deve realizar as seguintes tarefas: criar uma instância do objeto que implementa o serviço; e disponibilizar o serviço através do mecanismo de registro.

O desenvolvimento de um cliente RMI requer essencialmente a obtenção de uma referência remo- ta para o objeto que implementa o serviço, o que ocorre através do cadastro realizado pelo servidor. Uma vez obtida essa referência, a operação com o objeto remoto é indistingüível da operação com um objeto local.

 

Usando o serviço de nomes

O aplicativo rmiregistry faz parte da distribuição básica de Java. Tipicamente, esse aplica- tivo é executado como um processo de fundo (em background) que fica aguardando solicitações em uma porta, que pode ser especificada como argumento na linha de comando. Se nenhum argumento for especificado, a porta 1099 é usada como padrão.

O aplicativo rmiregistry é uma implementação de um serviço de nomes para RMI. O serviço de nomes é uma espécie de diretório, onde cada serviço disponibilizado na plataforma é registrado através de um nome do serviço, uma string única para cada objeto que implementa serviços em RMI. Para ter acesso ao serviço de nomes a partir de uma classe Java, são oferecidos dois mecanismos básicos. O primeiro utiliza a classe Naming, do pacote java.rmi. O segundo mecanismo utiliza

as facilidades oferecidas através das classes no pacote java.rmi.registry.

A classe Naming permite a realização da busca de um serviço pelo nome (lookup) usando o método estático lookup(String nome), que retorna uma referência para o objeto remoto. O serviço de registro aonde a busca se realiza é especificado pela string usando uma sintaxe similar à URL:

rmi://objreg.host:port/objname

O protocolo padrão é rmi, sendo no momento o único suportado através desse método. Se não especificado, o host é a máquina local e a porta é 1099. O nome de registro do objeto é a única parte obrigatória desse argumento.

Além de lookup() os métodos bind(), rebind(), unbind() e list(), descritos na seqüência, são também suportados.

Outra alternativa para ter acesso ao serviço de nomes a partir da aplicação Java é utilizar as funcionalidades do pacote java.rmi.registry, que oferece uma classe e uma interface para que classes Java tenham acesso ao serviço de nomes RMI.

A interface Registry representa uma interface para o registro de objetos RMI operando em uma máquina específica. Através de um objeto dessa classe, é possível invocar o método bind() que associa um nome de serviço (um String) ao objeto que o implementa.

Para obter uma referência para um objeto Registry são utilizados os métodos da classe Lo- cateRegistry, todos estáticos, tais como getRegistry(). Há quatro versões básicas desse método:

  1. getRegistry(): obtém referência para o registro local operando na porta default;

  2. getRegistry(int port): obtém referência para o registro local operando na porta es- pecificada;

 

  1. getRegistry(String host): obtém referência para o registro remoto operando na por- ta default;

  2. getRegistry(String host, int port): obtém referência para o registro remoto operando na porta especificada.

O método estático createRegistry(int port) pode ser utilizado para iniciar um servi- ço de registro na máquina virtual Java corrente na porta especificada como argumento, retornando também um objeto da classe Registry.

Inicialmente, é preciso obter uma referência para o serviço de registro, através da invocação do método:

Registry r = LocateRegistry.getRegistry();

Observe que a referência para Registry é em si uma referência para um objeto remoto, uma vez que a interface Registry é uma extensão da interface Remote.

Uma vez que a referência para o serviço de registro tenha sido obtida, é possível acessar as funcionalidades desse serviço através dos métodos da interface Registry. Particularmente, para registrar um novo serviço utiliza-se o método bind():

r.bind(serviceName, myCount);

O objeto que está sendo registrado deve implementar também a interface Remote, que identifica todos os objetos que podem ser acesados remotamente.

Outros serviços disponíveis através dos métodos de Registry incluem atualização, remoção e busca dos serviços lá registrados. Para atualizar um registro já existente, o método rebind() pode ser utilizado. Para eliminar um registro, utiliza-se o método unbind(). Dado o nome de um serviço, o objeto Remote que o implementa pode ser obtido pelo método lookup(). O método list() retorna um arranjo de String com os nomes de todos os serviços registrados.

 

Implementação do servidor RMI

Como observado, um objeto servidor RMI simples deve realizar as seguintes tarefas:

  1. Criar uma instância do objeto que implementa o serviço; e

  2. Disponibilizar o serviço através do mecanismo de registro.

Esse exemplo de servidor RMI para o contador remoto cria uma instância da implementação do serviço e coloca-a à disposição de potenciais clientes, registrando-o no registry RMI:

  1. import java.rmi.registry.*;

  2. public class CountServer {

  3. public static void main(String[] args) {

  4. try {

  5. String serviceName = "Count001";

  6. CountImpl myCount = new CountImpl();

  7. Registry r = LocateRegistry.getRegistry();

 

  1. r.bind(serviceName, myCount);

  2. System.out.println("Count Server ready.");

10                   }

  1. catch (Exception e) {

  2. System.out.println("Exception: " + e.getMessage());

  3. e.printStackTrace();

14                   }

15             }

16       }

 

Cliente RMI

A principal etapa no desenvolvimento de uma aplicação cliente RMI é a obtenção da referência remota para o objeto (remoto) que implementa o serviço desejado. Para tanto, o cliente RMI usa o serviço padrão oferecido pelo mecanismo de registro de nomes de serviços.

Uma vez que a referência remota seja obtida, ela pode ser convertida (downcast) para uma refe- rência para a interface que especifica o serviço. A partir de então, os métodos oferecidos pelo serviço remoto são invocados da mesma forma que ocorre para objetos locais.

Esses exemplos ilustram o desenvolvimento de código cliente em RMI. No primeiro exemplo desenvolve-se um cliente RMI que simplesmente invoca o método reset() através de uma refe- rência remota para o objeto servidor:

  1. import java.rmi.registry.*;

  2. public class CountReset {

  3. public static void main(String args[]) {

  4. try {

  5. Registry r = LocateRegistry.getRegistry();

  6. Count myCount = (Count) r.lookup("Count001");

  7. myCount.reset();

8                   }

9                   catch(Exception e) {

10                         e.printStackTrace();

11                   }

12                   System.exit(0);

13             }

14       }

Nesse outro exemplo, o cliente utiliza os métodos para modificar e obter o valor do contador remoto. Ele também ilustra a interação de um código com o registro RMI através da classe Naming:

  1. import java.rmi.*;

  2. public class CountClient {

  3. public static void main(String args[]) {

  4. try {

  5. Remote remRef = Naming.lookup("Count001");

  6. Count myCount = (Count) remRef;

 

  1. int initValue = myCount.get();

  2. System.out.print("De " + initValue + " para ");

  3. long startTime = System.currentTimeMillis();

10                         for (int i = 0 ; i < 1000 ; i++ )

  1. myCount.increment();

  2. long stopTime = System.currentTimeMillis();

  3. System.out.println(myCount.get());

  4. System.out.println("Avg Ping = "

15                                                                                + ((stopTime - startTime)/1000f)

16                                                                                + " msecs");

17                   }

  1. catch(Exception e) {

  2. e.printStackTrace();

20                   }

21                   System.exit(0);

22             }

23       }

Esse terceiro exemplo ilustra a utilização de RMI a partir de um cliente desenvolvido como um applet. Nesse applet, um campo de texto mostra o valor do contador no objeto servidor. Dois botões são fornecidos, um para incrementar o valor mil vezes Start) e outro para obter o valor atual do contador Get):

  1. import java.rmi.*;

  2. import java.awt.*;

  3. import java.awt.event.*;

  4. import java.applet.*;

  5. public class AppletClient extends Applet

  6. implements ActionListener {

  7. Count remCount;

  8. TextField tfCnt;

  9. Button bStart, bGet;

  10. String bslabel = "Start";

  11. String bglabel = "Get";

  12. public void init() {

  13. try {

  14. setLayout(new GridLayout(2,2));

  15. add(new Label("Count:"));

  16. tfCnt = new TextField(7);

  17. tfCnt.setEditable(false);

  18. add(tfCnt);

  19. bStart = new Button(bslabel);

  20. bStart.addActionListener(this);

  21. bGet = new Button(bglabel);

  22. bGet.addActionListener(this);

 

  1. add(bStart);

  2. add(bGet);

  3. showStatus("Binding remote object");

  4. remCount = (Count) Naming.lookup("Count001");

  5. tfCnt.setText(Integer.toString(remCount.get()));

28                              }

  1. catch (Exception e) {

  2. e.printStackTrace();

31                              }

32                   }

  1. public void paint() {

  2. try {

  3. tfCnt.setText(Integer.toString(remCount.get()));

36                              }

  1. catch (Exception e) {

  2. e.printStackTrace();

39                              }

40                   }

  1. public void actionPerformed (ActionEvent ev) {

  2. try {

  3. String botao = ev.getActionCommand();

  4. if (botao.equals(bslabel)) {

  5. showStatus("Incrementing...");

46                                                      for (int i = 0 ; i < 1000 ; i++ )

  1. remCount.increment();

  2. showStatus("Done");

49                                          }

  1. else {

  2. showStatus("Current count");

  3. paint();

53                                          }

54                              }

  1. catch (Exception e) {

  2. e.printStackTrace();

57                              }

58                   }

59       }

 

Definindo stubs e skeletons

Para que um serviço oferecido por um objeto possa ser acessado remotamente através de RMI, é preciso também as classes auxiliares internas de stubs e skeletons, responsáveis pela comunicação entre o objeto cliente e o objeto que implementa o serviço, conforme descrito na apresentação da arquitetura RMI.

Uma vez que a interface e a classe do serviço tenham sido criadas e compiladas para byteco-

 

des usando um compilador Java convencional, é possível criar os correspondentes stubs e skeletons. Para tanto, utiliza-se o aplicativo compilador RMI, rmic, disponibilizado juntamente com o kit de desenvolvimento Java.

Um exemplo ilustra o processo de compilação RMI para o serviço do contador remoto. Considere a implementação do serviço que foi previamente definida. O primeiro passo para a criação do stub e do skeleton para esse serviço é obter a classe compilada, que por sua vez precisa da classe da interface:

> javac Count.java

> javac CountImpl.java

Com a classe CountImpl.class disponível, a execução do comando

> rmic CountImpl

gera as classes CountImpl_Stub.class e CountImpl_Skel.class, correspondendo res- pectivamente ao stub e ao skeleton para o serviço. O stub deverá ser disponibilizado junto ao código do cliente RMI, enquanto que o skeleton deverá estar disponível junto ao código do servidor.

Uma classe stub oferece implementações dos métodos do serviço remoto que são invocadas no lado do cliente. Internamente, esses métodos empacotam marshall) os argumentos para o método e os envia ao servidor. A implementação correspondente no lado servidor, no skeleton, desempacota (unmarshall) os dados e invoca o método do serviço. Obtido o valor de retorno do serviço, o método no skeleton empacota e envia esse valor para o método no stub, que ainda estava aguardando esse retorno. Obtido o valor de retorno no stub, esse é desempacotado e retornado à aplicação cliente como resultado da invocação remota.

Internamente, o processo de marshalling utiliza o mecanismo de serialização de Java. Assim, argumentos e valores de retorno de métodos remotos invocados através de RMI estão restritos a tipos primitivos de Java e a objetos de classes que implementam Serializable.

fabrica objetos

Usando fábricas de objetos remotos

Pode haver situações em que não seja interessante registrar cada implementação de um serviço no registry — por exemplo, quando o servidor não sabe quantos objetos criar de antemão ou quando a quantidade de pequenos serviços registrados e tão grande que pode tornar a busca por um serviço ineficiente. Nessas situações, pode ser interessante utilizar uma fábrica de objetos remotos. Nesse caso, o servidor que está registrado em rmiregistry não é uma implementação individual do serviço, mas sim um gerenciador de instâncias de implementação do serviço. Esse gerenciador deve implementar uma interface remota que permita que o cliente obtenha uma referência remota para o serviço desejado em duas etapas:

  1. obtendo a referência para o gerenciador através da invocação do método lookup(); e

  2. obtendo a referência para o serviço propriamente dito através da invocação do método do gerenciador que retorna a referência.

 

Esses exemplos usando contadores inteiros ilustram a utilização do conceito de fábrica de objetos remotos. Além da implementação do serviço e da sua correspondente interface, é preciso inicialmente definir uma interface para a fábrica. Nesse exemplo, essa interface especifica a funcionalidade de um “gerenciador de contadores”, que recebe o nome do contador e retorna uma referência remota para um objeto contador:

  1. import java.rmi.*;

  2. public interface CountManager extends Remote {

  3. Count getCount(String nome) throws RemoteException;

4       }

No lado servidor, o que muda em relação ao exemplo anterior é que agora não é mais o objeto que implementa o contador que deve ser cadastrado no registry, mas sim o objeto fábrica, uma implementação da interface especificada para o “gerenciador de contadores”. Essa fábrica, por sua vez, mantém um registro interno dos objetos criados para poder retornar as referências solicitadas pelos clientes remotos. Essa classe combina as funcionalidades da implementação de uma interface remota com aquelas de um servidor RMI:

1

import java.rmi.*;

2

import java.rmi.registry.*;

3

import java.rmi.server.*;

4

import java.util.*;

5

public class CManagerImpl extends UnicastRemoteObject

6

implements CountManager {

7

private Hashtable counters = new Hashtable();

8

public CManagerImpl() throws RemoteException {

9

}

10

public Count getCount(String nome) throws RemoteException

{

11

Count rem = null;

12

if (counters.containsKey(nome))

13

rem = (Count) counters.get(nome);

14

else {

15

rem = new CountImpl();

16

counters.put(nome,rem);

17

System.out.println("New counter: " + nome);

18

}

 19

return rem;

20

}

21

public static void main(String[] args) {

22

try {

23

String serviceName = "CountFactory";

24

CManagerImpl myCM = new CManagerImpl();

25

Registry r = LocateRegistry.getRegistry();

26

r.bind(serviceName, myCM);

27

System.out.println("CountFactory ready.");

28

}

29

catch (Exception e) {

30

e.printStackTrace();

31

}

32

}

33       }

 

No lado do cliente há referências agora a duas interfaces remotas, uma para o “gerenciador de contadores” e outra para o contador. A primeira delas é resolvida através do serviço de registro do RMI, enquanto que a referência para o objeto do segundo tipo de interface é obtido a partir dessa referência para o gerenciador que foi obtida:

  1. import java.rmi.*;

  2. public class CountClient {

  3. public static void main(String args[]) {

  4. String nome = "Count001";

  5. try {

  6. CountManager cm =

  7. (CountManager) Naming.lookup("CountFactory");

  8. if (args.length > 0)

  9. nome = args[0];

  10. Count myCount = cm.getCount(nome);

  11. int initValue = myCount.get();

  12. System.out.print("De " + initValue + " para ");

  13. long startTime = System.currentTimeMillis();

14                         for (int i = 0 ; i < 1000 ; i++ )

  1. myCount.increment();

  2. long stopTime = System.currentTimeMillis();

  3. System.out.println(myCount.get());

  4. System.out.println("Avg Ping = "

19                                                                                + ((stopTime - startTime)/1000f)

20                                                                                + " msecs");

21                   }

  1. catch(Exception e) {

  2. e.printStackTrace();

24                   }

25                   System.exit(0);

26             }

27       }

 

Execução com RMI

A execução da aplicação cliente-servidor em RMI requer, além da execução da aplicação cliente e da execução da aplicação servidor, a execução do serviço de registro de RMI. Além do princípio básico de execução de aplicações RMI, a arquitetura RMI oferece facilidades para operação com código disponibilizado de forma distribuída e ativação dinâmica, além de outros serviços distribuídos.

 

O registro RMI (rmiregistry) executa isoladamente em uma máquina virtual Java. O servidor da aplicação, assim como a implementação do serviço, estão executando em outra máquina virtual Java; sua interação com o registro (ao invocar o método bind()) se dá através de uma referência remota. Da mesm forma, cada aplicação cliente pode ser executada em sua própria máquina virtual Java; suas interações com o registro (método lookup()) e com a implementação do serviço (usando os correspondentes ub e skeleton) dão-se também através de referências remotas.

Portanto, para executar uma aplicação RMI é preciso inicialmente disponibilizar o serviço de registro RMI. Para tanto, o aplicativo rmiregistry deve ser executado. Com o rmiregistry disponível, o servidor pode ser executado. Para tanto, essa máquina virtual Java deverá ser capaz de localizar e carregar as classes do servidor, da implementação do serviço e do skeleton.

Após a execução do comando que ativa a aplicação servidor, a mensagem “Count Server ready.” deverá surgir na tela, indicando que o servidor obteve sucesso na criação e registro do serviço e portanto está apto a responder às solicitações de clientes.

Finalmente, com o servidor já habilitado para responder às solicitações, o código cliente pode ser executado. Essa máquina virtual deverá ser capaz de localizar e carregar as classes com a aplicação cliente, a interface do serviço e o stub para a implementação do serviço. Seria possível também ter várias ativações simultâneas de CountClient em diferentes máquinas virtuais Java.

No caso mais simples de execução, as diversas máquinas virtuais Java estarão executando em

uma mesma máquina, compartilhando um CLASSPATH comum. No entanto, há mecanismos para permitir o carregamento de classes em uma aplicação RMI envolvendo classes remotas.

 

Operação com objetos em máquinas remotas

Na descrição da operação de aplicações distribuídas usando RMI, assumiu-se que as aplicações clientes, servidor e de registro eram processos distintos; porém considerou-se que todas as classes necessárias para a operação das aplicações estavam localizadas em algum diretório do CLASSPATH local.

No caso de execução em máquinas separadas, há duas formas de fazer a distribuição das classes de modo que clientes e servidores possam executar corretamente.  Na primeira forma, a estratégia  é distribuir explicitamente as classes necessárias e incluí-las em diretórios onde elas possam ser localizadas quando necessário. No lado cliente, essas classes complementares seriam a interface  do serviço e o stub para a implementação do serviço. No lado servidor, seriam essas as classes de implementação do serviço e o correspondente skeleton.

A outra forma é utilizar os mecanismos de carregamento dinâmico de classes distribuídas, em alternativa ao class loader padrão da máquina virtual Java. Por exemplo, se a execução do cliente se dá através de um applet, o AppletClassLoader oferece as funcionalidades necessárias para localizar uma classe que está localizada no mesmo diretório de onde foi carregada a classe original.

Em RMI, há uma alternativa adicional de se utilizar o RMIClassLoader, que permite o carregamento de stubs e skeletons a partir de um URL (especificado através da propriedade java.rmi. server.codebase). Essa propriedade deve ser estabelecida para a máquina virtual Java que irá executar o servidor, como em

 

>java -Djava.rmi.server.codebase=http://mhost/mdir/ CountServer

 

Deste modo, quando o servidor realizar o cadastro do serviço no registry, esse codebase será embutido na referência do objeto. Quando o cliente obtiver a referência ao objeto remoto do registry e seu class loader falhar em localizar a classe stub no CLASSPATH local, sua máquina virtual Java fará uma conexão HTTP com mhost para obter a classe correspondente — assim como outras classes eventualmente necessárias para execução do serviço no lado cliente.

De forma similar, caso o rmiregistry estivesse operando em outra máquina, distinta daquela onde as aplicações clientes e servidor estivessem executando, seria necessário especificar no código das aplicações a máquina que executa rmiregistry, seja através do método getRegistry() da classe LocateRegistry ou através da especificação de URL no protocolo RMI nos métodos da classe Naming.

Como para qualquer situação na qual a máquina virtual Java irá carregar classes localizadas de forma distribuída, é preciso adicionalmente estabelecer qual a política de segurança para operar com código proveniente das outras máquinas. Essa política será enforçada pelo gerenciador de segurança, que pode ser definido pela invocação do método correspondente antes de qualquer invocação a métodos de RMI:

System.setSecurityManager(new RMISecurityManager());

O uso dessas facilidades pode ser apreciado nos exemplos modificados para o código do servidor e cliente da aplicação do contador distribuído. No caso do servidor:

1

import

java.rmi.*;

2

import

java.rmi.server.*;

3

import

java.rmi.registry.*;

4

import

java.net.SocketPermission;

5

public

class CountServer {

  1. public static void main(String[] args) {

  2. // Create and install the security manager

  3. System.setSecurityManager(new RMISecurityManager());

  4. catch (Exception e) {

  5. e.printStackTrace();

11                   }

  1. try {

  2. String serviceName = "Count001";

  3. // Create CountImpl

  4. CountImpl myCount = new CountImpl();

  5. Registry r = LocateRegistry.getRegistry(args[0]);

  6. r.rebind(serviceName, myCount);

  7. System.out.println("Count Server ready.");

19                   }

  1. catch (Exception e) {

  2. System.out.println("Exception: " + e.getMessage());

  3. e.printStackTrace();

23                   }

24             }

25       }

 

No caso da aplicação cliente, o código modificado é apresentado abaixo:

  1. import java.rmi.*;

  2. import java.rmi.registry.*;

  3. public class CountClient {

  4. public static void main(String args[]) {

  5. // Create and install the security manager

  6. System.setSecurityManager(new RMISecurityManager());

  7. try {

  8. Count myCount = (Count)Naming.lookup("rmi://" +

9                                                                                                       args[0] + "/Count001");

  1. // Calculate Start time

  2. long startTime = System.currentTimeMillis();

  3. // Increment 1000 times

  4. System.out.print("Incrementing... ");

14                         for (int i = 0 ; i < 1000 ; i++ )

  1. myCount.increment();

  2. System.out.println(myCount.get());

  3. // Calculate stop time; print out statistics

  4. long stopTime = System.currentTimeMillis();

  5. System.out.println("Avg Ping = "

20                                                                                + ((stopTime - startTime)/1000f)

21                                                                                + " msecs");

22                   }

  1. catch(Exception e) {

  2. System.err.println("System Exception" + e);

25                   }

26                   System.exit(0);

27             }

28       }

A especificação da interface e a implementação do serviço permanecem inalteradas para esses exemplos.

 

Ativação dinâmica

Na primeira especificação de RMI (JDK 1.1), era necessário que um serviço oferecido por um objeto fosse explicitamente ativado em alguma máquina virtual Java e então fosse cadastrado em um serviço de registro.

O problema com essa abordagem ocorre principalmente quando, por algum motivo não previsto, o serviço torna-se indisponível — não há como sinalizar o cliente ou o registro sobre esse fato. O serviço de ativação, oferecido a partir de JDK 1.2, permite contornar essa limitação. As principais facilidades oferecidas pelo mecanismo de ativação remota incluem:

a possibilidade de criar automaticamente um objeto remoto devido a solicitações de obtenção de referências ao objeto;

o suporte a grupos de ativação, permitindo a ativação de vários objetos remotos executando em uma mesma máquina virtual;

a possibilidade de reiniciar a execução de objetos remotos que tenham encerrado sua execução em decorrência de alguma falha do sistema.

Para tornar um objeto que implementa um serviço como remotamente ativável, é preciso satisfazer três requisitos. Primeiro, é preciso implementar o serviço como uma subclasse de Activatable, do pacote java.rmi.activation. Segundo, é preciso criar construtores de ativação na implementação do serviço.

Finalmente, é preciso registrar o objeto e seu método de ativação no serviço de ativação.

A classe Activatable oferece construtores com argumentos específicos para o registro (no serviço de ativação) e a ativação de objetos (incluindo o URL onde o bytecode para o objeto pode ser localizado, um objeto da classe MarshalledObject representando os argumentos de inicialização do objeto e um flag booleano indicando se o objeto deve ser reiniciado com seu grupo) e para a reativação de objetos (incluindo como argumento um objeto ActivationID, previamente designado pelo serviço de ativação). Esses construtores deverão ser invocados nos construtores do objeto que implementam o serviço. Particularmente, o serviço de ativação busca um construtor com dois argumentos dos tipos ActivationID e MarshalledObject.

A classe ActivationDesc permite registrar a informação de ativação de um objeto sem criar uma instância deste objeto, usando para tanto o método estático register() da classe Activatable. Antes de criar um serviço ativável, é preciso criar ou especificar o grupo de ativação ao qual ele pertence. Isto é realizado através dos métodos das classes ActivationGroup, ActivationGroupID e ActivationGroupDesc, todas do pacote java.rmi.activation.

 

Um grupo define um conjunto de objetos ativáveis que devem compartilhar o mesmo espaço de endereçamento, executando na mesma máquina virtual. O serviço de ativação é implementado em uma máquina virtual com um objeto da classe Activator do pacote java.rmi.activation. A aplicação rimd, distribuída juntamente com o pacote básico do JDK 1.2, é um daemon que implementa esse serviço.

Coleta de lixo distribuída

O processo de remoção de objetos remotamente não-referenciados ocorre de maneira automática. Cada servidor com objetos exportados mantém uma lista de referências remotas aos objetos que ele oferece. Através de comunicação com o cliente, ele é notificado quando a referência é liberada na aplicação remota.

Cada referência remota recebe também um período de validade;  quando esse período expira, a referência é eliminada e o cliente é notificado. Esse mecanismo oferece uma alternativa para a liberação de objetos que tenham sido referenciados por clientes que eventualmente tenham falhado, ficando impedidos de sinalizar que a referência havia sido liberada. Embora não sejam utilizadas normalmente por programadores, as funcionalidades do distributed garbage collector estão especificadas através das classes do pacote java.rmi.dgc.

Callback

Nas aplicações em uma arquitetura de objetos distribuídos, nem sempre a comunicação no estilo cliente-servidor é suficiente para atender aos requisitos da aplicação. É usual que o servidor RMI aja algumas vezes como cliente, invertendo os papéis com o cliente RMI original. Considere o exemplo do applet cliente RMI. Nesse applet, não há como saber se outro cliente do mesmo objeto remoto realizou alguma atualização no valor do contador a não ser pressionando o botão Get e verificando se houve mudança. Essa é uma situação típica em muitas aplicações, sendo clara a necessidade de realizar tais notificações de forma automática.

O mecanismo para atingir esse objetivo é utilizar a estratégia de callback. Esta técnica é tipicamente utilizada quando a aplicação cliente requer um retorno do servidor mas não quer permanecer bloqueado aguardando a resposta. Através dessa técnica, o servidor obtém uma referência para o cliente de forma que pode invocar remotamente um método do objeto cliente. Assim, quando a execução do serviço solicitado é concluída, o servidor pode notificar o cliente através da invocação do método disponibilizado pelo cliente para uso remoto.

Basicamente, assim como para o objeto de serviço RMI, deve-se oferecer uma interface remota para o cliente a fim de permitir que o servidor tenha acesso ao “serviço” de atualização do cliente. Esse exemplo ilustra tal situação, com um método que será invocado pelo servidor quando seu valor for alterado, quando passará um valor inteiro para o cliente com a valor atualizado:

  1. import java.rmi.*;

  2. public interface CountClientInterface extends Remote {

  3. void update(int val) throws RemoteException;

4       }

Observe que neste exemplo a interface remota do serviço também foi atualizada de forma a permitir o cadastro dos clientes interessados na atualização:

  1. import java.rmi.*;

  2. public interface Count extends Remote {

  3. void set(int val) throws RemoteException;

  4. void reset() throws RemoteException;

  5. int get() throws RemoteException;

  6. int increment() throws RemoteException;

  7. void addClient(CountClientInterface c) throws RemoteException;

  8. void remClient(CountClientInterface c) throws RemoteException;

9       }

Com callback, ambos cliente e servidor deverão implementar o serviço remoto especificado.

Considere o código para o servidor:

1

import

java.util.*;

2

import

java.rmi.*;

3

import

java.rmi.server.UnicastRemoteObject;

4

public

class CountImpl extends UnicastRemoteObject

  1. implements Count {

  2. private int sum;

 

  1. // Lista de clientes registrados

  2. private Vector clientes = new Vector();

  3. public CountImpl() throws RemoteException  {

10             }

  1. public  void set(int val) throws RemoteException {

  2. sum = val;

13             }

  1. public  void reset() throws RemoteException {

  2. sum = 0;

16             }

  1. public int get() throws RemoteException {

  2. return sum;

19             }

  1. public int increment() throws RemoteException {

  2. sum++;

22                   if (sum%100 == 0)

  1. update();

  2. return sum;

25             }

  1. public void addClient(CountClientInterface client)

  2. throws RemoteException {

  3. clientes.add(client);

29             }

  1. public void remClient(CountClientInterface client)

  2. throws RemoteException {

  3. clientes.remove(client);

33             }

  1. public void update() throws RemoteException {

  2. CountClientInterface cci;

  3. for (int i=0; i<clientes.size(); ++i) {

  4. cci = (CountClientInterface) clientes.elementAt(i);

  5. cci.update(sum);

39                   }

40             }

41       }

Similarmente, para o código do cliente:

  1. import java.rmi.*;

  2. import java.awt.*;

  3. import java.awt.event.*;

  4. import java.applet.*;

  5. import java.rmi.server.*;

  6. public class AppletClient extends Applet

  7. implements ActionListener, CountClientInterface {

 

  1. Count remCount;

  2. TextField tfCnt;

  3. Button bStart;

  4. String bslabel = "Start";

  5. public void init() {

  6. try {

  7. setLayout(new GridLayout(3,1));

  8. add(new Label("Count:"));

  9. tfCnt = new TextField(7);

  10. tfCnt.setEditable(false);

  11. add(tfCnt);

  12. bStart = new Button(bslabel);

  13. bStart.addActionListener(this);

  14. add(bStart);

  15. UnicastRemoteObject.exportObject(this);

  16. showStatus("Binding remote object");

  17. remCount = (Count) Naming.lookup("Count001");

  18. showStatus("Registering with remote object");

  19. remCount.addClient(this);

  20. tfCnt.setText(Integer.toString(remCount.get()));

28                   }

  1. catch (Exception e) {

  2. e.printStackTrace();

31                   }

32             }

  1. public void paint() {

  2. try {

  3. tfCnt.setText(Integer.toString(remCount.get()));

36                   }

  1. catch (Exception e) {

  2. e.printStackTrace();

39                   }

40             }

  1. public void actionPerformed (ActionEvent ev) {

  2. try {

  3. showStatus("Incrementing...");

44                         for (int i = 0 ; i < 1000 ; i++ )

  1. remCount.increment();

  2. showStatus("Done");

47                   }

  1. catch (Exception e) {

  2. e.printStackTrace();

50                   }

51             }

 

  1. public void update(int val) throws RemoteException {

  2. showStatus("Update");

  3. tfCnt.setText(Integer.toString(val));

55             }

56       }

Como anteriormente, devem ser criados os stubs e skeletons para ambos os serviços. O código do servidor não sofre alteração em relação ao exemplo anterior, assim como a forma de execução da aplicação.

java idl

5.1.1 Java IDL

A API Java IDL, presente na plataforma Java desde a versão 1.2, permite a integração entre objetos Java e outros objetos, eventualmente desenvolvidos em outras linguagens de programação, através da arquitetura CORBA. Os principais pacotes que compõem essa API são org.omg.CORBA e org.omg.CosNaming.

A partir da versão 1.3 da plataforma Java, é possível gerar interfaces IDL para classes Java usan-

do o compilador rmic com a opção -idl". Outra opção, -iiop", indica que o protocolo de comunicação de CORBA, IIOP, será utilizado em stubs e ties (correspondentes aos skeletons) de RMI.

 

Arquitetura CORBA

CORBA (Common Object Request Broker Architecture) é um padrão definido pelo consórcio OMG Object Management Group) que define uma arquitetura de objetos, com uma linguagem para descrição de interfaces com mapeamentos padronizados para diversas linguagens e um conjunto de serviços básicos.

Como o padrão CORBA visa atender a diversas linguagens de programação, sua especificação é ampla e relativamente complexa. De forma extremamente simplificada, os componentes básicos dessa arquitetura são:

a linguagem de descrição de interfaces;

o intermediário para repassar requisições a objetos remotos;

o serviço para localizar objetos remotos; e

o protocolo de comunicação.

IDL é a Interface Description Language, uma linguagem que permite especificar interfaces de forma independente da linguagem de programação na qual a especificação é implementada. CORBA determina uma série de mapeamentos padronizados entre IDL e outras linguagens, tais como C, C++, COBOL e Java.

ORB é o Object Request Broker, o núcleo da arquitetura CORBA. É um programa que deve estar executando em cada máquina envolvida em uma aplicação CORBA, sendo o responsável pela conexão entre clientes e serviços através dos correspondentes stubs e skeletons.

 

O Serviço de Nomes de CORBA define uma estrutura para associar nomes a objetos remotos definidos na arquitetura. A estrutura definida é uma hierarquia (ou árvore), onde cada ramo define um contexto distinto e cujas folhas são os nomes dos serviços disponibilizados. Assim, a referência completa para o nome de um serviço é dada pelo contexto (os nomes dos nós intermediários) e pelo nome do serviço.

O protocolo de comunicação de CORBA especifica o padrão para que as requisições de objetos transmitidas entre ORBs, independentemente de como ou em qual linguagem esses ORBs foram implementados, possam ser reconhecidas. O protocolo de comunicação CORBA mais comum é o IIOP, o Internet Inter-ORB Protocol, em função da disseminação da Internet, mas outros protocolos podem ser obtidos para outras plataformas.

 

CORBA e Java

Uma vez definida ou obtida a interface IDL para um serviço, as classes auxiliares para acessar o objeto remoto que implementa o serviço são obtidas pela compilação da interface, usando o aplicativo idlj (ou idltojava ou ainda idl2java em versões anteriores à Java 1.3). Além de classes para stubs e skeletons, são geradas classes auxiliares (helpers e holders) para permitir a comunicação entre objetos Java e dados estabelecidos em outras linguagens. Na plataforma Java há uma implementação para o serviço de nomes de CORBA, oferecida pelo aplicativo tnameserv. Esse serviço está mapeado por default para a porta 900, podendo esta ser modificada pela opção -ORBInitialPort".

A interação entre um ORB e um programa Java dá-se através de métodos da classe ORB. Para inicializar a referência ao ORB, utiliza-se o método estático init() dessa classe. Para obter uma referência para o serviço de nomes utiliza-se o método resolve_initial_references(), tendo a NameService como argumento.

O exemplo a seguir é a implementação usando CORBA do clássico programa “Hello, world”. É composto por três arquivos: a interface IDL, o cliente e o servidor. A Interface IDL descreve um serviço com um único método, sendo aqui definida usando as construções da linguagem IDL:

  1. module HelloApp {

  2. interface Hello {

  3. string sayHello();

4             }

5       }

Usando-se o aplicativo idlj, gera-se a interface Java correspondente, com a tradução das construções IDL para as primitivas Java segundo o padrão estabelecido em CORBA, além de outros arquivos auxiliares (stub, skeleton, helper, holder), não apresentados aqui:

  1. package HelloApp;

  2. public interface Hello

  3. extends org.omg.CORBA.Object {

  4. String sayHello();

5       }

 

O código cliente ativa o ORB, obtém uma referência para o serviço de nomes e, a partir deste serviço, obtém uma referência remota para o objeto com o serviço Hello. Obtida a referência, o método é invocado normalmente:

  1. import HelloApp.*;

  2. import org.omg.CosNaming.*;

  3. import org.omg.CORBA.*;

  4. public class HelloClient {

  5. public static void main (String args[]) {

  6. try {

  7. ORB meuOrb = ORB.init(args,null);

  8. org.omg.CORBA.Object objRef =

  9. meuOrb.resolve_initial_references("NameService");

  10. NamingContext ncRef =  NamingContextHelper.narrow(objRef);

  11. NameComponent nc = new NameComponent("Hello","");

  12. NameComponent path[] = {nc};

  13. Hello helloRef = HelloHelper.narrow(ncRef.resolve(path));

  14. String hi = helloRef.sayHello();

  15. System.out.println(hi);

16                   }

  1. catch(Exception e) {

  2. System.out.println(e);

  3. e.printStackTrace(System.out);

20                   }

21             }

22       }

Nesse exemplo, combina-se a implementação do serviço e o correspondente servidor. A classe HelloServer é um servidor que ativa o ORB, cria o objeto que implementa o serviço, obtém uma referência para o serviço de nomes e registra o objeto neste diretório associado ao nome Hello. A classe HelloServant é uma implementação do serviço especificado; observe que essa classe é uma extensão de _HelloImplBase, o skeleton definido pelo aplicativo idlj:

  1. import HelloApp.*;

  2. import org.omg.CosNaming.*;

  3. import org.omg.CosNaming.NamingContextPackage.*;

  4. import org.omg.CORBA.*;

  5. public class HelloServer {

  6. public static void main(string args[]) {

  7. try {

  8. // Create the ORB

  9. ORB orb = ORB.init(args,null);

  10. // Instantiate the servant object

  11. HelloServant helloRef = new HelloServant();

  12. // Connect servant to the ORB

  13. orb.connect(helloRef);

 

  1. //Registering the servant

  2. org.omg.CORBA.Object objRef =

  3. orb.resolve_initial_references("NameService");

  4. NamingContext ncRef = NamingContextHelper.narrow(objRef);

  5. NameComponent nc = new NameComponent("Hello","");

  6. NameComponent path[] = {nc};

  7. ncRef.rebind(path, helloRef);

  8. // Wait for invocation

  9. java.lang.Object sync = new java.Lang.Object();

  10. synchronized(sync) {

  11. sync.wait();

25                         }

26                   }

  1. catch(Exception e) {

  2. System.out.println(e);

  3. e.printStackTrace(System.out);

30                   }

31             }

32       }

  1. class HelloServant extends _HelloImplBase {

  2. public String sayHello() {

  3. return "\nHelloWorld!\n";

36             }

37       }

Apêndice A

Palavras chaves de Java

 

As palavras a seguir são de uso reservado em Java e não podem ser utilizadas como nomes de identificadores:

identificadores java.jpg

(c) FMK 2023 - 2024.  Updated Sep 2024.

bottom of page