
Visão Geral
No artigo original que introduziu o RMI ao mundo, entitulado "A Distributed Object Model for the Java System", os autores dedicam aproximadamente 4 páginas para descrever, em alto nível, a estruturação básica da arquitetura do modelo. De maneira geral, um sistema RMI é dividido em três camadas: a de stub/esqueleto, a de referencimento remoto e a de transporte. O diagrama abaixo pode nos ajudar a entender melhor esta arrumação.

Uma invocação remota de um cliente para um servidor viaja de cima para baixo nas camadas do cliente até chegar à de transporte, que por sua vez leva os dados para o servidor. A invocação, então, viaja de baixo para cima até ser processada. O trabalho das diferentes camadas é descrito com mais detalhes abaixo, mas já podemos imaginar que, para que o nosso sistema funcione, precisaremos de uma camada que nos dê a interface do objeto remoto, outra que cuide das semânticas de nossa transação e, por fim, uma camada que gerencie a conexão do modelo. De fato, veremos que cada uma das três camadas apresentadas acima é responsável por basicamente uma dessas atribuições.
Camada de Stub/Esqueleto
A camada que realiza a interface entre a aplicação e o restante do sistema RMI é chamada de stub/esqueleto. Sua responsabilidade é transmitir dados para a camada de referenciamento remoto através da abstração de marshal streams.
A palavra marshalling, em ciência da computação, representa o processo de transformar as informações de um objeto encontrado na memória para uma representação de dados que seja mais adequada para finalidades de, em geral, armazenamento ou tráfego. Há muitas similaridades com a serialização de um objeto, ou seja, transformar estruturas de dados em uma sequência de bits que podem ser posteriormente reconstruídos, mas utilizamos o termo marshalling para especificamente nos referir a objetos remotos. Tipicamente, implementamos as técnicas desta área para mover dados de um programa para o outro, que podem ou não estar na mesma máquina.
Dessa forma, um marshal stream do RMI se preocupa exatamente com esta conversão de dados para um formato trafegável na rede e possível de ser posteriormente reconstruído, levando ainda em consideração os diferentes espaços de endereçamento no qual os objetos se encontram. Vale lembrar que esta não é uma tarefa fácil: por exemplo, a codificação de bits na memória em diferentes máquinas pode variar. Codificações big-endian (bit mais significativo é guardado no menor endereço, implementado na arquitetura Intel x86) e little-endian (bit menos significativo é guardado no menor endereço, implementado em algumas arquiteturas da IBM) são uma das dificuldades que os projetistas devem ter em mente ao propor um sistema.

