Skip to content

9. Programação TCP-IP

9.1. Generalidades

9.1.1. Os protocolos da Internet

Apresentamos aqui uma introdução aos protocolos de comunicação da Internet, também conhecidos como conjunto de protocolos TCP/IP (Transfer 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 abordar a criação de aplicações distribuídas.

O texto que se segue é uma tradução parcial de um texto que se encontra no documento «Lan Workplace for Dos - Administrator's Guide» da NOVELL, um documento do início dos anos 90.

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

O conceito geral de criar uma rede de computadores heterogéneos tem origem em investigações realizadas pela DARPA (Defense Advanced Research Projects Agency) 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 denominada ARPAnet, rede que mais tarde se tornou a rede INTERNET. Os protocolos TCP/IP definem formatos e regras de transmissão e receção independentes da organização das redes e do equipamento utilizado.

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

  • origem do pacote
  • destino
  • comprimento
  • tipo

9.1.2. O modelo OSI

Os protocolos TCP/IP seguem, em linhas gerais, o modelo de rede aberta denominado OSI (Open Systems Interconnection Reference Model), definido pela ISO (Organização Internacional de Normalização). Este modelo descreve uma rede ideal em que a comunicação entre máquinas pode ser representada por um modelo de sete camadas:

Cada camada recebe serviços da camada inferior e fornece os seus à camada superior. Suponhamos que duas aplicações localizadas em máquinas A e B diferentes pretendem comunicar: fazem-no ao nível da camada Application. Não precisam de conhecer todos os detalhes do funcionamento da rede: cada aplicação entrega a informação que pretende transmitir à camada inferior: a camada Présentation. A aplicação só precisa, portanto, de conhecer as regras de interface com a camada Présentation.

Assim que a informação chega à camada Présentation, é encaminhada, de acordo com outras regras, para a camada Session e assim sucessivamente, até que a informação chegue ao suporte físico e seja transmitida fisicamente para a máquina de destino. Aí, será submetida ao processo inverso ao que sofreu na máquina remetente.

Em cada camada, o processo remetente encarregado de enviar a informação transmite-a a um processo recetor na outra máquina, pertencente à mesma camada que ele. Faz-o de acordo com determinadas regras a que se chama protocolo da camada. Temos, portanto, o seguinte esquema de comunicação final:

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

Physique
Assegura a transmissão de bits num suporte físico. Nesta camada encontram-se equipamentos terminais de processamento de dados (E.T.T.D.), tais como terminais ou computadores, bem como equipamentos de terminação de circuitos de dados (E.T.C.D.), tais como moduladores/demoduladores, multiplexadores e concentradores. Os pontos de interesse a este nível são:
. a escolha do tipo de codificação da informação (analógica ou digital)
. a escolha do modo de transmissão (síncrono ou assíncrono).
Liaison de données
Oculta as características físicas da camada Física. Deteta e corrige os erros de transmissão.
Réseau
Gere o percurso que as informações enviadas pela rede devem seguir. A isto chama-se routage: determinar o percurso que uma informação deve seguir para chegar ao seu destinatário.
Transport
Permite a comunicação entre duas aplicações, enquanto as camadas anteriores apenas permitiam a comunicação entre máquinas. Um serviço prestado por esta camada pode ser o multiplexamento: a camada de transporte poderá utilizar uma mesma ligação de rede (de máquina para máquina) para transmitir informações pertencentes a várias aplicações.
Session
Nesta camada, encontramos serviços que permitem a uma aplicação abrir e manter uma sessão de trabalho numa máquina remota.
Présentation
O seu objetivo é uniformizar a representação dos dados nas diferentes máquinas. Assim, os dados provenientes de uma máquina A serão «formatados» pela camada Présentation da máquina A, de acordo com um formato padrão, antes de serem enviados pela rede. Ao chegarem à camada Présentation da máquina destinatária B, que as reconhecerá graças ao seu formato padrão, serão formatadas de outra forma para que a aplicação da máquina B as reconheça.
Application
Nesta fase, encontram-se as aplicações geralmente mais próximas do utilizador, tais como o correio eletrónico ou a transferência de ficheiros.

9.1.3. O modelo TCP/IP

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

Camada Física

Numa rede local, encontra-se geralmente a tecnologia Ethernet ou Token-Ring. Aqui, apresentamos apenas a tecnologia Ethernet.

Ethernet

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

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

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

  • Capacidade de 10 megabits/segundo.
  • Topologia em barramento: todos os equipamentos estão ligados ao mesmo cabo
  • Rede de difusão — Um equipamento que transmite transfere informações pelo cabo com a morada do equipamento destinatário. Todos os equipamentos ligados recebem então essas informações e apenas aquele a quem se destinam as retém.
  • O método de acesso é o seguinte: o transmissor que pretende transmitir escuta o cabo — deteta então a presença ou ausência de uma onda portadora, cuja presença significaria que está em curso uma transmissão. Trata-se da técnica CSMA (Carrier Sense Multiple Access). Na ausência de portadora, um transmissor pode decidir transmitir à sua vez. Podem ser vários os que tomam essa decisão. Os sinais emitidos misturam-se: diz-se que ocorre uma colisão. O transmissor deteta esta situação: ao mesmo tempo que emite no cabo, escuta o que realmente passa por ele. Se detetar que a informação que transita pelo cabo não é a que emitiu, deduz que há uma colisão e deixará de emitir. Os outros transmissores que estavam a emitir farão o mesmo. Cada um retomará a sua transmissão após um intervalo aleatório, que depende de cada transmissor. Esta técnica é designada por CD (Detecção de Colisão). O método de acesso é, assim, designado por CSMA/CD.
  • Um endereçamento de 48 bits. Cada máquina possui um endereço, aqui denominado endereço físico, que está gravado na placa que a 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 (Internet Protocol)
Transmite pacotes entre dois nós da rede
ICMP 
(Internet Control Message Protocol)
O ICMP estabelece a comunicação entre o programa do protocolo IP de uma máquina e o de outra máquina. Trata-se, portanto, de um protocolo de troca de mensagens no próprio âmbito do protocolo IP.
ARP
(Address Resolution Protocol)
estabelece a correspondência entre o endereço de Internet da máquina e o endereço físico da máquina
RARP
(Reverse Address Resolution Protocol)
faz a correspondência entre o endereço físico da máquina e o endereço de Internet da máquina

Camadas de Transporte/Sessão

Nesta camada, encontram-se os seguintes protocolos:

TCP (Transmission Control Protocol)
Assegura uma entrega fiável de informações entre dois clientes
UDP (User Datagram Protocol)
Assegura a entrega não fiável de informações entre dois clientes

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

Aqui encontram-se vários protocolos:

TELNET
Emulador de terminal que permite que uma máquina A se ligue a uma máquina B como terminal
FTP (File Transfer Protocol)
permite a transferência de ficheiros
TFTP (Trivial File
Transfer Protocol)
permite a transferência de ficheiros
SMTP (Simple Mail Transfer
protocol)
permite a troca de mensagens entre utilizadores da rede
DNS (Domain Name System)
converte um nome de computador num endereço de Internet do computador
XDR (eXternal Data 
Representation)
criado pela Sun MicroSystems, especifica uma representação padrão dos dados, independente dos computadores
RPC(Remote Procedures Call)
também definido pela Sun, é um protocolo de comunicação entre aplicações remotas, independente da camada de transporte. Este protocolo é importante: liberta o programador do conhecimento dos detalhes da camada de transporte e torna as aplicações portáveis. Este protocolo baseia-se no protocolo XDR
NFS (Network File System)
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 anterior

9.1.4. Funcionamento dos protocolos da Internet

As aplicações desenvolvidas no ambiente TCP/IP utilizam geralmente vários dos protocolos desse ambiente. Um programa de aplicação comunica com a camada mais elevada dos protocolos. Esta transmite a informação à camada inferior e assim sucessivamente até chegar ao suporte físico. Aí, a informação é fisicamente transferida para o equipamento de destino, onde voltará a atravessar as mesmas camadas, desta vez no sentido inverso, até chegar à aplicação de destino das informações enviadas. O esquema seguinte mostra o percurso da informação:

Vejamos um exemplo: a aplicação FTP, definida ao nível da camada Application e que permite a transferência de ficheiros entre máquinas.

  • A aplicação fornece uma sequência de bytes a transmitir à camada transport.
  • A camada transport divide esta sequência de bytes em segments e TCP, e acrescenta, no início de cada segmento, o número do mesmo. Os segmentos são encaminhados para a camada de rede, regida pelo protocolo IP.
  • A camada IP cria um pacote que encapsula o segmento TCP recebido. No cabeçalho 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. Tudo isto é encaminhado para a camada de ligação de dados e ligação 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 ao seu destinatário através do cabo.
  • Na máquina de destino, a camada de Ligação de Dados e Ligação Física faz o inverso: desencapsula o pacote IP da trama física e passa-o para a camada IP.
  • A camada IP verifica se o pacote está correto: calcula uma soma, em função dos bits recebidos (checksum), soma essa que deve ser encontrada no cabeçalho do pacote. Se não for esse o caso, o pacote é rejeitado.
  • Se o pacote for considerado correto, a camada IP desencapsula o segmento TCP que se encontra no seu interior e transmite-o à camada superior, a transport.
  • A camada transport — a camada TCP no nosso exemplo — examina o número do segmento para restabelecer a ordem correta dos segmentos.
  • 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.
  • Resta agora à camada TCP transmitir a parte de dados do segmento à aplicação destinatária desses dados na camada superior.

9.1.5. O endereçamento na Internet

Um noeud de uma rede pode ser um computador, uma impressora inteligente, um servidor de ficheiros, ou qualquer coisa que possa comunicar utilizando os protocolos TCP/IP. Cada nó possui um endereço físico cujo formato depende do tipo de rede. Numa rede Ethernet, o endereço físico é codificado em 6 octetos. Um endereço de uma rede X25 é um número de 14 dígitos.

O endereço de Internet de um nó é um endereço lógico: é independente do equipamento e da rede utilizada. Trata-se de um endereço de 4 octetos que identifica simultaneamente uma rede local e um nó dessa rede. O endereço de Internet é normalmente representado sob a forma de 4 números, correspondentes aos valores dos 4 octetos, separados por um ponto. Assim, o endereço do computador Lagaffe da Faculdade de Ciências de Angers é 193.49.144.1 e o do computador Liny é 193.49.144.9. Deduz-se, portanto, que o endereço de Internet da rede local é 193.49.144.0. Esta rede pode ter até 254 nós.

Como as moradas de Internet ou moradas IP são independentes da rede, um computador de uma rede A pode comunicar com um computador de uma rede B sem se preocupar com o tipo de rede em que se encontra: basta que conheça a sua morada IP. O protocolo IP de cada rede encarrega-se de efetuar a conversão entre a morada IP e a morada física, em ambos os sentidos.

As endereços IP devem ser todas diferentes. Em França, é a INRIA que se encarrega de atribuir as endereços IP. Na verdade, esta entidade 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 dessa rede pode, em seguida, atribuir os endereços IP 193.49.144.1 a 193.49.144.254 como entender. Este endereço é geralmente registado num ficheiro específico de cada máquina ligada à rede.

9.1.5.1. As classes de endereços IP

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

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

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

Classe A

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

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

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

O endereço de rede ocupa 7 bits e o endereço do nó, 24 bits. Assim, podem existir 127 redes de classe A, cada uma com até 224 nós.

Classe B

Neste caso, o endereço IP: I1.I2.I3.I4 tem o formato R1.R2.N1.N2, em que

R1.R2
é a morada da rede
N1.N2
é o endereço de um computador nessa rede

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

O endereço da rede ocupa 2 octetos (14 bits, exatamente), tal como o do nó. Assim, podem existir 2¹⁴ redes de classe B, cada uma com até 2¹⁶ nós.

Classe C

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

R1.R2.R3
é a morada da rede
N1
é o endereço de um computador nessa rede

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

O endereço de rede ocupa 3 octetos (menos 3 bits) e o endereço do nó ocupa 1 octeto. Assim, podem existir 221 redes de classe C com até 256 nós.

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

Endereços reservados

  • Algumas moradas IP são moradas de rede, em vez de moradas de nós na rede. São aquelas em que a parte do nó é definida como 0. Assim, o endereço 193.49.144.0 é o endereço IP da rede da Faculdade de Ciências de Angers. Consequentemente, nenhum nó de uma rede pode ter o endereço zero.
  • Quando, numa morada IP, a morada do nó é composta apenas por 1, temos então uma morada de difusão: esta morada designa todos os nós da rede.
  • Numa rede de classe C, que permite teoricamente 2⁸ = 256 nós, se retirarmos os dois endereços proibidos, restam apenas 254 endereços autorizados.

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

Vimos que, durante a transmissão de informações de uma máquina para outra, estas, ao atravessarem a camada IP, eram encapsuladas em pacotes. Estes 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 for transmitido à camada responsável por o enviar para a rede física, são-lhe adicionadas outras informações para formar a trama física que será finalmente enviada para a rede. Por exemplo, o formato de uma trama numa rede Ethernet é o seguinte:

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

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

  • Envia um pacote de um tipo especial, denominado pacote ARP, que contém a morada IP da máquina cuja morada física se pretende obter. Também se certificou de incluir nesse pacote a sua própria morada IP, bem como a sua morada física.
  • Este pacote é enviado a todos os nós da rede.
  • Estes reconhecem a natureza especial do pacote. O nó que reconhece a sua morada IP no pacote responde enviando ao remetente do pacote a sua morada física. Como é que isso é possível? Encontrou no pacote as moradas IP e a morada física do remetente.
  • O remetente recebe, assim, a morada física que procurava. Armazena-a na memória para poder utilizá-la posteriormente, caso seja necessário enviar outros pacotes para o mesmo destinatário.

O endereço IP de uma máquina está normalmente registado num dos seus ficheiros, pelo que pode consultá-lo para o conhecer. Este endereço pode ser alterado: basta editar o ficheiro. O endereço físico, por sua vez, está registado na memória da placa de rede e não pode ser alterado.

Quando um administrador pretende organizar a sua rede de forma diferente, pode ser necessário alterar os endereços IP de todos os nós e, por conseguinte, editar os diferentes ficheiros de configuração de cada um deles. Isto pode ser moroso e dar origem a erros, caso haja muitas máquinas. Um método consiste em não atribuir um endereço IP às máquinas: insere-se, então, um código especial no ficheiro onde a máquina deveria encontrar o seu endereço IP. Ao verificar que não possui o endereço IP, a máquina solicita-o através de um protocolo denominado RARP (Reverse Address Resolution Protocol). Em seguida, envia para a rede um pacote especial denominado pacote RARP, análogo ao pacote ARP anterior, no qual inclui a sua morada física. Este pacote é enviado a todos os nós, que reconhecem então um pacote RARP. Um deles, denominado servidor RARP, possui um ficheiro que indica a correspondência entre a morada física e a morada IP de todos os nós. Responde então ao remetente do pacote RARP, reenviando-lhe a sua morada IP. Um administrador que pretenda reconfigurar a sua rede só tem, portanto, de editar o ficheiro de correspondências do servidor RARP. Este deve, normalmente, ter uma morada fixa IP, que deve poder conhecer sem ter de utilizar ele próprio o protocolo RARP.

9.1.6. A camada de rede denominada camada IP da Internet

O protocolo IP (Internet Protocol) define a forma que os pacotes devem assumir e a forma como devem ser geridos durante a sua transmissão ou receção. Este tipo específico de pacote é denominado datagrama IP. Já o apresentámos:

O importante é que, para além dos dados a transmitir, o datagrama IP contém os endereços de Internet dos equipamentos de origem e de destino. Assim, o equipamento de destino sabe quem lhe está a enviar uma mensagem.

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

As tramas físicas circulam de nó em nó até ao seu destino, que pode não se encontrar na mesma rede física que a máquina remetente. O pacote IP pode, portanto, ser encapsulado sucessivamente em diferentes tramas físicas nos nós que fazem a ligação entre duas redes de tipos diferentes. Também é possível que o pacote IP seja demasiado grande para ser encapsulado numa trama física. O software IP do nó onde este problema ocorre divide então o pacote IP em fragments de acordo com regras precisas, sendo cada um deles posteriormente enviado pela rede física. Só serão reagrupados no seu destino final.

9.1.6.1. O encaminhamento

O encaminhamento é o método utilizado para encaminhar os pacotes IP até ao seu destino. Existem dois métodos: o encaminhamento direto e o encaminhamento indireto.

Roteamento direto

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

  • A máquina remetente de um datagrama IP tem a morada IP do destinatário.
  • Obtém a morada física deste último através do protocolo ARP ou nas suas tabelas, caso essa morada já tenha sido obtida.
  • Envia o pacote pela rede para essa morada física.

Roteamento indireto

O encaminhamento indireto refere-se ao encaminhamento de um pacote IP para um destino situado numa rede diferente daquela a que pertence o remetente. Neste caso, as partes de endereço de rede dos endereços IP das máquinas de origem e de destino são diferentes. A máquina de origem reconhece este facto. Envia então o pacote para um nó especial denominado router (router), nó que liga uma rede local a outras redes e cujo endereço IP encontra nas suas tabelas, endereço obtido inicialmente num ficheiro, numa memória permanente ou ainda através de informações que circulam na rede.

Um router está ligado a duas redes e possui um endereço IP no interior dessas duas 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 consiste em converter o pacote IP que recebe — e que está contido numa trama física típica da rede n.º 1 — numa trama física que possa circular na rede n.º 2. Se a morada IP do destinatário do pacote estiver na rede n.º 2, o router enviará o pacote diretamente para ele; caso contrário, enviá-lo-á para outro router, ligando a rede n.º 2 a uma rede n.º 3 e assim sucessivamente.

9.1.6.2. Mensagens de erro e de controlo

Ainda na camada de rede, ou seja, ao mesmo nível que o protocolo IP, existe o protocolo ICMP (Internet Control Message Protocol). Este serve para enviar mensagens sobre o funcionamento interno da rede: nós em avaria, congestionamento num router, etc... As mensagens ICMP são encapsuladas em pacotes IP e enviadas pela rede. As camadas IP dos diferentes nós tomam as medidas adequadas de acordo com as mensagens ICMP que recebem. Assim, uma aplicação, por si só, nunca deteta estes problemas específicos 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 não fiável de dados entre dois pontos, ou seja, o encaminhamento correto de um pacote para o seu destino não é garantido. A aplicação, se assim o desejar, pode gerir isso por si própria, aguardando, por exemplo, após o envio de uma mensagem, um aviso de receção, antes de enviar a seguinte.

Por enquanto, ao nível da rede, falámos de endereços IP de máquinas. No entanto, numa mesma máquina podem coexistir simultaneamente diferentes processos, todos eles capazes de comunicar entre si. Por isso, ao enviar uma mensagem, é necessário indicar não só o endereço IP da máquina destinatária, mas também o «nome» do processo destinatário. Este nome é, na verdade, um número, denominado número de porta. Alguns 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 geridos pelo protocolo UDP são também designados por datagramas. Têm a seguinte forma:

Estes datagramas serão encapsulados em pacotes IP e, posteriormente, em tramas físicas.

9.1.7.2. O protocolo TCP: Protocolo de Controlo de Transferência

Para comunicações seguras, o protocolo UDP é insuficiente: o programador de aplicações deve criar ele próprio um protocolo que lhe permita detetar o encaminhamento correto dos pacotes.

O protocolo TCP (Transfer Control Protocol) evita estes problemas. As suas características são as seguintes:

  • O processo que pretende transmitir estabelece, em primeiro lugar, uma ligação com o processo destinatário das informações que vai transmitir. Esta ligação é estabelecida entre uma porta da máquina emissora e uma porta da máquina recetora. Entre as duas portas é criado um caminho virtual, que ficará reservado exclusivamente aos 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.
  • A informação transmitida tem um caráter contínuo. O processo emissor envia informações ao seu próprio ritmo. Estas não são necessariamente enviadas de imediato: o protocolo TCP aguarda até ter quantidade suficiente para as enviar. São armazenadas numa estrutura denominada segmento TCP. Este segmento, uma vez preenchido, será transmitido para a camada IP, onde será encapsulado num pacote IP.
  • Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP destinatário verifica se recebe os segmentos na sequência correta. Por cada segmento recebido corretamente, envia um aviso de receção ao remetente.
  • Quando este último o recebe, informa o processo emissor. Este pode, assim, saber que um segmento chegou ao destino, o que não era possível com o protocolo UDP.
  • Se, após algum tempo, o protocolo TCP que emitiu um segmento não receber uma confirmação de receção, reenvia o segmento em questão, garantindo assim a qualidade do serviço de encaminhamento da informação.
  • O circuito virtual estabelecido entre os dois processos que comunicam entre si é o full-duplex: isto significa que a informação pode transitar nos dois sentidos. Assim, o processo de destino pode enviar confirmações de receção mesmo enquanto o processo de origem continua a enviar informações. Isto permite, por exemplo, que o protocolo de origem TCP envie vários segmentos sem esperar por um aviso de receção. Se, após algum tempo, verificar que não recebeu o aviso de receção de um determinado segmento n.º n, retomará a transmissão dos segmentos a partir desse ponto.

9.1.8. A camada de Aplicações

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

TELNET

Este protocolo permite que um utilizador de uma máquina A da rede se ligue a uma máquina B (frequentemente designada por máquina anfitriã). O TELNET emula na máquina A um terminal denominado universal. O utilizador comporta-se, assim, como se dispusesse de 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, tais como a criação de diretórios, por exemplo. Baseia-se no protocolo TCP.

TFTP: (Controlo 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 um computador remoto, por exemplo, através do FTP, tem de conhecer o endereço de Internet desse computador. Por exemplo, para efetuar FTP na máquina Lagaffe da Universidade de Angers, seria necessário executar o FTP da seguinte forma: FTP 193.49.144.1

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

máquina DPX2/320 da Universidade de Angers

máquina Sun da Universidade de Angers com o endereço ISERPA

É evidente que seria mais prático designar uma máquina por um nome em vez de pela sua morada IP. Coloca-se então o problema da unicidade do nome: existem milhões de máquinas interligadas. Poder-se-ia imaginar que uma entidade centralizada atribuísse os nomes. Isso seria, sem dúvida, bastante pesado. O controlo dos nomes foi, de facto, distribuído por domínios. Cada domínio é gerido por uma entidade geralmente muito ágil, que tem total liberdade na escolha dos nomes das máquinas. Assim, as máquinas em França pertencem ao domínio «fr», gerido pelo Inria de Paris. Para continuar a simplificar as coisas, o controlo é ainda mais distribuído: são criados domínios no interior do domínio «fr». Assim, a Universidade de Angers pertence ao domínio «univ-Angers». O serviço que gere este domínio tem total liberdade para nomear as máquinas da rede da Universidade de Angers. Por enquanto, este domínio não foi subdividido. Mas numa grande universidade com muitas máquinas em rede, tal subdivisão poderia ocorrer.

A máquina DPX2/320 da Universidade de Angers foi designada Lagaffe, enquanto um PC e um 486DX50 foram designados liny. Como referenciar estas máquinas a partir do exterior? Especificando a hierarquia dos 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 poderá ser referenciada por

Lagaffe.univ-Angers

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

Lagaffe

Uma aplicação pode, portanto, referenciar uma máquina pelo seu nome. No fim de contas, é necessário obter o endereço de Internet dessa máquina. Como é que isso é feito? Suponhamos que, a partir de uma máquina A, se pretenda comunicar com uma máquina B.

  • Se a máquina B pertencer ao mesmo domínio que a máquina A, provavelmente encontrar-se-á o seu endereço IP num ficheiro da máquina A.
  • Caso contrário, a máquina A encontrará, noutro ficheiro ou no mesmo que anteriormente, uma lista de alguns servidores de nomes com os seus endereços IP. Um servidor de nomes é responsável por estabelecer a correspondência entre o nome de uma máquina e o seu endereço IP. A máquina A enviará um pedido especial ao primeiro servidor de nomes da sua lista, denominado pedido DNS, incluindo, portanto, o nome da máquina procurada. Se o servidor consultado tiver esse nome nos seus registos, enviará à máquina A o endereço IP correspondente. Caso contrário, o servidor também encontrará nos seus ficheiros uma lista de servidores de nomes que pode consultar. E assim o fará. Desta forma, serão consultados vários servidores de nomes, não de forma aleatória, mas de modo a minimizar o número de pedidos. Se a máquina for finalmente encontrada, a resposta será enviada de volta à máquina A.

XDR: (Representação de dados eXternal)

Criado pela Sun MicroSystems, este protocolo especifica uma representação padrão dos dados, independente do equipamento.

RPC: (Remote Procedure Call)

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 do conhecimento dos detalhes da camada de transporte e torna as aplicações portáveis. 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 anterior.

9.1.9. Conclusão

Apresentámos nesta introdução algumas linhas gerais dos protocolos da Internet. Para aprofundar este tema, pode-se ler 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

Um computador na Internet é identificado de forma única por um endereço IP (Protocolo de Internet) com o formato I1.I2.I3.I4, em que In é um número entre 1 e 254. Pode também ser identificado por um nome igualmente único. Este nome não é obrigatório, uma vez que as aplicações acabam sempre por utilizar os endereços IP dos computadores. Servem apenas para facilitar a vida dos utilizadores. Assim, é mais fácil, com um navegador, aceder ao endereço http://www.ibm.com do que ao endereço URL http://129.42.17.99, embora ambos os métodos sejam possíveis. A associação entre o endereço IP <--> nomMachine é assegurada por um serviço distribuído da Internet denominado DNS (Domain Name System). A plataforma .NET disponibiliza a classe Dns para gerir os endereços da Internet:

Image

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

Overloads Public Shared Function
 GetHostByAddress(ByVal address As
 String) As IPHostEntry
Retorna um endereço IPHostEntry a partir de um endereço IP na forma «I1.I2.I3.I4». Lança uma exceção se a máquina address não for encontrada.
Public Shared Function
 GetHostByName(ByVal hostName As
 String) As IPHostEntry
retorna um endereço IPHostEntry a partir de um nome de máquina. Lança uma exceção se a máquina name não for encontrada.
Public Shared Function 
GetHostName() As String
retorna o nome da máquina na qual está a ser executado o programa que está a executar esta instrução

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

As propriedades que nos interessam:

Public Property AddressList 
As IPAddress ()
lista de endereços IP de uma máquina. Se um endereço IP designa uma e apenas uma máquina física, uma máquina física pode ter vários endereços IP. Será esse o caso se tiver várias placas de rede que a liguem a redes diferentes.
Public Property Aliases 
As String ()
lista de aliases de uma máquina, que pode ser designada por um nome principal e por aliases
Public Property HostName 
As String
o nome da máquina, caso tenha um

Da classe IPAddress, destacamos o construtor, as propriedades e os métodos seguintes:

Image

Um objeto [IPAddress] pode ser transformado numa cadeia I1.I2.I3.I4 com o método ToString(). Por outro lado, é possível obter um objeto IPAddress a partir de uma cadeia de caracteres I1.I2.I3.I4 utilizando o método estático IPAddress.Parse("I1.I2.I3.I4"). Consideremos o seguinte programa, que apresenta o nome da máquina na qual está a ser executado e, em seguida, de forma interativa, apresenta as correspondências entre o endereço IP e o nome da máquina:

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:


' opções
Option Explicit On 
Option Strict On

' espaços de nomes
Imports System
Imports System.Net
Imports System.Text.RegularExpressions

' módulo de teste
Public Module adresses

    Sub Main()
        ' exibe o nome do computador local
        ' e, em seguida, fornece informações de forma interativa sobre as máquinas da rede
        ' identificadas por um nome ou um endereço IP
        ' máquina local
        Dim localHost As String = Dns.GetHostName()
        Console.Out.WriteLine(("Machine Locale=" + localHost))

        ' perguntas e respostas interativas
        Dim machine As String
        Dim adresseMachine As IPHostEntry
        While True
            ' introdução do nome da máquina procurada
            Console.Out.Write("Machine recherchée (fin pour arrêter) : ")
            machine = Console.In.ReadLine().Trim().ToLower()
            ' concluído?
            If machine = "fin" Then
                Exit While
            End If

            ' endereço I1.I2.I3.I4 ou nome da máquina?
            Dim isIPV4 As Boolean = Regex.IsMatch(machine, "^\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s*$")
            ' gestão de exceções
            Try
                If isIPV4 Then
                    adresseMachine = Dns.GetHostByAddress(machine)
                Else
                    adresseMachine = Dns.GetHostByName(machine)
                End If
                ' o nome
                Console.Out.WriteLine(("Machine : " + adresseMachine.HostName))
                ' os endereços IP
                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()
                ' os 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
                ' a máquina não existe
                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. Generalidades

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

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

  • o endereço IP ou o nome da máquina B
  • o número da porta com a qual a aplicação AppB opera. Com efeito, a máquina B pode suportar várias aplicações que operam na Internet. Quando recebe informações provenientes da rede, tem de saber a que aplicação essas informações se destinam. As aplicações da máquina B têm acesso à rede através de interfaces, também denominadas portas de comunicação. Esta informação está contida no pacote recebido pela máquina B, para que seja entregue à aplicação correta.
  • Os protocolos de comunicação compreendidos pela máquina B. No nosso estudo, utilizaremos apenas os protocolos TCP-IP.
  • O protocolo de diálogo aceite pela aplicação AppB. Com efeito, as máquinas A e B vão «comunicar» entre si. O que vão dizer será encapsulado nos protocolos TCP-IP. No entanto, quando, no final da cadeia, a aplicação AppB receber a informação enviada pela aplicação AppA, terá de ser capaz de a interpretar. Isto é análogo à situação em que duas pessoas, A e B, comunicam por telefone: o seu diálogo é transportado pelo telefone. A fala será codificada sob a forma de sinais pelo telefone A, transportada por linhas telefónicas, chegará ao telefone B para aí ser descodificada. A pessoa B ouve então as palavras. É aqui que entra o conceito de protocolo de diálogo: se A falar francês e B não compreender essa língua, A e B não poderão dialogar de forma útil.

Por isso, as duas aplicações que comunicam entre si têm de chegar a acordo quanto ao tipo de diálogo que vão adotar. Por exemplo, o diálogo com um serviço ftp não é o mesmo que com um serviço pop: estes dois serviços não aceitam os mesmos comandos. Têm um protocolo de diálogo diferente.

9.3.2. As características do protocolo TCP

Aqui, iremos analisar apenas as comunicações de rede que utilizam o protocolo de transporte TCP. Recorde-se aqui as características deste protocolo:

  • O processo que pretende transmitir estabelece, em primeiro lugar, uma ligação com o processo destinatário das informações que vai transmitir. Esta ligação é estabelecida entre uma porta da máquina emissora e uma porta da máquina recetora. Entre as duas portas é criado um caminho virtual, que ficará reservado exclusivamente aos 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
  • A informação transmitida tem um caráter contínuo. O processo emissor envia informações ao seu próprio ritmo. Estas não são necessariamente enviadas de imediato: o protocolo TCP aguarda até ter quantidade suficiente para as enviar. São armazenadas numa estrutura denominada segmento TCP. Este segmento, uma vez preenchido, será transmitido para a camada IP, onde será encapsulado num pacote IP.
  • Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP destinatário verifica se recebe os segmentos na sequência correta. Por cada segmento recebido corretamente, envia um aviso de receção ao remetente.
  • Quando este último o recebe, informa o processo emissor. Este pode, assim, saber que um segmento chegou ao destino.
  • Se, após algum tempo, o protocolo TCP que emitiu um segmento não receber uma confirmação de receção, reenvia o segmento em questão, garantindo assim a qualidade do serviço de encaminhamento da informação.
  • O circuito virtual estabelecido entre os dois processos que comunicam entre si é o full-duplex: isto significa que a informação pode transitar nos dois sentidos. Assim, o processo de destino pode enviar confirmações de receção mesmo enquanto o processo de origem continua a enviar informações. Isto permite, por exemplo, que o protocolo de origem TCP envie vários segmentos sem esperar por um aviso de receção. Se, após algum tempo, verificar que não recebeu o aviso de receção de um determinado segmento n.º n, retomará a transmissão dos segmentos a partir desse ponto.

9.3.3. A relação cliente-servidor

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

9.3.4. Arquitetura de um cliente

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

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

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

O programa servidor trata de forma diferente o pedido de ligação inicial de um cliente das suas solicitações posteriores destinadas a obter um serviço. O programa não presta o serviço propriamente dito. Se o fizesse, durante o período em que o serviço estivesse a ser prestado, deixaria de estar à escuta dos pedidos de ligação e os clientes não seriam, então, atendidos. Por isso, procede de outra forma: assim que uma solicitação de ligação é recebida na porta de escuta e, em seguida, aceite, o servidor cria uma tarefa encarregada de prestar o serviço solicitado pelo cliente. Este serviço é prestado numa outra porta da máquina servidor, denominada porta de serviço. Desta forma, é possível atender vários clientes ao mesmo tempo. Uma tarefa de serviço terá a seguinte estrutura:

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

9.3.6. A classe TcpClient

A classe TcpClient é a classe adequada para representar o cliente de 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 hostname
 As String,ByVal port As Integer)
cria uma ligação TCP com o servidor a operar na porta indicada (port) da máquina indicada (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()
encerra a ligação ao servidor TCP
Public Function GetStream()
 As NetworkStream
obtém um fluxo NetworkStream de leitura e escrita para o servidor. É este fluxo que permite as trocas entre o cliente e o 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, é interessante utilizar os objetos StreamReader e StreamWriter para ler e escrever essas linhas no fluxo de rede. Quando duas máquinas comunicam entre si, existe, em cada extremidade da ligação, um objeto TcpClient. O método GetStream deste objeto permite aceder 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 através de um objeto TcpClient client1, pelo qual trocam linhas de texto, poderá 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 de client1 não passará por um buffer intermédio, mas irá diretamente para a rede. Este ponto é importante. Em geral, quando client1 envia uma linha de texto ao seu parceiro, espera uma resposta. Essa resposta nunca chegará se a linha tiver sido, na realidade, armazenada num buffer na máquina M1 e nunca tiver sido enviada. Para enviar uma linha de texto para a máquina M2, escrever-se-á:

client1.WriteLine("un texte")

Para ler a resposta de M2, escrever-se-á:

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

9.3.8. Arquitetura básica de um cliente de Internet

Temos agora os elementos necessários para definir a arquitetura básica de um cliente de Internet:


    Dim client As TcpClient = Nothing     ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing     ' le flux d'écriture du client
    Dim demande As String = Nothing         ' demande du client
    Dim réponse As String = Nothing         ' réponse du serveur

    Try
      ' estabelece-se ligação ao serviço em execução na porta P da máquina M
      client = New TcpClient(nomServeur, port)

      ' criam-se os fluxos de entrada e saída do cliente TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' ciclo de pedido-resposta
      While True
        ' prepara-se o pedido
        demande = ...
        ' envia-se a solicitação para o servidor
        OUT.WriteLine(demande)
        ' lê-se a resposta do servidor
        réponse = [IN].ReadLine()
        ' processa-se a resposta
        ...
            End While
            ' concluído
            client.Close()
        Catch ex As Exception
      ' gestiona-se a exceção
...
        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á aguardar (listen) pelos pedidos dos clientes numa porta passada como parâmetro (port), denominada porta de escuta da máquina local com a morada IP localadr.
Public Function AcceptTcpClient() 
As TcpClient
aceita o pedido de um cliente. Devolve como resultado um objeto TcpClient associado a outra porta, denominada porta de serviço.
Public Sub Start()
inicia a escuta de pedidos dos clientes
Public Sub Stop()
interrompe a escuta de pedidos dos clientes

9.3.10. Arquitetura básica de um servidor da Internet

Com base no que foi visto anteriormente, é possível deduzir a estrutura básica de um servidor:


    ' cria-se o serviço de escuta
    Dim ecoute As TcpListener = Nothing
    Dim port As Integer = ...
    Try
      ' cria-se o serviço
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' inicia-se o serviço
      ecoute.Start()
      ' ciclo do serviço
      Dim liaisonClient As TcpClient = Nothing
            While not fini
                ' aguarda um cliente
                liaisonClient = ecoute.AcceptTcpClient()
                ' o serviço é assegurado por outra tarefa
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf [méthode]))
                tache.Start()
            End While
        Catch ex As Exception
            ' é sinalizado o erro
