Skip to content

11. Programação na Internet

11.1. Généralités

11.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), em referência aos dois protocolos principais. Pode ser útil que o leitor tenha uma compreensão geral do funcionamento das redes e, em particular, dos protocolos TCP/IP antes de abordar a construçã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

11.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 da codificação da informação (analógica ou digital)
  • a escolha do modo de transmissão (síncrono ou assíncrono).
Liaison de données
Oculta as particularidades 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.

11.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 desse modelo 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 pela 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 emissor 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á a decorrer 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 transmissores a tomar 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 uma entrega não fiável de informações entre dois clientes

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

Encontram-se aqui 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

11.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.

11.1.5. Os problemas de endereçamento na Internet

Um noeud de uma rede pode ser um computador, uma impressora inteligente, um servidor de ficheiros, ou qualquer coisa, na verdade, 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.

11.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 3 classes: classes A, B e C.

Classe A

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

R1 é o endereço 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, a morada 193.49.144.0 é a morada IP da rede da Faculdade de Ciências de Angers. Consequentemente, nenhum nó de uma rede pode ter a morada 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, ficamos apenas com 254 endereços autorizados.

11.1.5.2. Os protocolos de conversão de 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 um 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.

11.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 foi 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.

11.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 a morada 193.49.144.6 e, dentro da rede n.º 2, a morada 193.49.145.3.

A função do router é 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 endereço IP do destinatário do pacote estiver na rede n.º 2, o router enviar-lhe-á o pacote diretamente; caso contrário, enviá-lo-á para outro router, ligando a rede n.º 2 a uma rede n.º 3 e assim sucessivamente.

11.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.

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

11.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 o seguinte formato:

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

11.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 (Protocolo de Controlo de Transferência) 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 é assim 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.

11.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 iniciar 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 um computador por um nome, em vez de pela sua morada IP. Coloca-se, então, o problema da unicidade do nome: existem milhões de computadores interligados. Poder-se-ia imaginar que uma entidade centralizada atribuísse os nomes. Isso seria, sem dúvida, bastante complicado. Na realidade, o controlo dos nomes foi 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, isso poderia acontecer.

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 encontrará também 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 das máquinas.

RPC: (Chamada de Procedimento Remoto)

Também definido pela Sun, trata-se de um protocolo de comunicação entre aplicações remotas, independente da camada de transporte. Este protocolo é importante: liberta o programador 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.

11.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

11.2. As classes .NET da gestão de endereços IP

Um computador na Internet é identificado de forma única por um endereço IP (Protocolo de Internet), que pode assumir duas formas:

  • IPv4: codificado em 32 bits e representado por uma cadeia de caracteres da forma «I1.I2.I3.I4», em que In é um número entre 1 e 254. Estas são as moradas IP mais comuns atualmente.
  • IPv6: codificado em 128 bits e representado por uma cadeia do tipo «[I1.I2.I3.I4.I5.I6.I7.I8]», em que In é uma cadeia de 4 dígitos hexadecimais. Neste documento, não utilizaremos os endereços IPv6.

Uma máquina também pode ser definida 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 das máquinas. Servem apenas para facilitar a vida dos utilizadores. Assim, é mais fácil, com um navegador, aceder a http://www.ibm.com (URL) do que a URL http://129.42.17.99, embora ambos os métodos sejam possíveis.

Um computador pode ter vários endereços IP se estiver fisicamente ligado a várias redes ao mesmo tempo. Nesse caso, tem um endereço IP em cada rede.

Um endereço IP pode ser representado de duas formas em .NET:

  • sob a forma de uma cadeia de caracteres «I1.I2.I3.I4» ou «[I1.I2.I3.I4.I5.I6.I7.I8]»
  • na forma de um objeto do tipo IPAddress

A classe IPAddress

Entre os métodos M, propriedades P e constantes C da classe IPAddress, encontram-se os seguintes:

AddressFamily AddressFamily
P
família do endereço IP. O tipo AddressFamily é uma enumeração. Os dois valores comuns são:
AddressFamily.InterNetwork: para um endereço IPv4
AddressFamily.InterNetworkV6: para um endereço IPv6
IPAddress Any
C
o endereço IP «0.0.0.0». Quando um serviço está associado a este endereço, isso significa que aceita clientes em todos os endereços IP da máquina na qual opera.
IPAddress LoopBack
C
o endereço IP «127.0.0.1». Denominado «endereço de loop». Quando um serviço está associado a este endereço, isso significa que só aceita clientes que se encontram na mesma máquina que ele.
IPAdress None
C
o endereço IP «255.255.255.255». Quando um serviço está associado a este endereço, isso significa que não aceita nenhum cliente.
bool TryParse(string ipString, out IPAddress address)
M
tenta passar o endereço IP ipString na forma «I1.I2.I3.I4» como um objeto IPAddress address. Retorna true se a operação for bem-sucedida.
bool IsLoopBack
M
retorna «true» se o endereço IP for «127.0.0.1»
string ToString()
M
converte o endereço IP para a forma «I1.I2.I3.I4» ou «[I1.I2.I3.I4.I5.I6.I7.I8]»

A associação entre o endereço IP e nomMachine é assegurada por um serviço distribuído da Internet denominado DNS (Domain Name System). Os métodos estáticos da classe Dns permitem estabelecer a associação entre os endereços IP <--> nomMachine:

GetHostEntry (string hostNameOrdAddress)
retorna um endereço IPHostEntry a partir de um endereço IP na forma de uma cadeia de caracteres ou a partir de um nome de máquina. Lança uma exceção se a máquina não for encontrada.
GetHostEntry (IPAddress ip)
retorna um endereço IPHostEntry a partir de um endereço IP do tipo IPAddress. Lança uma exceção se a máquina não for encontrada.
string GetHostName()
retorna o nome da máquina na qual está a ser executado o programa que está a executar esta instrução
IPAddress[] GetHostAddresses(string hostNameOrdAddress)
retorna os endereços IP da máquina identificada pelo seu nome ou por um dos seus endereços IP.

Uma instância IPHostEntry encapsula os endereços IP, os aliases e o nome de uma máquina. O tipo IPHostEntry é o seguinte:

IPAddress[] AddressList
P
tabela de endereços IP da máquina
String[] Aliases
P
os aliases DNS da máquina. Estes são os nomes correspondentes aos diferentes endereços IP da máquina.
string HostName
P
o nome de anfitrião principal da máquina

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:


using System;
using System.Net;

namespace Chap9 {
    class Program {
        static void Main(string[] args) {
            // 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
            Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());

            // perguntas e respostas interativas
            string machine;
            IPHostEntry ipHostEntry;
            while (true) {
                // introdução do nome ou endereço IP da máquina procurada
                Console.Write("Machine recherchée (rien pour arrêter) : ");
                machine = Console.ReadLine().Trim().ToLower();
                // concluído?
                if (machine == "") return;
                // gestão de exceções
                try {
                    // pesquisa de máquina
                    ipHostEntry = Dns.GetHostEntry(machine);
                    // o nome da máquina
                    Console.WriteLine("Machine : " + ipHostEntry.HostName);
                    // os endereços IP da máquina
                    Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
                    for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
                        Console.Write(", {0}" , ipHostEntry.AddressList[i]);
                    }
                    Console.WriteLine();
                    // os aliases da máquina
                    if (ipHostEntry.Aliases.Length != 0) {
                        Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
                        for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
                            Console.Write(", {0}" , ipHostEntry.Aliases[i]);
                        }
                        Console.WriteLine();
                    }
                } catch {
                    // a máquina não existe
                    Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
                }
            }
        }
    }
}

A execução produz os seguintes resultados:

Machine Locale= LISA-AUTO2005A
Machine recherchée (rien pour arrêter) : localhost
Machine : LISA-AUTO2005A
Adresses IP : 127.0.0.1
Machine recherchée (rien pour arrêter) : 127.0.0.1
Machine : LISA-AUTO2005A
Adresses IP : 127.0.0.1
Machine recherchée (rien pour arrêter) : istia.univ-angers.fr
Machine : istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherchée (rien pour arrêter) : 193.49.146.171
Machine : istia.istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherchée (rien pour arrêter) : xx
Impossible de trouver la machine [xx]

11.3. Noções básicas de programação na Internet

11.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 do computador B
  • o número da porta com a qual a aplicação AppB funciona. Com efeito, a máquina B pode suportar várias aplicações que funcionam 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 comunicação aceite pela aplicação AppB. Com efeito, as máquinas A e B vão «comunicar» entre si. O que vão comunicar 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.

11.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.

11.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.

11.3.4. Arquitetura de um cliente

A arquitetura de um programa de rede que solicita os 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
fermer la connexion

11.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 decorrer, 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

11.4. Descubra os protocolos de comunicação da Internet:

11.4.1. Introdução

Quando um cliente se liga a um servidor, estabelece-se um diálogo entre ambos. A natureza desse diálogo constitui o que se denomina protocolo de comunicação do servidor. Entre os protocolos mais comuns da Internet, encontram-se os seguintes:

  • HTTP: HyperText Transfer Protocol — o protocolo de comunicação com um servidor web (servidor HTTP)
  • SMTP: Simple Mail Transfer Protocol — o protocolo de comunicação com um servidor de envio de correio eletrónico (servidor SMTP)
  • POP: Post Office Protocol — o protocolo de comunicação com um servidor de armazenamento de correio eletrónico (servidor POP). Trata-se aqui de recuperar os e-mails recebidos e não de os enviar.
  • FTP: File Transfer Protocol — o protocolo de comunicação com um servidor de armazenamento de ficheiros (servidor FTP).

Todos estes protocolos têm a particularidade de serem protocolos de linhas de texto: o cliente e o servidor trocam entre si linhas de texto. Se tivermos um cliente capaz de:

  • estabelecer uma ligação com um servidor TCP
  • exibir na consola as linhas de texto que o servidor lhe envia
  • enviar ao servidor as linhas de texto que um utilizador introduziria

então é possível comunicar com um servidor TCP que utilize um protocolo de linhas de texto, desde que se conheçam as regras desse protocolo.

