Skip to content

8. Programação TCP-IP

8.1. Informações gerais

8.1.1. Os protocolos da Internet

Apresentamos aqui uma introdução aos protocolos de comunicação da Internet, também conhecidos como conjunto de protocolos TCP/IP (Protocolo de Controle de Transmissão / Protocolo de Internet), em referência aos dois principais protocolos. É recomendável que o leitor tenha uma compreensão geral do funcionamento das redes e, em especial, dos protocolos TCP/IP antes de abordar a construção de aplicativos distribuídos.

O texto a seguir é uma tradução parcial de um texto encontrado no documento “Lan Workplace for Dos - Administrator's Guide” da NOVELL, documento do início dos anos 90.


O conceito geral de criar uma rede de computadores heterogêneos tem origem em pesquisas realizadas pela DARPA (Agência de Projetos de Pesquisa Avançada de Defesa) nos Estados Unidos. A DARPA desenvolveu o conjunto de protocolos conhecido como TCP/IP, que permite que máquinas heterogêneas se comuniquem entre si. Esses protocolos foram testados em uma rede chamada ARPAnet, que posteriormente se tornou a rede INTERNET. Os protocolos TCP/IP definem formatos e regras de transmissão e recepção independentes da organização das redes e dos equipamentos utilizados.

A rede projetada pelo DARPA e gerenciada pelos protocolos TCP/IP é uma rede de comutação de pacotes. Essa rede transmite as informações pela rede em pequenos pedaços chamados pacotes. Assim, se um computador transmitir um arquivo grande, este será dividido em pequenos pedaços que serão enviados pela rede para serem recompostos no destino. O TCP/IP define o formato desses pacotes, a saber:

  • origem do pacote
  • destino
  • comprimento
  • tipo

8.1.2. O modelo OSI

Os protocolos TCP/IP seguem, em linhas gerais, o modelo de rede aberta denominado OSI (Modelo de Referência para Interconexão de Sistemas Abertos), definido pela ISO (Organização Internacional de Padronização). Esse modelo descreve uma rede ideal na qual a comunicação entre máquinas pode ser representada por um modelo de sete camadas:

Image

Cada camada recebe serviços da camada inferior e oferece os seus próprios serviços à camada superior. Suponhamos que duas aplicações localizadas em máquinas A e B diferentes desejam se comunicar: elas o fazem no nível da camada Application. Elas não precisam conhecer todos os detalhes do funcionamento da rede: cada aplicativo entrega a informação que deseja transmitir à camada inferior: a camada Présentation. O aplicativo precisa, portanto, conhecer apenas as regras de interface com a camada Présentation.

Assim que a informação chega à camada Présentation, ela é transferida, de acordo com outras regras, para a camada Session e assim por diante, até que a informação chegue ao suporte físico e seja transmitida fisicamente ao equipamento de destino. Lá, ela passará pelo processo inverso ao que ocorreu na máquina remetente.

Em cada camada, o processo remetente encarregado de enviar a informação a transmite a um processo receptor na outra máquina, pertencente à mesma camada que ele. Isso ocorre de acordo com certas regras, conhecidas como protocolo da camada. Assim, temos o seguinte esquema final de comunicação:

Image

A função das diferentes camadas é a seguinte:

Physique
Assegura a transmissão de bits por meio de um suporte físico. Nesta camada, encontram-se equipamentos terminais de processamento de dados (E.T.T.D.), tais como terminais ou computadores, bem como equipamentos de terminação de circuitos de dados (E.T.C.D.), tais como moduladores/demoduladores, multiplexadores e concentradores. Os pontos de interesse neste nível são:
. a escolha da codificação da informação (analógica ou digital)
. a escolha do modo de transmissão (síncrono ou assíncrono).
Liaison de données
Oculta as características físicas da camada Física. Detecta e corrige erros de transmissão.
Réseau
Gerencia o caminho que as informações enviadas pela rede devem seguir. Isso é chamado de routage: determinar a rota que uma informação deve seguir para chegar ao seu destinatário.
Transport
Permite a comunicação entre duas aplicações, enquanto as camadas anteriores permitiam apenas a comunicação entre máquinas. Um serviço fornecido por essa camada pode ser o multiplexamento: a camada de transporte poderá utilizar uma mesma conexão de rede (de máquina para máquina) para transmitir informações pertencentes a várias aplicações.
Session
Nesta camada, encontramos serviços que permitem que uma aplicação abra e mantenha uma sessão de trabalho em uma máquina remota.
Présentation
Ela visa uniformizar a representação dos dados nas diferentes máquinas. Assim, os dados provenientes de uma máquina A serão “formatados” pela camada Présentation da máquina A, de acordo com um formato padrão, antes de serem enviados pela rede. Ao chegarem à camada Présentation da máquina destinatária B, que as reconhecerá graças ao seu formato padrão, elas serão formatadas de outra maneira para que o aplicativo da máquina B as reconheça.
Application
Nesse nível, encontram-se as aplicações geralmente próximas ao usuário, como o e-mail ou a transferência de arquivos.

8.1.3. O modelo TCP/IP

O modelo OSI é um modelo ideal que ainda não foi concretizado. O conjunto de protocolos TCP/IP se aproxima dele da seguinte forma:

Image

Camada Física

Em redes locais, geralmente se utiliza a tecnologia Ethernet ou Token-Ring. Aqui, apresentamos apenas a tecnologia Ethernet.

Ethernet

Esse é o nome dado a uma tecnologia de redes locais com comutação de pacotes inventada pela Xerox no início da década de 1970 e padronizada pela Xerox, Intel e Digital Equipment em 1978. A rede é constituída fisicamente por um cabo coaxial com cerca de 1,27 cm de diâmetro e comprimento máximo de 500 m. Ela pode ser estendida por meio de répéteurs, sendo que duas máquinas não podem estar separadas por mais de dois repetidores. O cabo é passivo: todos os elementos ativos estão nos equipamentos conectados ao cabo. Cada equipamento é conectado ao cabo por meio de uma placa de acesso à rede que inclui:

  • um transmissor (transceiver) que detecta a presença de sinais no cabo e converte os sinais analógicos em sinais digitais e vice-versa.
  • um acoplador que recebe os sinais digitais do transmissor e os transmite ao computador para processamento, ou vice-versa.

As principais características da tecnologia Ethernet são as seguintes:

  • Capacidade de 10 megabits por segundo.
  • Topologia em barramento: todas as máquinas estão conectadas ao mesmo cabo

Image

  • Rede de difusão — Um equipamento que transmite envia informações pelo cabo com o endereço do equipamento destinatário. Todos os equipamentos conectados recebem essas informações, e apenas aquele a quem elas se destinam as retém.
  • O método de acesso é o seguinte: o transmissor que deseja transmitir escuta o cabo — ele detecta, então, a presença ou ausência de uma onda portadora, cuja presença significaria que uma transmissão está em andamento. Essa é a técnica CSMA (Carrier Sense Multiple Access). Na ausência de portadora, um transmissor pode decidir transmitir por sua vez. Vários transmissores podem tomar essa decisão. Os sinais emitidos se misturam: diz-se que ocorre uma colisão. O transmissor detecta essa situação: ao mesmo tempo em que transmite pelo cabo, ele escuta o que realmente está passando por ele. Se detectar que a informação que transita pelo cabo não é a que ele transmitiu, ele deduz que houve colisão e interrompe a transmissão. Os outros transmissores que estavam transmitindo farão o mesmo. Cada um retomará sua transmissão após um intervalo aleatório, que depende de cada transmissor. Essa técnica é chamada de CD (Detecção de Colisão). O método de acesso é, portanto, denominado CSMA/CD.
  • um endereçamento de 48 bits. Cada máquina possui um endereço, aqui denominado endereço físico, que está gravado na placa que a conecta ao cabo. Esse endereço é chamado de endereço Ethernet da máquina.

Camada de Rede

Nesta camada, encontramos os protocolos IP, ICMP, ARP e RARP.

IP (Protocolo de Internet)
Transmite pacotes entre dois nós da rede
ICMP
(Protocolo de Mensagens de Controle da Internet)
O ICMP estabelece a comunicação entre o programa do protocolo IP de uma máquina e o de outra máquina. Trata-se, portanto, de um protocolo de troca de mensagens dentro do próprio protocolo IP.
ARP
(Protocolo de Resolução de Endereços)
realiza a correspondência entre o endereço de Internet da máquina e o endereço físico da máquina
RARP
(Protocolo de Resolução de Endereços Reverso)
faz a correspondência entre o endereço físico da máquina e o endereço de Internet da máquina

Camadas de Transporte/Sessão

Nesta camada, encontram-se os seguintes protocolos:

TCP (Protocolo de Controle de Transmissão)
Garante a entrega confiável de informações entre dois clientes
UDP (Protocolo de Datagrama do Usuário)
Garante a entrega não confiável de informações entre dois clientes

Camadas de Aplicação/Apresentação/Sessão

Encontram-se aqui diversos protocolos:

TELNET
Emulador de terminal que permite que uma máquina A se conecte a uma máquina B como um terminal
FTP (File Transfer Protocol)
permite a transferência de arquivos
TFTP (Trivial File Transfer Protocol)
permite a transferência de arquivos
SMTP (Simple Mail Transfer protocol)
permite a troca de mensagens entre usuários da rede
DNS (Domain Name System)
converte um nome de máquina no endereço de Internet da máquina
XDR (eXternal Data Representation)
criado pela Sun MicroSystems, ele especifica uma representação padrão dos dados, independente das máquinas
RPC(Remote Procedures Call)
também definido pela Sun, é um protocolo de comunicação entre aplicativos remotos, independente da camada de transporte. Esse protocolo é importante: isenta o programador de conhecer os detalhes da camada de transporte e torna os aplicativos portáteis. Esse protocolo se baseia no protocolo XDR
NFS (Network File System)
também definido pela Sun; esse protocolo permite que uma máquina “veja” o sistema de arquivos de outra máquina. Ele se baseia no protocolo RPC mencionado anteriormente

8.1.4. Funcionamento dos protocolos da Internet

Os aplicativos desenvolvidos no ambiente TCP/IP geralmente utilizam vários dos protocolos desse ambiente. Um programa aplicativo se comunica com a camada mais alta dos protocolos. Essa camada repassa a informação para a camada inferior e assim por diante, até chegar ao suporte físico. Lá, a informação é fisicamente transferida para a máquina destinatária, onde atravessará as mesmas camadas, desta vez na direção oposta, até chegar ao aplicativo destinatário das informações enviadas. O esquema a seguir mostra o percurso da informação:

Image

Vejamos um exemplo: a aplicação FTP, definida na camada Application e que permite a transferência de arquivos entre máquinas.

  • A aplicação fornece uma sequência de bytes a ser transmitida à camada transport.
  • A camada transport divide essa sequência de bytes em segments e TCP, e acrescenta, no início de cada segmento, o número do próprio segmento. Os segmentos são encaminhados para a camada de rede, regida pelo protocolo IP.
  • A camada IP cria um pacote que encapsula o segmento TCP recebido. No cabeçalho desse pacote, ela insere os endereços de Internet das máquinas de origem e de destino. Ela também determina o endereço físico da máquina de destino. Tudo isso é encaminhado para a camada de enlace de dados e enlace físico, ou seja, para a placa de rede que conecta a máquina à rede física.
  • Lá, o pacote IP é, por sua vez, encapsulado em uma trama física e enviado ao seu destinatário pelo cabo.
  • Na máquina destinatária, a camada de enlace de dados e enlace físico faz o inverso: ela desencapsula o pacote IP da trama física e o repassa para a camada IP.
  • A camada IP verifica se o pacote está correto: ela calcula uma soma, com base nos bits recebidos (checksum), soma essa que deve ser encontrada no cabeçalho do pacote. Caso contrário, o pacote é rejeitado.
  • Se o pacote for considerado válido, a camada IP desencapsula o segmento TCP contido nele e o encaminha para a camada superior, a transport.
  • A camada transport — no nosso exemplo, a camada TCP — analisa o número do segmento para restabelecer a ordem correta dos segmentos.
  • Ela também calcula uma soma de verificação para o segmento TCP. Se for considerada correta, a camada TCP envia um aviso de recebimento à máquina de origem; caso contrário, o segmento TCP é rejeitado.
  • Resta à camada TCP apenas transmitir a parte de dados do segmento ao aplicativo destinatário desses dados na camada superior.

8.1.5. Problemas de endereçamento na Internet

Um noeud de uma rede pode ser um computador, uma impressora inteligente, um servidor de arquivos — na verdade, qualquer coisa capaz de se comunicar por meio dos protocolos TCP/IP. Cada nó possui um endereço físico cujo formato depende do tipo de rede. Em uma rede Ethernet, o endereço físico é codificado em 6 bytes. Um endereço de uma rede X25 é um número de 14 dígitos.

O endereço de Internet de um nó é um endereço lógico: ele é independente do hardware e da rede utilizada. Trata-se de um endereço de 4 bytes que identifica tanto uma rede local quanto um nó dessa rede. O endereço de Internet é normalmente representado na forma de 4 números, correspondentes aos valores dos 4 bytes, separados por um ponto. Assim, o endereço do computador Lagaffe da Faculdade de Ciências de Angers é 193.49.144.1 e o do computador Liny é 193.49.144.9. Deduz-se, portanto, que o endereço de Internet da rede local é 193.49.144.0. É possível ter até 254 nós nessa rede.

Como os endereços de Internet ou endereços IP são independentes da rede, um computador de uma rede A pode se comunicar com um computador de uma rede B sem se preocupar com o tipo de rede em que está: basta que ele conheça seu endereço IP. O protocolo IP de cada rede é responsável por fazer a conversão entre endereço IP e endereço físico, nos dois sentidos.

Todos os endereços IP devem ser diferentes. Órgãos oficiais são responsáveis por distribuí-los. Na verdade, esses órgãos atribuem um endereço para redes locais, por exemplo, 193.49.144.0 para a rede da Faculdade de Ciências de Angers. O administrador dessa rede pode, então, atribuir os endereços IP 193.49.144.1 a 193.49.144.254 conforme desejar. Esse endereço é geralmente registrado em um arquivo específico de cada máquina conectada à rede.

8.1.5.1. As classes de endereços IP

Um endereço IP é uma sequência de 4 bytes, frequentemente indicada como I1.I2.I3.I4, que contém, na verdade, dois endereços:

  • o endereço da rede
  • o endereço de um nó dessa rede

De acordo com o tamanho desses dois campos, os endereços IP são divididos em três classes: classes A, B e C.

Classe A

O endereço IP: I1.I2.I3.I4 tem o formato R1.N1.N2.N3, onde

R1 é o endereço da rede

N1.N2.N3 é o endereço de um computador nessa rede

Mais precisamente, o formato de um endereço IP da classe A é o seguinte:

Image

O endereço de rede ocupa 7 bits e o endereço do nó, 24 bits. Portanto, é possível ter 127 redes de classe A, cada uma com até 2²⁴ nós.

Classe B

Aqui, o endereço IP: I1.I2.I3.I4 tem o formato R1.R2.N1.N2, onde

R1.R2 é o endereço da rede

N1.N2 é o endereço de uma máquina nessa rede

Mais precisamente, o formato de um endereço IP da classe B é o seguinte:

Image

O endereço da rede ocupa 2 bytes (14 bits, exatamente), assim como o do nó. Portanto, é possível ter 2¹⁴ redes de classe B, cada uma com até 2¹⁶ nós.

Classe C

Nesta classe, o endereço IP: I1.I2.I3.I4 tem o formato R1.R2.R3.N1, onde

R1.R2.R3 é o endereço da rede

N1 é o endereço de uma máquina nessa rede