....
        End Try
        ' fim do serviço
        ecoute.Stop()

A classe Service é um thread que poderia ter o seguinte aspeto:


Public Class Service

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie

    ' construtor
    Public Sub New(ByVal liaisonClient As TcpClient, ...)
        Me.liaisonClient = liaisonClient
        ...
    End Sub

    ' método run
    Public Sub Run()
        ' retorna o serviço ao cliente
        Try
            ' fluxo de entrada
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' fluxo de saída
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' ciclo de leitura de pedido/gravação de resposta
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' processa-se a solicitação
                ...
                ' envia-se a resposta
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' próximo pedido
                demande = [IN].ReadLine
            End While
            ' fim da ligação
            liaisonClient.Close()
        Catch e As Exception
            ...
        End Try
        ' fim do serviço
    End Sub

9.4. Exemplos

9.4.1. Servidor de eco

Propomos escrever um servidor de eco que será iniciado a partir de uma janela DOS através do comando:

serveurEcho port

O servidor opera na porta indicada como parâmetro. Limita-se a reenviar ao cliente o pedido que este lhe enviou. O programa é o seguinte:


' opções
Option Explicit On 
Option Strict On

' espaços de nomes
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

' chamada: serveurEcho porta
' servidor de eco
' envia de volta ao cliente a linha que este lhe enviou

