You are on page 1of 62

Programaode RedesdeComputadores emJava

RafaelSantos

Material reescrito para os alunos da disciplina CAP312 Programao de Redes de Computadores do programa de ps-graduao em Computao Aplicada do Instituto Nacional de Pesquisas Espaciais (INPE) ltima modicao: 17 de junho de 2006
http://www.lac.inpe.br/rafael.santos

ii

Sumrio
1 2 Introduo Programao Cliente-Servidor 2.1 Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Conceitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Desenvolvimento de aplicaes cliente-servidor em Java Clientes simples 4.1 Exemplo: Cliente de daytime . . . . . . . . . . . . . . . . . . . . . . 4.2 Exemplo: Cliente de echo . . . . . . . . . . . . . . . . . . . . . . . . Servidores simples (requisies no-simultneas) 5.1 Exemplo: Servidor de strings revertidas . . . . . . 5.2 Exemplo: Servidor e cliente de instncias de classes 5.3 Exemplo: Servidor e cliente de nmeros aleatrios 5.4 Exemplo: Servidor simples de arquivos . . . . . . 1 1 1 3 4 8 8 10 13 13 17 21 25 31 31 35 39 42 43 49 51 58

3 4

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

Servidores para mltiplas requisies simultneas 6.1 O problema de mltiplas requisies simultneas . . . . . . . . . . . . 6.2 Linhas de execuo (threads) . . . . . . . . . . . . . . . . . . . . . . . 6.3 Exemplo: Servidor de nmeros aleatrios (para requisies simultneas) Aplicaes baseadas em um servidor e vrios clientes 7.1 Exemplo: Servidor de jogo-da-velha . . . . . . . . . . . . . . . . . . . Aplicaes baseadas em um cliente e vrios servidores 8.1 Exemplo: Cliente e servidores para clculo de integrais . . . . . . . . . Mais informaes

Lista de Figuras
1 2 3 4 5 6 7 Algoritmo para leitura e processamento de uma string . . . . . . . . . . Algoritmo para o servidor de strings . . . . . . . . . . . . . . . . . . . Algoritmo integrado do cliente e servidor de strings . . . . . . . . . . . Outro algoritmo integrado do cliente e servidores . . . . . . . . . . . . Protocolo de comunicao entre o cliente e o servidor de daytime . . . Protocolo de comunicao entre o cliente e o servidor de echo . . . . . Protocolo de comunicao entre o cliente e o servidor de inverso de strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 6 7 8 11 14

Rafael Santos

Programao Cliente-Servidor Usando Java

iii

8 9 10 11 12 13 14 15 16 17

Exemplo de acesso ao servidor de strings invertidas usando telnet . . Protocolo de comunicao entre o cliente e o servidor de instncias da classe Livro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Protocolo de comunicao entre o cliente e o servidor de nmeros aleatrios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Protocolo de comunicao entre o cliente e o servidor de arquivos . . . Protocolo de comunicao entre o cliente e o servidor de nmeros aleatrios (segunda verso) . . . . . . . . . . . . . . . . . . . . . . . . . . Posies para jogo e linhas vencedoras no jogo-da-velha . . . . . . . . Protocolo de comunicao entre o servidor e os clientes de jogo-da-velha Exemplo de interao entre um cliente (telnet) e servidor de jogo-davelha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Clculo de uma integral usando parties e trapzios . . . . . . . . . . Protocolo para aplicao que faz o clculo de uma integral . . . . . . .

17 18 22 26 32 43 44 50 51 53

Lista de Listagens
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Cliente de daytime . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cliente de echo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Servidor de strings invertidas . . . . . . . . . . . . . . . . . . . . . . . A classe Livro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Servidor de instncias da classe Livro . . . . . . . . . . . . . . . . . . Cliente para o servidor de livros . . . . . . . . . . . . . . . . . . . . . Servidor de nmeros aleatrios . . . . . . . . . . . . . . . . . . . . . . Cliente de nmeros aleatrios . . . . . . . . . . . . . . . . . . . . . . . Servidor de arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . Cliente de arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . Segunda verso do servidor de nmeros aleatrios . . . . . . . . . . . . Segunda verso do cliente de nmeros aleatrios . . . . . . . . . . . . Classe que representa um carro de corrida para simulao. . . . . . . . Simulao usando instncias de CarroDeCorrida. . . . . . . . . . . . Classe que representa um carro de corrida para simulao (herdando de Thread). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Simulao usando instncias de CarroDeCorridaIndependente. . . . Classe que implementa o servio de nmeros aleatrios . . . . . . . . . Terceira verso do servidor de nmeros aleatrios . . . . . . . . . . . . O servidor de Jogo-da-Velha . . . . . . . . . . . . . . . . . . . . . . . O servidor de clculo de integrais . . . . . . . . . . . . . . . . . . . . Uma linha de execuo para o cliente de clculo de integrais . . . . . . O cliente de clculo de integrais . . . . . . . . . . . . . . . . . . . . . 8 10 13 17 18 20 21 24 26 29 31 34 35 36 37 38 40 41 45 52 55 57

Rafael Santos

Programao Cliente-Servidor Usando Java

Introduo

Este documento ilustra os conceitos bsicos de programao cliente-servidor usando Java, usando protocolos, clientes e servidores desenvolvidos pelo programador. Para melhor compreenso dos tpicos, alguns clientes simples para servidores j existentes sero demonstrados. Para a compreenso dos exemplos deste documento, o leitor deve ter os seguintes conhecimentos tcnicos: Conhecer algoritmos e a implementao de diversas estruturas de controle em Java (condicionais, estruturas de repetio); Programao orientada a Objetos usando a linguagem Java. Em especial necessrio compreender os conceitos de encapsulamento e herana, mecanismo de excees e o uso de mtodos estticos; Conhecer alguns dos mecanismos de entrada e sada em Java (streams); Conhecer conceitos bsicos do funcionamento de um computador ligado em uma rede.

2
2.1

Programao Cliente-Servidor
Introduo

Consideremos os seguintes cenrios: 1. Precisamos executar uma aplicao especca mas o computador ao qual temos acesso no pode execut-la por uma razo qualquer (exemplos podem ser falta de hardware adequado, como disco, processador ou memria; necessidade de acesso a um hardware especco para execuo do software, etc.). 2. Precisamos de uma informao para efetuar um processamento ou tomar uma deciso, mas esta informao atualizada frequentemente. Existe um outro computador que tem a verso mais atual desta informao. 3. Precisamos executar uma tarefa usando computadores que precise de mediao, ou seja, de uma maneira imparcial e independente de avaliar resultados. Frequentemente existiro vrias fontes de informao que precisam ser avaliadas, e em muitos casos as fontes devem permanecer ocultas umas das outras. 4. Precisamos executar uma aplicao cujo tempo para concluso seria excessivamente longo no computador ao qual temos acesso, e possivelmente muito longo mesmo usando hardware mais eciente.
Rafael Santos Programao Cliente-Servidor Usando Java

5. Precisamos obter informaes que esto em algum outro computador (de forma semelhante ao caso 2) mas no sabemos onde estas informaes esto localizadas. Sabemos, no entanto, que um outro computador contm um catlogo destas informaes. No caso do exemplo 1, poderamos emprestar o hardware necessrio sem precisarmos mudar sicamente de lugar. Basta que exista a possibilidade de enviarmos a tarefa para este computador remoto e receber o resultado. Um exemplo deste cenrio seria a consulta a bancos de dados do Genoma, que so grandes e complexos demais para serem reproduzidos em computadores pessoais. O caso do exemplo 2 muito similar ao uso da Internet para acesso a informaes. O exemplo mais claro o acesso a jornais on-line: como a informao modicada a cada dia (ou, em alguns casos, o tempo todo), um usurio pode simplesmente recarregar, atravs de uma pgina, a informao desejada atualizada. Um outro exemplo mais simples seria de um registro global de tempo, que teria a hora certa em um computador especial, e outros computadores poderiam acertar seus relgios internos usando a hora fornecida por este computador especial. Para o exemplo 3 podemos pensar em enviar as informaes ou resultados de diversas fontes para um computador que tomaria a deciso assim que tivesse todas as fontes de dados. A aplicao mais simples deste tipo de cenrio seriam jogos de computador, onde cada jogador enviaria suas informaes (por exemplo, posies de um tabuleiro) e o computador mediador decidiria se as jogadas foram vlidas e quem seria o vencedor. Para o exemplo 4 podemos considerar uma soluo cooperativa: se o problema a ser resolvido puder ser dividido em vrios subproblemas menores independentes, poderamos usar vrios computadores diferentes para resolver cada um destes pequenos problemas. Um computador seria o responsvel para separar o problema em subproblemas, enviar estes subproblemas para diversos outros computadores e integrar o resultado. Um exemplo deste tipo de cenrio a anlise de dados de genoma e proteoma, onde usurios cedem parte do tempo de seus computadores para execuo de uma tarefa global. No caso do exemplo 5, poderamos ter os computadores dos usurios consultando um computador central que no contm informaes mas sabe quais computadores as contm. Este computador central teria ento meta-informaes ou meta-dados, ou seja, informaes sobre informaes. Este cenrio semelhante ao que ocorre em mecanismos de busca na Internet, que no contm pginas com uma informao especca mas podem indicar quais computadores as contm. Em todos estes casos, estaremos usando alguma variante de aplicao cliente-servidor, onde parte do processamento dos dados feito do lado do cliente ou usurio que deseja
Rafael Santos Programao Cliente-Servidor Usando Java

processar a informao; e parte processamento do lado do servidor, ou do computador que capaz de processar ou obter a informao desejada. Em alguns casos teremos a relao um cliente para um servidor (como nos cenrios 1 e 2), mas possvel considerar arquiteturas onde existem vrios clientes para um servidor (cenrio 3), vrios servidores para um cliente (cenrio 4) ou mesmo vrias camadas de servidores (cenrio 5). Em todos estes casos podemos supor que os computadores esto conectados via rede local, Internet ou outro mecanismo remoto qualquer. 2.2 Conceitos

Alguns conceitos cuja compreenso se faz necessria so listados a seguir. Servidor o computador que contm a aplicao que desejamos executar via remota. Servio uma aplicao sendo executada no servidor. Para cada tipo de servio (aplicao) pode ser necessrio ter um tipo de cliente especco, capaz de realizar a comunicao da forma adequada. comum usar os termos servio e servidor para designar a aplicao a ser executada (ex. servidor de FTP). Cliente a aplicao remota que far a comunicao e interao com o servio/servidor. Endereo a informao da localizao de um computador em uma rede local ou na Internet. Podem ser usados como endereos o nmero IP (Internet Protocol) do computador ou um nome que possa ser resolvido por um servidor DNS (Domain Name System). Porta um endereo local em um computador conectado a uma rede, identicado por um nmero nico. Todos os dados originados ou destinados a um computador na rede passam pela conexo (geralmente nica) daquele computador, identicada pelo seu endereo. Como vrias aplicaes podem estar enviando e recebendo dados, necessrio um segundo identicador para que o computador saiba que dados devem ir para cada aplicao. Este segundo identicador a porta, associada com um servio. Porta Bem Conhecida uma porta cujo nmero menor que 1024, e que corresponde a servios bem conhecidos. Alguns exemplos so as portas 7 (servio echo), 13 (servio daytime), 21 (servio ftp) e 80 (servio http). Usurios comuns (isto , sem privilgios de administrao) geralmente no podem usar nmeros de portas bem conhecidas para instalar servios. Protocolo o dilogo que ser travado entre cliente e servidor1 . Este dilogo muito importante quando vamos criar clientes e servidores especcos, pois dados devem ser enviados do cliente para o servidor em uma ordem e formatos predeterminados.
termo protocolo mais conhecido para designar a maneira pela qual os dados sero recebidos e enviados entre cliente e servidor; alguns exemplos mais conhecidos so TCP (Transmission Control Protocol) e UDP (User Datagram Protocol).
1O

Rafael Santos

Programao Cliente-Servidor Usando Java

O protocolo dene que partes do algoritmo que resolve o problema em questo sero resolvidas pelo cliente, resolvidas pelo servidor ou enviadas de/para o cliente e o servidor. Socket o terminal de um canal de comunicaes de duas vias entre cliente e servidor. Para que a comunicao entre cliente e servidor seja efetuada, o cliente e o servidor criaro cada um um socket que sero associados um com o outro para o envio e recebimento de informaes.

Desenvolvimento de aplicaes cliente-servidor em Java

Alguns dos exemplos comentados na seo 2.1 podem ser resolvidos com servidores e clientes j existentes por exemplo, para receber notcias de um jornal basta usarmos um navegador que acessar o servidor HTTP da empresa que mantm o jornal. Em muitos casos, entretanto, teremos que escrever o servidor pois ele far tarefas especcas que no so especialidade de servidores j existentes. Em alguns casos, tambm deveremos escrever um cliente especial pois o protocolo de comunicao com o servidor e os tipos de dados que sero enviados e recebidos devem ter tratamento especial. Uma aplicao cliente-servidor em Java segue um padro relativamente simples de desenvolvimento: a parte complexa determinar que parte do processamento ser feita pelo cliente e que parte pelo servidor, e como e quando os dados devero trafegar entre o cliente e o servidor. Imaginemos uma aplicao onde o cliente deva recuperar uma linha de texto enviada pelo servidor (a linha de texto pode conter a hora local, uma cotao de moeda, etc.). O algoritmo do cliente (mostrado como um uxograma ou diagrama semelhante a um diagrama de atividades de UML) seria como o mostrado na gura 1.

Rafael Santos

Programao Cliente-Servidor Usando Java