Mais precisamente, o formato de um endereço IP da classe C é o seguinte:

Image

O endereço de rede ocupa 3 bytes (menos 3 bits) e o endereço do nó, 1 byte. Portanto, é possível ter 221 redes de classe C com até 256 nós.

Como o endereço do computador Lagaffe da Faculdade de Ciências de Angers é 193.49.144.1, vemos que o byte de peso forte vale 193, ou seja, em binário 11000001. Deduz-se, portanto, que a rede é da classe C.

Endereços reservados

. Algumas endereços IP são endereços de rede, e não endereços de nós na rede. São aquelas em que o endereço do nó é definido como 0. Assim, o endereço 193.49.144.0 é o endereço IP da rede da Faculdade de Ciências de Angers. Consequentemente, nenhum nó de uma rede pode ter o endereço zero.

. Quando, em um endereço IP, o endereço do nó contém apenas 1s, trata-se de um endereço de difusão: esse endereço designa todos os nós da rede.

. Em uma rede de classe C, que teoricamente permite 2⁸ = 256 nós, se retirarmos os dois endereços proibidos, restam apenas 254 endereços autorizados.

8.1.5.2. Os protocolos de conversão Endereço da Internet <--> Endereço físico

Vimos que, durante a transmissão de informações de uma máquina para outra, essas informações, ao atravessarem a camada IP, eram encapsuladas em pacotes. Estes têm o seguinte formato:

Image

O pacote IP contém, portanto, os endereços de Internet das máquinas de origem e de destino. Quando esse pacote for transmitido à camada responsável por enviá-lo para a rede física, outras informações são adicionadas a ele para formar a trama física que será finalmente enviada para a rede. Por exemplo, o formato de uma trama em uma rede Ethernet é o seguinte:

Image

Na trama final, constam os endereços físicos dos computadores de origem e de destino. Como eles são obtidos?

O equipamento remetente, conhecendo o endereço IP do equipamento com o qual deseja se comunicar, obtém o endereço físico deste último utilizando um protocolo específico chamado ARP (Address Resolution Protocol).

  • Ela envia um pacote de um tipo especial chamado pacote ARP, contendo o endereço IP da máquina cujo endereço físico está sendo procurado. Ela também se certificou de incluir nele seu próprio endereço IP, bem como seu endereço físico.
  • Esse pacote é enviado a todos os nós da rede.
  • Esses nós reconhecem a natureza especial do pacote. O nó que reconhece seu endereço IP no pacote responde enviando ao remetente do pacote seu endereço físico. Como ele consegue fazer isso? Ele encontrou no pacote os endereços IP e o endereço físico do remetente.
  • O remetente recebe, assim, o endereço físico que procurava. Ele o armazena na memória para poder usá-lo posteriormente, caso outros pacotes precisem ser enviados ao mesmo destinatário.

O endereço IP de uma máquina normalmente está registrado em um de seus arquivos, que ela pode consultar para obtê-lo. Esse endereço pode ser alterado: basta editar o arquivo. Já o endereço físico está registrado na memória da placa de rede e não pode ser alterado.

Quando um administrador deseja organizar sua rede de maneira diferente, ele pode precisar alterar os endereços IP de todos os nós e, portanto, editar os diversos arquivos de configuração de cada um deles. Isso pode ser trabalhoso e uma fonte de erros se houver muitas máquinas. Um método consiste em não atribuir um endereço IP às máquinas: nesse caso, insere-se um código especial no arquivo no qual a máquina deveria encontrar seu endereço IP. Ao perceber que não possui o endereço IP, a máquina o solicita por meio de um protocolo chamado RARP (Reverse Address Resolution Protocol). Em seguida, ela envia para a rede um pacote especial chamado pacote RARP, análogo ao pacote ARP anterior, no qual insere seu endereço físico. Esse pacote é enviado a todos os nós, que então reconhecem um pacote RARP. Um deles, chamado servidor RARP, possui um arquivo que fornece a correspondência entre endereço físico e endereço IP de todos os nós. Ele então responde ao remetente do pacote RARP, reenviando-lhe seu endereço IP. Um administrador que deseje reconfigurar sua rede precisa, portanto, apenas editar o arquivo de correspondências do servidor RARP. Este, normalmente, deve ter um endereço fixo IP, que deve poder conhecer sem precisar utilizar ele mesmo o protocolo RARP.

8.1.6. A camada de rede conhecida como camada IP da Internet

O protocolo IP (Protocolo de Internet) define o formato que os pacotes devem assumir e a maneira como devem ser gerenciados durante sua transmissão ou recepção. Esse tipo específico de pacote é chamado de datagrama IP. Já o apresentamos anteriormente:

Image

O importante é que, além dos dados a serem transmitidos, o datagrama IP contém os endereços de Internet dos computadores de origem e de destino. Assim, o computador de destino sabe quem está enviando uma mensagem para ele.

Ao contrário de um quadro de rede, cujo comprimento é determinado pelas características físicas da rede pela qual transita, o comprimento do datagrama IP é definido pelo software e, portanto, será o mesmo em diferentes redes físicas. Vimos que, ao descer da camada de rede para a camada física, o datagrama IP era encapsulado em um quadro físico. Demos o exemplo do quadro físico de uma rede Ethernet:

Image

As tramas físicas circulam de nó em nó até seu destino, que pode não estar na mesma rede física que a máquina remetente. O pacote IP pode, portanto, ser encapsulado sucessivamente em diferentes quadros físicos nos nós que fazem a conexão entre duas redes de tipos diferentes. Também é possível que o pacote IP seja grande demais para ser encapsulado em um quadro físico. O software IP do nó onde esse problema ocorre divide, então, o pacote IP em fragments de acordo com regras precisas, sendo cada um deles enviado posteriormente pela rede física. Eles só serão remontados em seu destino final.

8.1.6.1. O roteamento

O roteamento é o método de encaminhamento dos pacotes IP até seu destino. Existem dois métodos: o roteamento direto e o roteamento indireto.

Roteamento direto

O roteamento direto refere-se ao encaminhamento de um pacote IP diretamente do remetente ao destinatário dentro da mesma rede:

  • A máquina remetente de um datagrama IP possui o endereço IP do destinatário.
  • Ela obtém o endereço físico do destinatário por meio do protocolo ARP ou em suas tabelas, caso esse endereço já tenha sido obtido.
  • Ela envia o pacote pela rede para esse endereço físico.

Roteamento indireto

O roteamento indireto refere-se ao encaminhamento de um pacote IP para um destino localizado em uma rede diferente daquela à qual pertence o remetente. Nesse caso, as partes de endereço de rede dos endereços IP das máquinas de origem e de destino são diferentes. A máquina de origem reconhece esse fato. Ela então envia o pacote para um nó especial chamado roteador (router), nó que conecta uma rede local a outras redes e cujo endereço IP ela encontra em suas tabelas — endereço obtido inicialmente em um arquivo, em uma memória permanente ou ainda por meio de informações que circulam na rede.

Um roteador está conectado a duas redes e possui um endereço IP dentro dessas duas redes.

Image

No nosso exemplo acima:

  • A rede nº 1 tem o endereço de Internet 193.49.144.0 e a rede nº 2, o endereço 193.49.145.0.
  • Dentro da rede nº 1, o roteador tem o endereço 193.49.144.6 e, dentro da rede nº 2, o endereço 193.49.145.3.

A função do roteador é transformar o pacote IP que ele recebe — e que está contido em uma trama física típica da rede nº 1 — em uma trama física capaz de circular na rede nº 2. Se o endereço IP do destinatário do pacote estiver na rede nº 2, o roteador enviará o pacote diretamente a ele; caso contrário, enviá-lo-á a outro roteador, conectando a rede nº 2 a uma rede nº 3 e assim por diante.

8.1.6.2. Mensagens de erro e de controle

Ainda na camada de rede, portanto no mesmo nível do protocolo IP, existe o protocolo ICMP (Internet Control Message Protocol). Ele serve para enviar mensagens sobre o funcionamento interno da rede: nós com falha, congestionamento em um roteador, etc... As mensagens ICMP são encapsuladas em pacotes IP e enviadas pela rede. As camadas IP dos diferentes nós tomam as medidas adequadas de acordo com as mensagens ICMP que recebem. Assim, um aplicativo, por si só, nunca percebe esses problemas específicos da rede.

Um nó utilizará as informações ICMP para atualizar suas tabelas de roteamento.

8.1.7. A camada de transporte: os protocolos UDP e TCP

8.1.7.1. O protocolo UDP: Protocolo de Datagrama do Usuário

O protocolo UDP permite uma troca não confiável de dados entre dois pontos, ou seja, o encaminhamento correto de um pacote até seu destino não é garantido. O aplicativo, se desejar, pode gerenciar isso por conta própria, aguardando, por exemplo, após o envio de uma mensagem, um aviso de recebimento, antes de enviar a seguinte.

Por enquanto, no nível da rede, falamos de endereços IP de máquinas. No entanto, em uma máquina, podem coexistir simultaneamente diferentes processos, todos capazes de se comunicar. Portanto, ao enviar uma mensagem, é preciso indicar não apenas o endereço IP da máquina destinatária, mas também o “nome” do processo destinatário. Esse nome é, na verdade, um número, chamado de número de porta. Alguns números são reservados para aplicativos padrão: a porta 69 para o aplicativo tftp (trivial file transfer protocol), por exemplo.

Os pacotes gerenciados pelo protocolo UDP também são chamados de datagramas. Eles têm o seguinte formato:

Image

Esses datagramas serão encapsulados em pacotes IP e, em seguida, em quadros físicos.

8.1.7.2. O protocolo TCP: Protocolo de Controle de Transferência

Para comunicações seguras, o protocolo UDP é insuficiente: o desenvolvedor de aplicativos deve criar ele mesmo um protocolo que lhe permita detectar o encaminhamento correto dos pacotes.

O protocolo TCP (Protocolo de Controle de Transferência) evita esses problemas. Suas características são as seguintes:

  • O processo que deseja transmitir estabelece, em primeiro lugar, uma conexão com o processo destinatário das informações que irá transmitir. Essa conexão é estabelecida entre uma porta da máquina transmissora e uma porta da máquina receptora. Entre as duas portas, é criado um caminho virtual que será reservado exclusivamente aos dois processos que estabeleceram a conexão.
  • Todos os pacotes enviados pelo processo de origem seguem esse caminho virtual e chegam na ordem em que foram enviados, o que não era garantido no protocolo UDP, uma vez que os pacotes podiam seguir caminhos diferentes.
  • A informação transmitida tem um caráter contínuo. O processo emissor envia informações em seu próprio ritmo. Essas informações não são necessariamente enviadas imediatamente: o protocolo TCP aguarda até ter quantidade suficiente para enviá-las. Elas são armazenadas em uma estrutura chamada segmento TCP. Uma vez preenchido, esse segmento será transmitido para a camada IP, onde será encapsulado em um pacote IP.
  • Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP destinatário verifica se está recebendo os segmentos na sequência correta. Para cada segmento recebido corretamente, ele envia um aviso de recebimento ao remetente.
  • Quando este último o recebe, ele informa o processo emissor. Assim, este pode saber que um segmento chegou ao destino, o que não era possível com o protocolo UDP.
  • Se, após um certo tempo, o protocolo TCP que enviou um segmento não receber uma confirmação de recebimento, ele retransmite o segmento em questão, garantindo assim a qualidade do serviço de encaminhamento da informação.
  • O circuito virtual estabelecido entre os dois processos que se comunicam é full-duplex: isso significa que a informação pode transitar nos dois sentidos. Assim, o processo de destino pode enviar confirmações de recebimento mesmo enquanto o processo de origem continua enviando informações. Isso permite, por exemplo, que o protocolo de origem TCP envie vários segmentos sem esperar pelo aviso de recebimento. Se, após um certo tempo, ele perceber que não recebeu o aviso de recebimento de um determinado segmento nº n, ele retomará a transmissão dos segmentos a partir desse ponto.

8.1.8. A camada de aplicativos

Acima dos protocolos UDP e TCP, existem diversos protocolos padrão:

TELNET

Este protocolo permite que um usuário de uma máquina A da rede se conecte a uma máquina B (frequentemente chamada de máquina host). O TELNET emula na máquina A um terminal denominado universal. O usuário, portanto, age como se tivesse um terminal conectado à máquina B. O Telnet utiliza o protocolo TCP.

FTP: (Protocolo de Transferência de Arquivos)

Este protocolo permite a troca de arquivos entre duas máquinas remotas, bem como operações com arquivos, como a criação de diretórios, por exemplo. Ele se baseia no protocolo TCP.

TFTP: (Controle Trivial de Transferência de Arquivos)

Este protocolo é uma variante do FTP. Ele se baseia no protocolo UDP e é menos sofisticado que o FTP.

DNS: (Sistema de Nomes de Domínio)

Quando um usuário deseja trocar arquivos com uma máquina remota, por meio do FTP, por exemplo, ele precisa saber o endereço da Internet dessa máquina. Por exemplo, para realizar uma conexão FTP na máquina Lagaffe da Universidade de Angers, seria necessário iniciar o FTP da seguinte forma: FTP 193.49.144.1

Isso exige que haja um diretório que faça a correspondência entre máquina <--> endereço IP. Provavelmente, nesse diretório, as máquinas seriam designadas por nomes simbólicos, tais como:

máquina DPX2/320 da Universidade de Angers

máquina Sun da ISERPA de Angers

É evidente que seria mais prático designar uma máquina por um nome, em vez de por seu endereço IP. Surge, então, o problema da exclusividade do nome: existem milhões de máquinas interconectadas. Seria possível imaginar que um órgão centralizado atribuísse os nomes. Isso seria, sem dúvida, bastante complicado. O controle dos nomes foi, na verdade, distribuído por domínios. Cada domínio é gerenciado por um órgão geralmente muito ágil, que tem total liberdade quanto à escolha dos nomes das máquinas. Assim, as máquinas na França pertencem ao domínio fr, gerenciado pelo Inria de Paris. Para simplificar ainda mais as coisas, o controle é distribuído novamente: domínios são criados dentro do domínio fr. Assim, a Universidade de Angers pertence ao domínio univ-Angers. O serviço responsável por esse domínio tem total liberdade para nomear os computadores da rede da Universidade de Angers. Por enquanto, esse domínio não foi subdividido. Mas, em uma grande universidade com muitos computadores em rede, isso poderia ocorrer.

A máquina DPX2/320 da Universidade de Angers foi nomeada Lagaffe, enquanto uma máquina PC e uma 486DX50 foram nomeadas liny. Como referenciar essas máquinas externamente? Especificando a hierarquia dos domínios aos quais elas pertencem. Assim, o nome completo da máquina Lagaffe será:

        Lagaffe.univ-Angers.fr

Dentro dos domínios, é possível usar nomes relativos. Assim, dentro do domínio fr e fora do domínio univ-Angers, a máquina Lagaffe poderá ser referenciada por

        Lagaffe.univ-Angers

Por fim, dentro do domínio univ-Angers, ela poderá ser referenciada simplesmente por

        Lagaffe