Por fim, devemos nos atentar para o fato da camada stub/esqueleto ter funções distintas entre o cliente e o servidor, levando à diferenciação do nome stub para o cliente e esqueleto para o servidor. Por exemplo, um stub do lado do cliente é responsável por:
- iniciar uma chamada para o objeto remoto (chamando a camada de referenciamento remoto);
- fazer marhsalling dos argumentos para um marshal stream;
- informar à camada de referenciamento remoto que a chamada deve ser invocada;
- fazer unmarshalling do valor de retorno contido em um marshal stream;
- informar à camada de referenciamento remoto que a chamada foi completada;
Por outro lado, um esqueleto é uma entidade existente no lado do servidor que contém um método que despacha chamadas para a implementação do objeto remoto. Suas responsabilidades, portanto, são:
- fazer unmarshalling dos argumentos contidos em marshal streams;
- realizar a chamada para a verdadeira implementação do objeto remoto;
- fazer marshalling do valor de retorno de uma chamada para um marshal stream;
Juntas, essas duas entidades formam a camada stub/esqueleto e entregam um grande poder de abstração para o programador, servindo como valiosas interfaces no sistema RMI.
Camada de Referenciamento Remoto
Tendo estabelecido uma interface, precisamos agora de uma camada que se comunique com as funções de transporte de baixo nível. Além disso, nossa Camada de Referenciamento Remoto deve ser responsável por implementar um protocolo de invocação que seja independente do stub do cliente e do esqueleto do servidor. A ideia é que cada objeto remoto escolha seu próprio protocolo de invocação, que será fixo por toda a vida do objeto. Alguns exemplos de protocolo são:
- invocação unicast;
- invocação multicast;
- suporte para uma estratégia de replicação específica;
- estratégias de reconexão (caso o objeto remoto se torne inacessível).
Tais protocolos não são mutualmente exclusivos e podem ser combinados. Por exemplo, um objeto remoto pode requerer uma invocação unicast junto com uma estratégia de reconexão específica, e ambos os procedimentos seriam implementados pela Camada de Referenciamento Remoto. Portanto, resta agora entender como o funcionamento de tais implementações funcionam de uma maneira geral no sistema RMI.
Um protocolo de invocação é dividido em duas partes que cooperam entre si, sendo uma situada no lado do cliente e, a outra, no do servidor. A parte no lado do cliente possui informações específicas do servidor remoto (ou servidores, caso a invocação seja destinada a um grupo multicast) e se comunica através da camada de transporte com a componente no lado do servidor. A seguir, vamos observar como ambos os lados utilizam esta oportunidade para intervir no processo de invocação e garantir as semânticas corretas de cada transação.
Suponha, por exemplo, que um objeto remoto seja parte de um grupo multicast. Neste caso, a Camada de Referenciamento Remoto do lado do cliente intervém e encaminha a invocação para um grupo ao invés de um único objeto remoto. De forma similar, a componente do lado do servidor também possui uma chance de intervenção antes de entregar uma invocação remota para o esqueleto, podendo, por exemplo, garantir uma entrega multicast atômica ao se comunicar com outras réplicas no grupo e utilizar algum algoritmo de coordenação. Assim, concluímos que nossa camada intermediária se torna essencial para garantir as diversas necessidades semânticas do sistema RMI.
Camada de Transporte
Enfim, chegamos à camada mais inferior do sistema RMI. De um modo geral, a componente de transporte é responsável por:
- configurar conexões com espaços de endereçamento remotos;
- administrar conexões;
- monitorar caso uma conexão caia;
- ouvir novas chamadas;
- manter uma tabela de objetos remotos que vivem no espaço de endereçamento local;
- configurar uma conexão para realizar uma nova chamada.
Uma grande parte da implementação desta camada é inspirada no sistema de transporte de objetos da linguagem Modula-3, uma ferramenta desenvolvida nos anos 80 com suporte a orientação a objetos que, mesmo tendo influenciado muitas outras linguagens (como Java e C#), nunca foi adotada na indústria de forma considerável. Baseando-nos neste sistema, podemos subdividir nossa camada de transporte em quatro abstrações básicas: endpoints, transporte, canal e conexão.
Um endpoint indica um espaço de endereçamento, que pode então ser mapeado para seu transporte. Assim, dado um endpoint, uma instância específica de transporte pode ser obtida. Por sua vez, a abstração de transporte gerencia canais, que nada mais são do que conexões virtuais entre dois espaços de endereçamento. Em um único transporte, existe um canal para cada par de espaços de endereçamento (local e remoto). Além disso, o transporte também fica responsável por aceitar e realizar as diversas chamadas entre servidor e cliente. Por fim, uma conexão será a nossa abstração para a transferência de dados (realizando input e output). Existe ainda a possibilidade de haver diferentes transportes entre os mesmos espaços de endereçamento, o que nos permite, por exemplo, ter suporte simultâneo a TCP e UDP entre as mesmas duas máquinas.
Isto conclui nossa análise da arquitetura do sistema RMI. Para mais informações, o artigo de Jim Waldo et al., entitulado "A Distributed Object Model for the Java System", entra em maiores detalhes arquiteturais acerca do RMI.