Cliente
Incio
Abre conexo

L string

Processa a string

Fim

Figura 1: Algoritmo para leitura e processamento de uma string

O diagrama/algoritmo para o servidor o mostrado na gura 2.

Servidor
Incio
Aguarda conexo do cliente

Cria conexo

Envia string

Figura 2: Algoritmo para o servidor de strings

O algoritmo mostrado na gura 1 no est completo: aps o passo Solicita conexo a aplicao deveria aguardar a conexo entre o cliente e servidor ser estabelecida antes de ler a string enviada pelo servidor. Da mesma forma, o algoritmo do servidor (mostrado na gura 2) no cria conexes e envia strings a qualquer hora: estes passos ocorrem em resposta a solicitaes do cliente. Os dois algoritmos devem ento estar sincronizados para a aplicao funcionar corretamente. Para descrevermos a aplicao como um todo usando diagramas similares aos de ati-

Rafael Santos

Programao Cliente-Servidor Usando Java

Cliente
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cria conexo

L string

Envia string

Imprime String lida

Fim
Figura 3: Algoritmo integrado do cliente e servidor de strings

vidades2 devemos descrever os algoritmos lado a lado, inclusive registrando os passos nos quais os algoritmos devem estar sincronizados. Para isto, basta mudar o uxo dos passos para incluir a dependncia de passos no cliente e servidor. Um algoritmo que mostra as atividades do cliente e servidor ilustrando os passos que precisam ser sincronizados mostrado na gura 3. Um outro exemplo de diagrama simplicado que mostra a interao entre um cliente e dois servidores mostrada na gura 4. Este algoritmo de uma aplicao hipottica na qual um cliente verica preos em dois servidores diferentes para comparao e deciso. interessante observar no algoritmo da gura 4 que alguns passos poderiam estar em ordem diferente sem afetar o funcionamento da aplicao como um todo. Por exemplo, as conexes do cliente com os servidores poderiam ser estabelecidas uma logo aps a outra assim como a leitura dos dados. Como no existe um tempo de processamento grande entre o estabelecimento da conexo e a leitura dos dados, esta modicao no causaria tempo de conexo sem uso desnecessariamente longo com o servidor.
2 Evitando

usar as notaes de bifurcao e unio para manter os diagramas simples

Rafael Santos

Programao Cliente-Servidor Usando Java

Cliente
Incio
Solicita conexo

Servidor 1
Incio
Aguarda conexo

Servidor 2

Cria conexo

L valor do produto

Envia valor

Incio
Solicita conexo Aguarda conexo

Cria conexo

L valor do produto

Envia valor

Processa informaes

Fim
Figura 4: Outro algoritmo integrado do cliente e servidores

Rafael Santos

Programao Cliente-Servidor Usando Java

Clientes simples

Veremos nesta seo alguns clientes simples para servios existentes3 . Estes clientes so totalmente funcionais, e servem para demonstrar o uso das classes para comunicao entre clientes e servidores. 4.1 Exemplo: Cliente de daytime

O servio daytime registrado na porta 13 e retorna, quando conectado e solicitado, uma string contendo a data e hora do servidor. O protocolo de comunicao com um servidor destes , ento, muito simples: basta estabelecer a conexo, ler uma nica string e encerrar a conexo. Este protocolo mostrado na gura 5.

Cliente
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cria conexo

L string

Envia string

Imprime String lida

Fim
Figura 5: Protocolo de comunicao entre o cliente e o servidor de daytime

No sistema operacional Linux, o servio daytime pode ser acessado com o aplicativo telnet, que deve receber como argumentos o nome do servidor e o nmero da porta, separados por espao (por exemplo, telnet localhost 13). Ao invs de acessar o servio desta forma, vamos escrever uma aplicao em Java que o faa para ns. A aplicao mostrada na listagem 1.
Listagem 1: Cliente de daytime
1 2 3

package cap312; import java.io.BufferedReader; import java.io.IOException;


servios utilizados nesta seo so servios padro do sistema operacional Linux, desabilitados por default. Para computadores executando o sistema operacional Red Hat, verique os arquivos no diretrio /etc/xinetd.d para habilitao dos servios.
3 Os

Rafael Santos

Programao Cliente-Servidor Usando Java

9
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

import java.io.InputStreamReader; import java.net.Socket; import java.net.UnknownHostException; /** * Esta classe implementa um cliente simples para o servio daytime (porta 13). * A classe simplesmente cria um socket para um servidor (no exemplo, * localhost) usando aquela porta, obtm um BufferedReader (usando um * InputStreamReader a partir do InputStream da conexo) e l uma linha do * servidor. */ public class ClienteDeDaytime { public static void main(String[] args) { String servidor = "localhost"; // Tentamos fazer a conexo e ler uma linha... try { Socket conexo = new Socket(servidor,13); // A partir do socket podemos obter um InputStream, a partir deste um // InputStreamReader e partir deste, um BufferedReader. BufferedReader br = new BufferedReader( new InputStreamReader(conexo.getInputStream())); String linha = br.readLine(); System.out.println("Agora so "+linha+" no servidor "+servidor); // Fechamos a conexo. br.close(); conexo.close(); } // Se houve problemas com o nome do host... catch (UnknownHostException e) { System.out.println("O servidor no existe ou est fora do ar."); } // Se houve problemas genricos de entrada e sada... catch (IOException e) { System.out.println("Erro de entrada e sada."); } } }

Os pontos interessantes e chamadas a APIs da listagem 1 so: O primeiro passo do algoritmo (a criao de uma conexo) feita pela criao de uma instncia da classe Socket. O construtor desta classe espera um nome de servidor (ou o seu IP, na forma de uma string) e o nmero da porta para conexo. A criao de uma instncia de Socket pode falhar, criando a exceo UnknownHostException se o endereo IP do servidor no puder ser localizado ou IOException se houver um erro de entrada ou sada na criao do socket. Podemos obter uma stream de entrada a partir da instncia de Socket (usando o mtodo getInputStream), e a partir desta stream podemos criar uma instncia de InputStreamReader (que transforma os bytes lidos pela stream em caracteres). A
Rafael Santos Programao Cliente-Servidor Usando Java

10

partir desta instncia de InputStreamReader (usando-a como argumento para o construtor) podemos criar uma instncia de BufferedReader que pode ser usada para ler linhas de texto completas. O resto do processamento realmente simples, lemos uma string do servidor (na verdade, solicitamos a leitura, e o servidor criar esta string para ser enviada quando for requisitado), imprimimos a string lida e fechamos a stream e conexo. Uma nota interessante sobre a aplicao mostrada na listagem 1: aps terminar a leitura da string vinda do servidor, devemos manualmente fechar a conexo com o mesmo (pois sabemos, de acordo com seu protocolo, que somente uma string ser enviada a cada conexo). Se usarmos o aplicativo telnet para acessar este servio, o mesmo automaticamente encerrar a conexo com o servidor. 4.2 Exemplo: Cliente de echo

Um servio ligeiramente mais complexo que o daytime o echo, que responde por conexes na porta 7 e retorna cada string enviada pelo cliente de volta. O servidor reenvia cada string enviada pelo cliente at que a string seja um sinal de trmino (o caracter de controle Control+] no caso do cliente telnet) indicando o m da interao. O cliente precisa, ento, de um mecanismo que envie e receba dados do servidor, sendo que estes dados vo ser somente texto (strings). A gura 6 ilustra o protocolo de comunicao entre um cliente e um servidor de echo. A diferena principal entre o diagrama mostrado na gura 6 e outros vistos anteriormente que este tem uma ramicao possvel (condicional), dependendo da entrada do usurio um dos dois possveis caminhos ser tomado. Para a implementao em Java, ao invs de usar o cdigo de controle do telnet, usaremos uma string composta do caracter ponto (.). Se esta string for enviada, fecharemos a conexo com o servidor. A implementao do cliente de echo mostrada na listagem 2.

Listagem 2: Cliente de echo


1 2 3 4 5 6 7 8 9 10 11 12

package cap312; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.net.UnknownHostException; /** * Esta classe implementa um cliente simples para o servio echo (porta 7). * A classe simplesmente cria um socket para um servidor (no exemplo,

Rafael Santos

Programao Cliente-Servidor Usando Java

11

Cliente
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cria conexo

L string do teclado
Sinal de trmino? sim no

Envia string para o servidor

L string do cliente

L string do servidor

Envia string para o cliente

Imprime string lida

Encerra conexo

Fim
Figura 6: Protocolo de comunicao entre o cliente e o servidor de echo
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

* localhost) usando aquela porta, obtm um BufferedReader (usando um * InputStreamReader a partir do InputStream da conexo) e um BufferedWriter * (usando um OutputStreamWriter a partir do OutputStream da conexo), e repete * o seguinte lao: * 1 - L uma string do teclado * 2 - Envia esta string para o servidor * 3 - L uma string do servidor * 4 - Imprime a string lida do servidor * Se a string entrada via teclado for igual a um ponto (".") o lao ser * interrompido. */ public class ClienteDeEcho { public static void main(String[] args) { String servidor = "localhost"; // Tentamos fazer a conexo e ler uma linha... try { Socket conexo = new Socket(servidor,7);

Rafael Santos

Programao Cliente-Servidor Usando Java

12
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

// A partir do socket podemos obter um InputStream, a partir deste um // InputStreamReader e partir deste, um BufferedReader. BufferedReader br = new BufferedReader( new InputStreamReader(conexo.getInputStream())); // A partir do socket podemos obter um OutputStream, a partir deste um // OutputStreamWriter e partir deste, um Bufferedwriter. BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(conexo.getOutputStream())); // Executamos este lao "para sempre": while(true) { // Lemos uma linha do console. String linhaEnviada = Keyboard.readString(); // Se o usurio tiver digitado Cancel, samos do lao. if (linhaEnviada.equals(".")) break; // Enviamos a linha para o servidor. bw.write(linhaEnviada); bw.newLine(); bw.flush(); // Lemos uma linha a partir do servidor e a imprimimos no console. String linhaRecebida = br.readLine(); System.out.println(linhaRecebida); } // Fechamos a conexo. br.close(); bw.close(); conexo.close(); } // Se houve problemas com o nome do host... catch (UnknownHostException e) { System.out.println("O servidor no existe ou est fora do ar."); } // Se houve problemas genricos de entrada e sada... catch (IOException e) { System.out.println("Erro de entrada e sada."); } } }

Os pontos interessantes da listagem 2 so: Novamente o primeiro passo a criao de uma instncia da classe Socket, novamente usando como exemplo o servidor localhost mas com a porta 7. Como preciso enviar e receber strings do cliente para o servidor e vice-versa, precisamos criar mecanismos para o envio e recebimento de strings. Isto feito separadamente: para o recebimento de strings enviadas do servidor, obtemos uma stream de leitura com o mtodo getInputStream da instncia de Socket, usamos esta stream para criar uma instncia de InputStreamReader e usamos esta instncia para criar uma instncia de BufferedReader. Similarmente, usamos uma stream de escrita obtida com o mtodo getOutputStream da instncia de
Rafael Santos Programao Cliente-Servidor Usando Java

13

Socket, para criar uma instncia de OutputStreamWriter e usamos esta instncia para criar uma instncia de BufferedWriter. Aps a criao das streams de entrada e sada, entramos no lao principal do cliente, que recebe uma string do teclado, compara com a string de trmino, e se for diferente, envia a string para o servidor, recebendo uma string de volta e repetindo o lao. Notem que aps enviar uma string para o servidor, necessrio executar os mtodos newLine e flush da classe BufferedWriter para que os bytes correspondentes string sejam realmente enviados. Este exemplo usa a classe Keyboard para entrada de strings. Esta classe pode ser copiada do site http://www.directnet.com.br/users/rafael.santos/4 .

5
5.1

Servidores simples (requisies no-simultneas)


Exemplo: Servidor de strings revertidas

Vamos ver um exemplo de servidor agora, ou seja, uma aplicao que vai enviar informaes a um cliente. O cliente pode ser um aplicativo simples como o prprio telnet. Considere que seja necessrio por algum razo inverter uma string. A inverso de uma string a simples modicao da ordem de seus caracteres para que a mesma seja lida ao contrrio, ou da direita para a esquerda. Como um exemplo, a inverso da string Java seria avaJ. O algoritmo de inverso simples, basta criar uma string vazia e concatenar a esta string cada caracter da string original, lidos de trs para frente. O protocolo de comunicao entre um cliente e o servidor de strings invertidas mostrado na gura 7. O diagrama mostra uma nfase no passo Inverte string pois este realiza o que, em princpio, o cliente no saberia como ou teria recursos para processar. Em muitos casos de aplicaes cliente-servidor este passo seria o mais crucial: o processamento da informao do lado do servidor. A implementao do servidor de strings invertidas mostrada na listagem 3.
Listagem 3: Servidor de strings invertidas
1 2 3 4 5

package cap312; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader;
se usarmos o mtodo showInputDialog da classe JOptionPane, a aplicao no ser terminada corretamente para isso deveremos usar o mtodo System.exit ao nal da aplicao.
4 Curiosamente,

Rafael Santos

Programao Cliente-Servidor Usando Java

14

Cliente
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cria conexo

L string do teclado

Envia string para o servidor

L string do cliente

Inverte string

L string do servidor

Envia string para o cliente

Imprime string lida

Fim

Figura 7: Protocolo de comunicao entre o cliente e o servidor de inverso de strings


6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

import import import import

java.io.OutputStreamWriter; java.net.BindException; java.net.ServerSocket; java.net.Socket;

/** * Esta classe implementa um servidor simples que fica aguardando conexes de * clientes. Quando uma conexo solicitada, o servidor recebe do cliente uma * string, a inverte e envia este resuldado para o cliente, fechando a conexo. */ public class ServidorDeStringsInvertidas { // Mtodo que permite a execuo da classe. public static void main(String[] args) { ServerSocket servidor; try { // Criamos a instncia de ServerSocket que responder por solicitaes // porta 10101. servidor = new ServerSocket(10101); // O servidor aguarda "para sempre" as conexes.

Rafael Santos

Programao Cliente-Servidor Usando Java

15
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

while(true) { // Quando uma conexo feita, Socket conexo = servidor.accept(); // ... o servidor a processa. processaConexo(conexo); } } // Pode ser que a porta 10101 j esteja em uso ! catch (BindException e) { System.out.println("Porta j em uso."); } // Pode ser que tenhamos um erro qualquer de entrada ou sada. catch (IOException e) { System.out.println("Erro de entrada ou sada."); } } // Este mtodo atende a uma conexo feita a este servidor. private static void processaConexo(Socket conexo) { try { // Criamos uma stream para receber strings, usando a stream de entrada // associado conexo. BufferedReader entrada = new BufferedReader(new InputStreamReader(conexo.getInputStream())); // Criamos uma stream para enviar strings, usando a stream de sada // associado conexo. BufferedWriter sada = new BufferedWriter(new OutputStreamWriter(conexo.getOutputStream())); // Lemos a string que o cliente quer inverter. String original = entrada.readLine(); String invertida = ""; // Invertemos a string. for(int c=0;c<original.length();c++) { invertida = original.charAt(c)+invertida; } // Enviamos a string invertida ao cliente. sada.write(invertida); sada.newLine(); sada.flush(); // Ao terminar de atender a requisio, fechamos as streams de entrada e sada. entrada.close(); sada.close(); // Fechamos tambm a conexo. conexo.close(); } // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro atendendo a uma conexo !"); } } }

Existem vrios pontos interessantes na listagem 3, descritos a seguir:


Rafael Santos Programao Cliente-Servidor Usando Java

16

Para comodidade e compreenso de tpicos intermedirios, a listagem foi dividida em duas partes (dois mtodos): o mtodo main que representa o uxo principal do servidor (criao de sockets e lao principal) e o mtodo processaConexo que ser responsvel por processar uma nica conexo com um cliente. O mtodo main cria uma instncia de ServerSocket que ser responsvel por aguardar conexes do cliente. Uma instncia de ServerSocket criada passandose para o seu construtor a porta na qual este servidor ir responder por conexes. O mtodo main tambm contm um lao aparentemente innito (while(true)) onde car aguardando conexes de clientes. Quando um cliente se conectar a este servidor, o mtodo accept da classe ServerSocket criar uma instncia de Socket para a comunicao com o cliente, e prosseguir a execuo do lado do servidor. importante observar que enquanto o cliente no solicitar uma conexo, o mtodo accept bloquear o processamento do servidor, como se a aplicao estivesse pausada naquele ponto. Assim que uma conexo do cliente for aceita, o mtodo processaConexo ser executado, usando como argumento a instncia recm-criada da classe Socket, correspondente conexo com o cliente. Ainda no mtodo main temos os blocos catch responsveis pelo processamento das excees que podem ocorrer neste mtodo: BindException que ser criada caso o endereo desejado j esteja em uso e IOException que ser criada caso ocorra algum erro de entrada e sada genrico. O mtodo processaConexo ser responsvel por processar uma nica conexo com este servidor, criada quando o mtodo accept foi executado. Este mtodo criar streams para envio e recebimento de strings, conforme ilustrado e descrito na seo 4.2. importante notar que a stream de entrada para o servidor ser associado stream de sada do cliente e vice-versa. O mtodo processaConexo ento recebe uma string do cliente, a inverte com um algoritmo simples e a envia para o cliente (garantindo que os bytes desta string sero realmente enviados usando os mtodos newLine e flush da classe BufferedWriter). O mtodo processaConexo tambm processa em um bloco catch a exceo IOException que pode ocorrer. Para testar este servidor, um cliente simples como telnet ou mesmo a aplicao ClienteDeEcho (listagem 2) modicada para usar a porta 10101 poderia ser usado. Um exemplo de execuo de interao com o servidor de strings invertidas usando o telnet mostrado na gura 8.

Rafael Santos

Programao Cliente-Servidor Usando Java

17

5.2

Exemplo: Servidor e cliente de instncias de classes

At agora vimos clientes que se comunicam com servidores conhecidos ou servidores que podem usar clientes existentes como telnet Veremos agora um exemplo mais especco que exige que tanto o cliente quanto o servidor sejam escritos especialmente para atender ao protocolo de comunicao entre cliente e servidor (ou para poder receber e enviar dados que no sejam strings). Em outras palavras, o servidor ser diferente dos existentes (echo, daytime) e o cliente no poder ser o telnet. Vamos ver como podemos escrever um servidor para servir instncias de classes em Java. O cliente tambm dever ser capaz de receber do servidor estas instncias. Vamos usar a classe Livro (listagem 4) para criar as instncias que sero enviadas.
Listagem 4: A classe Livro
package cap312; import java.io.Serializable; public class Livro implements Serializable { private String ttulo; private String autores; private int ano; public Livro(String t,String aut,int a) { ttulo = t; autores = aut; ano = a; } public void imprime() { System.out.println(ttulo+" ("+autores+"),"+ano); } }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

Para que instncias de classes possam ser enviadas e recebidas por servidores e clientes necessrio que elas sejam serializveis. Para isto, basta declarar a classe como implementando a interface Serializable, sem precisar implementar nenhum mtodo

Figura 8: Exemplo de acesso ao servidor de strings invertidas usando telnet Rafael Santos Programao Cliente-Servidor Usando Java

18

adicional. A classe Livro (listagem 4) implementa esta interface. O protocolo de comunicao entre o servidor e o cliente de instncias da classe Livro bem simples, e similar a outros mostrados anteriormente. Este protocolo mostrado na gura 9.

Cliente
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cria conexo

L instncia de Livro

Envia instncia de Livro

Executa mtodo da instncia lida

Fim

Figura 9: Protocolo de comunicao entre o cliente e o servidor de instncias da classe Livro

O servidor de instncias da classe Livro segue o mesmo padro do servidor de strings invertidas (listagem 3): a classe contm um mtodo main responsvel por criar o socket do lado do servidor e atender a requisies dos clientes, que sero processadas pelo mtodo processaConexo. Este mtodo cria uma stream do tipo ObjectOutputStream para enviar uma das instncias da classe Livro contidas na estrutura coleo. O servidor de instncias da classe Livro implementado na classe ServidorDeLivros, mostrada na listagem 5.
Listagem 5: Servidor de instncias da classe Livro

1 2 3 4 5 6 7 8 9 10

package cap312; import java.io.IOException; import java.io.ObjectOutputStream; import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; /** * Esta classe implementa um servidor simples que fica aguardando conexes de

Rafael Santos

Programao Cliente-Servidor Usando Java

19
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71

* clientes. Este servidor, quando conectado, envia uma instncia da classe * Livro para o cliente e desconecta. */ public class ServidorDeLivros { // Estrutura que armazenar uma coleo de livros (instncias da classe Livro). private static ArrayList<Livro> coleo; // Mtodo que permite a execuo da classe. public static void main(String[] args) { // Criamos uma coleo de livros. coleo = new ArrayList<Livro> (); coleo.add(new Livro("Java 1.4 Game Programming", "Andrew Mulholland, Glen Murphy",2003)); coleo.add(new Livro("Developing Games In Java","David Bracken",2003)); coleo.add(new Livro("Java 2 Game Programming","Thomas Petchel",2001)); coleo.add(new Livro("Board And Table Games From Many Civilizations", "R.C.Bell",1969)); coleo.add(new Livro("A Gamut Of Games","Sid Sackson",1969)); try { // Criamos a instncia de ServerSocket que responder por solicitaes // porta 12244. ServerSocket servidor = new ServerSocket(12244); // O servidor aguarda "para sempre" as conexes. while(true) { // Quando uma conexo feita, Socket conexo = servidor.accept(); // ... o servidor a processa. processaConexo(conexo); } } // Pode ser que a porta j esteja em uso ! catch (BindException e) { System.out.println("Porta j em uso."); } // Pode ser que tenhamos um erro qualquer de entrada ou sada. catch (IOException e) { System.out.println("Erro de entrada ou sada."); } } // Este mtodo atende a uma conexo feita a este servidor. private static void processaConexo(Socket conexo) { try { // Criamos uma stream para enviar instncias de classes, usando a stream // de sada associado conexo. ObjectOutputStream sada = new ObjectOutputStream(conexo.getOutputStream()); // Escolhemos um livro aleatoriamente. int qual = (int)(Math.random()*coleo.size()); sada.writeObject(coleo.get(qual)); // Ao terminar de atender a requisio, fechamos a stream de sada. sada.close(); // Fechamos tambm a conexo.

Rafael Santos

Programao Cliente-Servidor Usando Java

20
72 73 74 75 76 77 78 79 80

conexo.close(); } // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro atendendo a uma conexo !"); } } }

Para acessar este servio, no podemos usar clientes como telnet: uma conexo feita via telnet para o servidor executando esta aplicao retornaria strings com alguns caracteres de controle (correspondente serializao de uma instncia da classe Livro) que torna os resultados da interao praticamente inutilizveis. Precisamos de um cliente especializado em receber e processar instncias da classe Livro. O cliente para o servidor mostrado na listagem 5 tambm simples e similar a outros clientes vistos anteriormente. A principal diferena que ao invs de usar uma instncia de BufferedReader para ler dados do servidor, iremos usar uma instncia de ObjectInputStream. O cliente (classe ClienteDeLivros) mostrado na gura 6.
Listagem 6: Cliente para o servidor de livros

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

package cap312; import java.io.IOException; import java.io.ObjectInputStream; import java.net.Socket; import java.net.UnknownHostException;

/** * Esta classe implementa um cliente simples para o servio de livros. * Ele se conecta ao servidor (pela porta 12244) e recebe uma instncia da classe * Livro enviada pelo servidor. */ public class ClienteDeLivros { public static void main(String[] args) { String servidor = "localhost"; // Tentamos fazer a conexo e ler uma linha... try { Socket conexo = new Socket(servidor,12244); // A partir do socket podemos obter um InputStream, a partir deste um // ObjectInputStream. ObjectInputStream dis = new ObjectInputStream(conexo.getInputStream()); Livro umLivro = (Livro)dis.readObject(); System.out.println("Livro obtido do servidor:"); umLivro.imprime(); // Fechamos a conexo. dis.close(); conexo.close(); } // Se houve problemas com o nome do host...

Rafael Santos

Programao Cliente-Servidor Usando Java

21
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

catch (UnknownHostException e) { System.out.println("O servidor no existe ou est fora do ar."); } // Se houve problemas genricos de entrada e sada... catch (IOException e) { System.out.println("Erro de entrada e sada."); } // Essa exceo poderia ocorrer se tivssemos o cliente sendo executado em // outro computador e a classe Livro no tivesse sido distribuda. catch (ClassNotFoundException e) { System.out.println("A classe Livro no est presente no cliente."); } } }

Existe uma outra diferena entre este cliente e outros: este deve, obrigatoriamente, ter um bloco catch para processar a exceo ClassNotFoundException, que ser criada caso a classe cuja instncia desejamos recuperar no existir do lado do cliente. Embora para nalidade de testes esta classe deva estar sempre presente, importante lembrar que em aplicaes mais complexas o servidor pode enviar uma instncia de uma classe que no exista no cliente quando a aplicao do lado cliente for distribuda para uso, devemos sempre lembrar de incluir todas as classes que podem ser serializadas para aquela aplicao. 5.3 Exemplo: Servidor e cliente de nmeros aleatrios

Consideremos como um outro exemplo um servidor que envie para o cliente um nmero aleatrio5 entre zero e um, do tipo double. Embora qualquer computador que tenha a mquina virtual Java possa facilmente obter nmeros aleatrios, podemos imaginar razes para fazer disto um servio: uma razo comum que o nmero deve ser coerente com os enviados para outros clientes (no caso de jogos). O protocolo de comunicao entre um cliente e um servidor de nmeros aleatrios mostrado na gura 10. O protocolo de comunicao entre o cliente e servidor de nmeros aleatrios to simples quanto o protocolo de comunicao entre cliente e servidor de echo, de strings invertidas ou de uma instncia de uma classe, a diferena que em vez de strings ou instncias estaremos recebendo valores de um dos tipos nativos (no caso, double). A implementao do servidor semelhante do servidor de strings invertidas ou de instncias da classe Livro. O cdigo-fonte do servidor mostrado na listagem 7.
Listagem 7: Servidor de nmeros aleatrios
so pseudo-aleatrios, mas para as nalidades deste documento vamos ignorar a diferena entre nmeros realmente aleatrios e os gerados pelo mtodo Math.random.
5 OK,

Rafael Santos

Programao Cliente-Servidor Usando Java

22

Cliente
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cria conexo

L double

Envia double

Imprime valor lido

Fim

Figura 10: Protocolo de comunicao entre o cliente e o servidor de nmeros aleatrios

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

package cap312; import java.io.DataOutputStream; import java.io.IOException; import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; /** * Esta classe implementa um servidor simples que fica aguardando conexes de * clientes. Quando uma conexo solicitada, o servidor envia para o cliente * um nmero aleatrio e fecha a conexo. */ public class ServidorDeNumerosAleatorios { // Mtodo que permite a execuo da classe. public static void main(String[] args) { ServerSocket servidor; try { // Criamos a instncia de ServerSocket que responder por solicitaes // porta 9999. servidor = new ServerSocket(9999); // O servidor aguarda "para sempre" as conexes. while(true) { // Quando uma conexo feita, Socket conexo = servidor.accept(); // ... o servidor a processa. processaConexo(conexo); } }

Rafael Santos

Programao Cliente-Servidor Usando Java

23
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70

// Pode ser que a porta 9999 j esteja em uso ! catch (BindException e) { System.out.println("Porta j em uso."); } // Pode ser que tenhamos um erro qualquer de entrada ou sada. catch (IOException e) { System.out.println("Erro de entrada ou sada."); } } // Este mtodo atende a uma conexo feita a este servidor. private static void processaConexo(Socket conexo) { try { // Criamos uma stream para enviar valores nativos, usando a stream de sada // associado conexo. DataOutputStream sada = new DataOutputStream(conexo.getOutputStream()); double rand = Math.random(); sada.writeDouble(rand); // Para demonstrar que realmente funciona, vamos imprimir um log. System.out.println("Acabo de enviar o nmero "+rand+" para o cliente "+ conexo.getRemoteSocketAddress()); // Ao terminar de atender a requisio, fechamos a stream de sada. sada.close(); // Fechamos tambm a conexo. conexo.close(); } // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro atendendo a uma conexo !"); } } }

O cdigo-fonte do servidor segue praticamente os mesmos passos do servidor de strings invertidas (listagem 3) exceto pela criao da classe que ser responsvel pelo envio dos dados do servidor para o cliente. Usaremos, para este servidor, a instncia de OutputStream retornada pelo mtodo getInputStream da classe Socket para criar uma instncia da classe DataOutputStream. Para instncias desta classe no necessrio enviar terminadores de linhas nem executar mtodos que forcem o envio dos bytes. Um ponto de interesse no cdigo do servidor (listagem 7) que quando um nmero aleatrio enviado para o cliente, uma mensagem impressa no terminal onde o servidor est sendo executado. Isso facilita a depurao do servidor, caso seja necessrio, e serve como ilustrao da criao de arquivos de registro (logs) como feito com servidores mais complexos. O servidor de nmeros aleatrios est pronto para receber conexes pela porta 9999, mas os clientes existentes no so capazes de receber bytes correspondentes a valores
Rafael Santos Programao Cliente-Servidor Usando Java

24

do tipo double: se executarmos o comando telnet localhost 9999 o mesmo retornar caracteres sem sentido. Precisamos de um cliente especco para este tipo de servio. Um cliente adequado para receber um nico valor do tipo double de um servidor mostrado na listagem 8. Novamente a diferena entre um cliente que processa strings, como ClienteDeEcho (listagem 2) o tipo de classe que ser criado usando as streams de entrada e sada obtidas da classe Socket. No caso do cliente, usamos uma instncia de DataInputStream criada a partir do InputStream obtido pelo mtodo getInputStream da classe Socket.
Listagem 8: Cliente de nmeros aleatrios
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

package cap312; import java.io.DataInputStream; import java.io.IOException; import java.net.Socket; import java.net.UnknownHostException;

/** * Esta classe implementa um cliente simples para o servio de nmeros aleatrios. * Ele se conecta ao servidor (pela porta 9999) e recebe um nmero aleatrio * gerado no servidor. */ public class ClienteDeNumerosAleatorios { public static void main(String[] args) { String servidor = "localhost"; // Tentamos fazer a conexo e ler uma linha... try { Socket conexo = new Socket(servidor,9999); // A partir do socket podemos obter um InputStream, a partir deste um // DataInputStream. DataInputStream dis = new DataInputStream(conexo.getInputStream()); double valor = dis.readDouble(); System.out.println("Li o valor "+valor+" do servidor "+servidor+"."); // Fechamos a conexo. dis.close(); conexo.close(); } // Se houve problemas com o nome do host... catch (UnknownHostException e) { System.out.println("O servidor no existe ou est fora do ar."); } // Se houve problemas genricos de entrada e sada... catch (IOException e) { System.out.println("Erro de entrada e sada."); } } }

Rafael Santos

Programao Cliente-Servidor Usando Java

25

5.4

Exemplo: Servidor simples de arquivos

Vamos considerar mais um exemplo de aplicao cliente-servidor, onde o tipo de dados e seu processamento ser diferenciado durante a execuo do protocolo de comunicao. Consideremos um servidor extremamente simples de arquivos, similar a um servidor de FTP mas com boa parte da funcionalidade (ex. acesso a diferentes diretrios, autenticao de usurios) removida. Durante a interao com o cliente, este servidor ter que se comunicar enviando e recebendo strings (comandos, listas de arquivos, etc.) que sero impressas no terminal e tambm enviando bytes que sero gravados em um arquivo pelo cliente. O protocolo de comunicao entre o servidor e o cliente simples: o servidor envia para o cliente a lista de arquivos, o cliente seleciona um, o servidor envia os bytes daquele arquivo para o cliente que armazena estes bytes em um arquivo local. Este protocolo ilustrado na gura 11. A implementao do servidor de arquivos feita na classe ServidorDeArquivos, que mostrada na listagem 9.

Rafael Santos

Programao Cliente-Servidor Usando Java

26

Cliente
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cria conexo

Imprime lista de arquivos

Envia lista de arquivos locais

Seleciona arquivo e envia

L nome do arquivo selecionado Envia contedo do arquivo selecionado

L contedo do arquivo selecionado

Armazena contedo em disco

Fim

Figura 11: Protocolo de comunicao entre o cliente e o servidor de arquivos

Listagem 9: Servidor de arquivos


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

package cap312; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; /** * Esta classe implementa um servidor simples que fica aguardando conexes de * clientes. Este servidor, quando conectado, envia uma lista de arquivos locais * para o cliente, aguarda a seleo de um nome de arquivo e envia o contedo * deste arquivo para o cliente. */ public class ServidorDeArquivos { // Mtodo que permite a execuo da classe. public static void main(String[] args) {

Rafael Santos

Programao Cliente-Servidor Usando Java

27
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84

try { // Criamos a instncia de ServerSocket que responder por solicitaes // porta 2048. ServerSocket servidor = new ServerSocket(2048); // O servidor aguarda "para sempre" as conexes. while(true) { // Quando uma conexo feita, Socket conexo = servidor.accept(); // ... o servidor a processa. processaConexo(conexo); } } // Pode ser que a porta j esteja em uso ! catch (BindException e) { System.out.println("Porta j em uso."); } // Pode ser que tenhamos um erro qualquer de entrada ou sada. catch (IOException e) { System.out.println("Erro de entrada ou sada."); } } // Este mtodo atende a uma conexo feita a este servidor. private static void processaConexo(Socket conexo) { try { // Criamos uma stream para receber comandos do cliente - estes comandos // sero somente strings. BufferedReader entrada = new BufferedReader(new InputStreamReader(conexo.getInputStream())); // Criamos uma stream para enviar textos e dados para o cliente. DataOutputStream sada = new DataOutputStream(conexo.getOutputStream()); // Mandamos uma mensagem de boas vindas. sada.writeUTF("Bem-vindo ao servidor de arquivos locais."); sada.writeUTF("========================================="); // Enviamos ao cliente a lista de arquivos locais. File diretrio = new File("."); // bom para Linux, ser que funciona no Windows? String[] arquivos = diretrio.list(); // oops, inclui diretrios tambm ! for(int a=0;a<arquivos.length;a++) { sada.writeUTF(arquivos[a]); } // Aguardamos a seleo do usurio. sada.writeUTF("-----------------------------------------"); sada.writeUTF("Selecione um dos arquivos acima."); // Informamos ao cliente que terminamos de mandar texto. sada.writeUTF("#####"); // Garantimos que as mensagens foram enviadas ao cliente. sada.flush(); // Lemos o nome de arquivo selecionado pelo cliente. String nomeSelecionado = entrada.readLine(); // Criamos uma representao do arquivo. File selecionado = new File(nomeSelecionado); // Enviamos uma mensagem esclarecedora para o cliente. sada.writeUTF("Enviando arquivo "+nomeSelecionado+" ("+

Rafael Santos

Programao Cliente-Servidor Usando Java

28
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115

selecionado.length()+" bytes)"); sada.flush(); // Abrimos o arquivo localmente. DataInputStream entradaLocal = new DataInputStream(new FileInputStream(selecionado)); // Lemos todos os bytes do arquivo local, enviando-os para o cliente. // Para maior eficincia, vamos ler e enviar os dados em blocos de 25 bytes. byte[] arrayDeBytes = new byte[25]; while(true) { // Tentamos ler at 25 bytes do arquivo de entrada. int resultado = entradaLocal.read(arrayDeBytes,0,25); if (resultado == -1) break; // Escrevemos somente os bytes lidos. sada.write(arrayDeBytes,0,resultado); } // Ao terminar de ler o arquivo local, o fechamos. entradaLocal.close(); // Ao terminar de atender a requisio, fechamos a stream de sada. sada.close(); // Fechamos tambm a conexo. conexo.close(); } // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro atendendo a uma conexo !"); e.printStackTrace(); } } }

Alguns dos pontos de interesse na classe ServidorDeArquivos (listagem 9) so: O servidor segue o mesmo padro de ter uma classe main e uma processaConexo isto far o desenvolvimento de servidores capazes de atender mltiplas requisies (veja seo 6) mais simples. Para receber os dados do cliente, o servidor usa uma instncia de BufferedReader, pois assumimos que o cliente somente enviar strings para o servidor. Para enviar os dados para o cliente, usando a mesma conexo, usamos uma instncia da classe DataOutputStream, que capaz de enviar strings (usando o mtodo writeUTF) e valores de tipos nativos, inclusive arrays de bytes, com o mtodo write. Em determinado ponto da interao entre cliente e servidor, o servidor ir enviar um nmero desconhecido de linhas de texto para o cliente (os nomes de arquivos disponveis no servidor). Para facilitar a interao com o cliente, que no sabe antecipadamente quantas linhas de texto sero enviadas, usamos um marcador para informar ao cliente que no existem mais linhas de texto a ser lidas: quando o cliente receber a string ##### ele saber que deve parar de ler linhas de texto do servidor. Quando o servidor for enviar os bytes do arquivo selecionado para o cliente, o procedimento ser outro: o servidor enviar bytes enquanto existirem (em um lao
Rafael Santos Programao Cliente-Servidor Usando Java

29

que ser encerrado quando no for mais possvel ler bytes do arquivo local) e o cliente monitorar estes bytes lidos para vericar o nal da transmisso. Desta forma, demonstramos duas maneiras de vericar se todos os dados de uma sesso de comunicao foram enviados pelo servidor ou cliente: um usando marcadores ou tags que indicam nal de transmisso e outro usando mtodos da API de Java. Vejamos agora o cliente para este servidor de arquivos. No poderemos usar o cliente telnet pois em determinado momento da comunicao o servidor enviar bytes para o cliente, que devem ser armazenados em um arquivo e no mostrados na tela. Um cliente adequado mostrado na classe ClienteDeArquivos (listagem 10).
Listagem 10: Cliente de arquivos
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

package cap312; import java.io.BufferedWriter; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.Socket; import java.net.UnknownHostException; /** * Esta classe implementa um cliente simples para o servidor de arquivos. * A principal diferena entre este cliente e um cliente simples de texto * (telnet, por exemplo) que existe uma ordem no protocolo de comunicao * entre cliente-servidor que pede que em determinado momento, ao invs de * receber textos para mostrar no terminal, o cliente deve receber dados para * armazenar em um arquivo. */ public class ClienteDeArquivos { public static void main(String[] args) { String servidor = "localhost"; // Tentamos fazer a conexo... try { Socket conexo = new Socket(servidor,2048); // A partir do socket podemos obter um InputStream, a partir deste um // InputStreamReader e partir deste, um BufferedReader. DataInputStream entrada = new DataInputStream(conexo.getInputStream()); // A partir do socket podemos obter um OutputStream, a partir deste um // OutputStreamWriter e partir deste, um BufferedWriter. BufferedWriter sada = new BufferedWriter( new OutputStreamWriter(conexo.getOutputStream())); // Lemos a lista de arquivos do servidor. Sabemos que vamos ler um // nmero desconhecido de linhas do servidor. Usamos um lao que // l linhas enquanto existirem dados a ser lidos. O servidor enviar // a string ##### quando no houverem mais dados. while(true) { String linha = entrada.readUTF();

Rafael Santos

Programao Cliente-Servidor Usando Java

30
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94

if(linha.equals("#####")) break; // cdigo do servidor: acabaram as linhas. System.out.println(linha); } // Lemos o nome do arquivo que desejamos do teclado e o // enviamos para o servidor. String arquivo = Keyboard.readString(); sada.write(arquivo); sada.newLine(); sada.flush(); // O servidor envia para o cliente mais uma informao como texto. String linha = entrada.readUTF(); System.out.println(linha); // Abrimos o arquivo local. Vamos dar um nome diferente para ele s // para no complicar a vida de quem estiver rodando cliente e servidor // no mesmo computador. DataOutputStream sadaParaArquivo = new DataOutputStream(new FileOutputStream("_"+arquivo)); // A partir deste momento o servidor vai nos enviar bytes. Lemos todos os // bytes enviados do servidor, gravando-os em um arquivo local. // Para maior eficincia, vamos ler os dados em blocos de 25 bytes. byte[] array = new byte[25]; while(true) { int resultado = entrada.read(array,0,25); if (resultado == -1) break; // Escrevemos somente os bytes lidos. sadaParaArquivo.write(array,0,resultado); } // Fechamos as streams e a conexo. sada.close(); entrada.close(); conexo.close(); } // Se houve problemas com o nome do host... catch (UnknownHostException e) { System.out.println("O servidor no existe ou est fora do ar."); } // Se houve problemas genricos de entrada e sada... catch (EOFException e) { System.out.println("Erro de EOF."); } // Se houve problemas genricos de entrada e sada... catch (IOException e) { System.out.println("Erro de entrada e sada."); } } }

O cliente mostrado na listagem 10 tem, como pontos de interesse, o esquema de leitura de vrias linhas do servidor, que continua lendo linhas do servidor at que a linha ##### seja enviada; e o trecho que l bytes do servidor (em blocos de 25 bytes) e armazena estes bytes em um arquivo local.

Rafael Santos

Programao Cliente-Servidor Usando Java

31

Servidores para mltiplas requisies simultneas

At agora os servidores implementados (exceto pelos servidores j existentes como echo e daytime) so capazes de atender uma nica requisio simultnea dos clientes - se um cliente j estivesse acessando o servio e outros clientes tambm tentassem o acesso, estes outros clientes teriam que aguardar a conexo com o primeiro cliente ser encerrada. Felizmente, os servidores demonstrados so muito simples, enviando dados para o cliente e imediatamente encerrando a conexo, permitindo a conexo por parte de outro cliente logo em seguida. caso os servidores, por alguma razo, demorassem demais para atender os clientes, outros clientes experimentariam uma demora no acesso ou mesmo a negao do servio. Podemos assumir que servidores comuns (como, por exemplo, http) so capazes de atender mltiplas requisies simultaneamente, caso contrrio um usurio teria que esperar outro terminar a sua conexo em uma espcie de la. Nesta seo veremos por que que alguns servidores devem estar preparados para atender mltiplas requisies simultaneamente e como podemos preparar servidores para esta tarefa. 6.1 O problema de mltiplas requisies simultneas

Consideremos uma pequena alterao no protocolo de comunicao entre o cliente e o servidor de nmeros aleatrios: o cliente agora envia para o servidor o nmero de valores que este deseja receber. O servidor, por sua vez, ao invs de enviar somente um nmero aleatrio, envia a quantidade de nmeros solicitada pelo cliente. O protocolo de comunicao para esta verso de cliente e servidor mostrado na gura 12. Embora o diagrama da gura 12 parea mais complicado do que os outros, a nica diferena fundamental que existem dois laos, do lado do cliente e do lado do servidor, que garantem que o mesmo nmero de valores ser requisitado e lido. Considerando estas modicaes, a classe servidora de nmeros aleatrios foi reescrita como mostra a listagem 11. Esta classe segue o mesmo padro de dois mtodos, um main e um responsvel por processar uma requisio.
Listagem 11: Segunda verso do servidor de nmeros aleatrios
1 2 3 4

package cap312; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException;

Rafael Santos

Programao Cliente-Servidor Usando Java

32
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; /** * Esta classe implementa um servidor simples que fica aguardando conexes de * clientes. Quando uma conexo solicitada, o servidor l do cliente a * quantidade de nmeros aleatrios desejada e envia para o cliente * estes nmeros aleatrios, terminando ento a conexo. */ public class SegundoServidorDeNumerosAleatorios { // Mtodo que permite a execuo da classe. public static void main(String[] args) { ServerSocket servidor; try { // Criamos a instncia de ServerSocket que responder por solicitaes

Cliente
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cria conexo

L valor inteiro do teclado

Envia inteiro para servidor


Aindaexistem valores aserlidos?

L inteiro do cliente
Aindaexistem valores aserenviados?

N S S

L double do servidor

Envia double para cliente

Imprime valor lido

Fim

Figura 12: Protocolo de comunicao entre o cliente e o servidor de nmeros aleatrios (segunda verso) Rafael Santos Programao Cliente-Servidor Usando Java

33
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83

// porta 9999. servidor = new ServerSocket(9999); // O servidor aguarda "para sempre" as conexes. while(true) { // Quando uma conexo feita, Socket conexo = servidor.accept(); // ... o servidor a processa. processaConexo(conexo); } } // Pode ser que a porta 9999 j esteja em uso ! catch (BindException e) { System.out.println("Porta j em uso."); } // Pode ser que tenhamos um erro qualquer de entrada ou sada. catch (IOException e) { System.out.println("Erro de entrada ou sada."); } } // Este mtodo atende a uma conexo feita a este servidor. private static void processaConexo(Socket conexo) { try { // Criamos uma stream para receber valores nativos, usando a stream de entrada // associado conexo. DataInputStream entrada = new DataInputStream(conexo.getInputStream()); // Criamos uma stream para enviar valores nativos, usando a stream de sada // associado conexo. DataOutputStream sada = new DataOutputStream(conexo.getOutputStream()); // Lemos do cliente quantos nmeros ele deseja. int quantidade = entrada.readInt(); // Enviamos para o cliente os nmeros. for(int q=0;q<quantidade;q++) { double rand = Math.random(); sada.writeDouble(rand); // Para demonstrar que realmente funciona, vamos imprimir um log. System.out.println("Acabo de enviar o nmero "+rand+" para o cliente "+ conexo.getRemoteSocketAddress()); } // Ao terminar de atender a requisio, fechamos as streams. entrada.close(); sada.close(); // Fechamos tambm a conexo. conexo.close(); } // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro atendendo a uma conexo !"); } } }

Rafael Santos

Programao Cliente-Servidor Usando Java

34

O cliente para este servio tambm precisa ser reescrito. O cliente para a segunda verso do servidor de nmeros aleatrios mostrado na listagem 12.
Listagem 12: Segunda verso do cliente de nmeros aleatrios
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

package cap312; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; import java.net.UnknownHostException; public class SegundoClienteDeNumerosAleatorios { public static void main(String[] args) { String servidor = "localhost"; // Tentamos fazer a conexo e ler uma linha... try { Socket conexo = new Socket(servidor,9999); // A partir do socket podemos obter um InputStream, a partir deste um // DataInputStream. DataInputStream dis = new DataInputStream(conexo.getInputStream()); // A partir do socket podemos obter um OutputStream, a partir deste um // DataOutputStream. DataOutputStream dos = new DataOutputStream(conexo.getOutputStream()); // Quantos nmeros aleatrios vamos querer ? System.out.print("Quantos nmeros:"); int quantos = Keyboard.readInt(); // Enviamos esta informao para o servidor. dos.writeInt(quantos); // Lemos os nmeros desejados do servidor. for(int q=0;q<quantos;q++) { double valor = dis.readDouble(); System.out.println("O "+(q+1)+"o valor "+valor+"."); } // Fechamos a conexo. dis.close(); conexo.close(); } // Se houve problemas com o nome do host... catch (UnknownHostException e) { System.out.println("O servidor no existe ou est fora do ar."); } // Se houve problemas genricos de entrada e sada... catch (IOException e) { System.out.println("Erro de entrada e sada."); } } }

O cliente modicado mostrado na listagem 12 tambm bem simples, contendo somente um lao adicional quando comparado com outros clientes.
Rafael Santos Programao Cliente-Servidor Usando Java

35

O cliente e servidor de mltiplos nmeros aleatrios funcionam corretamente, mas podem causar um srio problema. Imagine que este seja um servio concorrido, com vrias pessoas a qualquer momento solicitando nmeros aleatrios. Se um usurio solicitar uma quantidade exageradamente grande de nmeros aleatrios, outro cliente estar impossibilitado de usar o servio, pois o mtodo processaConexo do servidor deve terminar o seu processamento para retornar o controle ao mtodo main, que ento poder atender outra conexo quando o mtodo accept for executado. Temos algumas possibilidades para a soluo deste problema em potencial: Fazer clientes aguardarem sua vez sem maiores informaes - no existe maneira de informar aos clientes quanto tempo eles tero que esperar na la para atendimento (analogia: telefone ocupado). Executar mais de um servidor simultaneamente, em portas diferentes - se a primeira tentativa de conexo fracassar, o cliente dever descobrir, por conta prpria, qual das portas que oferecem o servio est desocupada (analogia: vrios telefones para a mesma pessoa, alguns ocupados). Limitar a quantidade de nmeros a serem servidos para que a conexo seja liberada o mais rpido possvel para outros clientes. Evidentemente estas opes so muito restritivas. Uma outra alternativa, mais interessante, seria fazer uso de linhas de execuo ou threads. Basicamente threads permitem que aplicaes executem mais de uma tarefa de forma aparentemente simultnea. Continuamos com uma aplicao que tem um nico mtodo main, mas durante a execuo deste mtodo ele cria vrias instncias de classes que podem ser executadas concorrentemente. Para escrever um servidor capaz de tratar vrias requisies de forma aparentemente simultnea, podemos usar os conceitos de threads e as classes em Java que os implementam. 6.2 Linhas de execuo (threads)

Para melhor compreender linhas de execuo, consideremos um exemplo simples no relacionado com programao cliente-servidor. Imaginemos uma simulao que envolva alguns objetos que poderiam se comportar independentemente, como por exemplo, uma simulao de corrida onde cada carro tem uma velocidade e independente de outros carros. Vamos criar uma classe CarroDeCorrida para representar um carro de corrida para esta simulao. Esta classe mostrada na listagem 13.
Listagem 13: Classe que representa um carro de corrida para simulao.
1 2

package cap312; /**

Rafael Santos

Programao Cliente-Servidor Usando Java

36
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

* Esta classe representa um carro da Corrida Maluca para uma simulao. */ public class CarroDeCorrida { private String nome; private int distncia; private int velocidade; /** * O construtor da classe inicializa o nome do carro e a velocidade do mesmo. */ public CarroDeCorrida(String n,int vel) { nome = n; distncia = 0; velocidade = vel; } /** * Este mtodo imprime os passos percorridos pelo carro. */ public void executa() { while(distncia <= 1200) { System.out.println(nome+" rodou "+distncia+" km."); distncia += velocidade; // Pausa o processamento com um clculo intil. for(int sleep=0;sleep<1000000;sleep++) { double x = Math.sqrt(Math.sqrt(Math.sqrt(sleep))); } } } }

A classe CarroDeCorrida (listagem 13) realmente simples, tendo como mtodos somente o construtor e um mtodo executa que executar a simulao para a instncia da classe. A simulao consiste em mudar a distncia percorrida pelo carro, imprimir na tela e fazer uma pequena pausa (articialmente criada por um lao que executa uma operao matemtica). A simulao de uma corrida poderia ser feita atravs de uma classe com vrias instncias da classe CarroDeCorrida. A classe SimulacaoSemThreads, na listagem 14, cria uma simulao simples.
Listagem 14: Simulao usando instncias de CarroDeCorrida.
1 2 3 4 5 6 7

package cap312; /** * Esta classe faz uma simulao de corrida usando instncias da classe * CarroDeCorrida. */ public class SimulacaoSemThreads {

Rafael Santos

Programao Cliente-Servidor Usando Java

37
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// Este mtodo permite a execuo da classe. public static void main(String[] args) { // Criamos instncias da classe CarroDeCorrida. CarroDeCorrida penlope = new CarroDeCorrida("Penlope Charmosa",60); CarroDeCorrida dick = new CarroDeCorrida("Dick Vigarista",100); CarroDeCorrida quadrilha = new CarroDeCorrida("Quadrilha da Morte",120); // Criados os carros, vamos executar as simulaes. penlope.executa(); dick.executa(); quadrilha.executa(); } }

A classe SimulacaoSemThreads tambm relativamente simples: criamos as instncias de CarroDeCorrida e executamos os seus mtodos executa. O problema, para uma simulao, que os trs mtodos so executados de forma dependente um do outro: s podemos executar o mtodo executa da instncia quadrilha depois de ter terminado completamente a execuo dos mtodos das instncias penlope e dick. Para uma simulao mais realista, isso no seria aceitvel os mtodos deveriam ser executados em paralelo. O resultado da execuo da classe SimulacaoSemThreads mostrar a simulao para a instncia penlope, seguida da simulao para a instncia dick e nalmente a simulao para a instncia quadrilha. A forma mais simples para fazer com que as instncias possam ter um de seus mtodos executados em paralelo com outros fazer a classe que contm o mtodo herdar da classe Thread e implementar o mtodo run, que ser o mtodo a ser executado em paralelo. Isto demonstrado na classe CarroDeCorridaIndependente, mostrada na listagem 15.
Listagem 15: Classe que representa um carro de corrida para simulao (herdando de Thread).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

package cap312; /** * Esta classe representa um carro da Corrida Maluca para uma simulao. * Esta verso da classe herda da classe Thread, ento o mtodo run de * instncias desta classe poder ser executado independentemente. */ public class CarroDeCorridaIndependente extends Thread { private String nome; private int distncia; private int velocidade; /** * O construtor da classe inicializa o nome do carro e a velocidade do mesmo. */ public CarroDeCorridaIndependente(String n,int vel) { nome = n;

Rafael Santos

Programao Cliente-Servidor Usando Java

38
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

distncia = 0; velocidade = vel; } /** * Este mtodo imprime a distncia percorrida at agora. */ public void run() { while(distncia <= 1200) { System.out.println(nome+" rodou "+distncia+" km."); distncia += velocidade; // Pausa o processamento com um clculo intil. for(int sleep=0;sleep<1000000;sleep++) { double x = Math.sqrt(Math.sqrt(Math.sqrt(sleep))); } } } }

importante observar que a assinatura do mtodo run deve obrigatoriamente ser public void este mtodo no deve receber argumentos nem relanar excees. A classe SimulacaoComThreads (listagem 16) demonstra a simulao de instncias da classe CarroDeCorridaIndependente.
Listagem 16: Simulao usando instncias de CarroDeCorridaIndependente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

package cap312; /** * Esta classe faz uma simulao de corrida usando instncias da classe * CarroDeCorridaIndependente. */ public class SimulacaoComThreads { // Este mtodo permite a execuo da classe. public static void main(String[] args) { // Criamos instncias da classe CarroDeCorrida. CarroDeCorridaIndependente penlope = new CarroDeCorridaIndependente("Penlope Charmosa",60); CarroDeCorridaIndependente dick = new CarroDeCorridaIndependente("Dick Vigarista",100); CarroDeCorridaIndependente quadrilha = new CarroDeCorridaIndependente("Quadrilha da Morte",120); // Criados os carros, vamos executar as simulaes. penlope.start(); dick.start(); quadrilha.start(); } }

Quando a classe SimulacaoComThreads (listagem 16) for executada, veremos que as impresses dos nomes dos carros e respectivas posies aparecero intercaladas e em
Rafael Santos Programao Cliente-Servidor Usando Java

39

ordem aparentemente imprevisvel, pois esto sendo executadas concorrentemente. Se o trecho de cdigo que simula uma pausa no processamento for reescrito para a pausa ser maior, este efeito de ordem imprevisvel ser aumentado. Se o trecho for eliminado, quase sempre as simulaes sero executadas na mesma ordem em que os mtodos run forem chamados, embora no exista garantia que isso sempre vai acontecer. Mesmo se o tempo de pausa fosse simulado de forma aleatria para cada execuo, o resultado da execuo da classe SimulacaoSemThreads seria o mesmo (na ordem de chamada do mtodo executa) enquanto que seguramente os resultados seriam diferentes para cada execuo da classe SimulacaoComThreads. importante notar que declaramos o mtodo run na classe CarroDeCorridaIndependente (listagem 15) como sendo o ponto de entrada do processamento a ser feito em paralelo, mas devemos executar o mtodo start (veja listagem 16) para dar incio execuo do mtodo run. O que possibilita a execuo de mtodos concorrentemente o fato do mtodo start inicializar a execuo da thread e retornar imediatamente. No exemplo da classe SimulacaoComThreads (listagem 16), logo depois que a thread para processamento dos dados da instncia penlope inicializada, o controle retorna para o mtodo main, que executa o mtodo start para a instncia dick, retornando novamente o controle para o mtodo main, que nalmente executa o mtodo start para a instncia quadrilha. Enquanto o mtodo main continua sendo executado, os mtodos run das instncias da classe CarroDeCorridaIndependente iam so executados concorrentemente. 6.3 Exemplo: Servidor de nmeros aleatrios (para requisies simultneas)

Vamos reescrever o servidor de nmeros aleatrios (segunda verso) para usar linhas de execuo, assim fazendo com que o servidor no bloqueie usurios que tentem se conectar enquanto uma sesso estiver em andamento. Usando o conceito de linhas de execuo, o servidor, ao invs de executar um mtodo de sua prpria classe (processaConexo) quando tiver que atender um cliente, ir criar uma classe que herda de Thread cujo mtodo run executa exatamente o que o mtodo processaConexo faria. Os passos para transformar uma classe que implementa um servidor sem mltiplas linhas de execuo para classes que implementam servidores com mltiplas linhas de execuo so, ento: 1. Criar uma classe que implementa o atendimento a uma nica conexo. Esta classe dever herdar da classe Thread, ter um construtor (para possibilitar o armazenamento da instncia de Socket relacionada com a conexo que deve ser atendida por uma instncia desta mesma classe) e o mtodo run, que dever ser responRafael Santos Programao Cliente-Servidor Usando Java

40

svel pelo processamento do atendimento da conexo. Em geral, o contedo do mtodo run deve ser o mesmo dos mtodos processaConexo usados em outros exemplos. 2. Reescrever a classe do servidor para que a mesma, ao receber uma conexo (atravs do mtodo accept da classe ServerSocket, crie uma nova instncia da classe responsvel pelo atendimento a uma nica conexo e execute o mtodo start desta instncia. Usando estes passos, reescreveremos o servidor de nmeros aleatrios. Primeiro, escrevemos a classe ServicoDeNumerosAleatorios cujas instncias sero responsveis por processar uma nica conexo. A classe ServicoDeNumerosAleatorios mostrada na listagem 17 e contm somente o construtor e o mtodo run que executa todo o atendimento conexo.
Listagem 17: Classe que implementa o servio de nmeros aleatrios
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

package cap312; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; /** * Esta classe representa o processamento que deve ser feito quando uma * nica requisio for feita a um servidor. A classe herda de Thread para que * vrias instncias dela possam ser executadas concorrentemente. */ public class ServicoDeNumerosAleatorios extends Thread { // Precisamos armazenar o socket correspondente conexo. private Socket conexo = null; /** * O construtor desta classe recebe como argumento uma instncia de Socket * e o armazena em um campo da classe. */ public ServicoDeNumerosAleatorios(Socket conn) { conexo = conn; } /** * Este mtodo executa a rotina de atendimento a uma conexo com o servidor. */ public void run() { try { // Criamos uma stream para receber valores nativos, usando a stream de entrada // associado conexo. DataInputStream entrada = new DataInputStream(conexo.getInputStream()); // Criamos uma stream para enviar valores nativos, usando a stream de sada // associado conexo.

Rafael Santos

Programao Cliente-Servidor Usando Java

41
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65

DataOutputStream sada = new DataOutputStream(conexo.getOutputStream()); // Lemos do cliente quantos nmeros ele deseja. int quantidade = entrada.readInt(); // Enviamos para o cliente os nmeros. for(int q=0;q<quantidade;q++) { double rand = Math.random(); sada.writeDouble(rand); // Para demonstrar que realmente funciona, vamos imprimir um log. System.out.println("Acabo de enviar o nmero "+rand+" para o cliente "+ conexo.getRemoteSocketAddress()); } // Ao terminar de atender a requisio, fechamos as streams. entrada.close(); sada.close(); // Fechamos tambm a conexo. conexo.close(); } // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro atendendo a uma conexo !"); } } }

necessrio armazenar na classe ServicoDeNumerosAleatorios (listagem 17) uma instncia da classe Socket que corresponde ao socket que foi criado para comunicao com o cliente, pois no poderamos passar esta instncia como argumento para o mtodo run o mesmo deve ser sempre executado sem argumentos. A classe que representa o servidor tambm foi modicada de acordo com os passos indicados. A classe TerceiroServidorDeNumerosAleatorios contm agora somente o mtodo main que cria novas instncias de ServicoDeNumerosAleatorios quando novas conexes so estabelecidas, e que executa o mtodo start destas instncias, retornando o controle ao mtodo main imediatamente. A classe TerceiroServidorDeNumerosAleatorios mostrada na listagem 18.
Listagem 18: Terceira verso do servidor de nmeros aleatrios
1 2 3 4 5 6 7 8 9 10 11 12 13

package cap312; import java.io.IOException; import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; /** * Esta classe implementa um servidor simples que fica aguardando conexes de * clientes. Quando uma conexo solicitada, o servidor l do cliente a * quantidade de nmeros aleatrios desejada e envia para o cliente * estes nmeros aleatrios, terminando ento a conexo. */ public class TerceiroServidorDeNumerosAleatorios

Rafael Santos

Programao Cliente-Servidor Usando Java

42
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

{ // Mtodo que permite a execuo da classe. public static void main(String[] args) { ServerSocket servidor; try { // Criamos a instncia de ServerSocket que responder por solicitaes // porta 9999. servidor = new ServerSocket(9999); // O servidor aguarda "para sempre" as conexes. while(true) { // Quando uma conexo feita, Socket conexo = servidor.accept(); // ... criamos um servio para atender a esta conexo... ServicoDeNumerosAleatorios s = new ServicoDeNumerosAleatorios(conexo); // ... e executamos o servio (que ser executado independentemente). s.start(); } } // Pode ser que a porta 9999 j esteja em uso ! catch (BindException e) { System.out.println("Porta j em uso."); } // Pode ser que tenhamos um erro qualquer de entrada ou sada. catch (IOException e) { System.out.println("Erro de entrada ou sada."); } } }

Quando o mtodo main da classe TerceiroServidorDeNumerosAleatorios (listagem 18) for executado, aguardar uma conexo do cliente. Quando esta conexo for criada (ou seja, quando o mtodo accept for executado e retornar uma instncia de Socket, uma instncia da classe ServicoDeNumerosAleatorios ser criada para tratar desta conexo, e o seu mtodo start ser executado. Imediatamente (isto , sem aguardar o nal da execuo do mtodo start ou run) o controle passar de volta para o mtodo main, que estar liberado para aceitar novas conexes. O cliente desta classe no precisa de modicaes, como cliente podemos usar a classe SegundoClienteDeNumerosAleatorios (listagem 12).

Aplicaes baseadas em um servidor e vrios clientes

Veremos agora como funciona uma aplicao onde existe um servidor e vrios clientes conectados simultaneamente, mas de forma que cada cliente tenha a sua vez de interagir com o servidor (os clientes no so independentes). Este exemplo de interao ilustra o papel de um servidor como mediador em um jogo, por exemplo.
Rafael Santos Programao Cliente-Servidor Usando Java

43

7.1

Exemplo: Servidor de jogo-da-velha

Vejamos um exemplo de aplicao cliente-servidor com um servidor e dois clientes sincronizados (isto , que tem a sua vez para interagir com o servidor determinadas e em ordem). O jogo-da-velha um bom exemplo: dois jogadores, atravs dos seus clientes, se conectam com o servidor, e um depois do outro, enviam comandos para este. Alm de receber e processar os movimentos feitos pelos clientes, o servidor responsvel por analisar estes movimentos (isto , vericar se eles so vlidos), informar os jogadores dos movimentos de ambos e determinar quando o jogo tiver um vencedor (terminando as conexes com os clientes). O algoritmo para controlar as jogadas e vericar se houve vencedores no jogo-da-velha simples. Consideremos que o jogo implementado como um tabuleiro de 3x3 posies. Internamente este tabuleiro ser um array bidimensional de caracteres, onde cada posio (caracter) poder ser igual a um X ou O se estiver ocupado ou espao se estiver livre. Para que seja mais fcil para o jogador indicar a posio onde quer jogar, as linhas do tabuleiro sero indicadas por A, B e C e as colunas por 1, 2 e 3. A gura 13 mostra o tabuleiro com os indicadores de linhas e colunas.

1 A B C

Figura 13: Posies para jogo e linhas vencedoras no jogo-da-velha

O algoritmo que verica se houve um vencedor para o jogo tambm simples. Existem oito combinaes de posies que se forem ocupadas pelo mesmo jogador determinam a vitria deste jogador: se as trs linhas, trs colunas ou duas diagonais do tabuleiro forem totalmente preenchidas pelo mesmo jogador, este ganhou o jogo. As setas mostradas na gura 13 indicam estas combinaes. Para que um jogo possa ser executado, so necessrios dois jogadores (dois clientes). Conforme mencionado anteriormente, os clientes no se conectam e enviam/recebem informaes do servidor simultaneamente (usando linhas de execuo): a comunicao feita por vezes, uma vez sendo a de um cliente/jogador e a vez seguinte sendo do outro.

Rafael Santos

Programao Cliente-Servidor Usando Java

44

O protocolo do jogo simples: o servidor aguarda a conexo de dois clientes, e alterna o recebimento de comandos (posies para jogo) de um cliente para o outro. Depois da jogada de cada cliente, o servidor verica se a jogada vlida, modicando o tabuleiro do jogo se o for. Se houver vencedor, ou se no houver mais posies onde o jogador da vez possa jogar, o resultado apresentado e a conexo com os clientes encerrada. O protocolo de interao entre os clientes e o servidor mostrado na gura 14.

Cliente 1
Incio
Solicita conexo

Servidor
Incio
Aguarda conexo

Cliente 2
Incio
Solicita conexo

Cria conexo

Aguarda conexo

Cria conexo

Envia movimento

Recebe movimento
Venceuo jogo?

S N Imprime situao Envia situao Imprime situao

Recebe movimento
Venceuo jogo?

Envia movimento

N S

Imprime situao

Envia situao

Imprime situao

Fim

Fim

Figura 14: Protocolo de comunicao entre o servidor e os clientes de jogo-da-velha

A implementao de uma classe que atua como servidora de jogo-da-velha (classe ServidorDeJogoDaVelha) mostrada na listagem 19.
Rafael Santos Programao Cliente-Servidor Usando Java

45 Listagem 19: O servidor de Jogo-da-Velha


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

package cap312; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; /** * Esta classe implementa um servidor de jogo-da-velha. Este servidor aguarda * a conexo de dois clientes e gerencia o jogo entre eles. */ public class ServidorDeJogoDaVelha { // Declaramos uma matriz de caracteres como sendo o tabuleiro do jogo. private static char[][] tabuleiro; // Assumimos que toda a entrada e sada ser feita por strings. // Declaramos instncias de BufferedReader e BufferedWriter para cada cliente. private static BufferedReader entrada1,entrada2; private static BufferedWriter sada1,sada2; // Mtodo que permite a execuo da classe. public static void main(String[] args) { // Alocamos memria para o tabuleiro do jogo. tabuleiro = new char[3][3]; try { // Criamos a instncia de ServerSocket que responder por solicitaes // porta 12345. ServerSocket servidor = new ServerSocket(12345); // O servidor aguarda "para sempre" as conexes. while(true) { // Aguardamos a primeira conexo... Socket conexo1 = servidor.accept(); System.out.println("Conexo 1 aceita para o cliente "+ conexo1.getRemoteSocketAddress()); // Criamos as streams de entrada e sada para o cliente 1. entrada1 = new BufferedReader(new InputStreamReader(conexo1.getInputStream())); sada1 = new BufferedWriter(new OutputStreamWriter(conexo1.getOutputStream())); // Mandamos uma mensagem de boas vindas. enviaMensagem(sada1,"Ol, voc ser o primeiro jogador (X)."); enviaMensagem(sada1,"Por favor aguarde o outro jogador..."); // Aguardamos a segunda conexo... Socket conexo2 = servidor.accept(); System.out.println("Conexo 2 aceita para o cliente "+ conexo2.getRemoteSocketAddress()); // Criamos as streams de entrada e sada para o cliente 1. entrada2 = new BufferedReader(new InputStreamReader(conexo2.getInputStream())); sada2 = new BufferedWriter(new OutputStreamWriter(conexo2.getOutputStream())); // Mandamos uma mensagem de boas vindas.

Rafael Santos

Programao Cliente-Servidor Usando Java

46
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120

enviaMensagem(sada2,"Ol, voc ser o segundo jogador (O)."); enviaMensagem(sada1,"Segundo jogador conectado."); // Quando as duas conexes tiverem sido estabelecidas e as streams // criadas, iniciamos o processamento. processaJogo(); // Ao terminar de atender a requisio de jogo, fechamos os streams. entrada1.close(); entrada2.close(); sada1.close(); sada2.close(); // Fechamos tambm as conexes. conexo1.close(); conexo2.close(); } } // Pode ser que a porta 12345 j esteja em uso ! catch (BindException e) { System.out.println("Porta j em uso."); } // Pode ser que tenhamos um erro qualquer de entrada ou sada. catch (IOException e) { System.out.println("Erro de entrada ou sada."); } } // Este mtodo executa a lgica do jogo, tendo acesso aos mecanismos de entrada // e sada de duas conexes. private static void processaJogo() { // A primeira coisa a fazer "limpar" o tabuleiro. for(int linha=0;linha<3;linha++) for(int coluna=0;coluna<3;coluna++) tabuleiro[linha][coluna] = ; // String que ser reusada para as entradas de comandos. String jogada; // Coordenadas da jogada. int linha,coluna; // Pea do vencedor. char vencedor; try { // Enviamos o tabuleiro para os dois jogadores pela primeira vez. mostraTabuleiro(); // Executamos um lao "eterno" while(true) { // Lemos e verificamos a jogada do primeiro jogador. enviaMensagem(sada2,"Aguarde movimento do jogador X."); enviaMensagem(sada1,"Jogador X, entre seu movimento."); jogada = entrada1.readLine(); enviaMensagem(sada1,"Jogador X escolheu "+jogada); enviaMensagem(sada2,"Jogador X escolheu "+jogada); // Quais so as coordenadas da jogada ? linha = jogada.charAt(0)-A; // A = 0; B = 1; C = 2; coluna = jogada.charAt(1)-1; // 1 = 0; 2 = 1; 3 = 2; // A jogada vlida ? if ((linha >= 0) && (linha <= 2) && (coluna >= 0) && (coluna <= 2) && (tabuleiro[linha][coluna] == ))

Rafael Santos

Programao Cliente-Servidor Usando Java

47
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181

{ tabuleiro[linha][coluna] = X; } else { enviaMensagem(sada1,"Jogada do jogador X em posio invlida."); enviaMensagem(sada2,"Jogada do jogador X em posio invlida."); } // Enviamos o tabuleiro para os dois jogadores. mostraTabuleiro(); // Verificamos se algum venceu o jogo. vencedor = verificaVencedor(); if (vencedor == V) { mostraTabuleiro(); enviaMensagem(sada1,"Empate!"); enviaMensagem(sada2,"Empate!"); break; } else if (vencedor != ) { mostraTabuleiro(); enviaMensagem(sada1,"Vencedor: jogador "+vencedor); enviaMensagem(sada2,"Vencedor: jogador "+vencedor); break; } // Lemos e verificamos a jogada do segundo jogador. enviaMensagem(sada1,"Aguarde movimento do jogador O."); enviaMensagem(sada2,"Jogador O, entre seu movimento."); jogada = entrada2.readLine(); enviaMensagem(sada1,"Jogador O escolheu "+jogada); enviaMensagem(sada2,"Jogador O escolheu "+jogada); // Quais so as coordenadas da jogada ? linha = jogada.charAt(0)-A; // A = 0; B = 1; C = 2; coluna = jogada.charAt(1)-1; // 1 = 0; 2 = 1; 3 = 2; // A jogada vlida ? if ((linha >= 0) && (linha <= 2) && (coluna >= 0) && (coluna <= 2) && (tabuleiro[linha][coluna] == )) { tabuleiro[linha][coluna] = O; } else { enviaMensagem(sada1,"Jogada do jogador O em posio invlida."); enviaMensagem(sada2,"Jogada do jogador O em posio invlida."); } // Enviamos o tabuleiro para os dois jogadores. mostraTabuleiro(); // Verificamos se algum venceu o jogo. vencedor = verificaVencedor(); if (vencedor == V) { mostraTabuleiro(); enviaMensagem(sada1,"Empate!"); enviaMensagem(sada2,"Empate!"); break; } else if (vencedor != ) { enviaMensagem(sada1,"Vencedor: jogador "+vencedor);

Rafael Santos

Programao Cliente-Servidor Usando Java

48
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242

enviaMensagem(sada2,"Vencedor: jogador "+vencedor); break; } } // fim do while true } // fim do bloco try // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro executando o lao principal do jogo !"); } } /** * Este mtodo mostra o tabuleiro do jogo para os dois clientes. Ele monta * uma string contendo o tabuleiro (inclusive movimentos j jogados) com alguma * decorao para o usurio saber que posies ainda podem ser jogadas. A string * ento enviada para os dois clientes. */ private static void mostraTabuleiro() { String tabuleiroFormatado = ""; String tempLinha; tabuleiroFormatado += " \n"; tabuleiroFormatado += " 1 2 3 \n"; tabuleiroFormatado += " +-+-+-+\n"; for(int linha=0;linha<3;linha++) { tempLinha = (char)(A+linha)+"|"; for(int coluna=0;coluna<3;coluna++) tempLinha += tabuleiro[linha][coluna]+"|"; tempLinha += "\n"; tabuleiroFormatado += tempLinha; tabuleiroFormatado += " +-+-+-+\n"; } enviaMensagem(sada1,tabuleiroFormatado); enviaMensagem(sada2,tabuleiroFormatado); } /** * Este mtodo verifica se houve algum vencedor considerando a situao atual do * tabuleiro. Se houver vencedor, o mtodo retorna o caracter correspondente ao * vencedor (X ou O), seno retorna espao. */ private static char verificaVencedor() { // Vamos verificar as 8 possveis combinaes para ganhar o jogo. // Primeira linha. if ((tabuleiro[0][0] == tabuleiro[0][1]) && (tabuleiro[0][1] == tabuleiro[0][2])) return tabuleiro[0][0]; // Segunda linha. else if ((tabuleiro[1][0] == tabuleiro[1][1]) && (tabuleiro[1][1] == tabuleiro[1][2])) return tabuleiro[1][0]; // Terceira linha. else if ((tabuleiro[2][0] == tabuleiro[2][1]) && (tabuleiro[2][1] == tabuleiro[2][2])) return tabuleiro[2][0]; // Primeira coluna. else if ((tabuleiro[0][0] == tabuleiro[1][0]) && (tabuleiro[1][0] == tabuleiro[2][0])) return tabuleiro[0][0]; // Segunda coluna. else if ((tabuleiro[0][1] == tabuleiro[1][1]) && (tabuleiro[1][1] == tabuleiro[2][1])) return tabuleiro[0][1];

Rafael Santos

Programao Cliente-Servidor Usando Java

49
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282

// Terceira coluna. else if ((tabuleiro[0][2] == tabuleiro[1][2]) && (tabuleiro[1][2] == tabuleiro[2][2])) return tabuleiro[0][2]; // Diagonal descendente. else if ((tabuleiro[0][0] == tabuleiro[1][1]) && (tabuleiro[1][1] == tabuleiro[2][2])) return tabuleiro[0][0]; // Diagonal ascendente. else if ((tabuleiro[2][0] == tabuleiro[1][1]) && (tabuleiro[1][1] == tabuleiro[0][2])) return tabuleiro[2][0]; else // Nenhuma das combinaes existe. { // Ainda existe posies abertas no tabuleiro ? int posiesAbertas = 0; for(int linha=0;linha<3;linha++) for(int coluna=0;coluna<3;coluna++) if(tabuleiro[linha][coluna] == ) posiesAbertas++; if (posiesAbertas == 0) return V; // Velha ! else return ; // Ningum venceu at agora. } } /** * Este mtodo facilita o envio de mensagens para os clientes, executando * o mtodos write, newLine e flush de uma instncia de BufferedWriter. */ private static void enviaMensagem(BufferedWriter bw,String mensagem) { try { bw.write(mensagem); bw.newLine(); bw.flush(); } catch (IOException e) { System.out.println("Erro enviando mensagem."); } } }

Um exemplo de interao de um cliente (telnet) com o servidor de jogo-da-velha pode ser visto na gura 15.

Aplicaes baseadas em um cliente e vrios servidores

Se considerarmos que servidor o lado da aplicao que fornece uma informao ou executa um processamento e cliente o lado que consume esta informao ou usa o recurso do servidor, podemos imaginar aplicaes aonde ao invs de ter um ou mais clientes usando os servios de um servidor, podemos ter um nico cliente usando os servios de vrios servidores. Consideremos uma tarefa computacional cujo tempo para execuo seja potencialmente longo, mas que possa ser dividida em subtarefas menores. Podemos escrever uma apliRafael Santos Programao Cliente-Servidor Usando Java

50

Figura 15: Exemplo de interao entre um cliente (telnet) e servidor de jogo-da-velha

cao servidora que ser executada ao mesmo tempo em vrios computadores, cada uma responsvel pela soluo de uma parte do problema. Podemos criar uma aplicao cliente que solicita a cada aplicao servidora a execuo de sua parte, e que integra as solues em uma s. Evidentemente este tipo de aplicao deve envolver o processamento de dados cuja transmisso entre cliente e servidor gaste muito menos tempo do que o processamento, caso contrrio a execuo da aplicao ser mais lenta. Cuidados especiais tambm devem ser tomados para garantir a integridade do resultado, caso um dos servidores falhe. O lado cliente das aplicaes tambm precisa ser criado de forma a dar suporte a mltiplas linhas de execuo (seo 6.2), caso contrrio ele no poder enviar requisies a vrios servidores simultaneamente. Tambm ser necessrio usar algum mecanismo que informe aplicao cliente que todos os servidores j terminaram o processamento
Rafael Santos Programao Cliente-Servidor Usando Java

51

f(x)

delta

Figura 16: Clculo de uma integral usando parties e trapzios

de seus dados. Nesta seo veremos um exemplo de tarefa computacional que pode ser dividida, enviada a servidores para processamento e integrada pelo cliente, alcanando assim um possvel tempo menor de execuo quando comparado com a execuo em um nico computador. 8.1 Exemplo: Cliente e servidores para clculo de integrais

Um bom exemplo de problema que pode ser dividido em pequenos problemas menores o clculo de integrais. Consideremos uma funo matemtica qualquer, considerada entre os valores A e B. O valor da integral desta funo entre A e B a rea que a funo projeta sobre o eixo X, como mostrado na gura 8.1. Na parte superior da gura 8.1 temos uma funo f (x) denida entre os pontos A e B. O valor da integral de f (x) entre A e B a rea mostrada em cinza na gura 8.1. Uma maneira de calcular esta rea de forma aproximada dividir a rea em muitos pequeRafael Santos Programao Cliente-Servidor Usando Java

52

nos trapzios que cobrem a mesma rea, como mostrado na parte inferior da gura 8.1. Para calcular a rea coberta pela funo basta ento calcular a somatria das reas dos trapzios. Considerando a funo f (x) entre os pontos A e B e que a rea da funo ser dividida de forma que a largura da base de cada trapzio seja igual a delta, podemos calcular a rea do mesmo como sendo a somatria da rea do trapzio cuja base delta, lado direito igual a f (a) e lado esquerdo igual a f (a + delta), ou seja, igual a delta ( f (a) + f (a + delta))/2. De posse do mtodo e equaes acima, fcil ver que podemos dividir o problema da somatria em problemas menores, onde cada um deles, para ser executado, dever receber o valor de A (primeiro valor, ou valor inferior para clculo), B (segundo valor, ou valor superior), delta e claro, a funo a ser integrada. O protocolo de comunicao entre cliente e servidor(es) de clculo de integrais mostrado na gura 17. Nesta gura mostramos somente um cliente (o normal para este tipo de aplicao) e um servidor, mas em casos normais o clculo dever ser efetuado usando vrios servidores. Uma linha cinza usada para mostrar, do lado do cliente, que trecho do cdigo ser executado concomitantemente pelas linhas de execuo do cliente. Para cada linha de execuo do lado do cliente, teremos uma conexo com um dos servidores. O cdigo para o servidor de clculo de integrais mostrado na listagem 20.
Listagem 20: O servidor de clculo de integrais
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

package cap312; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; /** * Esta classe implementa um servidor simples que fica aguardando conexes de * clientes. Quando uma conexo solicitada, o servidor l do cliente o * intervalo e preciso para o clculo da funo, efetua o clculo e retorna * para o cliente o resultado. */ public class ServidorDeCalculoDeIntegrais { // Mtodo que permite a execuo da classe. public static void main(String[] args) { // O nmero da porta ser obtido da linha de comando. int porta = Integer.parseInt(args[0]); ServerSocket servidor; try { // Criamos a instncia de ServerSocket que responder por solicitaes // porta. servidor = new ServerSocket(porta);

Rafael Santos

Programao Cliente-Servidor Usando Java

53

Cliente
Incio
Divide tarefa em subtarefas
Para cada subtarefa...

Servidor 1
Incio

Solicita conexo

Aguarda conexo

Cria conexo

Envia valores A, B e delta

Recebe valores

Efetua clculo

Recebe resultado

Envia resultado

Aguarda todos os resultados

Imprime resultado

Fim

Figura 17: Protocolo para aplicao que faz o clculo de uma integral
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

// O servidor aguarda "para sempre" as conexes. while(true) { // Quando uma conexo feita, Socket conexo = servidor.accept(); // ... o servidor a processa. processaConexo(conexo); } } // Pode ser que a porta j esteja em uso ! catch (BindException e) { System.out.println("Porta "+porta+" j em uso."); } // Pode ser que tenhamos um erro qualquer de entrada ou sada. catch (IOException e) { System.out.println("Erro de entrada ou sada.");

Rafael Santos

Programao Cliente-Servidor Usando Java

54
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106

} } // Este mtodo atende a uma conexo feita a este servidor. private static void processaConexo(Socket conexo) { try { // Criamos uma stream para receber valores nativos, usando a stream de entrada // associado conexo. DataInputStream entrada = new DataInputStream(conexo.getInputStream()); // Criamos uma stream para enviar valores nativos, usando a stream de sada // associado conexo. DataOutputStream sada = new DataOutputStream(conexo.getOutputStream()); // Lemos do cliente o intervalo da funo. double incio = entrada.readDouble(); double fim = entrada.readDouble(); // Lemos do cliente a largura do trapzio para clculo da funo. double delta = entrada.readDouble(); // Fazemos o clculo. double resultado = calculaIntegral(incio,fim,delta); // Enviamos para o cliente o valor do clculo. sada.writeDouble(resultado); // Para demonstrar que realmente funciona, vamos imprimir um log. System.out.println("Acabo de enviar o resultado "+resultado+" para o cliente "+ conexo.getRemoteSocketAddress()); // Ao terminar de atender a requisio, fechamos as streams. entrada.close(); sada.close(); // Fechamos tambm a conexo. conexo.close(); } // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro atendendo a uma conexo !"); } } /** * Este mtodo calcula a integral de uma funo entre os valores passados * usando como delta o valor tambm passado. O mtodo usa a regra do * trapzio. */ private static double calculaIntegral(double incio,double fim,double delta) { double a = incio; double somaDasreas = 0; // Vamos percorrendo os valores para argumentos da funo... while((a+delta) <= fim) { double alturaA = funo(a); double alturaB = funo(a+delta); // Calculamos a rea do trapzio local. double reaTrapzio = delta*(alturaA+alturaB)/2; // Somamos rea total. somaDasreas += reaTrapzio; a += delta; }

Rafael Santos

Programao Cliente-Servidor Usando Java

55
107 108 109 110 111 112 113 114 115 116 117 118

return somaDasreas; } /** * Este mtodo calcula uma funo em determinado ponto. */ private static double funo(double argumento) { return Math.sin(argumento)*Math.sin(argumento); } }

Alguns pontos notveis no cdigo da classe ServidorDeCalculoDeIntegrais (listagem 20) so: Quando executarmos a aplicao servidora devemos passar o nmero da porta como parmetro. Em algumas situaes isso pode ser incmodo (o cliente dever saber em que endereos e em que portas ele deve se conectar), o uso de uma porta padro recomendado (isso no foi feito para nalidades de testes). O servidor usa o mesmo padro de desenvolvimento de outros servidores: um lao eterno que aguarda conexes do cliente para receber seus dados, process-los e retorn-los. Desta forma o mesmo servidor pode ser reutilizado por um ou mais clientes, embora somente processe uma requisio de cada vez o servidor no executa mltiplas linhas de execuo. O mtodo processaConexo processa uma nica conexo do cliente, onde sero recebidos os valores de A, B e delta; e enviado o valor da integral entre A e B. Este mtodo executa o mtodo calculaIntegral usando estes parmetros, e calculando a somatria das reas de todos os trapzios entre A e B. Para isto, o mtodo calculaIntegral executa o mtodo funo que calcula o valor da funo em um determinado ponto. Para este exemplo, a funo foi codicada diretamente no servidor, e a funo sin(x) sin(x), escolhida pois sua integral entre os valores 0 e /2 exatamente6 igual a /4, facilitando a vericao do algoritmo. Conforme mencionado anteriormente, o cliente para esta aplicao dever ser capaz de tratar com linhas de execuo. Mais exatamente, o cliente dever criar uma linha de execuo para cada subtarefa que ser enviada por sua parte para um servidor. A classe que representa uma subtarefa ou linha de execuo a classe LinhaDeExecucaoDeCalculoDeIntegrais, mostrada na listagem 21.
Listagem 21: Uma linha de execuo para o cliente de clculo de integrais
1 2

package cap312; import java.io.DataInputStream;


o valor do resultado desejado (/4) para comparar o resultado obtido, mas j sabendo de antemo que os resultados no sero exatamente iguais por causa de vrios fatores: impreciso do valor /4, erros de arredondamento naturais e preciso do algoritmo de clculo dependente do valor delta.
6 Usaremos

Rafael Santos

Programao Cliente-Servidor Usando Java

56
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

import import import import

java.io.DataOutputStream; java.io.IOException; java.net.Socket; java.net.UnknownHostException;

/** * Esta classe representa o processamento que deve ser feito quando uma * requisio de clculo de intgrais for feita a um servidor. * A classe herda de Thread para que vrias instncias dela possam ser * executadas concorrentemente. */ public class LinhaDeExecucaoDeCalculoDeIntegrais extends Thread { // Precisamos armazenar o socket correspondente conexo. private Socket conexo = null; // Tambm precisamos armazenar os valores para clculo. private double incio,fim,delta,resultado; /** * O construtor desta classe recebe como argumento o nmero da porta do * servidor e o armazena em um campo da classe. O construtor tambm recebe * como argumentos o intervalo e delta para clculo da integral. */ public LinhaDeExecucaoDeCalculoDeIntegrais(String servidor,int porta, double i,double f,double d) { try { conexo = new Socket(servidor,porta); } catch (UnknownHostException e) { System.out.println("Host desconhecido !"); } catch (IOException e) { System.out.println("Erro de entrada ou sada !"); } incio = i; fim = f; delta = d; } /** * Este mtodo executa a rotina de atendimento a uma conexo com o servidor. */ public void run() { try { // Criamos uma stream para receber valores nativos, usando a stream de entrada // associado conexo. DataInputStream entrada = new DataInputStream(conexo.getInputStream()); // Criamos uma stream para enviar valores nativos, usando a stream de sada // associado conexo. DataOutputStream sada = new DataOutputStream(conexo.getOutputStream()); // Enviamos ao servidor o intervalo da funo. sada.writeDouble(incio); sada.writeDouble(fim);

Rafael Santos

Programao Cliente-Servidor Usando Java

57
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88

// Enviamos ao servidor o nmero de intervalos da funo. sada.writeDouble(delta); // Lemos do servidor o resultado do clculo. resultado = entrada.readDouble(); // Ao terminar de atender a requisio, fechamos as streams. entrada.close(); sada.close(); // Fechamos tambm a conexo. conexo.close(); } // Se houve algum erro de entrada ou sada... catch (IOException e) { System.out.println("Erro atendendo a uma conexo !"); } } /** * Este mtodo permite recuperar o valor calculado. */ public double getResultado() { return resultado; } }

A classe LinhaDeExecucaoDeCalculoDeIntegrais (listagem 21) contm campos para armazenar o nome e porta do servidor com o qual ir se comunicar, alm dos parmetros para clculo da integral (A, B e delta). Estes campos so inicializados pelo construtor da classe. Como a classe herda de Thread deve implementar seu mtodo run para que suas instncias possam ser executadas concomitantemente. O mtodo run simplesmente cria a conexo com o servidor, envia os dados, recebe o valor da somatria e o armazena para que outra aplicao o possa recuperar com o mtodo getResultado. A classe que implementa o cliente, que por sua vez ir criar algumas instncias da classe LinhaDeExecucaoDeCalculoDeIntegrais, a classe ClienteDeCalculoDeIntegrais, mostrada na listagem 22.
Listagem 22: O cliente de clculo de integrais
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

package cap312; /** * Esta classe implementa um cliente simples para o servio de clculo de * integrais. * Ele se conecta a dois servidores (pela portas nas quais estes servidores * esto respondendo), envia trechos para clculo da integral e calcula a soma * dos resultados. */ public class ClienteDeCalculoDeIntegrais { public static void main(String[] args) { String servidor = "localhost"; // Dividimos a tarefa em duas metades que sero executadas com diferentes // precises.

Rafael Santos

Programao Cliente-Servidor Usando Java

58
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

LinhaDeExecucaoDeCalculoDeIntegrais l1 = new LinhaDeExecucaoDeCalculoDeIntegrais("localhost",11111, 0,Math.PI/4,0.000001); LinhaDeExecucaoDeCalculoDeIntegrais l2 = new LinhaDeExecucaoDeCalculoDeIntegrais("localhost",11112, Math.PI/4,Math.PI/2,0.0000001); // Iniciamos a execuo das duas tarefas. l1.start(); l2.start(); // Aguardamos at que as duas instncias tenham terminado de processar // o mtodo run. while(l1.isAlive() || l2.isAlive()) { } // Calculamos o resultado final e comparamos com o esperado. double resFinal = l1.getResultado()+l2.getResultado(); System.out.println("Resultado:"+resFinal); System.out.println("Esperado :"+Math.PI/4); } }

A classe ClienteDeCalculoDeIntegrais (listagem 22) simplesmente cria duas instncias da classe LinhaDeExecucaoDeCalculoDeIntegrais, usando (para demonstrao), o servidor local e duas portas previamente denidas, onde servidores esto aguardando conexes. Cada linha de execuo calcular metade dos trapzios correspondentes rea da integral, mas a segunda linha de execuo calcular a sua parte da integral com um valor de delta mais preciso, para que o tempo de execuo seja denitivamente diferente. Em algum ponto precisaremos vericar se as linhas de execuo j terminaram o seu processamento, para evitar erros grosseiros de clculo. Estes erros podem acontecer facilmente pois a partir do momento em que o mtodo start da classe LinhaDeExecucaoDeCalculoDeIntegrais for executado, j ser possvel executar o mtodo getResultado das mesmas, que pode retornar zero se o clculo no tiver sido terminado em outras palavras, se o mtodo run ainda estiver sendo executado. Para garantir que o mtodo getResultado somente ser executado depois que o mtodo run tiver terminado seu processamento, podemos criar um lao innito que aguarda at que o resultado dos mtodos isAlive das classes que herdam de Thread sejam ambos false este mtodo verica se uma linha de execuo ainda est viva, se o mtodo retorna true se a linha de execuo ainda est sendo executada. Ao nal do processamento basta somar os resultados obtidos pelas duas linhas de execuo e apresentar o resultado esperado para comparao.

Mais informaes

Considero este documento ainda inacabado existem outros tpicos interessantes que podemos explorar, como por exemplo:
Rafael Santos Programao Cliente-Servidor Usando Java

59

Criao de aplicaes que usem servidores em mltiplas camadas, como servidores de meta-dados, por exemplo. Estas aplicaes tem clientes que precisam de dados mas no sabem onde estes dados esto: estes clientes se conectam a um servidor de meta-dados, que por sua vez se comunica com vrios servidores (conhecidos) de dados para descobrir qual deles contm a informao que o cliente necessita. Balanceamento simples de carga: consideremos o exemplo do clculo de integrais apresentado na seo 8.1. Duas linhas de execuo foram criadas, cada uma efetuando um clculo desbalanceado: a primeira linha de execuo demorou muito mais do que a segunda. Podemos considerar esquemas simples de balanceamento, que fazem com que servidores aparentemente ociosos recebam carga a mais, para tentar chegar ao nal do processamento como um todo em menos tempo. Servios de descoberta de servidores e balanceamento dinmico, que combinam as idias apresentadas nos dois tpicos acima. O cliente de clculo de integrais poderia ento tentar descobrir quais servidores existem e esto disposio, mesmo enquanto estiver sendo executado, para obter melhor performance. Esta descoberta seria mediada por um servidor de meta-dados, que estaria sendo atualizado permanentemente com informaes sobre os servidores e sua carga. Explorar outros exemplos como renderizao de cenas por computadores distribudos, processamento de imagens distribudo, srevios de anlise e descoberta de informaes, jogos multi-usurios, servios Peer to Peer, etc. como estes exemplos envolvem outras tecnologias e conhecimentos alm de programao, possivelmente sero tratados caso a caso, se houver interesse por parte de algum. Como existiram restries de tempo quando escrevi este documento, e o contedo do mesmo atende plenamente nalidade original (servir de subsdio aos alunos da disciplina Linguagem de Programao Orientada a Objetos do curso de Tecnlogo em Redes de Computadores do Instituto Brasileiro de Tecnologia Avanada - IBTA, Unidade de So Jos dos Campos), encerrei o documento com somente os tpicos apresentados. Em outra ocasio, se houver demanda, poderei cobrir alguns dos tpicos sugeridos acima.

Rafael Santos Maro de 2004

Rafael Santos

Programao Cliente-Servidor Usando Java

You might also like