Uma aplicação pode, portanto, referenciar uma máquina pelo seu nome. No fim das contas, é preciso, ainda assim, obter o endereço de Internet dessa máquina. Como isso é feito? Suponhamos que, a partir de uma máquina A, queiramos nos comunicar com uma máquina B.

  • Se a máquina B pertencer ao mesmo domínio que a máquina A, provavelmente encontraremos seu endereço IP em um arquivo da máquina A.
  • Caso contrário, a máquina A encontrará, em outro arquivo ou no mesmo arquivo anterior, uma lista de alguns servidores de nomes com seus endereços IP. Um servidor de nomes é responsável por estabelecer a correspondência entre o nome de uma máquina e seu endereço IP. A máquina A enviará uma solicitação especial ao primeiro servidor de nomes de sua lista, chamada solicitação DNS, incluindo, portanto, o nome da máquina procurada. Se o servidor consultado tiver esse nome em seus registros, ele enviará à máquina A o endereço IP correspondente. Caso contrário, o servidor também encontrará em seus arquivos uma lista de servidores de nomes que pode consultar. Ele fará isso então. Assim, vários servidores de nomes serão consultados, não de forma aleatória, mas de modo a minimizar o número de consultas. Se a máquina for finalmente encontrada, a resposta será enviada de volta à máquina A.

XDR: (Representação de Dados eXternal)

Criado pela Sun MicroSystems, este protocolo especifica uma representação padrão dos dados, independente das máquinas.

RPC: (Chamada de Procedimento Remoto)

Também definido pela Sun, trata-se de um protocolo de comunicação entre aplicativos remotos, independente da camada de transporte. Esse protocolo é importante: ele dispensa o programador de conhecer os detalhes da camada de transporte e torna os aplicativos portáteis. Esse protocolo baseia-se no protocolo XDR

NFS: Sistema de Arquivos em Rede

Também definido pela Sun, esse protocolo permite que uma máquina “veja” o sistema de arquivos de outra máquina. Ele se baseia no protocolo RPC mencionado anteriormente.

8.1.9. Conclusão

Apresentamos nesta introdução algumas linhas gerais dos protocolos da Internet. Para aprofundar esse assunto, recomenda-se a leitura do excelente livro de Douglas Comer:

Título: TCP/IP: Arquitetura, Protocolos, Aplicações.

Autor: Douglas COMER

Editora: InterEditions

8.2. Gerenciamento de endereços de rede em Java

8.2.1. Definição

Cada máquina na Internet é identificada por um endereço ou um nome único. Essas duas entidades são gerenciadas em Java pela classe InetAddress, cujos métodos são apresentados a seguir:

byte [] getAddress()
retorna os 4 bytes do endereço IP da instância InetAddress atual
String getHostAddress()
retorna o endereço IP da instância atual InetAddress
String getHostName()
fornece o nome na Internet da instância atual InetAddress
String toString()
fornece a identidade (endereço IP) e o nome na Internet da instância atual InetAddress
InetAddress getByName(String Host)
cria a instância InetAddress da máquina designada por Host. Gera uma exceção se Host for desconhecido. Host pode ser o nome na Internet de uma máquina ou seu endereço IP na forma I1.I2.I3.I4
InetAddress getLocalHost()
cria a instância InetAddress da máquina na qual está sendo executado o programa que contém esta instrução.

8.2.2. Alguns exemplos

8.2.2.1. Identificar a máquina local


import java.net.*;

public class localhost{
  public static void main (String arg[]){
    try{
      InetAddress adresse=InetAddress.getLocalHost();
    byte[] IP=adresse.getAddress();
    System.out.print("IP=");
    int i;
    for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
    System.out.println(IP[i]);
      System.out.println("adresse="+adresse.getHostAddress());
    System.out.println("nom="+adresse.getHostName());
    System.out.println("identité="+adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getLocalHost : "+e);
    }// fim do try
  }// fim de main
}// fim da classe

Os resultados da execução são os seguintes:

IP=127.0.0.1
adresse=127.0.0.1
nom=tahe
identité=tahe/127.0.0.1

Cada máquina possui um endereço interno IP, que é 127.0.0.1. Quando um programa utiliza esse endereço de rede, ele está se referindo à máquina na qual está sendo executado. A vantagem desse endereço é que ele não requer uma placa de rede. Assim, é possível testar programas de rede sem estar conectado a uma rede. Outra maneira de designar a máquina local é usar o nome localhost.

8.2.2.2. Identificar qualquer máquina


import java.net.*;

public class getbyname{
  public static void main (String arg[]){
    String nomMachine;
    // recupera-se o argumento
    if(arg.length==0) 
      nomMachine="localhost";
    else nomMachine=arg[0];
    // tentamos obter o endereço da máquina
    try{
      InetAddress adresse=InetAddress.getByName(nomMachine);
      System.out.println("IP : "+  adresse.getHostAddress());
      System.out.println("nom : "+ adresse.getHostName());
      System.out.println("identité : "+ adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getByName : "+e);
    }// fim do try
  }// fim da função main
}// fim da classe

Com a chamada **java getbyname**, obtêm-se os seguintes resultados:

IP : 127.0.0.1
nom : localhost
identité : localhost/127.0.0.1

Com a chamada java getbyname shiva.istia.univ-angers.fr, obtém-se:

IP : 193.52.43.5
nom : shiva.istia.univ-angers.fr
identité : shiva.istia.univ-angers.fr/193.52.43.5

Com a chamada Java getbyname www.ibm.com, obtém-se:

IP : 204.146.18.33
nom : www.ibm.com
identité : www.ibm.com/204.146.18.33

8.3. Comunicações TCP-IP

8.3.1. Informações gerais

Image

Quando um aplicativo AppA de uma máquina A deseja se comunicar com um aplicativo AppB de uma máquina B na Internet, ele precisa saber algumas informações:

  • o endereço IP ou o nome da máquina B
  • o número da porta com a qual a aplicação AppB opera. De fato, a máquina B pode hospedar várias aplicações que operam na Internet. Quando recebe informações provenientes da rede, ela precisa saber a qual aplicação essas informações se destinam. Os aplicativos da máquina B têm acesso à rede por meio de interfaces, também chamadas de portas de comunicação. Essa informação está contida no pacote recebido pela máquina B para que ele seja entregue ao aplicativo correto.
  • Os protocolos de comunicação compreendidos pela máquina B. Em nosso estudo, utilizaremos apenas os protocolos TCP-IP.
  • o protocolo de comunicação aceito pelo aplicativo AppB. De fato, as máquinas A e B vão “se comunicar”. O que elas vão transmitir será encapsulado nos protocolos TCP-IP. No entanto, quando, no final da cadeia, o aplicativo AppB receber a informação enviada pelo aplicativo AppA, ele precisará ser capaz de interpretá-la. Isso é semelhante à situação em que duas pessoas, A e B, se comunicam por telefone: o diálogo delas é transmitido pelo telefone. A fala será codificada na forma de sinais pelo telefone A, transmitida pelas linhas telefônicas, chegará ao telefone B para ser decodificada. A pessoa B então ouve as palavras. É aí que entra o conceito de protocolo de diálogo: se A fala francês e B não entende essa língua, A e B não poderão dialogar de forma eficaz.

Portanto, os dois aplicativos que se comunicam devem chegar a um acordo sobre o tipo de diálogo que irão adotar. Assim, por exemplo, o diálogo com um serviço ftp não é o mesmo que com um serviço pop: esses dois serviços não aceitam os mesmos comandos. Elas possuem um protocolo de comunicação diferente.

8.3.2. As características do protocolo TCP

Aqui, estudaremos apenas as comunicações de rede que utilizam o protocolo de transporte TCP. Vale lembrar as características desse protocolo:

  • O processo que deseja transmitir estabelece, em primeiro lugar, uma conexão com o processo destinatário das informações que irá transmitir. Essa conexão é estabelecida entre uma porta da máquina transmissora e uma porta da máquina receptora. Entre as duas portas, é criado um caminho virtual que ficará reservado exclusivamente aos dois processos que estabeleceram a conexão.
  • Todos os pacotes enviados pelo processo de origem seguem esse caminho virtual e chegam na ordem em que foram enviados
  • A informação transmitida tem um caráter contínuo. O processo emissor envia informações em seu próprio ritmo. Essas informações não são necessariamente enviadas imediatamente: o protocolo TCP aguarda até ter quantidade suficiente para enviá-las. Elas são armazenadas em uma estrutura chamada segmento TCP. Esse segmento, uma vez preenchido, será transmitido para a camada IP, onde será encapsulado em um pacote IP.
  • Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP, que atua como destinatário, verifica se está recebendo os segmentos na sequência correta. Para cada segmento recebido corretamente, ele envia um aviso de recebimento ao remetente.
  • Quando o remetente recebe essa confirmação, ele informa o processo emissor. Assim, este pode saber que um segmento chegou ao destino.
  • Se, após um certo tempo, o protocolo TCP, que emitiu um segmento, não receber uma confirmação de recebimento, ele retransmite o segmento em questão, garantindo assim a qualidade do serviço de encaminhamento da informação.
  • O circuito virtual estabelecido entre os dois processos que se comunicam é full-duplex: isso significa que a informação pode transitar nos dois sentidos. Assim, o processo de destino pode enviar confirmações de recebimento mesmo enquanto o processo de origem continua enviando informações. Isso permite, por exemplo, que o protocolo de origem TCP envie vários segmentos sem esperar pelo aviso de recebimento. Se, após um certo tempo, ele perceber que não recebeu o aviso de recebimento de um determinado segmento nº n, ele retomará a transmissão dos segmentos a partir desse ponto.

8.3.3. A relação cliente-servidor

Frequentemente, a comunicação na Internet é assimétrica: a máquina A inicia uma conexão para solicitar um serviço à máquina B, especificando que deseja estabelecer uma conexão com o serviço SB1 da máquina B. Esta aceita ou recusa. Se aceitar, a máquina A pode enviar suas solicitações ao serviço SB1. Essas solicitações devem estar em conformidade com o protocolo de comunicação compreendido pelo serviço SB1. Estabelece-se, assim, um diálogo de solicitação-resposta entre a máquina A, chamada de máquina cliente, e a máquina B, chamada de máquina servidor. Um dos dois parceiros encerrará a conexão.

8.3.4. Arquitetura de um cliente

A arquitetura de um programa de rede que solicita os serviços de um aplicativo servidor será a seguinte:

ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
        tant que ce n'est pas fini
            préparer une demande
            l'émettre vers la machine B
            attendre et récupérer la réponse
            la traiter
        fin tant que
finsi

8.3.5. Arquitetura de um servidor

A arquitetura de um programa que oferece serviços será a seguinte:

ouvrir le service sur la machine locale
tant que le service est ouvert
        se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
        lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

O programa servidor trata de maneira diferente a solicitação inicial de conexão de um cliente e suas solicitações posteriores para obter um serviço. O programa não presta o serviço propriamente dito. Se o fizesse, durante o tempo em que o serviço estivesse sendo prestado, ele não estaria mais à escuta das solicitações de conexão e, consequentemente, os clientes não seriam atendidos. Portanto, ele procede de outra forma: assim que uma solicitação de conexão é recebida na porta de escuta e, em seguida, aceita, o servidor cria uma tarefa encarregada de prestar o serviço solicitado pelo cliente. Esse serviço é prestado em outra porta da máquina servidora, chamada de porta de serviço. Dessa forma, é possível atender a vários clientes ao mesmo tempo.

Uma tarefa de serviço terá a seguinte estrutura:

tant que le service n'a pas été rendu totalement
        attendre une demande sur le port de service
        lorsqu'il y en a une, élaborer la réponse
        transmettre la réponse via le port de service
fin tant que
libérer le port de service

8.3.6. A classe Socket

8.3.6.1. Définition

A ferramenta básica utilizada pelos programas que se comunicam pela Internet é o socket. Essa palavra em inglês significa “tomada elétrica”. Aqui, ela é estendida para significar “tomada de rede”. Para que um aplicativo possa enviar e receber informações pela Internet, ele precisa de uma tomada de rede, uma socket. Essa ferramenta foi criada inicialmente nas versões do Unix da Universidade de Berkeley. Desde então, foi adaptada para todos os sistemas Unix, bem como para o ambiente Windows. Ela também existe em máquinas virtuais Java em duas formas: a classe Socket para aplicativos clientes e a classe ServerSocket para aplicativos servidor. Apresentamos aqui alguns dos construtores e métodos da classe Socket:

public Socket(String host, int port)
abre uma conexão remota com a porta port da máquina host
public int getLocalPort()
retorna o número da porta local utilizada pelo soquete
  
public int getPort()
retorna o número da porta remota à qual o soquete está conectado
  
public InetAddress getLocalAdress()
retorna o endereço local InetAddress ao qual o soquete está vinculado
  
public InetAddress getInetAdress()
retorna o endereço remoto InetAddress ao qual o soquete está vinculado
  
public InputStream getInputStream()
retorna um fluxo de entrada que permite ler os dados enviados pelo parceiro remoto
  
public OutputStream getOutputStream()
retorna um fluxo de saída que permite enviar dados ao parceiro remoto
  
public void shutdownInput()
fecha o fluxo de entrada do soquete
  
public void shutdownOutput()
fecha o fluxo de saída do soquete
  
public void close()
fecha o soquete e seus fluxos de E/S
  
public String toString()
retorna uma sequência de caracteres que “representa” o soquete
 

8.3.6.2. Abertura de uma conexão com um servidor

Vimos que, para que uma máquina A abra uma conexão com um serviço de uma máquina B, ela precisa de duas informações:

  • o endereço IP ou o nome da máquina B
  • o número da porta em que o serviço desejado está em funcionamento

O construtor

    public Socket(String  host, int  port);

cria um socket e o conecta à máquina host na porta port. Esse construtor gera uma exceção em diferentes casos:

  • endereço incorreto
  • porta incorreta
  • solicitação recusada

Precisamos lidar com essa exceção:


    Socket  sClient=null;
    try{
        sClient=new Socket(host,port);
    } catch(Exception e){
        // a conexão falhou — processa-se o erro
        ….
    }

Se a solicitação de conexão for bem-sucedida, é atribuída localmente ao cliente uma porta para se comunicar com a máquina B. Uma vez estabelecida a conexão, é possível obter essa porta por meio do método:

public int getLocalPort();

Se a conexão for bem-sucedida, vimos que, por sua vez, o servidor faz com que o serviço seja prestado por outra tarefa que opera em uma porta denominada de serviço. Esse número de porta pode ser obtido com o método:

public int getPort();

8.3.6.3. Enviar informações pela rede

É possível obter um fluxo de gravação no soquete e, portanto, na rede, com o método:

public OutputStream getOutputStream();

Tudo o que for enviado nesse fluxo será recebido na porta de serviço do servidor. Muitas aplicações apresentam um diálogo na forma de linhas de texto terminadas por uma quebra de linha. Por isso, o método println é bastante prático nesses casos. Transforma-se, então, o fluxo de saída OutputStream no fluxo PrintWriter, que possui o método println. A gravação pode gerar uma exceção.

8.3.6.4. Ler informações provenientes da rede

É possível obter um fluxo de leitura das informações que chegam ao soquete com o método:

public InputStream getInputStream();

Tudo o que for lido nesse fluxo provém da porta de serviço do servidor. Para aplicativos com diálogo na forma de linhas de texto terminadas por uma quebra de linha, é recomendável utilizar o método readLine. Para isso, transforma-se o fluxo de entrada InputStream no fluxo BufferedReader, que possui o método readLine(). A leitura pode gerar uma exceção.

8.3.6.5. Fechamento da conexão

Isso é feito com o método:

public void close();

O método pode gerar uma exceção. Os recursos utilizados, principalmente a porta de rede, são liberados.

8.3.6.6. A arquitetura do cliente

Agora temos os elementos necessários para descrever a arquitetura básica de um cliente da Internet:


    Socket  sClient=null;
    try{
            // conectando-se ao serviço em execução na porta P da máquina M
        sClient=new Socket(M,P);

        // criando os fluxos de entrada e saída do soquete do cliente
        BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
        PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);

