Skip to content

9. Programação TCP/IP

9.1. Informações Gerais

9.1.1. Protocolos da Internet

Apresentamos aqui uma introdução aos protocolos de comunicação da Internet, também conhecidos como conjunto de protocolos TCP/IP (Transmission Control Protocol / Internet Protocol), cujo nome deriva dos dois protocolos principais. É aconselhável que o leitor tenha uma compreensão geral do funcionamento das redes e, em particular, dos protocolos TCP/IP, antes de se dedicar ao desenvolvimento de aplicações distribuídas.

O texto que se segue é uma tradução parcial de um texto encontrado no documento «LAN Workplace for DOS - Administrator's Guide» da NOVELL, um documento do início da década de 1990.

-----------------------------------

O conceito geral de criação de uma rede de computadores heterogéneos tem origem na investigação conduzida pela DARPA (Agência de Projetos de Investigação 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 comuniquem entre si. Estes protocolos foram testados numa rede chamada ARPAnet, que mais tarde se tornou a Internet. Os protocolos TCP/IP definem formatos e regras para a transmissão e receção que são independentes da arquitetura da rede e do hardware utilizado.

A rede concebida pela DARPA e gerida pelos protocolos TCP/IP é uma rede de comutação de pacotes. Uma rede deste tipo transmite informação pela rede em pequenos pedaços chamados pacotes. Assim, se um computador transmitir um ficheiro de grandes dimensões, este será dividido em pequenos pedaços que são enviados pela rede para serem reajuntados no destino. O TCP/IP define o formato destes pacotes, nomeadamente:

  • origem do pacote
  • destino
  • comprimento
  • tipo

9.1.2. O Modelo OSI

Os protocolos TCP/IP seguem geralmente o modelo de rede aberta conhecido como OSI (Modelo de Referência para Interconexão de Sistemas Abertos), definido pela ISO (Organização Internacional de Normalização). Este modelo descreve uma rede ideal onde a comunicação entre máquinas pode ser representada por um modelo de sete camadas:

Cada camada recebe serviços da camada abaixo dela e fornece os seus próprios serviços à camada acima dela. Suponha que duas aplicações localizadas em máquinas diferentes, A e B, queiram comunicar: fazem-no na camada de Aplicação. Não precisam de conhecer todos os detalhes de como a rede funciona: cada aplicação passa a informação que deseja transmitir para a camada abaixo dela: a camada de Apresentação. A aplicação, portanto, só precisa de conhecer as regras para interagir com a camada de Apresentação.

Assim que a informação chega à camada de Apresentação, é passada de acordo com outras regras para a camada de Sessão, e assim sucessivamente, até que a informação chegue ao meio físico e seja fisicamente transmitida para a máquina de destino. Aí, será submetida ao processo inverso daquele a que foi submetida na máquina de envio.

Em cada camada, o processo emissor responsável pelo envio da informação envia-a a um processo recetor na outra máquina pertencente à mesma camada que ele próprio. Faz-o de acordo com certas regras conhecidas como protocolo da camada. Temos, portanto, o seguinte diagrama de comunicação final:

As funções das diferentes camadas são as seguintes:

Física
Assegura a transmissão de bits através de um meio físico. Esta camada inclui equipamentos terminais de processamento de dados (DPTE), tais como terminais ou computadores, bem como equipamentos de terminação de circuitos de dados (DCTE), tais como moduladores/demoduladores, multiplexadores e concentradores. Os pontos-chave 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).
Ligação de dados
Oculta as características físicas da Camada Física. Deteta e corrige erros de transmissão.
Rede
Gere o caminho que a informação enviada pela rede deve seguir. Isto denomina-se encaminhamento: determinar a rota que a informação deve seguir para chegar ao seu destino.
Transporte
Permite a comunicação entre duas aplicações, enquanto as camadas anteriores apenas permitiam a comunicação entre máquinas. Um serviço prestado por esta camada pode ser a multiplexação: a camada de transporte pode utilizar uma única ligação de rede (de máquina para máquina) para transmitir dados pertencentes a várias aplicações.
Sessão
Esta camada fornece serviços que permitem a uma aplicação abrir e manter uma sessão de trabalho numa máquina remota.
Apresentação
Tem como objetivo padronizar a representação de dados entre diferentes máquinas. Assim, os dados provenientes da máquina A serão «formatados» pela camada de apresentação da máquina A de acordo com um formato padrão antes de serem enviados pela rede. Ao chegarem à camada de apresentação da máquina de destino B, que os reconhecerá graças ao seu formato padrão, serão reformatados de modo a que a aplicação na máquina B os possa reconhecer.
Aplicação
Neste nível, encontramos aplicações que estão geralmente próximas do utilizador, tais como o e-mail ou a transferência de ficheiros.

9.1.3. O Modelo TCP/IP

O modelo OSI é um modelo ideal que nunca foi totalmente concretizado. O conjunto de protocolos TCP/IP aproxima-se dele da seguinte forma:

Camada Física

Nas redes locais, utiliza-se geralmente a tecnologia Ethernet ou Token Ring. Aqui, iremos centrar-nos exclusivamente na tecnologia Ethernet.

Ethernet

Este é o nome dado a uma tecnologia de rede de área local com comutação de pacotes, inventada na Xerox PARC no início da década de 1970 e padronizada pela Xerox, Intel e Digital Equipment em 1978. A rede consiste fisicamente num cabo coaxial com aproximadamente 1,27 cm de diâmetro e até 500 m de comprimento. Pode ser estendida utilizando repetidores, com um máximo de dois repetidores a separar quaisquer duas máquinas. O cabo é passivo: todos os componentes ativos estão localizados nas máquinas ligadas ao cabo. Cada máquina está ligada ao cabo através de uma placa de acesso à rede que inclui:

  • um transceptor que deteta a presença de sinais no cabo e converte sinais analógicos em sinais digitais e vice-versa.
  • um acoplador que recebe sinais digitais do transceptor 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 de barramento: todas as máquinas estão ligadas ao mesmo cabo
  • Rede de difusão — Uma máquina transmissora envia informações pelo cabo com o endereço da máquina receptora. Todas as máquinas ligadas recebem então estas informações, e apenas o destinatário pretendido as retém.
  • O método de acesso é o seguinte: o transmissor que pretende transmitir escuta o cabo — deteta então se existe ou não uma onda portadora, cuja presença indicaria que está em curso uma transmissão. Esta é a técnica CSMA (Carrier Sense Multiple Access). Na ausência de uma onda portadora, um transmissor pode decidir transmitir por sua vez. Vários transmissores podem tomar essa decisão. Os sinais transmitidos misturam-se: isto é chamado de colisão. O transmissor deteta esta situação: enquanto transmite pelo cabo, também escuta o que está realmente a passar por ele. Se detetar que a informação que circula pelo cabo não é a que transmitiu, conclui que ocorreu uma colisão e interrompe a transmissão. Os outros transmissores que estavam a transmitir farão o mesmo. Cada um retomará a transmissão após um atraso aleatório, dependendo do transmissor individual. Esta técnica é chamada de CD (Collision Detect). O método de acesso é, portanto, chamado de CSMA/CD.
  • Endereçamento de 48 bits. Cada máquina possui um endereço, aqui designado por endereço físico, que está indicado na placa que a liga ao cabo. Este endereço é designado por 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 de rede
ICMP
(Protocolo de Mensagens de Controlo da Internet)
O ICMP facilita a comunicação entre o programa do protocolo IP numa máquina e o da outra máquina. É, portanto, um protocolo de troca de mensagens dentro do próprio protocolo IP.
ARP
(Protocolo de Resolução de Endereços)
mapeia o endereço de Internet de uma máquina para o seu endereço físico
RARP
(Protocolo de Resolução de Endereços Inverso)
mapeia o endereço físico de uma máquina para o seu endereço de Internet

Camadas de transporte/sessão

Esta camada inclui os seguintes protocolos:

TCP (Protocolo de Controlo de Transmissão)
Garante a entrega fiável de informações entre dois clientes
UDP (Protocolo de Datagrama do Utilizador)
Garante a entrega não fiável de informações entre dois clientes

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

Aqui encontram-se vários protocolos:

TELNET
Um emulador de terminal que permite que a máquina A se ligue à máquina B como um terminal
FTP (Protocolo de Transferência de Ficheiros)
Permite a transferência de ficheiros
TFTP (Protocolo Trivial de
)
permite a transferência de ficheiros
SMTP (Protocolo Simples de Transferência de Correio
)
permite a troca de mensagens entre utilizadores da rede
DNS (Sistema de Nomes de Domínio)
converte o nome de um computador no endereço de Internet desse computador
XDR (Representação de Dados Externos
)
Criado pela Sun Microsystems, especifica um padrão de representação de dados independente da máquina
RPC (Chamada de Procedimento Remoto)
Também definido pela Sun, trata-se de um protocolo de comunicação entre aplicações remotas, independente da camada de transporte. Este protocolo é importante: liberta o programador da necessidade de conhecer os detalhes da camada de transporte e torna as aplicações portáteis. Este protocolo baseia-se no protocolo XDR
NFS (Sistema de Ficheiros em Rede)
Também definido pela Sun, este protocolo permite que uma máquina «veja» o sistema de ficheiros de outra máquina. Baseia-se no protocolo RPC mencionado acima

9.1.4. Como funcionam os protocolos da Internet

As aplicações desenvolvidas no ambiente TCP/IP utilizam geralmente vários dos protocolos deste ambiente. Um programa de aplicação comunica com a camada mais elevada dos protocolos. Esta camada transmite a informação para a camada abaixo dela, e assim sucessivamente, até chegar ao meio físico. Aí, a informação é transferida fisicamente para a máquina de destino, onde passará novamente pelas mesmas camadas, desta vez na direção oposta, até chegar à aplicação destinada a receber a informação enviada. O diagrama seguinte mostra o percurso da informação:

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

  • A aplicação envia uma sequência de bytes para ser transmitida à camada de transporte.
  • A camada de transporte divide esta sequência de bytes em segmentos TCP e adiciona o número do segmento ao início de cada segmento. Os segmentos são passados para a camada de rede, que é regida pelo protocolo IP.
  • A camada IP cria um pacote que encapsula o segmento TCP recebido. No cabeçalho deste pacote, coloca os endereços de Internet das máquinas de origem e de destino. Determina também o endereço físico da máquina de destino. O pacote completo é passado para a camada de ligação de dados e física, ou seja, para a placa de rede que liga a máquina à rede física.
  • Aí, o pacote IP é, por sua vez, encapsulado numa trama física e enviado para o seu destino através do cabo.
  • Na máquina de destino, a Camada de Ligação de Dados e Física faz o oposto: desencapsula o pacote IP da trama física e o passa para a camada IP.
  • A camada IP verifica se o pacote é válido: calcula uma soma de verificação com base nos bits recebidos, que deve corresponder à soma de verificação no cabeçalho do pacote. Se não corresponder, o pacote é descartado.
  • Se o pacote for considerado válido, a camada IP desencapsula o segmento TCP nele contido e o passa para a camada de transporte.
  • A camada de transporte — a camada TCP no nosso exemplo — examina o número do segmento para garantir que os segmentos estão na ordem correta.
  • Calcula também uma soma de verificação para o segmento TCP. Se for considerada correta, a camada TCP envia um aviso de receção à máquina de origem; caso contrário, o segmento TCP é rejeitado.
  • Tudo o que resta à camada TCP é transmitir a parte de dados do segmento para a aplicação destinada a recebê-la na camada superior.

