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 chamados de conjunto de protocolos TCP/IP (Transfer Control Protocol / Internet Protocol), em referência aos dois principais protocolos. 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 aplicativos distribuídos. O texto a seguir é uma tradução parcial de um texto encontrado no documento “Lan Workplace for Dos - Administrator's Guide” da NOVELL, documento do início dos anos 90.
O conceito geral de criar uma rede de computadores heterogêneos tem origem em pesquisas realizadas pela DARPA (Agência de Projetos de Pesquisa Avançada de Defesa) nos Estados Unidos. A DARPA desenvolveu o conjunto de protocolos conhecido como TCP/IP, que permite que máquinas heterogêneas se comuniquem entre si. Esses protocolos foram testados em uma rede chamada ARPAnet, que posteriormente se tornou a rede INTERNET. Os protocolos TCP/IP definem formatos e regras de transmissão e recepção independentes da organização das redes e dos equipamentos utilizados.
A rede projetada pelo DARPA e gerenciada pelos protocolos TCP/IP é uma rede de comutação de pacotes. Essa rede transmite as informações pela rede em pequenos pedaços chamados pacotes. Assim, se um computador transmitir um arquivo grande, este será dividido em pequenos pedaços que serão enviados pela rede para serem recompostos no destino. O TCP/IP define o formato desses pacotes, a saber:
- origem do pacote
- destino
- comprimento
- tipo
11.1.2. O modelo OSI
Os protocolos TCP/IP seguem, em linhas gerais, o modelo de rede aberta denominado OSI (Modelo de Referência para Interconexão de Sistemas Abertos), definido pela ISO (Organização Internacional de Padronização). Esse modelo descreve uma rede ideal na qual a comunicação entre máquinas pode ser representada por um modelo de sete camadas:
![]() |
Cada camada recebe serviços da camada inferior e oferece os seus próprios serviços à camada superior. Suponhamos que duas aplicações localizadas em máquinas A e B diferentes desejam se comunicar: elas o fazem no nível da camada Application. Elas não precisam conhecer todos os detalhes do funcionamento da rede: cada aplicação entrega a informação que deseja transmitir à camada inferior: a camada Présentation. A aplicação precisa, portanto, conhecer apenas as regras de interface com a camada Présentation.
Uma vez que a informação está na camada Présentation, ela é encaminhada, de acordo com outras regras, para a camada Session e assim por diante, até que a informação chegue ao suporte físico e seja transmitida fisicamente para a máquina de destino. Lá, ela passará pelo processo inverso ao que ocorreu na máquina remetente.
Em cada camada, o processo remetente encarregado de enviar a informação a transmite a um processo receptor na outra máquina, pertencente à mesma camada que ele. Isso ocorre de acordo com certas regras, conhecidas como protocolo da camada. Assim, temos o seguinte esquema final de comunicação:
![]() |
A função das diferentes camadas é a seguinte:
Assegura a transmissão de bits por meio de um suporte físico. Nesta camada, encontram-se equipamentos terminais de processamento de dados (E.T.T.D.), tais como terminais ou computadores, bem como equipamentos de terminação de circuitos de dados (E.T.C.D.), tais como moduladores/demoduladores, multiplexadores e concentradores. Os pontos de interesse neste nível são:
| |
Oculta as particularidades físicas da camada Física. Detecta e corrige erros de transmissão. | |
Gerencia o caminho que as informações enviadas pela rede devem seguir. Isso é chamado de routage: determinar a rota que uma informação deve seguir para chegar ao seu destinatário. | |
Permite a comunicação entre duas aplicações, enquanto as camadas anteriores permitiam apenas a comunicação entre máquinas. Um serviço fornecido por essa camada pode ser o multiplexamento: a camada de transporte poderá utilizar uma mesma conexão de rede (de máquina para máquina) para transmitir informações pertencentes a várias aplicações. | |
Nesta camada, encontramos serviços que permitem que uma aplicação abra e mantenha uma sessão de trabalho em uma máquina remota. | |
Ela visa uniformizar a representação dos dados nas diferentes máquinas. Assim, os dados provenientes de uma máquina A serão “formatados” pela camada Présentation da máquina A, de acordo com um formato padrão, antes de serem enviados pela rede. Ao chegarem à camada Présentation da máquina destinatária B, que as reconhecerá graças ao seu formato padrão, elas serão formatadas de outra maneira para que o aplicativo da máquina B as reconheça. | |
Nesse nível, encontram-se as aplicações geralmente próximas ao usuário, como o e-mail ou a transferência de arquivos. |
11.1.3. O modelo TCP/IP
O modelo OSI é um modelo ideal que ainda não foi concretizado. O conjunto de protocolos TCP/IP se aproxima dele da seguinte forma:
![]() |
Camada Física
Em redes locais, geralmente se utiliza a tecnologia Ethernet ou Token-Ring. Aqui, apresentamos apenas a tecnologia Ethernet.
Ethernet
Esse é o nome dado a uma tecnologia de redes locais com comutação de pacotes inventada pela Xerox no início da década de 1970 e padronizada pela Xerox, Intel e Digital Equipment em 1978. A rede é constituída fisicamente por um cabo coaxial com cerca de 1,27 cm de diâmetro e comprimento máximo de 500 m. Ela pode ser estendida por meio de répéteurs, sendo que duas máquinas não podem estar separadas por mais de dois repetidores. O cabo é passivo: todos os elementos ativos estão nos equipamentos conectados ao cabo. Cada equipamento é conectado ao cabo por meio de uma placa de acesso à rede que inclui:
- um transmissor (transceiver) que detecta a presença de sinais no cabo e converte os sinais analógicos em sinais digitais e vice-versa.
- um acoplador que recebe os sinais digitais do transmissor e os transmite ao computador para processamento, ou vice-versa.
As principais características da tecnologia Ethernet são as seguintes:
- Capacidade de 10 megabits por segundo.
- Topologia em barramento: todas as máquinas estão conectadas ao mesmo cabo
![]() |
- Rede de difusão — Um equipamento que transmite envia informações pelo cabo com o endereço do equipamento destinatário. Todos os equipamentos conectados recebem essas informações, e apenas aquele a quem elas se destinam as retém.
- O método de acesso é o seguinte: o transmissor que deseja transmitir escuta o cabo — ele detecta, então, a presença ou ausência de uma onda portadora, cuja presença significaria que uma transmissão está em andamento. Essa é a técnica CSMA (Carrier Sense Multiple Access). Na ausência de portadora, um transmissor pode decidir transmitir por sua vez. Vários transmissores podem tomar essa decisão. Os sinais transmitidos se misturam: diz-se que ocorre uma colisão. O transmissor detecta essa situação: ao mesmo tempo em que transmite pelo cabo, ele escuta o que realmente está passando por ele. Se detectar que a informação que transita pelo cabo não é a que ele transmitiu, ele deduz que houve colisão e interrompe a transmissão. Os outros transmissores que estavam transmitindo farão o mesmo. Cada um retomará sua transmissão após um intervalo aleatório, que depende de cada transmissor. Essa técnica é chamada de CD (Detecção de Colisão). O método de acesso é, portanto, denominado CSMA/CD.
- Um endereçamento de 48 bits. Cada máquina possui um endereço, denominado aqui de endereço físico, que está gravado na placa que a conecta ao cabo. Esse endereço é chamado de endereço Ethernet da máquina.
Camada de Rede
Nesta camada, encontramos os protocolos IP, ICMP, ARP e RARP.
Transmite pacotes entre dois nós da rede | |
O ICMP estabelece a comunicação entre o programa do protocolo IP de uma máquina e o de outra máquina. Trata-se, portanto, de um protocolo de troca de mensagens dentro do próprio protocolo IP. | |
faz a correspondência entre o endereço de Internet da máquina e o endereço físico da máquina | |
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:
Garante a entrega confiável de informações entre dois clientes | |
Garante a entrega não confiável de informações entre dois clientes |
Camadas de Aplicação/Apresentação/Sessão
Encontram-se aqui diversos protocolos:
Emulador de terminal que permite que uma máquina A se conecte a uma máquina B como um terminal | |
permite a transferência de arquivos | |
permite a transferência de arquivos | |
permite a troca de mensagens entre usuários da rede | |
converte um nome de máquina no endereço de Internet da máquina | |
criado pela Sun MicroSystems, ele especifica uma representação padrão dos dados, independente das máquinas | |
também definido pela Sun, é um protocolo de comunicação entre aplicativos remotos, independente da camada de transporte. Esse protocolo é importante: isenta o programador de conhecer os detalhes da camada de transporte e torna os aplicativos portáteis. Esse protocolo se baseia no protocolo XDR | |
também definido pela Sun; esse protocolo permite que uma máquina “veja” o sistema de arquivos de outra máquina. Ele se baseia no protocolo RPC mencionado anteriormente |
11.1.4. Funcionamento dos protocolos da Internet
Os aplicativos desenvolvidos no ambiente TCP/IP geralmente utilizam vários dos protocolos desse ambiente. Um programa aplicativo se comunica com a camada mais alta dos protocolos. Essa camada repassa a informação para a camada inferior e assim por diante, até chegar ao suporte físico. Lá, a informação é fisicamente transferida para a máquina destinatária, onde atravessará as mesmas camadas, desta vez na direção oposta, até chegar ao aplicativo destinatário das informações enviadas. O esquema a seguir mostra o percurso da informação:
![]() |
Vejamos um exemplo: a aplicação FTP, definida na camada Application e que permite a transferência de arquivos entre máquinas.
- A aplicação fornece uma sequência de bytes a ser transmitida à camada transport.
- A camada transport divide essa sequência de bytes em segments e TCP, e acrescenta, no início de cada segmento, o número do próprio segmento. Os segmentos são encaminhados para a camada de rede, regida pelo protocolo IP.
- A camada IP cria um pacote que encapsula o segmento TCP recebido. No cabeçalho desse pacote, ela insere os endereços de Internet das máquinas de origem e de destino. Ela também determina o endereço físico da máquina destinatária. Tudo isso é encaminhado para a camada de enlace de dados e enlace físico, ou seja, para a placa de rede que conecta a máquina à rede física.
- Lá, o pacote IP é, por sua vez, encapsulado em uma trama física e enviado ao seu destinatário pelo cabo.
- Na máquina destinatária, a camada de enlace de dados e enlace físico faz o inverso: ela desencapsula o pacote IP da trama física e o repassa para a camada IP.
- A camada IP verifica se o pacote está correto: ela calcula uma soma com base nos bits recebidos (checksum), soma essa que deve ser encontrada no cabeçalho do pacote. Caso contrário, o pacote é rejeitado.
- Se o pacote for considerado válido, a camada IP desencapsula o segmento TCP contido nele e o encaminha para a camada superior, a transport.
- A camada transport — camada TCP em nosso exemplo — examina o número do segmento para restabelecer a ordem correta dos segmentos.
- Ela também calcula uma soma de verificação para o segmento TCP. Se for considerada correta, a camada TCP envia um aviso de recebimento para a máquina de origem; caso contrário, o segmento TCP é rejeitado.
- Resta à camada TCP apenas transmitir a parte de dados do segmento ao aplicativo destinatário desses dados na camada superior.
11.1.5. Problemas de endereçamento na Internet
Um noeud de uma rede pode ser um computador, uma impressora inteligente, um servidor de arquivos — na verdade, qualquer coisa capaz de se comunicar por meio dos protocolos TCP/IP. Cada nó possui um endereço físico cujo formato depende do tipo de rede. Em uma rede Ethernet, o endereço físico é codificado em 6 bytes. Um endereço de uma rede X25 é um número de 14 dígitos.
O endereço de Internet de um nó é um endereço lógico: ele é independente do hardware e da rede utilizada. Trata-se de um endereço de 4 bytes que identifica tanto uma rede local quanto um nó dessa rede. O endereço de Internet é normalmente representado na forma de 4 números, correspondentes aos valores dos 4 bytes, separados por um ponto. Assim, o endereço do computador Lagaffe da Faculdade de Ciências de Angers é 193.49.144.1 e o do computador Liny é 193.49.144.9. Deduz-se, portanto, que o endereço de Internet da rede local é 193.49.144.0. É possível ter até 254 nós nessa rede.
Como os endereços de Internet ou endereços IP são independentes da rede, um computador de uma rede A pode se comunicar com um computador de uma rede B sem se preocupar com o tipo de rede em que está: basta que ele conheça seu endereço IP. O protocolo IP de cada rede é responsável por fazer a conversão entre endereço IP e endereço físico, nos dois sentidos.
Todos os endereços IP devem ser diferentes entre si. Na França, é o INRIA que se encarrega de atribuir os endereços IP. Na verdade, esse órgão atribui um endereço para sua rede local, por exemplo, 193.49.144.0 para a rede da Faculdade de Ciências de Angers. O administrador dessa rede pode, então, atribuir os endereços IP 193.49.144.1 a 193.49.144.254 como entender. Esse endereço geralmente é registrado em um arquivo específico de cada máquina conectada à rede.
11.1.5.1. As classes de endereços IP
Um endereço IP é uma sequência de 4 bytes, frequentemente indicada como I1.I2.I3.I4, que contém, na verdade, dois endereços:
- o endereço da rede
- o endereço de um nó dessa rede
De acordo com o tamanho desses dois campos, os endereços IP são divididos em três classes: classes A, B e C.
Classe A
O endereço IP: I1.I2.I3.I4 tem o formato R1.N1.N2.N3, onde
R1 é o endereço da rede
N1.N2.N3 é o endereço de um computador nessa rede
Mais precisamente, o formato de um endereço IP da classe A é o seguinte:
O endereço de rede ocupa 7 bits e o endereço do nó, 24 bits. Portanto, é possível ter 127 redes de classe A, cada uma com até 2²⁴ nós.
Classe B
Aqui, o endereço IP: I1.I2.I3.I4 tem o formato R1.R2.N1.N2, onde
R1.R2 é o endereço da rede
N1.N2 é o endereço de um computador nessa rede
Mais precisamente, o formato de um endereço IP da classe B é o seguinte:
O endereço da rede ocupa 2 bytes (14 bits, exatamente), assim como o do nó. Portanto, é possível ter 2¹⁴ redes de classe B, cada uma com até 2¹⁶ nós.
Classe C
Nesta classe, o endereço IP: I1.I2.I3.I4 tem o formato R1.R2.R3.N1, onde
R1.R2.R3 é o endereço da rede
N1 é o endereço de uma máquina nessa rede
Mais precisamente, o formato de um endereço IP da classe C é o seguinte:
![]() |
O endereço de rede ocupa 3 bytes (menos 3 bits) e o endereço do nó, 1 byte. Assim, é possível ter 221 redes de classe C com até 256 nós.
Como o endereço do computador Lagaffe da Faculdade de Ciências de Angers é 193.49.144.1, vemos que o byte de peso forte vale 193, ou seja, em binário 11000001. Deduz-se, portanto, que a rede é da classe C.
Endereços reservados
- Certos endereços, como IP, são endereços de rede, e não endereços de nós na rede. São aquelas em que o endereço do nó é definido como 0. Assim, o endereço 193.49.144.0 é o endereço IP da rede da Faculdade de Ciências de Angers. Consequentemente, nenhum nó de uma rede pode ter o endereço zero.
- Quando, em um endereço IP, o endereço do nó contém apenas 1s, temos então um endereço de difusão: esse endereço designa todos os nós da rede.
- Em uma rede da classe C, que teoricamente permite 2⁸ = 256 nós, se retirarmos os dois endereços proibidos, restam apenas 254 endereços permitidos.
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, essas informações, ao atravessarem a camada IP, eram encapsuladas em pacotes. Esses pacotes têm o seguinte formato:
![]() |
O pacote IP contém, portanto, os endereços de Internet das máquinas de origem e de destino. Quando esse pacote for transmitido à camada responsável por enviá-lo para a rede física, outras informações são adicionadas a ele para formar a trama física que será finalmente enviada para a rede. Por exemplo, o formato de uma trama em uma rede Ethernet é o seguinte:
![]() |
Na trama final, constam os endereços físicos dos computadores de origem e de destino. Como eles são obtidos?
O equipamento remetente, que conhece o endereço IP do equipamento com o qual deseja se comunicar, obtém o endereço físico deste último utilizando um protocolo específico chamado ARP (Address Resolution Protocol).
- Ela envia um pacote de um tipo especial chamado pacote ARP, contendo o endereço IP da máquina cujo endereço físico está sendo procurado. Ela também se certificou de incluir nele seu próprio endereço IP, bem como seu endereço físico.
- Esse pacote é enviado a todos os nós da rede.
- Esses nós reconhecem a natureza especial do pacote. O nó que reconhece seu endereço IP no pacote responde enviando ao remetente do pacote seu endereço físico. Como ele consegue fazer isso? Ele encontrou no pacote os endereços IP e o endereço físico do remetente.
- O remetente recebe, assim, o endereço físico que procurava. Ele o armazena na memória para poder usá-lo posteriormente, caso outros pacotes precisem ser enviados ao mesmo destinatário.
O endereço IP de uma máquina normalmente está registrado em um de seus arquivos, que ela pode consultar para obtê-lo. Esse endereço pode ser alterado: basta editar o arquivo. Já o endereço físico está registrado na memória da placa de rede e não pode ser alterado.
Quando um administrador deseja organizar sua rede de maneira diferente, ele pode precisar alterar os endereços IP de todos os nós e, portanto, editar os diversos arquivos de configuração de cada um deles. Isso pode ser trabalhoso e uma fonte de erros se houver muitas máquinas. Um método consiste em não atribuir um endereço IP às máquinas: nesse caso, insere-se um código especial no arquivo no qual a máquina deveria encontrar seu endereço IP. Ao perceber que não possui um endereço IP, a máquina o solicita por meio de um protocolo chamado RARP (Reverse Address Resolution Protocol). Em seguida, ela envia para a rede um pacote especial chamado pacote RARP, análogo ao pacote ARP anterior, no qual insere seu endereço físico. Esse pacote é enviado a todos os nós, que então reconhecem um pacote RARP. Um deles, chamado servidor RARP, possui um arquivo que fornece a correspondência entre endereço físico e endereço IP de todos os nós. Ele então responde ao remetente do pacote RARP, reenviando-lhe seu endereço IP. Um administrador que deseje reconfigurar sua rede precisa, portanto, apenas editar o arquivo de correspondências do servidor RARP. Este, normalmente, deve ter um endereço fixo IP, que deve ser capaz de identificar sem precisar utilizar ele próprio o protocolo RARP.
11.1.6. A camada de rede conhecida como camada IP da Internet
O protocolo IP (Protocolo de Internet) define o formato que os pacotes devem assumir e a maneira como devem ser gerenciados durante sua transmissão ou recepção. Esse tipo específico de pacote é chamado de datagrama IP. Já o apresentamos anteriormente:
![]() |
O importante é que, além dos dados a serem transmitidos, o datagrama IP contém os endereços de Internet dos computadores de origem e de destino. Assim, o computador de destino sabe quem está enviando uma mensagem para ele.
Ao contrário de um quadro de rede, cujo comprimento é determinado pelas características físicas da rede pela qual transita, o comprimento do datagrama IP é definido pelo software e, portanto, será o mesmo em diferentes redes físicas. Vimos que, ao descer da camada de rede para a camada física, o datagrama IP foi encapsulado em um quadro físico. Demos o exemplo do quadro físico de uma rede Ethernet:
As tramas físicas circulam de nó em nó até seu destino, que pode não estar na mesma rede física que a máquina remetente. O pacote IP pode, portanto, ser encapsulado sucessivamente em diferentes quadros físicos nos nós que fazem a conexão entre duas redes de tipos diferentes. Também é possível que o pacote IP seja grande demais para ser encapsulado em um quadro físico. O software IP do nó onde esse problema ocorre divide, então, o pacote IP em fragments de acordo com regras precisas, sendo cada um deles enviado posteriormente pela rede física. Eles só serão remontados em seu destino final.
11.1.6.1. O roteamento
O roteamento é o método de encaminhamento dos pacotes IP até seu destino. Existem dois métodos: o roteamento direto e o roteamento indireto.
Roteamento direto
O roteamento direto refere-se ao encaminhamento de um pacote IP diretamente do remetente ao destinatário dentro da mesma rede:
- A máquina remetente de um datagrama IP possui o endereço IP do destinatário.
- Ela obtém o endereço físico do destinatário por meio do protocolo ARP ou em suas tabelas, caso esse endereço já tenha sido obtido.
- Ela envia o pacote pela rede para esse endereço físico.
Roteamento indireto
O roteamento indireto refere-se ao encaminhamento de um pacote IP para um destino localizado em uma rede diferente daquela à qual pertence o remetente. Nesse caso, as partes de endereço de rede dos endereços IP das máquinas de origem e de destino são diferentes. A máquina de origem reconhece esse fato. Ela então envia o pacote para um nó especial chamado roteador (router), nó que conecta uma rede local a outras redes e cujo endereço IP ela encontra em suas tabelas — endereço obtido inicialmente em um arquivo, em uma memória permanente ou ainda por meio de informações que circulam na rede.
Um roteador está conectado a duas redes e possui um endereço IP dentro dessas duas redes.
![]() |
No nosso exemplo acima:
. A rede nº 1 tem o endereço de Internet 193.49.144.0 e a rede nº 2, o endereço 193.49.145.0.
. Dentro da rede nº 1, o roteador tem o endereço 193.49.144.6 e, dentro da rede nº 2, o endereço 193.49.145.3.
A função do roteador é converter o pacote IP que recebe — e que está contido em uma trama física típica da rede nº 1 — em uma trama física capaz de circular na rede nº 2. Se o endereço IP do destinatário do pacote estiver na rede nº 2, o roteador enviará o pacote diretamente a ele; caso contrário, enviá-lo-á a outro roteador, conectando a rede nº 2 à rede nº 3 e assim por diante.
11.1.6.2. Mensagens de erro e de controle
Ainda na camada de rede, portanto no mesmo nível do protocolo IP, existe o protocolo ICMP (Internet Control Message Protocol). Ele serve para enviar mensagens sobre o funcionamento interno da rede: nós com falha, congestionamento em um roteador, etc... As mensagens ICMP são encapsuladas em pacotes IP e enviadas pela rede. As camadas IP dos diferentes nós tomam as medidas adequadas de acordo com as mensagens ICMP que recebem. Assim, um aplicativo, por si só, nunca percebe esses problemas específicos da rede.
Um nó utilizará as informações ICMP para atualizar suas tabelas de roteamento.
11.1.7. A camada de transporte: os protocolos UDP e TCP
11.1.7.1. O protocolo UDP: Protocolo de Datagrama do Usuário
O protocolo UDP permite uma troca não confiável de dados entre dois pontos, ou seja, o encaminhamento correto de um pacote até seu destino não é garantido. O aplicativo, se desejar, pode gerenciar isso por conta própria, aguardando, por exemplo, após o envio de uma mensagem, um aviso de recebimento, antes de enviar a seguinte.
Por enquanto, no nível da rede, falamos de endereços IP de máquinas. No entanto, em uma máquina, podem coexistir simultaneamente diferentes processos, todos capazes de se comunicar. Portanto, ao enviar uma mensagem, é preciso indicar não apenas o endereço IP da máquina destinatária, mas também o “nome” do processo destinatário. Esse nome é, na verdade, um número, chamado número de porta. Alguns números são reservados para aplicativos padrão: a porta 69 para o aplicativo tftp (trivial file transfer protocol), por exemplo.
Os pacotes gerenciados pelo protocolo UDP também são chamados de datagramas. Eles têm o seguinte formato:
Esses datagramas serão encapsulados em pacotes IP e, em seguida, em quadros físicos.
11.1.7.2. O protocolo TCP: Protocolo de Controle de Transferência
Para comunicações seguras, o protocolo UDP é insuficiente: o desenvolvedor de aplicativos deve criar ele mesmo um protocolo que lhe permita detectar o encaminhamento correto dos pacotes. O protocolo TCP (Protocolo de Controle de Transferência) evita esses problemas. Suas características são as seguintes:
- O processo que deseja transmitir estabelece, em primeiro lugar, uma conexão com o processo destinatário das informações que irá transmitir. Essa conexão é estabelecida entre uma porta da máquina transmissora e uma porta da máquina receptora. Entre as duas portas, é criado um caminho virtual que será reservado exclusivamente aos dois processos que estabeleceram a conexão.
- Todos os pacotes enviados pelo processo de origem seguem esse caminho virtual e chegam na ordem em que foram enviados, o que não era garantido no protocolo UDP, já que os pacotes podiam seguir caminhos diferentes.
- A informação transmitida tem um caráter contínuo. O processo emissor envia informações em seu próprio ritmo. Essas informações não são necessariamente enviadas imediatamente: o protocolo TCP aguarda até ter quantidade suficiente para enviá-las. Elas são armazenadas em uma estrutura chamada segmento TCP. Uma vez preenchido, esse segmento será transmitido para a camada IP, onde será encapsulado em um pacote IP.
- Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP destinatário verifica se está recebendo os segmentos na sequência correta. Para cada segmento recebido corretamente, ele envia um aviso de recebimento ao remetente.
- Quando este último o recebe, ele informa o processo emissor. Assim, este pode saber que um segmento chegou ao destino, o que não era possível com o protocolo UDP.
- Se, após um certo tempo, o protocolo TCP que enviou um segmento não receber uma confirmação de recebimento, ele retransmite o segmento em questão, garantindo assim a qualidade do serviço de encaminhamento da informação.
- O circuito virtual estabelecido entre os dois processos que se comunicam é full-duplex: isso significa que a informação pode transitar nos dois sentidos. Assim, o processo de destino pode enviar confirmações de recebimento mesmo enquanto o processo de origem continua enviando informações. Isso permite, por exemplo, que o protocolo de origem TCP envie vários segmentos sem esperar pelo aviso de recebimento. Se, após um certo tempo, ele perceber que não recebeu o aviso de recebimento de um determinado segmento nº n, ele retomará a transmissão dos segmentos a partir desse ponto.
11.1.8. A camada de Aplicações
Acima dos protocolos UDP e TCP, existem diversos protocolos padrão:
TELNET
Esse protocolo permite que um usuário de uma máquina A da rede se conecte a uma máquina B (frequentemente chamada de máquina host). O TELNET emula na máquina A um terminal denominado universal. O usuário, portanto, age como se tivesse um terminal conectado à máquina B. O Telnet utiliza o protocolo TCP.
FTP: (Protocolo de Transferência de Arquivos)
Este protocolo permite a troca de arquivos entre duas máquinas remotas, bem como operações com arquivos, como a criação de diretórios, por exemplo. Ele se baseia no protocolo TCP.
TFTP: (Controle Trivial de Transferência de Arquivos)
Este protocolo é uma variante do FTP. Ele se baseia no protocolo UDP e é menos sofisticado que o FTP.
DNS: (Sistema de Nomes de Domínio)
Quando um usuário deseja trocar arquivos com uma máquina remota, por meio do FTP, por exemplo, ele precisa saber o endereço da Internet dessa máquina. Por exemplo, para realizar uma conexão FTP na máquina Lagaffe da Universidade de Angers, seria necessário iniciar o FTP da seguinte forma: FTP 193.49.144.1
Isso exige que haja um diretório que faça a correspondência entre máquina <--> endereço IP. Provavelmente, nesse diretório, as máquinas seriam designadas por nomes simbólicos, tais como:
máquina DPX2/320 da Universidade de Angers
máquina Sun da Universidade de Angers com o endereço ISERPA
É evidente que seria mais prático designar uma máquina por um nome, em vez de por seu endereço IP. Surge, então, o problema da exclusividade do nome: existem milhões de máquinas interconectadas. Seria possível imaginar que um órgão centralizado atribuísse os nomes. Isso seria, sem dúvida, bastante complicado. O controle dos nomes foi, na verdade, distribuído por domínios. Cada domínio é gerenciado por um órgão geralmente muito ágil, que tem total liberdade quanto à escolha dos nomes das máquinas. Assim, as máquinas na França pertencem ao domínio fr, gerenciado pelo Inria de Paris. Para simplificar ainda mais as coisas, o controle é distribuído novamente: domínios são criados dentro do domínio fr. Assim, a Universidade de Angers pertence ao domínio univ-Angers. O serviço responsável por esse domínio tem total liberdade para nomear os computadores da rede da Universidade de Angers. Por enquanto, esse domínio não foi subdividido. Mas, em uma grande universidade com muitos computadores em rede, isso poderia ocorrer.
A máquina DPX2/320 da Universidade de Angers foi nomeada Lagaffe, enquanto um PC e um 486DX50 foram nomeados liny. Como referenciar essas máquinas externamente? Especificando a hierarquia dos domínios aos quais elas pertencem. Assim, o nome completo da máquina Lagaffe será:
Lagaffe.univ-Angers.fr
Dentro dos domínios, é possível usar nomes relativos. Assim, dentro do domínio fr e fora do domínio univ-Angers, a máquina Lagaffe poderá ser referenciada por
Lagaffe.univ-Angers
Por fim, dentro do domínio univ-Angers, ela poderá ser referenciada simplesmente por
Lagaffe
Uma aplicação pode, portanto, referenciar uma máquina pelo seu nome. No fim das contas, é preciso, ainda assim, obter o endereço de Internet dessa máquina. Como isso é feito? Suponhamos que, a partir de uma máquina A, queiramos nos comunicar com uma máquina B.
- Se a máquina B pertencer ao mesmo domínio que a máquina A, provavelmente encontraremos seu endereço IP em um arquivo da máquina A.
- Caso contrário, a máquina A encontrará, em outro arquivo ou no mesmo arquivo anterior, uma lista de alguns servidores de nomes com seus endereços IP. Um servidor de nomes é responsável por estabelecer a correspondência entre o nome de uma máquina e seu endereço IP. A máquina A enviará uma solicitação especial ao primeiro servidor de nomes de sua lista, chamada solicitação DNS, incluindo, portanto, o nome da máquina procurada. Se o servidor consultado tiver esse nome em seus registros, ele enviará à máquina A o endereço IP correspondente. Caso contrário, o servidor também encontrará em seus arquivos uma lista de servidores de nomes que pode consultar. Ele fará isso então. Assim, vários servidores de nomes serão consultados, não de forma aleatória, mas de maneira a minimizar o número de consultas. Se a máquina for finalmente encontrada, a resposta será enviada de volta à máquina A.
XDR: (Representação de Dados eXternal)
Criado pela Sun MicroSystems, este protocolo especifica uma representação padrão dos dados, independente das máquinas.
RPC: (Chamada de Procedimento Remoto)
Também definido pela Sun, é um protocolo de comunicação entre aplicativos remotos, independente da camada de transporte. Esse protocolo é importante: ele dispensa o programador de conhecer os detalhes da camada de transporte e torna os aplicativos portáteis. Esse protocolo se baseia no protocolo XDR
NFS: Sistema de Arquivos em Rede
Também definido pela Sun, esse protocolo permite que uma máquina “veja” o sistema de arquivos de outra máquina. Ele se baseia no protocolo RPC mencionado anteriormente.
11.1.9. Conclusão
Apresentamos nesta introdução algumas linhas gerais dos protocolos da Internet. Para aprofundar esse assunto, recomenda-se a leitura do excelente livro de Douglas Comer:
Título TCP/IP: Arquitetura, Protocolos, Aplicações.
Autor Douglas COMER
Editora InterEditions
11.2. As classes .NET do gerenciamento 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 sequência do tipo “I1.I2.I3.I4”, em que In é um número entre 1 e 254. Esses são os endereços IP mais comuns atualmente.
- IPv6: codificado em 128 bits e representado por uma sequência do tipo “[I1.I2.I3.I4.I5.I6.I7.I8]”, em que In é uma sequência 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. Esse nome não é obrigatório, pois os aplicativos acabam sempre utilizando os endereços IP das máquinas. Eles existem para facilitar a vida dos usuários. Assim, é mais fácil, com um navegador, acessar o endereço http://www.ibm.com (URL) do que o endereço 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 conectado a várias redes ao mesmo tempo. Nesse caso, ele possui um endereço IP em cada rede.
Um endereço IP pode ser representado de duas maneiras em .NET:
- na forma de uma sequência 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:
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 | |
C | o endereço IP “0.0.0.0”. Quando um serviço está associado a esse endereço, isso significa que ele aceita clientes em todos os endereços IP da máquina na qual está em operação. | |
C | o endereço IP “127.0.0.1”. Chamado de “endereço de loop”. Quando um serviço está associado a esse endereço, isso significa que ele aceita apenas clientes que estão na mesma máquina que ele. | |
C | o endereço IP “255.255.255.255”. Quando um serviço está associado a esse endereço, isso significa que ele não aceita nenhum cliente. | |
M | tenta passar o endereço IP ipString na forma “I1.I2.I3.I4” como um objeto de endereço IPAddress. Retorna true se a operação for bem-sucedida. | |
M | retorna true se o endereço IP for “127.0.0.1” | |
M | converte o endereço IP para o formato “I1.I2.I3.I4” ou “[I1.I2.I3.I4.I5.I6.I7.I8]” |
A associação entre os endereços IP <--> nomMachine é garantida por um serviço distribuído da Internet chamado DNS (Sistema de Nomes de Domínio). Os métodos estáticos da classe Dns permitem estabelecer a associação entre os endereços IP <--> nomMachine:
retorna um endereço IPHostEntry a partir de um endereço IP na forma de uma string ou a partir de um nome de máquina. Lança uma exceção se a máquina não puder ser encontrada. | |
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 puder ser encontrada. | |
retorna o nome da máquina na qual está sendo executado o programa que está executando esta instrução | |
retorna os endereços IP da máquina identificada por seu nome ou por um de 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:
P | tabela de endereços IP da máquina | |
P | os aliases DNS da máquina. Esses são os nomes correspondentes aos diferentes endereços IP da máquina. | |
P | o nome de host principal da máquina |
Consideremos o seguinte programa, que exibe o nome da máquina na qual está sendo executado e, em seguida, de forma interativa, apresenta as correspondências entre endereço IP e nome da máquina:
using System;
using System.Net;
namespace Chap9 {
class Program {
static void Main(string[] args) {
// exibe o nome do computador local
// em seguida, fornece informações interativamente 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) {
// inserçã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 {
// busca por máquina
ipHostEntry = Dns.GetHostEntry(machine);
// nome da máquina
Console.WriteLine("Machine : " + ipHostEntry.HostName);
// os endereços da máquina IP
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:
11.3. Noções básicas de programação para a Internet
11.3.1. Noções gerais
Consideremos a comunicação entre duas máquinas remotas A e B:
![]() |
Quando um aplicativo AppA da máquina A deseja se comunicar com um aplicativo AppB da máquina B na Internet, ele precisa saber várias coisas:
- o endereço IP ou o nome da máquina B
- o número da porta com a qual a aplicação AppB opera. De fato, a máquina B pode suportar diversas aplicações que operam na Internet. Quando recebe informações provenientes da rede, ela precisa saber a qual aplicação essas informações se destinam. As aplicações da máquina B têm acesso à rede por meio de interfaces, também chamadas de portas de comunicação. Essa informação está contida no pacote recebido pela máquina B para que ele seja entregue à aplicação correta.
- Os protocolos de comunicação compreendidos pela máquina B. Em nosso estudo, utilizaremos apenas os protocolos TCP-IP.
- o protocolo de comunicação aceito pelo aplicativo AppB. De fato, as máquinas A e B vão “se comunicar”. O que elas vão transmitir será encapsulado nos protocolos TCP-IP. No entanto, quando, no final da cadeia, o aplicativo AppB receber a informação enviada pelo aplicativo AppA, ele precisará ser capaz de interpretá-la. Isso é análogo à situação em que duas pessoas, A e B, se comunicam por telefone: o diálogo delas é transmitido pelo telefone. A fala será codificada na forma de sinais pelo telefone A, transportada pelas linhas telefônicas, chegará ao telefone B para ser decodificada. A pessoa B então ouvirá as palavras. É aí que entra o conceito de protocolo de diálogo: se A fala francês e B não entende essa língua, A e B não poderão dialogar de forma eficaz.
Portanto, os dois aplicativos que se comunicam devem chegar a um acordo sobre o tipo de diálogo que irão adotar. Por exemplo, o diálogo com um serviço ftp não é o mesmo que com um serviço pop: esses dois serviços não aceitam os mesmos comandos. Eles possuem um protocolo de diálogo diferente.
11.3.2. As características do protocolo TCP
Aqui, estudaremos apenas as comunicações de rede que utilizam o protocolo de transporte TCP. Vale lembrar, neste contexto, as características desse protocolo:
- O processo que deseja transmitir estabelece, em primeiro lugar, uma conexão com o processo destinatário das informações que irá transmitir. Essa conexão é estabelecida entre uma porta da máquina transmissora e uma porta da máquina receptora. Entre as duas portas, é criado um caminho virtual, que será reservado exclusivamente aos dois processos que estabeleceram a conexão.
- Todos os pacotes enviados pelo processo de origem seguem esse caminho virtual e chegam na ordem em que foram enviados
- A informação transmitida tem um caráter contínuo. O processo emissor envia informações em seu próprio ritmo. Essas informações não são necessariamente enviadas imediatamente: o protocolo TCP aguarda até ter quantidade suficiente para enviá-las. Elas são armazenadas em uma estrutura chamada segmento TCP. Esse segmento, uma vez preenchido, será transmitido para a camada IP, onde será encapsulado em um pacote IP.
- Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP destinatário verifica se está recebendo os segmentos na sequência correta. Para cada segmento recebido corretamente, ele envia um aviso de recebimento ao remetente.
- Quando este último o recebe, ele informa o processo emissor. Assim, este pode saber que um segmento chegou ao destino.
- Se, após um certo tempo, o protocolo TCP, que emitiu um segmento, não receber uma confirmação de recebimento, ele retransmite o segmento em questão, garantindo assim a qualidade do serviço de encaminhamento da informação.
- O circuito virtual estabelecido entre os dois processos que se comunicam é full-duplex: isso significa que a informação pode transitar nos dois sentidos. Assim, o processo de destino pode enviar confirmações de recebimento mesmo enquanto o processo de origem continua enviando informações. Isso permite, por exemplo, que o protocolo de origem TCP envie vários segmentos sem esperar pelo aviso de recebimento. Se, após um certo tempo, ele perceber que não recebeu o aviso de recebimento de um determinado segmento nº n, ele retomará a transmissão dos segmentos a partir desse ponto.
11.3.3. A relação cliente-servidor
Frequentemente, a comunicação na Internet é assimétrica: a máquina A inicia uma conexão para solicitar um serviço à máquina B, especificando que deseja estabelecer uma conexão com o serviço SB1 da máquina B. Esta aceita ou recusa. Se aceitar, a máquina A pode enviar suas solicitações ao serviço SB1. Essas solicitações devem estar em conformidade com o protocolo de comunicação compreendido pelo serviço SB1. Estabelece-se, assim, um diálogo de solicitação-resposta entre a máquina A, chamada de máquina cliente, e a máquina B, chamada de máquina servidor. Um dos dois parceiros encerrará a conexão.
11.3.4. Arquitetura de um cliente
A arquitetura de um programa de rede que solicita os serviços de um aplicativo servidor será a seguinte:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
fermer la connexion
11.3.5. Arquitetura de um servidor
A arquitetura de um programa que oferece serviços será a seguinte:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
O programa servidor trata de maneira diferente a solicitação inicial de conexão de um cliente e suas solicitações posteriores para obter um serviço. O programa não presta o serviço propriamente dito. Se o fizesse, durante o tempo em que o serviço estivesse sendo prestado, ele não estaria mais à escuta das solicitações de conexão e, consequentemente, os clientes não seriam atendidos. Portanto, ele procede de outra forma: assim que uma solicitação de conexão é recebida na porta de escuta e, em seguida, aceita, o servidor cria uma tarefa encarregada de prestar o serviço solicitado pelo cliente. Esse serviço é prestado em outra porta da máquina servidora, chamada de porta de serviço. Dessa forma, é possível atender a vários clientes ao mesmo tempo.
Uma tarefa de serviço terá a seguinte estrutura:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
11.4. Conheça os protocolos de comunicação da Internet:
11.4.1. Introdução
Quando um cliente se conecta a um servidor, estabelece-se então um diálogo entre eles. A natureza desse diálogo define o que chamamos de 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 e-mails (servidor SMTP)
- POP: Post Office Protocol — o protocolo de comunicação com um servidor de armazenamento de e-mails (servidor POP). Trata-se de recuperar os e-mails recebidos, e não de enviá-los.
- FTP: File Transfer Protocol — o protocolo de comunicação com um servidor de armazenamento de arquivos (servidor FTP).
Todos esses protocolos têm a particularidade de serem protocolos de linhas de texto: o cliente e o servidor trocam linhas de texto. Se tivermos um cliente capaz de:
- estabelecer uma conexão com um servidor TCP
- exibir no console as linhas de texto que o servidor lhe envia
- enviar ao servidor as linhas de texto que um usuário digitaria
então é possível interagir com um servidor TCP que utilize um protocolo de linhas de texto, desde que se conheçam as regras desse protocolo.
O programa telnet, encontrado em máquinas Unix ou Windows, é um cliente desse tipo. Em máquinas Windows, também existe uma ferramenta chamada putty, e é ela que vamos utilizar aqui. O putty pode ser baixado no endereço [http://www.putty.org/]. Trata-se de um executável (.exe) pronto para uso. Vamos configurá-lo da seguinte maneira:
![]() |
- [1]: o endereço IP do servidor TCP ao qual queremos nos conectar ou seu nome
- [2]: a porta de escuta do servidor TCP
- [3]: utilize o modo Raw, que designa uma conexão TCP bruta.
- [4]: ativar o modo Never para impedir que a janela do cliente putty seja fechada caso o servidor encerre a conexão.
- [6,7]: número de colunas/linhas do console
- [5]: o número máximo de linhas mantidas na memória. Um servidor HTTP pode enviar muitas linhas. É necessário poder “rolar” a tela para visualizá-las.
![]() |
- [8,9]: para manter as configurações anteriores, atribua um nome à configuração [8] e salve-a [9].
- [11,12]: para recuperar uma configuração salva, selecione [11] e carregue-a como [12].
Com essa ferramenta assim configurada, vamos conhecer alguns protocolos TCP.
11.4.2. O protocolo HTTP (Protocolo de Transferência HyperText)
Vamos conectar nosso cliente [1] ao servidor web da máquina istia.univ-angers.fr [2], porta 80 [3]:
![]() |
No console de putty, criamos a seguinte caixa de diálogo HTTP:
- as linhas 1 a 4 correspondem à solicitação do cliente, digitada no teclado
- as linhas 5 a 19 são a 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 que feche a conexão assim que enviar sua resposta. O modo [Keep-Alive] solicita que ela seja mantida aberta.
- linha 4: linha vazia. As linhas 1 a 3 são chamadas de cabeçalhos HTTP. Pode haver outros além dos apresentados aqui. O fim dos cabeçalhos HTTP é sinalizado por uma linha vazia.
- linhas 5-13: os cabeçalhos HTTP da resposta do servidor — também terminam 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: data e hora do servidor
- linha 7: identificação do software que fornece o serviço web — neste caso, um servidor Apache em um sistema Linux/Debian
- linha 8: o documento foi gerado dinamicamente pelo PHP
- linha 9: cookie de identificação do cliente — se ele quiser ser reconhecido na próxima conexão, deverá reenviar esse cookie em seus cabeçalhos HTTP.
- linha 10: indica que, após servir o documento solicitado, o servidor encerrará a conexão
- linha 11: o documento será transmitido em partes (chunked) e não em um único bloco.
- linha 12: tipo do 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 esse número for 0 (linha 19), o cliente saberá que recebeu o documento completo.
- linhas 15-18: parte do documento recebido.
A conexão foi encerrada e o cliente putty está inativo. Vamos nos reconectar [1] e limpar a tela das exibições anteriores [2,3]:
![]() |
A caixa de diálogo desta vez é a seguinte:
- linha 1: foi solicitado um documento inexistente
- linha 5: o servidor HTTP respondeu com o código 404, indicando que o documento solicitado não foi encontrado.
Se solicitarmos esse documento usando o navegador Firefox:

Se solicitarmos a visualização do código-fonte [Affichage/Code source]:
Obtemos as linhas 13 a 22 recebidas pelo nosso cliente putty. O interesse disso é nos mostrar, além disso, os cabeçalhos HTTP da resposta. Também é possível obtê-los com o Firefox.
11.4.3. O protocolo SMTP (Simple Mail Transfer Protocol)
![]() |
Os servidores SMTP geralmente operam na porta 25 [2]. A conexão é feita com o servidor [1]. Nesse caso, 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 solicitações de computadores que pertençam ao mesmo domínio que eles. Além disso, com bastante frequência, os firewalls ou antivírus dos computadores pessoais estão configurados para não aceitar conexões na porta 25 provenientes 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:
Abaixo, (D) representa uma solicitação 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 ser enviada, terminada por uma linha contendo apenas um ponto.
- linha 13: (R) resposta do servidor
- linha 14: (D) o cliente sinaliza que concluiu
- linha 15: (R) resposta do servidor, que em seguida encerra a conexão
11.4.4. O protocolo POP (Post Office Protocol)
![]() |
Os servidores POP geralmente operam na porta 110 [2]. Conectamo-nos ao servidor [1]. A caixa de diálogo POP na janela do cliente putty é a seguinte:
- linha 1: (R) mensagem de boas-vindas do servidor POP
- linha 2: (D) o cliente fornece seu identificador POP, c.a.d. O login com o qual ele acessa seus e-mails
- linha 3: (R) a resposta do servidor
- linha 4: (D) a senha do cliente
- linha 5: (R) a resposta do servidor
- linha 6: (D) o cliente solicita a lista de 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 contendo os cabeçalhos da mensagem e as linhas 23-24 contendo o corpo da mensagem.
- linha 26: (D) o cliente indica que concluiu a operação
- linha 27: (R) resposta do servidor, que em seguida encerrará a conexão.
11.4.5. O protocolo FTP (Protocolo de Transferência de Arquivos)
O protocolo FTP é mais complexo do que os apresentados anteriormente. Para identificar 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 do Windows para realizar transferências de arquivos. As ações do usuário na interface do Windows são convertidas em comandos FTP, que são registrados no [1]. Essa é uma boa maneira de descobrir os comandos do protocolo FTP.
11.5. As classes .NET da programação para a internet
11.5.1. Escolher a classe adequada
O framework .NET oferece diversas classes para trabalhar com a rede:
![]() |
- A classe Socket é a que opera mais próximo da rede. Ela permite gerenciar a conexão de rede com precisão. O termo socket designa uma tomada elétrica. O termo foi ampliado para designar uma conexão de rede virtual. Em uma comunicação TCP-IP entre duas máquinas A e B, são dois sockets que se comunicam entre si. Um aplicativo pode trabalhar diretamente com os sockets. Esse é o caso do aplicativo A mencionado acima. Um soquete pode ser um soquete client ou serveur.
- Se desejarmos trabalhar em um nível menos detalhado do que o da classe Socket, poderemos utilizar as classes
- TcpClient para criar um cliente TCP
- TcpListener para criar um servidor TCP
Essas duas classes oferecem ao aplicativo que as utiliza uma visão mais simples da comunicação de rede, gerenciando para ele os detalhes técnicos do gerenciamento de sockets.
- .NET oferece classes específicas para determinados protocolos:
- a classe SmtpClient para gerenciar o protocolo SMTP de comunicação com um servidor SMTP de envio de e-mails
- a classe WebClient para gerenciar os protocolos HTTP ou FTP de comunicação com um servidor web.
Vale ressaltar que a classe Socket é suficiente por si só para gerenciar toda a comunicação TCP/IP, mas procuraremos, antes de tudo, utilizar as classes de nível superior para 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 seus construtores C, métodos M e propriedades P, ela possui os seguintes:
C | cria uma conexão TCP com o serviço em execução na porta indicada (port) da máquina indicada (hostname). Por exemplo, new TcpClient("istia.univ-angers.fr", 80) para se conectar à porta 80 da máquina istia.univ-angers.fr | |
P | o soquete usado pelo cliente para se comunicar com o servidor. | |
M | obtém um fluxo de leitura e gravação para o servidor. É esse fluxo que permite as trocas entre cliente e servidor. | |
M | encerra a conexão. O soquete e o fluxo NetworkStream também são encerrados | |
P | verdadeiro se a conexão foi estabelecida |
A classe NetworkStream representa o fluxo de rede entre o cliente e o servidor. Ela é 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 gravar essas linhas no fluxo de rede. Assim, se uma máquina M1 estabeleceu uma conexão com uma máquina M2 por meio de um objeto TcpClient client1 e elas trocam linhas de texto, ela poderá criar seus fluxos de leitura e gravação da seguinte maneira:
StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=new StreamWriter(client1.GetStream());
out1.AutoFlush=true;
A instrução
significa que o fluxo de gravação de client1 não passará por um buffer intermediário, mas irá diretamente para a rede. Esse ponto é importante. Geralmente, quando client1 envia uma linha de texto ao seu parceiro, ele espera uma resposta. Essa resposta nunca chegará se a linha tiver sido, na verdade, armazenada em buffer na máquina M1 e nunca enviada para a máquina M2.
Para enviar uma linha de texto para a máquina M2, escrever-se-á:
Para ler a resposta da máquina M2, escrever-se-á:
Agora temos os elementos necessários para escrever a arquitetura básica de um cliente da Internet com o seguinte protocolo básico de comunicação com o servidor:
- o cliente envia uma solicitação contida em uma única linha
- o servidor envia uma resposta contida em uma única linha
using System;
using System.IO;
using System.Net.Sockets;
namespace ... {
class ... {
static void Main(string[] args) {
...
try {
// estamos nos conectando 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 solicitação-resposta
while (true) {
// a solicitação vem do teclado
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// Concluído?
if (demande.Trim().ToLower() == "bye")
break;
// enviando a solicitação ao servidor
writer.WriteLine(demande);
// lendo a resposta do servidor
réponse = reader.ReadLine();
// processando a resposta
...
}
}
}
}
}
} catch (Exception e) {
// erro
...
}
}
}
}
- linha 11: criação da conexão do cliente — a cláusula using garante que os recursos associados a ela serão liberados ao sair do using.
- linha 12: abertura do fluxo de rede em uma cláusula using
- linha 13: criação e execução do fluxo de leitura em uma cláusula using
- linha 14: criação e execução do fluxo de gravação em uma cláusula using
- linha 16: não armazenar em buffer o fluxo de saída
- linhas 18-31: o ciclo solicitação do cliente / resposta do servidor
- linha 26: o cliente envia sua solicitação ao servidor
- linha 28: o cliente aguarda a resposta do servidor. Trata-se de uma operação bloqueante, semelhante à leitura do teclado. A espera termina com a chegada de uma sequência de caracteres terminada por “\n” ou com o fim do fluxo. Isso ocorrerá se o servidor fechar a conexão que abriu 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 seus construtores C, métodos M e propriedades P, ela possui os seguintes:
C | cria um serviço TCP que ficará à espera (listen) de solicitações dos clientes em uma porta passada como parâmetro (port), chamada de porta de escuta. Se a máquina estiver conectada a várias redes IP, o serviço ficará à escuta em cada uma delas. | |
C | O mesmo, mas a escuta ocorre apenas no endereço IP especificado. | |
M | inicia a escuta das solicitações dos clientes | |
M | aceita a solicitação de um cliente. Em seguida, abre uma nova conexão com ele, chamada conexão de serviço. A porta utilizada no lado do servidor é aleatória e escolhida pelo sistema. É chamada de porta de serviço. AcceptTcpClient retorna como resultado o objeto TcpClient associado, no lado do servidor, à conexão de serviço. | |
M | deixa de ouvir as solicitações dos clientes | |
P | o soquete de escuta do servidor |
A estrutura básica de um servidor TCP que se comunicaria com seus clientes de acordo com o seguinte protocolo:
- o cliente envia uma solicitação contida em uma única linha
- o servidor envia uma resposta contida em uma ú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 ... {
...
// criando o serviço de escuta
TcpListener ecoute = null;
try {
// cria-se o serviço — ele ficará à escuta em todas as interfaces de rede da máquina
ecoute = new TcpListener(IPAddress.Any, port);
// iniciando o serviço
ecoute.Start();
// loop do serviço
TcpClient tcpClient = null;
// loop infinito — será interrompido com Ctrl-C
while (true) {
// aguardando 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();
}
}
// -------------------------------------------------------
// atende um cliente
public static void Service(Object infos) {
// recupera-se o cliente a ser atendido
Client client = infos as Client;
// exploração da conexã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 solicitação/gravação de resposta
bool fini=false;
while (! fini) != null) {
// aguardando solicitação do cliente — operação bloqueante
demande=reader.ReadLine();
// preparação da resposta
réponse=...;
// envio da resposta ao cliente
writer.WriteLine(réponse);
// próxima solicitação
}
}
}
}
}
} 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 possui pelo menos dois endereços IP: o endereço “127.0.0.1”, que é seu endereço de loopback, e o endereço “I1.I2.I3.I4”, que ela possui na rede à qual está conectada. Ela pode ter outros endereços IP se estiver conectada 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. Ele já havia sido criado anteriormente, mas ainda não estava em escuta. Estar em escuta significa aguardar as solicitações dos clientes.
- linhas 20-26: o ciclo de espera por solicitação do cliente / atendimento ao cliente é repetido para cada novo cliente
- linha 22: a solicitação de um cliente é aceita. O método AcceptTcpClient retorna uma instância TcpClient, denominada de serviço:
- o cliente fez sua solicitação com sua própria instância TcpClient no lado do cliente, que chamaremos de TcpClientDemande
- o servidor aceita essa solicitação com AcceptTcpClient. Esse método cria uma instância TcpClient no lado do servidor, que chamaremos de TcpClientService. Temos, então, uma conexão TCP aberta com as instâncias TcpClientDemande <--> TcpClientService em ambas as extremidades.
- A comunicação cliente/servidor que ocorre a seguir é feita por meio dessa conexão. O serviço de escuta não intervém mais.
- linha 24: para que o servidor possa atender a vários clientes ao mesmo tempo, o serviço é garantido por threads, sendo 1 thread por cliente.
- linha 32: o serviço de escuta é encerrado
- linha 38: o método executado pela thread de serviço para um cliente. Ele recebe como parâmetro a instância TcpClient já conectada ao cliente que deve ser atendido.
- linhas 38-71: encontramos um código semelhante ao do cliente TCP básico estudado 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 pelo comando:
ServeurEcho porta
O servidor opera na porta passada como parâmetro. Ele se limita a reenviar ao cliente a solicitação 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
// reenviasse 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;
}
// esse argumento deve ser um número inteiro maior que 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 — ele ficará à escuta em todas as interfaces de rede da máquina
ecoute = new TcpListener(IPAddress.Any, port);
// iniciando 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) {
// aguardando 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 ser atendido
Client client = infos as Client;
// presta o serviço ao cliente
Console.WriteLine("Début de service au client {0}", client.NumClient);
// operação da conexã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 (solicitação)/gravação (resposta)
string demande = null;
while ((demande = reader.ReadLine()) != null) {
// monitoramento do console
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
// eco da solicitação para o cliente
writer.WriteLine("[{0}]", demande);
// monitoramento do console
Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
// o serviço é encerrado 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; } // conexã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. Comentaremos apenas a parte “atendimento ao cliente”:
- linha 79: a solicitação do cliente é lida
- linha 83: ela é devolvida ao cliente entre colchetes
- linha 79: o serviço é encerrado quando o cliente encerra a conexão
Em uma janela do DOS, usamos o executável do projeto C#:
Em seguida, iniciamos dois clientes putty, que conectamos à porta 100 da máquina localhost:
![]() |
A exibição do console do servidor de eco fica assim:
O cliente 1 e, em seguida, o cliente 0 enviam os seguintes textos:
![]() |
- [1]: o cliente nº 1
- [2]: o cliente nº 0
- [3]: o console do servidor de eco
![]() |
- em [4]: o cliente 1 se desconecta com o comando bye.
- em [5]: o servidor detecta isso
O servidor pode ser encerrado com Ctrl-C. O cliente nº 0 detecta isso então com [6].
11.6.2. Um cliente para o servidor de eco
Agora vamos escrever um cliente para o servidor anterior. Ele será chamado da seguinte forma:
ClientEcho nomServeur porta
Ele se conecta à 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 {
// conecta-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 maior que 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; // solicitação do cliente
string réponse = null; // resposta do servidor
try {
// conectando-se 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 solicitação-resposta
while (true) {
// a solicitação vem do teclado
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// Concluído?
if (demande.Trim().ToLower() == "bye")
break;
// enviando a solicitação ao servidor
writer.WriteLine(demande);
// lendo a resposta do servidor
réponse = reader.ReadLine();
// processando a resposta
Console.WriteLine("Réponse : {0}", réponse);
}
}
}
}
}
} catch (Exception e) {
// erro
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
A estrutura desse 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 em uma janela do DOS
- na mesma máquina, dois clientes são iniciados em duas outras janelas do DOS
Na janela do cliente A (n.º 0), temos as seguintes exibições:
Na janela do cliente B (n.º 1):
Na janela do servidor:
O cliente A nº 0 se desconecta:
No console do servidor:
11.6.3. Um cliente genérico TCP
Vamos escrever um cliente TCP genérico que será iniciado da seguinte forma: ClientTcpGenerique servidor porta. Ele funcionará de maneira semelhante ao cliente PuTTY, mas terá uma interface de console e não apresentará opções de configuração.
No aplicativo 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 possui seu protocolo específico, e também podemos encontrar as seguintes situações:
- o cliente precisa enviar várias linhas de texto antes de receber uma resposta
- a resposta de um servidor pode conter várias linhas de texto
Portanto, o ciclo de envio de uma única linha ao servidor / recebimento de uma única linha enviada pelo servidor nem sempre é adequado. Para lidar com protocolos mais complexos do que o de eco, o cliente TCP genérico terá duas threads:
- a thread principal lerá as linhas de texto digitadas no teclado e as enviará ao servidor.
- uma thread secundária trabalhará em paralelo e se dedicará à leitura das linhas de texto enviadas pelo servidor. Assim que receber uma, ela a exibirá no console. A thread só é interrompida quando o servidor encerra a conexão. Portanto, ela trabalha continuamente.
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
// conecta-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 maior que 0
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// conectamos-nos 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; // solicitação do cliente
try {
// utiliza-se a conexão do cliente
using (tcpClient) {
// cria-se um fluxo de gravação 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 solicitação-resposta
while (true) {
demande = Console.ReadLine();
// Concluído?
if (demande.Trim().ToLower() == "bye")
break;
// envia-se a solicitação 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 no console
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 da 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 se conecta ao servidor
- linha 43: é iniciado um thread para ler as linhas de texto do servidor. Ele deve executar o método Receive da linha 73. Passa-se para esse método a instância TcpClient que foi conectada ao servidor.
- linhas 57-64: o loop de entrada de comandos do teclado / envio de comandos ao servidor. A entrada dos comandos do teclado é realizada pela thread principal.
- linhas 75-98: o método Receive executado pela thread de leitura das linhas de texto. Esse método recebe como parâmetro a instância TcpClient, que foi conectada ao servidor.
- linhas 84-87: o loop contínuo de leitura das linhas de texto enviadas pelo servidor. Ele só é interrompido quando o servidor encerra a conexão aberta com o cliente.
Aqui estão alguns exemplos que reproduzem aqueles utilizados com o cliente putty no parágrafo 11.4. O cliente é executado em um console DOS.
Protocolo HTTP
Convidamos o leitor a reler as explicações fornecidas no parágrafo 11.4.2. Comentaremos apenas o que é específico da aplicação:
- linha 28: após o envio da linha 27, o servidor HTTP encerrou a conexão, o que provocou o encerramento da thread de leitura. A thread principal, que lê os comandos digitados no teclado, continua ativa. O comando da linha 29, digitado no teclado, a encerra.
Protocolo SMTP
Recomenda-se ao leitor que releia as explicações fornecidas no parágrafo 11.4.3 e teste os outros exemplos utilizados com o cliente putty.
11.6.4. Um servidor TCP genérico
Agora, vamos nos concentrar em um servidor
- que exibe na tela os comandos enviados por seus clientes
- e lhes envia como resposta as linhas de texto digitadas no teclado por um usuário. Portanto, é o próprio usuário que atua como servidor.
O programa é executado em uma janela do DOS por meio do comando: ServeurTcpGenerique portEcoute, onde portEcoute é a porta à qual os clientes devem se conectar. O atendimento ao cliente será garantido por duas threads:
- o thread principal, que:
- processará os clientes um após o outro, e não em paralelo;
- que lerá as linhas digitadas pelo usuário no teclado e as enviará ao cliente. O usuário indicará, por meio do comando “bye”, que está encerrando a conexão com o cliente. Como o console não pode ser usado por dois clientes simultaneamente, nosso servidor processa apenas um cliente por vez.
- um thread secundário dedicado exclusivamente à leitura das linhas de texto enviadas pelo cliente
O servidor, por sua vez, nunca para, a menos que o usuário pressione Ctrl-C no teclado.
Vejamos alguns exemplos. O servidor é iniciado na porta 100 e usamos o cliente genérico do paragraphe11.6.3 para nos comunicarmos com ele. A janela do cliente é a seguinte:
As linhas que começam com <-- são as enviadas do servidor para o cliente; as demais, do cliente para o servidor. A janela do servidor é a seguinte:
As linhas que começam com <-- são as enviadas do cliente para o servidor; as demais são as enviadas pelo servidor para o cliente. A linha 9 indica que o thread de leitura das solicitações do cliente foi interrompido. O thread principal do servidor continua aguardando comandos digitados no teclado para enviá-los ao cliente. É necessário, então, digitar no teclado o comando bye da linha 10 para passar para o próximo cliente. O servidor ainda está ativo, embora o cliente 1 tenha sido encerrado. Iniciamos um segundo cliente para o mesmo servidor:
A janela do servidor fica então assim:
Após a linha 6 acima, o servidor ficou aguardando um novo cliente. É possível encerrá-lo com Ctrl-C.
Vamos agora simular um servidor web, iniciando nosso servidor genérico na porta 88:
Vamos agora abrir um navegador e acessar a página http://localhost:88/exemple.html. O navegador se conectará à porta 88 da máquina localhost e, em seguida, solicitará a página /exemple.html:
![]() |
Vamos agora observar a janela do nosso servidor:
Podemos observar os cabeçalhos HTTP enviados pelo navegador. Isso nos permite identificar outros cabeçalhos HTTP além dos já encontrados. Vamos elaborar uma resposta para o nosso cliente. O usuário que está digitando no teclado é, neste caso, o verdadeiro servidor e pode elaborar uma resposta manualmente. Vamos relembrar a resposta enviada por um servidor web em um exemplo anterior:
Vamos tentar dar uma resposta semelhante, limitando-nos ao estritamente necessário:
Limitamos nossa resposta aos cabeçalhos HTTP das linhas 1 a 4. Não informamos o tamanho do documento que vamos enviar (Content-Length), mas nos limitamos a indicar que vamos encerrar a conexão (Connection: close) após o envio do mesmo. Isso é suficiente para o navegador. Ao perceber que a conexão foi encerrada, ele saberá que a resposta do servidor está concluída e exibirá a página HTML que lhe foi enviada. Esta última corresponde às linhas 6 a 9. O usuário, pelo teclado, encerra então a conexão com o cliente digitando o comando bye, na linha 10. Com esse comando digitado, o thread principal encerra a conexão com o cliente. Isso 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 conexão com o cliente e lançou uma exceção. Após a linha 12, o servidor fica aguardando um novo cliente.
O navegador do cliente exibe agora o seguinte:
![]() |
Se, no exemplo acima, digitarmos Affichage/Source para ver o que o navegador recebeu, obtemos [2], ou seja, exatamente o que foi enviado pelo 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);
}
// esse argumento deve ser um número inteiro maior que 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);
// ele é iniciado
ecoute.Start();
// acompanhamento
Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
while (true) {
// aguardando 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
// utiliza-se a conexão do cliente
using (tcpClient) {
// cria-se um fluxo de gravação 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 entrada de respostas pelo teclado
while (true) {
réponse = Console.ReadLine();
// Concluído?
if (réponse.Trim().ToLower() == "bye")
break;
// envia-se a solicitação ao cliente
writer.WriteLine(réponse);
}
}
}
}
}
} catch (Exception ex) {
// notificamos 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; // solicitação do cliente
string idClient=null; // identidade do cliente
// gestão da conexã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 no console
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. Ele 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 usuário encerrará o servidor com Ctrl-C.
- linha 37: espera por um cliente — operação bloqueante. Quando o cliente chega, a instância TcpClient retornada pelo método AcceptTcpClient representa o lado do servidor de uma conexão aberta com o cliente.
- linha 40: o fluxo de leitura das solicitações do cliente é atribuído a um thread separado.
- linha 45: uso da conexão com o cliente em uma cláusula using para garantir que ela seja fechada independentemente do que acontecer.
- linha 47: uso do fluxo de rede em uma cláusula using
- linha 48: criação, em uma cláusula using, de um fluxo de gravação no fluxo de rede
- linha 50: o fluxo de gravação não será armazenado em buffer
- linhas 52-59: loop de entrada por teclado dos comandos a serem enviados ao cliente
- linha 69: fim do serviço de escuta. Essa instrução nunca será executada aqui, pois o servidor é interrompido por Ctrl-C.
- linha 78: o método Receive, que exibe continuamente no console as linhas de texto enviadas pelo cliente. Aqui encontramos o que foi visto para o cliente genérico TCP.
11.6.5. Um cliente Web do tipo “ ”
Vimos no exemplo anterior alguns dos cabeçalhos HTTP enviados por um navegador:
Vamos escrever um cliente web ao qual seria passado como parâmetro um URL e que exibiria na tela o texto enviado pelo servidor. Suporemos que este suporte o protocolo HTTP 1.1. Dos cabeçalhos anteriores, utilizaremos apenas os seguintes:
- o primeiro cabeçalho indica o documento desejado
- o segundo, o servidor consultado
- o terceiro indica que desejamos que o servidor encerre a conexão após nos responder.
Se, na linha 1 acima, substituirmos GET por HEAD, o servidor nos enviará apenas os cabeçalhos HTTP e não o documento especificado na linha 1.
Nosso cliente web será chamado da seguinte forma: ClientWeb URL cmd, onde URL é oURL desejado e cmd uma das duas palavras-chave GET ou HEAD para indicar se deseja apenas os cabeçalhos (HEAD) ou também o conteúdo da página (GET). Vejamos um primeiro exemplo:
- linha 1: solicitamos apenas os cabeçalhos HTTP (HEAD)
- linhas 2 a 9: a resposta do servidor
Se usarmos 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;
}
// observa-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 {
// conectando-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;
// solicitando 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();
// lê-se a resposta
string réponse = null;
while ((réponse = reader.ReadLine()) != null) {
// exibe-se a resposta no console
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 é o uso da classe Uri. O programa recebe um URL (Uniform Resource Locator) ou URI (Uniform Resource Identifier) no formato http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... A classe Uri nos permite decompor a string do URL em seus diferentes elementos.
- linhas 26-33: um objeto Uri é construído a partir da sequência stringURI recebida como parâmetro. Se a string URI recebida como parâmetro não for uma URI válida (ausência do protocolo, do servidor, etc.), uma exceção é lançada. Isso nos permite verificar a validade do parâmetro recebido. Uma vez construído o objeto Uri, temos acesso aos diferentes elementos dessa URI. Assim, se o objeto uri do código anterior tiver sido construído a partir da string http://serveur:port/document?param1=val1¶m2=val2;..., teremos:
- uri.Host=serveur,
- uri.Port=port,
- uri.Path = document,
- uri.Query=param1=val1¶m2=val2;...,
- uri.pathAndQuery= cheminPageHTML?param1=val1¶m2=val2;...,
- uri.Scheme=http.
11.6.6. Um cliente web que lida com redirecionamentos
O cliente da Web anterior não lida com um possível redirecionamento do URL que ele mesmo solicitou. Veja um exemplo:
- linha 2: o código 302 Found indica um redirecionamento. O endereço para o qual o navegador deve ser redirecionado está no corpo do documento, na linha 16.
Um segundo exemplo:
- 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:
- linha 2: o código 302 Moved Temporarily indica um redirecionamento. O endereço para o qual o navegador deve ser redirecionado está indicado na linha 5, no cabeçalho HTTP Location.
Um quarto exemplo com um servidor IIS local na máquina:
- linha 2: o código 302 Object moved indica um redirecionamento. O endereço para o qual o navegador deve ser redirecionado está indicado na linha 5, no cabeçalho HTTP Location. Observe que, ao contrário dos exemplos anteriores, o endereço de redirecionamento é relativo. O endereço completo é, na verdade, http://localhost/localstart.asp.
Propomos gerenciar as redirecionamentos quando a primeira linha dos cabeçalhos HTTP contiver a palavra-chave moved (sem distinção entre maiúsculas e minúsculas) e o endereço de redirecionamento estiver no cabeçalho HTTP Location.
Se retomarmos os três últimos exemplos, obtemos os seguintes resultados:
URL: http://www.bull.com
- linha 11: o redirecionamento ocorre para o endereço da linha 6
URL: http://www.gouv.fr
- linha 11: o redirecionamento ocorre para o endereço da linha 6
URL: http://localhost
- linha 13: o redirecionamento ocorre para o endereço da linha 6
- linha 15: o acesso à página http://localhost/localstart.asp nos foi negado.
O programa responsável pelo 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 o URI foi 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;
}
const int nbRedirsMax = 1; // não é permitido mais de um redirecionamento
int nbRedirs = 0; // número de redirecionamentos em andamento
// expressão regular para localizar um URL de redirecionamento
Regex location = new Regex(@"^Location: (.+?)$");
try {
// pode haver várias URL a serem solicitadas caso haja redirecionamentos
while (nbRedirs <= nbRedirsMax) {
// gerenciamento de redirecionamento
bool redir = false;
bool locationFound = false;
string locationString = null;
// conectamos-nos 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 sem 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();
// lê-se a primeira linha da resposta
string premièreLigne = reader.ReadLine();
// eco na tela
Console.WriteLine(premièreLigne);
// redirecionamento?
if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
// há um redirecionamento
redir = true;
nbRedirs++;
}
// cabeçalhos HTTP seguintes 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 for encontrada, registra-se o URL de redirecionamento
locationString = résultat.Groups[1].Value;
// registra-se que foi encontrado
locationFound = true;
}
}
}
// os cabeçalhos HTTP foram esgotados — escreve-se a linha vazia
Console.WriteLine(réponse);
// em seguida, passa-se para o corpo do documento
while ((réponse = reader.ReadLine()) != null) {
Console.WriteLine(réponse);
}
}
}
}
// já terminamos?
if (!locationFound || nbRedirs > nbRedirsMax)
break;
// há um redirecionamento a ser feito — 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);
}
// log da console
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) {
// exibição da 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 antes era executado para uma única URI agora pode ser executado sucessivamente para várias URIs.
- linha 66: lê-se a primeira linha dos cabeçalhos HTTP enviados pelo servidor. É ela 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, ela é registrada.
- linhas 79-93: leitura dos demais cabeçalhos HTTP até encontrar a linha vazia que sinaliza o fim deles. 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 é exibido no console.
- linhas 105-106: a URI solicitada foi totalmente processada e exibida. Se não houver redirecionamento a ser feito 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 ser solicitada. É necessário realizar um pequeno ajuste, dependendo se o endereço de redirecionamento encontrado era absoluto (linha 111) ou relativo (linha 114).
11.7. As classes .NET especializadas em um protocolo específico da Internet
Nos exemplos anteriores do cliente web, o protocolo HTTP era gerenciado por um cliente TCP. Portanto, precisávamos gerenciar nós mesmos 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. Essas classes conhecem o protocolo de comunicação entre o cliente e o servidor e evitam que o desenvolvedor precise gerenciá-los. Apresentamos essas classes a seguir.
11.7.1. A classeWebClient
Existe uma classe WebClient capaz de se comunicar com um servidor web. Consideremos o exemplo do cliente web do parágrafo 11.6.5, tratado aqui 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;
}
// observa-se que o URI foi 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 será feita. Veremos que outros cabeçalhos serão enviados por padrão.
- linha 38: o cliente web solicita a URI fornecida pelo usuário e lê o documento enviado. [WebClient].OpenRead(Uri) abre a conexão com Uri e lê a resposta. É aí que reside a utilidade da classe. Ela se encarrega da comunicação 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 dele.
- linha 39: utiliza-se um StreamReader e, na linha 41, 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:
Iniciamos o cliente web anterior da seguinte maneira:
A URI solicitada é a do servidor genérico. Este, então, exibe os cabeçalhos HTTP que lhe foram enviados pelo cliente web:
Assim, observamos que:
- que o cliente web envia 3 cabeçalhos HTTP por padrão (linhas 3, 5, 6)
- linha 4: o cabeçalho que nós mesmos geramos (linha 37 do código)
- que o cliente web utiliza, por padrão, o método GET (linha 3). Existem outros métodos, entre os quais POST e HEAD.
Agora, vamos solicitar um recurso inexistente:
- 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, vamos encerrar solicitando um recurso existente:
O arquivo istia.univ-angers.txt gerado pelo comando é o seguinte:
- linha 1: o documento HTML solicitado.
- linhas 3-10: os cabeçalhos da resposta HTTP em uma 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):
para baixar um recurso como uma matriz de bytes (por exemplo, uma imagem) | |
para baixar um recurso e salvá-lo em um arquivo local | |
para baixar um recurso e recuperá-lo como uma sequência de caracteres (por exemplo, um arquivo HTML) | |
o equivalente a OpenRead, mas para enviar dados ao servidor | |
o equivalente a DownLoadData, mas para o servidor | |
o equivalente a DownLoadFile, mas para o servidor | |
o equivalente a DownLoadString, mas direcionado para o servidor | |
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 em que transmite ao servidor as informações necessárias para determinar o documento real a ser enviado. Essas informações são enviadas como documento ao servidor, daí o nome UpLoad do método. Elas são enviadas após a linha vazia dos cabeçalhos HTTP na forma param1=valeur1¶m2=valeur2&...:
O mesmo documento poderia ser solicitado pelo método GET:
A diferença entre os dois métodos é que o navegador que exibe a URI solicitada mostrará /document no caso de POST e /document?param1=valeur1¶m2=valeur2&... no caso de GET. |
11.7.2. As classes WebRequest / WebResponse
Às vezes, a classe WebClient não é flexível o suficiente para fazer o que se deseja. Retomemos o exemplo do cliente web com redirecionamento analisado no parágrafo 11.6.6. Precisamos emitir o cabeçalho HTTP:
Vimos que os cabeçalhos HTTP emitidos por padrão pelo cliente web eram os seguintes:
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, pois não segue o formato chave:valor. Não descobri como alterar o GET para HEAD na linha 1 a partir da classe WebClient (talvez eu não tenha procurado direito?). Quando a classe WebClient atingir seus limites, é possível passar para as classes WebRequest / WebResponse:
- WebRequest: representa a totalidade da solicitação do cliente da Web.
- WebResponse: representa a totalidade da resposta do servidor web
Já mencionamos que a classe WebClient gerencia os esquemas http:, https:, ftp: e file:. As solicitações e respostas desses diferentes protocolos não têm o mesmo formato. Portanto, é necessário manipular o tipo exato desses elementos, em vez de seus tipos genéricos WebRequest e WebResponse. Assim, utilizaremos as classes:
- HttpWebRequest, HttpWebResponse para um cliente HTTP
- FtpWebRequest, FtpWebResponse para um cliente FTP
Agora, utilizando as classes HttpWebRequest e HttpWebresponse, abordamos 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;
}
// observa-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 {
// configurando a solicitação
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = commande;
httpWebRequest.Proxy = null;
// executando-a
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 no console
Console.WriteLine(reader.ReadToEnd());
}
}
} catch (WebException e1) {
// a resposta é recuperada
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: um objeto do tipo WebRequest é criado com o método estático WebRequest.Create(Uri uri), em que uri é a URI do documento a ser baixado. Como sabemos que o protocolo da URI é HTTP, o tipo do resultado é alterado para HttpWebRequest, a fim de 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. Aqui, será GET ou HEAD.
- linha 42: em uma rede privada corporativa, é comum que os computadores da empresa fiquem isolados da internet por motivos de segurança. Para isso, a rede privada utiliza endereços de internet que os roteadores da internet não encaminham. A rede privada é conectada à Internet por meio de máquinas específicas chamadas proxies, que estão conectadas tanto à rede privada da empresa quanto à Internet. Esse é um exemplo de máquinas com vários endereços: IP. Um computador da rede privada não pode estabelecer por conta própria uma conexão com um servidor da internet, como um servidor web, por exemplo. Ele precisa solicitar que um servidor proxy faça isso por ele. Um servidor proxy pode hospedar servidores proxy para diferentes protocolos. O termo “proxy HTTP” é usado para designar o serviço responsável por enviar as solicitações HTTP em nome dos computadores da rede privada. Se existir um servidor proxy HTTP, é necessário indicá-lo no campo [WebRequest].proxy. Por exemplo, escrever-se-á:
se o proxy HTTP estiver operando na porta 3128 da máquina pproxy.istia.uang. Insira “null” no campo [WebRequest].proxy se a máquina tiver acesso direto à Internet e não precisar passar por um proxy.
- linha 44: o método GetResponse() solicita o documento identificado por sua URI e retorna um objeto WebRequestResponse, que aqui é transformado em um objeto HttpWebResponse. Esse objeto representa a resposta do servidor à solicitação do documento.
- linha 47:
- [HttpWebResponse].ResponseUri: é a URI do servidor que enviou o documento. Em caso de redirecionamento, ela pode ser diferente da URI do servidor consultado inicialmente. Observe-se que o código não lida com o redirecionamento. Ele é gerenciado automaticamente pelo método GetResponse. Mais uma vez, essa é 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.
Veja um exemplo de execução:
- linhas 1 e 3: o servidor que respondeu não é o mesmo que foi consultado. Portanto, houve 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
Mostramos agora como as classes anteriores nos permitem explorar os recursos da web.
11.7.3.1. L'application
Existem sites de tradução na web. O que será utilizado aqui é o site http://trans.voila.fr/traduction_voila.php:
![]() | O texto a ser traduzido é inserido em [1], o sentido da tradução é escolhido em [2]. A tradução é solicitada por [3] e obtida em [4]. |
Vamos escrever um aplicativo cliente para Windows da aplicação acima. Ele não fará nada além do que o aplicativo do site [trans.voila.fr]. Sua interface será a seguinte:
![]() |
11.7.3.2. A arquitetura do aplicativo
O aplicativo 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 dessa interface
- WebTraductionsException: uma exceção específica da aplicação
A interface IServiceTraduction é a seguinte:
using System.Collections.Generic;
namespace dao {
public interface IServiceTraduction {
// idiomas utilizados
IDictionary<string, string> LanguesTraduites { get; }
// tradução
string Traduire(string texte, string deQuoiVersQuoi);
}
}
- linha 6: a propriedade LanguesTraduites define o dicionário de idiomas aceitos pelo servidor de tradução. Esse dicionário possui entradas no formato ["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 ser traduzido
- deQuoiVersQuoi é uma das chaves do dicionário de idiomas traduzidos
- o método retorna a tradução do texto
ServiceTraduction é uma classe de implementação da interface IServiceTraduction. Detalhamos isso na seção a seguir.
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 do nosso aplicativo:
![]() |
A classe [ServiceTraduction] que precisamos escrever é um cliente do serviço web de tradução [trans.voila.fr]. Para escrevê-la, precisamos entender
- o que o servidor de tradução espera de seu cliente
- o que ele retorna ao cliente
Vejamos, por meio de um exemplo, o diálogo cliente/servidor que ocorre durante uma tradução. Retomemos o exemplo apresentado na introdução do aplicativo:
![]() | O texto a ser traduzido é inserido em [1], o sentido da 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 campo de endereço):
http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=o+cachorro+está+doente
É bastante simples de entender:
- http://trans.voila.fr/traduction_voila.php é a 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 ser traduzido em um formato chamado URL codificada. De fato, certos caracteres não podem aparecer em uma URL. É o caso, por exemplo, do espaço, que aqui foi codificado por um +. O framework .NET oferece o método estático System.Web.HttpUtility.UrlEncode para realizar esse trabalho de codificação.
Conclui-se, portanto, que, para consultar o servidor de tradução, nossa classe [ServiceTraduction] poderá utilizar a string
, em que os marcadores {0} e {1} serão substituídos, respectivamente, pelo sentido da tradução e pelo texto a ser traduzido.
Como saber quais são os sentidos de tradução aceitos pelo servidor? Na captura de tela acima, os idiomas traduzidos estão na lista suspensa. Se, no navegador, verificarmos (Exibir / Código-fonte) o código HTML da página, encontraremos o seguinte para a lista suspensa:
Não é um código HTML muito limpo, já que cada tag <option> deveria, normalmente, ser fechada por uma tag </option>. Dito isso, os atributos “value” nos fornecem 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 e os valores, os textos exibidos pela lista suspensa.
Agora, vamos verificar (Exibir / Código-fonte) onde se encontra, na página HTML, a tradução retornada pelo servidor de tradução:
A tradução está bem no meio da página HTML retornada. Como encontrá-la? É possível usar uma expressão regular com a sequência ...</div>, pois a tag só está presente nesse ponto da página HTML. A expressão regular em C# que permite recuperar o texto traduzido é a seguinte:
Agora temos 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 ser traduzido
string texteATraduire = HttpUtility.UrlEncode(texte);
// URI a ser solicitado
string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
// expressão regular para localizar 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) {
// busca a 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 é a URL a ser solicitada ao servidor de tradução: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, onde o marcador {0} deve ser substituído pelo idioma de destino e o marcador {1} pelo texto a ser traduzido — inicializada externamente
- linha 14: a propriedade ProxyHttp é o eventual proxy HTTP a ser utilizado, 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 retornado pelo servidor de tradução, por exemplo, @"<div class=""txtTrad"">(.*?)</div>" — inicializada externamente
- essas quatro propriedades serão, em nosso aplicativo, inicializadas pelo Spring.
- linhas 20-22: verifica-se se o sentido da tradução solicitado realmente existe no dicionário de idiomas traduzidos. Caso contrário, é lançada uma exceção.
- linha 24: o texto a ser traduzido é 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 string 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 ser traduzido.
- linha 28: o modelo de busca da tradução na resposta HTML do servidor de tradução é construído.
- linhas 33, 60: a operação de consulta ao servidor de tradução ocorre em um bloco try/catch
- linha 35: o objeto HttpWebRequest, que será usado para consultar o servidor de tradução, é criado com a URI do documento solicitado.
- linha 36: o método de consulta é GET. Seria possível dispensar essa instrução, pois GET é provavelmente o método padrão do objeto HttpWebRequest.
- linha 37: define-se a propriedade Proxy do objeto HttpWebRequest.
- linha 39: é feita a solicitação ao servidor de tradução e se recupera 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 ela é encontrada, interrompe-se a leitura da resposta HTML e fecham-se todos os fluxos que estavam 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 ocorreu uma exceção durante a troca cliente/servidor, ela é encapsulada em uma exceção do tipo WebTraductionsException para indicar isso.
- linhas 64-68: se uma exceção foi registrada, ela é lançada; caso contrário, a tradução encontrada é retornada.
Nosso exemplo pressupõe que o proxy HTTP não exija autenticação. Caso contrário, escreveríamos algo como:
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");
Usamos aqui WebRequest / WebResponse em vez de WebClient porque não precisamos utilizar toda a resposta HTML do servidor de tradução. Assim que a tradução é encontrada nessa resposta, não precisamos mais do restante das linhas da resposta. A classe WebClient não permite fazer isso.
Aqui está 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 localizar 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:
O projeto [dao] da solução é compilado em um DLL HttpTraductions.dll:
![]() |
11.7.3.6. A interface gráfica do aplicativo
Vamos revisar a arquitetura do nosso aplicativo:
![]() |
Agora, vamos escrever a camada [ui]. Ela é 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
- aquelas da camada [dao]: HttpTraductions
O arquivo [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&translationDirection={0}&stext={1}"/>
<!--
<property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
-->
<property name="RegexTraduction" value="<div class="txtTrad">(.*?)</div>"/>
<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 serem instanciados 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. Há um problema com o caractere & da URL. Esse caractere tem um significado em um arquivo XML. Portanto, ele deve ser protegido. O mesmo se aplica a outros caracteres que encontraremos ao longo do arquivo. Eles devem ser substituídos por uma sequência [&code;]: & por [&], < por [<], > por [>], " por ["].
- linha 21: a propriedade ProxyHttp da classe ServiceTraduction. Uma propriedade não inicializada permanece em null. Não definir essa propriedade equivale a dizer que não há proxy HTTP.
- linha 23: a propriedade RegexTraduction da classe ServiceTraduction. Na expressão regular, foi necessário substituir os caracteres [< > "] por seus equivalentes protegidos.
- linhas 24-33: a propriedade LanguesTraduites da classe ServiceTraduction.
O programa [Program.cs] é executado ao iniciar o aplicativo. 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 do aplicativo.
/// </summary>
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// --------------- Código do desenvolvedor
// 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) {
// armazenamento da exceção
ex = e1;
}
// formulário a ser exibido
Form form = null;
// houve alguma exceção?
if (ex != null) {
// sim — cria-se a mensagem de erro a ser exibida
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 é passada a mensagem de erro a ser exibida
Form2 form2 = new Form2();
form2.MsgErreur = msgErreur.ToString();
// essa será a janela a ser exibida
form = form2;
} else {
// tudo correu bem
// criação da interface gráfica [Form1], para a qual é passada a referência ao serviço de tradução
Form1 form1 = new Form1();
form1.ServiceTraduction = serviceTraduction;
// esta será a janela a ser exibida
form = form1;
}
// exibição da janela
Application.Run(form);
}
}
}
Esse código já foi utilizado no aplicativo Impostos versão 6, no parágrafo 7.6.2.
- O serviço de tradução é criado na linha 27 pelo Spring. Se essa criação tiver ocorrido corretamente, o formulário [Form1] será exibido (linhas 52-55); caso contrário, será exibido o formulário de erro [Form2] (linhas 36-48).
O formulário [Form2] é o utilizado no aplicativo 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 | campo de entrada do texto a ser traduzido 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 idiomas
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 idiomas
foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
// idiomas
string langues = ServiceTraduction.LanguesTraduites[code];
// adição (idiomas, código) ao dicionário inverso
languesInversées[langues] = code;
}
// preenchimento da lista suspensa em 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 algo 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. Essa propriedade pública foi inicializada por [Program.cs], linha 53. Portanto, quando os métodos Form1_Load (linha 20) ou buttonTraduire_Click (linha 40) são executados, esse campo já está inicializado.
- linha 12: o dicionário de idiomas traduzidos com entradas do tipo ["Français-Anglais","fe"], c.a.d. O inverso do dicionário LanguesTraduites gerado pelo serviço de tradução.
- linha 20: o método Form1_Load é executado ao carregar o formulário.
- linhas 22-27: utiliza-se o dicionário serviceTraduction.LanguesTraduites ["fe","Français-Anglais"] para construir o dicionário languesInversées ["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: essa tabela é ordenada para apresentar, na lista suspensa, os sentidos de tradução em ordem alfabética
- linhas 31-33: a lista suspensa de idiomas é preenchida.
- linha 40: o método executado quando o usuário 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 ser traduzido; o segundo, o código da direção de tradução. Esse código é encontrado no dicionário languesInversées com base no elemento selecionado na lista suspensa de idiomas.
- linha 48: se houver uma exceção, ela é exibida no lugar da tradução.
11.7.3.7. Conclusion
Este aplicativo demonstrou que os clientes web do framework .NET nos permitiam explorar os recursos da web. A técnica é sempre semelhante:
- determinar a URI a ser consultada. Essa URI é, na maioria das vezes, parametrizada.
- consultá-la
- encontrar na resposta do servidor o que estamos procurando por meio de expressões regulares
Essa técnica é aleatória. De fato, com o passar do tempo, a URI consultada ou a expressão regular que permite encontrar o resultado esperado podem mudar. Portanto, é recomendável colocar essas duas informações em um arquivo de configuração. Mas isso pode se revelar insuficiente. Veremos no próximo capítulo 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 e-mails. A classe .NET SmtpClient encapsula totalmente as necessidades desse tipo de cliente. O desenvolvedor não precisa conhecer os detalhes do protocolo SMTP. Nós conhecemos esse protocolo. Ele foi apresentado no parágrafo 11.4.3.
Apresentamos a classe SmtpClient no contexto de um aplicativo básico do Windows que permite enviar e-mails com anexos. O aplicativo se conectará à porta 25 de um servidor SMTP. Vale lembrar que, na maioria dos sistemas Windows, os firewalls ou outros antivírus bloqueiam as conexões com a porta 25. Portanto, é necessário desativar essa proteção para testar o aplicativo:
![]() |
O cliente SMTP terá uma arquitetura de camada única:
![]() |
O projeto do Visual Studio é o seguinte:
![]() |
A interface gráfica [SendMailForm.cs] do aplicativo é a seguinte:
![]() |
n.º | tipo | nome | função |
1 | TextBox | textBoxServeur | nome do servidor SMTP ao qual se conectar |
2 | NumericUpDown | numericUpDownPort | a porta à qual se conectar |
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 desses três campos de preenchimento receberão a mesma mensagem com os mesmos anexos. Os destinatários da mensagem poderão saber quais endereços constavam nos campos 4 e 5, mas não os do campo 6. O Bcc é, portanto, uma forma de colocar alguém em cópia sem que os outros destinatários da mensagem saibam. |
7 | Botão | buttonAjouter | para adicionar um anexo ao e-mail |
8 | ListBox | listBoxPiecesJointes | lista de anexos a serem incluídos 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 | exibe 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 arquivos local |
No exemplo anterior, o resumo exibido 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) {
// configuramos 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 obtém-se seu resultado
if (openFileDialog1.ShowDialog() == DialogResult.OK) {
// recupera-se o nome do arquivo
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) {
// removê-lo
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 “Remover”
buttonRetirer.Enabled = true;
}
}
// envio da mensagem com 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 faremos comentários sobre esse código, pois ele não apresenta novidades. Para compreender o método buttonAjouter_Click da linha 14, o leitor é convidado a reler o parágrafo 7.5.1.
O método buttonEnvoyer_Click da linha 55, que envia o e-mail, é 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 — é exibido 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) {
// exibe 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. Ele precisa de dois parâmetros: o nome do servidor SMTP e a porta na qual ele opera
- linha 8: é criada uma mensagem do tipo MailMessage. É ela que irá encapsular toda a mensagem a ser enviada.
- 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 sequência de caracteres “xx@yy.zz”. Essa sequência deve ter o formato esperado para um endereço de e-mail; caso contrário, uma exceção é lançada. Nesse caso, ela será exibida no campo textBoxResultat (linha 63) de forma pouco intuitiva.
- linhas 13-19: os endereços de e-mail dos destinatários são inseridos na lista “Para” da mensagem. Esses 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 anexados à mensagem. Cada anexo é adicionado na forma de um objeto Attachment ao campo Attachments da mensagem. Um objeto Attachment é instanciado a partir do caminho completo do arquivo a ser anexado no sistema de arquivos local.
- linha 43: a mensagem é enviada por meio do 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 ocorria no modo bloqueante, também chamado de modo síncrono:
- quando um cliente se conecta a um servidor, ele aguarda a resposta do servidor a essa solicitação antes de continuar.
- quando um cliente lê uma linha de texto enviada pelo servidor, ele fica bloqueado até que o servidor a envie.
- Do lado do servidor, os threads de serviço que atendem ao cliente funcionam da mesma maneira descrita acima.
Em interfaces gráficas, muitas vezes é necessário não bloquear o usuário durante operações demoradas. O caso frequentemente citado é o download de um arquivo grande. Durante esse download, é preciso permitir que o usuário continue interagindo livremente 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 conexão com o servidor, mas não ficará bloqueado aguardando que ela seja estabelecida
- o cliente iniciará um envio ao servidor, mas não ficará bloqueado aguardando que ele seja concluído
- o cliente iniciará a recepção de dados provenientes do servidor, mas não ficará bloqueado aguardando o término da recepção.
Vale lembrar em que nível o objeto Socket se situa na comunicação cliente/servidor TCP:
![]() |
A classe Socket é a que opera mais próximo da rede. Ela permite gerenciar a conexão de rede com precisão. O termo socket designa uma tomada elétrica. O termo foi estendido para designar uma tomada de rede virtual. Em uma comunicação TCP-IP entre duas máquinas A e B, são dois sockets que se comunicam entre si. Um aplicativo pode trabalhar diretamente com os sockets. Esse é o caso do aplicativo A mencionado acima. Um soquete pode ser um soquete client ou serveur.
11.8.2. A interface gráfica do cliente TCP assíncrono
O aplicativo do Visual Studio é o seguinte:
![]() |
[ClientTcpAsynchrone.cs] é a interface gráfica. Ela é a seguinte:
![]() |
n.º | tipo | nome | função |
1 | TextBox | textBoxNomServeur | nome do servidor TCP ao qual se conectar |
2 | NumericUpDown | numericUpDownPortServeur | a porta à qual se conectar |
3 | RadioButton | radioButtonLF radioButtonRCLF | para indicar o caractere de fim de linha que o cliente deve usar: LF "\n" ou RCLF "\r\n" |
4 | Botão | buttonConnexion | para se conectar à porta [2] do servidor [1]. O botão exibe o texto [Connecter] quando o cliente não está conectado a um servidor e [Déconnecter] quando está conectado. |
5 | TextBox | textBoxMsgToServeur | mensagem a ser enviada ao servidor assim que a conexão for estabelecida. Quando o usuário pressiona 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 exibidos os principais eventos da conexão cliente/servidor: conexão, desconexão, fechamento de fluxo, erros de comunicação |
7 | ListBox | listBoxDialogue | lista na qual são exibidas as mensagens do diálogo cliente/servidor |
8 | Botão | buttonRazEvts | para limpar a lista [6] |
4 | Botão | buttonRazDialogue | para apagar a lista [7] |
Os princípios de funcionamento desta interface são os seguintes:
- o usuário conecta seu cliente TCP gráfico a um serviço TCP por meio de [1, 2, 3, 4].
- Um thread assíncrono recebe continuamente todos os dados enviados pelo servidor TCP e os exibe na lista [7]. Esse thread é independente das demais atividades da interface.
- O usuário pode enviar mensagens ao servidor no seu próprio ritmo por meio de [5]. Cada mensagem é enviada por um thread assíncrono. Ao contrário do thread de recepção, que nunca é interrompido, o thread de transmissão é encerrado assim que a mensagem é enviada. Um novo thread assíncrono será utilizado para a mensagem seguinte.
- A comunicação cliente/servidor é encerrada quando um dos participantes encerra a conexão. O usuário pode tomar essa iniciativa com o botão [4], que, uma vez estabelecida a conexão, exibe a legenda [Déconnecter].
Aqui está uma captura de tela de uma execução:
![]() |
- em [1]: conexão a um serviço POP
- em [2]: exibição dos eventos ocorridos durante a conexão
- em [3]: a mensagem enviada pelo servidor POP ao final da conexã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 conexão
- em [2], esse encerramento do lado do servidor foi detectado. O cliente, então, encerrou a conexão do seu lado.
- em [3], o botão [Déconnecter] voltou a ser um botão [Connecter]
11.8.3. Conexão assíncrona com o servidor
Ao clicar no botão [Connecter], o seguinte método é executado:
private void buttonConnexion_Click(object sender, EventArgs e) {
// conectar ou desconectar?
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 conexã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);
// conexã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 conexão foi estabelecida
private void connecté(IAsyncResult résultat) {
// recuperando o soquete 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.
Vários dados devem ser compartilhados entre diversos métodos do formulário. São eles:
- linha 7: client é o socket de comunicação com o servidor
- linhas 6 e 8: o cliente receberá suas mensagens em um 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 padrão como RCLF, mas pode ser alterado pelo usuário por meio dos botões de opção [3].
O procedimento connexion da linha 19 realiza a conexão com o servidor TCP:
- linhas 21-25: verifica-se se o nome do servidor não está vazio. Caso contrário, o evento é registrado em listBoxEvts pelo método logEvent da linha 49.
- linha 27: sinaliza-se que a conexão será estabelecida
- 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 conexões TCP/IP
- ProtocolType protocolType: o tipo de protocolo de internet utilizado; neste caso, o protocolo TCP
- linha 32: a conexão é estabelecida de forma assíncrona. A conexão é iniciada, mas a execução continua sem aguardar seu término. O método [Socket].BeginConnect aceita quatro parâmetros:
- IPAddress ipAddress: o endereço IP da máquina na qual está sendo executado o serviço ao qual é necessário se conectar
- Int32 port: a porta do serviço
- AsyncCallBack asyncCallBack: AsyncCallBack é um tipo delegado:
O método asyncCallBack, passado como terceiro parâmetro do método BeginConnect, deve ser um método que aceite um tipo IAsyncCallBack e não retorne nenhum resultado. Esse é o método que será chamado assim que a conexão for estabelecida. Aqui, passamos como terceiro parâmetro o método connecté da linha 41.
- (continuação)
- Objeto state: um objeto a ser passado para o método asyncCallBack. Esse 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 é encerrado. O usuário pode voltar a interagir com a interface gráfica. A conexão ocorre em segundo plano, paralelamente ao gerenciamento dos eventos da interface gráfica. Ainda paralelamente, o método connecté da linha 41 será chamado ao final da conexão, independentemente de ela ter sido bem-sucedida ou não.
O código do método connecté é o seguinte:
// a conexão foi estabelecida
private void connecté(IAsyncResult résultat) {
// recuperando o 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. Vale lembrar que esse objeto é o mesmo passado como quarto parâmetro do método BeginConnect.
- linha 7: a tentativa de conexão é encerrada pelo método EndConnect, ao qual deve-se passar o parâmetro résultat recebido pelo método.
- linha 9: o evento é registrado na lista de eventos
- linha 11: o botão [Connecter] passa a ser um botão [Déconnecter] para que o usuário possa solicitar o logout.
- linha 13: a resposta do servidor é inicializada. Ela será atualizada por chamadas repetidas ao método assíncrono BeginReceive.
- linha 14: primeira chamada ao método assíncrono BeginReceive. Este é chamado com os seguintes parâmetros:
- byte[] buffer: o buffer no qual serão colocados os dados a serem recebidos — aqui, o buffer é data
- int offset: a partir de qual posição do buffer os dados a serem recebidos devem ser colocados — aqui, 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 bytes do buffer — aqui, o tamanho é tailleBuffer.
- SocketFlags socketFlags: configuração do soquete — neste caso, nenhuma configuração
- AsyncCallBack asyncCallBack: o método a ser chamado quando a recepção for concluída. Isso ocorrerá seja porque o buffer recebeu dados, seja porque a conexão foi encerrada. Aqui, o método de retorno é o método lecture da linha 22.
- Objeto state: o objeto a ser passado para o método de retorno asyncCallBack. Aqui, passa-se novamente o socket do cliente.
Observe-se que tudo isso ocorre sem nenhuma ação do usuário, além da solicitação inicial de conexão com o botão [Connecter]. Ao final do método connecté, outro método é executado em segundo plano: o método lecture, que examinaremos agora.
// recepção de dados
private void lecture(IAsyncResult résultat) {
// recuperação do soquete 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 não responde mais
logEvent("le serveur a fermé la connexion");
}
} catch (Exception e) {
// ocorreu um problema na recepção
logEvent(String.Format("erreur de réception : {0}", e.Message));
erreur = true;
}
// concluído?
if (nbOctetsReçus == 0 || erreur) {
// desconectamos o cliente, se necessário
déconnexion();
// exibimos o fim da resposta
afficherRéponseServeur(réponse, true);
// fim da leitura
return;
}
// recuperamos 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 a leitura
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 conexão é encerrada pelo servidor.
- linha 9: a solicitação assíncrona de leitura é encerrada pelo método EndReceive. Novamente, esse método deve ser chamado com o parâmetro recebido pela função de retorno de chamada. O método EndReceive retorna o número de bytes recebidos no buffer de leitura.
- linha 10: se o número de bytes for zero, significa que a conexão foi encerrada pelo servidor.
- linha 12: registra-se o evento na lista de eventos
- linha 14: trata-se de uma eventual exceção
- linhas 16-17: registra-se o evento na lista de eventos e registra-se o erro
- linha 20: verifica-se se é necessário fechar a conexão
- linha 22: fecha-se a conexão do lado do cliente com o método déconnexion, que veremos mais adiante.
- linha 24: a resposta do servidor, c.a.d. A variável global réponse é exibida na lista de diálogo listBoxDialogue por meio de um método privado afficherRéponseServeur.
- linha 26: fim do método assíncrono lecture
- linha 29: os bytes recebidos são inseridos em uma string no formato UTF8.
- linha 31: eles são adicionados à resposta que está sendo construída
- linha 33: a resposta é exibida na lista listBoxDialogue.
- linha 35: retoma-se a espera por dados provenientes do servidor
Em suma, o método assíncrono lecture nunca termina. De forma contínua, ele lê os dados provenientes do servidor e os exibe na lista listBoxDialogue. Ele só termina quando a conexão é encerrada, seja pelo servidor, seja pelo próprio usuário.
11.8.4. Desconexão do servidor
Ao clicar no botão [Déconnecter], o seguinte método é executado:
private void buttonConnexion_Click(object sender, EventArgs e) {
// conexão ou desconexão?
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 déconnexion garante o desligamento do cliente:
private void déconnexion() {
// fechamento do socket
if (client != null && client.Connected) {
try {
// acompanhamento
logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
// desconexão
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 conectado
- linha 6: a desconexão é anunciada em listBoxEvts. A propriedade client.RemoteEndPoint fornece o par (endereço IP, porta) da outra extremidade da conexão, c.a.d, neste caso, do servidor.
- linha 8: o fluxo de dados do soquete é fechado com o método ShutDown. O fluxo de dados de um soquete é bidirecional: o soquete transmite e recebe dados. O parâmetro do método ShutDown pode ser: ShutDown.Receive para fechar o fluxo de recepção, Shutdonw.Send para fechar o fluxo de transmissão ou ShutDown.Both para fechar ambos os fluxos.
- linha 9: libera-se os recursos associados ao soquete
- linha 11: o botão [Déconnecter] passa a ser o botão [Connecter]
- linhas 12-15: tratamento de uma possível exceção
11.8.5. Envio assíncrono de dados ao servidor
Quando o usuário confirma a mensagem do campo textBoxMsgToServeur, o seguinte método é executado:
private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
// tecla [Entrée]?
if (e.KeyChar == 13 && client.Connected) {
envoyerMessage();
}
}
- linhas 3-5: se o usuário tiver pressionado a tecla [Entrée] e se o soquete do cliente estiver conectado, 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);
// ela 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 insere-se na matriz de bytes message.
- linha 6: uma transmissão assíncrona é iniciada com o método BeginSend. Os parâmetros do método BeginSend são idênticos aos do método BeginReceive. Ao final da operação de envio assíncrono da mensagem, o método écriture será chamado.
- 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. Esse socket era o 5º parâmetro do método BeginSend.
- linha 5: encerra-se a operação assíncrona de envio.
Não se aguarda o término da transmissão de uma mensagem para devolver o controle ao usuário. Assim, o usuário pode enviar uma segunda mensagem mesmo que a transmissão da primeira ainda não tenha sido concluída.
11.8.6. Exibição dos eventos e do diálogo cliente/servidor
Os eventos são exibidos 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 ser adicionada à lista listBoxEvts.
- linha 3: não é possível utilizar diretamente o componente listBoxEvents. De fato, o método logEvents é chamado por dois tipos de threads:
- o thread principal, proprietário da interface gráfica, por exemplo, quando sinaliza que uma tentativa de conexão está em andamento
- um thread secundário responsável por uma operação assíncrona. Esse tipo de thread não é proprietário dos componentes e seu acesso a um componente C deve ser controlado por uma operação C.Invoke. Essa operação indica ao controle C que um thread deseja realizar uma operação nele. O método Invoke aceita dois parâmetros:
- uma função de retorno do tipo delegate. Essa função de retorno será executada pela thread proprietária da interface gráfica e não pela thread que executa o método C.Invoke.
- um objeto que será passado para a 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 possui um parâmetro do tipo string e não retorna nenhum resultado. O parâmetro será a mensagem a ser registrada em listBoxEvts.
Na linha 3, o primeiro parâmetro passado para o método Invoke é o método logEventCallBack da linha 6. Ele corresponde corretamente à 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 controle execute o método de retorno de chamada.
- linha 6: o método de retorno executado pela thread da interface gráfica recebe a mensagem a ser exibida no controle listBoxEvts.
- linha 9: o evento é registrado na primeira posição da lista, de modo 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 do método logEvent.
As mensagens recebidas pelo cliente são exibidas pelo método afficherRéponseServeur:
private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}
O primeiro parâmetro é a mensagem a ser exibida. Essa mensagem pode consistir em várias linhas. De fato, o cliente lê os dados provenientes do servidor em blocos de tailleBuffer (1024) bytes. Nesses 1024 bytes, podem ser encontradas várias linhas, reconhecíveis por seu caractere de fim de linha “\n”. A última linha pode estar incompleta, com seu caractere de fim de linha localizado nos 1024 bytes seguintes. O método identifica na mensagem as linhas que terminam com “\n” e, em seguida, solicita ao logDialogue que as exiba. O segundo parâmetro do método indica se a última linha encontrada deve ser exibida ou se deve permanecer no buffer para ser completada pela mensagem seguinte. O código é bastante complexo e não apresenta interesse neste contexto. Portanto, não será comentado.
11.8.7. Conclusão
O mesmo exemplo poderia ser tratado com operações síncronas. Aqui, o aspecto assíncrono da interface gráfica agrega pouco ao usuário. No entanto, se ele se conectar e, em seguida, perceber que o servidor “não responde mais”, ele tem a possibilidade de se desconectar, graças ao fato de que a interface gráfica continua respondendo aos eventos durante a execução das operações assíncronas. Este exemplo bastante complexo nos permitiu apresentar novos conceitos:
- o uso de sockets
- o uso de métodos assíncronos. O que foi abordado faz parte de um padrão. Existem outros métodos assíncronos que funcionam seguindo o mesmo modelo.
- a atualização de controles de uma interface gráfica por meio de 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 seus clientes por meio de threads secundárias. Se seu pool de threads tiver N threads, isso significa que ele só pode atender N clientes simultaneamente. Se todas as N threads estiverem realizando uma operação bloqueante (síncrona), não haverá mais threads disponíveis para um novo cliente até que uma das operações bloqueantes seja concluída e libere 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. Aplicativo de exemplo, versão 8: Servidor de cálculo de impostos
11.9.1. A arquitetura da nova versão
Retomamos o aplicativo de cálculo de impostos já abordado de diversas formas. Recordemos sua última versão, a da versão 7 do parágrafo 9.8.
![]() |
Os dados estavam em um banco de dados e a camada [ui] era uma interface gráfica:
![]() |
Vamos retomar essa arquitetura e distribuí-la por duas máquinas:
![]() |
- uma máquina [serveur] hospedará as camadas [metier] e [dao] da versão 7. Uma camada TCP/IP [serveur] [1] será criada para permitir que clientes da Internet consultem o serviço de cálculo de impostos.
- Uma máquina [client] hospedará a camada [ui] da versão 7. Uma camada TCP/IP [client] [2] será criada para permitir que a camada [ui] consulte o serviço de cálculo de impostos.
A arquitetura sofre uma mudança profunda aqui. A versão 7 era um aplicativo Windows para um único usuário. A versão 8 passa a ser um aplicativo cliente/servidor da Internet. O servidor poderá atender a vários clientes simultaneamente.
Vamos, em primeiro lugar, escrever a parte [serveur] do aplicativo.
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 cálculo de impostos na forma de um aplicativo de console.
- [dbimpots.sdf]: o banco de dados SQL Server Compact da versão 7, descrito no parágrafo 9.8.5.
- [App.config]: o arquivo de configuração do aplicativo.
- Na pasta [2], a pasta [lib] contém os arquivos 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 do aplicativo
O arquivo [App.config] é utilizado pelo Spring. 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 arquivo de configuração utilizado na camada [ui] da versão 7. Ele foi apresentado no parágrafo 9.8.4.
11.9.2.3. Funcionamento do servidor
Ao iniciar o servidor, o aplicativo servidor instancia as camadas [metier] e [dao] e, em seguida, exibe uma interface de console de administração:
O console de administração aceita os seguintes comandos:
para iniciar o serviço em uma porta específica | |
para interromper o serviço. Ele pode ser reiniciado posteriormente na mesma porta ou em outra. | |
para ativar o eco do diálogo cliente/servidor no console | |
para desativar o eco | |
para exibir o status ativo/inativo do serviço | |
para sair do aplicativo |
Vamos iniciar o servidor:
Vamos agora executar o cliente TCP gráfico assíncrono analisado anteriormente no parágrafo 11.8.

O cliente está conectado. Ele pode enviar os seguintes comandos ao servidor de cálculo de impostos:
para obter a lista de comandos permitidos | |
para calcular o imposto de alguém que tenha nbEnfants filhos e um salário de salaireAnnuel euros. marié é igual a o se a pessoa for casada, n caso contrário. | |
para encerrar a conexão com o servidor |
Aqui está um exemplo de diálogo:
![]() |
No lado do servidor, o console exibe o seguinte:
Vamos ativar o eco e reiniciar uma nova sessão a partir do cliente gráfico:
![]() |
O console de administração exibe então o seguinte:
- linha 1: o eco do diálogo cliente/servidor está ativado
- linha 2: chegou um cliente
- linha 3: ele enviou o comando [aide]
- linhas 4-7: a resposta do servidor em 4 linhas.
Vamos interromper o serviço:
- linha 1: solicitamos a interrupção do serviço (não da própria aplicação)
- linha 2: uma exceção devido ao fato de que o servidor, que estava bloqueado em uma espera pelo cliente, foi abruptamente interrompido devido ao encerramento do serviço de escuta.
- linha 3: o serviço agora pode ser reiniciado por “start port” ou encerrado por quit.
Antes de o serviço de escuta ser encerrado, um cliente estava sendo atendido em outra conexão. Essa conexão não é fechada pelo encerramento do soquete de escuta. O cliente pode continuar a enviar comandos: o thread de serviço que estava associado a ele antes do encerramento do serviço de escuta continua a responder a ele:

11.9.3. O código do servidor TCP para cálculo de impostos
![]() |
1 ![]() |
O código do servidor [ServeurImpot.cs] é o seguinte:
...
namespace Chap9 {
public class ServeurImpot {
// dados compartilhados 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 pool de threads
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// lê os comandos de administração do servidor digitados no teclado em um loop infinito
string commande = null;
string[] champs = null;
while (true) {
// prompt
Console.Write("Serveur de calcul d'impôt >");
// leitura de 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 {
// iniciando 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":
// saindo do aplicativo
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 pool de threads do aplicativo com um mínimo e um máximo de 10 threads.
- linhas 30-86: o loop 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, cujo objetivo é iniciar o serviço de escuta
- linha 40: se o serviço já estiver ativo, não há nada a ser feito
- linha 45: verifica-se se a porta está presente e correta. Se estiver, a variável global port da linha 7 é definida.
- linha 49: o serviço de escuta será gerenciado por um thread secundário para que o thread principal possa continuar executando os comandos do console. Se o método doEcoute estabelecer a conexã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 no console
- 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: parada do serviço de escuta
- linhas 68-75: o comando status, que exibe o status ativo/inativo do serviço
- linhas 76-80: o comando quit, que encerra tudo.
O thread responsável por escutar as solicitações dos clientes executa o seguinte método doEcoute:
private static void doEcoute(Object data) {
// thread de escuta de solicitações dos clientes
try {
// criando 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) {
// aguardando um cliente
tcpClient = service.AcceptTcpClient();
// o atendimento é realizado 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; } // conexã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. Comentaremos apenas o que é diferente:
- linha 7: o serviço de atendimento foi lançado
- linha 9: observa-se que o serviço agora está 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) {
// identifica-se 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);
// análise da conexã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 sem 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 solicitação/gravação de resposta
string demande = null;
bool serviceFini = false;
while (!serviceFini && (demande = reader.ReadLine()) != null) {
// monitoramento do console
if (echo) {
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
}
// análise da solicitação
demande = demande.Trim().ToLower();
// solicitação vazia?
if (demande.Length == 0) {
// solicitação inválida
writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
return;
}
// decompondo a 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 no console?
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. Comentaremos apenas o que é diferente:
- linha 15: assim que o cliente se conecta, o servidor envia a ele uma mensagem de boas-vindas.
- linhas 19-52: o loop de leitura dos comandos do cliente. O loop é interrompido quando o cliente envia o comando “aurevoir”.
- linha 27: caso de um comando vazio
- linha 34: a solicitação é dividida em campos para ser analisada
- linha 37: comando aide: o cliente solicita a lista de comandos autorizados
- linha 40: comando impot: o cliente solicita um cálculo de imposto. Respondemos com a mensagem retornada pelo método calculImpot, que detalharemos em breve.
- linha 44: comando aurevoir: o cliente indica que concluiu a operação.
- linha 45: nos preparamos para sair do ciclo de leitura das solicitações do cliente (linhas 19-52)
- linha 46: respondemos ao cliente com uma mensagem de despedida
- linha 48: um comando incorreto. Enviamos ao cliente uma mensagem de erro.
O processamento do comando impot é realizado pelo seguinte método calculImpot:
private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
// solicitação de cálculo para 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.";
}
// Tudo certo — 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, ele terá o formato “impost marié nbEnfants salaireAnnuel”. O método retorna como resultado a resposta a ser enviada ao cliente.
- linha 4: verifica-se se o comando possui 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]. Vale lembrar que essa camada está encapsulada em uma DLL.
- linha 26: se a camada [metier] retornou um resultado, este é retornado ao cliente.
- linha 28: se a camada [metier] tiver lançado uma exceção, a mensagem da exceção é enviada 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 do aplicativo
- em [2], o cliente TCP que atua como camada [metier] para a camada [ui]. Portanto, utilizaremos os dois termos.
- em [3], a camada [ui] da versão 7, com um pequeno detalhe sobre o qual falaremos
11.9.4.2. A camada [metier]
A interface IImpotMetier não sofreu alterações. Continua sendo a mesma da versão 7:
namespace Metier {
public interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
A implementação dessa 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) {
// conectando-se 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 sem buffer
writer.AutoFlush = true;
// pular a mensagem de boas-vindas
reader.ReadLine();
// solicitação
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
- essas duas propriedades serão inicializadas pelo Spring durante a instanciação da classe [ImpotMetierTcp].
- linha 11: o método de cálculo do imposto. Quando ele é executado, as propriedades Serveur e Port já estão inicializadas. No código, vemos a abordagem clássica de um cliente TCP
- linha 13: a conexão com o servidor é aberta
- linhas 14-16: recupera-se (linha 14) o fluxo de rede associado a essa conexão, do qual se extrai um fluxo de leitura (linha 15) e um fluxo de gravação (linha 16).
- linha 18: o fluxo de gravação deve ser não-bufferizado
- linha 20: aqui, é preciso lembrar que, ao abrir a conexão, o servidor envia ao cliente uma primeira linha, que é a mensagem de boas-vindas “Bem-vindo ao servidor de cálculo de impostos”. Essa mensagem é lida e ignorada.
- linha 22: envia-se ao servidor o comando do tipo: imposto o 2 60000 para solicitar que ele 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 no formato “4282” ou com uma mensagem de erro caso o comando esteja incorreto (o que não acontecerá neste caso) ou se houver algum problema no cálculo do imposto. Aqui, esse último caso não é tratado, mas certamente teria sido mais “limpo” fazê-lo. De fato, se a linha lida for uma mensagem de erro, uma exceção será lançada porque a conversão para um inteiro falhará. A exceção capturada pela interface gráfica será um erro de conversão, embora a exceção original seja de natureza totalmente diferente. O leitor é convidado a melhorar esse código.
- linhas 25-28: liberação de todos os recursos utilizados com uma cláusula “using”.
A camada [metier] é compilada na DLL ImpotsV8-metier.dll:

11.9.4.3. A camada [ui]
![]() |
A camada [ui] [1,3] é a mesma analisada na versão 7, no parágrafo 9.8.4, com exceção de três detalhes:
- a configuração da camada [metier] na [App.config] é diferente, pois sua implementação foi alterada
- a interface gráfica [Form1.cs] foi modificada para exibir uma eventual exceção
- a camada [metier] está na DLL [ImpotsV8-metier.dll].
O arquivo [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 operará na porta 27.
A interface gráfica apresentada ao usuário é a seguinte:
![]() |
- no [1], foi adicionado um TextBox para exibir uma eventual exceção. Esse campo não existia na versão anterior.
Além desse detalhe, o código do formulário é o mesmo já analisado no parágrafo 6.4.3. Recomenda-se ao leitor que consulte esse parágrafo. Em [2], vemos um exemplo de execução obtido com um servidor iniciado da seguinte maneira:
A captura de tela [2] do cliente corresponde às linhas do cliente 9 acima.
11.9.5. Conclusão
Mais uma vez, conseguimos reutilizar o código existente, sem modificações (camadas [metier] e [dao] do servidor) ou com muito poucas modificações (camada [ui] do cliente). Isso foi possível graças ao nosso uso sistemático de interfaces e à instânciação dessas interfaces com o Spring. Se, na versão 7, tivéssemos colocado o código de negócio diretamente nos manipuladores de eventos da interface gráfica, esse código de negócio não teria sido reutilizável. Essa é a principal desvantagem das arquiteturas de camada única.
Por fim, vale ressaltar que a camada [ui] não tem conhecimento de que é um servidor remoto que calcula o valor do imposto para ela.























































