Departamento de Engenharia Electrotécnica REDES INTEGRADAS DE TELECOMUNICAÇÕES II 2012 / 2013 Mestrado Integrado em Engenharia Electrotécnica e de Computadores 4º ano 8º semestre Introdução ao desenvolvimento de aplicações CORBA em Java http://tele1.dee.fct.unl.pt Luis Bernardo / Pedro Amaral ÍNDICE Índice ......................................................................................................................................................................... 1 1. Objectivo ............................................................................................................................................................... 2 2. A arquitectura CORBA ......................................................................................................................................... 2 3. Construção de aplicações CORBA ........................................................................................................................ 3 3.1. Definição de interfaces (IDL) ......................................................................................................................... 4 3.2. Clientes (invocação de funções) ..................................................................................................................... 8 3.2.1. Referências para objectos ........................................................................................................................ 9 3.2.2. Realização de um cliente ......................................................................................................................... 9 3.3. Servidores (recepção de invocações) ............................................................................................................ 10 3.3.1. Realização de um servidor ..................................................................................................................... 10 3.3.2. Geração de excepções de utilizador....................................................................................................... 13 3.3.3. Modos de activação de objectos no CORBA ........................................................................................ 13 3.3.4. Integração do CORBA no Java ............................................................................................................. 13 3.4. Passagem de parâmetros em funções IDL .................................................................................................... 15 3.5. Serviço de Nomes ......................................................................................................................................... 16 3.6. Serviço de Eventos ....................................................................................................................................... 18 4. OpenORB ............................................................................................................................................................ 21 5. Exemplos de aplicações ....................................................................................................................................... 22 5.1. Aplicação cliente-servidor utilizando o serviço de nomes ........................................................................... 22 5.1.1. Cliente ........................................................................................................................................................ 22 5.1.2. Servidor ..................................................................................................................................................... 23 5.1.3. Exercício .................................................................................................................................................... 26 5.2. Aplicação utilizando o serviço de eventos ................................................................................................... 26 5.2.1. Emissor ...................................................................................................................................................... 27 5.2.2. Receptor ..................................................................................................................................................... 29 5.2.3. Exercício .................................................................................................................................................... 31 7. Bibliografia adicional .......................................................................................................................................... 32 1 1. OBJECTIVO Familiarização com o ambiente CORBA para Java e com o desenvolvimento de aplicações. Este documento descreve o método para a criação de aplicações CORBA e o conjunto de classes e funções disponíveis em Java. Adicionalmente, é apresentado o código integral de algumas aplicações que pode ser introduzido seguindo as instruções do enunciado, facilitando a aprendizagem. 2. A ARQUITECTURA CORBA A arquitectura CORBA (Common Object Request Broker Arquitecture) foi desenvolvida para facilitar o desenvolvimento de aplicações distribuídas numa rede heterogénea. A arquitectura CORBA usa uma abordagem orientada para objectos para a criação de software. Cada objecto encapsula os detalhes da sua implementação oferecendo uma interface bem conhecida, descrita por um ficheiro de especificação IDL (Interface Definition Language). O ORB (Object Request Broker) é o componente da arquitectura que liga as aplicações clientes com os objectos que pretendem usar. O programa cliente não precisa de saber se o objecto reside na mesma máquina, ou está localizado noutro computador na rede. O programa cliente só necessita de saber o nome do objecto e de saber usar a interface do objecto. O ORB trata de todos os detalhes de localizar o objecto, enviar os pedidos e receber as respostas. Programa Cliente Objecto A Invocação bind() do objecto A ORB localiza objecto A e liga-o ao cliente ORB Após realizar a associação a um objecto (bind), o cliente pode invocar transparentemente qualquer função da interface do objecto A, pois o ORB vai encarregar-se de codificar os parâmetros de entrada da função numa mensagem, vai enviá-la para a máquina onde se encontra a correr o objecto A, e vai invocar a função sobre o objecto A. O retorno de parâmetros ou valores também é enviado através do ORB. A utilização do ORB oferece uma total transparência na invocação de operações entre objectos, sendo possível interligar objectos realizados em diferentes linguagens (Java, C, C++, Cobol, etc.) ou arquitecturas (ex. Intel, Linux, Unix, etc.). O ORB não é um processo separado mas uma colecção de bibliotecas que se ligam aos executáveis e de recursos computacionais suportados por aplicações. O JDK 1.4.2 inclui um ORB compatível com a versão 2.4 da norma CORBA e o serviço de nomes. Nesta disciplina foi adoptada uma realização de código aberto de um ORB, o OpenORB. Em relação ao JDK tem a vantagem de ser uma realização completa da arquitectura CORBA. Neste documento não se estuda toda a arquitectura CORBA com os vários serviços e funcionalidades suportados introduzindo-se apenas os serviços e funcionalidades necessários para a realização do trabalho prático proposto. 2 3. CONSTRUÇÃO DE APLICAÇÕES CORBA O primeiro passo no processo de desenvolvimento de aplicações consiste na identificação dos objectos que fazem parte da aplicação (geralmente um cliente e um servidor). Em seguida seguem-se os passos indicados na figura: 1. Escreve-se um ficheiro 'Nome.idl' com a especificação da interface de cada objecto servidor; 2. Usa-se o compilador de IDL (“idl2java -d . Nome.idl” no OpenORB) para gerar vários ficheiros (caso a interface esteja num módulo, os ficheiros são gerados dentro de uma directoria com o nome do módulo): • 'Nome.java' – mapeamento da interface CORBA numa interface Java ; • 'NomeHolder.java' – suporta parâmetros de saída e entrada-saída dos métodos; • 'NomeHelper.java' – suporta várias funções auxiliares, como conversão de tipos explícita (narrow), traduções de e para uma variável do tipo any (insert e extract), escrita e leitura em feixes de dados, etc.; • '_NomeStub.java' – stub para o cliente; • 'NomeOperations.java' – declaração de interface Java com os métodos suportados na interface CORBA; • 'NomePOA.java' – skeleton para o objecto; • 'NomePOATie.java' – realização alternativa de skeleton; 3. Programa-se o código do cliente em 'Cliente.java'. Para usar o objecto define-se uma variável do tipo referência para objecto (Nome) definido em 'Nome.java'. Após inicializada, a variável permite realizar todas as operações definidas na interface; 4. Programa-se o código do servidor estendendo a classe abstracta pura (com o nome 'NomePOA') definida em 'NomePOA.java', onde se define a implementação de todas as funções (declaradas em 'NomeOperations.java'). O objecto só fica activo após se registar no POA (Portable Object Adapter), o componente do ORB que recebe as invocações; 5. Compila-se os programas cliente e servidor utilizando o compilador de Java; 6. Arranca-se o servidor; 7. Arranca-se o cliente, que utilizando o ORB comunica com o objecto no servidor. 1 Compilador IDL Nome.idl 2 Nome.java _NomeStub.java _NomeStub.java 3 NomePOA.java NomePOA.java Cliente.java Compilador java Compilador Java 5 Cliente Código Cliente 4 Servidor.java 5 Stub IDL Skeleton IDL 7 Código Servidor Servidor 6 ORB No ambiente NetBeans é necessário realizar a compilação dos vários ficheiros IDL na linha de comando, integrando posteriormente estes ficheiros no projecto NetBeans onde se desenvolve o cliente e o servidor. 3 3.1. Definição de interfaces (IDL) A norma CORBA define um conjunto de tipos básicos para a definição de interfaces, que inclui os tipos indicados na tabela seguinte com a correspondência para Java: Tipo IDL Tipo Java float float double double unsigned long / long int unsigned short / short short boolean boolean string / wstring java.lang.String Para além dos tipos básicos, o programador pode definir novos tipos a partir de tipos existentes, ou criar novos tipos estruturados. A instrução typedef define um novo tipo (NomeTipo) a partir de Tipo: typedef Tipo NomeTipo; A instrução struct define um tipo estruturado equivalente à struct da linguagem C com o nome NomeTipo, composto por vários campos: struct NomeTipo { TipoCampo_1 NomeCampo_1; … }; Depois de compilar o IDL, dá origem à declaração de uma classe com o nome NomeTipo, com membros de acesso público. Os arrays de tamanho fixo são declarados de uma forma equivalente à usada em C++, dando origem à declaração de um array em Java no código gerado pelo compilador de IDLs: typedef short NomeTipo[10]; Os arrays de tamanho variável (no exemplo de inteiros com um tamanho máximo opcional de 10) são declarados com a palavra chave sequence e dão origem à declaração de um array em Java no código gerado pelo compilador de IDLs: typedef sequence < long, 10> NomeTipo; Os tipos enumerados têm uma sintaxe semelhante ao C++: enum TipoEnum { UM, DOIS }; Como não existe este tipo em Java, é gerada uma classe com o nome do tipo enumerado, que associa a cada membro da enumeração um valor inteiro constante (final int) e um do tipo da classe gerada. O valor inteiro é usado em instruções switch ou para indexar arrays, enquanto a segunda versão é usada em parâmetros para funções, para validação de tipos. São fornecidos métodos para converter entre os dois formatos (value e from_int), e para devolver uma string com o nome do membro (toString). Para o exemplo anterior seria gerada a seguinte classe: 4 public final class TipoEnum implements org.omg.CORBA.portable.IDLEntity { /** Enum member UM value */ public static final int _UM = 0; /** Enum member UM */ public static final TipoEnum UM = new TipoEnum(_UM); /** Enum member DOIS value */ public static final int _DOIS = 1; /** Enum member DOIS */ public static final TipoEnum DOIS = new TipoEnum(_DOIS); /** Internal member value */ private final int _TipoEnum_value; /** Private constructor */ private TipoEnum( final int value ) { _TipoEnum_value = value; } /** Return the internal member value */ public int value() { return _TipoEnum_value; } /** Return a enum member from its value */ public static TipoEnum from_int(int value) { switch (value) { case 0 : return UM; case 1 : return DOIS; } throw new org.omg.CORBA.BAD_OPERATION(); } /** Return a string representation */ public java.lang.String toString() { switch (_TipoEvento_value) { case 0 : return "UM"; case 1 : return "DOIS"; } throw new org.omg.CORBA.BAD_OPERATION(); } } A norma CORBA define um tipo genérico (org.omg.CORBA.Any), que pode transportar qualquer tipo de dados. Este tipo é particularmente útil no serviço de trading, com valores de propriedades dinâmicos, e no serviço de eventos. O tipo Any é realizado por uma classe abstracta, que memoriza o tipo dos dados, os dados, e um conjunto de funções para inserir valores ou para os exportar para uma variável de um tipo básico. Os objectos do tipo Any são criados utilizando-se o método create_Any() do objecto ORB. O preenchimento e leitura do valor pode ser realizado de duas maneiras, dependendo do tipo do objecto a colocar: • Num tipo predefinido CORBA, a classe Any fornece dois métodos (e.g. int): public void insert_int(int value) throws org.omg.CORBA.BAD_OPERATION; public int extract_int() throws org.omg.CORBA.BAD_OPERATION; 5 • Num tipo definido pelo utilizador (e.g. usertype), são criados dois métodos na classe usertypeHelper que é sempre criada durante o processo de compilação do IDL: public static void insert(Any a, usertype value); public static usertype extract(Any a); A classe Any oferece o método type() para obter o tipo de objecto transportado. No exemplo da secção 6.1 pode encontrar um exemplo de utilização do tipo any. Adicionalmente, a norma CORBA define outros tipos de dados estruturados (unions, arrays de tamanho variável (designados de sequências), etc.) que não são usados no trabalho prático. A definição de uma interface é realizada numa secção precedida da palavra-chave interface, contendo uma lista de uma ou mais funções que podem ser invocadas num objecto. Cada função define o tipo do valor retornado pela função, ou void caso não retorne nada. Adicionalmente define uma lista de argumentos de tamanho variável. Todas as invocações de funções são fiáveis. No caso de uma função não devolver nada ao cliente pode-se declarar como oneway, tendo nesse caso uma semântica de "melhor-esforço", sem garantias de fiabilidade na invocação. interface NomeInterface { TipoRetorno NomeFunção(lista de argumentos); oneway void NomeFunção(lista de argumentos); }; A lista de argumentos é constituída por um conjunto de argumentos separados por vírgulas precedidos de uma palavra-chave: • in TipoArg Nome • out TipoArg Nome • inout TipoArg Nome A palavra-chave define se o argumento é enviado do cliente para o objecto (in), se é enviado do objecto para o cliente (out), ou se é enviado e depois modificado e recebido no cliente (inout). Os servidores podem ainda notificar os clientes de falhas graves no objecto através do retorno de excepções para os clientes. Neste caso, a interface inclui a definição do nome da excepção e do conjunto de campos (eventualmente vazio) que compõem a excepção (acedidos de forma semelhante aos campos de uma estrutura). Caso se use excepções de utilizador, tem de se definir quais as funções que retornam excepções (raises): interface NomeInterface { exception NomeExcepção {lista de campos}; … TipoRetorno NomeFunção(lista de argumentos) raises (NomeExcepção); }; Por vezes, quando há várias interfaces que partilham os mesmos tipos, há vantagens em declará-las integradas num único módulo. A sintaxe é a seguinte: module NomeMódulo { definição de uma ou mais interfaces ou de tipos }; Após compilação do IDL, o módulo dá origem a um package Java, dentro de uma directoria com o nome do módulo, que contém internamente as declarações de interfaces e de tipos declarados no módulo. Assim, para um módulo de nome Banco com um tipo DadosConta e uma interface Conta são criados respectivamente: um tipo Banco.DadosConta (tipo DadosConta dentro do package Banco) e uma classe Banco.Conta (interface Conta dentro do package Banco). 6 Todas as declarações de tipos e de interfaces num ficheiro IDL dão origem à criação de duas classes adicionais durante a sua compilação (e.g. Tipo): TipoHolder e TipoHelper. Todos os tipos predefinidos do CORBA também têm as classes Holder e Helper correspondentes. A classe TipoHolder foi introduzida para resolver o problema do Java não suportar a passagem de parâmetros por referência – apenas por valor. Sempre que um parâmetro de um método é out ou inout, é usada uma variável do tipo TipoHolder na invocação do método, ou na realização do método no servidor. Esta classe tem uma variável interna pública (value) onde se preenche o valor retornado pelo método, mais dois construtores e funções para escrever e ler o valor de e para um feixe IIOP: final public class TipoHolder implements org.omg.CORBA.portable.Streamable { /** Internal Tipo value */ public Tipo value; /** Default constructor */ public TipoHolder() { } /** Constructor with value initialisation */ public TipoHolder(Tipo initial) { value = initial; } /** Read Tipo from a marshalled stream */ public void _read(org.omg.CORBA.portable.InputStream istream) { … } /** Write Tipo into a marshalled stream */ public void _write(org.omg.CORBA.portable.OutputStream ostream) { … } /** Return the Tipo TypeCode */ public org.omg.CORBA.TypeCode _type() { … } } A classe TipoHelper foi introduzida para lidar com tipos numa linguagem fortemente tipificada, como é o Java. Todos os objectos CORBA herdam da classe (org.omg.CORBA.Object), i.e. são objectos desta classe. A classe TipoHelper inclui métodos estáticos para: converter explicitamente o tipo de uma referência para objecto para Tipo (devolvendo null caso não seja desse tipo); lidar com variáveis do tipo Any; serializar os dados de objectos da classe Tipo; e para obter informação acerca do tipo (código de tipo e identificação do repositório de interfaces): public class TipoHelper { /** Insert Tipo into an any */ public static void insert(org.omg.CORBA.Any a, Tipo t) { … } /** Extract Tipo from an any */ public static Tipo extract(org.omg.CORBA.Any a) { … } /** Return the Tipo TypeCode */ public static org.omg.CORBA.TypeCode type() { … } /** Return the Tipo IDL ID */ public static String id() 7 { return _id; } /** Read Tipo from a marshalled stream */ public static Tipo read(org.omg.CORBA.portable.InputStream istream) { … } /** Write Tipo into a marshalled stream */ public static void write(org.omg.CORBA.portable.OutputStream ostream, Tipo value) { … } /** Narrow CORBA::Object to Tipo */ public static Tipo narrow(org.omg.CORBA.Object obj) { … } } 3.2. Clientes (invocação de funções) Para cada interface, o compilador de IDL gera uma interface Java com o mesmo nome da interface CORBA, e que é realizada num procurador de um objecto. Por exemplo, para o ficheiro 'Hello.idl' seguinte: module HelloApp { interface Hello { string sayHello(); }; }; A interface gerada no ficheiro 'HelloApp/Hello.java' realiza os métodos de org.omg.CORBA.Object, a super-classe de todos os objectos CORBA, e de HelloApp.HelloOperations: package HelloApp; public interface Hello extends HelloOperations, org.omg.CORBA.Object, org.omg.CORBA.portable.IDLEntity { } A interface HelloOperations define o método sayHello. O stub de cliente (classe _HelloStub) realiza esta interface, e permite que o cliente invoque uma operação no objecto remoto transparentemente. package HelloApp; public interface HelloOperations { public String sayHello(); } A figura seguinte sistematiza a relação entre as várias classes e interfaces usadas na realização do cliente para uma interface genérica com o nome InterfaceName: 8 3.2.1. Referências para objectos Todos os objectos CORBA implementam a interface org.omg.CORBA.Object. Através desta interface têm acesso a algumas funções oferecidas pelo ORB. As restantes são acessíveis utilizando uma referência para a pseudo-interface do ORB (orb da classe org.omg.CORBA.ORB). Nesta secção são ilustradas algumas das operações disponíveis, para o caso de uma referência para objecto if_ref do tipo HelloApp.Hello. Converte uma referência para objecto numa string no formato IOR (String IOR). Esta função requer a referência para o ORB (orb) inicializada previamente: • IOR= orb.object_to_string(if_ref); Usa uma string no formato IOR (String IOR) para inicializar uma referência para objecto. Esta função requer uma referência para o ORB (orb) inicializada previamente e uma referência genérica (org.omg.CORBA.Object obj): • obj = orb.string_to_object(IOR); if_ref= HelloApp.HelloHelper.narrow (obj); Testa se o objecto referenciado não existe. Contacta servidor e devolve TRUE se não existir, podendo gerar excepção: • if (!if_ref._non_existent()) { /* não existe */ } Testa se duas referências para objecto referenciam o mesmo objecto. Caso existam várias referências para o objecto encadeadas, o método pode retornar false mas serem referências para o mesmo objecto: • if (!if_ref._is_equivalent(obj)) { /* é equivalente */ } 3.2.2. Realização de um cliente A realização de um cliente que faz uma invocação remota num objecto requer quatro fases: 1. 2. 3. 4. Inicializar o ORB; Inicializar a referência para o objecto; Invocar a operação sobre o objecto; Libertar a referência, se necessário. 9 A inicialização do ORB (org.omg.CORBA.ORB orb) é feita utilizando métodos estáticos da classe ORB: public static ORB init(String[] args, Properties props); ou public static ORB init(); A inicialização da referência para objecto (if_ref) pode ser feita: recebendo uma string com o IOR do objecto pretendido (ver secção anterior); ou utilizando o serviço de nomes (ver secção 3.5). A invocação da operação sayHello é feita com a mesma sintaxe que seria num objecto local. No entanto como pode envolver comunicação por rede, é necessário acrescentar o código para apanhar as excepções a informar sobre problemas na invocação da função: try { System.out.println(if_ref.sayHello()); } catch ( org.omg.CORBA.SystemException e ) { System.err.println("Excepção: "+e); … } 3.3. Servidores (recepção de invocações) Os servidores são realizados como um conjunto de objectos CORBA, cada um associado a uma interface, registados no adaptador de objectos POA (Portable Object Adapter). No trabalho prático da disciplina vai ser usado o OpenORB 1.4.0 para Java, um ambiente CORBA de código aberto compatível com a versão 2.4.2 da norma. 3.3.1. Realização de um servidor Um servidor é realizado através de um objecto Java que é registado no POA. Este objecto é construído a partir da classe InterfaceNamePOA (resultante da compilação de um IDL com o nome InterfaceName), acrescentando-se os métodos suportados na interface (declaradas num ficheiro com o nome InterfaceNameOperations também gerado pelo compilador de IDL) e pelo menos um construtor. Este modelo de herança é o mais comum no desenvolvimento de servidores. Um segundo modelo alternativo, baseado em delegação (Tie approach), recorre a uma classe de interface ao POA (definida em InterfaceNamePOATie, também gerada pelo compilador de IDL) que invoca explicitamente métodos num objecto do programador, que realiza todas as funções de interface. A figura seguinte sistematiza a relação entre as várias classes e interfaces usadas na realização do servidor: 10 Nas linhas seguintes vai ser introduzido um exemplo completo de realização de um servidor. Considere-se novamente o ficheiro IDL Hello.idl: module HelloApp { interface Hello { string sayHello(); }; }; A compilação do IDL dá origem à criação de uma classe HelloApp.HelloPOA, que realiza o skeleton. Para realizar um objecto servidor vai-se definir uma classe (HelloImpl) que estende a classe HelloApp.HelloPOA, com um construtor e a implementação da função sayHello (no caso genérico seriam todas as funções da interface HelloApp. HelloOperations). Observe-se que na definição do construtor são inicializados os dados internos do objecto: public class HelloImpl extends HelloPOA { private String salute; // Constructor HelloImpl( String salute ) { This.salute= salute; } // stores string public String sayHello() { return salute; } // Returns string } 11 Após codificar a classe com a implementação do objecto é necessário: 1. inicializar o ORB e o POA; 2. criar um novo objecto Java do tipo HelloImpl; 3. registar o objecto no POA 4. Chamar a função que prepara o POA para receber invocações Para evitar escrever sucessivamente o nome completo das classes, vai-se admitir que são usadas as duas linhas seguintes no restante código desta secção. import org.omg.CORBA.*; import org.omg.PortableServer.*; A inicialização do ORB (ORB orb) e do POA (POA poa) é feita com as instruções seguintes. Observe-se que a referência para o POA é obtida através da interface do ORB, pedindo a referência inicial para o serviço "RootPOA". Seria possível criar um POA diferente com configurações mais específicas a nível da gestão de paralelismo objectos, ou de persistência de objectos. Para o trabalho proposto vai-se usar a configuração por omissão – objectos não persistentes, que desaparecem quando se desliga o servidor. orb= ORB.init(args, null); // args contém argumentos do comando poa= POAHelper.narrow(orb.resolve_initial_references("RootPOA")); Quando se obtém a referência para o POA ele está inactivo. É necessário activá-lo antes de se poder usá-lo para registar objectos: poa.the_POAManager().activate(); A criação de uma nova variável deve ser feita com a instrução new, passando os argumentos do construtor para iniciar o estado do objecto Java: HelloImpl pt= new HelloImpl("Olá mundo!"); O registo do objecto no POA é feito com a instrução: org.omg.CORBA.Object obj= poa.servant_to_reference( pt ); Por fim, é necessário arrancar o ciclo principal do POA. Esta operação é bloqueante, não deixando que nada corra a partir do instante em que é invocada excepto as funções dos objectos registados no POA. orb.run(); Um objecto activo é identificado globalmente pela referência devolvida pelo método servant_to_reference, de um tipo compatível com org.omg.CORBA.Object, mas localmente ao servidor, também é identificado pelo ponteiro para o objecto Java (compatível com org.omg.PortableServer.Servant), vulgarmente denominado servant, ou pelo identificador de objecto atribuído pelo POA durante o registo (do tipo byte[]). Em qualquer altura é possível desactivar um objecto CORBA, cancelando o registo do objecto no POA com a operação deactivate_object. Para isso é necessário obter o identificador de objecto (OID) do objecto. Uma das maneiras é usar o ponteiro para o objecto Java (servant) como argumento do método servant_to_id da interface POA. 12 try { poa.deactivate_object( poa.servant_to_id(helloImpl)); } catch (org.omg.PortableServer.POAPackage.ServantNotActive e) { // Servant não está registado } catch (org.omg.PortableServer.POAPackage.ObjectNotActive e) { // O objecto CORBA não está activo } catch (org.omg.PortableServer.POAPackage.WrongPolicy e) { // o POA não suporta registo de servidores } catch (SystemException e) { // Falhou comunicação } 3.3.2. Geração de excepções de utilizador Considere-se o ficheiro IDL anterior modificado de maneira a incluir uma excepção de utilizador: module HelloApp { exception UtilizadorInvalido {}; // sem parâmetros interface Hello { string sayHello() raises (UtilizadorInvalido); }; }; Após compilar o IDL esta modificação acrescenta a declaração de mais três classes (UtilizadorInvalido e os ficheiros Holder e Helper correspondentes) e modifica a assinatura da função sayHello. A função poderá então retornar um valor (com return) ou uma excepção com (throw), que deverá ser tratada no código do cliente: public String sayHello() throws HelloApp.UtilizadorInvalido { } if (…utilizador invalido…) // testa se o utilizador existe throw new HelloApp.UtilizadorInvalidor(); // sai da função return salute; // Returns string } 3.3.3. Modos de activação de objectos no CORBA Existem vários modos como o POA pode funcionar. O modo descrito na secção 3.3.1 é o modo mais simples, onde o programador arranca e pára os objectos explicitamente. No entanto, recorrendo a componentes do CORBA como o repositório de implementações é possível registar uma implementação de um objecto no POA e deixar que este seja arrancado quando necessário, podendo-se definir diferentes políticas de activação (um objecto para todas as invocações ou um objecto novo por cada invocação). Noutro modo de activação designado de DII (Dynamic Interface Invocation) / DSI (Dynamic Skeleton Invocation), é usado um serviço de repositório de interfaces para obter as funções e os parâmetros das funções de um tipo de interface, construindo-se dinamicamente os stubs e skeletons de uma forma semelhante à que foi usada para enviar e receber mensagens directamente sobre sockets em disciplinas anteriores. 3.3.4. Integração do CORBA no Java Para realizar um objecto CORBA num programa com uma interface gráfica é necessário recorrer a pelo menos duas tarefas: uma para lidar com a recepção de eventos gráficos (toque na tecla do rato, teclas, sockets, etc.); outra para lidar com a recepção de invocações de métodos 13 CORBA. Para facilitar a programação de servidores CORBA é fornecido o ficheiro corba_thread.java, que facilita a criação da thread CORBA: import org.omg.CORBA.*; import org.omg.PortableServer.*; public class corba_thread extends Thread { protected ORB orb; protected POA poa; /** Creates a new instance of corba_thread */ public corba_thread(ORB _orb) { this.orb= _orb; try{ poa = ( POA ) orb.resolve_initial_references( "RootPOA" ); poa.the_POAManager().activate(); // start POA } catch (SystemException e) { System.err.println(e); } catch(Exception e) { System.err.println("ERROR: " + e); } } public void run() { try{ System.out.println("ORB running"); orb.run(); } catch (SystemException e) { System.err.println("Exception in Corba thread: " + e); } catch(Exception e) { System.err.println("Exception in Corba thread: " + e); } } public static ORB init_orb(String args[]) { try { ORB _orb= ORB.init(args, null); return _orb; } catch (SystemException e) { System.err.println(e); } catch(Exception e) { System.err.println("ERROR: " + e); } return null; } public ORB orb() { return orb; } public POA poa() { return poa; } } Esta classe oferece um método estático (init_orb) para obter a referência inicial para o ORB e um construtor que inicia a referência para o POA. O arranque da tarefa CORBA pode ser realizado com o seguinte código: ORB orb= corba_thread.init_orb(args); if (orb == null) … corba_thread ct= new corba_thread(orb); ct.start(); 14 O facto de haver pelo menos duas tarefas a poderem invocar métodos em paralelo obriga a ter cuidados especiais na programação de rotinas que acedem à interface gráfica, ou que manipulam estruturas de dados. Deve-se usar métodos ou troços de código synchronized sempre que possa haver problemas resultantes do acesso concorrente a partir de várias tarefas. Tenha em atenção que pode haver o risco do programa parar se um troço de acesso exclusivo for chamado numa altura onde uma variável já está bloqueada. public synchronized void Log(String s) { jTextArea1.insert(s, 0); System.out.print(s); } ou void f(…) { synchonized (variavel) { // Apenas uma tarefa acede à lista neste troço de código } } 3.4. Passagem de parâmetros em funções IDL A passagem de parâmetros de métodos em Java é trivial devido à gestão automática de memória e à utilização das classes TipoHolder, que existem para todos os tipos. Comparativamente, a realização dos mesmos métodos em C ou C++ é muitíssimo mais complicada. Todos os parâmetros de entrada (in) dos métodos são passados por valor, recebendo um valor da classe do parâmetro. Os parâmetros de saída (out) e de entrada-saída (inout) são realizados com objectos das classes TipoHolder, tanto no cliente como no servidor, funcionando como ponteiros para um objecto que é modificado após a invocação. O IDL seguinte ilustra um exemplo de um método com valores retornados e com os três tipos de parâmetros, com tipos genéricos: interface Exemplo { tipo0 op(in tipo1 p_in, inout tipo2 p_inout, out tipo3 p_out); }; Esta interface é compilada ExemploOperations.java: para a seguinte interface no ficheiro public interface ExemploOperations { public tipo0 op(tipo1 p_in, tipo2Holder p_inout, tipo3Holder p_out); } O código do cliente deve declarar variáveis dos tipos declarados na interface anterior (inicializando o valor dos parâmetros in e inout), invocar a operação e processar os valores retornados. Um exemplo de código do cliente que chama a função op é: Exemplo obj = ...; // obtém referência para o objecto tipo1 t1= new tipo1(); tipo2Holder t2= new tipo2Holder(…/*valor inicial*/); tipo3Holder t3= new tipo3Holder(); //chama função tipo0 ret_val= obj.op(t1, t2, t3); // usa valores: t2.value, t3.value e ret_val O código do servidor terá de alocar todos os objectos devolvidos pelo método: 15 public tipo0 op(tipo1 p_in, tipo2Holder p_inout, tipo3Holder p_out) { // usa p_inout.value e p_in … p_inout.value= new tipo2(); // Modifica valor de p_inout p_out.value= new tipo3(); // Preenche valor de p_out return new tipo0(…/*preenche valor retornado*/); } 3.5. Serviço de Nomes O serviço de nomes permite associar um ou mais nomes do tipo CosNaming.Name a um objecto, possibilitando a sua modificação ao longo do tempo. O nome é constituído por uma sequência de componentes, cada um composto por dois campos (id e kind): Module CosNaming { typedef string Istring; struct NameComponent { Istring id; Istring kind; }; // array de tamanho variável typedef Sequence<NameComponent> Name; }; Embora a norma defina dois campos, normalmente deixa-se o campo kind vazio, usandose apenas um nome para cada elemento da sequência (ex. 'unl/dee/rit2/grupo2'). A uma cadeia de nomes pode estar associada uma sequência de servidores de nomes, cada um responsável por cada contexto. A linguagem Java oferece uma maneira simplificada para lidar com os nomes. A classe NamingContextExt permite converter nomes entre o formato CosNaming.Name e uma string: import org.omg.CosNaming.*; NamingContextExt root= …; // referência para servidor nomes org.omg.CosNaming.Name name; String str= "rit2/leilao"; try { name= root.to_name(str); } catch (org.omg.CosNaming.NamingContextPackage.InvalidName e) { …// Nome inválido } Antes de usar o serviço de nomes, tem de se obter uma referência para um objecto servidor de nomes. Na maior parte dos ORB é possível obter a referência para o servidor de nomes local ao ORB usando um método do ORB para pesquisar o serviço "NameService". No entanto, para que o método funcione, o programa tem de receber um parâmetro a definir o IP e porto do servidor de nomes, ou o ORB tem de ser configurado. No JDK 1.4.2 esta definição é preenchida automaticamente. No OpenORB 1.4.0 é necessário fornecer o IP e porto do servidor de nomes. NamingContextExt root; try { root= NamingContextExtHelper.narrow ( orb.resolve_initial_references("NameService") ); if (root == null) { /* falhou associação a servidor de nomes */ } } catch(org.omg.CORBA.SystemException e) { // … trata erro … } 16 Por vezes, é necessário escolher qual é o servidor de nomes que prendemos usar como raiz do espaço de nomes. Neste caso, é possível usar uma forma compacta de IOR para definir uma referência para o servidor de nomes inicial "corbaloc:iiop:[email protected]:20000/NameService". Caso seja fornecida uma string ns com o endereço IP e o porto (e.g. 172.16.33.1:20000) poderse-ia fazer a inicialização com o seguinte código: NamingContextExt root; try { root= NamingContextExtHelper.narrow ( orb.string_to_object("corbaloc:iiop:1.2@" + ns + "/NameService")); if (root == null) { /* a referência está incorrecta */ } } catch ( org.omg.CORBA.BAD_PARAM e) { … // Nome inválido } catch(org.omg.CORBA.SystemException e) { // … trata erro … } A interacção com o serviço usa funções da interface CosNaming::NamingContext para realizar três tipos de acções: • registo de nomes; • pesquisa de nomes; • cancelamento de nomes. A associação de um objecto (Hello ola) a um nome pode ser realizado utilizando a função bind ou a função rebind. A diferença está no que ocorre quando se tenta registar um nome que já existe. No primeiro caso (representado) devolve uma excepção (CosNaming.AlreadyBound) enquanto que com rebind substitui o registo anterior. NamingContextExt root= …; String nome= …; // Regista nome try { root.bind(root.to_name(nome), ola); } catch (org.omg.CosNaming.NamingContextPackage.InvalidName e) { … // nome inválido } catch (org.omg.CosNaming.NamingContextPackage.NotFound e) { … // Contexto não existe ? } catch (org.omg.CosNaming.NamingContextPackage.CannotProceed e) { … // Falhou registo } catch (org.omg.CosNaming.NamingContextPackage.AlreadyBound e) { … // só para bind! } catch (SystemException e) { … // Falha na comunicação com servidor de nomes } catch (Exception e) { … // Erro } Caso um nome tenha vários componentes, convém criar o contexto (com bind_new_context) com o caminho até ao último nome antes de registar um novo nome: 17 // Testa se contexto está criado int n= nome.lastIndexOf('/'); if (n > -1) { String pnome= nome.substring(0, n); try { root.bind_new_context(root.to_name(pnome)); } catch( AlreadyBound ab ) { // Ignore } catch (SystemException e) { … // Falha na comunicação com servidor de nomes } catch (Exception e) { … // Erro } } A pesquisa de nomes usa a função resolve para localizar um nome, podendo retornar a excepção CosNaming.NotFound caso o nome não exista. Após obter a referência obj, é necessário convertê-la para uma referência para o tipo pretendido utilizando uma mudança explícita de tipo com o método narrow da classe Helper correspondente. try { org.omg.CORBA.Object obj= root.resolve(root.to_name(nome)); } catch (org.omg.CosNaming.NamingContextPackage.InvalidName e) { … // Nome inválido } catch (org.omg.CosNaming.NamingContextPackage.NotFound e) { … // Não existe contexto } catch (org.omg.CosNaming.NamingContextPackage.CannotProceed e) { … // Falhou operação } catch (SystemException e) { … // Falhou comunicação com servidor } catch (Exception e) { … // Erro } O cancelamento de nomes usa a função unbind para libertar o nome, podendo retornar a excepção CosNaming.NotFound caso o nome não exista: try { root.unbind(root.to_name(nome)); } catch (org.omg.CosNaming.NamingContextPackage.InvalidName e) { … // Nome inválido } catch (org.omg.CosNaming.NamingContextPackage.NotFound e) { … // Não existe contexto } catch (org.omg.CosNaming.NamingContextPackage.CannotProceed e) { … // Falhou operação } catch (SystemException e) { … // Falhou comunicação com servidor } catch (Exception e) { … // Erro } Recomenda-se uma leitura do exemplo 5.1 sobre o serviço de nomes. 3.6. Serviço de Eventos O serviço de eventos suporta a comunicação anónima entre objectos. A comunicação é realizada através do envio de mensagens de tipo genérico (any) através de um canal de eventos. Cada canal de eventos está associado a um servidor, identificado por um nome, que oferece interfaces procuradoras, tanto para os fornecedores como para os consumidores de eventos. 18 canal eventos fornecedor consumidor notificação notificação procurador consumidor notificação procurador fornecedor O serviço suporta dois modelos de interacção: • Push (empurrar): os consumidores registam uma interface PushConsumer no canal de eventos; os fornecedores invocam o método "push" sobre o procurador de consumidor; O canal de eventos invoca o mesmo método sobre os consumidores. • Pull (puxar): os fornecedores registam uma interface PullSupplier no canal de eventos; 2. os consumidores invocam o método "pull" sobre o procurador de fornecedor; O canal de eventos invoca o mesmo método sobre os fornecedores ou usa mensagens recebidas anteriormente. No trabalho apenas vai ser usado o modelo Push, sendo usadas as duas seguintes interfaces para o envio e recepção de eventos: module CosEventCom { exception Disconnected {}; … interface PushConsumer { void push(in any data) raises(Disconnected); void disconnected_push_consumer(); }; interface PushSupplier { void disconnected_push_supplier(); }; }; Cada fornecedor oferece uma interface do tipo PushSupplier e cada consumidor uma interface do tipo PushConsumer. Para além do método push, para enviar eventos, as interfaces oferecem métodos para os fornecedores e consumidores possam ser desligados do canal. Os canais de eventos oferecem uma interface CosEventChannelAdmin que permite obter as referências para os procuradores de consumidores e de fornecedores, e estabelecer as ligações ao canal: module CosEventChannelAdmin { exception AlreadyConnected {}; exception TypeError {}; interface ProxyPushSupplier : CosEventComm::PushSupplier { void connect_push_consumer( in CosEventComm::PushConsumer push_consumer ) raises(AlreadyConnected, TypeError); }; interface ProxyPushConsumer : CosEventComm::PushConsumer { void connect_push_supplier( in CosEventComm::PushSupplier push_supplier ) raises(AlreadyConnected); }; … interface ConsumerAdmin { ProxyPushSupplier obtain_push_supplier(); 19 … }; interface SupplierAdmin { ProxyPushConsumer obtain_push_consumer(); … }; interface EventChannel { ConsumerAdmin for_consumers(); SupplierAdmin for_suppliers(); }; }; Um consumidor de eventos começa por invocar o método for_consumers de modo a obter uma referência para uma interface de gestão (ConsumerAdmin), onde é possível obter uma referência para a interface de um objecto procurador de fornecedor. Por fim, nesta interface cria uma ligação ao canal com o método connect_push_consumer. Um fornecedor de eventos começa por invocar o método for_suppliers de modo a obter uma referência para uma interface de gestão (SupplierAdmin), onde é possível obter uma referência para a interface de um objecto procurador de consumidor. Por fim, caso pretenda ser conhecido pelo procurador (para ser avisado em caso de desconexão), cria uma ligação ao canal com o método connect_push_supplier. O OpenORB oferece uma interface adicional para se poder criar dinamicamente canais (org.openorb.event.EventChannelFactory). ORB orb = …; POA poa = …; org.omg.CORBA.Object obj= null; org.omg.CosEventChannelAdmin.EventChannel canal = null; try { obj = orb.string_to_object( "corbaname:rir:#COS/EventService/EventChannelFactory" ); } catch ( org.omg.CORBA.BAD_PARAM e) { … // Nome inválido } catch(org.omg.CORBA.SystemException e) { … // Trata erro } if ( obj == null ) { … // Serviço de eventos não está activo } // Cria ou associa-se a canal com nome "Rit2" try { // Tenta criar o canal canal = org.openorb.event.EventChannelFactoryHelper.narrow( obj ).create_channel( "Rit2" ); System.out.println("Created channel Rit2\n"); } catch ( org.openorb.event.NameAlreadyUsed ex ) { try { // Associa-se a canal canal = org.openorb.event.EventChannelFactoryHelper.narrow( obj ).join_channel( "Rit2" ); System.out.println("Joined channel Rit2\n"); } catch ( Exception e ) { … // Erro } } catch ( Exception e ) { … // Erro } 20 Recomenda-se uma leitura do exemplo 5.2 sobre a utilização destas interfaces num fornecedor e num consumidor de eventos. 4. OPENORB O OpenORB 1.4.0 é uma realização da arquitectura CORBA em código aberto, compatível com a norma 2.4.2, e disponível na Internet a partir do endereço http://openorb.sourceforge.net. O pacote inclui o ORB e um conjunto alargado de serviços: • Serviço de nomes; • Serviço de trading; • Serviço de Concorrência e Controlo; • Serviço de eventos; • Serviço de Notificação; • Serviço de persistência de estado; • Serviço de propriedades; • Serviço de tempo; • Serviço de transacções. No trabalho de laboratório vai-se usar apenas o ORB e o serviço de trading. Estes pacotes podem ser descarregados a partir de http://sourceforge.net/project/showfiles.php?group_id=43608. Sugere-se que crie a directoria /opt/CORBA; descarregue os ficheiros no formato tgz; e descomprima os ficheiros para a directoria utilizando o comando "tar xzf nome.tgz". Os ficheiros contêm um conjunto de ficheiros jar com as classes do núcleo ORB e dos serviços. Para correr estes serviços é necessário ter um JDK 1.3 ou 1.4, a variável de ambiente JAVA_HOME definida, e acrescentar a variável de ambiente TCOO_HOME com a directoria raiz da instalação do OpenORB. O pacote tools inclui um utilitário para realizar a configuração automática para um interpretador de linha de comandos bash em Linux: "source /opt/CORBA/tools/bin/setenv" Antes de usar o OpenORB falta apenas indicar ao JDK qual o ORB activo (no manual do OpenORB, em http://openorb.sourceforge.net/docs/1.4.0/OpenORB/doc/orb.html#N10469, são indicados outros métodos alternativos para realizar esta configuração): cp /opt/CORBA/OpenORB/config/orb.properties $JAVA_HOME/jre/lib Para compilar um ficheiro IDL (Nome.idl) usa-se a seguinte linha de comando: idl2java -d . Nome.idl O servidor de nomes é lançado com a seguinte linha de comandos (no exemplo define o número de porto). Pode usar o comando ins –h para ver outros parâmetros. ins -ORBPort=2001 -u Para compilar um ficheiro ST (Nome.st) usa-se a seguinte linha de comando: stdl2java –export -d . Nome.st O servidor de trading é lançado com a seguinte linha de comandos (o número de porto pode ser modificado). O servidor de trading deve ser lançado após o lançamento do servidor de nomes. tradersvc -ORBPort=2002 -u 21 O ambiente NetBeans não suporta o desenvolvimento integrado de aplicações CORBA, obrigando a realizar a compilação dos ficheiros IDL através da linha de comandos. A partir desse momento, o desenvolvimento da aplicação passa a poder ser feito no NetBeans desde que se acrescente os ficheiros jar presentes na lista de classes (CLASSPATH) ao projecto. 5. EXEMPLOS DE APLICAÇÕES Nesta secção são descritas duas aplicações que ilustram a utilização do CORBA com serviço de nomes e do serviço de eventos, e do ambiente de programação. 5.1. Aplicação cliente-servidor utilizando o serviço de nomes Esta secção ilustra o desenvolvimento de uma aplicação CORBA que usa serviço de nomes no OpenORB, passo a passo, de forma a se aprender a usar o ambiente. Pretende-se criar uma aplicação muito simples com um servidor gráfico e um cliente em modo de linha de comando, que realiza um único método, sayHello, que escreve no ecrã do servidor "Olá mundo …" e retorna a string para o cliente. O primeiro passo é criar um ficheiro Hello.idl : module HelloApp { interface Hello { string sayHello(); }; }; 5.1.1. Cliente Começa-se por compilar o ficheiro Hello.idl, e em seguida entra-se no NetBeans para introduzir a classe HelloClient com o seguinte código: import import import import org.omg.CORBA.*; org.omg.CosNaming.*; org.omg.CosNaming.NamingContextPackage.*; HelloApp.*; // The package containing our stubs. public class HelloClient { /** Returns a reference to the naming server */ private static NamingContextExt get_nameserver(ORB orb, String loc) { try { org.omg.CORBA.Object obj = orb.string_to_object( "corbaloc:iiop:1.2@"+loc+"/NameService"); if ( obj == null ) { System.out.println("The NamingService reference is incorrect."); System.out.println("Check if the Naming server is running."); return null; } NamingContextExt root = NamingContextExtHelper.narrow(obj); if (root == null) { System.out.println("The Naming service root cannot be found."); System.out.println("Check if the Naming server is running."); return null; } 22 return root; } catch (SystemException e) { System.out.println("The Naming service root cannot be found."); System.out.println("Check if the Naming server is running."); return null; } } public static void main(String args[]) { if (args.length != 2) { System.out.println("Usage: java HelloClient NamingServer ServerName"); System.out.println( " e.g. java HelloClient 127.1.1.1:2001 rit2/teste"); return; } try{ // Create and initialize the ORB ORB orb= ORB.init(args, null); NamingContextExt root= get_nameserver(orb, args[0]); if (root == null) return; // Resolve the object reference in naming Hello helloRef; try { helloRef= HelloHelper.narrow(root.resolve(root.to_name(args[1]))); } catch (SystemException e) { System.out.println("ERROR - Server not available : " + e); return; } // Call the Hello server object and print results String hello; try { hello = helloRef.sayHello(); } catch (SystemException e) { System.out.println("Exception during invocation of server: "+e); return; } System.out.println("Server returned: '"+hello+"'"); } catch (Exception e) { System.out.println("Exception: "+e); } } } São definidas duas funções estáticas nesta classe: • a função get_nameserver obtém a referência para o servidor de nomes; • a função main inicializa o ORB e as variáveis iniciais, usando os argumentos da linha de comando (args), pesquisa o serviço de nomes obtendo uma referência para o servidor, invoca a operação, e finalmente, escreve a string recebida. Recebe com o argumentos a localização do servidor de nomes e o nome do objecto a invocar. 5.1.2. Servidor O servidor vai ser desenvolvido como uma aplicação gráfica. Assim, começa-se por criar uma nova janela (HelloServer do tipo JForm), e por mudar a propriedade Layout para "Y 23 axis". Em seguida deve-se acrescentar um JPanel e um JScroolPanel. Dentro do JPanel deve-se colocar sucessivamente: • um JLabel, com a string "Nome: "; • um JTextField, com a string "rit2/teste", cujo nome deve ser mudado para jTextNome; • um JLabel, com a string " NS: " • um JTextField, com a string "127.1.1.1:20001", cujo nome deve ser mudado para jTextNS; • um JToggleButton1, com a string "Activo"; A JScroolPanel deve ser dimensionada para [333,200]. Dentro do JScroolPanel deve-se colocar uma JTextArea, dimensionada para [315,2000]. Passando agora à edição do ficheiro, deve-se começar por definir a lista de ficheiros a incluir: import import import import import org.omg.CORBA.*; org.omg.CosNaming.*; org.omg.CosNaming.NamingContextPackage.*; org.omg.PortableServer.*; HelloApp.*; // The package containing our stubs. Dentro da classe pode-se começar por definir a função Log e todas as variáveis da classe: public void Log(String s) { System.out.print(s); jTextArea1.append(s); } public corba_thread c_thread; HelloImpl helloImpl; public ORB orb; public POA poa; // POA thread // Hello object implementation O arranque do servidor é feito em dois passos: No construtor e na função main arranca-se com o ORB, o POA e com a thread que vai receber as invocações CORBA. /** Creates new form HelloServer */ public HelloServer(String args[]) { initComponents(); orb= corba_thread.init_orb(args); // Start ORB and POA c_thread= new corba_thread(orb); poa= c_thread.poa(); c_thread.start(); // Start CORBA thread - ready to start local objects } public static void main(String args[]) { new HelloServer(args).show(); } O arranque do objecto CORBA é realizado quando se liga o jToggleButton. Após a criação do objecto, ele é registado no serviço de nomes. Quando se desliga o botão, cancela-se este registo e desliga-se o objecto. Esta função usa uma função get_nameserver semelhante à declarada anteriormente no cliente, mas que nesta classe deve ser declarada como um método do objecto (sem static), e sem o parâmetro orb. 24 private void jToggleButton1ActionPerformed(java.awt.event.ActionEvent evt) { // Get naming server reference NamingContextExt root= get_nameserver(jTextNS.getText()); if (root == null) { jToggleButton1.setSelected(false); return; } if (jToggleButton1.isSelected()) { // Create server object helloImpl = new HelloImpl(this); org.omg.CORBA.Object helloRef= null; try { // Regist it in POA helloRef= poa.servant_to_reference(helloImpl); } catch(org.omg.PortableServer.POAPackage.ServantNotActive e) { Log("Object is not active "+e+"\n"); } catch(org.omg.PortableServer.POAPackage.WrongPolicy e) { Log("Invalid POA policy: "+e+"\n"); } if (helloRef == null) { Log("Failed to activate Hello object\n"); jToggleButton1.setSelected(false); return; } try { // Get context name (directory where name is stored) String context= jTextNome.getText(); int n= jTextNome.getText().lastIndexOf("/"); if (n != -1) { context= context.substring(0,n); } try { // make sure context is bound root.bind_new_context(root.to_name(context)); } catch( AlreadyBound ab ) { // Ignore } // Bind the object reference in naming try { root.bind(root.to_name(jTextNome.getText()), helloRef); } catch( AlreadyBound ab ) { Log("Did not bound name: "+ab); // EXERCÍCIO: CORRIGIR ESTA LIMITAÇÃO } Log("Server is running\n"); } catch (Exception e) { Log("Failed to register object name: " + e + "\n"); // EXERCÍCIO: FALTA DESREGISTAR DO POA, E LIBERTAR REFERÊNCIAS // PARA OBJECTO } } else { // Unregister object in Naming service try { root.unbind(root.to_name(jTextNome.getText())); } catch (org.omg.CosNaming.NamingContextPackage.InvalidName e) { Log("Invalid name : " + e + "\n"); } catch (org.omg.CosNaming.NamingContextPackage.NotFound e) { Log("Server not found : " + e + "\n"); } catch (org.omg.CosNaming.NamingContextPackage.CannotProceed e) { Log("Cancelation cannot proceed : " + e); } catch (Exception e) { 25 Log("Failed to cancel object name: " + e + "\n"); } // Unregister object in the POA try { poa.deactivate_object(poa.servant_to_id(helloImpl)); } catch (org.omg.PortableServer.POAPackage.ServantNotActive e) { Log("Invalid hello object: " + e); } catch (org.omg.PortableServer.POAPackage.ObjectNotActive e) { Log("Hello object not active: " + e); } catch (org.omg.PortableServer.POAPackage.WrongPolicy e) { Log("Invalid POA: " + e); } catch (SystemException e) { Log("Error deactivating object: "+e+"\n"); } // Free server object - activates garbage collecting helloImpl= null; Log("Server stopped\n"); } } Finalmente, falta programar a classe HelloImpl que realiza a função definida na interface HelloApp.HelloOperations.java. O construtor desta função recebe um apontador para a janela principal, para poder invocar a operação Log, que ecoa uma string para a janela. /** HelloImpl.java */ import HelloApp.*; public class HelloImpl extends HelloPOA { HelloServer server; HelloImpl(HelloServer srv) { server= srv; } public String sayHello() { server.Log("Server received invocation 'sayHello()'\n"); return "Olá Mundo! / Hello world!"; } } 5.1.3. Exercício O servidor realizado falha quando já existe outro objecto registado com o mesmo nome. Modifique o programa de maneira a passar a funcionar sempre, informando o utilizador quando substituir um registo. 5.2. Aplicação utilizando o serviço de eventos Esta secção ilustra a utilização do serviço de eventos numa aplicação que corre na linha de comando. Novamente são fornecidas as instruções para realizar, passo a passo, uma aplicação com um emissor de eventos do tipo string, e de um receptor que recebe esses dados através de um canal. Neste caso não é necessário usar nenhum ficheiro IDL, uma vez que só vão ser usados tipos primitivos da arquitectura CORBA. Este exemplo resultou da modificação de um exemplo distribuído com a plataforma OpenORB, da autoria de Jerome Daniel e Olivier Modica. 26 5.2.1. Emissor O programa emissor tem um funcionamento simples: no início, liga-se ao canal e fica em ciclo infinito à espera que o utilizador introduza uma string através do teclado. Sempre que isso acontece envia a string para o canal de eventos. A programação do programa emissor (ou fornecedor) é realizado em três classes: a classe PushServer contém a função main e uma classe interna Event_Thread, que realiza o ciclo de envio de eventos, e a classe myPushSupplier que realiza o objecto de ligação ao procurador de fornecedor no canal. Foi necessário utilizar uma tarefa porque vão ser usados dois ciclos: o do POA e o de leitura do teclado. Sempre que se realiza um emissor de eventos é necessário fornecer ao canal uma referência para um objecto de ligação que realiza a interface org.omg.CosEventComm. PushSupplier. No emissor, este objecto recebe a invocação do canal a informar que a ligação foi desligada. public class myPushSupplier extends org.omg.CosEventComm.PushSupplierPOA { private org.omg.CORBA.ORB orb; /** Reference to the consumer */ private org.omg.CosEventComm.PushConsumer consumer; /** Constructor */ public myPushSupplier( org.omg.CosEventComm.PushConsumer consumer ) { this.consumer = consumer; } /** Disconnection from supplier */ public void disconnect_push_supplier() { consumer.disconnect_push_consumer(); consumer = null; } } A classe PushServer faz as inicializações do ORB, do POA, do canal, do objecto myPushSupplier, e estabelece a ligação ao canal. Finalmente, arranca com a tarefa que envia os eventos e fica em ciclo infinito à espera de receber invocações do canal: public class PushServer { public static void main( String args [] ) { org.omg.CORBA.Object obj = null; org.omg.CosEventChannelAdmin.EventChannel canal = null; org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init( args, null ); org.omg.PortableServer.POA rootPOA = null; try { obj = orb.resolve_initial_references( "RootPOA" ); } catch ( org.omg.CORBA.ORBPackage.InvalidName ex ) { ex.printStackTrace(); System.exit( 1 ); } rootPOA = org.omg.PortableServer.POAHelper.narrow( obj ); try { rootPOA.the_POAManager().activate(); 27 } catch ( java.lang.Exception ex ) { System.out.println( "Internal error..." ); ex.printStackTrace(); } System.out.println( "Event Service example" ); System.out.println( "Push model / server side" ); try { obj = orb.string_to_object( "corbaname:rir:#COS/EventService/EventChannelFactory" ); } catch ( org.omg.CORBA.BAD_PARAM ex ) { System.out.println( "The EventService cannot be found." ); System.out.println( "Check if the Event and Naming servers are running." ); System.exit( -1 ); } catch ( java.lang.Exception ex ) { System.out.println( "The EventService cannot be found." ); System.out.println( "Check if the Event and Naming servers are running." ); System.exit( -1 ); } if ( obj == null ) { System.out.println( "The EventService reference is incorrect." ); System.out.println( "Check if the Event and Naming servers are running." ); System.exit( -1 ); } try { canal = org.openorb.event.EventChannelFactoryHelper.narrow( obj ).create_channel( "Rit2" ); System.out.println("Created channel Rit2\n"); } catch ( org.openorb.event.NameAlreadyUsed ex ) { try { canal = org.openorb.event.EventChannelFactoryHelper.narrow( obj ).join_channel( "Rit2" ); System.out.println("Joined channel Rit2\n"); } catch ( java.lang.Throwable error ) { error.printStackTrace(); System.exit( -1 ); } } catch ( org.omg.CORBA.SystemException e ) { error.printStackTrace(); System.exit( -1 ); } org.omg.CosEventChannelAdmin.SupplierAdmin supplierAdmin = canal.for_suppliers(); // Get administration object org.omg.CosEventChannelAdmin.ProxyPushConsumer consumer = supplierAdmin.obtain_push_consumer(); // Get proxy reference // Make local supplier object myPushSupplier supplier = new myPushSupplier( consumer ); try { consumer.connect_push_supplier( supplier._this( orb ) ); /* supplier._this( orb ) is a compact notation for: PushSupplierHelper.narrow(rootPOA.servant_to_reference(supplier)) */ } catch ( java.lang.Exception ex_connect ) { System.out.println( "Unable to connect to consumer" ); ex_connect.printStackTrace(); } Event_Thread t= new Event_Thread(orb, consumer); t.start(); orb.run(); /* ORB Loop */ } } 28 A classe Event_Thread define um ciclo infinito, que lê uma linha do teclado, prepara um evento e envia-o para o canal. /** Event supplier thread */ class Event_Thread extends Thread { private org.omg.CosEventComm.PushConsumer consumer; private org.omg.CORBA.ORB orb; /** Constructor */ public Event_Thread(org.omg.CORBA.ORB orb, org.omg.CosEventComm.PushConsumer consumer) { this.orb= orb; this.consumer= consumer; } public void run() { org.omg.CORBA.Any any = orb.create_any(); String s = null; while ( true ) { java.io.InputStreamReader inread = new java.io.InputStreamReader( System.in ); java.io.BufferedReader reader = new java.io.BufferedReader( inread ); System.out.print( "Message to send : " ); try { s = reader.readLine(); } catch ( java.io.IOException ioex ) { any.insert_string( "Error at PushSupplier" ); } any.insert_string( s ); try { consumer.push( any ); } catch ( java.lang.Exception ex ) { System.out.println( "End of PushSupplier" ); ex.printStackTrace(); return ; } } } } 5.2.2. Receptor O programa receptor tem um funcionamento simples: no início liga-se ao canal e regista o objecto local receptor de eventos; de seguida fica em ciclo infinito no ORB à espera de receber eventos. A programação do programa receptor (ou consumidor) é realizada em duas classes: a classe PushClient contém a função main, que faz as inicializações, e a classe myPushConsumer que recebe os eventos do canal. Sempre que se realiza um receptor de eventos é necessário fornecer ao canal uma referência para um objecto de ligação que realiza a interface org.omg.CosEventComm. PushClient. No receptor, este objecto recebe as invocações do canal com eventos e a informar que a ligação foi desligada. 29 class myPushConsumer extends org.omg.CosEventComm.PushConsumerPOA { private org.omg.CosEventComm.PushSupplier supplier; /** Constructor */ public myPushConsumer( org.omg.CosEventComm.PushSupplier supplier ) { this.supplier = supplier; } /** Disconnection from consumer */ public void disconnect_push_consumer() { supplier.disconnect_push_supplier(); supplier = null; } /** Receive event */ public void push( org.omg.CORBA.Any any ) { String s = any.extract_string(); System.out.println( "Received : " + s ); } } A classe PushClient faz as inicializações do ORB, do POA, do canal, do objecto myPushConsumer, e estabelece a ligação ao canal. Finalmente, fica em ciclo infinito à espera de receber eventos: public class PushClient { public static void main( String args [] ) { org.omg.CORBA.Object obj = null; org.omg.CosEventChannelAdmin.EventChannel canal = null; System.out.println( "Event Service example" ); System.out.println( "Push model / client side" ); org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init( args, null ); org.omg.PortableServer.POA rootPOA = null; try { obj = orb.resolve_initial_references( "RootPOA" ); } catch ( org.omg.CORBA.ORBPackage.InvalidName ex ) { ex.printStackTrace(); System.exit( 1 ); } rootPOA = org.omg.PortableServer.POAHelper.narrow( obj ); try { rootPOA.the_POAManager().activate(); } catch ( java.lang.Exception ex ) { System.out.println( "Internal error..." ); ex.printStackTrace(); } try { obj = orb.string_to_object("corbaname:rir:#COS/EventService/EventChannelFactory" ); } catch ( org.omg.CORBA.BAD_PARAM ex ) { System.out.println( "The EventService cannot be found." ); System.out.println( "Check if the Event and Naming servers are running." ); System.exit( -1 ); 30 } catch ( org.omg.CORBA.SystemException e ) { System.out.println( "The EventService cannot be found." ); System.out.println( "Check if the Event and Naming servers are running." ); System.exit( -1 ); } if ( obj == null ) { System.out.println( "The EventService reference is incorrect." ); System.out.println( "Check if the Event and Naming servers are running." ); System.exit( -1 ); } try { canal = org.openorb.event.EventChannelFactoryHelper.narrow( obj ).create_channel( "Rit2" ); System.out.println("Created channel Rit2\n"); } catch ( org.openorb.event.NameAlreadyUsed ex ) { try { canal = org.openorb.event.EventChannelFactoryHelper.narrow( obj ).join_channel( "Rit2" ); System.out.println("Joined channel Rit2\n"); } catch ( java.lang.Throwable error ) { error.printStackTrace(); System.exit( -1 ); } } catch (org.omg.CORBA.SystemException e) { System.out.println("Failed channel creation: "+e); System.exit( -1 ); } org.omg.CosEventChannelAdmin.ConsumerAdmin consumerAdmin = canal.for_consumers(); // Get administration object org.omg.CosEventChannelAdmin.ProxyPushSupplier supplier = consumerAdmin.obtain_push_supplier(); // Get proxy reference myPushConsumer consumer = new myPushConsumer( supplier ); try { supplier.connect_push_consumer( consumer._this( orb ) ); /* consumer._this( orb ) does the same thing as: PushConsumerHelper.narrow(rootPOA.servant_to_reference(consumer)) */ } catch ( java.lang.Exception ex_connect ) { System.out.println( "Unable to connect to consumer" ); ex_connect.printStackTrace(); } orb.run(); /* ORB Loop */ } } 5.2.3. Exercício Modifique o serviço de eventos de maneira a passar a enviar uma estrutura com um número do tipo short e uma string. Para realizar este exercício necessita de criar um ficheiro IDL com a declaração da estrutura: struct Tipo { string str; short num; }; 31 7. BIBLIOGRAFIA ADICIONAL Este documento resume uma pequena parte das especificações CORBA necessária para a realização do trabalho prático. Caso necessite de mais informação do que a fornecida por este documento recomenda-se a consulta de: "Java Programming with CORBA", de Gerald Brose, Andreas Vogel, e Keith Duddy, John Wiley & Sons, Inc., 2001, ISBN: 0-471-37681-7. Disponível da biblioteca da FCT-UNL. Manuais online do OpenORB disponíveis a partir do URL http://openorb.sourceforge.net e nos computadores do laboratório 32