9.1.5. Endereçamento na Internet

Um de rede pode ser um computador, uma impressora inteligente, um servidor de ficheiros — qualquer coisa, na verdade, que possa comunicar utilizando protocolos TCP/IP. Cada nó tem um endereço físico com um formato que depende do tipo de rede. Numa rede Ethernet, o endereço físico é codificado em 6 bytes. Um endereço de rede X.25 é um número de 14 dígitos.

O endereço de Internet de um nó é um endereço lógico: é independente do hardware e da rede utilizada. É um endereço de 4 bytes que identifica tanto uma rede local como um nó nessa rede. O endereço de Internet é normalmente representado por quatro números — os valores dos quatro bytes — separados por um ponto. Assim, o endereço da máquina Lagaffe na Faculdade de Ciências de Angers é 193.49.144.1, e o da máquina Liny é 193.49.144.9. A partir daí, podemos deduzir que o endereço de Internet da rede local é 193.49.144.0. Podem existir até 254 nós nesta rede.

Como os endereços de Internet (endereços IP) são independentes da rede, uma máquina na rede A pode comunicar com uma máquina na rede B sem se preocupar com o tipo de rede em que se encontra: basta saber o seu endereço IP. O protocolo IP de cada rede trata da conversão entre endereço IP e endereço físico, em ambos os sentidos.

Todos os endereços IP devem ser únicos. Em França, o INRIA é responsável pela atribuição de endereços IP. Na verdade, esta organização atribui um endereço à sua rede local, por exemplo 193.49.144.0 para a rede da Faculdade de Ciências de Angers. O administrador desta rede pode então atribuir os endereços IP 193.49.144.1 a 193.49.144.254 conforme entender. Este endereço é geralmente armazenado num ficheiro específico em cada máquina ligada à rede.

9.1.5.1. Classes de endereços IP

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

  • o endereço de rede
  • o endereço de um nó nessa rede

Dependendo do tamanho destes dois campos, os endereços IP são divididos em 3 classes: classes A, B e C.

Classe A

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

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

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

O endereço de rede tem 7 bits e o endereço de host tem 24 bits. Por conseguinte, podem existir 127 redes de Classe A, cada uma contendo até 2²⁴ hosts.

Classe B

Aqui, o endereço IP: I1.I2.I3.I4 tem a forma R1.R2.N1.N2, em que

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

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

O endereço de rede tem 2 bytes (exatamente 14 bits), tal como o endereço do nó. Por conseguinte, podem existir 2¹⁴ redes de Classe B, cada uma contendo até 2¹⁶ nós.

Classe C

Nesta classe, o endereço IP: I1.I2.I3.I4 tem a forma R1.R2.R3.N1, em que

R1.R2.R3
é o endereço de rede
N1
é o endereço de uma máquina nessa rede

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

O endereço de rede ocupa 3 bytes (menos 3 bits) e o endereço do host ocupa 1 byte. Por conseguinte, podem existir 2²¹ redes de Classe C, cada uma contendo até 256 hosts.

Uma vez que o endereço da máquina Lagaffe na Faculdade de Ciências de Angers é 193.49.144.1, vemos que o byte mais significativo é 193, que é 11000001 em binário. Podemos deduzir que a rede é uma rede de Classe C.

Endereços reservados

  • Alguns endereços IP são endereços de rede, em vez de endereços de nó dentro da rede. Estes são aqueles em que o endereço do nó está 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ó numa rede pode ter o endereço zero.
  • Quando o endereço de nó num endereço IP é composto inteiramente por 1s, trata-se de um endereço de difusão: este endereço refere-se a todos os nós da rede.
  • Numa rede de Classe C, que teoricamente permite 2⁸ = 256 nós, se retirarmos os dois endereços proibidos, ficamos apenas com 254 endereços autorizados.

9.1.5.2. Protocolos de conversão de endereços de Internet <--> endereços físicos

Vimos que, quando os dados são transmitidos de uma máquina para outra, são encapsulados em pacotes à medida que passam pela camada IP. Estes pacotes têm a seguinte forma:

O pacote IP contém, portanto, os endereços de Internet das máquinas de origem e de destino. Quando este pacote é transmitido para a camada responsável pelo seu envio através da rede física, são adicionadas informações adicionais para formar a trama física que será, em última instância, enviada pela rede. Por exemplo, o formato de uma trama numa rede Ethernet é o seguinte:

No quadro final, encontram-se os endereços físicos das máquinas de origem e de destino. Como são obtidos?

A máquina remetente, sabendo o endereço IP da máquina com a qual pretende comunicar, obtém o endereço físico dessa máquina utilizando um protocolo específico chamado ARP (Address Resolution Protocol).

  • Envia um tipo especial de pacote chamado pacote ARP, contendo o endereço IP da máquina cujo endereço físico está a ser procurado. Também inclui o seu próprio endereço IP e endereço físico no pacote.
  • Este pacote é enviado para todos os nós da rede.
  • Estes nós reconhecem a natureza especial do pacote. O nó que reconhece o seu endereço IP no pacote responde enviando o seu endereço físico ao remetente do pacote. Como é que consegue fazer isto? Encontrou os endereços IP e físicos do remetente no pacote.
  • O remetente recebe assim o endereço físico que procurava. Armazena-o na memória para poder utilizá-lo mais tarde, caso seja necessário enviar outros pacotes para o mesmo destinatário.

O endereço IP de uma máquina é normalmente armazenado num dos seus ficheiros de configuração, que pode ser consultado para o recuperar. Este endereço pode ser alterado: basta editar o ficheiro. O endereço físico, no entanto, é armazenado na memória da placa de rede e não pode ser alterado.

Quando um administrador deseja reorganizar a rede, pode ser necessário alterar os endereços IP de todos os nós e, assim, editar os vários ficheiros de configuração de cada nó. Isto pode ser tedioso e propenso a erros se houver muitas máquinas. Um método envolve não atribuir um endereço IP às máquinas: em vez disso, é inserido um código especial no ficheiro onde a máquina normalmente encontraria o seu endereço IP. Ao descobrir que não tem endereço IP, a máquina solicita um usando um protocolo chamado RARP (Reverse Address Resolution Protocol). Em seguida, envia um pacote especial chamado pacote RARP pela rede — semelhante ao pacote ARP anterior — contendo o seu endereço físico. Este pacote é enviado a todos os nós que reconhecem um pacote RARP. Um deles, chamado servidor RARP, mantém um ficheiro contendo os mapeamentos de endereço físico <--> endereço IP para todos os nós. Em seguida, responde ao remetente do pacote RARP, devolvendo o seu endereço IP. Um administrador que pretenda reconfigurar a sua rede precisa, portanto, apenas de editar o ficheiro de mapeamento do servidor RARP. O servidor RARP deve normalmente ter um endereço IP fixo que deve ser capaz de conhecer sem ter de utilizar o próprio protocolo RARP.

9.1.6. A camada de rede, conhecida como camada do Protocolo de Internet (IP)

O IP (Protocolo de Internet) define o formato que os pacotes devem assumir e como devem ser tratados durante a transmissão ou receção. Este tipo específico de pacote é chamado de datagrama IP. Já o discutimos:

O ponto importante é que, além dos dados a serem transmitidos, o datagrama IP contém os endereços de Internet das máquinas de origem e de destino. Assim, a máquina receptora sabe quem lhe está a enviar uma mensagem.

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 é fixado pelo software e, por isso, será o mesmo em diferentes redes físicas. Vimos que, à medida que descemos da camada de rede para a camada física, o datagrama IP é encapsulado num quadro físico. Apresentámos o exemplo do quadro físico de uma rede Ethernet:

Os quadros físicos viajam de nó em nó em direção ao seu destino, que pode não estar na mesma rede física que a máquina emissora. O pacote IP pode, portanto, ser sucessivamente encapsulado em diferentes quadros físicos nos nós que ligam duas redes de tipos diferentes. Também é possível que o pacote IP seja demasiado grande para ser encapsulado num único quadro físico. O software IP no nó onde este problema ocorre divide então o pacote IP em fragmentos de acordo com regras específicas, sendo cada um deles enviado pela rede física. Só serão reagrupados no seu destino final.

9.1.6.1. Roteamento

O encaminhamento é o método de direcionar pacotes IP para o seu destino. Existem dois métodos: encaminhamento direto e encaminhamento indireto.

Roteamento direto

O encaminhamento direto refere-se à transmissão de um pacote IP diretamente do remetente para o destinatário dentro da mesma rede:

  • A máquina que envia um datagrama IP possui o endereço IP do destinatário.
  • Obtém o endereço físico do destinatário através do protocolo ARP ou a partir das suas tabelas, caso esse endereço já tenha sido obtido.
  • Envia o pacote pela rede para esse endereço físico.

Roteamento indireto

O encaminhamento indireto refere-se à transmissão de um pacote IP para um destino localizado numa rede diferente daquela a que o remetente pertence. Neste caso, as partes do 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 isso. Em seguida, envia o pacote para um nó especial chamado router, um nó que liga uma rede local a outras redes e cujo endereço IP encontra nas suas tabelas — um endereço inicialmente obtido a partir de um ficheiro, da memória permanente ou através de informações que circulam na rede.

Um router está ligado a duas redes e possui um endereço IP em ambas as redes.

No nosso exemplo acima:

  • A Rede n.º 1 tem o endereço de Internet 193.49.144.0 e a Rede n.º 2 tem o endereço 193.49.145.0.
  • Dentro da rede n.º 1, o router 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 router é pegar no pacote IP que recebe — que está contido num quadro físico típico da rede n.º 1 — e colocá-lo num quadro físico que possa viajar pela rede n.º 2. Se o endereço IP do destino do pacote estiver na rede n.º 2, o router enviará o pacote diretamente para lá; caso contrário, enviá-lo-á para outro router, ligando a rede n.º 2 à rede n.º 3, e assim sucessivamente.

9.1.6.2. Mensagens de erro e controlo

Também dentro da camada de rede — no mesmo nível que o protocolo IP — está o ICMP (Internet Control Message Protocol). É utilizado para enviar mensagens relativas ao funcionamento interno da rede: nós com falha, congestionamento num router, etc. As mensagens ICMP são encapsuladas em pacotes IP e enviadas pela rede. As camadas IP dos vários nós tomam as medidas adequadas com base nas mensagens ICMP que recebem. Assim, uma aplicação em si nunca vê estas questões específicas da rede. Um nó utilizará as informações ICMP para atualizar as suas tabelas de encaminhamento.

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