Public Class serveurEcho
  Private Shared syntaxe As String = "Syntaxe : serveurEcho port"

  ' programa principal
  Public Shared Sub Main(ByVal args() As String)

    ' existe algum argumento
    If args.Length <> 1 Then
      erreur(syntaxe, 1)
    End If
    ' esse argumento deve ser um número inteiro >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
    ' cria-se o serviço de escuta
    Dim ecoute As TcpListener = Nothing
    Dim nbClients As Integer = 0 ' nbre de clients traités
    Try
      ' cria-se o serviço
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' inicia-se o serviço
      ecoute.Start()
      ' acompanhamento
      Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & port))
      Console.Out.WriteLine(ecoute.LocalEndpoint)

      ' ciclo do serviço
      Dim liaisonClient As TcpClient = Nothing
            While True
                ' loop infinito — será interrompido com Ctrl-C
                ' à espera de um cliente
                liaisonClient = ecoute.AcceptTcpClient()

                ' o serviço é assegurado por outra tarefa
                nbClients += 1
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf New traiteClientEcho(liaisonClient, nbClients).Run))
                tache.Start()
            End While
            ' volta-se a ficar à escuta de pedidos
        Catch ex As Exception
            ' o erro é sinalizado
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
        ' fim do serviço
        ecoute.Stop()
    End Sub

    ' exibição de erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição do erro
        System.Console.Error.WriteLine(msg)
        ' encerramento com erro
        Environment.Exit(exitCode)
    End Sub