        // ciclo de solicitação-resposta
        boolean  fini=false;
        String demande;
        String réponse;
        while (! fini){
            // prepara-se a solicitação
            demande=…
            // envia-se a solicitação
            out.println(demande);
            // lê-se a resposta
            réponse=in.readLine();
            // processando a resposta

        }
        // concluído
        sClient.close();
    } catch(Exception e){
        // gerenciamento da exceção
        ….
    }

Não procuramos lidar com os diferentes tipos de exceção gerados pelo construtor Socket ou pelos métodos readline, getInputStream, getOutputStream e close, para não complicar o exemplo. Tudo foi agrupado em uma única exceção.

8.3.7. A classe ServerSocket

8.3.7.1. Définition

Essa classe destina-se ao gerenciamento de sockets no lado do servidor. Apresentamos aqui alguns dos construtores e métodos dessa classe:

public ServerSocket(int port)
cria um soquete de escuta na porta port
public ServerSocket(int port, int count)
idem, mas define em count o tamanho da fila de espera e, em c.a.d, o número máximo de conexões de clientes colocadas em espera caso o servidor esteja ocupado no momento em que a conexão do cliente chegar.
public int getLocalPort()
retorna o número da porta de escuta utilizada pelo soquete
public InetAddress getInetAdress()
retorna o endereço local InetAddress ao qual o soquete está vinculado
public Socket accept()
coloca o servidor em espera por uma conexão (operação bloqueante). Ao receber uma conexão do cliente, retorna um soquete a partir do qual o serviço será prestado ao cliente.
public void close()
fecha o soquete e seus fluxos de E/S
public String toString()
retorna uma sequência de caracteres que “representa” o soquete
public void close()
fecha o soquete de serviço e libera os recursos a ele associados

8.3.7.2. Abertura do serviço

Isso é feito com os dois construtores:

public ServerSocket(int  port);    
public ServerSocket(int  port, int  count);

port é a porta de escuta do serviço: aquela para a qual os clientes enviam suas solicitações de conexão. count é o tamanho máximo da fila de espera do serviço (50 por padrão), que armazena as solicitações de conexão dos clientes às quais o servidor ainda não respondeu. Quando a fila de espera está cheia, as solicitações de conexão que chegam são rejeitadas. Ambos os construtores geram uma exceção.

8.3.7.3. Aceitação de uma solicitação de conexão

Quando um cliente faz uma solicitação de conexão na porta de escuta do serviço, este a aceita com o método:

    public Socket accept();

Esse método retorna uma instância de Socket: trata-se do soquete de serviço, por meio do qual o serviço será prestado, na maioria das vezes por outra tarefa. O método pode gerar uma exceção.

8.3.7.4. Leitura/gravação por meio do soquete de serviço

Como o soquete de serviço é uma instância da classe Socket, consulte as seções anteriores onde esse assunto foi abordado.

8.3.7.5. Identificar o cliente

Depois de obter o socket de serviço, o cliente pode ser identificado com o método

    public InetAddress getInetAddress()

da classe Socket. Assim, teremos acesso ao endereço IP e ao nome do cliente.

8.3.7.6. Fechar o serviço

Isso é feito com o método

    public void close();

da classe ServerSocket. Isso libera os recursos ocupados, principalmente a porta de escuta. O método pode gerar uma exceção.

8.3.7.7. Arquitetura básica de um servidor

Com base no que foi dito, é possível descrever a estrutura básica de um servidor:


SocketServer sEcoute=null;
try{
    // abertura do serviço
    int portEcoute=…
    int maxConnexions=…
    sEcoute=new ServerSocket(portEcoute,maxConnexions);

    // processamento das solicitações de conexão
    boolean fini=false;
    Socket sService=null;
    while( ! fini){
        // aguardando e aceitando uma solicitação
        sService=sEcoute.accept();

        // o serviço é prestado por outra tarefa, para a qual se repassa o soquete de serviço
        new Service(sService).start();

        // retorna à espera por solicitações de conexão
    }
    // concluído — o serviço é encerrado
    sEcoute.close();
} catch (Exception e){
    // a exceção é tratada

}

A classe Service é uma thread que poderia ter a seguinte aparência:


public class Service extends Thread{

    Socket sService;        // o soquete de serviço

    // construtor
    public Service(Socket S){
        sService=S;
    }

// execução
public void run(){
    try{
        // criando os fluxos de entrada e saída
    BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
    PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);

    // ciclo de solicitação-resposta
    boolean  fini=false;
    String demande;
    String réponse;
    while (! fini){
        // lê-se a solicitação
        demande=in.readLine();

        // processa-se a solicitação 


        // prepara-se a resposta
        réponse=…

        // envia-se a resposta
        out.println(réponse);
    }
    // concluído
    sService.close();
    } catch(Exception e){
    // gerencia-se a exceção
    ….
    }// try
} // executar

8.4. Aplicações

8.4.1. Servidor de eco

Propomos escrever um servidor de eco que será iniciado a partir de uma janela DOS pelo comando:

    java serveurEcho port

O servidor opera na porta passada como parâmetro. Ele se limita a reenviar ao cliente a solicitação que este lhe enviou, acompanhada de sua identidade (IP+nome). Ele aceita duas conexões em sua fila de espera. Temos aqui todos os componentes de um servidor TCP. O programa é o seguinte:

// chamada: serveurEcho porta
// servidor de eco
// retorna ao cliente a linha que este enviou


import java.net.*;
import java.io.*;

public class serveurEcho{
    public final static String syntaxe="Syntaxe : serveurEcho port";
    public final static int nbConnexions=2;

    // programa principal
    public static void main (String arg[]){

     // existe algum argumento
     if(arg.length != 1)
        erreur(syntaxe,1);

     // esse argumento deve ser um número inteiro maior que 0
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[0]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // cria-se o socket de escuta
     ServerSocket ecoute=null;
     try{
        ecoute=new ServerSocket(port,nbConnexions);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
     }

     // acompanhamento
     System.out.println("Serveur d'écho lancé sur le port " + port);

     // loop de serviço
     boolean serviceFini=false;
     Socket service=null;
     while (! serviceFini){
         // aguardando um cliente
        try{
            service=ecoute.accept();
        } catch (IOException e){
                erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
        }

         // identifica-se a conexão
        try{
            System.out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }


         // o serviço é prestado por outra tarefa
        new traiteClientEcho(service).start();
     }// fim do while
    }// fim de main

// exibição de erros
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identifica
    private static String identifie(InetAddress Host){
        // identificação do host
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fim da classe


// presta serviço a um cliente do servidor de eco

class traiteClientEcho extends Thread{

    private Socket service;            // soquete de serviço
    private BufferedReader in;        // fluxo de entrada
    private PrintWriter out;            // fluxo de saída

     // construtor
    public traiteClientEcho(Socket service){
        this.service=service;
    }

     // método run
    public void run(){

         // criação dos fluxos de entrada e saída
        try{
            in=new BufferedReader(new InputStreamReader(service.getInputStream()));
        } catch (IOException e){
                erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
        }// fim do try
        try{
            out=new PrintWriter(service.getOutputStream(),true);
        } catch (IOException e){
                erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
        }// fim do try

         // a identificação da conexão é enviada ao cliente
        try{
            out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }

         // ciclo de leitura (solicitação)/gravação (resposta)
        String demande,reponse;
        try{
             // o serviço é interrompido quando o cliente envia um marcador de fim de arquivo
            while ((demande=in.readLine())!=null){
                // eco da solicitação
                reponse="["+demande+"]";
                out.println(reponse);
                 // o serviço é encerrado quando o cliente envia “fim”
                if(demande.trim().toLowerCase().equals("fin")) break;
            }// fim do while
        } catch (IOException e){
                erreur("Erreur lors des échanges client/serveur ("+e+")",3);
        }// fim do try

         // fechando o soquete
        try{
            service.close();
        } catch (IOException e){
            erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
        }// fim do try
    }// fim de execução

     // exibição de erros
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }// fim do erro

     // identifica
    private String identifie(InetAddress Host){
         // identificação do host
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fim da classe

As duas classes necessárias para o serviço foram reunidas em um único arquivo-fonte. Apenas uma delas, aquela que possui a função main, possui o atributo public. A estrutura do servidor está em conformidade com a arquitetura geral dos servidores TCP. Foi adicionado um método (identifie) que permite identificar a conexão entre o servidor e um cliente. Aqui estão alguns resultados:

O servidor é iniciado pelo comando

    java serveurEcho 187

Em seguida, ele exibe na janela de controle a seguinte mensagem:

Serveur d'écho lancé sur le port 187

Para testar esse servidor, utiliza-se o programa telnet, disponível tanto no Unix quanto no Windows. O Telnet é um cliente TCP universal, compatível com todos os servidores que aceitam linhas de texto terminadas por um caractere de fim de linha em suas comunicações. Esse é o caso do nosso servidor de eco. Iniciamos um primeiro cliente telnet no Windows (2000, neste exemplo) digitando telnet em uma janela DOS:


DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1

Le caractère d'échappement est 'CTRL+$'

Microsoft Telnet> help

Les commandes peuvent être abrégées. Les commandes prises en charge sont :

close           ferme la connexion en cours
display         affiche les paramètres d'opération
open            ouvre une connexion à un site
quit            quitte telnet
set             définit les options (entrez 'set ?' pour afficher la liste)
status          affiche les informations d'état
unset           annule les options (entrez 'unset ?' pour afficher la liste)
? ou help       affiche des informations d'aide

Microsoft Telnet> set ?
NTLM            Active l'authentification NTLM.
LOCAL_ECHO      Active l'écho local.
TERM x          (où x est ANSI, VT100, VT52 ou VTNT))
CRLF            Envoi de CR et de LF

Microsoft Telnet> set local_echo

Microsoft Telnet> open localhost 187

Por padrão, o programa telnet não exibe o eco dos comandos digitados no teclado. Para ativar esse eco, execute o comando:

Microsoft Telnet> set local_echo

Para estabelecer uma conexão com o servidor, especificando a porta do serviço de eco (187) e o endereço da máquina na qual ele está instalado (localhost), execute o comando:

Microsoft Telnet> open localhost 187

Na janela do DOS do cliente, é exibida a seguinte mensagem:

Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

Na janela do servidor, aparece a mensagem:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

Aqui, tahe e localhost referem-se à mesma máquina. Na janela do cliente telnet, é possível digitar linhas de texto. O servidor as repete:

Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]

Observe-se que a porta do cliente (1059) foi detectada corretamente, mas que a porta do serviço (187) é idêntica à porta de escuta (187), o que é inesperado. De fato, seria de se esperar obter a porta do soquete de serviço e não a porta de escuta. Seria necessário verificar se obtemos os mesmos resultados no Unix. Agora, vamos iniciar um segundo cliente telnet. A janela do servidor fica assim:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]

Na janela do segundo cliente, também é possível digitar linhas de texto:

Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]

Assim, vemos que o servidor de eco pode atender a vários clientes ao mesmo tempo. Os clientes telnet podem ser encerrados fechando a janela do DOS na qual estão sendo executados.

8.4.2. Um cliente Java para o servidor de eco

Na seção anterior, utilizamos um cliente telnet para testar o serviço de eco. Agora, vamos escrever nosso próprio cliente:

// chamada: clientEcho porta da máquina
// cliente do servidor de eco
// envia linhas ao servidor, que as devolve por eco

import java.net.*;
import java.io.*;

public class clientEcho{
    public final static String syntaxe="Syntaxe : clientEcho machine port";

    // programa principal    
    public static void main (String arg[]){

     // existem dois argumentos
     if(arg.length != 2)
        erreur(syntaxe,1);

     // o primeiro argumento deve ser o nome de uma máquina existente
    String machine=arg[0];
    InetAddress serveurAddress=null;
    try{
        serveurAddress=InetAddress.getByName(machine);
    } catch (Exception e){
        erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
    }

     // a porta deve ser um número inteiro maior que 0
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[1]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\nPort incorrect ("+E+")",3);

     // estabelece-se conexão com o servidor
     Socket sClient=null;
     try{
        sClient=new Socket(machine,port);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
     }

     // identifica-se a conexão
    try{
        System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
        sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
        + "," + sClient.getPort() + "]");
    } catch (Exception e) {
        erreur("identification liaison ("+e+")",5);
    }

     // criação do fluxo de leitura das linhas digitadas no teclado
    BufferedReader IN=null;
    try{
        IN=new BufferedReader(new InputStreamReader(System.in));
    } catch (Exception e){
        erreur("Création du flux d'entrée clavier ("+e+")",6);
    }
     // criação do fluxo de entrada associado ao soquete do cliente
    BufferedReader in=null;
    try{
        in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
    } catch (Exception e){
        erreur("Création du flux d'entrée de la socket client("+e+")",7);
    }
     // criação do fluxo de saída associado ao soquete do cliente
    PrintWriter out=null;
    try{
        out=new PrintWriter(sClient.getOutputStream(),true);
    } catch (Exception e){
        erreur("Création du flux de sortie de la socket ("+e+")",8);
    }

     // ciclo de solicitações e respostas
    boolean serviceFini=false;
    String demande=null;
    String reponse=null;

    // leitura da mensagem enviada pelo servidor logo após a conexão 
    try{
        reponse=in.readLine();
    } catch (IOException e){
            erreur("Lecture réponse ("+e+")",4);
    }        

     // exibição da resposta
    System.out.println("Serveur : " +reponse);

    while (! serviceFini){
         // leitura de uma linha digitada no teclado
        System.out.print("Client : ");
        try{
            demande=IN.readLine();
        } catch (Exception e){
            erreur("Lecture ligne ("+e+")",9);
        }
         // envio de solicitação pela rede
        try{
            out.println(demande);
        } catch (Exception e){
            erreur("Envoi demande ("+e+")",10);
        }
         // aguardando/lendo a resposta
        try{
            reponse=in.readLine();
        } catch (IOException e){
                erreur("Lecture réponse ("+e+")",4);
        }
         // exibição da resposta
        System.out.println("Serveur : " +reponse);
         //  terminou?
        if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
    }
     //  terminou
    try{
        sClient.close();
    } catch(Exception e){
        erreur("Fermeture socket ("+e+")",11);
    }
}// manual

// exibição de erros
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identificar
    private static String identifie(InetAddress Host){
        // identificação do host
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fim da classe

A estrutura deste cliente está em conformidade com a arquitetura geral dos clientes tcp. Aqui, as diversas exceções possíveis foram tratadas uma a uma, o que torna o programa mais pesado. Seguem os resultados obtidos ao testar este cliente:

Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]

As linhas que começam com Client são as enviadas pelo cliente, e as que começam com Serveur são as que o servidor devolveu como resposta.

8.4.3. Um cliente TCP genérico

Muitos serviços criados nos primórdios da Internet funcionam de acordo com o modelo do servidor de eco estudado anteriormente: as trocas entre cliente e servidor ocorrem por meio de trocas de linhas de texto. Vamos escrever um cliente TCP genérico que será iniciado da seguinte maneira: java cltTCPgenerique servidor porta

Esse cliente TCP se conectará à porta port do servidor serveur. Feito isso, ele criará duas threads:

  1. uma thread responsável por ler os comandos digitados no teclado e enviá-los ao servidor
  2. um thread encarregado de ler as respostas do servidor e exibi-las na tela

Por que duas threads, se na aplicação anterior essa necessidade não se fez sentir? Nessa última, o protocolo de comunicação era conhecido: o cliente enviava uma única linha e o servidor respondia com uma única linha. Cada serviço tem seu protocolo específico, e também podemos encontrar as seguintes situações:

  • o cliente precisa enviar várias linhas de texto antes de receber uma resposta
  • a resposta de um servidor pode conter várias linhas de texto