9.1.7.1. O protocolo UDP: Protocolo de Datagrama do Utilizador

O protocolo UDP permite uma troca de dados não fiável entre dois pontos, o que significa que a entrega bem-sucedida de um pacote no seu destino não é garantida. A aplicação, se assim o desejar, pode lidar com isto por si própria, por exemplo, aguardando uma confirmação após enviar uma mensagem antes de enviar a seguinte.

Até agora, ao nível da rede, discutimos endereços IP de máquinas. No entanto, numa única máquina, podem coexistir simultaneamente diferentes processos, todos os quais podem comunicar. Por isso, ao enviar uma mensagem, é necessário especificar não só o endereço IP da máquina de destino, mas também o «nome» do processo de destino. Este nome é, na verdade, um número, chamado número de porta. Certos números estão reservados para aplicações padrão: a porta 69 para a aplicação TFTP (Trivial File Transfer Protocol), por exemplo. Os pacotes tratados pelo protocolo UDP também são chamados datagramas. Têm a seguinte forma:

Estes datagramas serão encapsulados em pacotes IP e, posteriormente, em quadros físicos.

9.1.7.2. O Protocolo TCP: Protocolo de Controlo de Transmissão

Para comunicações seguras, o protocolo UDP é insuficiente: o programador da aplicação deve criar ele próprio um protocolo para garantir que os pacotes sejam encaminhados corretamente.

O TCP (Protocolo de Controlo de Transmissão) evita estes problemas. As suas características são as seguintes:

  • O processo que pretende enviar dados estabelece primeiro uma ligação com o processo que irá receber os dados. Esta ligação é estabelecida entre uma porta na máquina de envio e uma porta na máquina de receção. É assim criado um caminho virtual entre as duas portas, que é reservado exclusivamente para os dois processos que estabeleceram a ligação.
  • Todos os pacotes enviados pelo processo de origem seguem este 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.
  • Os dados transmitidos parecem contínuos. O processo de envio envia dados ao seu próprio ritmo. Estes dados não são necessariamente enviados imediatamente: o protocolo TCP aguarda até ter dados suficientes para enviar. São armazenados numa estrutura denominada segmento TCP. Assim que este segmento estiver cheio, é transmitido para a camada IP, onde é encapsulado num pacote IP.
  • Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP de receção verifica se está a receber os segmentos em sequência. Por cada segmento recebido corretamente, envia um aviso de receção ao remetente.
  • Quando o remetente recebe esta confirmação, notifica o processo de envio. O processo de envio pode assim confirmar que um segmento chegou em segurança, o que não era possível com o protocolo UDP.
  • Se, após um determinado período de tempo, o protocolo TCP que enviou um segmento não receber um aviso de receção, retransmite o segmento em questão, garantindo assim a qualidade do serviço de entrega de informação.
  • O circuito virtual estabelecido entre os dois processos em comunicação é full-duplex: isto significa que a informação pode circular em ambos os sentidos. Assim, o processo de destino pode enviar confirmações mesmo enquanto o processo de origem continua a enviar informação. Isto permite, por exemplo, que o protocolo TCP de origem envie vários segmentos sem esperar por uma confirmação. Se, após um determinado período de tempo, perceber que não recebeu um aviso de receção para um segmento específico n.º n, retomará o envio de segmentos a partir desse ponto.

9.1.8. A Camada de Aplicação

Acima dos protocolos UDP e TCP, existem vários protocolos padrão:

TELNET

Este protocolo permite que um utilizador na máquina A da rede se ligue à máquina B (frequentemente designada por máquina anfitriã). O TELNET emula um chamado terminal universal na máquina A. O utilizador comporta-se, assim, como se tivesse um terminal ligado à máquina B. O Telnet baseia-se no protocolo TCP.

FTP: (Protocolo de Transferência de Ficheiros)

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

TFTP: (Protocolo Trivial de Transferência de Ficheiros)

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

DNS: (Sistema de Nomes de Domínio)

Quando um utilizador pretende trocar ficheiros com uma máquina remota, por exemplo, através do FTP, tem de conhecer o endereço de Internet dessa máquina. Por exemplo, para utilizar o FTP na máquina Lagaffe da Universidade de Angers, teria de executar o FTP da seguinte forma: FTP 193.49.144.1

Isto requer um diretório que mapeie máquinas para endereços IP. Neste diretório, as máquinas seriam provavelmente designadas por nomes simbólicos, tais como:

Máquina DPX2/320 na Universidade de Angers

Máquina Sun no ISERPA em Angers

É evidente que seria mais conveniente referir-se a uma máquina por um nome do que pelo seu endereço IP. Isto levanta a questão da exclusividade dos nomes: existem milhões de máquinas interligadas. Poder-se-ia imaginar um organismo centralizado a atribuir os nomes. Isso seria, sem dúvida, bastante complicado. O controlo sobre os nomes foi, de facto, distribuído por domínios. Cada domínio é gerido por uma organização geralmente muito enxuta, que tem total liberdade na escolha dos nomes das máquinas. Assim, as máquinas em França pertencem ao domínio fr, que é gerido pelo Inria em Paris. Para simplificar ainda mais as coisas, o controlo é distribuído ainda mais: são criados domínios dentro do domínio fr. Assim, a Universidade de Angers pertence ao domínio univ-Angers. O departamento que gere este domínio tem total liberdade para nomear as máquinas na rede da Universidade de Angers. Por enquanto, este domínio não foi subdividido. Mas numa grande universidade com muitas máquinas em rede, isso poderia acontecer.

A máquina DPX2/320 da Universidade de Angers foi batizada de Lagaffe, enquanto um PC 486DX50 recebeu o nome de liny. Como se faz referência a estas máquinas a partir do exterior? Especificando a hierarquia de domínios a que pertencem. Assim, o nome completo da máquina Lagaffe será:

Lagaffe.univ-Angers.fr

Dentro dos domínios, podem ser utilizados nomes relativos. Assim, dentro do domínio fr e fora do domínio univ-Angers, a máquina Lagaffe pode ser referenciada como

Lagaffe.univ-Angers

Por fim, dentro do domínio univ-Angers, pode ser referenciada simplesmente como

Lagaffe

Uma aplicação pode, portanto, referir-se a uma máquina pelo seu nome. No entanto, em última análise, ainda é necessário obter o endereço IP da máquina. Como é que isso se faz? Suponhamos que, a partir da máquina A, queremos comunicar com a máquina B.

  • Se a máquina B pertencer ao mesmo domínio que a máquina A, o seu endereço IP provavelmente estará num ficheiro na máquina A.
  • Caso contrário, a máquina A encontrará, noutro ficheiro ou no mesmo de antes, uma lista de vários servidores de nomes com os seus endereços IP. Um servidor de nomes é responsável por mapear o nome de uma máquina para o seu endereço IP. A máquina A enviará um pedido especial ao primeiro servidor de nomes da sua lista, conhecido como consulta DNS, que inclui o nome da máquina procurada. Se o servidor consultado tiver esse nome nos seus registos, enviará o endereço IP correspondente à máquina A. Caso contrário, o servidor também encontrará nos seus ficheiros uma lista de servidores de nomes que pode consultar. E assim o fará. Assim, serão consultados vários servidores de nomes, não de forma aleatória, mas de uma forma que minimize o número de pedidos. Se a máquina for finalmente encontrada, a resposta será enviada de volta à máquina A.

XDR: (eXternal Data Representation)

Criado pela Sun Microsystems, este protocolo especifica uma representação de dados padrão e independente da máquina.

RPC: (Chamada de Procedimento Remoto)

Também definido pela Sun, este é um protocolo de comunicação entre aplicações remotas, independente da camada de transporte. Este protocolo é importante: dispensa o programador de conhecer os detalhes da camada de transporte e torna as aplicações portáteis. Este protocolo baseia-se no protocolo XDR

NFS: Sistema de Ficheiros em Rede

Também definido pela Sun, este protocolo permite que uma máquina «veja» o sistema de ficheiros de outra máquina. Baseia-se no protocolo RPC mencionado acima.

9.1.9. Conclusão

Nesta introdução, apresentámos algumas das principais características dos protocolos da Internet. Para aprofundar este tema, os leitores podem consultar o excelente livro de Douglas Comer:

Título
TCP/IP: Arquitetura, Protocolos, Aplicações.
Autor
Douglas COMER
Editora
InterEditions

9.2. Gestão de endereços de rede

Uma máquina na Internet é identificada de forma única por um endereço IP (Protocolo de Internet) no formato I1.I2.I3.I4, em que I1 é um número entre 1 e 254. Também pode ser identificada por um nome único. Este nome não é obrigatório, uma vez que as aplicações acabam sempre por utilizar os endereços IP das máquinas. Estes nomes existem para facilitar a vida aos utilizadores. Assim, é mais fácil utilizar um navegador para solicitar o URL http://www.ibm.com do que o URL http://129.42.17.99, embora ambos os métodos sejam possíveis. O mapeamento entre endereço IP e nome da máquina é tratado por um serviço de Internet distribuído chamado DNS (Sistema de Nomes de Domínio). A plataforma .NET fornece a classe Dns para gerir endereços de Internet:

Image

A maioria dos métodos da classe é estática. Vejamos os que nos interessam:

Sobrecargas Função Pública Partilhada
 GetHostByAddress(ByVal address As
 String) As IPHostEntry
Retorna um IPHostEntry a partir de um endereço IP no formato "I1.I2.I3.I4". Lança uma exceção se o endereço da máquina não for encontrado.
Função Pública Partilhada
 GetHostByName(ByVal hostName As
 String) As IPHostEntry
retorna um IPHostEntry a partir de um nome de máquina. Lança uma exceção se o nome da máquina não for encontrado.
Função Pública Partilhada
GetHostName() As String
retorna o nome da máquina na qual o programa que executa esta instrução está a ser executado

Os endereços de rede IPHostEntry têm o seguinte formato:

As propriedades que nos interessam:

Propriedade pública AddressList
Como IPAddress ()
Lista de endereços IP de uma máquina. Embora um endereço IP identifique uma e apenas uma máquina física, uma máquina física pode ter vários endereços IP. É o que acontece se tiver várias placas de rede que a ligam a redes diferentes.
Propriedade pública Aliases
Como String ()
Lista de aliases para uma máquina, que pode ser identificada por um nome principal e aliases
Propriedade pública HostName
Como String
O nome da máquina, caso exista

Da classe IPAddress, utilizaremos o seguinte construtor, propriedades e métodos:

Image

Um objeto [IPAddress] pode ser convertido para a cadeia de caracteres I1.I2.I3.I4 utilizando o método ToString(). Por outro lado, um objeto IPAddress pode ser obtido a partir da cadeia de caracteres I1.I2.I3.I4 utilizando o método estático IPAddress.Parse("I1.I2.I3.I4"). Considere o seguinte programa, que apresenta o nome da máquina na qual está a ser executado e, em seguida, fornece de forma interativa as correspondências entre endereços IP e nomes de máquinas:

dos>address1
Machine Locale=tahe

Machine recherchée (fin pour arrêter) : istia.univ-angers.fr
Machine : istia.univ-angers.fr
Adresses IP : 193.49.146.171

Machine recherchée (fin pour arrêter) : 193.49.146.171
Machine : istia.istia.univ-angers.fr
Adresses IP : 193.49.146.171
Alias : 171.146.49.193.in-addr.arpa

Machine recherchée (fin pour arrêter) : www.ibm.com
Machine : www.ibm.com
Adresses IP : 129.42.17.99,129.42.18.99,129.42.19.99,129.42.16.99

Machine recherchée (fin pour arrêter) : 129.42.17.99
Machine : www.ibm.com
Adresses IP : 129.42.17.99

Machine recherchée (fin pour arrêter) : x.y.z
Impossible de trouver la machine [x.y.z]

Machine recherchée (fin pour arrêter) : localhost
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : 127.0.0.1
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : tahe
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : fin

O programa é o seguinte:


' options
Option Explicit On 
Option Strict On
 
' namespaces
Imports System
Imports System.Net
Imports System.Text.RegularExpressions
 
' test module
Public Module adresses
 
    Sub Main()
        ' displays the name of the local machine
        ' then interactively provides information on network machines
        ' identified by name or address IP
        ' local machine
        Dim localHost As String = Dns.GetHostName()
        Console.Out.WriteLine(("Machine Locale=" + localHost))
 
        ' interactive Q&A
        Dim machine As String
        Dim adresseMachine As IPHostEntry
        While True
            ' enter the name of the machine you are looking for
            Console.Out.Write("Machine recherchée (fin pour arrêter) : ")
            machine = Console.In.ReadLine().Trim().ToLower()
            ' finished?
            If machine = "fin" Then
                Exit While
            End If
 
            ' address I1.I2.I3.I4 or machine name?
            Dim isIPV4 As Boolean = Regex.IsMatch(machine, "^\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s*$")
            ' management exception
            Try
                If isIPV4 Then
                    adresseMachine = Dns.GetHostByAddress(machine)
                Else
                    adresseMachine = Dns.GetHostByName(machine)
                End If
                ' the name
                Console.Out.WriteLine(("Machine : " + adresseMachine.HostName))
                ' IP addresses
                Console.Out.Write(("Adresses IP : " + adresseMachine.AddressList(0).ToString))
                Dim i As Integer
                For i = 1 To adresseMachine.AddressList.Length - 1
                    Console.Out.Write(("," + adresseMachine.AddressList(i).ToString))
                Next i
                Console.Out.WriteLine()
                ' aliases
                If adresseMachine.Aliases.Length <> 0 Then
                    Console.Out.Write(("Alias : " + adresseMachine.Aliases(0)))
                    For i = 1 To adresseMachine.Aliases.Length - 1
                        Console.Out.Write(("," + adresseMachine.Aliases(i)))
                    Next i
                    Console.Out.WriteLine()
                End If
            Catch
                ' the machine doesn't exist
                Console.Out.WriteLine("Impossible de trouver la machine [" + machine + "]")
            End Try
        End While
    End Sub
End Module

9.3. Programação TCP/IP

9.3.1. Informações gerais

Considere a comunicação entre duas máquinas remotas A e B:

Quando uma aplicação AppA na máquina A pretende comunicar com uma aplicação AppB na máquina B através da Internet, tem de saber várias coisas:

  • o endereço IP ou nome de host da máquina B
  • o número da porta utilizada pela aplicação AppB. Com efeito, a máquina B pode hospedar muitas aplicações em execução na Internet. Quando recebe informações da rede, deve saber a que aplicação se destinam essas informações. As aplicações na máquina B acedem à rede através de interfaces também conhecidas como portas de comunicação. Esta informação está contida no pacote recebido pela máquina B, para que possa ser entregue à aplicação correta.
  • Os protocolos de comunicação compreendidos pela máquina B. No nosso estudo, utilizaremos apenas protocolos TCP-IP.
  • O protocolo de comunicação aceite pela aplicação AppB. Com efeito, as máquinas A e B irão «comunicar» entre si. O que dizem será encapsulado nos protocolos TCP/IP. No entanto, quando, no final da cadeia, a aplicação AppB recebe a informação enviada pela aplicação AppA, tem de ser capaz de a interpretar. Isto é análogo à situação em que duas pessoas, A e B, comunicam por telefone: a sua conversa é transportada pelo telefone. A fala é codificada como sinais pelo telefone A, transmitida através das linhas telefónicas e chega ao telefone B para ser descodificada. A pessoa B ouve então as palavras. É aqui que entra em jogo o conceito de protocolo de comunicação: se A falar francês e B não compreender essa língua, A e B não conseguirão comunicar eficazmente.

Por conseguinte, as duas aplicações em comunicação devem chegar a acordo quanto ao tipo de protocolo de comunicação que irão utilizar. Por exemplo, a comunicação com um serviço FTP não é a mesma que com um serviço POP: estes dois serviços não aceitam os mesmos comandos. Têm protocolos de comunicação diferentes.

9.3.2. Características do Protocolo TCP

Aqui, iremos examinar apenas as comunicações de rede que utilizam o protocolo de transporte TCP. Vamos rever as suas características:

  • O processo que pretende transmitir dados estabelece primeiro uma ligação com o processo que irá receber a informação que está prestes a transmitir. Esta ligação é estabelecida entre uma porta na máquina emissora e uma porta na máquina recetora. É assim criado um caminho virtual entre as duas portas, que será reservado exclusivamente para os dois processos que estabeleceram a ligação.
  • Todos os pacotes enviados pelo processo de origem seguem este caminho virtual e chegam na ordem em que foram enviados
  • Os dados transmitidos parecem contínuos. O processo de envio envia dados ao seu próprio ritmo. Estes dados não são necessariamente enviados imediatamente: o protocolo TCP aguarda até ter dados suficientes para enviar. São armazenados numa estrutura denominada segmento TCP. Assim que este segmento estiver cheio, é transmitido para a camada IP, onde é encapsulado num pacote IP.
  • Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP de receção verifica se está a receber os segmentos em sequência. Por cada segmento recebido corretamente, envia um aviso de receção ao remetente.
  • Quando o remetente recebe esta confirmação, notifica o processo de envio. O processo de envio pode assim confirmar que um segmento chegou em segurança.
  • Se, após um determinado período de tempo, o protocolo TCP que enviou um segmento não receber um aviso de receção, retransmite o segmento em questão, garantindo assim a qualidade do serviço de entrega de informação.
  • O circuito virtual estabelecido entre os dois processos em comunicação é full-duplex: isto significa que a informação pode circular em ambos os sentidos. Assim, o processo de destino pode enviar confirmações mesmo enquanto o processo de origem continua a enviar informação. Isto permite, por exemplo, que o protocolo TCP de origem envie vários segmentos sem esperar por uma confirmação. Se, após um determinado período de tempo, perceber que não recebeu um aviso de receção para um segmento específico n.º n, retomará o envio de segmentos a partir desse ponto.

9.3.3. A relação cliente-servidor

A comunicação pela Internet é frequentemente assimétrica: a máquina A inicia uma ligação para solicitar um serviço à máquina B, especificando que pretende abrir uma ligação com o serviço SB1 na máquina B. A máquina B aceita ou recusa. Se aceitar, a máquina A pode enviar os seus pedidos ao serviço SB1. Estes pedidos devem estar em conformidade com o protocolo de comunicação compreendido pelo serviço SB1. Estabelece-se assim um diálogo de pedido-resposta entre a máquina A, conhecida como máquina cliente, e a máquina B, conhecida como máquina servidor. Um dos dois parceiros encerrará a ligação.

9.3.4. Arquitetura do Cliente

A arquitetura de um programa de rede que solicita os serviços de uma aplicação de 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

9.3.5. Arquitetura do 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 o pedido de ligação inicial de um cliente de forma diferente dos seus pedidos de serviço subsequentes. O programa não presta o serviço propriamente dito. Se o fizesse, deixaria de estar à escuta de pedidos de ligação enquanto o serviço estivesse em curso, e os clientes não seriam atendidos. Por isso, procede de forma diferente: assim que um pedido de ligação é recebido na porta de escuta e depois aceite, o servidor cria uma tarefa responsável por prestar o serviço solicitado pelo cliente. Este serviço é prestado noutra porta da máquina do servidor, denominada porta de serviço. Isto permite que vários clientes sejam atendidos 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

9.3.6. A classe TcpClient

A classe TcpClient é a classe adequada para representar um cliente de serviço TCP. É definida da seguinte forma:

Image

Os construtores, métodos e propriedades que nos interessam são os seguintes:

Public Sub New(ByVal hostname
 As String, ByVal port As Integer)
cria uma ligação TCP com o servidor em execução na porta especificada (port) da máquina especificada (hostname). Por exemplo, new TcpClient("istia.univ-angers.fr", 80) para se ligar à porta 80 da máquina istia.univ-angers.fr
Public Sub Close()
fecha a ligação ao servidor TCP
Public Function GetStream()
 Como NetworkStream
Obtém um NetworkStream para leitura e gravação no servidor. Este fluxo permite a comunicação cliente-servidor.

9.3.7. A classe NetworkStream

A classe NetworkStream representa o fluxo de rede entre o cliente e o servidor. A classe é definida da seguinte forma:

Image

A classe NetworkStream deriva da classe Stream. Muitas aplicações cliente-servidor trocam linhas de texto terminadas pelos caracteres de fim de linha "\r\n". Por isso, é útil utilizar objetos StreamReader e StreamWriter para ler e escrever essas linhas no fluxo de rede. Quando duas máquinas comunicam entre si, existe um objeto TcpClient em cada extremidade da ligação. O método GetStream deste objeto fornece acesso ao fluxo de rede (NetworkStream) que liga as duas máquinas. Assim, se uma máquina M1 tiver estabelecido uma ligação com uma máquina M2 utilizando um objeto TcpClient client1 e estiverem a trocar linhas de texto, pode criar os seus fluxos de leitura e escrita da seguinte forma:

Dim in1 as StreamReader=new StreamReader(client1.GetStream())
Dim out1 as StreamWriter=new StreamWriter(client1.GetStream())
out1.AutoFlush=true

A instrução

out1.AutoFlush=true

significa que o fluxo de escrita do cliente1 não passará por um buffer intermédio, mas irá diretamente para a rede. Este ponto é importante. Geralmente, quando o cliente1 envia uma linha de texto ao seu parceiro, espera uma resposta. Esta resposta nunca chegará se a linha tiver sido efetivamente armazenada em buffer na máquina M1 e nunca enviada. Para enviar uma linha de texto à máquina M2, escrevemos:

client1.WriteLine("un texte")

Para ler a resposta da M2, escrevemos:

Dim réponse as String=client1.ReadLine()

9.3.8. Arquitetura básica de um cliente de Internet