End Class

' -------------------------------------------------------
' presta serviço a um cliente do servidor de eco
Public Class traiteClientEcho

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private numClient As Integer    ' n° de client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie

    ' construtor
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal numClient As Integer)
        Me.liaisonClient = liaisonClient
        Me.numClient = numClient
    End Sub

    ' método run
    Public Sub Run()
        ' presta o serviço ao cliente
        Console.Out.WriteLine(("Début de service au client " & numClient))
        Try
            ' fluxo de entrada
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' fluxo de saída
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' ciclo de leitura (pedido)/gravação (resposta)
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' acompanhamento
                Console.Out.WriteLine(("Client " & numClient & " : " & demande))
                ' o serviço termina quando o cliente envia um marcador de fim de ficheiro
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' o serviço termina quando o cliente envia «fim»
                If demande.Trim().ToLower() = "fin" Then
                    Exit While
                End If
                ' próximo pedido
                demande = [IN].ReadLine
            End While
            ' fim da ligação
            liaisonClient.Close()
        Catch e As Exception
            erreur("Erreur lors de la fermeture de la liaison client (" + e.ToString + ")", 2)
        End Try
        ' fim do serviço
        Console.Out.WriteLine(("Fin de service au client " & numClient))
    End Sub

    ' exibição de erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição de erro
        System.Console.Error.WriteLine(msg)
        ' paragem com erro
        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

Este liga-se à máquina nomServeur na porta port e, em seguida, envia ao servidor linhas de texto que este lhe devolve como eco.


' opções
Option Explicit On 
Option Strict On

' espaços de nomes
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

Public Class clientEcho

  ' liga-se a um servidor de eco
  ' qualquer linha digitada no teclado é então recebida como eco
  Public Shared Sub Main(ByVal args() As String)
    ' sintaxe
    Const syntaxe As String = "pg machine port"

    ' número de argumentos
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' anota-se o nome do servidor
    Dim nomServeur As String = args(0)

    ' a porta deve ser um número inteiro >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

        ' é possível trabalhar
    Dim client As TcpClient = Nothing ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
    Dim demande As String = Nothing ' demande du client
    Dim réponse As String = Nothing ' réponse du serveur
    Try
      ' liga-se ao serviço em execução na porta P da máquina M
      client = New TcpClient(nomServeur, port)

      ' criam-se os fluxos de entrada e saída do cliente TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' ciclo de pedido-resposta
      While True
        ' a solicitação provém do teclado
        Console.Out.Write("demande (fin pour arrêter) : ")
        demande = Console.In.ReadLine()
        ' envia-se para o servidor
        OUT.WriteLine(demande)
        ' lê-se a resposta do servidor
        réponse = [IN].ReadLine()
        ' processa-se a resposta
        Console.Out.WriteLine(("Réponse : " + réponse))
        ' Concluído?
        If demande.Trim().ToLower() = "fin" Then
          Exit While
                End If
            End While
            ' Está concluído
            client.Close()
        Catch ex As Exception
      ' trata-se da exceção
      erreur(ex.Message, 3)
        End Try
    End Sub

    ' exibição dos erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição do erro
        System.Console.Error.WriteLine(msg)
        ' encerramento com erro
        Environment.Exit(exitCode)
    End Sub
End Class

A estrutura deste cliente está em conformidade com a arquitetura geral dos clientes tcp.Voici, conforme os resultados obtidos na seguinte configuração:

  • o servidor é iniciado 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, obtêm-se 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]

Na do 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]

Na 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 em simultâneo.

9.4.3. Um cliente genérico TCP

Muitos dos serviços criados nos primórdios da Internet funcionam segundo o modelo do servidor de eco analisado anteriormente: as interações cliente-servidor realizam-se através da 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 do servidor serveur. Feito isto, criará duas threads:

  1. um thread encarregado de ler os comandos digitados no teclado e de os enviar para o servidor
  2. um thread encarregado de ler as respostas do servidor e de as apresentar no ecrã

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

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

Por isso, o ciclo de envio de uma única linha ao servidor — receção de uma única linha enviada pelo servidor — nem sempre é adequado. Vamos, portanto, criar dois ciclos separados:

  • um ciclo de leitura dos comandos digitados no teclado para serem enviados ao servidor. O utilizador indicará o fim dos comandos com a palavra-chave fin.
  • um ciclo de receção e exibição das respostas do servidor. Este será um ciclo infinito que só será interrompido pelo encerramento do fluxo de rede pelo servidor ou pelo utilizador, que digitará no teclado o comando fin.

Para que estes dois ciclos fiquem 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 (SendMail Transfer Protocol). Este serviço é responsável pelo encaminhamento do correio eletrónico aos destinatários. Funciona na porta 25 e utiliza um protocolo de comunicação do tipo troca de linhas de 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 entre cliente e 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 dispõem de um comando help que fornece indicações sobre os comandos que podem ser utilizados com o serviço. Neste caso, não é esse o caso. Os comandos SMTP utilizados no exemplo são os seguintes:
    • mail from: expéditeur, para indicar o endereço de e-mail do remetente da mensagem
    • rcpt to: destinataire, para indicar o endereço de e-mail do destinatário da mensagem. Se houver vários destinatários, o comando rcpt to: é repetido tantas vezes quantas forem necessárias para cada um dos destinatários.
    • data, que sinaliza ao servidor SMTP que se vai enviar a mensagem. Conforme indicado na resposta do servidor, trata-se de uma sequência de linhas terminada por uma linha que contém apenas o caractere ponto. Uma mensagem pode ter cabeçalhos separados do corpo da mensagem por uma linha em branco. No nosso exemplo, colocámos um assunto com a palavra-chave Subject:
  • assim que a mensagem for enviada, pode-se indicar ao servidor que terminámos com o comando quit. O servidor encerra então a ligação réseau.Le; o thread de leitura pode detetar este evento e parar.
  • O utilizador digita então «fin» no teclado para interromper também o thread de leitura dos comandos digitados no teclado.

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

Image

Note-se 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 from de uma mensagem. Neste caso, 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 os clientes desses serviços. Vamos descobrir o protocolo de comunicação do serviço POP (Post Office Protocol), que permite recuperar os 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:

  • user login, onde se introduz o nome de utilizador na máquina que armazena os nossos e-mails
  • pass password, onde se introduz a palavra-passe associada ao nome de utilizador anterior
  • list, para obter a lista de mensagens com o número e o tamanho em bytes
  • retr i, para ler a mensagem n.º i
  • quit, para encerrar a sessão.

Vamos agora descobrir o protocolo de comunicação entre um cliente e um servidor Web, que normalmente opera 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 ao servidor de acordo com o seguinte esquema:

commande1
commande2
...
commanden
[ligne vide]

Só depois de receber a linha vazia é que o servidor Web responde. No exemplo, utilizámos apenas um comando:

GET /index.html HTTP/1.0

que solicita ao servidor o URL /index.html e indica que está a trabalhar com o protocolo HTTP versão 1.0. 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, uma vez que se observa que o thread de leitura das respostas terminou. Antes de enviar o conteúdo do ficheiro index.html, o servidor web enviou uma série de cabeçalhos terminada por uma linha em branco:

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

A linha <html> é a primeira linha do ficheiro /index.html. O que precede denomina-se cabeçalhos HTTP (Protocolo de Transferência HyperText). Não vamos detalhar aqui estes cabeçalhos, mas convém lembrar que o nosso cliente genérico permite o acesso aos mesmos, o que pode ser útil para os compreender. A primeira linha, por exemplo:

<-- HTTP/1.1 200 OK

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

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

informam ao cliente que este irá receber 11 251 bytes correspondentes ao texto HTML (HyperText Markup Language) e que, no final da transmissão, a ligação será encerrada. Temos, portanto, aqui um cliente TCP muito prático. Na verdade, este cliente já existe nas máquinas, onde se chama telnet, mas foi interessante escrevê-lo nós próprios. O programa do cliente TCP genérico é o seguinte:


' espaços de nomes
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

' a classe
Public Class clientTcpGénérique
   
    
   ' recebe como parâmetro as características de um serviço na forma
   ' servidor porta
   ' liga-se ao serviço
   ' cria um thread para ler os comandos digitados no teclado
   ' estas serão enviadas para o servidor
   ' cria um thread para ler as respostas do servidor
   ' estas serão apresentadas no ecrã
   ' e o processo termina com o comando «fin» digitado no teclado
  Public Shared Sub Main(ByVal args() As String)

    ' sintaxe
    Const syntaxe As String = "pg serveur port"

    ' número de argumentos
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' anota-se o nome do servidor
    Dim serveur As String = args(0)

    ' a porta deve ser um número inteiro >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
    ' podem ocorrer problemas
    Try
      ' estabelece-se a ligação ao serviço
      client = New TcpClient(serveur, port)
    Catch ex As Exception
      ' erro
      Console.Error.WriteLine(("Impossible de se connecter au service (" & serveur & "," & port & "), erreur : " & ex.Message))
      ' fim
      Return
        End Try
        ' criam-se os threads de leitura/gravação
        Dim thReceive As New Thread(New ThreadStart(AddressOf New clientReceive(client).Run))
        Dim thSend As New Thread(New ThreadStart(AddressOf New clientSend(client).Run))

        ' inicia-se a execução das duas threads
        thSend.Start()
        thReceive.Start()

        ' fim do thread principal
        Return
    End Sub

    ' exibição de erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição de erro
        System.Console.Error.WriteLine(msg)
        ' paragem com erro
        Environment.Exit(exitCode)
    End Sub
End Class

Public Class clientSend
    ' classe responsável por ler os comandos digitados no teclado
    ' e de as enviar para um servidor através de um cliente TCP passado ao construtor
    Private client As TcpClient    ' le client tcp

    ' construtor
    Public Sub New(ByVal client As TcpClient)
        ' indica-se o cliente TCP
        Me.client = client
    End Sub

    ' método Run da thread
    Public Sub Run()

        ' dados locais
        Dim OUT As StreamWriter = Nothing        ' flux d'écriture réseau
        Dim commande As String = Nothing        ' commande lue au clavier
        ' gestão de erros
        Try
            ' criação do fluxo de escrita de rede
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' ciclo de introdução e envio de comandos
            Console.Out.WriteLine("Commandes : ")
            While True
                ' leitura do comando digitado no teclado
                commande = Console.In.ReadLine().Trim()
                ' Concluído?
                If commande.ToLower() = "fin" Then
                    Exit While
                End If
                ' envio do comando para o servidor
                OUT.WriteLine(commande)
            End While
        Catch ex As Exception
            ' erro
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fim - encerramos os fluxos
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' notifica-se o fim do thread
        Console.Out.WriteLine("[fin du thread d'envoi des commandes au serveur]")
    End Sub
End Class


Public Class clientReceive
    ' classe encarregada de ler as linhas de texto destinadas a um 
    ' cliente TCP passado ao construtor
    Private client As TcpClient    ' le client tcp

    ' construtor
    Public Sub New(ByVal client As TcpClient)
        ' regista-se o cliente TCP
        Me.client = client
    End Sub

    'construtor
    ' método Run da thread
    Public Sub Run()

        ' dados locais
        Dim [IN] As StreamReader = Nothing        ' flux lecture réseau
        Dim réponse As String = Nothing        ' réponse serveur
        ' gestão de erros
        Try
            ' criação do fluxo de leitura de rede
            [IN] = New StreamReader(client.GetStream())
            ' loop de leitura de linhas de texto do fluxo IN
            While True
                ' leitura do fluxo de rede
                réponse = [IN].ReadLine()
                ' fluxo encerrado?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' exibição
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' erro
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fim - encerramento dos fluxos
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' notifica-se o fim do 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 centrar-nos num servidor

  • que exibe no ecrã os comandos enviados pelos seus clientes
  • e lhes envia, como resposta, as linhas de texto digitadas no teclado por um utilizador. É, portanto, este último que funciona como servidor.

O programa é iniciado com: srvgen portEcoute, em que portEcoute é a porta à qual os clientes devem ligar-se. O serviço ao cliente será assegurado por duas threads:

  • um thread dedicado exclusivamente à leitura das linhas de texto enviadas pelo cliente
  • um thread dedicado exclusivamente à leitura das respostas digitadas pelo utilizador. Este indicará, através do comando «fin», que encerra 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 servidor, por sua vez, nunca se encerra, a não ser que o utilizador prima Ctrl-C no teclado. Vejamos alguns exemplos.

O servidor é iniciado na porta 100 e utiliza-se o cliente genérico para comunicar com ele. A janela do cliente é a seguinte:

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 por <-- são as enviadas do servidor para o cliente; as restantes são as enviadas 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 por <-- são as enviadas do cliente para o servidor. As linhas N: são as enviadas do servidor para o cliente n.º N. O servidor acima ainda está ativo, embora o cliente 1 já tenha terminado. Inicia-se 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 fica então assim:

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

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

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

Vamos agora abrir um navegador e aceder à página http://localhost:88/exemple.html. O navegador irá então ligar-se à porta 88 da máquina localhost e, em seguida, solicitar a página /exemple.html:

Image

Vejamos agora a 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
<--

Descobrimos assim os cabeçalhos HTTP enviados pelo navegador. Isto permite-nos descobrir, pouco a pouco, o protocolo HTTP. Num exemplo anterior, criámos um cliente Web que enviava apenas o comando GET. Isso tinha sido suficiente. Vemos aqui que o navegador envia outras informações para o servidor. O objetivo destas informações é indicar ao servidor que tipo de cliente tem diante de si. Vemos também que os cabeçalhos HTTP terminam com uma linha em branco. Vamos elaborar uma resposta para o nosso cliente. O utilizador ao teclado é, neste caso, o verdadeiro servidor e pode elaborar uma resposta manualmente. Recordemos a resposta dada 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 dar uma resposta semelhante:

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

As linhas que começam por 2: são enviadas do servidor para o cliente n.º 2. O comando fin encerra a ligação do servidor ao cliente. Limitaram-nos, na nossa resposta, aos seguintes cabeçalhos HTTP:

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

Não indicamos o tamanho do ficheiro que vamos enviar (Content-Length), mas limitamo-nos a indicar que vamos encerrar a ligação (Connection: close) após o envio do mesmo. Isso é suficiente para o navegador. Ao verificar que a ligação foi encerrada, o navegador saberá que a resposta do servidor está concluída e apresentará a página HTML que lhe foi enviada. Esta 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>

Em seguida, o utilizador encerra a ligação ao cliente digitando o comando fin. O navegador fica então a saber que a resposta do servidor está concluída e pode, assim, apresentá-la:

Image

Se, no exemplo acima, introduzirmos Affichage/Source para ver o que o navegador recebeu, obtemos:

Image

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


' espaços de nomes
Imports System
Imports System.Net
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

Public Class serveurTcpGénérique

    ' programa principal
    Public Shared Sub Main(ByVal args() As String)

        ' recebe a porta de escuta das solicitações dos clientes
        ' cria um thread para ler os pedidos do cliente
        ' estas serão apresentadas no ecrã
        ' cria um thread para ler os comandos digitados no teclado
        ' estas serão enviadas como resposta ao cliente
        ' tudo termina com o comando «fin» digitado no teclado

        Const syntaxe As String = "Syntaxe : pg port"

        ' existe algum argumento
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' esse argumento deve ser um número inteiro >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
        ' Cria-se o serviço de escuta
        Dim ecoute As TcpListener = Nothing
        Dim nbClients As Integer = 0     ' nbre de clients traités
        Try
            ' criamos o serviço
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
            ' inicia-se o serviço
            ecoute.Start()
            ' acompanhamento
            Console.Out.WriteLine(("Serveur générique lancé sur le port " & port))

            ' ciclo de atendimento aos clientes
            Dim client As TcpClient = Nothing
            While True        ' boucle infinie - sera arrêtée par Ctrl-C
                ' aguarda um cliente
                client = ecoute.AcceptTcpClient()

                ' o serviço é executado em threads separadas
                nbClients += 1
                ' thread de leitura dos pedidos dos clientes
                Dim thReceive As New Thread(New ThreadStart(AddressOf New serveurReceive(client, nbClients).Run))
                ' thread de leitura das respostas digitadas pelo utilizador
                Dim thSend As New Thread(New ThreadStart(AddressOf New serveurSend(client, nbClients).Run))

                ' inicia-se a execução das duas threads
                thSend.Start()
                thReceive.Start()
            End While
            ' volta-se a ficar à espera de pedidos
        Catch ex As Exception
            ' é sinalizado o erro
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub

    ' exibição dos erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição do erro
        System.Console.Error.WriteLine(msg)
        ' encerramento com erro
        Environment.Exit(exitCode)
    End Sub
End Class


Public Class serveurSend
    ' classe responsável por ler as respostas digitadas no teclado
    ' e de as enviar a um cliente através de um cliente TCP passado ao construtor
    Private client As TcpClient    ' le client tcp
    Private numClient As Integer    ' n° de client

    ' construtor
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' regista-se o cliente TCP
        Me.client = client
        ' e o seu n.º
        Me.numClient = numClient
    End Sub

    ' método Run da thread
    Public Sub Run()

        ' dados locais
        Dim OUT As StreamWriter = Nothing        ' flux d'écriture réseau
        Dim réponse As String = Nothing        ' réponse lue au clavier
        ' acompanhamento
        Console.Out.WriteLine(("Thread de lecture des réponses du serveur au client " & numClient & " lancé"))
        ' gestão de erros
        Try
            ' criação do fluxo de gravação na rede
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' ciclo de introdução e envio de comandos
            While True
                ' identificação do cliente
                Console.Out.Write((numClient & " : "))
                ' leitura da resposta digitada no teclado
                réponse = Console.In.ReadLine().Trim()
                ' terminado?
                If réponse.ToLower() = "fin" Then
                    Exit While
                End If
                ' envio da resposta para o servidor
                OUT.WriteLine(réponse)
            End While
            ' resposta seguinte
        Catch ex As Exception
            ' erro
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fim - encerramento dos fluxos
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' notifica-se o fim do thread
        Console.Out.WriteLine(("[fin du Thread de lecture des réponses du serveur au client " & numClient & "]"))
    End Sub
End Class

Public Class serveurReceive
    ' classe responsável por ler as linhas de texto enviadas ao servidor 
    ' através de um cliente TCP passado ao construtor
    Private client As TcpClient     ' le client tcp
    Private numClient As Integer    ' n° de client

    ' construtor
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' regista-se o cliente TCP
        Me.client = client
        ' e o seu número
        Me.numClient = numClient
    End Sub

    ' método Run da thread
    Public Sub Run()
        ' dados locais
        Dim [IN] As StreamReader = Nothing        ' flux lecture réseau
        Dim réponse As String = Nothing        ' réponse serveur
        ' acompanhamento
        Console.Out.WriteLine(("Thread de lecture des demandes du client " & numClient & " lancé"))
        ' gestão de erros
        Try
            ' criação do fluxo de leitura de rede
            [IN] = New StreamReader(client.GetStream())
            ' ciclo de leitura de linhas de texto do fluxo IN
            While True
                ' leitura do fluxo de rede
                réponse = [IN].ReadLine()
                ' fluxo encerrado?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' exibição
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' erro
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fim - encerrar fluxos
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' notifica-se o fim do 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 que um navegador enviava:

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

Vamos criar um cliente Web ao qual seria passado como parâmetro um URL e que exibiria no ecrã o texto enviado pelo servidor. Partiremos do princípio de que este suporta o protocolo HTTP 1.1. Dos cabeçalhos anteriores, utilizaremos apenas os seguintes:

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

Se, no exemplo acima, substituirmos GET por HEAD, 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: clientweb URL cmd, em que URL é aURL pretendida e «cmd» uma das duas palavras-chave «GET» ou «HEAD» para indicar se se pretende 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 mudou de local (ou seja, de URL). O novo endereço URL é indicado pelo cabeçalho Location:

Location: /IISSamples/Default/welcome.htm

Se utilizarmos GET em vez de HEAD na chamada ao 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>

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


' espaços de nomes
Imports System
Imports System.Net.Sockets
Imports System.IO


Public Class clientWeb1

    ' solicita um URL
    ' exibe o conteúdo da mesma no ecrã
    Public Shared Sub Main(ByVal args() As String)
        ' sintaxe
        Const syntaxe As String = "pg URI GET/HEAD"

        ' número de argumentos
        If args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' regista-se o URI solicitado
        Dim URIstring As String = args(0)
        Dim commande As String = args(1).ToUpper()

        ' verificação da validade do URI
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI incorreto
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try
        ' verificação do pedido
        If commande <> "GET" And commande <> "HEAD" Then
            ' encomenda incorreta
            erreur("Le second paramètre doit être GET ou HEAD", 3)
        End If

        ' é possível trabalhar
        Dim client As TcpClient = Nothing        ' le client
        Dim [IN] As StreamReader = Nothing        ' le flux de lecture du client
        Dim OUT As StreamWriter = Nothing        ' le flux d'écriture du client
        Dim réponse As String = Nothing        ' réponse du serveur
        Try
            ' ligamo-nos ao servidor
            client = New TcpClient(uri.Host, uri.Port)

            ' estão a ser criados os fluxos de entrada e saída do cliente TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True

            ' solicita-se o URL - envio dos cabeçalhos HTTP
            OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
            OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
            OUT.WriteLine("Connection: close")
            OUT.WriteLine()
            ' lê-se a resposta
            réponse = [IN].ReadLine()
            While Not (réponse Is Nothing)
                ' processa-se a resposta
                Console.Out.WriteLine(réponse)
                ' leitura da resposta
                réponse = [IN].ReadLine()
            End While
            ' concluído
            client.Close()
        Catch e As Exception
            ' gestão da exceção
            erreur(e.Message, 4)
        End Try
    End Sub

    ' exibição dos erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição do erro
        System.Console.Error.WriteLine(msg)
        ' encerramento com erro
        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) com o formato http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... A classe Uri permite-nos decompor a cadeia do URL nos seus diferentes elementos. É criado um objeto Uri a partir da cadeia URIstring recebida como parâmetro:


        ' verificação da validade do URI
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI incorreto
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

Se a cadeia «URI» recebida como parâmetro não for válida (ausência do protocolo, do servidor, etc.), é lançada uma exceção. Isto permite-nos verificar a validade do parâmetro recebido. Depois de construído o objeto Uri, temos acesso aos diferentes elementos dessa Uri. Assim, se o objeto uri do código anterior tiver sido construído a partir da cadeia http://serveur:port/cheminPageHTML?param1=val1;param2=val2;..., teremos:

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

9.4.6. Cliente Web que gere os redirecionamentos

O cliente Web anterior não gere uma eventual redireção do URL que solicitou. O cliente seguinte gere-a.

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

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:


' espaços de nomes
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

' classe do cliente web
Public Class clientWeb
   
  ' solicita um URL e apresenta o seu conteúdo no ecrã
  Public Shared Sub Main(ByVal args() As String)
    ' sintaxe
    Const syntaxe As String = "pg URI GET/HEAD"

    ' número de argumentos
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' regista-se o URI solicitado
    Dim URIstring As String = args(0)
    Dim commande As String = args(1).ToUpper()

    ' verificação da validade do URI
    Dim uri As Uri = Nothing
    Try
      uri = New Uri(URIstring)
    Catch ex As Exception
      ' URI incorreto
      erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
    End Try 'catch
    ' verificação do pedido
    If commande <> "GET" And commande <> "HEAD" Then
      ' encomenda incorreta
      erreur("Le second paramètre doit être GET ou HEAD", 3)
    End If

    ' é possível trabalhar
    Dim client As TcpClient = Nothing ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
    Dim réponse As String = Nothing ' réponse du serveur
    Const nbRedirsMax As Integer = 1 ' pas plus d'une redirection acceptée
    Dim nbRedirs As Integer = 0 ' nombre de redirections en cours
    Dim premièreLigne As String ' 1ère ligne de la réponse
    Dim redir As Boolean = False ' indique s'il y a redirection ou non
    Dim locationString As String = "" ' la chaîne URI d'une éventuelle redirection
    ' expressão regular para encontrar um URL de redirecionamento
    Dim location As New Regex("^Location: (.+?)$") '

        ' gestão de erros
    Try
      ' é possível ter vários URL a consultar, caso haja redirecionamentos
      While nbRedirs <= nbRedirsMax
        ' estabelece-se ligação ao servidor
        client = New TcpClient(uri.Host, uri.Port)

        ' criam-se os fluxos de entrada e saída do cliente TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' envia-se os cabeçalhos HTTP para solicitar o URL
        OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
        OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
        OUT.WriteLine("Connection: close")
        OUT.WriteLine()

        ' lê-se a primeira linha da resposta
        premièreLigne = [IN].ReadLine()
        ' eco no ecrã
        Console.Out.WriteLine(premièreLigne)

        ' redirecionamento?
        If Regex.IsMatch(premièreLigne, "302 Object moved$") Then
          ' existe um redirecionamento
          redir = True
          nbRedirs += 1
                End If

                ' seguem-se os cabeçalhos HTTP até encontrar a linha vazia que indica o fim dos cabeçalhos
                Dim locationFound As Boolean = False
                réponse = [IN].ReadLine()
                While réponse <> ""
                    ' exibe-se a resposta
                    Console.Out.WriteLine(réponse)
                    ' se houver redirecionamento, procura-se o cabeçalho Location
                    If redir And Not locationFound Then
                        ' compara-se a linha com a expressão relacional «location»
                        Dim résultat As Match = location.Match(réponse)
                        If résultat.Success Then
                            ' se for encontrada, regista-se o URL de redirecionamento
                            locationString = résultat.Groups(1).Value
                            ' regista-se que foi encontrada
                            locationFound = True
                        End If
                    End If
                    ' linha seguinte
                    réponse = [IN].ReadLine()
                End While

                ' linhas seguintes da resposta
                Console.Out.WriteLine(réponse)
                réponse = [IN].ReadLine()
                While Not (réponse Is Nothing)
                    ' exibe-se a resposta
                    Console.Out.WriteLine(réponse)
                    ' linha seguinte
                    réponse = [IN].ReadLine()
                End While

                ' a ligação está a ser encerrada
                client.Close()
                ' já terminámos?
                If Not locationFound Or nbRedirs > nbRedirsMax Then
                    Exit While
                End If

                ' é necessário efetuar um redirecionamento — estamos a construir a nova URI
                URIstring = uri.Scheme + "://" & uri.Host & ":" & uri.Port & locationString
                uri = New Uri(URIstring)
                ' acompanhamento
                Console.Out.WriteLine((ControlChars.Lf + "<--Redirection vers l'URL " + URIstring + "-->" + ControlChars.Lf))
            End While
        Catch e As Exception
      ' a exceção está a ser tratada
      erreur(e.Message, 4)
        End Try
    End Sub

    ' exibição dos erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição do erro
        System.Console.Error.WriteLine(msg)
        ' encerramento com erro
        Environment.Exit(exitCode)
    End Sub