Portanto, o ciclo de envio de uma única linha ao servidor — recebimento de uma única linha enviada pelo servidor — nem sempre é adequado. Vamos, portanto, criar dois ciclos separados:

  • um ciclo para ler os comandos digitados no teclado a serem enviados ao servidor. O usuário indicará o fim dos comandos com a palavra-chave fin.
  • um ciclo de recepção e exibição das respostas do servidor. Esse será um ciclo infinito que só será interrompido pelo fechamento do fluxo de rede pelo servidor ou pelo usuário no teclado, ao digitar o comando fin.

Para manter esses dois loops separados, precisamos de duas threads independentes. Vejamos um exemplo de execução em que nosso cliente TCP genérico se conecta a um serviço SMTP (Protocolo de Transferência SendMail). Esse serviço é responsável pelo encaminhamento de e-mails aos seus destinatários. Ele opera na porta 25 e utiliza um protocolo de comunicação do tipo troca de linhas de texto.


Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test

ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Vamos comentar essas trocas entre cliente e servidor:

  • o serviço SMTP envia uma mensagem de boas-vindas quando um cliente se conecta a ele:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • alguns serviços possuem um comando help que fornece informações sobre os comandos que podem ser utilizados com o serviço. Neste caso, isso não ocorre. Os comandos SMTP utilizados no exemplo são os seguintes:
    • mail from: expéditeur, para indicar o endereço de e-mail do remetente da mensagem
    • rcpt to: destinataire, para indicar o endereço de e-mail do destinatário da mensagem. Se houver vários destinatários, o comando rcpt to: deve ser repetido quantas vezes forem necessárias para cada um deles.
    • data, que sinaliza ao servidor SMTP que a mensagem será enviada. Conforme indicado na resposta do servidor, trata-se de uma sequência de linhas terminada por uma linha contendo apenas o caractere ponto. Uma mensagem pode ter cabeçalhos separados do corpo da mensagem por uma linha em branco. Em nosso exemplo, definimos um assunto com a palavra-chave Subject:
  • assim que a mensagem for enviada, é possível indicar ao servidor que terminamos com o comando quit. O servidor então encerra a conexão de rede. O thread de leitura pode detectar esse evento e ser encerrado.
  • O usuário então digita “fin” no teclado para interromper também o thread de leitura dos comandos digitados no teclado.

Se verificarmos o e-mail recebido, temos o seguinte (Outlook):

Image

Observe-se que o serviço SMTP não consegue detectar se um remetente é válido ou não. Portanto, nunca se pode confiar no campo from de uma mensagem. Neste caso, o remetente machin@univ-angers.fr não existia.

Esse cliente TCP genérico pode nos permitir descobrir o protocolo de comunicação dos serviços da Internet e, a partir daí, criar classes especializadas para clientes desses serviços. Vamos descobrir o protocolo de comunicação do serviço POP (Post Office Protocol), que permite recuperar e-mails armazenados em um servidor. Ele opera na porta 110.


Dos> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<--     by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<--     Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<--     by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<--     Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2                  Tl.  : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 ,  Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Os principais comandos são os seguintes:

  • user login, onde se insere o nome de usuário no servidor que hospeda nossos e-mails
  • pass password, onde se fornece a senha associada ao login anterior
  • list, para obter a lista de mensagens no formato número, tamanho em bytes
  • retr i, para ler a mensagem nº i
  • quit, para encerrar a sessão.

Vamos agora conhecer o protocolo de comunicação entre um cliente e um servidor Web, que normalmente opera na porta 80:


Dos> java clientTCPgenerique istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise  jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Um cliente web envia seus comandos ao servidor de acordo com o seguinte esquema:

commande1
commande2
...
commanden
[ligne vide]

Somente após receber a linha vazia é que o servidor web responde. No exemplo, utilizamos apenas um comando:

GET /index.html HTTP/1.0

que solicita ao servidor o URL /index.html e indica que está trabalhando com o protocolo HTTP versão 1.0. A versão mais recente desse protocolo é a 1.1. O exemplo mostra que o servidor respondeu enviando o conteúdo do arquivo index.html e, em seguida, encerrou a conexão, já que é possível observar que o thread de leitura das respostas foi encerrado. Antes de enviar o conteúdo do arquivo index.html, o servidor web enviou uma série de cabeçalhos encerrada por uma linha em branco:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

A linha <html> é a primeira linha do arquivo /index.html. O que vem acima é chamado de cabeçalhos HTTP (Protocolo de Transferência HyperText). Não vamos detalhar esses cabeçalhos aqui, mas vale lembrar que nosso cliente genérico permite acessá-los, o que pode ser útil para compreendê-los. A primeira linha, por exemplo:

<-- HTTP/1.1 200 OK

indica que o servidor web contatado compreende o protocolo HTTP/1.1 e que encontrou o arquivo solicitado (200 OK), sendo 200 um código de resposta HTTP. As linhas

<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html

informam ao cliente que ele receberá 11.251 bytes representando o texto HTML (HyperText Markup Language) e que, ao final do envio, a conexão será encerrada.

Portanto, temos aqui um cliente TCP muito prático. Ele certamente faz menos do que o programa telnet que usamos anteriormente, mas foi interessante escrevê-lo nós mesmos. O programa do cliente TCP genérico é o seguinte:

// pacotes importados
import java.io.*;
import java.net.*;

public class clientTCPgenerique{

    // recebe como parâmetro as características de um serviço na forma
     // servidor e porta
     // conecta-se ao serviço
     // cria um thread para ler comandos digitados no teclado
     // esses comandos serão enviados ao servidor
     // cria um thread para ler as respostas do servidor
     // elas serão exibidas na tela
     // tudo termina com o comando “fin” digitado no teclado

   // variável de instância
  private static Socket client;

    public static void main(String[] args){

         // sintaxe
        final String syntaxe="pg serveur port";

         // número de argumentos
        if(args.length != 2)
            erreur(syntaxe,1);

         // anota-se o nome do servidor
        String serveur=args[0];

         // a porta deve ser um número inteiro maior que 0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[1]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

        client=null;
         // podem ocorrer problemas
        try{
             // conectando-se ao serviço
            client=new Socket(serveur,port);
        }catch(Exception ex){
             // erro
            erreur("Impossible de se connecter au service ("+ serveur
                +","+port+"), erreur : "+ex.getMessage(),3);
             // fim
            return;
        }//catch

         // criando os threads de leitura/gravação
    new ClientSend(client).start();
    new ClientReceive(client).start();

        // fim do thread principal
        return;
    }// main

     // exibição de erros
    public static void erreur(String msg, int exitCode){
         // exibição de erro
        System.err.println(msg);
         // encerramento com erro
        System.exit(exitCode);
    }//erro
}//classe  

class ClientSend extends Thread {
    // classe responsável por ler comandos digitados no teclado
     // e enviá-los a um servidor por meio de um cliente TCP passado como parâmetro

    private Socket client;    // o cliente TCP

     // construtor
    public ClientSend(Socket client){
         // observa-se o cliente TCP
        this.client=client;
    }//construtor

     // método Run da thread
    public void run(){

        // dados locais
        PrintWriter OUT=null;            // fluxo de gravação de rede
    BufferedReader IN=null;        // fluxo do teclado
        String commande=null;            // comando lido no teclado

         // gerenciamento de erros
        try{
             // criação do fluxo de gravação de rede
            OUT=new PrintWriter(client.getOutputStream(),true);
      // criação do fluxo de entrada do teclado
      IN=new BufferedReader(new InputStreamReader(System.in));
            // ciclo de entrada e envio de comandos
            System.out.println("Commandes : ");
            while(true){
                 // leitura do comando digitado no teclado
                commande=IN.readLine().trim();
                // Concluído?
                if (commande.toLowerCase().equals("fin")) break;
                // envio do comando ao servidor
                OUT.println(commande);
                 // próximo comando
            }//while
        }catch(Exception ex){
             // erro
            System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // fim — fechamento dos fluxos
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // sinaliza-se o fim do thread
        System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
    }//execução
}//classe

class ClientReceive extends Thread{
    // classe responsável por ler as linhas de texto destinadas a um 
     // cliente TCP passado como parâmetro

    private Socket client;    // o cliente TCP

     // construtor
    public ClientReceive(Socket client){
         // registra-se o cliente TCP
        this.client=client;
    }//construtor

     // método Run da thread
    public void run(){

        // dados locais
        BufferedReader IN=null;        // fluxo de leitura de rede
        String réponse=null;        // resposta do servidor

         // gerenciamento de erros
        try{
             // criação do fluxo de leitura de rede
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // ciclo de leitura das linhas de texto do fluxo IN
            while(true){
                 // leitura do fluxo de rede
                réponse=IN.readLine();
                 // fluxo encerrado?
                if(réponse==null) break;
                // exibição
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // erro
            System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // fim — fechando os fluxos
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // notifica-se o fim do thread
        System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
    }//execução
}//classe

8.4.4. Um servidor TCP genérico

Agora vamos nos concentrar em um servidor

  • que exibe na tela os comandos enviados por seus clientes
  • e lhes envia, como resposta, as linhas de texto digitadas no teclado por um usuário. Portanto, é o próprio usuário que atua como servidor.

O programa é executado por: java serveurTCPgenerique portEcoute, onde portEcoute é a porta à qual os clientes devem se conectar. O atendimento ao cliente será garantido por duas threads:

  • um thread dedicado exclusivamente à leitura das linhas de texto enviadas pelo cliente
  • um thread dedicado exclusivamente à leitura das respostas digitadas pelo usuário. Este sinalizará, por meio do comando fin, que está encerrando a conexão com o cliente.

O servidor cria duas threads por cliente. Se houver n clientes, haverá 2n threads ativas ao mesmo tempo. O servidor, por sua vez, nunca é interrompido, a menos que o usuário pressione Ctrl-C no teclado. Vejamos alguns exemplos.

O servidor é iniciado na porta 100 e usamos o cliente genérico para nos comunicarmos com ele. A janela do cliente é a seguinte:

E:\data\serge\MSNET\c#\rede\cliente tcp genérico> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

As linhas que começam com <-- são as enviadas do servidor para o cliente; as demais, do cliente para o servidor. A janela do servidor é a seguinte:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]

As linhas que começam com <-- são aquelas enviadas do cliente para o servidor. As linhas N: são as linhas enviadas do servidor para o cliente nº N. O servidor acima ainda está ativo, embora o cliente 1 tenha sido encerrado. Iniciamos um segundo cliente para o mesmo servidor:


Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

A janela do servidor fica assim:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C

Vamos agora simular um servidor web, iniciando nosso servidor genérico na porta 88:


Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88

Agora, vamos abrir um navegador e acessar a página http://localhost:88/exemple.html. O navegador se conectará à porta 88 da máquina localhost e, em seguida, solicitará a página /exemple.html:

Image

Vamos agora observar a janela do nosso servidor:

Dos>java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Assim, identificamos os cabeçalhos HTTP enviados pelo navegador. Isso nos permite descobrir, aos poucos, o protocolo HTTP. Em um exemplo anterior, criamos um cliente web que enviava apenas o comando GET. Isso já havia sido suficiente. Vemos aqui que o navegador envia outras informações ao servidor. Elas têm como objetivo indicar ao servidor que tipo de cliente ele tem diante de si. Vemos também que os cabeçalhos HTTP terminam com uma linha em branco.

Vamos elaborar uma resposta para o nosso cliente. O usuário ao teclado é, neste caso, o verdadeiro servidor e pode elaborar uma resposta manualmente. Lembremo-nos da resposta fornecida por um servidor web em um exemplo anterior:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

Vamos tentar dar uma resposta semelhante:

...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]

As linhas que começam com 2: são enviadas do servidor para o cliente nº 2. O comando fin encerra a conexão do servidor com o cliente. Limitamos nossa resposta aos seguintes cabeçalhos HTTP:

HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :

Não informamos o tamanho do arquivo que vamos enviar (Content-Length), mas nos limitamos a indicar que vamos encerrar a conexão (Connection: close) após o envio do mesmo. Isso é suficiente para o navegador. Ao perceber que a conexão foi encerrada, ele saberá que a resposta do servidor foi concluída e exibirá a página HTML que lhe foi enviada. Esta última é a seguinte:

2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>

Em seguida, o usuário encerra a conexão com o cliente digitando o comando fin. O navegador reconhece então que a resposta do servidor foi concluída e pode exibi-la:

Image

Se, no exemplo acima, digitarmos View/Source para ver o que o navegador recebeu, obtemos:

Image

ou seja, exatamente o que foi enviado a partir do servidor genérico.

O código do servidor TCP genérico é o seguinte:

// pacotes
import java.io.*;
import java.net.*;

public class serveurTCPgenerique{

    // programa principal
    public static void main (String[] args){

    // recebe as solicitações dos clientes na porta de escuta
     // cria um thread para ler as solicitações do cliente
     // estas serão exibidas na tela
     // cria um thread para ler os comandos digitados no teclado
     // estas serão enviadas como resposta ao cliente
     // tudo termina com o comando fin digitado no teclado

    final String syntaxe="Syntaxe : pg port";
   // variável de instância
         // existe algum argumento
     if(args.length != 1)
        erreur(syntaxe,1);

         // a porta deve ser um número inteiro >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // criamos o serviço de escuta
    ServerSocket ecoute=null;
    int nbClients=0;    // número de clientes atendidos
        try{
             // criamos o serviço
            ecoute=new ServerSocket(port);
             // acompanhamento
            System.out.println("Serveur générique lancé sur le port " + port);

             // ciclo de atendimento aos clientes
            Socket client=null;
            while (true){ // loop infinito  será interrompido com Ctrl-C
                 // aguardando um cliente
                client=ecoute.accept();

                 // o serviço é executado em threads separadas
                nbClients++;

                 // são criadas as threads de leitura/gravação
        new ServeurSend(client,nbClients).start();
        new ServeurReceive(client,nbClients).start();

                // retornamos à escuta de solicitações
            }// fim do while
        }catch(Exception ex){
             // reporta-se o erro
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }// fim de main

     // exibição dos erros
    public static void erreur(String msg, int exitCode){
         // exibição do erro
        System.err.println(msg);
         // parada com erro
        System.exit(exitCode);
    }//erro
}//classe

class ServeurSend extends Thread{
    // classe responsável por ler respostas digitadas no teclado
     // e enviá-las a um cliente por meio de um cliente TCP passado ao construtor

    Socket client;    // o cliente TCP
    int numClient;        //  do cliente

     // construtor
    public ServeurSend(Socket client, int numClient){
         // observa-se o cliente TCP
        this.client=client;
         // e seu número
        this.numClient=numClient;
    }//fabricante

     // método Run da thread
    public void run(){

        // dados locais
        PrintWriter OUT=null;        // fluxo de gravação de rede
        String réponse=null;        // resposta lida no teclado
    BufferedReader IN=null;    // fluxo do teclado

        // acompanhamento
        System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
         // gerenciamento de erros
        try{
             // criação do fluxo de gravação de rede
            OUT=new PrintWriter(client.getOutputStream(),true);
      // criação do fluxo do teclado
      IN=new BufferedReader(new InputStreamReader(System.in));
            // ciclo de entrada e envio de comandos
            while(true){
                 // identificação do cliente
                System.out.print("--> " + numClient + " : ");
                 // Leitura da resposta digitada no teclado
                réponse=IN.readLine().trim();
                // Concluído?
                if (réponse.toLowerCase().equals("fin")) break;
                // envio da resposta ao servidor
                OUT.println(réponse);
                 // próxima resposta
            }//enquanto
        }catch(Exception ex){
             // erro
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // fim  fechamento dos fluxos
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // notificamos o fim do thread
        System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
    }//execução
}//classe

class ServeurReceive extends Thread{
    // classe responsável por ler as linhas de texto enviadas ao servidor 
     // por meio de um cliente TCP passado ao construtor