Temos agora os elementos para escrever a arquitetura básica de um cliente de Internet:


    Dim client As TcpClient = Nothing     ' the customer
    Dim [IN] As StreamReader = Nothing ' the customer's reading flow
    Dim OUT As StreamWriter = Nothing     ' the customer's writing flow
    Dim demande As String = Nothing         ' customer request
    Dim réponse As String = Nothing         ' server response
 
    Try
      ' connect to the service running on port P of machine M
      client = New TcpClient(nomServeur, port)
 
      ' create customer input/output flows TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True
 
      ' request-response loop
      While True
        ' preparing the application
        demande = ...
        ' we send it to the server
        OUT.WriteLine(demande)
        ' we read the server response
        réponse = [IN].ReadLine()
        ' the answer is processed
        ...
            End While
            ' it's over
            client.Close()
        Catch ex As Exception
      ' we handle the exception
...
        End Try

9.3.9. A classe TcpListener

A classe TcpListener é a classe adequada para representar um serviço TCP. Está definida da seguinte forma:

Image

Os construtores, métodos e propriedades que nos interessam são os seguintes:

Public Sub New(ByVal localaddr
As IPAddress, ByVal port As Integer)
cria um serviço TCP que irá escutar pedidos de clientes numa porta passada como parâmetro (port), conhecida como a porta de escuta da máquina local com endereço IP localaddr.
Função Pública AcceptTcpClient()
As TcpClient
aceita um pedido de cliente. Devolve um objeto TcpClient associado a outra porta, denominada porta de serviço.
Sub pública Start()
Inicia a escuta de pedidos do cliente
Public Sub Stop()
Interrompe a escuta de pedidos do cliente

9.3.10. Arquitetura básica de um servidor de Internet

A partir do que vimos anteriormente, podemos deduzir a estrutura básica de um servidor:


    ' we create the listening service
    Dim ecoute As TcpListener = Nothing
    Dim port As Integer = ...
    Try
      ' create the service
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' launch it
      ecoute.Start()
      ' service loop
      Dim liaisonClient As TcpClient = Nothing
            While not fini
                ' waiting for a customer
                liaisonClient = ecoute.AcceptTcpClient()
                ' the service is provided by another task
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf [méthode]))
                tache.Start()
            End While
        Catch ex As Exception
            ' we report the error
....
        End Try
        ' end of service
        ecoute.Stop()

A classe Service é um segmento de execução que pode ter o seguinte aspeto:


Public Class Service
 
    Private liaisonClient As TcpClient    ' customer liaison
    Private [IN] As StreamReader    ' iNPUTS
    Private OUT As StreamWriter    ' output flow
 
    ' manufacturer
    Public Sub New(ByVal liaisonClient As TcpClient, ...)
        Me.liaisonClient = liaisonClient
        ...
    End Sub
 
    ' run method
    Public Sub Run()
        ' renders service to the customer
        Try
            ' iNPUTS
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' output flow
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' loop read request/write response
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' we process demand
                ...
                ' we send the answer
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' following request
                demande = [IN].ReadLine
            End While
            ' end link
            liaisonClient.Close()
        Catch e As Exception
            ...
        End Try
        ' end of service
    End Sub

9.4. Exemplos

9.4.1. Servidor Echo

Vamos escrever um servidor de eco que será iniciado a partir de uma janela do DOS utilizando o comando:

serveurEcho port

O servidor funciona na porta passada como parâmetro. Ele simplesmente reenvia ao cliente o pedido que este lhe enviou. O programa é o seguinte:


' options
Option Explicit On 
Option Strict On
 
' namespaces
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic
 
' call: serveurEcho port
' echo server
' returns the line sent to the customer
 
Public Class serveurEcho
  Private Shared syntaxe As String = "Syntaxe : serveurEcho port"
 
  ' main program
  Public Shared Sub Main(ByVal args() As String)
 
    ' is there an argument
    If args.Length <> 1 Then
      erreur(syntaxe, 1)
    End If
    ' this argument must be integer >0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(0))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
    ' we create the listening service
    Dim ecoute As TcpListener = Nothing
    Dim nbClients As Integer = 0 ' of customers handled
    Try
      ' create the service
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' launch it
      ecoute.Start()
      ' follow-up
      Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & port))
      Console.Out.WriteLine(ecoute.LocalEndpoint)
 
      ' service loop
      Dim liaisonClient As TcpClient = Nothing
            While True
                ' infinite loop - will be stopped by Ctrl-C
                ' waiting for a customer
                liaisonClient = ecoute.AcceptTcpClient()
 
                ' the service is provided by another task
                nbClients += 1
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf New traiteClientEcho(liaisonClient, nbClients).Run))
                tache.Start()
            End While
            ' back to listening to requests
        Catch ex As Exception
            ' we report the error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
        ' end of service
        ecoute.Stop()
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class
 
' -------------------------------------------------------
' provides service to an echo server client
Public Class traiteClientEcho
 
    Private liaisonClient As TcpClient    ' customer liaison
    Private numClient As Integer    ' customer no
    Private [IN] As StreamReader    ' iNPUTS
    Private OUT As StreamWriter    ' output flow
 
    ' manufacturer
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal numClient As Integer)
        Me.liaisonClient = liaisonClient
        Me.numClient = numClient
    End Sub

    ' run method
    Public Sub Run()
        ' renders service to the customer
        Console.Out.WriteLine(("Début de service au client " & numClient))
        Try
            ' iNPUTS
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' output flow
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' loop read request/write response
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' follow-up
                Console.Out.WriteLine(("Client " & numClient & " : " & demande))
                ' the service stops when the client sends an end-of-file marker
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' service stops when client sends "end
                If demande.Trim().ToLower() = "fin" Then
                    Exit While
                End If
                ' following request
                demande = [IN].ReadLine
            End While
            ' end link
            liaisonClient.Close()
        Catch e As Exception
            erreur("Erreur lors de la fermeture de la liaison client (" + e.ToString + ")", 2)
        End Try
        ' end of service
        Console.Out.WriteLine(("Fin de service au client " & numClient))
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

A estrutura do servidor está em conformidade com a arquitetura geral dos servidores TCP.

9.4.2. Um cliente para o servidor de eco

Vamos agora escrever um cliente para o servidor anterior. Será chamado da seguinte forma:

clientEcho nomServeur port

Ele liga-se à máquina servername na porta port e, em seguida, envia linhas de texto para o servidor, que as repete de volta.


' options
Option Explicit On 
Option Strict On
 
' namespaces
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic
 
Public Class clientEcho
 
  ' connects to an echo server
  ' any line typed on the keyboard is then received as an echo
  Public Shared Sub Main(ByVal args() As String)
    ' syntax
    Const syntaxe As String = "pg machine port"
 
    ' number of arguments
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' note the server name
    Dim nomServeur As String = args(0)
 
    ' port must be integer >0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(1))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
 
        ' we can work
    Dim client As TcpClient = Nothing ' the customer
    Dim [IN] As StreamReader = Nothing ' the customer's reading flow
    Dim OUT As StreamWriter = Nothing ' the customer's writing flow
    Dim demande As String = Nothing ' customer request
    Dim réponse As String = Nothing ' server response
    Try
      ' connect to the service running on port P of machine M
      client = New TcpClient(nomServeur, port)
 
      ' create customer input/output flows TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True
 
      ' request-response loop
      While True
        ' demand comes from the keyboard
        Console.Out.Write("demande (fin pour arrêter) : ")
        demande = Console.In.ReadLine()
        ' we send it to the server
        OUT.WriteLine(demande)
        ' we read the server response
        réponse = [IN].ReadLine()
        ' the answer is processed
        Console.Out.WriteLine(("Réponse : " + réponse))
        ' finished?
        If demande.Trim().ToLower() = "fin" Then
          Exit While
                End If
            End While
            ' it's over
            client.Close()
        Catch ex As Exception
      ' we handle the exception
      erreur(ex.Message, 3)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

A estrutura deste cliente está em conformidade com a arquitetura geral dos clientes TCP. Aqui estão os resultados obtidos com a seguinte configuração:

  • O servidor está a ser executado na porta 100 numa janela do DOS
  • Na mesma máquina, são iniciados dois clientes em duas outras janelas do DOS

Na janela do Cliente 1, obtemos os seguintes resultados:

dos>clientEcho localhost 100
demande (fin pour arrêter) : ligne1
Réponse : [ligne1]
demande (fin pour arrêter) : ligne1B
Réponse : [ligne1B]
demande (fin pour arrêter) : ligne1C
Réponse : [ligne1C]
demande (fin pour arrêter) : fin
Réponse : [fin]

No cliente 2:

dos>clientEcho localhost 100
demande (fin pour arrêter) : ligne2A
Réponse : [ligne2A]
demande (fin pour arrêter) : ligne2B
Réponse : [ligne2B]
demande (fin pour arrêter) : fin
Réponse : [fin]

No lado do servidor:

dos>serveurEcho 100
Serveur d'écho lancé sur le port 100
0.0.0.0:100
Début de service au client 1
Client 1 : ligne1
Début de service au client 2
Client 2 : ligne2A
Client 2 : ligne2B
Client 1 : ligne1B
Client 1 : ligne1C
Client 2 : fin
Fin de service au client 2
Client 1 : fin
Fin de service au client 1
^C

Note-se que o servidor conseguiu, de facto, atender dois clientes simultaneamente.

9.4.3. Um cliente TCP genérico

Muitos serviços criados nos primórdios da Internet funcionam de acordo com o modelo de servidor de eco discutido anteriormente: as trocas cliente-servidor consistem na troca de linhas de texto. Vamos escrever um cliente TCP genérico que será iniciado da seguinte forma: cltgen servidor porta

Este cliente TCP irá ligar-se à porta port no servidor server. Uma vez ligado, irá criar duas threads:

  1. uma thread responsável por ler os comandos digitados no teclado e enviá-los para o servidor
  2. uma thread responsável por ler as respostas do servidor e exibi-las no ecrã

Porquê duas threads quando isso não era necessário na aplicação anterior? Nessa aplicação, o protocolo de comunicação era fixo: o cliente enviava uma única linha e o servidor respondia com uma única linha. Cada serviço tem o seu próprio protocolo específico, e também nos deparamos com as seguintes situações:

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

Por isso, o ciclo que envia uma única linha para o servidor e recebe uma única linha do servidor nem sempre é adequado. Vamos, portanto, criar dois ciclos separados:

  • um loop para ler os comandos digitados no teclado a serem enviados ao servidor. O utilizador sinalizará o fim dos comandos com a palavra-chave «fin».
  • um ciclo para receber e exibir as respostas do servidor. Este será um ciclo infinito que só será interrompido pelo servidor ao fechar a ligação de rede ou pelo utilizador ao digitar o comando «end» no teclado.

Para ter estes dois loops separados, precisamos de dois threads independentes. Vejamos um exemplo de execução em que o nosso cliente TCP genérico se liga a um serviço SMTP (Simple Mail Transfer Protocol). Este serviço é responsável pelo encaminhamento de e-mails aos destinatários. Opera na porta 25 e utiliza um protocolo de troca baseado em texto.