O programa telnet, que se encontra em máquinas Unix ou Windows, é um cliente desse tipo. Nas máquinas Windows, existe também uma ferramenta chamada putty e é essa que vamos utilizar aqui. O putty pode ser descarregado na morada [http://www.putty.org/]. Trata-se de um executável (.exe) pronto a utilizar. Vamos configurá-lo da seguinte forma:

  • [1]: o endereço IP do servidor TCP ao qual pretendemos ligar-nos ou o seu nome
  • [2]: a porta de escuta do servidor TCP
  • [3]: utilizar o modo Raw, que designa uma ligação TCP bruta.
  • [4]: ativar o modo Never para impedir que a janela do cliente putty se feche caso o servidor encerre a ligação.
  • [6,7]: número de colunas/linhas da consola
  • [5]: o número máximo de linhas mantidas na memória. Um servidor HTTP pode enviar muitas linhas. É necessário poder «deslizar» por elas.
  • [8,9]: para manter os parâmetros anteriores, atribua um nome à configuração [8] e guarde-a [9].
  • [11,12]: para recuperar uma configuração guardada, selecione [11] e carregue-a em [12].

Com esta ferramenta assim configurada, vamos descobrir alguns protocolos TCP.

11.4.2. O protocolo HTTP (Protocolo de Transferência HyperText)

Vamos ligar o nosso cliente [1] ao servidor web da máquina istia.univ-angers.fr [2], porta 80 [3]:

Na consola de putty, criamos o seguinte diálogo HTTP:

GET / HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: close

HTTP/1.1 200 OK
Date: Sat, 03 May 2008 07:53:47 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=0d2e64b317; path=/
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0
  • as linhas 1-4 correspondem ao pedido do cliente, digitado no teclado
  • as linhas 5 a 19 correspondem à resposta do servidor
  • linha 1: sintaxe GET UrlDocument HTTP/1.1 — solicitamos a URL /, c.a.d. a raiz do site [istia.univ-angers.fr].
  • linha 2: sintaxe Host: máquina:porta
  • linha 3: sintaxe Connection: [mode de la connexion]. O modo [close] indica ao servidor para encerrar a ligação assim que tiver enviado a sua resposta. O modo [Keep-Alive] solicita que a ligação seja mantida aberta.
  • linha 4: linha vazia. As linhas 1-3 são denominadas cabeçalhos HTTP. Podem existir outros além dos aqui apresentados. O fim dos cabeçalhos HTTP é assinalado por uma linha vazia.
  • linhas 5-13: os cabeçalhos HTTP da resposta do servidor — terminam também aqui com uma linha vazia.
  • linhas 14-19: o documento enviado pelo servidor, neste caso um documento HTML
  • linha 5: sintaxe HTTP/1.1 código msg — o código 200 indica que o documento solicitado foi encontrado.
  • linha 6: a data e a hora do servidor
  • linha 7: identificação do software que presta o serviço web — neste caso, um servidor Apache num sistema Linux/Debian
  • linha 8: o documento foi gerado dinamicamente pelo PHP
  • linha 9: cookie de identificação do cliente — se este quiser ser reconhecido na sua próxima ligação, terá de reenviar este cookie nos seus cabeçalhos HTTP.
  • linha 10: indica que, após ter servido o documento solicitado, o servidor encerrará a ligação
  • linha 11: o documento será transmitido em partes (chunked) e não num único bloco.
  • linha 12: tipo de documento: neste caso, um documento HTML
  • linha 13: a linha vazia que indica o fim dos cabeçalhos HTTP do servidor
  • linha 14: número hexadecimal que indica o número de caracteres do primeiro bloco do documento. Quando este número for 0 (linha 19), o cliente saberá que recebeu o documento na íntegra.
  • linhas 15-18: parte do documento recebido.

A ligação foi encerrada e o cliente putty está inativo. Vamos voltar a ligar-nos a [1] e limpar o ecrã das exibições anteriores [2,3]:

Desta vez, a caixa de diálogo é a seguinte:

GET /inconnu HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: Close

HTTP/1.1 404 Not Found
Date: Sat, 03 May 2008 08:16:02 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=iso-8859-1

11a
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
                                                  <HTML><HEAD>
                                                              <TITLE>404 Not Found</TITLE>
                                                                                          </HEAD><BODY>
                                                                                                       <H1>Not Found</H1>
 The requested URL /inconnu was not found on this server.<P>
                                                            <HR>
                                                                <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
                   </BODY></HTML>

0
  • linha 1: foi solicitado um documento inexistente
  • linha 5: o servidor HTTP respondeu com o código 404, o que significa que o documento solicitado não foi encontrado.

Se solicitarmos este documento com o navegador Firefox:

Image

Se solicitarmos a visualização do código-fonte [Affichage/Code source]:

1
2
3
4
5
6
7
8
9
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>404 Not Found</TITLE>
</HEAD><BODY>
<H1>Not Found</H1>
The requested URL /inconnu was not found on this server.<P>
<HR>
<ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
</BODY></HTML>

Obtemos as linhas 13 a 22 recebidas pelo nosso cliente putty. O interesse deste é mostrar-nos, além disso, os cabeçalhos HTTP da resposta. Também é possível obter esses cabeçalhos com o Firefox.

11.4.3. O protocolo SMTP (Simple Mail Transfer Protocol)

Os servidores SMTP operam geralmente na porta 25 [2]. A ligação é estabelecida ao servidor [1]. Aqui, é geralmente necessário escolher um servidor

que pertença ao mesmo domínio IP que o computador, pois, na maioria das vezes, os servidores SMTP estão configurados para aceitar apenas pedidos de computadores que pertençam ao mesmo domínio que eles. Além disso, também é bastante frequente que os firewalls ou antivírus dos computadores pessoais estejam configurados para não aceitarem ligações à porta 25 de um computador externo. Nesse caso, pode ser necessário reconfigurar esse firewall ou antivírus.

A caixa de diálogo SMTP na janela do cliente putty é a seguinte:

220 neuf-infra-smtp-out-sp604001av.neufgp.fr neuf telecom Service relais mail ready
HELO istia.univ-angers.fr
250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
mail from: @expéditeur
250 2.1.0 <@expéditeur> sender ok
rcpt to: @destinataire
250 2.1.5 <@destinataire> destinataire ok
data
354 enter mail, end with "." on a line by itself
ligne1
ligne2
.
250 2.0.0 LwiU1Z00V4AoCxw0200000 message ok
quit
221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection

Abaixo, (D) representa um pedido do cliente e (R) uma resposta do servidor.

  • linha 1: (R) mensagem de boas-vindas do servidor SMTP
  • linha 2: (D) comando HELO para dizer «olá»
  • linha 3: (R) resposta do servidor
  • linha 4: (D) endereço do remetente, por exemplo, e-mail de: someone@gmail.com
  • linha 5: (R) resposta do servidor
  • linha 6: (D) endereço do destinatário, por exemplo, rcpt to: someoneelse@gmail.com
  • linha 7: (R) resposta do servidor
  • linha 8: (D) indica o início da mensagem
  • linha 9: (R) resposta do servidor
  • linhas 10-12: (D) a mensagem a enviar, terminada por uma linha que contém apenas um ponto.
  • linha 13: (R) resposta do servidor
  • linha 14: (D) o cliente indica que terminou
  • linha 15: (R) resposta do servidor, que em seguida encerra a ligação

11.4.4. O protocolo POP (Post Office Protocol)

Os servidores POP operam geralmente na porta 110 [2]. Estabelece-se ligação ao servidor [1]. O diálogo POP na janela do cliente putty é o seguinte:

+OK Hello there.
user xx
+OK Password required.
pass yy
+OK logged in.
list
+OK POP3 clients that break here, they violate STD53.
1 10105
2 55875
...
64 1717
.
retr 64
+OK 1717 octets follow.
Return-Path: <xx@neuf.fr>
X-Original-To: xx@univ-angers.fr
Delivered-To: xx@univ-angers.fr
....
Date: Sat,  3 May 2008 10:59:25 +0200 (CEST)
From: xx@neuf.fr
To: undisclosed-recipients:;

ligne1
ligne2
.
quit
+OK Bye-bye.
  • linha 1: (R) mensagem de boas-vindas do servidor POP
  • linha 2: (D) o cliente fornece o seu identificador POP, c.a.d. O nome de utilizador com o qual acede ao seu correio
  • linha 3: (R) a resposta do servidor
  • linha 4: (D) a palavra-passe do cliente
  • linha 5: (R) a resposta do servidor
  • linha 6: (D) o cliente solicita a lista dos seus e-mails
  • linhas 7-12: (R) a lista de mensagens na caixa de correio do cliente, no formato [N° du message taille en octets du message]
  • linha 13: (D) solicita-se a mensagem n.º 64
  • linhas 14-25: (R) a mensagem n.º 64, com as linhas 15-22 a conterem os cabeçalhos da mensagem e as linhas 23-24 a conterem o corpo da mensagem.
  • linha 26: (D) o cliente indica que terminou
  • linha 27: (R) resposta do servidor, que, em seguida, encerrará a ligação.

11.4.5. O protocolo FTP (Protocolo de Transferência de Ficheiros)

O protocolo FTP é mais complexo do que os apresentados anteriormente. Para descobrir as linhas de texto trocadas entre o cliente e o servidor, pode-se utilizar uma ferramenta como o FileZilla [http://www.filezilla.fr/].

O FileZilla é um cliente FTP que oferece uma interface Windows para a transferência de ficheiros. As ações do utilizador na interface do Windows são traduzidas em comandos FTP, que são registados no [1]. Esta é uma boa forma de descobrir os comandos do protocolo FTP.

11.5. As classes .NET da programação na Internet

11.5.1. Escolher a classe adequada

O framework .NET oferece várias classes para trabalhar com a rede:

  • A classe Socket é a que opera mais próximo da rede. Permite gerir a ligação à rede com grande precisão. O termo socket designa uma tomada elétrica. O termo foi alargado para designar uma tomada de rede virtual. Numa comunicação TCP-IP entre duas máquinas A e B, são dois sockets que comunicam entre si. Uma aplicação pode trabalhar diretamente com os sockets. É o caso da aplicação A acima referida. Um socket pode ser um socket client ou serveur.
  • Se se pretender trabalhar a um nível menos detalhado do que o da classe Socket, poderá utilizar-se as classes
  • TcpClient para criar um cliente TCP
  • TcpListener para criar um servidor TCP

Estas duas classes oferecem à aplicação que as utiliza uma visão mais simples da comunicação de rede, gerindo por ela os detalhes técnicos da gestão dos sockets.

  • .NET oferece classes específicas para determinados protocolos:
  • a classe SmtpClient para gerir o protocolo SMTP de comunicação com um servidor SMTP de envio de e-mails
  • a classe WebClient para gerir os protocolos HTTP ou FTP de comunicação com um servidor web.

É importante referir que a classe Socket é, por si só, suficiente para gerir toda a comunicação TCP/IP, mas procuraremos, acima de tudo, utilizar as classes de nível superior, a fim de facilitar a programação da aplicação TCP/IP.

11.5.2. A classe TcpClient

A classe TcpClient é a classe adequada na maioria dos casos para criar o cliente de um serviço TCP. Entre os seus construtores C, métodos M e propriedades P, possui os seguintes:

TcpClient(string hostname, int port)
C
cria uma ligação TCP com o serviço a funcionar 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
Socket Client
P
o socket utilizado pelo cliente para comunicar com o servidor.
NetworkStream GetStream()
M
obtém um fluxo de leitura e escrita para o servidor. É este fluxo que permite as trocas entre o cliente e o servidor.
void Close()
M
encerra a ligação. O socket e o fluxo NetworkStream são igualmente encerrados
bool Connected()
P
verdadeiro se a ligação tiver sido estabelecida

A classe NetworkStream representa o fluxo de rede entre o cliente e o servidor. É derivada 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. Assim, se uma máquina M1 tiver estabelecido uma ligação com uma máquina M2 utilizando um objeto TcpClient client1 e ambas trocarem linhas de texto, poderá criar os seus fluxos de leitura e escrita da seguinte forma:

StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=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 o 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 a máquina M2.

Para enviar uma linha de texto para a máquina M2, escrever-se-á:

client1.WriteLine("un texte");

Para ler a resposta da máquina M2, escreve-se:

string réponse=client1.ReadLine();

Temos agora os elementos necessários para definir a arquitetura básica de um cliente da Internet com o seguinte protocolo de comunicação básico com o servidor:

  • o cliente envia um pedido contido numa única linha
  • o servidor envia uma resposta contida numa única linha

using System;
using System.IO;
using System.Net.Sockets;

namespace ... {
    class ... {
        static void Main(string[] args) {
            ...
            try {
                // está a ser estabelecida uma ligação ao serviço
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // ciclo de pedido-resposta
                                while (true) {
                                    // a solicitação provém do teclado
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // Concluído?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // envia-se a solicitação ao servidor
                                    writer.WriteLine(demande);
                                    // lê-se a resposta do servidor
                                    réponse = reader.ReadLine();
                                    // processa-se a resposta
                                    ...
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // erro
                ...
            }
        }
    }
}
  • linha 11: criação da ligação do cliente — a cláusula using garante que os recursos associados a esta ligação serão libertados ao sair do using.
  • linha 12: abertura do fluxo de rede numa cláusula using
  • linha 13: criação e execução do fluxo de leitura numa cláusula using
  • linha 14: criação e execução do fluxo de escrita numa cláusula using
  • linha 16: não armazenar em buffer o fluxo de saída
  • linhas 18-31: o ciclo pedido do cliente / resposta do servidor
  • linha 26: o cliente envia o seu pedido ao servidor
  • linha 28: o cliente aguarda a resposta do servidor. Trata-se de uma operação bloqueante, tal como a leitura a partir do teclado. A espera termina com a chegada de uma cadeia de caracteres terminada por «\n» ou com o fim do fluxo. Este último ocorrerá se o servidor encerrar a ligação que estabeleceu com o cliente.

11.5.3. A classe TcpListener

A classe TcpListener é a classe adequada na maioria dos casos para criar um serviço TCP. Entre os seus construtores C, métodos M e propriedades P, possui os seguintes:

TcpListener(int port)
C
cria um serviço TCP que irá aguardar (listen) os pedidos dos clientes numa porta passada como parâmetro (port), denominada porta de escuta. Se o computador estiver ligado a várias redes IP, o serviço escuta em cada uma dessas redes.
TcpListener(IPAddress ip, int port)
C
O mesmo, mas a escuta ocorre apenas no endereço IP especificado.
void Start()
M
inicia a escuta dos pedidos dos clientes
TcpClient AcceptTcpClient()
M
aceita o pedido de um cliente. Em seguida, abre uma nova ligação com este, denominada ligação de serviço. A porta utilizada do lado do servidor é aleatória e escolhida pelo sistema. É designada por porta de serviço. AcceptTcpClient devolve como resultado o objeto TcpClient associado, do lado do servidor, à ligação de serviço.
void Stop()
M
deixa de ouvir os pedidos dos clientes
Socket Server
P
o socket de escuta do servidor

A estrutura básica de um servidor TCP que comunicaria com os seus clientes de acordo com o seguinte protocolo:

  • o cliente envia um pedido contido numa única linha
  • o servidor envia uma resposta contida numa única linha

poderia ser semelhante a isto:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;

namespace ... {
    public class ... {
            ...
            // cria-se o serviço de escuta
            TcpListener ecoute = null;
            try {
                // cria-se o serviço — este ficará à escuta em todas as interfaces de rede da máquina
                ecoute = new TcpListener(IPAddress.Any, port);
                // inicia-se o serviço
                ecoute.Start();
                // ciclo de serviço
                TcpClient tcpClient = null;
                // loop infinito — será interrompido com Ctrl-C
                while (true) {
                    // aguarda um cliente
                    tcpClient = ecoute.AcceptTcpClient();
                    // o serviço é prestado por outra tarefa
                    ThreadPool.QueueUserWorkItem(Service, tcpClient);
                    // próximo cliente
                }
            } catch (Exception ex) {
                // o erro é sinalizado
                ...
            } finally {
                // fim do serviço
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // presta o serviço a um cliente
        public static void Service(Object infos) {
            // recupera-se o cliente a quem se deve prestar o serviço
            Client client = infos as Client;
            // exploração da ligação TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // ciclo de leitura de pedido/gravação de resposta
                                bool fini=false;
                                while (! fini) != null) {
                                    // aguardar pedido do cliente - operação bloqueante
                                    demande=reader.ReadLine();
                                    // preparação da resposta
                                    réponse=...;
                                    // envio da resposta ao cliente
                                    writer.WriteLine(réponse);
                                    // solicitação seguinte
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // erro
                ...
            } finally {
                // fim do cliente
                ...
            }
        }
    }
}
  • linha 14: o serviço de escuta é criado para uma determinada porta e um determinado endereço IP. É importante lembrar aqui que uma máquina tem, pelo menos, dois endereços IP: o endereço «127.0.0.1», que é o seu endereço de loopback, e o endereço «I1.I2.I3.I4», que possui na rede à qual está ligada. Pode ter outros endereços IP se estiver ligada a várias redes IP. IPAddress.Any designa todos os endereços IP de uma máquina.
  • linha 16: o serviço de escuta é iniciado. Anteriormente, já tinha sido criado, mas ainda não estava a escutar. Escutar significa aguardar os pedidos dos clientes.
  • linhas 20-26: o ciclo de espera por pedido do cliente / atendimento ao cliente é repetido para cada novo cliente
  • linha 22: a solicitação de um cliente é aceite. O método AcceptTcpClient devolve uma instância TcpClient, denominada de serviço:
    • o cliente fez o seu pedido com a sua própria instância TcpClient do lado do cliente, à qual chamaremos TcpClientDemande
    • o servidor aceita esta solicitação com AcceptTcpClient. Este método cria uma instância TcpClient do lado do servidor, à qual chamaremos TcpClientService. Temos então uma ligação TCP aberta com as instâncias TcpClientDemande <--> TcpClientService em ambas as extremidades.
    • A comunicação cliente/servidor que se segue ocorre através desta ligação. O serviço de escuta já não intervém.
  • linha 24: para que o servidor possa processar vários clientes ao mesmo tempo, o serviço é assegurado por threads, 1 thread por cliente.
  • linha 32: o serviço de escuta é encerrado
  • linha 38: o método executado pelo thread de serviço para um cliente. Recebe como parâmetro a instância TcpClient já ligada ao cliente que deve ser atendido.
  • linhas 38-71: encontramos aqui um código semelhante ao do cliente TCP básico analisado anteriormente.

11.6. Exemplos de clientes/servidores TCP

11.6.1. Um servidor de eco

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

ServeurEcho porta

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


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;

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

namespace Chap9 {
    public class ServeurEcho {
        public const string syntaxe = "Syntaxe : [serveurEcho] port";

        // programa principal
        public static void Main(string[] args) {

            // existe algum argumento?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }
            // este argumento deve ser um número inteiro >0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port<=0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // cria-se o serviço de escuta
            TcpListener ecoute = null;
            int numClient = 0;    // próximo número de cliente
            try {
                // Cria-se o serviço — este irá escutar em todas as interfaces de rede da máquina
                ecoute = new TcpListener(IPAddress.Any, port);
                // inicia-se o serviço
                ecoute.Start();
                // acompanhamento
                Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
                // threads do serviço
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);
                // loop do serviço
                TcpClient tcpClient = null;
                // loop infinito — será interrompido com Ctrl-C
                while (true) {
                    // à espera de um cliente
                    tcpClient = ecoute.AcceptTcpClient();
                    // o serviço é prestado por outra tarefa
                    ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // próximo cliente
                    numClient++;
                }
            } catch (Exception ex) {
                // o erro é sinalizado
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            } finally {
                // fim do serviço
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // presta o serviço a um cliente do servidor de eco
        public static void Service(Object infos) {
            // recupera-se o cliente a quem se deve prestar serviço
            Client client = infos as Client;
            // presta o serviço ao cliente
            Console.WriteLine("Début de service au client {0}", client.NumClient);
            // exploração da ligação TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // ciclo de leitura de pedido/gravação de resposta
                                string demande = null;
                                while ((demande = reader.ReadLine()) != null) {
                                    // monitorização da consola
                                    Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    // eco da solicitação para o cliente
                                    writer.WriteLine("[{0}]", demande);
                                    // acompanhamento da consola
                                    Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
                                    // o serviço encerra quando o cliente envia «bye»
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // erro
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                // fim do cliente
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }
    }

    // informações do cliente
    internal class Client {
        public TcpClient CanalTcp { get; set; }        // ligação com o cliente
        public int NumClient { get; set; }            // n.º do cliente
    }
}

A estrutura do servidor de eco está em conformidade com a arquitetura básica dos servidores TCP apresentada anteriormente. Iremos comentar apenas a parte relativa ao «serviço ao cliente»:

  • linha 79: a solicitação do cliente é lida
  • linha 83: é devolvida ao cliente entre colchetes
  • linha 79: o serviço é encerrado quando o cliente encerra a ligação

Numa janela do DOS, utilizamos o executável do projeto C#:

...\Chap9\02\bin\Release>dir
 03/05/2008  11:46             7 168 ServeurEcho.exe
...>ServeurEcho 100
Serveur d'écho lancé sur le port 0.0.0.0:100

Em seguida, iniciamos dois clientes putty, que ligamos à porta 100 da máquina localhost:

 

A saída da consola do servidor de eco passa a ser:

1
2
3
Serveur d'écho lancé sur le port 0.0.0.0:100
Début de service au client 0
Début de service au client 1

O cliente 1 e, em seguida, o cliente 0 enviam os seguintes textos:

  • [1]: o cliente n.º 1
  • [2]: o cliente n.º 0
  • [3]: a consola do servidor de eco
  • em [4]: o cliente 1 desliga-se com o comando bye.
  • em [5]: o servidor deteta-o

O servidor pode ser interrompido com Ctrl-C. O cliente n.º 0 deteta então [6].

11.6.2. Um cliente para o servidor de eco

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

ClientEcho nomServeur porta

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


using System;
using System.IO;
using System.Net.Sockets;

namespace Chap9 {
    // liga-se a um servidor de eco
    // qualquer linha digitada no teclado é recebida como eco
    class ClientEcho {
        static void Main(string[] args) {
            // sintaxe
            const string syntaxe = "pg machine port";

            // número de argumentos
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

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

            // a porta deve ser um número inteiro >0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }

            // é possível trabalhar
            string demande = null;        // pedido do cliente
            string réponse = null;        // resposta do servidor
            try {
                // estabelece-se ligação ao serviço
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // ciclo de pedido-resposta
                                while (true) {
                                    // a solicitação provém do teclado
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // Concluído?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // envia-se a solicitação ao servidor
                                    writer.WriteLine(demande);
                                    // lê-se a resposta do servidor
                                    réponse = reader.ReadLine();
                                    // a resposta está a ser processada
                                    Console.WriteLine("Réponse : {0}", réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // erro
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

A estrutura deste cliente está em conformidade com a arquitetura geral básica proposta para os clientes Tcp. Aqui estão 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 A (n.º 0), temos as seguintes exibições:

1
2
3
4
5
6
...\Chap9\03\bin\Release>ClientEcho localhost 100
Demande (bye pour arrêter) : ligne1A
Réponse : [ligne1A]
Demande (bye pour arrêter) : ligne2A
Réponse : [ligne2A]
Demande (bye pour arrêter) :

Na janela do cliente B (n.º 1):

1
2
3
4
5
6
...\Chap9\03\bin\Release>ClientEcho localhost 100
Demande (bye pour arrêter) : ligne1B
Réponse : [ligne1B]
Demande (bye pour arrêter) : ligne2B
Réponse : [ligne2B]
Demande (bye pour arrêter) :

Na janela do servidor:

...\Chap9\02\bin\Release>ServeurEcho 100
Serveur d'écho lancé sur le port 0.0.0.0:100
Début de service au client 0
<--- Client 0 : ligne1A
---> Client 0 : ligne1A
<--- Client 0 : ligne2A
---> Client 0 : ligne2A
Début de service au client 1
<--- Client 1 : ligne1B
---> Client 1 : ligne1B
<--- Client 1 : ligne2B
---> Client 1 : ligne2B

O cliente A n.º 0 desliga-se:

1
2
3
4
Demande (bye pour arrêter) : ligne1A
Réponse : [ligne1A]
...
Demande (bye pour arrêter) : bye

A consola do servidor:

1
2
3
Serveur d'écho lancé sur le port 0.0.0.0:100
...
Fin du service au client 0

11.6.3. Um cliente genérico TCP

Vamos escrever um cliente TCP genérico que será iniciado da seguinte forma: ClientTcpGenerique servidor porta. O seu funcionamento será semelhante ao do cliente PuTTY, mas terá uma interface de consola e não apresentará opções de configuração.

Na aplicação anterior, 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 para o servidor / receção de uma única linha enviada pelo servidor nem sempre é adequado. Para gerir protocolos mais complexos do que o de eco, o cliente TCP genérico terá duas threads:

  • o thread principal irá ler as linhas de texto digitadas no teclado e enviá-las para o servidor.
  • uma thread secundária funcionará em paralelo e dedicar-se-á à leitura das linhas de texto enviadas pelo servidor. Assim que receber uma, exibe-a na consola. A thread só termina quando o servidor encerra a ligação. Funciona, portanto, de forma contínua.

O código é o seguinte:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;

namespace Chap9 {
    // recebe como parâmetro as características de um serviço no formato: servidor porta
    // liga-se ao serviço
    // envia ao servidor cada linha digitada no teclado
    // cria um thread para ler continuamente as linhas de texto enviadas pelo servidor
    class ClientTcpGenerique {
        static void Main(string[] args) {
            // sintaxe
            const string syntaxe = "pg serveur port";

            // número de argumentos
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

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

            // a porta deve ser um número inteiro >0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // estabelece-se a ligação ao serviço
            TcpClient tcpClient = null;
            try {
                tcpClient = new TcpClient(serveur, port);
            } catch (Exception ex) {
                // erro
                Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
                // fim
                return;
            }

            // inicia-se um thread separado para ler as linhas de texto enviadas pelo servidor
            ThreadPool.QueueUserWorkItem(Receive, tcpClient);

            // a leitura dos comandos do teclado é feita no thread principal
            Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
            string demande = null;        // pedido do cliente
            try {
                // utiliza-se a ligação do cliente
                using (tcpClient) {
                    // cria-se um fluxo de escrita para o servidor
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamWriter writer = new StreamWriter(networkStream)) {
                            // fluxo de saída não armazenado em buffer
                            writer.AutoFlush = true;
                            // ciclo de pedido-resposta
                            while (true) {
                                demande = Console.ReadLine();
                                // Concluído?
                                if (demande.Trim().ToLower() == "bye")
                                    break;
                                // envia-se o pedido ao servidor
                                writer.WriteLine(demande);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // erro
                Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
            }
        }

        // thread de leitura cliente <-- servidor
        public static void Receive(object infos) {
            // dados locais
            string réponse = null;    // resposta do servidor
            // criação do fluxo de entrada
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // loop de leitura contínua das linhas de texto do fluxo de entrada
                            while ((réponse = reader.ReadLine()) != null) {
                                // exibição na consola
                                Console.WriteLine("<-- {0}", réponse);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // erro
                Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // é sinalizado o fim do thread de leitura
                Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
            }
        }
    }
}
  • linha 34: o cliente liga-se ao servidor
  • linha 43: é iniciado um thread para a leitura das linhas de texto do servidor. Este deve executar o método Receive da linha 73. Passa-se a este método a instância TcpClient que foi ligada ao servidor.
  • linhas 57-64: o ciclo de introdução de comandos de teclado / envio de comandos para o servidor. A introdução dos comandos de teclado é assegurada pelo thread principal.
  • linhas 75-98: o método Receive executado pelo thread de leitura das linhas de texto. Este método recebe como parâmetro a instância TcpClient que foi ligada ao servidor.
  • linhas 84-87: o ciclo contínuo de leitura das linhas de texto enviadas pelo servidor. Este só termina quando o servidor encerra a ligação aberta com o cliente.

Eis alguns exemplos que retomam os utilizados com o cliente putty no parágrafo 11.4. O cliente é executado numa consola DOS.

Protocolo HTTP

...\Chap9\04\bin\Release>ClientTcpGenerique istia.univ-angers.fr 80
Tapez vos commandes (bye pour arrêter) :
GET /inconnu HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: Close

<-- HTTP/1.1 404 Not Found
<-- Date: Sat, 03 May 2008 12:35:11 GMT
<-- Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29

<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html; charset=iso-8859-1
<--
<-- 11a
<-- <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<-- <HTML><HEAD>
<-- <TITLE>404 Not Found</TITLE>
<-- </HEAD><BODY>
<-- <H1>Not Found</H1>
<-- The requested URL /inconnu was not found on this server.<P>
<-- <HR>
<-- <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
<-- </BODY></HTML>
<--
<-- 0
<--
[Fin du thread de lecture des réponses du serveur]
bye

...\Chap9\04\bin\Release>

Convidamos o leitor a reler as explicações apresentadas no parágrafo 11.4.2. Limitamo-nos a comentar apenas os aspetos específicos da aplicação:

  • linha 28: após o envio da linha 27, o servidor HTTP encerrou a ligação, o que provocou o fim do thread de leitura. O thread principal, que lê os comandos introduzidos pelo teclado, continua ativo. O comando da linha 29, introduzido pelo teclado, encerra-o.

Protocolo SMTP

...\Chap9\04\bin\Release>ClientTcpGenerique smtp.neuf.fr 25
Tapez vos commandes (bye pour arrêter) :
<-- 220 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom Service relais mail ready
HELO istia.univ-angers.fr
<-- 250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
mail from: xx@neuf.fr
<-- 250 2.1.0 <xx@neuf.fr> sender ok
rcpt to: yy@univ-angers.fr
<-- 250 2.1.5 <yy@univ-angers.fr> destinataire ok
data
<-- 354 enter mail, end with "." on a line by itself
ligne1
ligne2
.
<-- 250 2.0.0 M0jL1Z0044AoCxw0200000 message ok
quit
<-- 221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection
[Fin du thread de lecture des réponses du serveur]
bye

...\Chap9\04\bin\Release>

Recomenda-se ao leitor que releia as explicações fornecidas no parágrafo 11.4.3 e que teste os outros exemplos utilizados com o cliente putty.

11.6.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 desempenha a função de servidor.

O programa é iniciado numa janela do DOS através do comando: ServeurTcpGenerique portEcoute, em que portEcoute é a porta à qual os clientes devem ligar-se. O serviço prestado ao cliente será assegurado por duas threads:

  • o thread principal, que:
    • processará os clientes um a um e não em paralelo;
    • que lerá as linhas digitadas pelo utilizador no teclado e as enviará ao cliente. O utilizador indicará, através do comando «bye», que está a encerrar a ligação com o cliente. É porque a consola não pode ser utilizada por dois clientes em simultâneo que o nosso servidor processa apenas um cliente de cada vez.
  • um thread secundário dedicado exclusivamente à leitura das linhas de texto enviadas pelo cliente

O servidor, por sua vez, nunca se encerra, a não ser que o utilizador pressione Ctrl-C no teclado.

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

1
2
3
4
5
6
7
...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
Tapez vos commandes (bye pour arrêter) :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
bye

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:

...\Chap9\05\bin\Release>ServeurTcpGenerique 100
Serveur générique lancé sur le port 0.0.0.0:100
Client 127.0.0.1:4165
Tapez vos commandes (bye pour arrêter) :
<-- commande 1 du client 1
réponse 1 au client 1
<-- commande 2 du client 1
réponse 2 au client 1
[Fin du thread de lecture des demandes du client]
bye

As linhas que começam por <-- são as enviadas do cliente para o servidor; as restantes são as enviadas pelo servidor para o cliente. A linha 9 indica que o thread de leitura dos pedidos do cliente foi interrompido. O thread principal do servidor continua à espera de comandos digitados no teclado para os enviar ao cliente. É, portanto, necessário digitar no teclado o comando bye da linha 10 para passar para o cliente seguinte. O servidor continua ativo, embora o cliente 1 já tenha terminado. Inicia-se um segundo cliente para o mesmo servidor:

1
2
3
4
5
...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
Tapez vos commandes (bye pour arrêter) :
commande 3 du client 2
<-- réponse 3 au client 2
bye

A janela do servidor fica então assim:

1
2
3
4
5
6
Tapez vos commandes (bye pour arrêter) :
Client 127.0.0.1:4166
<-- commande 3 du client 2
réponse 3 au client 2
[Fin du thread de lecture des demandes du client]
bye

Após a linha 6 acima, o servidor ficou à espera de um novo cliente. É possível pará-lo com Ctrl-C.

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

1
2
3
...\Chap9\05\bin\Release>ServeurTcpGenerique 88

Serveur générique lancé sur le port 0.0.0.0:88

Abramos agora um navegador e acedamos à 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:

 

Vejamos agora a janela do nosso servidor:

Serveur générique lancé sur le port 0.0.0.0:88
Client 127.0.0.1:4167
Tapez vos commandes (bye pour arrêter) :
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
ication, application/x-silverlight, */*
<-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
<-- UA-CPU: x86
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Podemos ver os cabeçalhos HTTP enviados pelo navegador. Isto permite-nos descobrir outros cabeçalhos HTTP além dos que já encontrámos. 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 enviada por um servidor Web num exemplo anterior:

HTTP/1.1 200 OK
Date: Sat, 03 May 2008 07:53:47 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=0d2e64b317; path=/
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0

Vamos tentar dar uma resposta semelhante, limitando-nos ao estritamente necessário:

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

<html>
<head><title>Serveur generique</title></head>
<body><h2>Reponse du serveur generique</h2></body>
</html>
bye
Flux de lecture des lignes de texte du client : l'erreur suivante s'est produite : Unable to read data from the transport connection: Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall.
[Fin du thread de lecture des demandes du client]

Limitámo-nos, na nossa resposta, aos cabeçalhos HTTP das linhas 1 a 4. Não indicamos o tamanho do documento que vamos enviar (Content-Length), mas limitamo-nos a indicar que vamos encerrar a ligação (Connection: close) após o envio do mesmo. Isto é suficiente para o navegador. Ao verificar que a ligação foi encerrada, saberá que a resposta do servidor está concluída e exibirá a página HTML que lhe foi enviada. Esta última corresponde às linhas 6 a 9. O utilizador, através do teclado, encerra então a ligação com o cliente digitando o comando bye, linha 10. Com este comando de teclado, o thread principal encerra a ligação com o cliente. Isto provoca a exceção da linha 11. O thread responsável pela leitura das linhas de texto do cliente foi interrompido abruptamente pelo encerramento da ligação com o cliente e lançou uma exceção. Após a linha 12, o servidor fica à espera de um novo cliente.

O navegador do cliente apresenta agora o seguinte:

Se, no exemplo acima, introduzirmos Affichage/Source para ver o que o navegador recebeu, obtemos [2], ou seja, exatamente o que foi enviado a partir do servidor genérico.

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


using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Chap9 {
    public class ServeurTcpGenerique {
        public const string syntaxe = "Syntaxe : ServeurGénérique Port";

        // programa principal
        public static void Main(string[] args) {

            // existe algum argumento?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                Environment.Exit(1);
            }
            // este argumento deve ser um número inteiro >0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port <= 0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                Environment.Exit(2);
            }
            // cria-se o serviço de escuta
            TcpListener ecoute = null;
            try {
                // criamos o serviço
                ecoute = new TcpListener(IPAddress.Any, port);
                // iniciamo-lo
                ecoute.Start();
                // acompanhamento
                Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
                while (true) {
                    // aguarda um cliente
                    Console.WriteLine("Attente du client suivant...");
                    TcpClient tcpClient = ecoute.AcceptTcpClient();
                    Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
                    // inicia-se um thread separado para ler as linhas de texto enviadas pelo cliente
                    ThreadPool.QueueUserWorkItem(Receive, tcpClient);
                    // a leitura dos comandos do teclado é feita no thread principal
                    Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
                    string réponse = null;        // resposta do servidor
                    // a ligação do cliente é processada
                    using (tcpClient) {
                        // cria-se um fluxo de escrita para o cliente
                        using (NetworkStream networkStream = tcpClient.GetStream()) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // ciclo de introdução de respostas pelo teclado
                                while (true) {
                                    réponse = Console.ReadLine();
                                    // Concluído?
                                    if (réponse.Trim().ToLower() == "bye")
                                        break;
                                    // envia-se o pedido ao cliente
                                    writer.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // notifica-se o erro
                Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // fim da escuta
                ecoute.Stop();
            }
        }

        // thread de leitura servidor <-- cliente
        public static void Receive(object infos) {
            // dados locais
            string demande = null;    // pedido do cliente
            string idClient=null;    // identidade do cliente

            // gestão da ligação do cliente
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    // identidade do cliente
                    idClient = tcpClient.Client.RemoteEndPoint.ToString();
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // ciclo de leitura contínua das linhas de texto do fluxo de entrada
                            while ((demande = reader.ReadLine()) != null) {
                                // exibição na consola
                                Console.WriteLine("<-- {0}", demande);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // erro
                Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
            } finally {
                // é sinalizado o fim do thread de leitura
                Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
            }
        }
    }
}
  • linha 29: o serviço de escuta é criado, mas não é iniciado. Este escuta todas as interfaces de rede da máquina.
  • linha 31: o serviço de escuta é iniciado
  • linha 34: loop infinito de espera por clientes. O utilizador irá encerrar o servidor com Ctrl-C.
  • linha 37: espera por um cliente – operação bloqueante. Quando o cliente chega, a instância TcpClient devolvida pelo método AcceptTcpClient representa o lado do servidor de uma ligação aberta com o cliente.
  • linha 40: o fluxo de leitura dos pedidos do cliente é atribuído a um thread separado.
  • linha 45: utilização da ligação ao cliente numa cláusula using para garantir que esta será encerrada independentemente do que acontecer.
  • linha 47: utilização do fluxo de rede numa cláusula using
  • linha 48: criação, numa cláusula using, de um fluxo de escrita no fluxo de rede
  • linha 50: o fluxo de escrita não será armazenado em buffer
  • linhas 52-59: ciclo de introdução, via teclado, dos comandos a enviar ao cliente
  • linha 69: fim do serviço de escuta. Esta instrução nunca será executada aqui, uma vez que o servidor é interrompido com Ctrl-C.
  • linha 78: o método Receive que exibe continuamente na consola as linhas de texto enviadas pelo cliente. Aqui encontramos o que já vimos para o cliente genérico TCP.

11.6.5. Um cliente Web « »

No exemplo anterior, vimos alguns dos cabeçalhos HTTP enviados por um navegador:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
ication, application/x-silverlight, */*
<-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
<-- UA-CPU: x86
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

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

1
2
3
4
<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
<--
  • o primeiro cabeçalho indica o documento pretendido
  • o segundo, o servidor consultado
  • o terceiro indica que pretendemos que o servidor encerre a ligação após nos ter respondido.

Se, na linha 1 acima, substituirmos GET por HEAD, o servidor enviar-nos-á apenas os cabeçalhos HTTP e não o documento especificado na linha 1.

O nosso cliente web será chamado da seguinte forma: ClientWeb URL cmd, em que URL é oURL pretendido 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:

...\Chap9\06\bin\Release>ClientWeb http://istia.univ-angers.fr:80 HEAD
HTTP/1.1 200 OK
Date: Sat, 03 May 2008 14:05:24 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=e668408ac1; path=/
Connection: close
Content-Type: text/html;charset=iso-8859-1

...\Chap9\06\bin\Release>
  • na linha 1, solicitamos apenas os cabeçalhos HTTP (HEAD)
  • linhas 2-9: a resposta do servidor

Se utilizarmos GET em vez de HEAD na chamada ao cliente Web, obtemos o mesmo resultado que com HEAD, além do corpo do documento solicitado.

O código do cliente Web é o seguinte:


using System;
using System.IO;
using System.Net.Sockets;

namespace Chap9 {
    class ClientWeb {
        static void Main(string[] args) {
            // sintaxe
            const string syntaxe = "pg URI GET/HEAD";

            // número de argumentos
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // regista-se o URI solicitado
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verificação da validade do URI
            if(! stringURI.StartsWith("http://")){
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI incorreto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verificação do pedido
            if (commande != "GET" && commande != "HEAD") {
                // pedido incorreto
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // a ligar-se ao serviço
                using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // solicitação do URL - envio dos cabeçalhos HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // leitura da resposta
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != null) {
                                    // a resposta é apresentada na consola
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // exibe-se a exceção
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

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.

  • linhas 26-33: é criado um objeto Uri a partir da cadeia stringURI recebida como parâmetro. Se a cadeia «URI» recebida como parâmetro não for um «URI» válido (ausência do protocolo, do servidor, etc.), é lançada uma exceção. Isto permite-nos verificar a validade do parâmetro recebido. Uma vez construído o objeto Uri, temos acesso aos diferentes elementos desta URI. Assim, se o objeto uri do código anterior tiver sido construído a partir da cadeia http://serveur:port/document?param1=val1&param2=val2;..., teremos:
    • uri.Host=serveur,
    • uri.Port=port,
    • uri.Path = document,
    • uri.Query=param1=val1&param2=val2;...,
    • uri.pathAndQuery= cheminPageHTML?param1=val1&param2=val2;...,
    • uri.Scheme=http.

11.6.6. Um cliente Web que gere redirecionamentos

O cliente Web anterior não gere uma eventual redireção do URL que solicitou. Eis um exemplo:

...\Chap9\06\bin\Release>ClientWeb http://www.ibm.com GET
HTTP/1.1 302 Found
Date: Sat, 03 May 2008 14:50:52 GMT
Server: IBM_HTTP_Server
Location: http://www.ibm.com/us/
Content-Length: 206
Kp-eeAlive: timeout=10, max=73
Connection: Keep-Alive
Content-Type: text/html

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ibm.com/us/">here</a>.</p>
</body></html>
  • linha 2: o código 302 Found indica um redirecionamento. O endereço para o qual o navegador deve ser redirecionado encontra-se no corpo do documento, na linha 16.

Um segundo exemplo:

...\Chap9\06\bin\Release>ClientWeb http://www.bull.com GET
HTTP/1.1 301 Moved Permanently
Date: Sat, 03 May 2008 14:52:31 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Location: http://www.bull.com/index.php
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html

0
  • linha 2: o código 301 Moved Permanently indica um redirecionamento. O endereço para o qual o navegador deve ser redirecionado está indicado na linha 6, no cabeçalho HTTP Location.

Um terceiro exemplo:

1
2
3
4
5
6
7
...\Chap9\06\bin\Release>ClientWeb http://www.gouv.fr GET
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/fr/
Date: Sat, 03 May 2008 14:56:53 GMT
Connection: close
  • linha 2: o código 302 «Moved Temporarily» indica um redirecionamento. O endereço para o qual o navegador deve ser redirecionado é indicado na linha 5, no cabeçalho HTTP «Location».

Um quarto exemplo com um servidor IIS local na máquina:

...\istia\Chap9\06\bin\Release>ClientWeb.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:16:56 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=FDJLADLCOLDHGKGNIPMLHIIA; path=/
Cache-control: private
  • linha 2: o código 302 «Object moved» indica um redirecionamento. O endereço para o qual o navegador deve ser redirecionado é indicado na linha 5, no cabeçalho «HTTP Location». Note-se que, ao contrário dos exemplos anteriores, o endereço de redirecionamento é relativo. O endereço completo é, na verdade, http://localhost/localstart.asp.

Propomos gerir as redireções quando a primeira linha dos cabeçalhos HTTP contiver a palavra-chave moved (sem distinção entre maiúsculas e minúsculas) e a morada de redireção estiver no cabeçalho HTTP Location.

Se retomarmos os três últimos exemplos, obtemos os seguintes resultados:

URL: http://www.bull.com

...\Chap9\06B\bin\Release>ClientWebAvecRedirection http://www.bull.com HEAD
HTTP/1.1 301 Moved Permanently
Date: Sun, 04 May 2008 10:22:48 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Location: http://www.bull.com/index.php
Connection: close
Content-Type: text/html


<--Redirection vers l'URL http://www.bull.com/index.php-->

HTTP/1.1 200 OK
Date: Sun, 04 May 2008 10:22:49 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Connection: close
Content-Type: text/html
  • linha 11: a redireção ocorre para o endereço da linha 6

URL: http://www.gouv.fr

...\Chap9\06B\bin\Release>ClientWebAvecRedirect
ion http://www.gouv.fr HEAD
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/fr/
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close


<--Redirection vers l'URL http://www.premier-ministre.gouv.fr/fr/-->

HTTP/1.1 200 OK
Server: Apache
X-Powered-By: PHP/4.4.1
Last-Modified: Sun, 04 May 2008 10:29:48 GMT
Content-Type: text/html
Expires: Sun, 04 May 2008 10:40:38 GMT
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close
  • linha 11: o redirecionamento ocorre para o endereço da linha 6

URL: http://localhost

...\Chap9\06B\bin\Release>ClientWebAvecRedirection.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=GDJLADLCJCMPCHFFEJEFPKMK; path=/
Cache-control: private


<--Redirection vers l'URL http://localhost/localstart.asp-->

HTTP/1.1 401 Access Denied
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="localhost"
Connection: close
Content-Length: 4766
Content-Type: text/html
  • linha 13: o redirecionamento ocorre para o endereço da linha 6
  • linha 15: o acesso à página http://localhost/localstart.asp foi-nos recusado.

O programa que gere o redirecionamento é o seguinte:


using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;

namespace Chap9 {
    class ClientWebAvecRedirection {
        static void Main(string[] args) {
            // sintaxe
            const string syntaxe = "pg URI GET/HEAD";

            // número de argumentos
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // observa-se que é solicitado o URI
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verificação da validade do URI
            if (!stringURI.StartsWith("http://")) {
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI incorreto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verificação do pedido
            if (commande != "GET" && commande != "HEAD") {
                // pedido incorreto
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            const int nbRedirsMax = 1;        // não é permitido mais do que um redirecionamento
            int nbRedirs = 0;                            // número de redirecionamentos em curso

            // expressão regular para encontrar um URL de redirecionamento
            Regex location = new Regex(@"^Location: (.+?)$");
            try {
                // podem existir vários URL a solicitar, caso existam redirecionamentos
                while (nbRedirs <= nbRedirsMax) {
                    // gestão de redirecionamentos
                    bool redir = false;
                    bool locationFound = false;
                    string locationString = null;
                    // liga-se ao serviço
                    using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                        using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
                            using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // solicita-se o URL - envio dos cabeçalhos HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // leitura da primeira linha da resposta
                                string premièreLigne = reader.ReadLine();
                                // eco no ecrã
                                Console.WriteLine(premièreLigne);

                                // redirecionamento?
                                if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
                                    // existe um redirecionamento
                                    redir = true;
                                    nbRedirs++;
                                }

                                // seguem-se os cabeçalhos HTTP até encontrar a linha vazia que indica o fim dos cabeçalhos
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != "") {
                                    // exibe-se a resposta
                                    Console.WriteLine(réponse);
                                    // se houver redirecionamento, procura-se o cabeçalho Location
                                    if (redir && !locationFound) {
                                        // compara-se a linha atual com a expressão relacional «location»
                                        Match résultat = location.Match(réponse);
                                        if (résultat.Success) {
                                            // se tiver sido encontrada, regista-se o URL de redirecionamento
                                            locationString = résultat.Groups[1].Value;
                                            // regista-se que foi encontrado
                                            locationFound = true;
                                        }
                                    }
                                }

                                // os cabeçalhos HTTP foram esgotados — escreve-se a linha vazia
                                Console.WriteLine(réponse);
                                // depois passa-se para o corpo do documento
                                while ((réponse = reader.ReadLine()) != null) {
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                    // já terminámos?
                    if (!locationFound || nbRedirs > nbRedirsMax)
                        break;
                    // há um redirecionamento a efetuar — constrói-se a nova URI
                    try {
                        if (locationString.StartsWith("http")) {
                            // endereço http completo
                            uri = new Uri(locationString);
                        } else {
                            // endereço http relativo à URI atual
                            uri = new Uri(uri, locationString);
                        }
                        // registo da consola
                        Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
                    } catch (Exception ex) {
                        // problema com a URI
                        Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
                    }
                }
            } catch (Exception e) {
                // exibe-se a exceção
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

Em relação à versão anterior, as alterações são as seguintes:

  • linha 46: a expressão regular para recuperar o endereço de redirecionamento no cabeçalho HTTP Location: endereço.
  • linha 49: o código que anteriormente era executado para um único URI pode agora ser executado sucessivamente para vários URIs.
  • linha 66: lê-se a primeira linha dos cabeçalhos HTTP enviados pelo servidor. É esta linha que contém a palavra-chave moved, caso o documento solicitado tenha sido movido.
  • linhas 71-75: verifica-se se a primeira linha contém a palavra-chave moved. Se sim, regista-se essa informação.
  • linhas 79-93: leitura dos restantes cabeçalhos HTTP até encontrar a linha vazia que assinala o seu fim. Se a primeira linha anunciasse um redirecionamento, passa-se então para o cabeçalho HTTP Location: endereço para memorizar o endereço de redirecionamento em locationString.
  • linhas 98-100: o restante da resposta do servidor HTTP é apresentado na consola.
  • linhas 105-106: a URI solicitada foi totalmente analisada e apresentada. Se não houver redirecionamento a efetuar ou se o número de redirecionamentos permitidos for excedido, o programa é encerrado.
  • linhas 108-122: se houver redirecionamento, calcula-se a nova URI a solicitar. É necessário um pequeno ajuste, dependendo se a morada de redirecionamento encontrada era absoluta (linha 111) ou relativa (linha 114).

11.7. As classes .NET especializadas num protocolo específico da Internet

Nos exemplos anteriores do cliente web, o protocolo HTTP era gerido com um cliente TCP. Por isso, tivemos de gerir nós próprios o protocolo de comunicação específico utilizado. Poderíamos ter criado, de forma análoga, um cliente SMTP ou POP. O framework .NET oferece classes especializadas para os protocolos HTTP e SMTP. Estas classes conhecem o protocolo de comunicação entre o cliente e o servidor e evitam que o programador tenha de as gerir. Apresentamo-las agora.

11.7.1. A classeWebClient

Existe uma classe WebClient capaz de comunicar com um servidor web. Consideremos o exemplo do cliente web do parágrafo 11.6.5, aqui tratado com a classe WebClient.


using System;
using System.IO;
using System.Net;
namespace Chap9 {
    public class Program {
        public static void Main(string[] args) {
            // sintaxe: [prog] URI
            const string syntaxe = "pg URI";

            // número de argumentos
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }

            // regista-se o URI solicitado
            string stringURI = args[0];

            // verificação da validade do URI
            if (!stringURI.StartsWith("http://")) {
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI incorreto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }

            try {
                // criação de cliente web
                using (WebClient client = new WebClient()) {
                    // adição de um cabeçalho HTTP 
                    client.Headers.Add("user-agent", "st");
                    using (Stream stream = client.OpenRead(uri)) {
                        using (StreamReader reader = new StreamReader(stream)) {
                            // exibição da resposta do servidor web
                            Console.WriteLine(reader.ReadToEnd());
                            // exibição dos cabeçalhos da resposta do servidor
                            Console.WriteLine("---------------------");
                            foreach (string clé in client.ResponseHeaders.Keys) {
                                Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
                            }
                            Console.WriteLine("---------------------");
                        }
                    }
                }
            } catch (WebException e1) {
                Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
            } catch (Exception e2) {
                Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
            }
        }
    }
}
  • linha 35: o cliente web é criado, mas ainda não está configurado
  • linha 37: adiciona-se um cabeçalho HTTP à solicitação HTTP que vai ser efetuada. Veremos que outros cabeçalhos serão enviados por predefinição.
  • linha 38: o cliente web solicita a URI indicada pelo utilizador e lê o documento enviado. [WebClient].OpenRead(Uri) estabelece a ligação com Uri e lê a resposta. É aí que reside o interesse desta classe. Esta classe encarrega-se do diálogo com o servidor web. O resultado do método OpenRead é do tipo Stream e representa o documento solicitado. Os cabeçalhos HTTP enviados pelo servidor e que precedem o documento na resposta não fazem parte do mesmo.
  • linha 39: utiliza-se um StreamReader e, na linha 41, o seu método ReadToEnd para ler a resposta na íntegra.
  • linhas 44-46: exibem-se os cabeçalhos HTTP da resposta do servidor. [WebClient].ResponseHeaders representa uma coleção com valores cujas chaves são os nomes dos cabeçalhos HTTP e os valores, as cadeias de caracteres associadas a esses cabeçalhos.
  • linha 51: as exceções que são levantadas durante uma troca cliente/servidor são do tipo WebException.

Vejamos alguns exemplos.

Iniciamos o servidor genérico TCP criado no parágrafo 6.4.6:

...\Chap9\05\bin\Release>ServeurTcpGenerique.exe 88
Serveur générique lancé sur le port 0.0.0.0:88

Iniciamos o cliente web anterior da seguinte forma:

...\Chap9\09\bin\Release>09 http://localhost:88

A URI solicitada é a do servidor genérico. Este apresenta então os cabeçalhos HTTP que lhe foram enviados pelo cliente web:

1
2
3
4
5
6
7
Client 127.0.0.1:1415
Tapez vos commandes (bye pour arrêter) :
<-- GET / HTTP/1.1
<-- User-Agent: st
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Assim, verifica-se que:

  • que o cliente web envia 3 cabeçalhos HTTP por predefinição (linhas 3, 5, 6)
  • linha 4: o cabeçalho que nós próprios gerámos (linha 37 do código)
  • que o cliente web utiliza, por predefinição, o método GET (linha 3). Existem outros métodos, entre os quais o POST e o HEAD.

Agora, vamos solicitar um recurso inexistente:

1
2
3
4
5
...\Chap9\09\bin\Release>09 http://istia.univ-angers.fr/desconhecido
L'exception suivante s'est produite : System.Net.WebException: The remote server returned an error: (404) Not Found.
   at System.Net.WebClient.OpenRead(Uri address)
   at System.Net.WebClient.OpenRead(String address)
   at Chap9.WebClient1.Main(String[] args) in C:\data\2007-2008\c# 2008\poly\istia\Chap9\09\Program.cs:linha 16
  • linha 2: ocorreu uma exceção do tipo WebException porque o servidor respondeu com o código 404 Not Found para indicar que o recurso solicitado não existia.

Por fim, terminemos solicitando um recurso existente:

...\istia\Chap9\09\bin\Release>09 http://istia.univ-angers.fr >istia.univ-angers.txt

O ficheiro istia.univ-angers.txt gerado pelo comando é o seguinte:

<!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
...
</html>
---------------------
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1
Date: Sun, 04 May 2008 14:30:53 GMT
Set-Cookie: fe_typo_user=22eaaf283a; path=/
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
---------------------
  • linha 1: o documento HTML solicitado.
  • linhas 3-10: os cabeçalhos da resposta HTTP numa ordem que não é necessariamente a mesma em que foram enviados.

A classe WebClient dispõe de métodos que permitem receber um documento (métodos DownLoad) ou enviá-lo (métodos UpLoad):

DownLoadData
para descarregar um recurso como uma matriz de bytes (por exemplo, uma imagem)
DownLoadFile
para descarregar um recurso e guardá-lo num ficheiro local
DownLoadString
para descarregar um recurso e recuperá-lo como uma cadeia de caracteres (por exemplo, um ficheiro HTML)
OpenWrite
o equivalente a OpenRead, mas para enviar dados para o servidor
UpLoadData
o equivalente a DownLoadData, mas para o servidor
UpLoadFile
o equivalente a DownLoadFile, mas para o servidor
UpLoadString
o equivalente a DownLoadString, mas para o servidor
UpLoadValues
para enviar ao servidor os dados de um comando POST e recuperar os resultados na forma de uma tabela de bytes. O comando POST solicita um documento, ao mesmo tempo que transmite ao servidor as informações necessárias para determinar o documento efetivo a enviar. Estas informações são enviadas como documento para o servidor, daí o nome UpLoad do método. São enviadas a seguir à linha vazia dos cabeçalhos HTTP, na forma param1=valeur1&param2=valeur2&...:
POST /document HTTP/1.1
...
[ligne vide]
param1=valeur1&param2=valeur2&...
O mesmo documento poderia ser solicitado através do método GET:
GET /document?param1=valeur1&param2=valeur2&...
...
[ligne vide]
A diferença entre os dois métodos é que o navegador que exibe o URI solicitado apresentará «/document» no caso do «POST» e «/document?param1=valeur1&param2=valeur2&...» no caso do «GET».

11.7.2. As classes WebRequest / WebResponse

Por vezes, a classe WebClient não é suficientemente flexível para fazer o que se pretende. Retomemos o exemplo do cliente web com redirecionamento analisado no parágrafo 11.6.6. Temos de emitir o cabeçalho HTTP:

HEAD /document HTTP/1.1

Vimos que os cabeçalhos HTTP emitidos por predefinição pelo cliente web eram os seguintes:

1
2
3
<-- GET / HTTP/1.1
<-- Host: machine:port
<-- Connection: Keep-Alive

Vimos também que era possível adicionar cabeçalhos HTTP aos anteriores com a coleção [WebClient].Headers. Apenas a linha 1 não é um cabeçalho pertencente à coleção Headers, uma vez que não tem o formato chave:valor. Não descobri como alterar o GET para HEAD na linha 1 a partir da classe WebClient (talvez não tenha procurado bem?). Quando a classe WebClient atingir os seus limites, é possível passar para as classes WebRequest / WebResponse:

  • WebRequest: representa a totalidade do pedido do cliente Web.
  • WebResponse: representa a totalidade da resposta do servidor Web

Já referimos que a classe WebClient gere os esquemas http:, https:, ftp: e file:. As solicitações e respostas destes diferentes protocolos não têm a mesma forma. Por isso, é necessário manipular o tipo exato destes elementos, em vez dos seus tipos genéricos WebRequest e WebResponse. Assim, utilizaremos as classes:

  • HttpWebRequest, HttpWebResponse para um cliente HTTP
  • FtpWebRequest, FtpWebResponse para um cliente FTP

Vamos agora abordar, com as classes HttpWebRequest e HttpWebresponse, o exemplo do cliente web com redirecionamento analisado no parágrafo 11.6.6. O código é o seguinte:


using System;
using System.IO;
using System.Net.Sockets;
using System.Net;

namespace Chap9 {
    class WebRequestResponse {
        static void Main(string[] args) {
            // sintaxe
            const string syntaxe = "pg URI GET/HEAD";

            // número de argumentos
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // regista-se o URI solicitado
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verificação da validade do URI
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI incorreto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verificação do pedido
            if (commande != "GET" && commande != "HEAD") {
                // pedido incorreto
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // a solicitação está a ser configurada
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = commande;
                httpWebRequest.Proxy = null;
                // a executar
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                // resultado
                Console.WriteLine("---------------------");
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
                // cabeçalhos HTTP
                Console.WriteLine("---------------------");
                foreach (string clé in httpWebResponse.Headers.Keys) {
                    Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
                }
                Console.WriteLine("---------------------");
                // documento
                using (Stream stream = httpWebResponse.GetResponseStream()) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        // exibe-se a resposta na consola
                        Console.WriteLine(reader.ReadToEnd());
                    }
                }
            } catch (WebException e1) {
                // recupera-se a resposta
                HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
            } catch (Exception e2) {
                // exibe-se a exceção
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
            }
        }
    }
}
  • linha 40: é criado um objeto do tipo WebRequest com o método estático WebRequest.Create(Uri uri), em que uri é o URI do documento a descarregar. Como sabemos que o protocolo da URI é HTTP, o tipo do resultado é alterado para HttpWebRequest, de modo a ter acesso aos elementos específicos do protocolo HTTP.
  • linha 41: definimos o método GET / POST / HEAD da primeira linha dos cabeçalhos HTTP. Neste caso, será GET ou HEAD.
  • linha 42: numa rede privada empresarial, é frequente que os computadores da empresa estejam isolados da Internet por razões de segurança. Para tal, a rede privada utiliza endereços de Internet que os routers da Internet não encaminham. A rede privada está ligada à Internet através de máquinas específicas chamadas proxy, que estão ligadas tanto à rede privada da empresa como à Internet. Este é um exemplo de máquinas com vários endereços: IP. Um equipamento da rede privada não pode estabelecer por si próprio uma ligação com um servidor da Internet, por exemplo, um servidor web. Tem de solicitar a um equipamento proxy que o faça por si. Um equipamento proxy pode alojar servidores proxy para diferentes protocolos. Utiliza-se o termo «proxy HTTP» para designar o serviço responsável por efetuar as solicitações HTTP em nome dos computadores da rede privada. Se existir um servidor proxy HTTP, este deve ser indicado no campo [WebRequest].proxy. Por exemplo, escrever-se-á:
[WebRequest].proxy=new WebProxy("pproxy.istia.uang:3128");

se o proxy HTTP estiver a funcionar na porta 3128 da máquina pproxy.istia.uang. Deve-se colocar «null» no campo [WebRequest].proxy se a máquina tiver acesso direto à Internet e não precisar de passar por um proxy.

  • linha 44: o método GetResponse() solicita o documento identificado pelo seu URI e devolve um objeto WebRequestResponse, que aqui é transformado num objeto HttpWebResponse. Este objeto representa a resposta do servidor ao pedido do documento.
  • linha 47:
    • [HttpWebResponse].ResponseUri: é a URI do servidor que enviou o documento. Em caso de redirecionamento, esta pode ser diferente da URI do servidor consultado inicialmente. Note-se que o código não gere o redirecionamento. Este é gerido automaticamente pelo método GetResponse. Mais uma vez, esta é a vantagem das classes de alto nível em relação às classes básicas do protocolo TCP.
    • [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription representam a primeira linha da resposta, por exemplo: HTTP/1.1 200 OK. StatusCode é 200 e StatusDescription é OK.
  • linha 50: [HttpWebResponse].Headers é a coleção de cabeçalhos HTTP da resposta.
  • linha 55: [HttpWebResponse].GetResponseStream: é o fluxo que permite obter o documento contido na resposta.
  • linha 61: pode ocorrer uma exceção do tipo WebException
  • linha 63: [WebException].Response é a resposta que provocou o lançamento da exceção.

Eis um exemplo de execução:

...\Chap9\09B\bin\Release>09B http://www.gouv.fr HEAD
---------------------
Le serveur http://www.premier-ministre.gouv.fr/fr/ respondeu: 200 OK
---------------------
Connection: keep-alive
Content-Type: text/html; charset=iso-8859-1
Date: Mon, 05 May 2008 13:02:29 GMT
Expires: Mon, 05 May 2008 13:07:20 GMT
Last-Modified: Mon, 05 May 2008 12:56:59 GMT
Server: Apache
X-Powered-By: PHP/4.4.1
---------------------
  • linhas 1 e 3: o servidor que respondeu não é o mesmo que foi consultado. Houve, portanto, um redirecionamento.
  • linhas 5-11: os cabeçalhos HTTP enviados pelo servidor

11.7.3. Aplicação: um cliente proxy de um servidor web de tradução

Vamos agora mostrar como as classes anteriores nos permitem explorar os recursos da Web.

11.7.3.1. L'application

Existem na Web sites de tradução. O que será utilizado aqui é o site http://trans.voila.fr/traduction_voila.php:

O texto a traduzir é inserido em [1], o sentido da tradução é selecionado em [2]. A tradução é solicitada por [3] e obtida em [4].

Vamos escrever uma aplicação Windows cliente da aplicação acima referida. Esta não fará nada mais do que a aplicação do site [trans.voila.fr]. A sua interface será a seguinte:

11.7.3.2. A arquitetura da aplicação

A aplicação terá a seguinte arquitetura de duas camadas:

11.7.3.3. O projeto do Visual Studio

O projeto do Visual Studio será o seguinte:

  • Em [1], a solução é composta por dois projetos,
  • [2]: um para a camada [dao] e as entidades por ela utilizadas,
  • [3]: o outro para a interface do Windows

11.7.3.4. O projeto [dao]

O projeto [dao] é composto pelos seguintes elementos:

  • IServiceTraduction.cs: a interface apresentada à camada [ui]
  • ServiceTraduction: a implementação desta interface
  • WebTraductionsException: uma exceção específica da aplicação

A interface IServiceTraduction é a seguinte:


using System.Collections.Generic;

namespace dao {
    public interface IServiceTraduction {
        // línguas utilizadas
        IDictionary<string, string> LanguesTraduites { get; }
        // tradução
        string Traduire(string texte, string deQuoiVersQuoi);
    }
}
  • linha 6: a propriedade LanguesTraduites define o dicionário de idiomas aceites pelo servidor de tradução. Este dicionário contém entradas do tipo ["fe","Français-Anglais"], em que o valor indica um sentido de tradução — neste caso, do francês para o inglês — e a chave «fe» é um código utilizado pelo servidor de tradução trans.voila.fr.
  • linha 8: o método Traduire é o método de tradução:
    • texte é o texto a traduzir
    • deQuoiVersQuoi é uma das chaves do dicionário das línguas traduzidas
    • o método devolve a tradução do texto

ServiceTraduction é uma classe de implementação da interface IServiceTraduction. Apresentamos-a em pormenor na secção seguinte.

WebTraductionsException é a seguinte classe de exceção:


using System;

namespace entites {
    public class WebTraductionsException : Exception {

        // código de erro
        public int Code { get; set; }

        // fabricantes
        public WebTraductionsException() {
        }
        public WebTraductionsException(string message)
            : base(message) {
        }
        public WebTraductionsException(string message, Exception e)
            : base(message, e) {
        }
    }
}
  • linha 7: um código de erro

11.7.3.5. O cliente web [ServiceTraduction]

Voltemos à arquitetura da nossa aplicação:

A classe [ServiceTraduction] que temos de escrever é um cliente do serviço web de tradução [trans.voila.fr]. Para a escrever, precisamos de compreender

  • o que o servidor de tradução espera do seu cliente
  • o que este devolve ao seu cliente

Vejamos, através de um exemplo, o diálogo cliente/servidor que ocorre numa tradução. Retomemos o exemplo apresentado na introdução da aplicação:

O texto a traduzir é inserido em [1], o sentido de tradução é selecionado em [2]. A tradução é solicitada por [3] e obtida em [4].

Para obter a tradução [4], o navegador enviou a seguinte solicitação GET (exibida no seu campo de endereço):

http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=o+cão+está+doente

É bastante simples de compreender:

  • http://trans.voila.fr/traduction_voila.php é o URL do serviço de tradução
  • isText=1 parece indicar que se trata de texto
  • translationDirection indica o sentido da tradução, neste caso Français-Anglais
  • stext é o texto a traduzir numa forma denominada «URL codificada». Com efeito, certos caracteres não podem aparecer numa URL. É o caso, por exemplo, do espaço, que aqui foi codificado por um +. O framework .Net disponibiliza o método estático System.Web.HttpUtility.UrlEncode para realizar este trabalho de codificação.

Conclui-se, portanto, que, para consultar o servidor de tradução, a nossa classe [ServiceTraduction] poderá utilizar a cadeia

"http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"

, em que os marcadores {0} e {1} serão substituídos, respetivamente, pelo sentido da tradução e pelo texto a traduzir.

Como se sabem os sentidos de tradução aceites pelo servidor? Na captura de ecrã acima, as línguas traduzidas encontram-se na lista suspensa. Se, no navegador, se visualizar (Exibir / Código-fonte) o código HTML da página, encontra-se o seguinte para a lista suspensa:

<select name="translationDirection" class="champs">
    <option selected value='fe'>Fran&ccedil;ais vers Anglais
    <option  value='ef'>Anglais vers Fran&ccedil;ais
    <option  value='fg'>Fran&ccedil;ais vers Allemand
    <option  value='gf'>Allemand vers Fran&ccedil;ais
    <option  value='fs'>Fran&ccedil;ais vers Espagnol
    <option  value='sf'>Espagnol vers Fran&ccedil;ais
    <option  value='fr'>Fran&ccedil;ais vers Russe
    <option  value='rf'>Russe vers Fran&ccedil;ais
    <option  value='es'>Anglais vers Espagnol
    <option  value='se'>Espagnol vers Anglais
    <option  value='eg'>Anglais vers Allemand
    <option  value='ge'>Allemand vers Anglais
    <option  value='ep'>Anglais vers Portugais
    <option  value='pe'>Portugais vers Anglais
    <option  value='ie'>Italien vers Anglais
    <option  value='gs'>Allemand vers Espagnol
    <option  value='sg'>Espagnol vers Allemand
</select>

Não se trata de um código HTML muito correto, na medida em que cada tag <option> deveria, normalmente, ser fechada por uma tag </option>. Dito isto, os atributos «value» fornecem-nos a lista dos códigos de tradução que devem ser enviados ao servidor. No dicionário LanguesTraduites da interface IServiceTraduction, as chaves serão os atributos «value» acima referidos e os valores, os textos apresentados pela lista suspensa.

Agora, vamos ver (Exibir / Código-fonte) onde se encontra na página HTML a tradução devolvida pelo servidor de tradução:

...                                                                
<strong>Texte traduit : </strong><div class="txtTrad">this dog is sick</div> 
...

A tradução encontra-se bem no meio da página HTML devolvida. Como é que a podemos localizar? Podemos utilizar uma expressão regular com a sequência <div class="txtTrad">...</div>, uma vez que a baliza <div class="txtTrad"> só está presente neste local da página HTML. A expressão regular em C# que permite recuperar o texto traduzido é a seguinte:

@"<div class=""txtTrad"">(.*?)</div>"

Temos agora os elementos necessários para escrever a classe de implementação ServiceTraduction da interface IServiceTraduction:


using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;

namespace dao {
    public class ServiceTraduction : IServiceTraduction {
        // propriedades de configuração automática do serviço
        public IDictionary<string, string> LanguesTraduites { get; set; }
        public string UrlServeurTraduction { get; set; }
        public string ProxyHttp { get; set; }
        public String RegexTraduction { get; set; }

        // tradução
        public string Traduire(string texte, string deQuoiVersQuoi) {
            // a tradução solicitada é possível?
            if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
                throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
            }
            // texto a traduzir
            string texteATraduire = HttpUtility.UrlEncode(texte);
            // URI a solicitar
            string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
            // expressão regular para encontrar a tradução na resposta
            Regex patternTraduction = new Regex(RegexTraduction);
            // exceção
            WebTraductionsException exception = null;
            // tradução
            string traduction = null;
            try {
                // configura-se a consulta
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = "GET";
                httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
                // executa-se a consulta
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                // documento
                using (Stream stream = httpWebResponse.GetResponseStream()) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        bool traductionTrouvée = false;
                        string ligne = null;
                        while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
                            // procura tradução na linha atual
                            MatchCollection résultats = patternTraduction.Matches(ligne);
                            // tradução encontrada?
                            if (résultats.Count != 0) {
                                traduction = résultats[0].Groups[1].Value.Trim();
                                traductionTrouvée = true;
                            }
                        }
                        // tradução encontrada?
                        if (!traductionTrouvée) {
                            exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
                        }
                    }
                }
            } catch (Exception e) {
                exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
            }
            // exceção?
            if (exception != null) {
                throw exception;
            } else {
                return traduction;
            }
        }
    }
}
  • linha 12: a propriedade LanguesTraduites da interface IServiceTraduction — inicializada externamente
  • linha 13: a propriedade UrlServeurTraduction é o URL a solicitar ao servidor de tradução: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, em que o marcador {0} deve ser substituído pelo sentido da tradução e o marcador {1} pelo texto a traduzir - inicializada externamente
  • linha 14: a propriedade ProxyHttp é o eventual proxy HTTP a utilizar, por exemplo: pproxy.istia.uang:3128 — inicializada externamente
  • linha 15: a propriedade RegexTraduction é a expressão regular que permite recuperar a tradução no fluxo HTML devolvido pelo servidor de tradução, por exemplo @"<div class=""txtTrad"">(.*?)</div>" - inicializada externamente
  • estas quatro propriedades serão, na nossa aplicação, inicializadas pelo Spring.
  • linhas 20-22: verifica-se se o sentido de tradução solicitado existe efetivamente no dicionário de línguas traduzidas. Se não for o caso, é lançada uma exceção.
  • linha 24: o texto a traduzir é codificado para poder fazer parte de uma URL
  • linha 26: a URI do serviço de tradução é construída. Se a propriedade UrlServeurTraduction for a cadeia http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, o marcador {0} é substituído pelo sentido da tradução e o marcador {1} pelo texto a traduzir.
  • linha 28: é construído o padrão de pesquisa da tradução na resposta HTML do servidor de tradução.
  • linhas 33, 60: a operação de consulta ao servidor de tradução ocorre num bloco try/catch
  • linha 35: o objeto HttpWebRequest, que será utilizado para consultar o servidor de tradução, é criado com a URI do documento solicitado.
  • linha 36: o método de consulta é GET. Esta instrução poderia ser omitida, uma vez que GET é provavelmente o método predefinido do objeto HttpWebRequest.
  • linha 37: define-se a propriedade Proxy do objeto HttpWebRequest.
  • linha 39: é enviada a solicitação ao servidor de tradução e é recuperada a sua resposta, que é do tipo HttpWebResponse.
  • linhas 41-42: utiliza-se um StreamReader para ler cada linha da resposta HTML do servidor.
  • linhas 45-53: em cada linha da resposta, procura-se a tradução. Quando esta é encontrada, interrompe-se a leitura da resposta HTML e fecham-se todos os fluxos que foram abertos.
  • linhas 55-57: se não tiver sido encontrada nenhuma tradução na resposta HTML, prepara-se uma exceção do tipo WebTraductionsException para indicar isso.
  • linhas 60-62: se tiver ocorrido uma exceção durante a comunicação cliente/servidor, esta é encapsulada numa exceção do tipo WebTraductionsException para indicar o problema.
  • linhas 64-68: se tiver sido registada uma exceção, esta é lançada; caso contrário, a tradução encontrada é devolvida.

O nosso exemplo pressupõe que o proxy HTTP não requer autenticação. Se não fosse esse o caso, escreveríamos algo como:


httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");

Utilizámos aqui WebRequest / WebResponse em vez de WebClient porque não precisamos de explorar a totalidade da resposta HTML do servidor de tradução. Assim que a tradução é encontrada nessa resposta, já não precisamos do resto das linhas da resposta. A classe WebClient não permite fazer isso.

Eis um programa de teste da classe ServiceTraduction:


using System;
using System.Collections.Generic;
using dao;
using entites;

namespace ui {
    class Program {
        static void Main(string[] args) {
            try {
                // criação do serviço de tradução
                ServiceTraduction serviceTraduction = new ServiceTraduction();
                // expressão regular para encontrar a tradução
                serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
                // URL do servidor de tradução
                serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
                // dicionário das línguas traduzidas
                Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
                languesTraduites["fe"]= "Français-Anglais";
                languesTraduites["fs"]= "Français-Espagnol";
                languesTraduites["ef"]= "Anglais-Français";
                serviceTraduction.LanguesTraduites = languesTraduites;
                // proxy
                //serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
                // tradução
                string texte = "ce chien est perdu";
                string deQuoiVersQuoi = "fe";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "l'été sera chaud";
                deQuoiVersQuoi = "fs";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "my tailor is rich";
                deQuoiVersQuoi = "ef";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "xx";
                deQuoiVersQuoi = "ef";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
            } catch (WebTraductionsException e) {
                // erro
                Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
            }
        }
    }
}

Os resultados obtidos são os seguintes:

1
2
3
4
Traduction [Français-Anglais] de [ce chien est perdu] : [this dog is lost]
Traduction [Français-Espagnol] de [l'été sera chaud] : [el verano será caliente]
Traduction [Anglais-Français] de [my tailor is rich] : [mon tailleur est riche]
Traduction [Anglais-Français] de [xx] : [xx]

O projeto [dao] da solução é compilado num DLL HttpTraductions.dll:

 

11.7.3.6. A interface gráfica da aplicação

Voltemos à arquitetura da nossa aplicação:

Estamos agora a escrever a camada [ui]. Esta é o objeto do projeto [ui] da solução em desenvolvimento:

A pasta [lib] [3] contém algumas das DLL referenciadas pelo projeto [4]:

  • as necessárias para o Spring: Spring.Core, Common.Logging, antlr.runtime
  • a da camada [dao]: HttpTraductions

O ficheiro [App.config] contém a configuração do Spring:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <description>Traductions sur le web</description>
            <!-- o serviço de tradução -->
            <object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
                <property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&amp;translationDirection={0}&amp;stext={1}"/>
                <!--
                <property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
                -->
                <property name="RegexTraduction" value="&lt;div class=&quot;txtTrad&quot;&gt;(.*?)&lt;/div&gt;"/>
                <property name="LanguesTraduites">
                    <dictionary key-type="string" value-type="string">
                        <entry key="fe" value="Français-Anglais"/>
                        <entry key="ef" value="Anglais-Français"/>
...
                        <entry key="ei" value="Anglais-Italien"/>
                        <entry key="ie" value="Italien-Anglais"/>
                    </dictionary>
                </property>
            </object>
        </objects>
    </spring>
</configuration>
  • linha 15: os objetos a instanciar pelo Spring. Haverá apenas um, o da linha 18, que instancia o serviço de tradução com a classe ServiceTraduction encontrada no DLL HttpTraductions.
  • linha 19: a propriedade UrlServeurTraduction da classe ServiceTraduction. Existe um problema com o caractere & da URL. Este caractere tem um significado num ficheiro XML. Por isso, deve ser protegido. O mesmo se aplica a outros caracteres que iremos encontrar ao longo do ficheiro. Devem ser substituídos por uma sequência [&code;]: & por [&amp;], < por [&lt;], > por [&gt;], " por [&quot;].
  • linha 21: a propriedade ProxyHttp da classe ServiceTraduction. Resta uma propriedade não inicializada: null. Não definir esta propriedade equivale a indicar que não existe um proxy HTTP.
  • linha 23: a propriedade RegexTraduction da classe ServiceTraduction. Na expressão regular, foi necessário substituir os caracteres [< > "] pelos seus equivalentes protegidos.
  • linhas 24-33: a propriedade LanguesTraduites da classe ServiceTraduction.

O programa [Program.cs] é executado no arranque da aplicação. O seu código é o seguinte:


using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;

namespace ui {
    static class Program {
        /// <summary>
        /// O ponto de entrada principal da aplicação.
        /// </summary>
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
    
            // --------------- Código do programador
            // instanciação do serviço de tradução
            IApplicationContext ctx = null;
            Exception ex = null;
            ServiceTraduction serviceTraduction = null;
            try {
                // contexto Spring
                ctx = ContextRegistry.GetContext();
                // solicitação de uma referência ao serviço de tradução
                serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
            } catch (Exception e1) {
                // registo da exceção
                ex = e1;
            }
            // formulário a apresentar
            Form form = null;
            // ocorreu alguma exceção?
            if (ex != null) {
                // sim — cria-se a mensagem de erro a apresentar
                StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
                Exception e = ex;
                while (e != null) {
                    msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
                    msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
                    e = e.InnerException;
                }
                // criação da janela de erro para a qual se passa a mensagem de erro a apresentar
                Form2 form2 = new Form2();
                form2.MsgErreur = msgErreur.ToString();
                // esta será a janela a apresentar
                form = form2;
            } else {
                // tudo correu bem
                // criação da interface gráfica [Form1], para a qual se passa a referência ao serviço de tradução
                Form1 form1 = new Form1();
                form1.ServiceTraduction = serviceTraduction;
                // esta será a janela a apresentar
                form = form1;
            }
            // exibição da janela
            Application.Run(form);
        }
    }
}

Este código já foi utilizado na aplicação Impostos, versão 6, no parágrafo 7.6.2.

  • O serviço de tradução é criado na linha 27 pelo Spring. Se esta criação tiver ocorrido com sucesso, será apresentado o formulário [Form1] (linhas 52-55); caso contrário, será apresentado o formulário de erro [Form2] (linhas 36-48).

O formulário [Form2] é o utilizado na aplicação Impostos, versão 6, e foi explicado no parágrafo 7.6.4.

O formulário [Form1] é o seguinte:

n.º
tipo
nome
função
1
TextBox
textBoxTexteATraduire
caixa de texto a traduzir
MultiLine=true
2
ComboBox
comboBoxLangues
lista de sentidos de tradução
3
Botão
buttonTraduire
para solicitar a tradução do texto [1] na direção [2]
4
TextBox
textBoxTraduction
a tradução do texto [1]

O código do formulário [Form1] é o seguinte:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;

namespace ui {
    public partial class Form1 : Form {
        // serviço de tradução
        public ServiceTraduction ServiceTraduction { get; set; }
        // dicionário de línguas
        Dictionary<string, string> languesInversées = new Dictionary<string, string>();

        // construtor
        public Form1() {
            InitializeComponent();
        }

        // carregamento inicial do formulário
        private void Form1_Load(object sender, EventArgs e) {
            // construção do dicionário inverso de línguas
            foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
                // línguas
                string langues = ServiceTraduction.LanguesTraduites[code];
                // adição (línguas, código) ao dicionário inverso
                languesInversées[langues] = code;
            }
            // preenchimento da lista suspensa por ordem alfabética dos idiomas
            string[] languesCombo = languesInversées.Keys.ToArray();
            Array.Sort<string>(languesCombo);
            foreach (string langue in languesCombo) {
                comboBoxLangues.Items.Add(langue);
            }
            // seleção da primeira língua
            if (comboBoxLangues.Items.Count != 0) {
                comboBoxLangues.SelectedIndex = 0;
            }
        }

        private void buttonTraduire_Click(object sender, EventArgs e) {
            // Tem alguma coisa para traduzir?
            string texte = textBoxTexteATraduire.Text.Trim();
            if (texte == "") return;
            // tradução
            try {
                textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
            } catch (Exception ex) {
                textBoxTraduction.Text = ex.Message;
            }
        }
    }
}
  • linha 10: uma referência ao serviço de tradução. Esta propriedade pública foi inicializada por [Program.cs], linha 53. Quando os métodos Form1_Load (linha 20) ou buttonTraduire_Click (linha 40) são executados, este campo já se encontra, portanto, inicializado.
  • linha 12: o dicionário das línguas traduzidas com entradas do tipo ["Français-Anglais","fe"], c.a.d. O inverso do dicionário LanguesTraduites fornecido pelo serviço de tradução.
  • linha 20: o método Form1_Load é executado ao carregar o formulário.
  • linhas 22-27: utilizam-se os dicionários serviceTraduction.LanguesTraduites e ["fe","Français-Anglais"] para construir os dicionários languesInversées e ["Français-Anglais", "fe"].
  • linha 29: languesCombo é a tabela de chaves dos dicionários languesInversées e c.a.d. Uma tabela de elementos ["Français-Anglais"]
  • linha 30: esta tabela está ordenada de forma a apresentar no menu suspenso os sentidos de tradução por ordem alfabética
  • linhas 31-33: a lista suspensa de idiomas é preenchida.
  • linha 40: o método executado quando o utilizador clica no botão [Traduire]
  • linha 46: basta chamar o método serviceTraduction.Traduire para solicitar a tradução. O primeiro parâmetro é o texto a traduzir, o segundo é o código da direção de tradução. Este código é encontrado no dicionário languesInversées com base no elemento selecionado na lista suspensa de idiomas.
  • linha 48: se ocorrer uma exceção, esta é apresentada no lugar da tradução.

11.7.3.7. Conclusion

Esta aplicação demonstrou que os clientes web do framework .NET nos permitiam explorar os recursos da web. A técnica é sempre semelhante:

  • determinar a URI a consultar. Esta URI é, na maioria das vezes, parametrizada.
  • consultá-la
  • encontrar na resposta do servidor o que se procura através de expressões regulares

Esta técnica é aleatória. Com efeito, ao longo do tempo, a URI consultada ou a expressão regular que permite encontrar o resultado esperado podem mudar. Por isso, é aconselhável colocar estas duas informações num ficheiro de configuração. Mas isso pode revelar-se insuficiente. Veremos no capítulo seguinte que existem recursos mais estáveis na Web: os serviços Web.

11.7.4. Um cliente SMTP (Simple Mail Transport Protocol) com a classe SmtpClient

Um cliente SMTP é um cliente de um servidor SMTP, servidor de envio de correio. A classe .NET SmtpClient encapsula totalmente as necessidades de um cliente deste tipo. O programador não precisa de conhecer os detalhes do protocolo SMTP. Nós conhecemos esse protocolo. Foi apresentado no parágrafo 11.4.3.

Apresentamos a classe SmtpClient no âmbito de uma aplicação Windows básica que permite enviar e-mails com anexos. A aplicação irá ligar-se à porta 25 de um servidor SMTP. Recorde-se que, na maioria dos sistemas Windows, as firewalls ou outros antivírus bloqueiam as ligações à porta 25. Por isso, é necessário desativar essa proteção para testar a aplicação:

O cliente SMTP terá uma arquitetura de camada única:

O projeto do Visual Studio é o seguinte:

  

A interface gráfica [SendMailForm.cs] da aplicação é a seguinte:

n.º
tipo
nome
função
1
TextBox
textBoxServeur
nome do servidor SMTP ao qual se deve ligar
2
NumericUpDown
numericUpDownPort
a porta à qual se deve ligar
3
TextBox
textBoxExpediteur
endereço do remetente da mensagem
4
TextBox
textBoxTo
endereços dos destinatários no formato: endereço1, endereço2, ...
5
TextBox
textBoxCc
endereços dos destinatários em cópia (CC=Carbon Copy) no formato: endereço1, endereço2, ...
6
TextBox
textBoxBcc
endereços dos destinatários em cópia oculta (BCC=Blind Carbon Copy) no formato: endereço1, endereço2, ... Todos os endereços destes três campos de introdução receberão a mesma mensagem com os mesmos anexos. Os destinatários da mensagem poderão saber quais eram os endereços que constavam nos campos 4 e 5, mas não os do campo 6. A CCO é, portanto, uma forma de colocar alguém em cópia sem que os outros destinatários da mensagem tenham conhecimento disso.
7
Botão
buttonAjouter
para adicionar um anexo ao e-mail
8
ListBox
listBoxPiecesJointes
lista dos anexos a incluir no e-mail
9
TextBox
textBoxSujet
assunto da carta
10
TextBox
textBoxMessage
o texto da mensagem.
MultiLine=true
11
Botão
buttonEnvoyer
para enviar a mensagem e eventuais anexos
12
TextBox
textBoxRésultat
apresenta um resumo da mensagem enviada ou uma mensagem de erro, caso tenha ocorrido algum problema
13
Botão
buttonEffacer
para apagar [12]
 
OpenfileDialog
openFileDialog1
verificação não visual que permite a seleção de um anexo no sistema de ficheiros local

No exemplo anterior, o resumo apresentado em [12] é o seguinte:

Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc : 
Bcc : 
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,

Vous trouverez ci-joint le dossier de candidature à l'ISTIA.

Cordialement,

ST

O código do formulário [SendMailForm.cs] é o seguinte:


using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;

namespace Chap9 {
    public partial class SendMailForm : Form {
        public SendMailForm() {
            InitializeComponent();
        }

        // adição de um anexo
        private void buttonAjouter_Click(object sender, EventArgs e) {
            // configurar a caixa de diálogo openfileDialog1
            openFileDialog1.InitialDirectory = Application.ExecutablePath;
            openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
            openFileDialog1.FilterIndex = 0;
            openFileDialog1.FileName = "";
            // exibe-se a caixa de diálogo e recupera-se o seu resultado
            if (openFileDialog1.ShowDialog() == DialogResult.OK) {
                // recuperar o nome do ficheiro
                listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
            }
        }

        private void textBoxServeur_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void setStatutEnvoyer() {
            buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
        }

        // remover um anexo
        private void buttonRetirer_Click(object sender, EventArgs e) {
            // anexo selecionado?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // retira-o
                listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
                // atualiza-se o botão «Remover»
                buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
            }
        }

        private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
            // anexo selecionado?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // atualiza-se o botão «Retirar»
                buttonRetirer.Enabled = true;
            }
        }

        // envio da mensagem com os seus anexos
        private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
        }

        private void textBoxTo_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void textBoxSujet_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void buttonEffacer_Click(object sender, EventArgs e) {
            textBoxResultat.Text = "";
        }
    }
}

Não iremos comentar este código, uma vez que não apresenta novidades. Para compreender o método buttonAjouter_Click da linha 14, convidamos o leitor a reler o parágrafo 7.5.1.

O método buttonEnvoyer_Click da linha 55, que envia o correio, é o seguinte:


private void buttonEnvoyer_Click(object sender, EventArgs e) {
            try {
                // ampulheta
                Cursor = Cursors.WaitCursor;
                // o cliente SMTP
                SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
                // a mensagem
                MailMessage message = new MailMessage();
                // remetente
                message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
                message.From = message.Sender;
                // destinatários
                Regex marqueur = new Regex("\\s*,\\s*");
                string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
                foreach (string destinataire in destinataires) {
                    if (destinataire.Trim() != "") {
                        message.To.Add(new MailAddress(destinataire));
                    }
                }
                // CC
                string[] copies = marqueur.Split(textBoxCc.Text.Trim());
                foreach (string copie in copies) {
                    if (copie.Trim() != "") {
                        message.CC.Add(new MailAddress(copie));
                    }
                }
                // BCC
                string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
                foreach (string blindCopie in blindCopies) {
                    if (blindCopie.Trim() != "") {
                        message.Bcc.Add(new MailAddress(blindCopie));
                    }
                }
                // assunto
                message.Subject = textBoxSujet.Text.Trim();
                // texto da mensagem
                message.Body = textBoxMessage.Text;
                // anexos
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    message.Attachments.Add(new Attachment(attachement));
                }
                // envio da mensagem
                smtpClient.Send(message);
                // Ok - é apresentado um resumo
                StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
                msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
                textBoxSujet.Clear();
                msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
                textBoxTo.Clear();
                msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
                textBoxCc.Clear();
                msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
                textBoxBcc.Clear();
                msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
                }
                msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
                listBoxPiecesJointes.Items.Clear();
                textBoxResultat.Text = msg.ToString();
            } catch (Exception ex) {
                // é apresentado o erro
                textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
            }
            // cursor normal
            Cursor = Cursors.Arrow;
        }
  • linha 6: o cliente SMTP é criado. Este necessita de dois parâmetros: o nome do servidor SMTP e a porta em que este opera
  • linha 8: é criada uma mensagem do tipo MailMessage. É esta que irá encapsular a totalidade da mensagem a enviar.
  • linha 10: o endereço de e-mail «Sender» do remetente é preenchido. Um endereço de e-mail é uma instância do tipo MailAddress construída a partir de uma cadeia de caracteres «xx@yy.zz». Esta cadeia de caracteres tem de ter o formato esperado para um endereço de e-mail; caso contrário, é lançada uma exceção. Nesse caso, será apresentada no campo textBoxResultat (linha 63) num formato pouco intuitivo.
  • linhas 13-19: os endereços de e-mail dos destinatários são colocados na lista «Para» da mensagem. Estes endereços são recuperados do campo textBoxTo. A expressão regular da linha 13 permite recuperar os diferentes endereços, que estão separados por uma vírgula.
  • linhas 21-26: repete-se o mesmo processo para inicializar o campo CC da mensagem com os endereços em cópia do campo textBoxCc.
  • linhas 28-33: repete-se o mesmo processo para inicializar o campo Cco da mensagem com os endereços em cópia oculta do campo textBoxBcc.
  • linha 35: o campo Subject da mensagem é inicializado com o assunto do campo textBoxSujet.
  • linha 37: o campo «Body» da mensagem é inicializado com o texto da mensagem textBoxMessage.
  • linhas 39-41: os anexos são adicionados à mensagem. Cada anexo é adicionado sob a forma de um objeto «Attachment» ao campo «Attachments» da mensagem. Um objeto «Attachment» é instanciado a partir do caminho completo do ficheiro a anexar no sistema de ficheiros local.
  • linha 43: a mensagem é enviada utilizando o método Send do cliente SMTP.
  • linhas 45-60: gravação do resumo do envio no campo textBoxResultat e reinicialização do formulário.
  • linha 63: exibição de um eventual erro

11.8. Um cliente TCP genérico assíncrono

11.8.1. Introdução

Em todos os exemplos deste capítulo, a comunicação cliente/servidor era feita em modo bloqueante, também designado por modo síncrono:

  • quando um cliente se liga a um servidor, aguarda a resposta do servidor a esse pedido antes de continuar.
  • Quando um cliente lê uma linha de texto enviada pelo servidor, fica bloqueado enquanto o servidor não a tiver enviado.
  • Do lado do servidor, os threads de serviço que prestam o serviço ao cliente funcionam da mesma forma que acima.

Nas interfaces gráficas, é frequentemente necessário não bloquear o utilizador durante operações demoradas. O caso frequentemente citado é o do download de um ficheiro de grande dimensão. Durante esse download, é necessário permitir que o utilizador continue a interagir com a interface gráfica.

Propomos aqui reescrever o cliente TCP genérico do parágrafo 11.6.3, introduzindo as seguintes alterações:

  • a interface será gráfica
  • a ferramenta de comunicação com o servidor será um objeto Socket
  • o modo de comunicação será assíncrono:
    • o cliente iniciará uma ligação ao servidor, mas não ficará bloqueado à espera que esta seja estabelecida
    • o cliente iniciará um envio para o servidor, mas não ficará bloqueado à espera que este seja concluído
    • o cliente iniciará a receção de dados provenientes do servidor, mas não ficará bloqueado à espera que esta termine.

Recorde-se em que nível se situa o objeto Socket na comunicação cliente/servidor TCP:

A classe Socket é a que opera mais próximo da rede. Permite gerir com precisão a ligação de rede. O termo socket designa uma tomada elétrica. O termo foi alargado para designar uma tomada de rede de software. Numa comunicação TCP-IP entre duas máquinas A e B, são dois sockets que comunicam entre si. Uma aplicação pode trabalhar diretamente com os sockets. É o caso da aplicação A acima referida. Um socket pode ser um socket client ou serveur.

11.8.2. A interface gráfica do cliente TCP assíncrono

A aplicação do Visual Studio é a seguinte:

  

[ClientTcpAsynchrone.cs] é a interface gráfica. Esta é a seguinte:

n.º
tipo
nome
função
1
TextBox
textBoxNomServeur
nome do servidor TCP ao qual se deve ligar
2
NumericUpDown
numericUpDownPortServeur
a porta à qual se deve ligar
3
RadioButton
radioButtonLF
radioButtonRCLF
para indicar o caractere de fim de linha que o cliente deve utilizar: LF "\n" ou RCLF "\r\n"
4
Botão
buttonConnexion
para se ligar à porta [2] do servidor [1]. O botão apresenta a legenda [Connecter] quando o cliente não está ligado a um servidor e [Déconnecter] quando está ligado.
5
TextBox
textBoxMsgToServeur
mensagem a enviar para o servidor assim que a ligação for estabelecida. Quando o utilizador premir a tecla [Entrée], a mensagem é enviada com o caractere de fim de linha selecionado em [3]
6
ListBox
listBoxEvts
lista na qual são apresentados os principais eventos da ligação cliente/servidor: ligação, desligamento, encerramento de fluxo, erros de comunicação
7
ListBox
listBoxDialogue
lista na qual são apresentadas as mensagens do diálogo cliente/servidor
8
Botão
buttonRazEvts
para apagar a lista [6]
4
Botão
buttonRazDialogue
para apagar a lista [7]

Os princípios de funcionamento desta interface são os seguintes:

  • o utilizador liga o seu cliente TCP gráfico a um serviço TCP através de [1, 2, 3, 4].
  • Um thread assíncrono aceita continuamente todos os dados enviados pelo servidor TCP e apresenta-os na lista [7]. Este thread está dissociado das outras atividades da interface.
  • O utilizador pode enviar mensagens ao servidor ao seu próprio ritmo através de [5]. Cada mensagem é enviada por um thread assíncrono. Ao contrário do thread de receção, que nunca pára, o thread de transmissão termina assim que a mensagem é enviada. Será utilizado um novo thread assíncrono para a mensagem seguinte.
  • A comunicação cliente/servidor termina quando um dos parceiros encerra a ligação. O utilizador pode tomar essa iniciativa através do botão [4], que, uma vez estabelecida a ligação, passa a ter a designação [Déconnecter].

Eis uma captura de ecrã de uma execução:

  • em [1]: ligação a um serviço POP
  • em [2]: exibição dos eventos ocorridos durante a ligação
  • em [3]: a mensagem enviada pelo servidor POP após a conclusão da ligação
  • em [4]: o botão [Connecter] passou a ser o botão [Déconnecter]
  • em [1], foi enviado o comando quit ao servidor POP. O servidor respondeu +OK goodbye e encerrou a ligação
  • em [2], este encerramento do lado do servidor foi detetado. O cliente encerrou então a ligação do seu lado.
  • em [3], o botão [Déconnecter] voltou a ser um botão [Connecter]

11.8.3. Ligação assíncrona ao servidor

Ao premir o botão [Connecter], é executado o seguinte método:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // ligar ou desligar?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • linha 3: o botão pode ter o texto [Connecter] ou [Déconnecter].

O método de ligação é o seguinte:


using System.Net.Sockets;
...

namespace Chap9 {
    public partial class ClientTcp : Form {
        const int tailleBuffer = 1024;
        private Socket client = null;
        private byte[] data = new byte[tailleBuffer];
        private string réponse = null;
        private string finLigne = "\r\n";

        // delegados
        public delegate void writeLog(string log);

        public ClientTcp() {
            InitializeComponent();
        }
....................................
    private void connexion() {
            // verificações de dados
            string nomServeur = textBoxNomServeur.Text.Trim();
            if (nomServeur == "") {
                logEvent("indiquez le nom du serveur");
                return;
            }
            // acompanhamento
            logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
            try {
                 // criação de socket
                client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                // ligação assíncrona
                client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);

            } catch (Exception ex) {
                logEvent(String.Format("erreur de connexion : {0}", ex.Message));
                return;
            }
        }

         // a ligação foi estabelecida
        private void connecté(IAsyncResult résultat) {
            // recuperação do socket do cliente
            Socket client = résultat.AsyncState as Socket;
    ...
        }


        // acompanhamento do processo
        private void logEvent(string msg) {
....
        }
    }
}
  • linha 1: a classe Socket faz parte do espaço de nomes System.Net.Sockets.

É necessário partilhar alguns dados entre vários métodos do formulário. São os seguintes:

  • linha 7: «client» é o socket de comunicação com o servidor
  • linhas 6 e 8: o cliente irá receber as suas mensagens num array de bytes chamado «data».
  • linha 9: «resposta» é a resposta enviada pelo servidor.
  • linha 10: finLigne é o marcador de fim de linha utilizado pelo cliente TCP — é inicializado por predefinição como RCLF, mas pode ser alterado pelo utilizador através dos botões de opção [3].

O procedimento connexion da linha 19 estabelece a ligação ao servidor TCP:

  • linhas 21-25: verifica-se se o nome do servidor não está vazio. Se não for esse o caso, o evento é registado em listBoxEvts através do método logEvent da linha 49.
  • linha 27: é sinalizado que a ligação vai ocorrer
  • linha 30: cria-se o objeto Socket necessário para a comunicação TCP/IP. O construtor aceita três parâmetros:
    • AddressFamily addressFamily: a família de endereços IP do cliente e do servidor, neste caso endereços IPv4 (AddressFamily.InterNetwork)
    • SocketType socketType: o tipo de socket. O tipo SocketType.Stream é adequado para ligações TCP/IP
    • ProtocolType protocolType: o tipo de protocolo de Internet utilizado, neste caso o protocolo TCP
  • linha 32: a ligação é estabelecida de forma assíncrona. A ligação é iniciada, mas a execução continua sem aguardar o seu término. O método [Socket].BeginConnect aceita quatro parâmetros:
    • IPAddress ipAddress: o endereço IP do computador no qual está a ser executado o serviço ao qual é necessário ligar-se
    • Int32 port: a porta do serviço
    • AsyncCallBack asyncCallBack: AsyncCallBack é um tipo delegado:
public void AsyncCallBack(IAsyncResult ar);

O método asyncCallBack, passado como terceiro parâmetro do método BeginConnect, deve ser um método que aceite um tipo IAsyncCallBack e que não retorne qualquer resultado. É este método que será chamado assim que a ligação for estabelecida. Aqui, passamos como terceiro parâmetro o método connecté da linha 41.

  • (continuação)
    • Objeto state: um objeto a passar para o método asyncCallBack. Este método recebe (ver delegado acima) um parâmetro ar do tipo IAsyncResult. O objeto state poderá ser recuperado em ar.AsyncState (linha 43). Aqui, passamos como quarto parâmetro o socket do cliente.
  • linha 38: o método está concluído. O utilizador pode voltar a interagir com a interface gráfica. A ligação decorre em segundo plano, em paralelo com a gestão dos eventos da interface gráfica. Ainda em paralelo, o método connecté da linha 41 será chamado no final da ligação, quer esta termine com sucesso ou não.

O código do método connecté é o seguinte:


// a ligação foi estabelecida
        private void connecté(IAsyncResult résultat) {
            // recuperação do socket do cliente
            Socket client = résultat.AsyncState as Socket;
            try {
                // a operação assíncrona é concluída
                client.EndConnect(résultat);
                // acompanhamento
                logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
                // formulário
                buttonConnexion.Text = "Déconnecter";
                // leitura assíncrona de dados provenientes do servidor
                réponse = "";
                client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
            } catch (SocketException e) {
                logEvent(String.Format("erreur de connexion : {0}", e.Message));
                return;
            }
}

        // recepção de dados
        private void lecture(IAsyncResult résultat) {
            // recuperação do socket do cliente
            Socket client = résultat.AsyncState as Socket;
...
        }

  • linha 4: o socket do cliente é recuperado no parâmetro résultat recebido pelo método. Recorde-se que este objeto é aquele passado como 4.º parâmetro do método BeginConnect.
  • linha 7: a tentativa de ligação é concluída pelo método EndConnect, ao qual deve ser passado o parâmetro résultat recebido pelo método.
  • linha 9: o evento é registado na lista de eventos
  • linha 11: o botão [Connecter] passa a ser um botão [Déconnecter] para que o utilizador possa solicitar o encerramento da sessão.
  • linha 13: a resposta do servidor é inicializada. Será atualizada através de chamadas repetidas ao método assíncrono BeginReceive.
  • linha 14: 1.ª chamada ao método assíncrono BeginReceive. Este é chamado com os seguintes parâmetros:
    • byte[] buffer: o buffer no qual colocar os dados que vão ser recebidos — neste caso, o buffer é data
    • int offset: a partir de que posição do buffer devem ser colocados os dados a receber — neste caso, o deslocamento é 0, c.a.d, o que significa que os dados são colocados a partir do primeiro byte do buffer.
    • int size: o tamanho em octetos do buffer — neste caso, o tamanho é tailleBuffer.
    • SocketFlags socketFlags: configuração do socket — neste caso, nenhuma configuração
    • AsyncCallBack asyncCallBack: o método a ser chamado quando a receção estiver concluída. Isso acontecerá quer porque o buffer recebeu dados, quer porque a ligação foi encerrada. Aqui, o método de chamada de retorno é o método lecture da linha 22.
    • Objeto state: o objeto a passar para o método de chamada de retorno asyncCallBack. Aqui, passa-se novamente o socket do cliente.

Note-se que tudo isto ocorre sem qualquer ação por parte do utilizador, para além do pedido inicial de ligação através do botão [Connecter]. No final do método connecté, é executado outro método em segundo plano: o método lecture, que vamos analisar agora.


// recepção de dados
        private void lecture(IAsyncResult résultat) {
            // recuperação do socket do cliente
            Socket client = résultat.AsyncState as Socket;
            int nbOctetsReçus = 0;
            bool erreur = false;
            try {
                // número de bytes recebidos
                nbOctetsReçus = client.EndReceive(résultat);
                if (nbOctetsReçus == 0) {
                    // o servidor deixou de responder
                    logEvent("le serveur a fermé la connexion");
                }
            } catch (Exception e) {
                // ocorreu um problema na receção
                logEvent(String.Format("erreur de réception : {0}", e.Message));
                erreur = true;
            }
            // terminado?
            if (nbOctetsReçus == 0 || erreur) {
                // desligamos o cliente, se necessário
                déconnexion();
                // exibe-se o fim da resposta
                afficherRéponseServeur(réponse, true);
                // fim da leitura
                return;
            }
            // recuperam-se os dados recebidos
            string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
            // adiciona-se aos dados já recebidos
            réponse += données;
            // exibe-se a resposta
            afficherRéponseServeur(réponse, false);
            // continua-se a ler
            client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
        }
  • linha 2: o método lecture é acionado em segundo plano quando o buffer data recebe dados ou quando a ligação é encerrada pelo servidor.
  • linha 9: o pedido assíncrono de leitura é concluído pelo método EndReceive. Mais uma vez, este método deve ser chamado com o parâmetro recebido pela função de retorno. O método EndReceive devolve o número de bytes recebidos no buffer de leitura.
  • linha 10: se o número de bytes for zero, significa que a ligação foi encerrada pelo servidor.
  • linha 12: regista-se o evento na lista de eventos
  • linha 14: trata-se de uma eventual exceção
  • linhas 16-17: regista-se o evento na lista de eventos e regista-se o erro
  • linha 20: verifica-se se é necessário encerrar a ligação
  • linha 22: encerra-se a ligação do lado do cliente com um método déconnexion, que veremos mais adiante.
  • linha 24: a resposta do servidor, c.a.d. A variável global réponse é apresentada na lista de diálogo listBoxDialogue através de um método privado afficherRéponseServeur.
  • linha 26: fim do método assíncrono lecture
  • linha 29: os bytes recebidos são colocados numa cadeia de caracteres no formato UTF8.
  • linha 31: são adicionados à resposta que está a ser construída
  • linha 33: a resposta é apresentada na lista listBoxDialogue.
  • linha 35: volta-se a aguardar dados provenientes do servidor

Em suma, o método assíncrono lecture nunca termina. De forma contínua, lê os dados provenientes do servidor e apresenta-os na lista listBoxDialogue. Só termina quando a ligação é encerrada, quer pelo servidor, quer pelo próprio utilizador.

11.8.4. Desligar do servidor

Ao premir o botão [Déconnecter], é executado o seguinte método:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // ligar ou desligar?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • linha 3: o botão pode ter a legenda [Connecter] ou [Déconnecter].

O método déconnexion assegura o desligamento do cliente:


private void déconnexion() {
            // fecho do socket
            if (client != null && client.Connected) {
                try {
                    // acompanhamento
                    logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
                    // desligar
                    client.Shutdown(SocketShutdown.Both);
                    client.Close();
                    // formulário
                    buttonConnexion.Text = "Connecter";
                } catch (Exception ex) {
                    // acompanhamento
                    logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
                }
            }
        }
  • linha 3: se o cliente existir e estiver ligado
  • linha 6: anuncia-se a desconexão em listBoxEvts. A propriedade client.RemoteEndPoint fornece o par (endereço IP, porta) da outra extremidade da ligação, c.a.d, neste caso, do servidor.
  • linha 8: o fluxo de dados do socket é fechado com o método ShutDown. O fluxo de dados de um socket é bidirecional: o socket transmite e recebe dados. O parâmetro do método ShutDown pode ser: ShutDown.Receive para fechar o fluxo de receção, Shutdonw.Send para fechar o fluxo de transmissão ou ShutDown.Both para fechar ambos os fluxos.
  • linha 9: libertam-se os recursos associados ao socket
  • linha 11: o botão [Déconnecter] passa a ser o botão [Connecter]
  • linhas 12-15: gestão de uma eventual exceção

11.8.5. Envio assíncrono de dados para o servidor

Quando o utilizador valida a mensagem do campo textBoxMsgToServeur, é executado o seguinte método:


        private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
            // tecla [Entrée] ?
            if (e.KeyChar == 13 && client.Connected) {
                envoyerMessage();
            }
}
  • linhas 3-5: se o utilizador tiver premido a tecla [Entrée] e se o socket do cliente estiver ligado, então a mensagem do campo textBoxMsgToServeur é enviada com o método envoyerMessage.

O método envoyerMessage é o seguinte:


        private void envoyerMessage() {
            // enviar uma mensagem de forma assíncrona
            // a mensagem
            byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
            // foi enviada
            client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
            // diálogo
            logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
            // limpar mensagem
            textBoxMsgToServeur.Clear();
}
  • linha 4: adiciona-se à mensagem o marcador de fim de linha do cliente e coloca-se na tabela de bytes message.
  • linha 6: é iniciada uma transmissão assíncrona com o método BeginSend. Os parâmetros do método BeginSend são idênticos aos do método BeginReceive. No final da operação de envio assíncrono da mensagem, será chamado o método écriture.
  • linha 8: a mensagem enviada é adicionada à lista listBoxDialogue para permitir o acompanhamento do diálogo cliente/servidor
  • linha 10: a mensagem enviada é removida da interface gráfica

O método de retorno écriture é o seguinte:


        private void écriture(IAsyncResult résultat) {
            // resultado do envio de uma mensagem
            Socket client = résultat.AsyncState as Socket;
            try {
                client.EndSend(résultat);
            } catch (Exception e) {
                // ocorreu um problema no envio
                logEvent(String.Format("erreur d'émission : {0}", e.Message));
            }
}
  • linha 4: o método de retorno écriture recebe um parâmetro de resultado do tipo IAsyncResult.
  • linha 3: no parâmetro résultat, recupera-se o socket do cliente. Este socket era o 5.º parâmetro do método BeginSend.
  • linha 5: conclui-se a operação assíncrona de envio.

Não se aguarda o fim da transmissão de uma mensagem para devolver o controlo ao utilizador. Desta forma, o utilizador pode enviar uma segunda mensagem enquanto a transmissão da primeira ainda não está concluída.

11.8.6. Exibição dos eventos e do diálogo cliente/servidor

Os eventos são apresentados pelo método logEvents:


        // acompanhamento do processo
        private void logEvent(string msg) {
            listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
        }

        private void logEventCallBack(string msg) {
            // exibição da mensagem
            msg = msg.Replace(finLigne, " ");
            listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
  • linha 2: o método logEvents recebe como parâmetro a mensagem a adicionar à lista listBoxEvts.
  • linha 3: não é possível utilizar diretamente o componente listBoxEvents. Com efeito, o método logEvents é chamado por dois tipos de threads:
    • o thread principal, proprietário da interface gráfica, por exemplo, quando sinaliza que está em curso uma tentativa de ligação
    • um thread secundário que assegura uma operação assíncrona. Este tipo de thread não é proprietário dos componentes e o seu acesso a um componente C deve ser controlado por uma operação C.Invoke. Esta operação indica ao controlo C que um thread pretende realizar uma operação sobre ele. O método Invoke aceita dois parâmetros:
      • uma função de retorno do tipo delegate. Esta função de retorno será executada pelo thread proprietário da interface gráfica e não pelo thread que executa o método C.Invoke.
      • um objeto que será passado à função de retorno de chamada.

Aqui, o primeiro parâmetro passado ao método Invoke é uma instância do seguinte delegado:


        public delegate void writeLog(string log);

O delegado writeLog tem um parâmetro do tipo string e não devolve qualquer resultado. O parâmetro será a mensagem a registar em listBoxEvts.

Na linha 3, o primeiro parâmetro passado ao método Invoke é o método logEventCallBack da linha 6. Este corresponde, de facto, à assinatura do delegado writeLog. O segundo parâmetro passado ao método Invoke é a mensagem que será passada como parâmetro ao método logEventCallBack.

A operação Invoke é uma operação síncrona. A execução da thread secundária fica bloqueada até que a thread proprietária do controlo execute o método de retorno.

  • linha 6: o método de retorno executado pela thread da interface gráfica recebe a mensagem a apresentar no controlo listBoxEvts.
  • linha 9: o evento é registado na primeira posição da lista, de modo a que os eventos mais recentes fiquem no topo da lista.

As mensagens do diálogo cliente/servidor são exibidas pelo método logDialogue:


        // acompanhamento do diálogo
        private void logDialogue(string msg) {
            listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
        }
        private void logDialogueCallBack(string msg) {
            // exibição da mensagem
            msg = msg.Replace(finLigne, " ");
            listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}

O princípio é o mesmo que no método logEvent.

As mensagens recebidas pelo cliente são apresentadas pelo método afficherRéponseServeur:


        private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}

O primeiro parâmetro é a mensagem a apresentar. Esta mensagem pode ser uma sequência de linhas. Com efeito, o cliente lê os dados provenientes do servidor em blocos de tailleBuffer (1024) octetos. Nestes 1024 octetos, podem encontrar-se várias linhas que se reconhecem pela sua marca de fim de linha «\n». A última linha pode estar incompleta, encontrando-se o seu símbolo de fim de linha nos 1024 octetos seguintes. O método identifica na mensagem as linhas que terminam em «\n» e, em seguida, solicita ao logDialogue que as exiba. O segundo parâmetro do método indica se se deve exibir a última linha encontrada ou deixá-la no buffer para ser completada pela mensagem seguinte. O código é bastante complexo e não apresenta interesse neste contexto. Por isso, não será comentado.

11.8.7. Conclusão

O mesmo exemplo poderia ser tratado com operações síncronas. Neste caso, o aspeto assíncrono da interface gráfica pouco acrescenta ao utilizador. No entanto, se o utilizador se ligar e, em seguida, perceber que o servidor «já não responde», tem a possibilidade de se desligar, graças ao facto de a interface gráfica continuar a responder aos eventos durante a execução das operações assíncronas. Este exemplo bastante complexo permitiu-nos apresentar novos conceitos:

  • a utilização de sockets
  • a utilização de métodos assíncronos. O que foi abordado faz parte de uma norma. Existem outros métodos assíncronos que funcionam segundo o mesmo modelo.
  • a atualização de controlos de uma interface gráfica por threads secundárias.

A comunicação TCP/IP assíncrona apresenta vantagens mais significativas para um servidor do que as demonstradas no exemplo anterior. Sabe-se que o servidor atende os seus clientes através de threads secundárias. Se o seu conjunto de threads tiver N threads, isso significa que só pode atender N clientes simultaneamente. Se todas as N threads estiverem a realizar uma operação bloqueante (síncrona), não haverá mais threads disponíveis para um novo cliente até que uma das operações bloqueantes termine e liberte uma thread. Se, nessas threads, forem realizadas operações assíncronas em vez de síncronas, uma thread nunca fica bloqueada e pode ser rapidamente reutilizada para novos clientes.

11.9. Aplicação de exemplo, versão 8: Servidor de cálculo de impostos

11.9.1. A arquitetura da nova versão

Retomamos a aplicação de cálculo de impostos já abordada sob diversas formas. Recordemos a sua última versão, a da versão 7 do parágrafo 9.8.

Os dados estavam numa base de dados e a camada [ui] era uma interface gráfica:

 

Vamos retomar esta arquitetura e distribuí-la por duas máquinas:

  • uma máquina [serveur] irá alojar as camadas [metier] e [dao] da versão 7. Será criada uma camada TCP/IP [serveur] [1] para permitir que os utilizadores da Internet consultem o serviço de cálculo de impostos.
  • Uma máquina [client] irá hospedar a camada [ui] da versão 7. Será criada uma camada TCP/IP [client] [2] para permitir que a camada [ui] consulte o serviço de cálculo de impostos.

A arquitetura sofre aqui uma mudança profunda. A versão 7 era uma aplicação Windows para um único utilizador. A versão 8 passa a ser uma aplicação cliente/servidor da Internet. O servidor poderá servir vários clientes simultaneamente.

Vamos, em primeiro lugar, escrever a parte [serveur] da aplicação.

11.9.2. O servidor de cálculo de impostos

11.9.2.1. O projeto do Visual Studio

O projeto do Visual Studio será o seguinte:

  • em [1], o projeto. Nele encontram-se os seguintes elementos:
  • [ServeurImpot.cs]: o servidor TCP/IP para o cálculo do imposto, sob a forma de uma aplicação de consola.
  • [dbimpots.sdf]: a base de dados SQL Server Compact da versão 7, descrita no parágrafo 9.8.5.
  • [App.config]: o ficheiro de configuração da aplicação.
  • Na pasta [2], a pasta [lib] contém os ficheiros DLL necessários para o projeto:
    • [ImpotsV7-dao]: a camada [dao] da versão 7
    • [ImpotsV7-metier]: a camada [metier] da versão 7
    • [antlr.runtime, CommonLogging, Spring.Core] para o Spring
  • em [3], as referências do projeto

11.9.2.2. Configuração da aplicação

O ficheiro [App.config] é utilizado pelo Spring. O seu conteúdo é o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
                <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
                <constructor-arg index="2" value="select data1, data2, data3 from data"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • linhas 16-20: configuração da camada [dao] associada à base SQL Server compact
  • linhas 21-23: configuração da camada [metier].

Este é o ficheiro de configuração utilizado na camada [ui] da versão 7. Foi apresentado no parágrafo 9.8.4.

11.9.2.3. Funcionamento do servidor

Ao iniciar o servidor, a aplicação do servidor instancia as camadas [metier] e [dao] e, em seguida, apresenta uma interface de consola de administração:

  

A consola de administração aceita os seguintes comandos:

start port
para iniciar o serviço numa porta específica
stop
para parar o serviço. Este pode, posteriormente, ser reiniciado na mesma porta ou noutra.
echo start
para ativar o eco do diálogo cliente/servidor na consola
echo stop
para desativar o eco
status
para exibir o estado ativo/inativo do serviço
quit
para sair da aplicação

Vamos iniciar o servidor:

1
2
3
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt lancé sur le port 27
Serveur de calcul d'impôt >

Vamos agora executar o cliente TCP gráfico assíncrono analisado anteriormente no parágrafo 11.8.

Image

O cliente está ligado. Pode enviar os seguintes comandos ao servidor de cálculo de impostos:

aide
para obter a lista de comandos autorizados
impot marié nbEnfants salaireAnnuel
para calcular o imposto de alguém que tenha nbEnfants filhos e um salário de salaireAnnuel euros. marié vale o se a pessoa for casada, n caso contrário.
aurevoir
para encerrar a ligação com o servidor

Eis um exemplo de diálogo:

No lado do servidor, a consola apresenta o seguinte:

1
2
3
4
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt >Serveur de calcul d'impôt lancé sur le port 27
Début du service au client 0
Fin du service au client 0

Vamos ativar o eco e reiniciar um novo diálogo a partir do cliente gráfico:

 

A consola de administração apresenta então o seguinte:

1
2
3
4
5
6
7
echo start
Serveur de calcul d'impôt >Début du service au client 1
<--- Client 1 : aide
---> Client 1 : Commandes acceptées
1-aide
2-impot marié(O/N) nbEnfants salaireAnnuel
3-aurevoir
  • linha 1: o eco do diálogo cliente/servidor está ativado
  • linha 2: chegou um cliente
  • linha 3: enviou o comando [aide]
  • linhas 4-7: a resposta do servidor em 4 linhas.

Vamos parar o serviço:

1
2
3
stop
L'erreur suivante s'est produite sur le serveur : Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall
Serveur de calcul d'impôt >
  • linha 1: solicita-se o encerramento do serviço (não da própria aplicação)
  • linha 2: ocorreu uma exceção devido ao facto de o servidor, que se encontrava bloqueado numa espera por um cliente, ter sido abruptamente interrompido devido ao encerramento do serviço de escuta.
  • linha 3: o serviço pode agora ser reiniciado através do comando «start port» ou encerrado através de quit.

Antes de o serviço de escuta ser encerrado, um cliente estava a ser atendido numa outra ligação. Esta ligação não é encerrada pelo encerramento do socket de escuta. O cliente pode continuar a enviar comandos: o thread de serviço que lhe tinha sido associado antes do encerramento do serviço de escuta continua a responder-lhe:

Image

11.9.3. O código do servidor TCP para o cálculo de impostos

1
  

O código do servidor [ServeurImpot.cs] é o seguinte:


...
namespace Chap9 {
    public class ServeurImpot {

        // dados partilhados entre threads e métodos
        private static IImpotMetier metier = null;
        private static int port;
        private static TcpListener service;
        private static bool actif = false;
        private static bool echo = false;

        // programa principal
        public static void Main(string[] args) {
            // instâncias das camadas [metier] e [dao]
            IApplicationContext ctx = null;
            metier = null;
            try {
                // contexto Spring
                ctx = ContextRegistry.GetContext();
                // é solicitada uma referência na camada [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");

                // configuração do conjunto de threads
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);

                // lê os comandos de administração do servidor introduzidos pelo teclado num ciclo infinito
                string commande = null;
                string[] champs = null;
                while (true) {
                    // prompt
                    Console.Write("Serveur de calcul d'impôt >");
                    // leitura do comando
                    commande = Console.ReadLine().Trim().ToLower();
                    champs = Regex.Split(commande, @"\s+");
                    // execução do comando
                    switch (champs[0]) {
                        case "start":
                            // ativo?
                            if (actif) {
                                //erro
                                Console.WriteLine("Le serveur est déjà actif");
                            } else {
                                // verificação da porta
                                if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
                                    Console.WriteLine("Syntaxe : start port. Port incorrect");
                                } else {
                                    // inicia-se o serviço de escuta
                                    ThreadPool.QueueUserWorkItem(doEcoute, null);
                                }
                            }
                            break;
                        case "echo":
                            // echo start / stop
                            if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
                                Console.WriteLine("Syntaxe : echo start / stop");
                            } else {
                                echo = champs[1] == "start";
                            }
                            break;
                        case "stop":
                            // fim do serviço
                            if (actif) {
                                service.Stop();
                                actif = false;
                            }
                            break;
                        case "status":
                            // estado do servidor
                            if (actif) {
                                Console.WriteLine("Le service est lancé sur le port {0}", port);
                            } else {
                                Console.WriteLine("Le service n'est pas lancé}");
                            }
                            break;
                        case "quit":
                            // sair da aplicação
                            Console.WriteLine("Fin du service");
                            Environment.Exit(0);
                            break;
                        default:
                            // comando incorreto
                            Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
                            break;
                    }
                }
            } catch (Exception e1) {
                // exibição de exceção
                Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
                return;
            }
        }


        private static void doEcoute(Object data) {
...
        }

....
    }
}
  • linhas 18-21: as camadas [metier] e [dao] são instanciadas pelo Spring, configurado por [App.config]. A variável global metier da linha 6 é então inicializada.
  • linhas 24-25: configura-se o conjunto de threads da aplicação com um mínimo e um máximo de 10 threads.
  • linhas 30-86: o ciclo de entrada dos comandos de administração do serviço (start, stop, quit, echo, status).
  • linha 32: prompt do servidor para cada novo comando
  • linha 34: leitura do comando de administração
  • linha 35: o comando é dividido em campos para ser analisado
  • linhas 38-52: o comando «start port», que tem como objetivo iniciar o serviço de escuta
    • linha 40: se o serviço já estiver ativo, não há nada a fazer
    • linha 45: verifica-se se a porta existe e está correta. Se sim, a variável global port da linha 7 é definida.
    • linha 49: o serviço de escuta será gerido por um thread secundário, para que o thread principal possa continuar a executar os comandos da consola. Se o método doEcoute estabelecer a ligação com sucesso, as variáveis globais service da linha 8 e actif da linha 9 são inicializadas.
  • linhas 53-60: o comando echo start / stop que ativa / desativa o eco do diálogo cliente/servidor na consola
    • linha 58: a variável global echo da linha 7 é definida
  • linhas 61-67: o comando stop que encerra o serviço de escuta.
    • linha 64: paragem do serviço de escuta
  • linhas 68-75: o comando status, que apresenta o estado ativo/inativo do serviço
  • linhas 76-80: o comando quit que encerra tudo.

O thread encarregado de ouvir os pedidos dos clientes executa o seguinte método doEcoute:


        private static void doEcoute(Object data) {
            // thread de escuta de pedidos dos clientes
            try {
                // a criar o serviço
                service = new TcpListener(IPAddress.Any, port);
                // iniciando o serviço
                service.Start();
                // o servidor está ativo
                actif = true;
                // acompanhamento
                Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
                // ciclo de atendimento aos clientes
                TcpClient tcpClient = null;
                // n.º do cliente
                int numClient = 0;
                // ciclo infinito
                while (true) {
                    // à espera de um cliente
                    tcpClient = service.AcceptTcpClient();
                    // o serviço é prestado por outra tarefa
                    ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // próximo cliente
                    numClient++;
                }
            } catch (Exception ex) {
                // o erro é sinalizado
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            }
        }

        // informações do cliente
        internal class Client {
            public TcpClient CanalTcp { get; set; }        // ligação com o cliente
            public int NumClient { get; set; }            // n.º do cliente
}

Temos aqui um código semelhante ao do servidor de eco analisado no parágrafo 11.6.1. Comentamos apenas o que é diferente:

  • linha 7: o serviço de apoio telefónico foi lançado
  • Linha 9: verifica-se que o serviço está agora ativo

Linha 21: os clientes são atendidos por threads de serviço que executam o seguinte método doService:


private static void doService(Object infos) {
            // recuperar o cliente a ser atendido
            Client client = infos as Client;
            // presta o serviço ao cliente
            Console.WriteLine("Début du service au client {0}", client.NumClient);
            // exploração da ligação TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // envio de uma mensagem de boas-vindas ao cliente
                                writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
                                // ciclo de leitura de pedido/gravação de resposta
                                string demande = null;
                                bool serviceFini = false;
                                while (!serviceFini && (demande = reader.ReadLine()) != null) {
                                    // monitorização da consola
                                    if (echo) {
                                        Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    }
                                    // análise da solicitação
                                    demande = demande.Trim().ToLower();
                                    // pedido vazio?
                                    if (demande.Length == 0) {
                                        // pedido incorreto
                                        writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
                                        return;
                                    }

                                    // descomposição da solicitação em campos
                                    string[] champs = Regex.Split(demande, @"\s+");
                                    // análise
                                    switch (champs[0].ToLower()) {
                                        case "aide":
                                            writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
                                            break;
                                        case "impot":
                                            // cálculo do imposto
                                            writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
                                            break;
                                        case "aurevoir":
                                            serviceFini = true;
                                            writeClient(writer, client.NumClient, "Au revoir...");
                                            break;
                                        default:
                                            writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
                                            break;
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // erro
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }

        private static void writeClient(StreamWriter writer, int numClient, string message) {
            // saída na consola?
            if (echo) {
                Console.WriteLine("---> Client {0} : {1}", numClient, message);
            }
            // envio de mensagem ao cliente
            writer.WriteLine(message);
}

Mais uma vez, temos aqui um código semelhante ao do servidor de eco analisado no parágrafo 11.6.1. Apenas comentamos o que é diferente:

  • linha 15: assim que o cliente se liga, o servidor envia-lhe uma mensagem de boas-vindas.
  • linhas 19-52: o ciclo de leitura dos comandos do cliente. O ciclo termina quando o cliente envia o comando «aurevoir».
  • linha 27: caso de um comando vazio
  • linha 34: o pedido é dividido em campos para ser analisado
  • linha 37: comando aide: o cliente solicita a lista de comandos autorizados
  • linha 40: comando impot: o cliente solicita um cálculo de imposto. Responde-se com a mensagem devolvida pelo método calculImpot, que iremos detalhar em breve.
  • linha 44: comando aurevoir: o cliente indica que terminou.
  • linha 45: preparamo-nos para sair do ciclo de leitura dos pedidos do cliente (linhas 19-52)
  • linha 46: responde-se ao cliente com uma mensagem de despedida
  • linha 48: um comando incorreto. Enviamos ao cliente uma mensagem de erro.

O processamento do comando impot é assegurado pelo seguinte método calculImpot:


private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
            // pergunta se é casado (S/N) nbEnfants salaireAnnuel
            // são necessários 4 campos
            if (champs.Length != 4) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campos [1]
            string marié = champs[1];
            if (marié != "o" && marié != "n") {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campos [2]
            int nbEnfants;
            if (!int.TryParse(champs[2], out nbEnfants)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campos [3]
            int salaireAnnuel;
            if (!int.TryParse(champs[3], out salaireAnnuel)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // Está tudo bem — vamos calcular o imposto
            int impot = 0;
            try {
                impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
                return impot.ToString();
            } catch (Exception ex) {
                return ex.Message;
            }
        }
  • linha 1: o método recebe como terceiro parâmetro a matriz de campos do pedido impot. Se este tiver sido formulado corretamente, tem o formato «impost marié nbEnfants salaireAnnuel». O método devolve como resultado a resposta a enviar ao cliente.
  • linha 4: verifica-se se o comando tem 4 campos
  • linha 8: verifica-se se o campo marié é válido
  • linha 14: verifica-se se o campo nbEnfants é válido
  • linha 19: verifica-se se o campo salaireAnnuel é válido
  • linha 25: o imposto é calculado utilizando o método CalculerImpot da camada [metier]. Recorde-se que esta camada está encapsulada numa DLL.
  • linha 26: se a camada [metier] tiver devolvido um resultado, este é devolvido ao cliente.
  • linha 28: se a camada [metier] tiver lançado uma exceção, a mensagem da mesma é devolvida ao cliente.

11.9.4. O cliente gráfico do servidor TCP de cálculo de impostos

11.9.4.1. O projeto « » no Visual Studio

O projeto do Visual Studio do cliente gráfico será o seguinte:

  • em [1], os dois projetos da solução, um para cada uma das duas camadas da aplicação
  • em [2], o cliente TCP que desempenha o papel de camada [metier] para a camada [ui]. Por isso, utilizaremos os dois termos.
  • em [3], a camada [ui] da versão 7, com uma pequena diferença de que falaremos

11.9.4.2. A camada [metier]

A interface IImpotMetier não sofreu alterações. Continua a ser a da versão 7:


namespace Metier {
    public interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}

A implementação desta interface é a seguinte classe [ImpotMetierTcp]:


using System.Net.Sockets;
using System.IO;
namespace Metier {
    public class ImpotMetierTcp : IImpotMetier {

        // informações [serveur]
        private string Serveur { get; set; }
        private int Port { get; set; }

        // cálculo do imposto
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
                // ligamo-nos ao serviço
                using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // fluxo de saída não armazenado em buffer
                                writer.AutoFlush = true;
                                // ignorar a mensagem de boas-vindas
                                reader.ReadLine();
                                // pedido
                                writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
                                // resposta
                                return int.Parse(reader.ReadLine());
                            }
                        }
                    }
                }
            }
        }
    }
  • linha 7: o nome ou o endereço IP do servidor TCP de cálculo de impostos
  • linha 8: a porta de escuta desse servidor
  • estas duas propriedades serão inicializadas pelo Spring aquando da instanciação da classe [ImpotMetierTcp].
  • linha 11: o método de cálculo do imposto. Quando este é executado, as propriedades Serveur e Port já se encontram inicializadas. No código, observa-se o procedimento clássico de um cliente TCP
  • linha 13: a ligação com o servidor é estabelecida
  • linhas 14-16: recupera-se (linha 14) o fluxo de rede associado a esta ligação, do qual se extrai um fluxo de leitura (linha 15) e um fluxo de escrita (linha 16).
  • linha 18: o fluxo de escrita deve ser não-bufferizado
  • linha 20: aqui, é importante lembrar que, ao abrir a ligação, o servidor envia ao cliente uma primeira linha que é a mensagem de boas-vindas «Bem-vindo ao servidor de cálculo de impostos». Esta mensagem é lida e ignorada.
  • linha 22: envia-se ao servidor o comando do tipo: imposto o 2 60000 para lhe pedir que calcule o imposto de uma pessoa casada com dois filhos e um salário anual de 60 000 euros.
  • linha 24: o servidor responde com o valor do imposto na forma «4282» ou com uma mensagem de erro, caso o comando esteja mal formulado (o que não acontecerá neste caso) ou se tiver ocorrido algum problema no cálculo do imposto. Neste caso, esta última situação não está prevista, mas teria sido certamente mais «limpo» tratá-la. Com efeito, se a linha lida for uma mensagem de erro, será lançada uma exceção porque a conversão para um inteiro irá falhar. A exceção captada pela interface gráfica será um erro de conversão, quando a exceção original é de natureza totalmente diferente. O leitor é convidado a melhorar este código.
  • linhas 25-28: libertação de todos os recursos utilizados com uma cláusula «using».

A camada [metier] é compilada na DLL ImpotsV8-metier.dll:

Image

11.9.4.3. A camada [ui]

A camada [ui] [1,3] é a que foi analisada na versão 7, no parágrafo 9.8.4, com exceção de três pormenores:

  • a configuração da camada [metier] na [App.config] é diferente, uma vez que a sua implementação sofreu alterações
  • a interface gráfica [Form1.cs] foi alterada para apresentar uma eventual exceção
  • a camada [metier] está incluída na DLL [ImpotsV8-metier.dll].

O ficheiro [App.config] é o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
                <property name="Serveur" value="localhost"/>
                <property name="Port" value="27"/>
            </object>
        </objects>
    </spring>
</configuration>
  • linha 16: instanciação da camada [metier] com a classe Metier.ImpotMetierTcp da DLL ImpotsV8-metier.dll
  • linhas 17-18: as propriedades Servidor e Porta da classe Metier.ImpotMetierTcp são inicializadas. O servidor estará na máquina localhost e funcionará na porta 27.

A interface gráfica apresentada ao utilizador é a seguinte:

  • em [1], foi adicionado um TextBox para apresentar uma eventual exceção. Este campo não existia na versão anterior.

Para além deste pormenor, o código do formulário é o já analisado no parágrafo 6.4.3. O leitor é convidado a consultar esse parágrafo. Em [2], vemos um exemplo de execução obtido com um servidor iniciado da seguinte forma:

1
2
3
4
5
6
7
8
9
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt lancé sur le port 27
Serveur de calcul d'impôt >echo start
Serveur de calcul d'impôt >
...
Début du service au client 9
<--- Client 9 : impot o 2 60000
---> Client 9 : 4282
Fin du service au client 9

A captura de ecrã [2] do cliente corresponde às linhas do cliente 9 acima.

11.9.5. Conclusão

Mais uma vez, conseguimos reutilizar código existente, sem alterações (camadas [metier] e [dao] do servidor) ou com muito poucas alterações (camada [ui] do cliente). Isto foi possível graças à nossa utilização sistemática de interfaces e à sua instanciação com o Spring. Se, na versão 7, tivéssemos colocado o código de negócio diretamente nos gestores de eventos da interface gráfica, esse código de negócio não teria sido reutilizável. Esta é a principal desvantagem das arquiteturas de 1 camada.

Por fim, note-se que a camada [ui] não tem qualquer conhecimento de que é um servidor remoto que calcula o montante do imposto.