    Socket client;    // o cliente TCP
    int numClient;        //  do cliente

     // construtor
    public ServeurReceive(Socket client, int numClient){
         // observa-se o cliente TCP
        this.client=client;
         // e seu número
        this.numClient=numClient;
    }//fabricante

     // método Run da thread
    public void run(){

        // dados locais
        BufferedReader IN=null;        // fluxo de leitura de rede
        String réponse=null;        // resposta do servidor

         // acompanhamento
        System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
         // gerenciamento de erros
        try{
             // criação do fluxo de leitura de rede
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // loop de leitura das linhas de texto do fluxo IN
            while(true){
                 // leitura do fluxo de rede
                réponse=IN.readLine();
                 // fluxo encerrado?
                if(réponse==null) break;
                // exibição
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // erro
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // fim  fechamos os fluxos
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // sinaliza-se o fim do thread
        System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
    }//execução
}//classe

8.4.5. Um cliente da Web

Vimos no exemplo anterior alguns dos cabeçalhos HTTP enviados por um navegador:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Vamos criar um cliente web ao qual seria passado como parâmetro um URL e que exibiria na tela o conteúdo desse URL. Vamos supor que o servidor Web contatado para o URL seja compatível com o protocolo HTTP 1.1. Dos cabeçalhos anteriores, utilizaremos apenas os seguintes:

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • o primeiro cabeçalho indica qual página desejamos
  • o segundo, qual servidor estamos consultando
  • o terceiro indica que desejamos que o servidor encerre a conexão após nos responder.

Se, no exemplo acima, substituirmos GET por HEAD, o servidor nos enviará apenas os cabeçalhos HTTP e não a página HTML.

Nosso cliente web será chamado da seguinte forma: java clientweb URL cmd, onde URL é oURL desejado e cmd uma das duas palavras-chave GET ou HEAD para indicar se queremos apenas os cabeçalhos (HEAD) ou também o conteúdo da página (GET). Vejamos um primeiro exemplo. Iniciamos o servidor IIS e, em seguida, o cliente web na mesma máquina:

dos>java clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private

A resposta

HTTP/1.1 302 Object moved

significa que a página solicitada mudou de endereço (ou seja, de URL). O novo endereço, URL, é fornecido pelo cabeçalho Location:

Location: /IISSamples/Default/welcome.htm

Se usarmos GET em vez de HEAD na chamada ao cliente Web:

dos>java clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private

<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

Obtemos o mesmo resultado que com HEAD, além do corpo da página HTML. O programa é o seguinte:

// pacotes importados
import java.io.*;
import java.net.*;

public class clientweb{

    // solicita um URL
     // exibe o conteúdo dela na tela

    public static void main(String[] args){
        // sintaxe
        final String syntaxe="pg URI GET/HEAD";

        // número de argumentos
        if(args.length != 2)
            erreur(syntaxe,1);

         // observa-se o URI solicitado
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // verificação da validade do URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorreto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//exceção
         // verificação do pedido
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // pedido incorreto
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // extraindo as informações úteis do URL
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // é possível prosseguir
        Socket  client=null;                        // o cliente
        BufferedReader IN=null;                    // o fluxo de leitura do cliente
        PrintWriter OUT=null;                        // o fluxo de gravação do cliente
        String réponse=null;                        // resposta do servidor
        try{
             // conectamos-nos ao servidor
            client=new Socket(host,port);

            // são criados os fluxos de entrada e saída do cliente TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // solicitação do URL — envio dos cabeçalhos HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // lê-se a resposta
            while((réponse=IN.readLine())!=null){
                 // processa-se a resposta
                System.out.println(réponse);
            }//enquanto
             // concluído
            client.close();
        } catch(Exception e){
            // tratamos a exceção
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // exibição de erros
    public static void erreur(String msg, int exitCode){
         // exibição do erro
        System.err.println(msg);
         // encerramento com erro
        System.exit(exitCode);
    }//erro
}//classe

A única novidade neste programa é o uso da classe URL. O programa recebe um URL (Uniform Resource Locator) ou URI (Uniform Resource Identifier) no formato http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... A classe URL permite decompor a string do URL em seus diferentes elementos. Um objeto URL é construído a partir da string URLstring recebida como parâmetro:

         // verificação da validade do URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorreto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//exceção

Se a string URL recebida como parâmetro não for um URL válido (falta do protocolo, do servidor, etc.), uma exceção é lançada. Isso nos permite verificar a validade do parâmetro recebido. Uma vez criado o objeto URL, temos acesso aos seus diversos elementos. Assim, se o objeto url do código anterior foi criado a partir da string

http://servidor:porta/cheminPageHTML?param1=val1;param2=val2;... 

, teremos:

url.getHost()=serveur

url.getPort()=port ou -1 se a porta não for indicada

url.getPath()=cheminPageHTML ou a string vazia se não houver caminho

url.getQuery()=param1=val1;param2=val2;... ou null se não houver solicitação

uri.getProtocol()=http

8.4.6. Cliente web que gerencia redirecionamentos

O cliente web anterior não lida com um possível redirecionamento do URL que ele solicitou. O cliente seguinte lida com isso.

  1. ele lê a primeira linha dos cabeçalhos HTTP enviados pelo servidor para verificar se há a string “302 Object moved”, que indica um redirecionamento
  2. ele lê os cabeçalhos seguintes. Se houver redirecionamento, ele procura a linha “Location: url”, que fornece o novo URL da página solicitada, e anota esse URL.
  3. ele exibe o restante da resposta do servidor. Se houver redirecionamento, as etapas 1 a 3 são repetidas com o novo URL. O programa não aceita mais de um redirecionamento. Esse limite é definido por uma constante que pode ser alterada.

Veja um exemplo:

Dos>java clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private

<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781

<html>

<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>

A programação é a seguinte:

// pacotes importados
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientweb2{

    // solicita um URL
     // exibe o conteúdo deste na tela

    public static void main(String[] args){
        // sintaxe
        final String syntaxe="pg URL GET/HEAD";

        // número de argumentos
        if(args.length != 2)
            erreur(syntaxe,1);

         // observa-se o URI solicitado
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // verificação da validade do URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorreto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//exceção
         // verificação do pedido
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // pedido incorreto
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // é possível trabalhar
        Socket  client=null;                        // o cliente
        BufferedReader IN=null;                    // o fluxo de leitura do cliente
        PrintWriter OUT=null;                        // o fluxo de gravação do cliente
        String réponse=null;                        // resposta do servidor
        final int nbRedirsMax=1;                // não é permitido mais de um redirecionamento
        int nbRedirs=0;                                    // número de redirecionamentos em andamento
        String premièreLigne;                        // primeira linha da resposta
        boolean redir=false;                        // indica se há ou não redirecionamento
        String locationString="";                // a sequência URL de um possível redirecionamento

         // expressão regular para localizar uma sequência URL de redirecionamento
        Pattern location=Pattern.compile("^Location: (.+?)$");

         // gestão de erros
        try{
             // pode haver várias cadeias URL a serem consultadas caso haja redirecionamentos
            while(nbRedirs<=nbRedirsMax){

                // extrai-se as informações úteis do URL
            String protocol=url.getProtocol();
            String path=url.getPath();
            if(path.equals("")) path="/";
            String query=url.getQuery();
            if(query!=null) query="?"+query; else query="";
            String host=url.getHost();
            int port=url.getPort();
            if(port==-1) port=url.getDefaultPort();

                 // conectamos-nos ao servidor
                client=new Socket(host,port);

                // criam-se os fluxos de entrada e saída do cliente TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // solicita-se o URL — envio dos cabeçalhos HTTP
                OUT.println(commande + " " + path + query + " HTTP/1.1");    
                OUT.println("Host: " + host + ":" + port);
                OUT.println("Connection: close");
                OUT.println();

                 // lê-se a primeira linha da resposta
                premièreLigne=IN.readLine();
                 // exibição na tela
                System.out.println(premièreLigne);

                // redirecionamento?
        if(premièreLigne.endsWith("302 Object moved")){     
                    // há um redirecionamento
                    redir=true;
                    nbRedirs++;
                }//if

                // os cabeçalhos HTTP a seguir até encontrar a linha vazia que indica o fim dos cabeçalhos
                boolean locationFound=false;
                while(!(réponse=IN.readLine()).equals("")){
                    // exibe-se a resposta
                    System.out.println(réponse);
                     // se houver redirecionamento, procura-se o cabeçalho Location
                    if(redir && ! locationFound){
                        // compara-se a linha com a expressão relacional location
                        Matcher résultat=location.matcher(réponse);
                        if(résultat.find()){
                            // se for encontrada, registra-se o URL de redirecionamento
                            locationString=résultat.group(1);             
                             // registra-se que foi encontrado
                            locationFound=true;
                        }//se
                    }//se
                    // cabeçalho seguinte
                }//enquanto

                 // linhas seguintes da resposta
                System.out.println(réponse);
                while((réponse=IN.readLine())!=null){
                     // a resposta é exibida
                    System.out.println(réponse);
                }//enquanto
                 // fecha-se a conexão
                client.close();
                 // já terminamos?
                if ( ! locationFound || nbRedirs>nbRedirsMax)
                    break;
                 // há um redirecionamento a ser feito — constrói-se a nova URL
                URLString=protocol +"://"+host+":"+port+locationString;
                url=new URL(URLString);
                // rastreamento
                System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
            }//enquanto
        } catch(Exception e){
            // tratamos a exceção
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // exibição de erros
    public static void erreur(String msg, int exitCode){
         // exibição de erros
        System.err.println(msg);
         // encerramento com erro
        System.exit(exitCode);
    }//erro
}//classe

8.4.7. Servidor de cálculo de impostos

Retomamos o exercício IMPOTS, já abordado de diversas formas. Recorde-se a última versão:

Foi criada uma classe base para impostos. Seus atributos são três tabelas de números:

public class impots{

  // os dados necessários para o cálculo do imposto
   // provêm de uma fonte externa

  protected double[] limites=null;
  protected double[] coeffR=null;
  protected double[] coeffN=null;

   // campo “construtor” vazio
  protected impots(){}

   // construtor
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{

A classe **impots** possui dois construtores:

  • um construtor ao qual são passadas as três matrizes de dados necessárias para o cálculo do imposto
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
  • um construtor sem parâmetros, utilizável apenas por classes filhas
  protected impots(){}

A partir dessa classe, foi derivada a classe impotsJDBC, que permite preencher as três tabelas limites, coeffR e coeffN a partir do conteúdo de um banco de dados:

public class impotsJDBC extends impots{
  // adição de um construtor que permite a construção
   // as tabelas limites, coeffr e coeffn a partir da tabela
   // de impostos de um banco de dados
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

    // dsnIMPOTS: nome DSN do banco de dados
     // userIMPOTS, mdpIMPOTS: login/senha de acesso ao banco de dados

Um aplicativo gráfico havia sido desenvolvido. O aplicativo utilizava um objeto da classe impotsJDBC. O aplicativo e esse objeto estavam na mesma máquina. Propomos colocar o programa de teste e o objeto impotsJDBC em máquinas diferentes. Teremos um aplicativo cliente-servidor em que o objeto remoto impotsJDBC será o servidor. A nova classe se chama ServeurImpots e é derivada da classe impotsJDBC:

// pacotes importados
import java.net.*;
import java.io.*;
import java.sql.*;

public class ServeurImpots extends impotsJDBC {

    // atributos
    int portEcoute;                // a porta de escuta para solicitações de clientes
    boolean actif;                // estado do servidor

     // construtor
    public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
      throws IOException, SQLException, ClassNotFoundException {
       // instância pai
        super(DSNimpots, USERimpots, MDPimpots);
         // observa-se a porta de escuta
        this.portEcoute=portEcoute;
         // por enquanto inativo
        actif=false;
         // cria e inicia um thread para ler os comandos digitados no teclado
         // o servidor será gerenciado a partir desses comandos
        Thread admin=new Thread(){
        public void run(){
          try{
            admin();
        }catch (Exception ignored){}
      }
    };
    admin.start();
    }//ServeurImpots

O único parâmetro novo no construtor é a porta de escuta para as solicitações dos clientes. Os demais parâmetros são passados diretamente para a classe base impotsJDBC. O servidor de impostos é controlado por comandos digitados no teclado. Por isso, criamos um thread para ler esses comandos. Haverá duas opções possíveis: start para iniciar o serviço e stop para encerrá-lo definitivamente. O método admin que gerencia esses comandos é o seguinte:

    public void admin() throws IOException{
        // lê os comandos de administração do servidor digitados no teclado
         // em um loop infinito
        String commande=null;
    BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
        while(true){
             // solicita
            System.out.print("Serveur d'impôts>");
             // leitura do comando
            commande=IN.readLine().trim().toLowerCase();
             // execução do comando
            if(commande.equals("start")){
                // ativo?
                if(actif){
                     //erro
                    System.out.println("Le serveur est déjà actif");
                     // continuamos
                    continue;
                }//se
                 // criamos e iniciamos o serviço de escuta
                Thread ecoute=new Thread(){
                public void run(){
                  ecoute();
              }
            };
            ecoute.start();
            }//se
            else if(commande.equals("stop")){
                // fim de todas as threads de execução
                System.exit(0);
            }//se
            else {
                // erro
                System.out.println("Commande incorrecte. Utilisez (start,stop)");
            }//se
        }//enquanto
    }//admin

Se o comando digitado no teclado for start, um thread de escuta de solicitações de clientes é iniciado. Se o comando digitado for stop, todos os threads são interrompidos. O thread de escuta executa o método ecoute:

    public void ecoute(){
         // thread de escuta de solicitações dos clientes
         // cria-se o serviço de escuta
        ServerSocket ecoute=null;
        try{
            // cria-se o serviço
            ecoute=new ServerSocket(portEcoute);
             // acompanhamento
            System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);

             // ciclo de serviço
            Socket liaisonClient=null;
            while (true){ // loop infinito
                 // aguardando um cliente
                liaisonClient=ecoute.accept();

                 // o serviço é executado por outra tarefa
                new traiteClientImpots(liaisonClient,this).start();

                 // retornamos à escuta de solicitações
            }// fim do while
        }catch(Exception ex){
             // o erro é sinalizado
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }//thread de escuta

Encontramos um servidor TCP clássico escutando na porta portEcoute. As solicitações dos clientes são processadas pelo método run da thread traiteCientImpots, cujo construtor recebe dois parâmetros:

  1. o objeto Socket liaisonClient, que permitirá acessar o cliente
  2. o objeto impotsJDBC this, que dará acesso ao método this.calculer de cálculo do imposto.
// -------------------------------------------------------
// presta serviço a um cliente do servidor de impostos

class traiteClientImpots extends Thread{

    private Socket liaisonClient;            // conexão com o cliente
    private BufferedReader IN;                // fluxo de entrada
    private PrintWriter OUT;                    // fluxo de saída
    private impotsJDBC objImpots;            // objeto Imposto

         // construtor
    public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
        this.liaisonClient=liaisonClient;
        this.objImpots=objImpots;
    }//construtor

O método run processa as solicitações dos clientes. Trata-se de linhas de texto que podem assumir duas formas:

  1. cálculo de casado(s) (s/n) nbEnfants salaireAnnuel
  2. fincalculs

O formato 1 permite o cálculo de um imposto; o formato 2 encerra a conexão cliente-servidor.

     // método de execução
    public void run(){
         // presta o serviço ao cliente
        try{
             // fluxo de entrada
            IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
             // fluxo de saída
            OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
             // envio de uma mensagem de boas-vindas ao cliente
            OUT.println("Bienvenue sur le serveur d'impôts");

             // ciclo de leitura de solicitação/gravação de resposta
            String demande=null;
            String[] champs=null;    // os elementos da solicitação
            String commande=null;    // o comando do cliente: cálculo ou fim dos cálculos
            while ((demande=IN.readLine())!=null){
                 // decompondo a solicitação em campos
                champs=demande.trim().toLowerCase().split("\\s+");
                 // duas solicitações aceitas: cálculo e fincalculos
                commande=champs[0];
                if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
                     // erro do cliente
                    OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
                     // próximo pedido
                    continue;
                }//if
                if(commande.equals("calcul")) calculerImpôt(champs);
                if(commande.equals("fincalculs")){
                     // mensagem de despedida ao cliente
                    OUT.println("Au revoir...");
                     // liberação de recursos
                    try{ OUT.close();IN.close();liaisonClient.close();}
                    catch(Exception ex){}
                     // fim
                    return;
                }//se
                 //próxima solicitação
            }//enquanto
        }catch (Exception e){
            erreur("L'erreur suivante s'est produite ("+e+")",2);
        }// fim do try
    }// fim da execução

O cálculo do imposto é realizado pelo método calculerImpôt, que recebe como parâmetro a tabela de campos da solicitação feita pelo cliente. A validade da solicitação é verificada e, se for o caso, o imposto é calculado e devolvido ao cliente.

     // cálculo de impostos
    public void calculerImpôt(String[] champs){
         // processa a solicitação: cálculo para casados nbEnfants salaireAnnuel
         // decomposta em campos na tabela de campos

        String marié=null;
        int nbEnfants=0;
        int salaireAnnuel=0;

         // validade dos argumentos
        try{
             // são necessários pelo menos 4 campos
            if(champs.length!=4) throw new Exception();
            // casado
            marié=champs[1];
            if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
            // filhos
            nbEnfants=Integer.parseInt(champs[2]);
             // salário
            salaireAnnuel=Integer.parseInt(champs[3]);
        }catch (Exception ignored){
            // erro de formato
            OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
             // concluído
            return;
        }//se
         // é possível calcular o imposto
        long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
         // envia-se a resposta ao cliente
        OUT.println(""+impot);
    }//calcular

Um programa de teste poderia ser o seguinte:

// chamada: serveurImpots porta dsnImpots userImpots mdpImpots

import java.io.*;

public class testServeurImpots{
    public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";

    // programa principal
    public static void main (String[] args){

        // são necessários 4 argumentos
        if(args.length != 4)
            erreur(syntaxe,1);

         // a porta deve ser um número inteiro maior que 0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

         // criamos o servidor de impostos
        try{
            new ServeurImpots(port,args[1],args[2],args[3]);
        }catch(Exception ex){
             //erro
            System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
        }//catch
    }//Main

     // exibição de erros
    public static void erreur(String msg, int exitCode){
         // exibição de erro
        System.err.println(msg);
         // parada com erro
        System.exit(exitCode);
    }//erro
}// fim da classe

Inserimos no programa de teste os dados necessários para a construção de um objeto ServeurImpots e, a partir daí, ele cria esse objeto.

Vamos tentar uma primeira execução:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

O comando

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots

cria um objeto ServeurImpots que ainda não está escutando as solicitações dos clientes. É o comando start, digitado no teclado, que inicia essa escuta. O comando stop encerra o servidor. Vamos agora utilizar um cliente. Usaremos o cliente genérico criado anteriormente. O servidor está em execução:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124

O cliente genérico é iniciado em outra janela do DOS:

Dos>java clientTCPgenerique localhost 124
Commandes :
<-- Bienvenue sur le serveur d'impôts

Percebe-se que o cliente recebeu corretamente a mensagem de boas-vindas do servidor. Enviamos outros comandos:

x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<--  syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Voltamos à janela do servidor para encerrá-lo:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

8.5. Exercícios

8.5.1. Exercício 1 - Cliente TCP genérico gráfico

8.5.1.1. Apresentação do aplicativo

Propomos criar um programa capaz de se comunicar pela Internet com os principais serviços TCP. Chamaremos isso de cliente TCP genérico. Ao compreender essa aplicação, percebe-se que todos os clientes TCP são semelhantes. A janela do programa é a seguinte:

Image

O significado dos diferentes controles é o seguinte:

n.º
nome
tipo
função
1
TxtRemoteHost
JTextField
nome da máquina que oferece o serviço desejado
2
TxtPort
JTextField
porta do serviço solicitado
3
TxtSend
JTextField
texto da mensagem que será enviada ao servidor pelo cliente
4
OptRCLF
OptLF
JCheckBox
botões que permitem indicar como as linhas terminam na comunicação cliente/servidor
RCLF: retorno de carro (#13) + quebra de linha (#10)
LF: quebra de linha (#10)
5
LstSuivi
JList
exibe mensagens sobre o status da comunicação entre o cliente e o servidor
6
LstDialogue
JList
exibe as mensagens trocadas entre o cliente (->) e o servidor (<-)
7
CmdAnnuler
JButton
oculto — localizado abaixo da lista de diálogo — Aparece quando a conexão está em andamento e permite interrompê-la caso o servidor não responda

As opções de menu disponíveis são as seguintes:

opção
subopções
função
Conexão
Conectar
conecta o cliente ao servidor
 
Desconectar
encerra a conexão
 
Sair
Encerra o programa
Mensagens
Enviar
Envia a mensagem do controle TxtSend para o servidor
 
RazSuivi
Apaga a lista LstSuivi
 
RazDialogue
Apaga a lista LstDialogue
Autor
 
exibe uma caixa de direitos autorais

8.5.1.2. FONCTIONNEMENT DE O APPLICATION

Inicialização do aplicativo

Quando a tela principal do aplicativo é carregada, ocorrem as seguintes ações:

  • a tela é centralizada na tela
  • apenas as opções de menu Connexion/Quitter e Auteur estão ativas
  • o botão Annuler fica oculto
  • as listas LstSuivi e LstDialogue estão vazias
Menu Login/Conectar

Essa opção só está disponível quando os campos “Host remoto” e “Número da porta” não estiverem vazios e não houver nenhuma conexão ativa no momento. Ao clicar nessa opção, ocorrem as seguintes operações:

  • a validade da porta é verificada: deve ser um número inteiro > 0
  • é iniciado um thread para estabelecer a conexão com o servidor
  • o botão Annuler é exibido para permitir que o usuário interrompa a conexão em andamento
  • todas as opções do menu são desativadas, exceto Quitter e Auteur

A conexão pode ser encerrada de várias maneiras:

  1. O usuário clicou no botão Annuler: interrompe-se o thread de conexão e o menu é restaurado ao seu estado inicial. Indica-se no registro de acompanhamento que a conexão foi encerrada pelo usuário.
  2. A conexão é encerrada com um erro: faz-se o mesmo que anteriormente e, além disso, no registro, indica-se a causa do erro.
  3. A conexão é concluída corretamente: removemos o botão Annuler, indicamos no registro que a conexão foi estabelecida, habilitamos o menu RazSuivi, desabilitamos o menu Connecter e habilitamos o menu Déconnecter
Menu Conectar/Desconectar

Essa opção só está disponível quando há uma conexão com o servidor. Quando ativada, ela encerra a conexão com o servidor e retorna o menu ao seu estado inicial. É indicado no registro que a conexão foi encerrada pelo cliente.

Menu Conectar/Sair

Esta opção encerra uma eventual conexão ativa com o servidor e encerra o aplicativo.

Menu Mensagens/Enviar

Esta opção só está disponível se as seguintes condições forem atendidas:

  • a conexão com o servidor foi estabelecida

  • há uma mensagem a ser enviada

Se essas condições forem atendidas, envia-se ao servidor o texto presente no campo TxtSend (3), terminado pela sequência RCLF se a opção RCLF tiver sido marcada; caso contrário, pela sequência LF. Um eventual erro na transmissão é sinalizado na lista de acompanhamento.

Menus RazSuivi e RazDialogue

Esvaziam, respectivamente, as listas LstSuivi e LstDialogue. Essas opções são desativadas quando as listas correspondentes estão vazias.

O botão Cancelar

Este botão, localizado na parte inferior do formulário, só aparece quando o cliente está se conectando ao servidor. Essa conexão pode não ser estabelecida porque o servidor não responde ou responde incorretamente. O botão Annuler oferece, então, ao usuário a possibilidade de interromper a tentativa de conexão.

As listas de acompanhamento

A lista LstSuivi (5) acompanha a conexão. Ela indica os momentos-chave da conexão:

  • sua abertura pelo cliente

  • seu encerramento pelo servidor ou pelo cliente

  • todos os erros que possam ocorrer enquanto a conexão estiver ativa

A lista LstDialogue (6) monitora o diálogo estabelecido entre o cliente e o servidor. Um thread monitora em segundo plano o que ocorre no soquete de comunicação do cliente e exibe essas informações na lista 6.

A opção Autor

Este menu abre uma janela chamada “Copyright”:

Image

Gerenciamento de erros

Os erros de conexão são sinalizados na lista de acompanhamento 6, enquanto os erros relacionados ao diálogo cliente/servidor são exibidos na lista de diálogo 7. Em caso de erro de conexão, o diálogo cliente/servidor é encerrado e o formulário é restaurado ao seu estado inicial, pronto para uma nova conexão.

8.5.1.3. TRAVAIL A FAIRE

Executar o trabalho descrito anteriormente de duas formas:

  • aplicativo autônomo
  • applet

8.5.2. Exercício 2 – Um servidor de recursos

8.5.2.1. INTRODUCTION

Uma instituição possui vários servidores de computação potentes acessíveis pela Internet. Qualquer máquina que deseje utilizar esses serviços de computação envia um arquivo de dados pela porta 756 de um dos servidores. Esse arquivo contém diversas informações: login, senha, comandos indicando o tipo de cálculo desejado e os dados sobre os quais o cálculo deve ser feito. Se o arquivo de dados estiver correto, o servidor de computação selecionado o utiliza e retorna os resultados ao cliente na forma de um arquivo de texto.

As vantagens dessa organização são múltiplas:

  • Qualquer tipo de cliente (PC, Mac, Unix, etc.) pode utilizar este serviço
  • o cliente pode estar em qualquer lugar na Internet
  • os recursos de computação são otimizados: são necessárias apenas algumas máquinas potentes. Assim, uma pequena organização sem recursos de computação pode utilizar este serviço mediante uma contribuição financeira calculada com base no tempo de computação utilizado.

Apesar do poder das máquinas, um cálculo pode, às vezes, demorar várias horas: o servidor fica, então, indisponível para outros clientes. Surge, então, para um cliente, o problema de encontrar um servidor de computação disponível. Para isso, utiliza-se um “gerenciador de recursos de computação”, denominado a seguir de servidor GRC. Esse serviço está instalado em uma única máquina e opera na porta 864 no modo TCP. É a ele que se dirige um cliente que deseja acessar um servidor de computação. O servidor GRC, que mantém a lista completa dos servidores de computação, responde enviando o nome de um servidor atualmente inativo. O cliente então só precisa enviar seus dados para o servidor que lhe foi indicado.

Propomos escrever o servidor GRC.

8.5.2.2. O INTERFACE VISUELLE

A interface visual será a seguinte:

Image

A interface apresenta duas listas de servidores:

  • à esquerda, a lista de servidores inativos e, portanto, disponíveis para cálculos
  • à direita, a lista de servidores ocupados pelos cálculos de um cliente.

A estrutura do menu é a seguinte:

Menu Principal
Menu secundário
Função
Serviço
Iniciar
Inicia o serviço TCP na porta 864
 
Parar
Encerra o serviço
 
Sair
Encerra o aplicativo
Autor
 
Informações de direitos autorais

A estrutura dos controles presentes no formulário é a seguinte:

Nome
Tipo
Função
listLibres
JList
Lista de servidores disponíveis
listOccupés
JList
Lista de servidores ocupados

8.5.2.3. FONCTIONNEMENT DE APPLICATION

Carregamento do aplicativo

Ao carregar o aplicativo, a lista listLibres é preenchida com a lista de nomes dos servidores de computação gerenciados pelo GRC. Esses nomes são definidos em um arquivo “Servidores” passado como parâmetro. Esse arquivo contém uma lista de nomes de servidores, com um por linha, e é, portanto, utilizado para preencher a lista listLibres. O menu “Iniciar” está habilitado, enquanto o menu “Parar” está desabilitado.

Opção Serviço/Iniciar

Esta opção

  • inicia o serviço de escuta na porta 864 da máquina
  • desativa o menu “Iniciar”
  • habilita o menu Parar
Opção Serviço/Parar

Esta opção interrompe o serviço:

  • a lista de servidores ocupados é esvaziada
  • a lista de servidores livres é preenchida com o conteúdo do arquivo Servidores
  • o menu “Iniciar” é habilitado
  • o menu “Encerrar” é desativado
Opção Serviço/Sair

O aplicativo é encerrado.

Diálogo cliente/servidor

A comunicação cliente/servidor ocorre por meio da troca de linhas de texto terminadas pela sequência RCLF. O servidor GRC reconhece dois comandos: getserveur e finservice. Detalhamos a função desses dois comandos:

  • 1-getserveur

O cliente consulta se há um servidor de cálculo disponível para ele.

O servidor GRC seleciona então o primeiro servidor encontrado em sua lista de servidores livres e retorna seu nome ao cliente no formato:

        100-nom du serveur

Além disso, ele insere o servidor atribuído ao cliente na lista de servidores ocupados no formato:

        serveur (IP du client)

como mostra o exemplo a seguir, em que o servidor calcul1.istia.univ-angers.fr está ocupado atendendo ao cliente com o endereço IP 193.52.43.5:

Image

Um cliente não pode enviar um comando getserveur se já tiver sido atribuído a ele um servidor de cálculo. Assim, antes de responder ao cliente, o servidor GRC verifica se o endereço IP do cliente já não está presente entre os registrados na lista de servidores ocupados. Se for o caso, o servidor GRC responde:

        501-Vous avez actuellement une demande en cours

Por fim, há o caso em que nenhum servidor de computação está disponível: a lista de servidores livres está vazia. Nesse caso, o servidor GRC responde:

        502- Il n’y a aucun serveur de calcul disponible

Em todos os casos, após responder ao cliente, o servidor GRC encerra a conexão com ele para poder atender outros clientes.

  • 2-finservice

O cliente indica que não precisa mais do servidor de cálculo que estava utilizando.

O servidor GRC verifica primeiro se o cliente é realmente aquele que ele estava atendendo. Para isso, ele verifica se o endereço IP do cliente está presente entre os registrados na lista de servidores ocupados. Caso contrário, o servidor GRC responde:

        503-Aucun serveur ne vous a été attribué

Se o cliente for reconhecido, o servidor GRC responde:

        101-Fin de service acceptée

e transfere o servidor de cálculo atribuído a esse cliente para a lista de servidores livres. Retomando o exemplo anterior, se o cliente enviar o comando finservice, a exibição do servidor GRC passa a ser:

Image

Após o envio da resposta, seja ela qual for, o servidor GRC encerra a conexão.

8.5.2.4. TRAVAIL para FAIRE

Escreva o aplicativo como um programa autônomo que possa ser testado, por exemplo, com um cliente telnet ou com o cliente TCP genérico do exercício anterior.

8.5.3. Exercício 3 — um cliente SMTP

8.5.3.1. INTRODUCTION

Nosso objetivo aqui é construir um cliente para o serviço SMTP (SendMail Transfer Protocol), que permite enviar e-mails. No Unix ou no Windows, o programa telnet é um cliente que opera com o protocolo tcp. Ele pode “se comunicar” com qualquer serviço tcp que aceite comandos no formato de texto terminados pela sequência RCLF, ou seja, os caracteres de código ASCII 13 e 10. Aqui está um exemplo de interação com o serviço smtp para envio de e-mail:

$ telnet istia.univ-angers.fr 25        // chamada do serviço SMTP

// resposta do servidor SMTP


Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here

// comentários --------------

O programa telnet pode chamar qualquer serviço usando a sintaxe

telnet machine_service port_service

As trocas entre cliente e servidor são feitas por meio de linhas de texto que terminam com a sequência RCLF.

As respostas do serviço smtp têm o seguinte formato:

número-da-mensagem ou

número da mensagem

O servidor SMTP pode enviar várias linhas de resposta. A última linha da resposta é indicada por um número seguido de um espaço, enquanto que, nas linhas anteriores da resposta, o número é seguido de um traço -.

Um número maior ou igual a 500 indica uma mensagem de erro.

// fim dos comentários

help                        // comando digitado no teclado

// resposta do servidor SMTP

214-Commands:
214-    HELO    EHLO    MAIL    RCPT    DATA
214-    RSET    NOOP    QUIT    HELP    VRFY
214-    EXPN    VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214-    sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info
mail from: serge.tahe@istia.univ-angers.fr    // novo comando digitado no teclado

// comentários ---------

O comando mail tem a seguinte sintaxe:

mail from: endereço de e-mail do remetente da mensagem

// fim dos comentários

// resposta do servidor SMTP

250 serge.tahe@istia.univ-angers.fr... Sender ok

// comentários

O servidor smtp não verifica a validade do endereço do remetente: ele aceita o endereço tal como foi fornecido

// fim dos comentários

rcpt to: user1@istia.univ-angers.fr        // novo comando digitado no teclado

// comentários ---------

O comando rcpt tem a seguinte sintaxe:

rcpt to: endereço de e-mail do destinatário da mensagem

Se o endereço de e-mail for um endereço da máquina na qual o servidor smtp está em execução, ele verifica se o endereço existe; caso contrário, não realiza nenhuma verificação. Se houver verificação e for detectado um erro, ele será sinalizado com um número >= 500.

É possível emitir quantos comandos *****rcpt to forem necessários: isso permite enviar uma mensagem para várias pessoas.*

// fim dos comentários

// resposta do servidor SMTP

250 user1@istia.univ-angers.fr... Recipient ok
data                        // novo comando digitado no teclado

// comentários ---------

O comando data tem a seguinte sintaxe:

data

linha1

linha2

...

.

Em seguida, vêm as linhas de texto que compõem a mensagem, a qual deve terminar com uma linha contendo apenas o caractere “ponto”.

A mensagem é então enviada ao destinatário indicado pelo comando rcpt.

// fim dos comentários

// resposta do servidor SMTP

354 Enter mail, end with "." on a line by itself

// texto da mensagem digitado no teclado


subject: essai smtp

essai smtp a partir de telnet
.

// comentários

Nas linhas de texto do comando data, é possível inserir uma linha subject: para especificar o assunto do e-mail. Essa linha deve ser seguida por uma linha em branco.

// resposta do servidor SMTP

250 HAA11627 Message accepted for delivery
quit                            // novo comando digitado no teclado

// comentários

O comando quit encerra a conexão com o serviço smtp

// fim dos comentários

// resposta do servidor SMTP

221 Istia.Istia.Univ-Angers.fr closing connection

8.5.3.2. O INTERFACE VISUELLE

Propomos criar um programa com a seguinte interface visual:

Image

Os controles têm as seguintes funções:

Número
Tipo
Função
1
JTextField
Sequência de endereços de e-mail separados por vírgula
2
JTextField
Texto do assunto da mensagem
3
JTextField
Sequência de endereços de e-mail separados por vírgula
4
JTextField
Sequência de endereços de e-mail separados por vírgula
5
JTextArea
Texto da mensagem
6
JList
lista de acompanhamento
7
JList
lista de diálogo
8
JButton
Botão “Cancelar” não exibido, que aparece quando o cliente solicita conexão com o servidor SMTP. Permite que o usuário interrompa essa solicitação caso o servidor não responda.

8.5.3.3. LES MENUS

A estrutura dos menus do aplicativo é a seguinte:

Menu Principal
Menu secundário
Função
Correio
  
 
Enviar
Envia a mensagem do controle 5
 
Sair
Sair do aplicativo
Opções
  
 
Ocultar acompanhamento
Oculta o controle 6
 
Redefinir acompanhamento
Esvazia a lista de acompanhamento 6
 
Ocultar Diálogo
Oculta a lista de diálogos 7
 
Limpar Diálogo
Esvazia a lista de diálogo 7
 
Configurar
Permite ao usuário especificar
- o endereço do servidor SMTP utilizado pelo programa
- seu endereço de e-mail
 
Salvar...
Salva a configuração anterior em um arquivo .ini
Autor
 
Informações de direitos autorais

8.5.3.4. FONCTIONNEMENT DE APPLICATION

Menu Opções/Configurar

Este menu exibe a seguinte janela:

Image

Os dois campos devem ser preenchidos para que o botão OK fique ativo. As duas informações devem ser armazenadas em variáveis globais para que fiquem disponíveis para outros módulos.

Menu Correio/Enviar

Esta opção só está disponível se as seguintes condições forem atendidas:

  • a configuração tiver sido feita
  • há uma mensagem a ser enviada
  • há um assunto
  • há pelo menos um destinatário nos campos 1, 3 e 4

Se essas condições forem atendidas, a sequência de eventos é a seguinte:

  • o formulário é colocado em um estado em que todas as ações que possam interferir na comunicação cliente/servidor são inibidas
  • ocorre uma conexão na porta 25 do servidor especificado na configuração
  • o cliente então se comunica com o servidor SMTP de acordo com o protocolo descrito acima
  • o campo “mail from:” utiliza o endereço de e-mail do remetente informado na configuração
  • o campo “rcpt to:” é utilizado para cada um dos endereços de e-mail encontrados nos campos 1, 3 e 4
  • nas linhas enviadas após o comando data, encontrar-se-ão os seguintes textos:
    • uma linha Subject: – texto do assunto da verificação 2
    • uma linha Cc: endereços da verificação 3
    • uma linha Bcc: endereços da verificação 4
    • o texto da mensagem da verificação 5
    • o ponto final
O botão Cancelar

Este botão, localizado na parte inferior do formulário, só aparece quando o cliente está se conectando ao servidor smtp. Essa conexão pode não ser estabelecida porque o servidor smtp não responde ou responde incorretamente. O botão Annuler oferece, então, ao usuário a possibilidade de interromper a tentativa de conexão.

As listas de acompanhamento

A lista (6) acompanha a conexão. Ela indica os momentos-chave da conexão:

  • sua abertura pelo cliente
  • seu encerramento pelo servidor ou pelo cliente
  • todos os erros de conexão

A lista (7) acompanha o diálogo smtp estabelecido entre o cliente e o servidor.

Essas duas listas estão associadas a opções do menu:

Ocultar Lista de Acompanhamento
Oculta a lista de acompanhamento 6, bem como o texto que está acima dela. Se a altura ocupada por esses dois controles for H, todos os controles localizados abaixo serão deslocados para cima em uma altura de H e o tamanho total do formulário será reduzido em H. Além disso, a opção “Ocultar Acompanhamento” torna invisível a opção RazSuivi abaixo.
Reverter acompanhamento
Esvazia a lista de acompanhamento 6
Ocultar Diálogo
Oculta a lista de diálogo 7, o texto que está acima dela e a opção de menu RazDialogue abaixo. Assim como em “Ocultar Rastreamento”, a posição dos controles localizados abaixo (talvez o botão Annuler) é recalculada e o tamanho da janela é reduzido.
Limpar Diálogo
Esvazia a lista de diálogo 7
A opção Autor

Este menu abre uma janela chamada “Direitos autorais”:

Image

Gerenciamento de erros

Os erros de conexão são sinalizados na lista de acompanhamento 6, enquanto os relacionados à comunicação cliente/servidor aparecem na lista de diálogo 7. Quando ocorre um erro, o usuário é avisado por meio de uma caixa de erro, e a lista contendo a causa do erro é exibida, caso estivesse oculta anteriormente. Além disso, o diálogo cliente/servidor é fechado e o formulário é restaurado ao seu estado inicial.

8.5.3.5. GESTION de UN FICHIER DE CONFIGURATION

É recomendável que o usuário não precise reconfigurar o software toda vez que o utilizar. Para isso, se a opção Opções/Salvar a configuração ao sair estiver marcada, ao fechar o programa, as duas informações obtidas pela opção Options/Configurer, bem como o estado das duas listas de acompanhamento, serão salvas em um arquivo sendmail.ini localizado no mesmo diretório que o arquivo .exe do programa. Esse arquivo tem o seguinte formato:

SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1

As linhas SmtpServer e ReplyAddress contêm as duas informações definidas na opção Opções/Configurar. As linhas Suivi e Dialogue indicam o status das listas de Acompanhamento e Diálogo: 1 (presente), 0 (ausente).

Ao carregar o programa, o arquivo sendmail.ini é lido, caso exista, e o formulário é configurado de acordo. Se o arquivo sendmail.ini não existir, procede-se como se ele existisse:

SmtpServer=
ReplyAddress=
Suivi=1
Dialogue=1

Se o arquivo sendmail.ini existir, mas estiver incompleto (linhas faltando), a linha que falta é substituída pela linha correspondente acima. Assim, como a linha Suivi=... está faltando, procede-se como se tivéssemos Suivi=1.

Todas as linhas que não correspondem ao modelo:

    mot clé= valeur

são ignoradas, assim como aquelas em que a palavra-chave é inválida. A palavra-chave pode estar em maiúsculas ou minúsculas: isso não faz diferença.

Na opção Options/Configurer, são exibidos os valores SmtpServer e ReplyAddress atualmente em uso. O usuário pode, então, alterá-los, se desejar.

8.5.3.6. TRAVAIL A FAIRE

Execute o trabalho descrito anteriormente. Recomenda-se tratar a gestão do arquivo de configuração por último.

8.5.4. Exercício 4 – cliente POPPASS

8.5.4.1. Introduction

Propõe-se a criação de um cliente TCP capaz de se comunicar com o servidor POPPASSD, que opera na porta 106. Esse serviço permite alterar a senha em uma máquina UNIX. O protocolo de comunicação cliente/servidor é o seguinte:

1 - As comunicações ocorrem por meio da troca de mensagens que terminam com a sequência RCLF

2 - O cliente envia comandos ao servidor

  • O servidor responde com mensagens que começam com números de três dígitos: XXX. Se XXX = 200, o comando foi executado corretamente; caso contrário, ocorreu um erro.

3 - A sequência das trocas de mensagens é a seguinte:

A    - le client se connecte
  • o servidor responde com uma mensagem de boas-vindas
B    - le client envoie USER login
  • o servidor responde solicitando a senha se o login for aceito; caso contrário, exibe uma mensagem de erro
C    - le client envoie PASS mot_de_passe
  • o servidor responde solicitando a nova senha; se a senha for aceita, exibe uma mensagem de erro; caso contrário, exibe uma mensagem de erro
D    - le client envoie NEWPASS nouveau_mot_de_passe
  • o servidor responde confirmando que a nova senha foi aceita; caso contrário, exibe uma mensagem de erro
E    - le client envoie la commande QUIT
  • o servidor envia uma mensagem de encerramento e encerra a conexão

8.5.4.2. O formulário do cliente

Image

O significado dos diferentes controles é o seguinte:

n.º
nome
tipo
função
1
txtRemoteHost
JTextField
nome do servidor
2
txtLogin
JTextField
login do usuário
3
txtMdp
JTextField
Senha do usuário
4
txtNewMdp
JTextField
Nova senha do usuário
5
txtConfirmation
JTextField
Confirmação da nova senha
6
lstSuivi
JList
Mensagens de acompanhamento da conexão
7
lstDialogue
JList
Mensagens do diálogo Cliente/Servidor
10
cmdAnnuler
JButton
não representado — Botão que aparece quando a conexão com o servidor está em andamento. Permite interrompê-la.

8.5.4.3. Os menus

Título
Nome do controle
Função
Conexão
mnuconexão
 
Conectar
mnuconnecter
inicia a conexão com o servidor
Sair
mnuQuitter
encerra o aplicativo
Mensagens
mnuMessages
 
RazSuivi
mnuRazSuivi
apaga a lista lstSuivi
RazDialogue
mnuRazDialogue
apaga a lista lstDialogue
Autor
mnuAuteur
exibe a caixa de direitos autorais

8.5.4.4. Funcionamento do aplicativo

Inicialização do aplicativo

Quando a tela principal do aplicativo é carregada, ocorrem as seguintes ações:

  • a tela é centralizada na tela
  • apenas as opções de menu Connexion/Quitter e Auteur estão ativas
  • o botão Annuler fica oculto
  • as listas LstSuivi e LstDialogue estão vazias
Menu Login/Conectar

Esta opção só está disponível quando os campos 1 a 5 forem preenchidos. Ao clicar nesta opção, ocorrem as seguintes operações:

  • é iniciado um thread para garantir a conexão com o servidor
  • o botão Annuler é exibido para permitir que o usuário interrompa a conexão em andamento
  • todas as opções do menu são desativadas, exceto Quitter e Auteur

A sequência de eventos é então a seguinte:

  1. O usuário clicou no botão Annuler: o thread de conexão é interrompido e o menu é restaurado ao seu estado inicial. É indicado no registro de acompanhamento que a conexão foi encerrada pelo usuário.
  2. A solicitação de conexão é aceita pelo servidor. Em seguida, inicia-se o diálogo com o servidor para alterar a senha. As trocas desse diálogo são registradas na lista LstDialogue. Uma vez concluído o diálogo, a conexão com o servidor é encerrada e o menu do formulário é restaurado ao seu estado inicial.
  3. Enquanto o diálogo estiver ativo, o botão Cancelar permanece visível para permitir que o usuário encerre a conexão, caso deseje.
  4. Se, durante a comunicação, ocorrer algum erro, a conexão é encerrada e a causa do erro é exibida na lista de acompanhamento LstSuivi.
Menu Conexão/Sair

Essa opção encerra uma eventual conexão ativa com o servidor e encerra o aplicativo.

Menus RazSuivi e RazDialogue

Esvaziam, respectivamente, as listas LstSuivi e LstDialogue. Essas opções ficam desativadas quando as listas correspondentes estão vazias.

O botão Cancelar

Este botão, localizado na parte inferior do formulário, só aparece quando o cliente está se conectando ou já está conectado ao servidor. O botão Annuler permite que o usuário interrompa a comunicação com o servidor.

As listas de acompanhamento

A lista LstSuivi (5) acompanha a conexão. Ela indica os momentos-chave da conexão:

  • sua abertura pelo cliente

  • seu encerramento pelo servidor ou pelo cliente

  • todos os erros que possam ocorrer enquanto a conexão estiver ativa

A lista LstDialogue (6) acompanha o diálogo estabelecido entre o cliente e o servidor.

A opção Autor

Este menu abre uma janela chamada “Copyright”:

Image

Gerenciamento de erros

Os erros de comunicação são indicados na lista de acompanhamento 6, enquanto os relacionados ao diálogo cliente/servidor aparecem na lista de diálogo 7. Em caso de erro de conexão, o diálogo cliente/servidor é encerrado e o formulário volta ao seu estado inicial, pronto para uma nova conexão.

8.5.4.5. TRAVAIL A FAIRE

Executar o trabalho descrito anteriormente na forma de um aplicativo autônomo e, em seguida, de um applet.