dos>cltgen 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 estas trocas cliente-servidor:

  • O serviço SMTP envia uma mensagem de boas-vindas quando um cliente se liga 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 têm um comando «help» que fornece informações sobre os comandos disponíveis para esse serviço. Não é esse o caso aqui. Os comandos SMTP utilizados no exemplo são os seguintes:
    • mail from: remetente, para especificar o endereço de e-mail do remetente
    • rcpt to: destinatário, para especificar o endereço de e-mail do destinatário da mensagem. Se houver vários destinatários, o comando rcpt to: é repetido tantas vezes quantas forem necessárias para cada destinatário.
    • data que sinaliza ao servidor SMTP que uma mensagem está prestes a ser enviada. Conforme indicado na resposta do servidor, estes dados consistem numa série de linhas que terminam com uma linha contendo apenas um ponto. Uma mensagem pode ter cabeçalhos separados do corpo da mensagem por uma linha em branco. No nosso exemplo, incluímos um assunto utilizando a palavra-chave Subject:
  • Assim que a mensagem for enviada, pode indicar ao servidor que terminou utilizando o comando quit. O servidor encerra então a ligação de rede. O segmento de leitura pode detetar este evento e parar.
  • O utilizador digita então «end» no teclado para também parar a thread que lê os comandos digitados no teclado.

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

Image

Note que o serviço SMTP não consegue detetar se um remetente é válido ou não. Por isso, nunca se pode confiar no campo «De» de uma mensagem. Aqui, o remetente machin@univ-angers.fr não existia. Este cliente TCP genérico permite-nos descobrir o protocolo de comunicação dos serviços da Internet e, a partir daí, criar classes especializadas para clientes desses serviços. Vamos explorar o protocolo de comunicação do serviço POP (Post Office Protocol), que permite aos utilizadores recuperar os seus e-mails armazenados num servidor. Funciona na porta 110.

dos>cltgen 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:

  • login do utilizador, onde introduz o seu nome de utilizador na máquina que aloja os seus e-mails
  • pass password, onde introduz a palavra-passe associada ao login anterior
  • list, para obter uma lista de mensagens no formato número, tamanho em bytes
  • retr i, para ler a mensagem número i
  • sair, para encerrar a sessão.

Agora vamos explorar o protocolo de comunicação entre um cliente e um servidor web, que normalmente funciona na porta 80:

dos>cltgen 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 os seus comandos para o servidor de acordo com o seguinte padrão:

commande1
commande2
...
commanden
[ligne vide]

O servidor web só responde após receber a linha vazia. Neste exemplo, utilizámos apenas um comando:

GET /index.html HTTP/1.0

que solicita a URL /index.html ao servidor e indica que está a utilizar a versão 1.0 do HTTP. A versão mais recente deste protocolo é a 1.1. O exemplo mostra que o servidor respondeu enviando o conteúdo do ficheiro index.html e, em seguida, encerrou a ligação, como podemos ver na thread de leitura da resposta a terminar. Antes de enviar o conteúdo do ficheiro index.html, o servidor web enviou uma série de cabeçalhos seguidos de uma linha vazia:

<-- 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 ficheiro /index.html. O texto que a precede denomina-se cabeçalhos HTTP (HyperText Transfer Protocol). Não vamos entrar em pormenores sobre estes cabeçalhos aqui, mas tenha em mente que o nosso cliente genérico permite aceder aos mesmos, o que pode ser útil para os compreender. Por exemplo, a primeira linha:

<-- HTTP/1.1 200 OK

indica que o servidor web contactado suporta o protocolo HTTP/1.1 e que encontrou com sucesso o ficheiro solicitado (200 OK), sendo que 200 é um código de resposta HTTP. As linhas

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

informa ao cliente que receberá 11 251 bytes de texto HTML (HyperText Markup Language) e que a ligação será encerrada assim que os dados forem enviados. Portanto, temos aqui um cliente TCP muito útil. Na verdade, este cliente já existe em máquinas onde é chamado de telnet, mas foi interessante escrevê-lo nós próprios. O programa genérico do cliente TCP é o seguinte:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic
 
' the class
Public Class clientTcpGénérique
   
    
   ' receives the characteristics of a service as a parameter in the form
   ' server port
   ' connects to the service
   ' creates a thread to read keyboard commands
   ' these will be sent to the
   ' creates a thread to read server responses
   ' these will be displayed on the screen
   ' the whole thing ends with the command end typed on the keyboard
  Public Shared Sub Main(ByVal args() As String)
 
    ' syntax
    Const syntaxe As String = "pg serveur port"

    ' number of arguments
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' note the server name
    Dim serveur As String = args(0)
 
    ' port must be integer >0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(1))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
    Dim client As TcpClient = Nothing
    ' there may be problems
    Try
      ' connect to the service
      client = New TcpClient(serveur, port)
    Catch ex As Exception
      ' error
      Console.Error.WriteLine(("Impossible de se connecter au service (" & serveur & "," & port & "), erreur : " & ex.Message))
      ' end
      Return
        End Try
        ' create read/write threads
        Dim thReceive As New Thread(New ThreadStart(AddressOf New clientReceive(client).Run))
        Dim thSend As New Thread(New ThreadStart(AddressOf New clientSend(client).Run))
 
        ' start execution of both threads
        thSend.Start()
        thReceive.Start()
 
        ' end of main thread
        Return
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class
 
Public Class clientSend
    ' class for reading keyboard commands
    ' and send them to a server via a tcp client passed to the
    Private client As TcpClient    ' tcp client
 
    ' manufacturer
    Public Sub New(ByVal client As TcpClient)
        ' we note the tcp client
        Me.client = client
    End Sub
 
    ' thread Run method
    Public Sub Run()
 
        ' local data
        Dim OUT As StreamWriter = Nothing        ' network write streams
        Dim commande As String = Nothing        ' command read from keyboard
        ' error management
        Try
            ' create network write stream
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' order entry-send loop
            Console.Out.WriteLine("Commandes : ")
            While True
                ' read command typed on keyboard
                commande = Console.In.ReadLine().Trim()
                ' finished?
                If commande.ToLower() = "fin" Then
                    Exit While
                End If
                ' send order to server
                OUT.WriteLine(commande)
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' end - we close the feeds
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' signals the end of the thread
        Console.Out.WriteLine("[fin du thread d'envoi des commandes au serveur]")
    End Sub
End Class

 
Public Class clientReceive
    ' class responsible for reading lines of text intended for a 
    ' tcp client passed to builder
    Private client As TcpClient    ' tcp client
 
    ' manufacturer
    Public Sub New(ByVal client As TcpClient)
        ' we note the tcp client
        Me.client = client
    End Sub
 
    'manufacturer
    ' thread Run method
    Public Sub Run()
 
        ' local data
        Dim [IN] As StreamReader = Nothing        ' network read stream
        Dim réponse As String = Nothing        ' server response
        ' error management
        Try
            ' create network read stream
            [IN] = New StreamReader(client.GetStream())
            ' loop read text lines from IN stream
            While True
                ' network streaming
                réponse = [IN].ReadLine()
                ' closed flow?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' display
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' end - close flows
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' signals the end of the thread
        Console.Out.WriteLine("[fin du thread de lecture des réponses du serveur]")
    End Sub
End Class

9.4.4. Um servidor TCP genérico

Agora vamos ver um servidor

  • que exibe no ecrã os comandos enviados pelos seus clientes
  • e lhes envia o texto introduzido por um utilizador através do teclado. O utilizador atua, portanto, como servidor.

O programa é iniciado por: srvgen listeningPort, onde listeningPort é a porta à qual os clientes devem ligar-se. O serviço ao cliente será tratado por duas threads:

  • uma thread dedicada exclusivamente à leitura das linhas de texto enviadas pelo cliente
  • uma thread dedicada exclusivamente à leitura das respostas digitadas pelo utilizador. Esta thread sinalizará, utilizando o comando fin, que está a encerrar a ligação com o cliente.

O servidor cria duas threads por cliente. Se houver n clientes, haverá 2n threads ativas ao mesmo tempo. O próprio servidor nunca pára, a menos que o utilizador pressione Ctrl-C no teclado. Vejamos alguns exemplos.

O servidor está a funcionar na porta 100 e usamos o cliente genérico para comunicar com ele. A janela do cliente tem este aspeto:

dos>cltgen 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 outras são do cliente para o servidor. A janela do servidor é a seguinte:

dos>srvgen 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 as enviadas do cliente para o servidor. As linhas N: são as enviadas do servidor para o cliente N. O servidor acima continua ativo, apesar de o cliente 1 ter terminado. Iniciamos um segundo cliente para o mesmo servidor:

dos>cltgen 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 tem agora o seguinte aspeto:

dos>srvgen 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

Agora vamos simular um servidor web executando o nosso servidor genérico na porta 88:

dos>srvgen 88
Serveur générique lancé sur le port 88

Agora vamos abrir um navegador e aceder ao URL http://localhost:88/exemple.html. O navegador irá então ligar-se à porta 88 na máquina localhost e solicitar a página /example.html:

Image

Agora vamos dar uma olhada na janela do nosso servidor:

dos>srvgen 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 que vemos os cabeçalhos HTTP enviados pelo navegador. Isto permite-nos aprender gradualmente sobre o protocolo HTTP. Num exemplo anterior, criámos um cliente web que enviava apenas um único pedido GET. Isso foi suficiente. Aqui vemos que o navegador envia outras informações para o servidor. Estas informações destinam-se a informar o servidor sobre o tipo de cliente com que está a lidar. Também vemos que os cabeçalhos HTTP terminam com uma linha em branco. Vamos criar uma resposta para o nosso cliente. O utilizador ao teclado é, neste caso, o servidor e pode criar uma resposta manualmente. Recordemos a resposta enviada por um servidor web num 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 fornecer 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 por 2: são enviadas do servidor para o cliente n.º 2. O comando «end» encerra a ligação do servidor ao cliente. Na nossa resposta, limitámo-nos 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 especificamos o tamanho do ficheiro que estamos a enviar (Content-Length), mas limitamo-nos a indicar que iremos encerrar a ligação (Connection: close) após o envio. Isto é suficiente para o navegador. Ao verificar que a ligação foi encerrada, o navegador saberá que a resposta do servidor está completa e exibirá a página HTML que lhe foi enviada. A página é 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>

O utilizador encerra então a ligação ao cliente digitando o comando «fin». O navegador fica assim a saber que a resposta do servidor está completa e pode exibi-la:

Image

Se, no exemplo acima, selecionar «Ver/Fonte» para ver o que o navegador recebeu, obtém:

Image

ou seja, exatamente o que foi enviado pelo servidor genérico. O código para o servidor TCP genérico é o seguinte:


' namespaces
Imports System
Imports System.Net
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic
 
