Capítulo 2 · Andrew S. Tanenbaum & Maarten Van Steen
Como organizar os componentes de software em sistemas distribuídos — estilos arquitetônicos, modelos cliente–servidor e arquiteturas peer-to-peer.
Uma arquitetura de software diz como os vários componentes de software devem ser organizados e como devem interagir.
A organização de sistemas distribuídos trata, em grande parte, dos componentes de software que constituem o sistema. Mas há uma distinção fundamental a fazer:
Organização lógica (arquitetura de software) ≠ realização física (arquitetura de sistema — onde os componentes são colocados nas máquinas reais).
Unidade modular com interfaces bem definidas, substituível dentro de seu ambiente (OMG, 2004b).
Mecanismo mediador da comunicação ou cooperação entre componentes — chamadas RPC, passagem de mensagem, fluxos de dados.
Topologia que descreve como componentes e conectores estão ligados para formar o sistema completo.
Formulado em termos de componentes, conectores, dados trocados e como esses elementos formam um sistema.
A especificação final de uma arquitetura de software é chamada arquitetura de sistema — ela vincula decisões lógicas a máquinas físicas.
Tanenbaum identifica quatro estilos principais para organizar sistemas distribuídos:
O modelo mais claro para lidar com a complexidade de sistemas distribuídos é pensar em termos de clientes que requisitam serviços de servidores.
Servidor: processo que implementa um serviço específico.
Cliente: processo que requisita o serviço enviando uma requisição e esperando a resposta.
Esse padrão — também chamado comportamento de requisição–resposta — pode usar protocolo sem conexão (UDP, eficiente) ou com conexão confiável (TCP, robusto).
Operações idempotentes podem ser reenviadas sem risco (ex.: "qual é o saldo?"). Operações não idempotentes não (ex.: "transfira R$ 10.000").
Nas arquiteturas cliente–servidor multidivididas, a distribuição dos clientes e servidores em máquinas diferentes é chamada distribuição vertical. Em contraste, o modelo P2P explora a distribuição horizontal:
Os processos que constituem um sistema peer-to-peer são todos iguais: cada processo pode agir como cliente e servidor ao mesmo tempo (comportamento de servente).
Os sistemas P2P se organizam em torno de uma rede de sobreposição — nós são os processos e as arestas representam os canais de comunicação TCP possíveis.
Nós e itens de dados recebem IDs aleatórios de 128–160 bits. Uma DHT mapeia cada chave ao nó responsável deterministicamente. Ex.: Chord organiza nós em anel; busca em O(log N) saltos.
Cada nó mantém lista aleatória de vizinhos (visão parcial). Busca por inundação — escalabilidade limitada. Ex.: Gnutella, BitTorrent (fase de busca).
Superpares (superpeers): nós selecionados que mantêm índices e intermediam a comunicação entre pares comuns. Hibridizam as vantagens de ambos os modelos — usados em CDNs colaborativas e KaZaA.
Middleware existe para esconder a heterogeneidade e prover transparência de distribuição. Mas a transparência total tem custo: desempenho e adaptabilidade sofrem. A solução é tornar o middleware adaptativo.
Quebram o fluxo usual de controle e permitem executar código específico de aplicação. Exemplo clássico: interceptor de chamada de objeto remoto que verifica localização atual antes de encaminhar.
Permitem que requisições sejam interceptadas, modificadas ou redirecionadas — sem alterar o código do cliente ou do servidor.
Abordagens para tornar o middleware flexível:
O sistema monitora seu próprio comportamento e toma providências quando necessário — sem intervenção humana direta.
Organizado como realimentações de contato: monitor → analisador → planejador → executor → componentes gerenciados.
IBM: "sistemas autonômicos computacionais"Ponto central do capítulo: todas as arquiteturas de software vistas — camadas, objetos, eventos, dados compartilhados — visam obter transparência de distribuição em um nível razoável. Como não há solução única, diferentes estilos são combinados conforme o requisito. (Tanenbaum & Van Steen, p. 32)
| Estilo | Acoplamento | Comunicação | Escalabilidade | Exemplo Prático | Trade-off Principal |
|---|---|---|---|---|---|
| Em Camadas | Médio | Síncrona (chamadas diretas) | Vertical | OSI, TCP/IP, HTTP | Rígida hierarquia; difícil pular camadas |
| Baseada em Objetos | Médio-alto | RPC / RMI | Moderada | CORBA, Java RMI | Encapsulamento x overhead de rede |
| Centrada em Dados | Baixo | Leitura/escrita em repositório | Alta (BD distribuído) | NFS, sistemas Web | Repositório pode ser gargalo |
| Baseada em Eventos | Muito baixo | Assíncrona (pub/sub) | Muito alta | Kafka, sistemas IoT | Depuração difícil; ordem de eventos |
| Cliente–Servidor | Alto | Requisição–resposta | Limitada (servidor único) | Web clássica, SSH | Ponto único de falha no servidor |
| Peer-to-Peer | Muito baixo | Simétrica (servente) | Horizontal | BitTorrent, Bitcoin | Consistência e descoberta complexas |
Na prática, sistemas distribuídos modernos combinam múltiplos estilos: um serviço pode usar camadas internamente, publicar eventos para outros serviços (pub/sub) e expor uma API REST no modelo cliente–servidor.
Implementar o comportamento de requisição–resposta descrito por Tanenbaum (Fig. 2.3) usando Sockets TCP em Java. O servidor fica aguardando conexões; o cliente envia uma string e recebe a resposta em maiúsculas.
// ServidorEcho.java — Lado servidor import java.net.*; import java.io.*; public class ServidorEcho { public static void main(String[] args) throws Exception { // 1. Cria socket de escuta na porta 9090 ServerSocket servidor = new ServerSocket(9090); System.out.println("Servidor aguardando na porta 9090..."); while (true) { // 2. Bloqueia até um cliente conectar Socket cliente = servidor.accept(); System.out.println("Cliente conectado: " + cliente.getInetAddress()); // 3. Lê requisição e envia resposta BufferedReader entrada = new BufferedReader( new InputStreamReader(cliente.getInputStream())); PrintWriter saida = new PrintWriter( cliente.getOutputStream(), true); String msg = entrada.readLine(); saida.println(msg.toUpperCase()); // processa e responde cliente.close(); } } }
// ClienteEcho.java — Lado cliente import java.net.*; import java.io.*; public class ClienteEcho { public static void main(String[] args) throws Exception { // 1. Conecta ao servidor Socket socket = new Socket("localhost", 9090); PrintWriter saida = new PrintWriter( socket.getOutputStream(), true); BufferedReader entrada = new BufferedReader( new InputStreamReader(socket.getInputStream())); // 2. Envia requisição saida.println("sistemas distribuídos"); // 3. Aguarda e exibe resposta String resposta = entrada.readLine(); System.out.println("Resposta do servidor: " + resposta); // → SISTEMAS DISTRIBUÍDOS socket.close(); } }
Relação com Tanenbaum: este código implementa literalmente a Fig. 2.3 do livro — cliente bloqueia aguardando resultado; servidor fornece o serviço; comunicação via TCP (protocolo orientado a conexão confiável, p. 22).
javac *.java → abra dois terminais → java ServidorEcho no primeiro → java ClienteEcho no segundo.
No estilo baseado em eventos, a ideia básica é que processos publiquem eventos após os quais o middleware assegura que somente os processos subscritos os receberão. Implementemos isso sem biblioteca externa.
import java.util.*; // Representa o "barramento de eventos" (Fig. 2.2a) public class BusDeEventos { // Mapa: tipo de evento → lista de assinantes private Map<String, List<Assinante>> assinantes = new HashMap<>(); public void subscrever(String evento, Assinante a) { assinantes.computeIfAbsent(evento, k -> new ArrayList<>()).add(a); } public void publicar(String evento, String dados) { List<Assinante> lista = assinantes.getOrDefault(evento, List.of()); // Middleware notifica APENAS os subscritos for (Assinante a : lista) a.notificar(evento, dados); } } // Interface do componente assinante interface Assinante { void notificar(String evento, String dados); }
public class DemoEventos { public static void main(String[] args) { BusDeEventos bus = new BusDeEventos(); // Componente A subscreve "PEDIDO_CRIADO" bus.subscrever("PEDIDO_CRIADO", (ev, d) -> System.out.println("[Estoque] processando: " + d)); // Componente B também subscreve o mesmo evento bus.subscrever("PEDIDO_CRIADO", (ev, d) -> System.out.println("[Pagamento] cobrando: " + d)); // Produtor publica — não conhece os assinantes! bus.publicar("PEDIDO_CRIADO", "Pizza#42"); /* Saída: [Estoque] processando: Pizza#42 [Pagamento] cobrando: Pizza#42 */ } }
Desacoplamento referencial: o publicador não precisa saber que Estoque e Pagamento existem. Adicionar um novo assinante (ex.: Notificações) não altera o publicador — exatamente o que Tanenbaum chama de componentes referencialmente desacoplados (p. 21).
Formulado em componentes, conectores e dados trocados. Os quatro principais: camadas, objetos, dados compartilhados, eventos.
Modelo centralizado: servidor implementa o serviço; cliente requisita. Organizado em 3 camadas lógicas: UI, processamento, dados.
Distribuição horizontal. Todos os nós iguais. DHT (estruturada) vs. aleatória (não estruturada). Superpares como híbrido.
Interceptores e reflexão computacional tornam o middleware flexível. Sistemas autonômicos auto-gerenciam sem humano.
Leitura obrigatória: Tanenbaum & Van Steen, Capítulo 2, pp. 20–41. Observe especialmente as Figuras 2.1, 2.3, 2.5, 2.7 e 2.12.
ServidorEcho para que cada cliente seja atendido em uma thread separada. Como isso altera a escalabilidade?Material elaborado com base em: TANENBAUM, A. S.; VAN STEEN, M. Sistemas Distribuídos: Princípios e Paradigmas. 2ª ed. Pearson Prentice Hall, 2007.