End Class

9.4.7. Servidor de cálculo de impostos

Retomamos o exercício IMPOTS, já abordado de várias formas. Recorde-se a última versão. Foi criada uma classe «imposto». Os seus atributos são três tabelas de números:


Public Class impôt
    ' os dados necessários para o cálculo do imposto
    ' provêm de uma fonte externa
    Private limites(), coeffR(), coeffN() as double

A classe tem dois construtores:

  • um construtor ao qual se passam os três tabuletos de dados necessários para o cálculo do imposto
     // fabricante 1
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
         ' inicializa as três tabelas de limites, coeffR, coeffN a partir
         ' dos parâmetros passados ao construtor
  • um construtor ao qual se passa o nome DSN de uma base de dados ODBC

    ' construtor 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' inicializa as três matrizes de limites, coeffR, coeffN a partir de
        ' do conteúdo da tabela Timpots da base de dados ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN são as três colunas desta tabela
        ' pode lançar uma exceção

Tinha sido 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

Neste caso, o programa de teste e o objeto impôt estavam na mesma máquina. Propomos colocar o programa de teste e o objeto impôt em máquinas diferentes. Teremos uma aplicação cliente-servidor em que o objeto remoto impôt será o servidor. A nova classe chama-se ServeurImpots e deriva da classe impôt:


Public Class ServeurImpots
    Inherits impôt

    ' atributos
    Private portEcoute As Integer    ' le port d'écoute des demandes clients
    Private actif As Boolean    ' état du serveur

    ' construtor
    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)
        ' observa-se a porta de escuta
        Me.portEcoute = portEcoute
        ' por enquanto inativo
        actif = False
        ' cria e inicia um thread para ler os comandos digitados no teclado
        ' o servidor será gerido a partir destes comandos
        Dim threadLecture As Thread = New Thread(New ThreadStart(AddressOf admin))
        threadLecture.Start()
    End Sub

O único parâmetro novo no construtor é a porta de escuta das solicitações dos clientes. Os restantes parâmetros são passados diretamente para a classe base impôt. O servidor de impostos é controlado por comandos digitados no teclado. Por isso, criamos um thread 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 gere estes comandos é o seguinte:


    Public Sub admin()
        ' lê os comandos de administração do servidor introduzidos pelo teclado
        ' num ciclo infinito
        Dim commande As String = Nothing
        While True
            ' solicita
            Console.Out.Write("Serveur d'impôts>")
            ' leitura do comando
            commande = Console.In.ReadLine().Trim().ToLower()
            ' execução do comando
            If commande = "start" Then
                ' ativo?
                If actif Then
                    'erro
                    Console.Out.WriteLine("Le serveur est déjà actif")
                Else
                    ' inicia-se o serviço de escuta
                    Dim threadEcoute As Thread = New Thread(New ThreadStart(AddressOf ecoute))
                    threadEcoute.Start()
                End If
            Else
                If commande = "stop" Then
                    ' fim de todos os threads de execução
                    Environment.Exit(0)
                Else
                    ' erro
                    Console.Out.WriteLine("Commande incorrecte. Utilisez (start,stop)")
                End If
            End If
        End While
    End Sub

Se o comando digitado no teclado for start, é iniciado um thread para ouvir os pedidos dos clientes. Se o comando digitado for stop, todos os threads são parados. O thread de escuta executa o método ecoute:


    Public Sub ecoute()
        ' thread de escuta de pedidos dos clientes
        ' criação do serviço de escuta
        Dim ecoute As TcpListener = Nothing
        Try
            ' criação do serviço
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), portEcoute)
            ' inicia-se o serviço
            ecoute.Start()
            ' acompanhamento
            Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & portEcoute))

            ' ciclo do serviço
            Dim liaisonClient As TcpClient = Nothing
            While True            ' boucle infinie
                ' aguarda um cliente
                liaisonClient = ecoute.AcceptTcpClient()
                ' o serviço é assegurado por outra tarefa
                Dim threadClient As Thread = New Thread(New ThreadStart(AddressOf New traiteClientImpots(liaisonClient, Me).Run))
                threadClient.Start()
            End While
            ' volta-se a ficar à escuta de pedidos
        Catch ex As Exception
            ' é sinalizado o erro
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub

    ' exibição dos erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição do erro
        System.Console.Error.WriteLine(msg)
        ' encerramento com erro
        Environment.Exit(exitCode)
    End Sub

Encontramos um servidor TCP clássico a escutar na porta portEcoute. Os pedidos dos clientes são processados pelo método Run de um objeto ao qual são passados dois parâmetros:

  1. o objeto TcpClient, que permitirá aceder ao cliente
  2. o objeto impôt this, que dará acesso ao método this.calculer para o cálculo do imposto.

' -------------------------------------------------------
' presta serviço a um cliente do servidor de impostos
Public Class traiteClientImpots

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie
    Private objImpôt As impôt     ' objet Impôt

    ' construtor
    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. cálculo para casados (s/n) nbEnfants salaireAnnuel
  2. cálculos financeiros

A forma 1 permite o cálculo de um imposto; a forma 2 encerra a ligação cliente-servidor.


    ' método Run
    Public Sub Run()
        ' presta o serviço ao cliente
        Try
            ' fluxo de entrada
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' fluxo de saída
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' envio de uma mensagem de boas-vindas ao cliente
            OUT.WriteLine("Bienvenue sur le serveur d'impôts")

            ' ciclo de leitura de pedido/gravação de resposta
            Dim demande As String = Nothing
            Dim champs As String() = Nothing            ' les éléments de la demande
            Dim commande As String = Nothing            ' la commande du client : calcul ou fincalculs
            demande = [IN].ReadLine()
            While Not (demande Is Nothing)
                ' decompõe-se a solicitação em campos
                champs = Regex.Split(demande.Trim().ToLower(), "\s+")
                ' duas solicitações aceites: cálculo e fim dos cálculos
                commande = champs(0)
                Dim erreur As Boolean = False
                If commande <> "calcul" And commande <> "fincalculs" Then
                    ' erro do cliente
                    OUT.WriteLine("Commande incorrecte. Utilisez (calcul,fincalculs).")
                End If
                If commande = "calcul" Then
                    calculerImpôt(champs)
                End If
                If commande = "fincalculs" Then
                    ' mensagem de despedida ao cliente
                    OUT.WriteLine("Au revoir...")
                    ' libertação de recursos
                    Try
                        OUT.Close()
                        [IN].Close()
                        liaisonClient.Close()
                    Catch
                    End Try
                    ' fim
                    Return
                End If
                ' nova solicitação
                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 é efetuado pelo método calculerImpôt, que recebe como parâmetro a tabela de campos do pedido efetuado pelo cliente. A validade do pedido é verificada e, se for o caso, o imposto é calculado e devolvido ao cliente.


    ' cálculo de impostos
    Public Sub calculerImpôt(ByVal champs() As String)
        ' processa o pedido: cálculo para casados nbEnfants salaireAnnuel
        ' descomposto em campos na tabela de campos
        Dim marié As String = Nothing
        Dim nbEnfants As Integer = 0
        Dim salaireAnnuel As Integer = 0

        ' validade dos argumentos
        Try
            ' são necessários, pelo menos, 4 campos
            If champs.Length <> 4 Then
                Throw New Exception
            End If
            ' casado
            marié = champs(1)
            If marié <> "o" And marié <> "n" Then
                Throw New Exception
            End If
            ' filhos
            nbEnfants = Integer.Parse(champs(2))
            ' salário
            salaireAnnuel = Integer.Parse(champs(3))
        Catch
            OUT.WriteLine(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel")
            ' concluído
            Exit Sub
        End Try
        ' é possível calcular o imposto
        Dim impot As Long = objImpôt.calculer(marié = "o", nbEnfants, salaireAnnuel)
        ' envia-se a resposta ao cliente
        OUT.WriteLine(impot.ToString)
    End Sub

    ' exibição de erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição de erro
        System.Console.Error.WriteLine(msg)
        ' encerramento com erro
        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 da classe impôt. Um programa de teste poderia ser o seguinte:


' espaços de nomes
Imports System
Imports System.IO
Imports Microsoft.VisualBasic

Public Class testServeurImpots
    Public Shared syntaxe As String = "Syntaxe : pg port dsnImpots Timpots colLimites colCoeffR colCoeffN"

    ' programa principal
    Public Shared Sub Main(ByVal args() As String)

        ' são necessários 6 argumentos
        If args.Length <> 6 Then
            erreur(syntaxe, 1)
        End If
        ' a porta deve ser um número inteiro >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
        ' a criar o servidor de impostos
        Try
            Dim srvimots As ServeurImpots = New ServeurImpots(port, args(1), args(2), args(3), args(4), args(5))
        Catch ex As Exception
            'erro
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
    End Sub

    ' exibição de erros
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' exibição de erros
        System.Console.Error.WriteLine(msg)
        ' encerramento com erro
        Environment.Exit(exitCode)
    End Sub
End Class

Passam-se ao programa de teste os dados necessários para a construção de um objeto ServeurImpots e, a partir daí, este cria esse objeto. Este programa de teste é compilado por:

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

Eis um primeiro teste:

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 ServeurImpots que ainda não está a ouvir os pedidos dos clientes. É o comando «start», digitado no teclado, que inicia essa escuta. O comando «stop» encerra o servidor. Vamos agora utilizar um cliente. Utilizaremos o cliente genérico criado anteriormente. O servidor está em execução:

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

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

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

Voltamos à janela do servidor para o desligar:

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