Public Class serveurTcpGénérique
 
    ' main program
    Public Shared Sub Main(ByVal args() As String)
 
        ' receives the port of listening to customer requests
        ' creates a thread to read client requests
        ' these will be displayed on the screen
        ' creates a thread to read keyboard commands
        ' these will be sent as a reply to the customer
        ' the whole thing ends with the command end typed on the keyboard
 
        Const syntaxe As String = "Syntaxe : pg port"
 
        ' is there an argument
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' this argument must be integer >0
        Dim port As Integer = 0
        Dim erreurPort As Boolean = False
        Dim E As Exception = Nothing
        Try
            port = Integer.Parse(args(0))
        Catch ex As Exception
            E = ex
            erreurPort = True
        End Try
        erreurPort = erreurPort Or port <= 0
        If erreurPort Then
            erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
        End If
        ' we create the listening service
        Dim ecoute As TcpListener = Nothing
        Dim nbClients As Integer = 0     ' of customers handled
        Try
            ' create the service
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
            ' launch it
            ecoute.Start()
            ' follow-up
            Console.Out.WriteLine(("Serveur générique lancé sur le port " & port))
 
            ' customer service loop
            Dim client As TcpClient = Nothing
            While True        ' infinite loop - will be stopped by Ctrl-C
                ' waiting for a customer
                client = ecoute.AcceptTcpClient()
 
                ' the service is provided by separate threads
                nbClients += 1
                ' thread for reading customer requests
                Dim thReceive As New Thread(New ThreadStart(AddressOf New serveurReceive(client, nbClients).Run))
                ' thread for reading responses typed on the keyboard by the user
                Dim thSend As New Thread(New ThreadStart(AddressOf New serveurSend(client, nbClients).Run))
 
                ' start execution of both threads
                thSend.Start()
                thReceive.Start()
            End While
            ' back to listening to requests
        Catch ex As Exception
            ' we report the error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class
 
 
Public Class serveurSend
    ' class responsible for reading typed responses
    ' and send them to a client via a tcp client passed to the
    Private client As TcpClient    ' tcp client
    Private numClient As Integer    ' customer no
 
    ' manufacturer
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' we note the tcp client
        Me.client = client
        ' and its
        Me.numClient = numClient
    End Sub
 
    ' thread Run method
    Public Sub Run()
 
        ' local data
        Dim OUT As StreamWriter = Nothing        ' network write streams
        Dim réponse As String = Nothing        ' answer read from keyboard
        ' follow-up
        Console.Out.WriteLine(("Thread de lecture des réponses du serveur au client " & numClient & " lancé"))
        ' error management
        Try
            ' network write stream creation
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' order entry-send loop
            While True
                ' customer identification
                Console.Out.Write((numClient & " : "))
                ' read response typed on keyboard
                réponse = Console.In.ReadLine().Trim()
                ' finished?
                If réponse.ToLower() = "fin" Then
                    Exit While
                End If
                ' send response to server
                OUT.WriteLine(réponse)
            End While
            ' following response
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' end - close flows
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' signals the end of the thread
        Console.Out.WriteLine(("[fin du Thread de lecture des réponses du serveur au client " & numClient & "]"))
    End Sub
End Class
 
Public Class serveurReceive
    ' class responsible for reading text lines sent to the server 
    ' via a tcp client passed to the builder
    Private client As TcpClient     ' tcp client
    Private numClient As Integer    ' customer no
 
    ' manufacturer
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' we note the tcp client
        Me.client = client
        ' and its
        Me.numClient = numClient
    End Sub
 
    ' thread Run method
    Public Sub Run()
        ' local data
        Dim [IN] As StreamReader = Nothing        ' network read stream
        Dim réponse As String = Nothing        ' server response
        ' follow-up
        Console.Out.WriteLine(("Thread de lecture des demandes du client " & numClient & " lancé"))
        ' error management
        Try
            ' create network read stream
            [IN] = New StreamReader(client.GetStream())
            ' loop read text lines from IN stream
            While True
                ' network streaming
                réponse = [IN].ReadLine()
                ' closed flow?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' display
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' end - close flows
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' signals the end of the thread
        Console.Out.WriteLine(("[fin du Thread de lecture des demandes du client " & numClient & "]"))
    End Sub
End Class

9.4.5. Um cliente Web

No exemplo anterior, vimos 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 escrever um cliente web que receba um URL como parâmetro e exiba no ecrã o texto enviado pelo servidor. Vamos assumir que o servidor suporta o protocolo HTTP 1.1. Dos cabeçalhos listados acima, vamos utilizar apenas os seguintes:

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • O primeiro cabeçalho indica qual a página que pretendemos
  • o segundo especifica qual o servidor que estamos a consultar
  • O terceiro indica que queremos que o servidor feche a ligação depois de nos responder.

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

O nosso cliente web será chamado da seguinte forma: webclient URL cmd, onde URL é o URL pretendido 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>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 foi movida (e, portanto, tem um novo URL). O novo URL é fornecido pelo cabeçalho Location:

Location: /IISSamples/Default/welcome.htm

Se utilizarmos GET em vez de HEAD na chamada para o cliente web:

dos>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:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
 
 
Public Class clientWeb1
 
    ' requests a URL
    ' displays its contents on the screen
    Public Shared Sub Main(ByVal args() As String)
        ' syntax
        Const syntaxe As String = "pg URI GET/HEAD"
 
        ' number of arguments
        If args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' note the URI required
        Dim URIstring As String = args(0)
        Dim commande As String = args(1).ToUpper()
 
        ' URI validity check
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try
        ' order verification
        If commande <> "GET" And commande <> "HEAD" Then
            ' incorrect order
            erreur("Le second paramètre doit être GET ou HEAD", 3)
        End If
 
        ' we can work
        Dim client As TcpClient = Nothing        ' the customer
        Dim [IN] As StreamReader = Nothing        ' the customer's reading flow
        Dim OUT As StreamWriter = Nothing        ' the customer's writing flow
        Dim réponse As String = Nothing        ' server response
        Try
            ' connect to the server
            client = New TcpClient(uri.Host, uri.Port)
 
            ' create customer input/output flows TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
 
            ' request URL - send HTTP headers
            OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
            OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
            OUT.WriteLine("Connection: close")
            OUT.WriteLine()
            ' we read the answer
            réponse = [IN].ReadLine()
            While Not (réponse Is Nothing)
                ' the answer is processed
                Console.Out.WriteLine(réponse)
                ' we read the answer
                réponse = [IN].ReadLine()
            End While
            ' it's over
            client.Close()
        Catch e As Exception
            ' we handle the exception
            erreur(e.Message, 4)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

A única novidade neste programa é a utilização da classe Uri. 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 Uri permite-nos decompor a cadeia de caracteres da URL nos seus vários componentes. Um objeto Uri é construído a partir da cadeia de caracteres URI recebida como parâmetro:


        ' URI validity check
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

Se a cadeia de caracteres URI recebida como parâmetro não for um URI válido (falta de protocolo, servidor, etc.), é lançada uma exceção. Isto permite-nos verificar a validade do parâmetro recebido. Assim que o objeto URI é construído, temos acesso aos seus vários elementos. Assim, se o objeto URI do código anterior foi construído a partir da string http://serveur:port/cheminPageHTML?param1=val1;param2=val2;..., teremos:

uri.Host=server, uri.Port=port, uri.Path=HTMLPagePath, uri.Query=param1=val1;param2=val2;..., uri.pathAndQuery=HTMLPagePath?param1=val1;param2=val2;..., uri.Scheme=http.

9.4.6. Cliente web a lidar com redirecionamentos

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

  1. Ele lê a primeira linha dos cabeçalhos HTTP enviados pelo servidor para verificar se contém a string «302 Object moved», o que indica um redirecionamento
  2. O programa lê os seguintes cabeçalhos. Se houver um redirecionamento, procura a linha «Location: url», que fornece o novo URL da página solicitada, e regista esse URL.
  3. Exibe o resto da resposta do servidor. Se houver um redirecionamento, os passos 1 a 3 são repetidos com a nova URL. O programa não aceita mais do que um redirecionamento. Este limite é definido por uma constante que pode ser modificada.

Eis um exemplo:

dos>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>

O programa é o seguinte:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic
 
' web client class
Public Class clientWeb
   
  ' requests a URL and displays its contents on the screen
  Public Shared Sub Main(ByVal args() As String)
    ' syntax
    Const syntaxe As String = "pg URI GET/HEAD"
 
    ' number of arguments
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' note the URI required
    Dim URIstring As String = args(0)
    Dim commande As String = args(1).ToUpper()
 
    ' URI validity check
    Dim uri As Uri = Nothing
    Try
      uri = New Uri(URIstring)
    Catch ex As Exception
      ' URI incorrect
      erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
    End Try 'catch
    ' order verification
    If commande <> "GET" And commande <> "HEAD" Then
      ' incorrect order
      erreur("Le second paramètre doit être GET ou HEAD", 3)
    End If
 
    ' we can work
    Dim client As TcpClient = Nothing ' the customer
    Dim [IN] As StreamReader = Nothing ' the customer's reading flow
    Dim OUT As StreamWriter = Nothing ' the customer's writing flow
    Dim réponse As String = Nothing ' server response
    Const nbRedirsMax As Integer = 1 ' no more than one redirection accepted
    Dim nbRedirs As Integer = 0 ' number of redirects in progress
    Dim premièreLigne As String ' 1st line of the answer
    Dim redir As Boolean = False ' indicates redirection or not
    Dim locationString As String = "" ' the URI string of a possible redirection
    ' regular expression to find a URL redirect
    Dim location As New Regex("^Location: (.+?)$") '
 
        ' error management
    Try
      ' you may have several URL to request if there are redirections
      While nbRedirs <= nbRedirsMax
        ' connect to the server
        client = New TcpClient(uri.Host, uri.Port)
 
        ' create customer input/output flows TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True
 
        ' we send HTTP headers to request URL
        OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
        OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
        OUT.WriteLine("Connection: close")
        OUT.WriteLine()
 
        ' read the first line of the answer
        premièreLigne = [IN].ReadLine()
        ' screen echo
        Console.Out.WriteLine(premièreLigne)
 
        ' redirection?
        If Regex.IsMatch(premièreLigne, "302 Object moved$") Then
          ' there is a redirection
          redir = True
          nbRedirs += 1
                End If
 
                ' next HTTP headers until you find the empty line signalling the end of the headers
                Dim locationFound As Boolean = False
                réponse = [IN].ReadLine()
                While réponse <> ""
                    ' the answer is displayed
                    Console.Out.WriteLine(réponse)
                    ' if there is a redirection, we search for the Location header
                    If redir And Not locationFound Then
                        ' compare the line with the relational expression location
                        Dim résultat As Match = location.Match(réponse)
                        If résultat.Success Then
                            ' if found, note the URL of redirection
                            locationString = résultat.Groups(1).Value
                            ' we note that we found
                            locationFound = True
                        End If
                    End If
                    ' next line
                    réponse = [IN].ReadLine()
                End While
 
                ' following lines of the answer
                Console.Out.WriteLine(réponse)
                réponse = [IN].ReadLine()
                While Not (réponse Is Nothing)
                    ' the answer is displayed
                    Console.Out.WriteLine(réponse)
                    ' next line
                    réponse = [IN].ReadLine()
                End While
 
                ' close the connection
                client.Close()
                ' are we done?
                If Not locationFound Or nbRedirs > nbRedirsMax Then
                    Exit While
                End If
 
                ' there is a redirection to be made - we build the new Uri
                URIstring = uri.Scheme + "://" & uri.Host & ":" & uri.Port & locationString
                uri = New Uri(URIstring)
                ' follow-up
                Console.Out.WriteLine((ControlChars.Lf + "<--Redirection vers l'URL " + URIstring + "-->" + ControlChars.Lf))
            End While
        Catch e As Exception
      ' we handle the exception
      erreur(e.Message, 4)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

9.4.7. Servidor de Cálculo de Impostos

Estamos a revisitar o exercício TAXES, que já foi abordado de várias formas. Vamos rever a versão mais recente. Foi criada uma classe de impostos. Os seus atributos são três matrizes de números:


Public Class impôt
    ' les données nécessaires au calcul de l'impôt
    ' proviennent d'une source extérieure
    Private limites(), coeffR(), coeffN() as double

A classe tem dois construtores:

  • um construtor que recebe as três matrizes de dados necessárias para calcular o imposto
    // constructeur 1
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' initializes the three limit arrays, coeffR, coeffN from
        ' parameters passed to the constructor
  • um construtor ao qual passamos o nome DSN de uma base de dados ODBC
    ' builder 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception

Foi escrito um programa de teste:

dos>vbc /r:impots.dll testimpots.vb

dos>test mysql-impots timpots limites coeffr coeffn
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22506 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 2 200000
impôt=33388 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 3 200000
impôt=16400 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 3 300000
impôt=50082 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 3 200000
impôt=22506 F

Aqui, o programa de teste e o objeto fiscal estavam na mesma máquina. Propomos colocar o programa de teste e o objeto fiscal em máquinas diferentes. Teremos uma aplicação cliente-servidor em que o objeto fiscal remoto será o servidor. A nova classe chama-se TaxServer e deriva da classe tax:


Public Class ServeurImpots
    Inherits impôt
 
    ' attributes
    Private portEcoute As Integer    ' the ability to listen to customer requests
    Private actif As Boolean    ' server status
 
    ' manufacturer
    Public Sub New(ByVal portEcoute As Integer, ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        MyBase.New(DSNimpots, Timpots, colLimites, colCoeffR, colCoeffN)
        ' we note the listening port
        Me.portEcoute = portEcoute
        ' currently inactive
        actif = False
        ' creates and launches a thread for reading keyboard commands
        ' the server will be managed using these commands
        Dim threadLecture As Thread = New Thread(New ThreadStart(AddressOf admin))
        threadLecture.Start()
    End Sub

O único parâmetro novo no construtor é a porta para ouvir os pedidos dos clientes. Os outros parâmetros são passados diretamente para a classe base do imposto. O servidor de impostos é controlado por comandos de teclado. Por isso, criamos um segmento de execução para ler esses comandos. Haverá dois comandos possíveis: «start» para iniciar o serviço e «stop» para o encerrar definitivamente. O método «admin» que trata destes comandos é o seguinte:


    Public Sub admin()
        ' reads server administration commands typed from the keyboard
        ' in an endless loop
        Dim commande As String = Nothing
        While True
            ' invite
            Console.Out.Write("Serveur d'impôts>")
            ' read command
            commande = Console.In.ReadLine().Trim().ToLower()
            ' order execution
            If commande = "start" Then
                ' active?
                If actif Then
                    'error
                    Console.Out.WriteLine("Le serveur est déjà actif")
                Else
                    ' we launch the listening service
                    Dim threadEcoute As Thread = New Thread(New ThreadStart(AddressOf ecoute))
                    threadEcoute.Start()
                End If
            Else
                If commande = "stop" Then
                    ' end of all execution threads
                    Environment.Exit(0)
                Else
                    ' error
                    Console.Out.WriteLine("Commande incorrecte. Utilisez (start,stop)")
                End If
            End If
        End While
    End Sub

Se o comando introduzido através do teclado for «start», é lançada uma thread que escuta os pedidos do cliente. Se o comando introduzido for «stop», todas as threads são paradas. A thread de escuta executa o método «ecoute»:


    Public Sub ecoute()
        ' thread for listening to customer requests
        ' we create the listening service
        Dim ecoute As TcpListener = Nothing
        Try
            ' create the service
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), portEcoute)
            ' launch it
            ecoute.Start()
            ' follow-up
            Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & portEcoute))
 
            ' service loop
            Dim liaisonClient As TcpClient = Nothing
            While True            ' infinite loop
                ' waiting for a customer
                liaisonClient = ecoute.AcceptTcpClient()
                ' the service is provided by another task
                Dim threadClient As Thread = New Thread(New ThreadStart(AddressOf New traiteClientImpots(liaisonClient, Me).Run))
                threadClient.Start()
            End While
            ' back to listening to requests
        Catch ex As Exception
            ' we report the error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub

Aqui temos um servidor TCP clássico a escutar na porta portEcoute. Os pedidos do cliente são tratados pelo método Run de um objeto ao qual são passados dois parâmetros:

  1. o objeto TcpClient, que permite o acesso ao cliente
  2. o objeto tax this, que fornece acesso ao método de cálculo de impostos this.calculate.

' -------------------------------------------------------
' provides service to a tax server client
Public Class traiteClientImpots
 
    Private liaisonClient As TcpClient    ' customer liaison
    Private [IN] As StreamReader    ' iNPUTS
    Private OUT As StreamWriter    ' output flow
    Private objImpôt As impôt     ' object Tax
 
    ' manufacturer
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal objImpôt As impôt)
        Me.liaisonClient = liaisonClient
        Me.objImpôt = objImpôt
    End Sub

O método Run processa os pedidos dos clientes. Estes podem assumir duas formas:

  1. calculateMarried(y/n) numChildren annualSalary
  2. endCalculations

O Formulário 1 calcula um imposto, enquanto o Formulário 2 encerra a ligação cliente-servidor.


    ' run method
    Public Sub Run()
        ' renders service to the customer
        Try
            ' iNPUTS
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' output flow
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' send a welcome message to the customer
            OUT.WriteLine("Bienvenue sur le serveur d'impôts")
 
            ' loop read request/write response
            Dim demande As String = Nothing
            Dim champs As String() = Nothing            ' elements of the request
            Dim commande As String = Nothing            ' customer order: calculation or fincalculs
            demande = [IN].ReadLine()
            While Not (demande Is Nothing)
                ' demand is broken down into fields
                champs = Regex.Split(demande.Trim().ToLower(), "\s+")
                ' two successful applications: calcul and fincalculs
                commande = champs(0)
                Dim erreur As Boolean = False
                If commande <> "calcul" And commande <> "fincalculs" Then
                    ' customer error
                    OUT.WriteLine("Commande incorrecte. Utilisez (calcul,fincalculs).")
                End If
                If commande = "calcul" Then
                    calculerImpôt(champs)
                End If
                If commande = "fincalculs" Then
                    ' good-bye message to customer
                    OUT.WriteLine("Au revoir...")
                    ' freeing up resources
                    Try
                        OUT.Close()
                        [IN].Close()
                        liaisonClient.Close()
                    Catch
                    End Try
                    ' end
                    Return
                End If
                ' new request
                demande = [IN].ReadLine()
            End While
        Catch e As Exception
            erreur("L'erreur suivante s'est produite (" + e.ToString + ")", 2)
        End Try
    End Sub

O cálculo do imposto é realizado pelo método CalculateTax, que recebe como parâmetro a matriz de campos da solicitação do cliente. A validade da solicitação é verificada e, se for válida, o imposto é calculado e devolvido ao cliente.


    ' tax calculation
    Public Sub calculerImpôt(ByVal champs() As String)
        ' processing the application: calculation married nbEnfants salaireAnnuel
        ' broken down into fields in the fields table
        Dim marié As String = Nothing
        Dim nbEnfants As Integer = 0
        Dim salaireAnnuel As Integer = 0
 
        ' validity of arguments
        Try
            ' at least 4 fields are required
            If champs.Length <> 4 Then
                Throw New Exception
            End If
            ' married
            marié = champs(1)
            If marié <> "o" And marié <> "n" Then
                Throw New Exception
            End If
            ' children
            nbEnfants = Integer.Parse(champs(2))
            ' salary
            salaireAnnuel = Integer.Parse(champs(3))
        Catch
            OUT.WriteLine(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel")
            ' finish
            Exit Sub
        End Try
        ' tax can be calculated
        Dim impot As Long = objImpôt.calculer(marié = "o", nbEnfants, salaireAnnuel)
        ' we send the response to the client
        OUT.WriteLine(impot.ToString)
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub

Esta classe é compilada por

dos>vbc /r:impots.dll /r:system.dll /t:library srvimpots.vb

onde impots.dll contém o código para a classe impôt. Um programa de teste pode ter o seguinte aspeto:


' namespaces
Imports System
Imports System.IO
Imports Microsoft.VisualBasic
 
Public Class testServeurImpots
    Public Shared syntaxe As String = "Syntaxe : pg port dsnImpots Timpots colLimites colCoeffR colCoeffN"
 
    ' main program
    Public Shared Sub Main(ByVal args() As String)
 
        ' you need6 arguments
        If args.Length <> 6 Then
            erreur(syntaxe, 1)
        End If
        ' port must be integer >0
        Dim port As Integer = 0
        Dim erreurPort As Boolean = False
        Dim E As Exception = Nothing
        Try
            port = Integer.Parse(args(0))
        Catch ex As Exception
            E = ex
            erreurPort = True
        End Try
        erreurPort = erreurPort Or port <= 0
        If erreurPort Then
            erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
        End If
        ' we create the tax server
        Try
            Dim srvimots As ServeurImpots = New ServeurImpots(port, args(1), args(2), args(3), args(4), args(5))
        Catch ex As Exception
            'error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

Passamos os dados necessários para construir um objeto ServeurImpots para o programa de teste, e a partir daí ele cria esse objeto. Este programa de teste é compilado por:

dos>vbc /r:srvimpots.dll /r:impots.dll testimpots.vb

Eis um teste inicial:

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

A linha

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn

cria um objeto TaxServer que ainda não está a escutar pedidos de clientes. É o comando start digitado no teclado que inicia este processo de escuta. O comando stop pára o servidor. Vamos agora utilizar um cliente. Utilizaremos o cliente genérico criado anteriormente. O servidor é iniciado:

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124

O cliente genérico é iniciado noutra janela do DOS:

dos> clttcpgenerique localhost 124Commandes :
<-- Bienvenue sur le serveur d'impôts

Podemos ver que o cliente recebeu com sucesso 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 o parar:

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop