Skip to content

11. Programación en Internet

11.1. Généralités

11.1.1. Los protocolos de Internet

Aquí ofrecemos una introducción a los protocolos de comunicación de Internet, también conocidos como el conjunto de protocolos TCP/IP (Protocolo de Control de Transmisión / Protocolo de Internet), en referencia a los dos protocolos principales. Puede ser útil que el lector tenga una comprensión general del funcionamiento de las redes y, en particular, de los protocolos TCP/IP antes de abordar la construcción de aplicaciones distribuidas. El texto que sigue es una traducción parcial de un texto que se encuentra en el documento «Lan Workplace for Dos - Administrator's Guide» de NOVELL, un documento de principios de los años 90.


El concepto general de crear una red de computadoras heterogéneas surge de investigaciones realizadas por la DARPA (Agencia de Proyectos de Investigación Avanzada de Defensa) en Estados Unidos. La DARPA desarrolló el conjunto de protocolos conocido como TCP/IP, que permite que máquinas heterogéneas se comuniquen entre sí. Estos protocolos se probaron en una red llamada ARPAnet, red que posteriormente se convirtió en la red INTERNET. Los protocolos TCP/IP definen formatos y reglas de transmisión y recepción independientes de la organización de las redes y del equipo utilizado.

La red diseñada por el DARPA y administrada por los protocolos TCP/IP es una red de conmutación de paquetes. Este tipo de red transmite la información por la red en pequeños fragmentos llamados paquetes. Así, si una computadora transmite un archivo grande, este se dividirá en pequeños fragmentos que se enviarán por la red para ser recomponidos en el destino. TCP/IP define el formato de estos paquetes, a saber:

  • origen del paquete
  • destino
  • longitud
  • tipo

11.1.2. El modelo OSI

Los protocolos TCP/IP siguen aproximadamente el modelo de red abierta denominado OSI (Modelo de Referencia de Interconexión de Sistemas Abiertos) definido por la ISO (Organización Internacional de Normalización). Este modelo describe una red ideal en la que la comunicación entre máquinas puede representarse mediante un modelo de siete capas:

Cada capa recibe servicios de la capa inferior y ofrece los suyos a la capa superior. Supongamos que dos aplicaciones ubicadas en máquinas A y B diferentes desean comunicarse: lo hacen a nivel de la capa Application. No necesitan conocer todos los detalles del funcionamiento de la red: cada aplicación entrega la información que desea transmitir a la capa inferior: la capa Présentation. Por lo tanto, la aplicación solo tiene que conocer las reglas de interfaz con la capa Présentation.

Una vez que la información se encuentra en la capa Présentation, se transfiere, siguiendo otras reglas, a la capa Session y así sucesivamente, hasta que la información llega al soporte físico y se transmite físicamente a la máquina de destino. Allí, se someterá al proceso inverso al que se le aplicó en la máquina de origen.

En cada capa, el proceso emisor encargado de enviar la información la envía a un proceso receptor en la otra máquina que pertenece a la misma capa que él. Lo hace siguiendo ciertas reglas que se denominan el protocolo de la capa. Por lo tanto, tenemos el siguiente esquema de comunicación final:

La función de las diferentes capas es la siguiente:

Physique
Asegura la transmisión de bits a través de un medio físico. En esta capa se encuentran equipos terminales de procesamiento de datos (E.T.T.D), tales como terminales u computadoras, así como equipos de terminación de circuitos de datos (E.T.C.D), tales como moduladores/demoduladores, multiplexores y concentradores. Los puntos de interés en este nivel son:
  • la elección de la codificación de la información (analógica o digital)
  • la elección del modo de transmisión (sincrónico o asincrónico).
Liaison de données
Oculta las características físicas de la capa física. Detecta y corrige los errores de transmisión.
Réseau
Administra la ruta que debe seguir la información enviada por la red. A esto se le llama routage: determinar la ruta que debe seguir la información para que llegue a su destinatario.
Transport
Permite la comunicación entre dos aplicaciones, mientras que las capas anteriores solo permitían la comunicación entre máquinas. Un servicio que brinda esta capa puede ser el multiplexado: la capa de transporte puede utilizar una misma conexión de red (de máquina a máquina) para transmitir información perteneciente a varias aplicaciones.
Session
En esta capa se encuentran servicios que permiten a una aplicación abrir y mantener una sesión de trabajo en una máquina remota.
Présentation
Su objetivo es estandarizar la representación de los datos en las diferentes máquinas. Así, los datos procedentes de una máquina A serán «formateados» por la capa Présentation de la máquina A, según un formato estándar, antes de ser enviados a la red. Al llegar a la capa Présentation de la máquina destinataria B, que los reconocerá gracias a su formato estándar, se les dará otro formato para que la aplicación de la máquina B los reconozca.
Application
En este nivel se encuentran las aplicaciones que suelen estar más cerca del usuario, como el correo electrónico o la transferencia de archivos.

11.1.3. El modelo TCP/IP

El modelo OSI es un modelo ideal que aún no se ha implementado. El conjunto de protocolos TCP/IP se acerca a él de la siguiente manera:

Capa física

En una red local, por lo general se utiliza tecnología Ethernet o Token-Ring. Aquí solo presentaremos la tecnología Ethernet.

Ethernet

Es el nombre que se le da a una tecnología de redes locales de conmutación de paquetes inventada en PARC Xerox a principios de la década de 1970 y estandarizada por Xerox, Intel y Digital Equipment en 1978. La red está compuesta físicamente por un cable coaxial de aproximadamente 1,27 cm de diámetro y una longitud máxima de 500 m. Se puede extender mediante répéteurs, sin que dos equipos puedan estar separados por más de dos repetidores. El cable es pasivo: todos los componentes activos se encuentran en las máquinas conectadas al cable. Cada máquina está conectada al cable mediante una tarjeta de acceso a la red que incluye:

  • un transmisor (transceiver) que detecta la presencia de señales en el cable y convierte las señales analógicas en digitales y viceversa.
  • un acoplador que recibe las señales digitales del transmisor y las transmite a la computadora para su procesamiento, o viceversa.

Las características principales de la tecnología Ethernet son las siguientes:

  • Capacidad de 10 megabits por segundo.
  • Topología de bus: todas las máquinas están conectadas al mismo cable
  • Red de difusión: una máquina emisora transmite información por el cable junto con la dirección de la máquina destinataria. Todas las máquinas conectadas reciben entonces esta información y solo la máquina a la que va dirigida la conserva.
  • El método de acceso es el siguiente: el transmisor que desea transmitir escucha el cable; así detecta si hay o no una onda portadora, cuya presencia indicaría que hay una transmisión en curso. Esta es la técnica CSMA (Carrier Sense Multiple Access). Si no hay portadora, un transmisor puede decidir transmitir a su vez. Puede haber varios que tomen esta decisión. Las señales emitidas se mezclan: se dice que hay una colisión. El transmisor detecta esta situación: al mismo tiempo que transmite por el cable, escucha lo que realmente está pasando por él. Si detecta que la información que transita por el cable no es la que él mismo transmitió, deduce que hay una colisión y dejará de transmitir. Los demás transmisores que estaban transmitiendo harán lo mismo. Cada uno reanudará su transmisión después de un tiempo aleatorio que depende de cada transmisor. Esta técnica se denomina CD (Detección de colisión). El método de acceso se denomina así CSMA/CD.
  • un direccionamiento de 48 bits. Cada máquina tiene una dirección, denominada aquí dirección física, que está grabada en la tarjeta que la conecta al cable. A esta dirección se le llama la dirección Ethernet de la máquina.

Capa de red

En esta capa encontramos los protocolos IP, ICMP, ARP y RARP.

IP (Internet Protocol)
Transmite paquetes entre dos nodos de la red
ICMP 
(Internet Control Message Protocol)
ICMP establece la comunicación entre el programa del protocolo IP de una máquina y el de otra máquina. Por lo tanto, es un protocolo de intercambio de mensajes dentro del propio protocolo IP.
ARP
(Address Resolution Protocol)
establece la correspondencia entre la dirección de Internet de la máquina y la dirección física de la máquina
RARP
(Reverse Address Resolution Protocol)
establece la correspondencia entre la dirección física de la máquina y la dirección de Internet de la máquina

Capas de transporte/sesión

En esta capa se encuentran los siguientes protocolos:

TCP (Transmission Control Protocol)
Garantiza una entrega confiable de información entre dos clientes
UDP (User Datagram Protocol)
Garantiza una entrega no confiable de información entre dos clientes

Capas de aplicación/presentación/sesión

Aquí se encuentran diversos protocolos:

TELNET
Emulador de terminal que permite que una máquina A se conecte a una máquina B como terminal
FTP (File Transfer Protocol)
permite la transferencia de archivos
TFTP (Trivial File Transfer Protocol)
permite la transferencia de archivos
SMTP (Simple Mail Transfer protocol)
permite el intercambio de mensajes entre usuarios de la red
DNS (Domain Name System)
convierte un nombre de máquina en la dirección de Internet de la máquina
XDR (eXternal Data Representation)
creado por Sun MicroSystems, especifica una representación estándar de los datos, independiente de las máquinas
RPC(Remote Procedures Call)
definido también por Sun, es un protocolo de comunicación entre aplicaciones remotas, independiente de la capa de transporte. Este protocolo es importante: libera al programador de la necesidad de conocer los detalles de la capa de transporte y hace que las aplicaciones sean portables. Este protocolo se basa en el protocolo XDR
NFS (Network File System)
también definido por Sun; este protocolo permite que una máquina «vea» el sistema de archivos de otra máquina. Se basa en el protocolo RPC mencionado anteriormente

11.1.4. Funcionamiento de los protocolos de Internet

Las aplicaciones desarrolladas en el entorno TCP/IP suelen utilizar varios de los protocolos de este entorno. Un programa de aplicación se comunica con la capa más alta de los protocolos. Esta transmite la información a la capa inferior y así sucesivamente hasta llegar al soporte físico. Allí, la información se transfiere físicamente a la máquina destinataria, donde volverá a atravesar las mismas capas, esta vez en sentido inverso, hasta llegar a la aplicación destinataria de la información enviada. El siguiente diagrama muestra el recorrido de la información:

Tomemos un ejemplo: la aplicación FTP, definida en el nivel de la capa Application y que permite la transferencia de archivos entre máquinas.

  • La aplicación entrega una secuencia de bytes para que se transmita a la capa transport.
  • La capa transport divide esta secuencia de bytes en segments y TCP, y agrega al inicio de cada segmento su número correspondiente. Los segmentos se pasan a la capa de red, que se rige por el protocolo IP.
  • La capa IP crea un paquete que encapsula el segmento TCP recibido. Al principio de este paquete, coloca las direcciones de Internet de las máquinas de origen y destino. También determina la dirección física de la máquina destinataria. Todo esto se pasa a la capa de enlace de datos y enlace físico, es decir, a la tarjeta de red que conecta la máquina a la red física.
  • Allí, el paquete IP se encapsula a su vez en una trama física y se envía a su destinatario por el cable.
  • En la máquina destinataria, la capa de enlace de datos y enlace físico hace lo contrario: desencapsula el paquete IP de la trama física y lo pasa a la capa IP.
  • La capa IP verifica que el paquete sea correcto: calcula una suma basada en los bits recibidos (checksum), suma que debe coincidir con la que figura en la cabecera del paquete. Si no es así, el paquete se rechaza.
  • Si el paquete se considera correcto, la capa IP desencapsula el segmento TCP que contiene y lo pasa a la capa superior, la transport.
  • La capa transport —la capa TCP en nuestro ejemplo— examina el número del segmento para restablecer el orden correcto de los segmentos.
  • También calcula una suma de verificación para el segmento TCP. Si se verifica que es correcto, la capa TCP envía un acuse de recibo a la máquina de origen; de lo contrario, se rechaza el segmento TCP.
  • A la capa TCP solo le queda transmitir la parte de datos del segmento a la aplicación destinataria de los mismos en la capa superior.

11.1.5. Los problemas de direccionamiento en Internet

Un noeud de una red puede ser una computadora, una impresora inteligente, un servidor de archivos, en realidad cualquier dispositivo que pueda comunicarse mediante los protocolos TCP/IP. Cada nodo tiene una dirección física cuyo formato depende del tipo de red. En una red Ethernet, la dirección física se codifica en 6 bytes. Una dirección de una red X25 es un número de 14 dígitos.

La dirección de Internet de un nodo es una dirección lógica: es independiente del equipo y de la red que se utilice. Es una dirección de 4 bytes que identifica tanto a una red local como a un nodo de dicha red. La dirección de Internet suele representarse en forma de 4 números, que son los valores de los 4 bytes, separados por un punto. Así, la dirección de la computadora Lagaffe de la Facultad de Ciencias de Angers se escribe como 193.49.144.1 y la de la computadora Liny como 193.49.144.9. De ello se deduce que la dirección de Internet de la red local es 193.49.144.0. Esta red puede tener hasta 254 nodos.

Dado que las direcciones de Internet o direcciones IP son independientes de la red, una máquina de la red A puede comunicarse con una máquina de la red B sin preocuparse por el tipo de red en la que se encuentra: basta con que conozca su dirección IP. El protocolo IP de cada red se encarga de realizar la conversión entre la dirección IP y la dirección física, en ambos sentidos.

Todas las direcciones IP deben ser diferentes. En Francia, es el INRIA el que se encarga de asignar las direcciones IP. De hecho, este organismo asigna una dirección para su red local, por ejemplo, 193.49.144.0 para la red de la Facultad de Ciencias de Angers. El administrador de esta red puede luego asignar las direcciones IP 193.49.144.1 a 193.49.144.254 como lo considere conveniente. Esta dirección suele registrarse en un archivo específico de cada equipo conectado a la red.

11.1.5.1. Las clases de direcciones IP

Una dirección IP es una secuencia de 4 bytes que a menudo se escribe como I1.I2.I3.I4, y que en realidad contiene dos direcciones:

  • la dirección de la red
  • la dirección de un nodo de esa red

Según el tamaño de estos dos campos, las direcciones IP se dividen en tres clases: clases A, B y C.

Clase A

La dirección IP: I1.I2.I3.I4 tiene la forma R1.N1.N2.N3, donde

R1 es la dirección de la red

N1.N2.N3 es la dirección de una máquina en esa red

Más precisamente, la forma de una dirección IP de clase A es la siguiente:

La dirección de red ocupa 7 bits y la dirección del nodo, 24 bits. Por lo tanto, pueden existir 127 redes de clase A, cada una con hasta 2²⁴ nodos.

Clase B

En este caso, la dirección IP: I1.I2.I3.I4 tiene el formato R1.R2.N1.N2, donde

R1.R2 es la dirección de la red

N1.N2 es la dirección de una máquina en esa red

Más precisamente, la forma de una dirección IP de clase B es la siguiente:

La dirección de la red ocupa 2 bytes (14 bits exactamente), al igual que la del nodo. Por lo tanto, pueden existir 2¹⁴ redes de clase B, cada una con hasta 2¹⁶ nodos.

Clase C

En esta clase, la dirección IP: I1.I2.I3.I4 tiene la forma R1.R2.R3.N1, donde

R1.R2.R3 es la dirección de la red

N1 es la dirección de una máquina en esa red

Más precisamente, la forma de una dirección IP de clase C es la siguiente:

La dirección de red ocupa 3 bytes (menos 3 bits) y la dirección del nodo, 1 byte. Por lo tanto, pueden existir 221 redes de clase C con hasta 256 nodos.

Dado que la dirección de la computadora Lagaffe de la Facultad de Ciencias de Angers es 193.49.144.1, vemos que el byte de mayor peso es 193, es decir, en binario 11000001. De ello se deduce que la red es de clase C.

Direcciones reservadas

  • Algunas direcciones IP son direcciones de red en lugar de direcciones de nodos dentro de la red. Son aquellas en las que la dirección del nodo se establece en 0. Así, la dirección 193.49.144.0 es la dirección IP de la red de la Facultad de Ciencias de Angers. Por lo tanto, ningún nodo de una red puede tener la dirección cero.
  • Cuando en una dirección IP la dirección del nodo contiene únicamente unos, se trata de una dirección de difusión: esta dirección designa a todos los nodos de la red.
  • En una red de clase C, que teóricamente permite 2⁸ = 256 nodos, si se eliminan las dos direcciones prohibidas, solo quedan 254 direcciones autorizadas.

11.1.5.2. Los protocolos de conversión de dirección de Internet <--> dirección física

Hemos visto que, al transmitir información de una máquina a otra, esta se encapsula en paquetes al atravesar la capa IP. Estos paquetes tienen la siguiente forma:

Por lo tanto, el paquete IP contiene las direcciones de Internet de las máquinas de origen y destino. Cuando este paquete se transmite a la capa encargada de enviarlo a la red física, se le agrega más información para formar la trama física que finalmente se enviará a la red. Por ejemplo, el formato de una trama en una red Ethernet es el siguiente:

En la trama final, se encuentran las direcciones físicas de los equipos de origen y destino. ¿Cómo se obtienen?

La máquina remitente, que conoce la dirección IP de la máquina con la que desea comunicarse, obtiene la dirección física de esta última utilizando un protocolo específico llamado ARP (Protocolo de resolución de direcciones).

  • Envía un paquete de un tipo especial llamado paquete ARP que contiene la dirección IP de la máquina cuya dirección física se busca. También se ha asegurado de incluir en él su propia dirección IP, así como su dirección física.
  • Este paquete se envía a todos los nodos de la red.
  • Estos reconocen la naturaleza especial del paquete. El nodo que reconoce su dirección IP en el paquete responde enviando al remitente del paquete su dirección física. ¿Cómo lo hace? Ha encontrado en el paquete las direcciones IP y la dirección física del remitente.
  • Así, el remitente recibe la dirección física que buscaba. La almacena en memoria para poder usarla más adelante si hay que enviar otros paquetes al mismo destinatario.

La dirección IP de una máquina normalmente está registrada en uno de sus archivos, por lo que puede consultarlo para conocerla. Esta dirección se puede cambiar: basta con editar el archivo. La dirección física, por su parte, está registrada en la memoria de la tarjeta de red y no se puede cambiar.

Cuando un administrador desea organizar su red de otra manera, puede verse en la necesidad de cambiar las direcciones IP de todos los nodos y, por lo tanto, de editar los distintos archivos de configuración de cada uno de ellos. Esto puede resultar tedioso y dar lugar a errores si hay muchas máquinas. Un método consiste en no asignar una dirección IP a las máquinas: para ello, se inserta un código especial en el archivo en el que la máquina debería encontrar su dirección IP. Al descubrir que no tiene una dirección IP, la máquina la solicita mediante un protocolo llamado RARP (Protocolo de resolución inversa de direcciones). A continuación, envía a la red un paquete especial llamado paquete RARP, similar al paquete ARP anterior, en el que incluye su dirección física. Este paquete se envía a todos los nodos, los cuales reconocen entonces un paquete RARP. Uno de ellos, llamado servidor RARP, cuenta con un archivo que indica la correspondencia entre la dirección física y la dirección IP de todos los nodos. A continuación, responde al remitente del paquete RARP, reenviándole su dirección IP. Por lo tanto, un administrador que desee reconfigurar su red solo tiene que editar el archivo de correspondencias del servidor RARP. Este, por lo general, debe tener una dirección fija IP que debe poder conocer sin tener que utilizar él mismo el protocolo RARP.

11.1.6. La capa de red conocida como capa IP de Internet

El protocolo IP (Protocolo de Internet) define la forma que deben adoptar los paquetes y la manera en que deben gestionarse al enviarlos o recibirlos. Este tipo específico de paquete se denomina datagrama IP. Ya lo hemos presentado:

Lo importante es que, además de los datos que se van a transmitir, el datagrama IP contiene las direcciones de Internet de las máquinas de origen y destino. De esta manera, la máquina destinataria sabe quién le envía un mensaje.

A diferencia de una trama de red, cuya longitud está determinada por las características físicas de la red por la que transita, la longitud del datagrama IP la establece el software y, por lo tanto, será la misma en diferentes redes físicas. Hemos visto que, al descender de la capa de red a la capa física, el datagrama IP se encapsulaba en una trama física. Hemos dado el ejemplo de la trama física de una red Ethernet:

Las tramas físicas circulan de nodo en nodo hacia su destino, el cual puede no estar en la misma red física que la máquina remitente. Por lo tanto, el paquete IP puede encapsularse sucesivamente en diferentes tramas físicas en los nodos que conectan dos redes de tipo diferente. También es posible que el paquete IP sea demasiado grande para encapsularse en una trama física. El software IP del nodo donde se presenta este problema descompone entonces el paquete IP en fragments siguiendo reglas precisas, y cada uno de ellos se envía posteriormente a la red física. Solo se volverán a ensamblar en su destino final.

11.1.6.1. El enrutamiento

El enrutamiento es el método para dirigir los paquetes IP a su destino. Existen dos métodos: el enrutamiento directo y el enrutamiento indirecto.

Enrutamiento directo

El enrutamiento directo se refiere al envío de un paquete IP directamente del remitente al destinatario dentro de la misma red:

  • La máquina que envía un datagrama IP cuenta con la dirección IP del destinatario.
  • Obtiene la dirección física de este último mediante el protocolo ARP o en sus tablas, si ya se ha obtenido dicha dirección.
  • Envía el paquete a través de la red a esa dirección física.

Enrutamiento indirecto

El enrutamiento indirecto se refiere al envío de un paquete IP a un destino que se encuentra en una red distinta a aquella a la que pertenece el remitente. En este caso, las partes de dirección de red de las direcciones IP de las máquinas de origen y destino son diferentes. La máquina de origen reconoce este hecho. A continuación, envía el paquete a un nodo especial llamado enrutador (router), nodo que conecta una red local con otras redes y cuya dirección IP encuentra en sus tablas; esta dirección se obtuvo inicialmente ya sea de un archivo, de una memoria permanente o mediante información que circula por la red.

Un enrutador está conectado a dos redes y tiene una dirección IP dentro de ambas redes.

En nuestro ejemplo anterior:

. La red n.º 1 tiene la dirección de Internet 193.49.144.0 y la red n.º 2, la dirección 193.49.145.0.

. Dentro de la red n.º 1, el enrutador tiene la dirección 193.49.144.6 y, dentro de la red n.º 2, la dirección 193.49.145.3.

La función del enrutador es convertir el paquete IP que recibe —y que está contenido en una trama física típica de la red n.º 1— en una trama física que pueda circular por la red n.º 2. Si la dirección IP del destinatario del paquete se encuentra en la red n.º 2, el enrutador le enviará el paquete directamente; de lo contrario, lo enviará a otro enrutador, que conecta la red n.º 2 con la red n.º 3, y así sucesivamente.

11.1.6.2. Mensajes de error y de control

Siempre en la capa de red, es decir, al mismo nivel que el protocolo IP, existe el protocolo ICMP (Protocolo de mensajes de control de Internet). Sirve para enviar mensajes sobre el funcionamiento interno de la red: nodos en falla, congestión en un enrutador, etc. Los mensajes ICMP se encapsulan en paquetes IP y se envían a la red. Las capas IP de los distintos nodos toman las medidas adecuadas según los mensajes ICMP que reciben. De este modo, una aplicación en sí misma nunca percibe estos problemas propios de la red.

Un nodo utilizará la información ICMP para actualizar sus tablas de enrutamiento.

11.1.7. La capa de transporte: los protocolos UDP y TCP

11.1.7.1. El protocolo UDP: Protocolo de datagramas de usuario

El protocolo UDP permite un intercambio no confiable de datos entre dos puntos, es decir, no se garantiza que un paquete llegue correctamente a su destino. La aplicación, si así lo desea, puede gestionar esto por sí misma, por ejemplo, esperando un acuse de recibo después de enviar un mensaje antes de enviar el siguiente.

Hasta ahora, a nivel de red, hemos hablado de direcciones IP de máquinas. Sin embargo, en una misma máquina pueden coexistir al mismo tiempo diferentes procesos, todos los cuales pueden comunicarse entre sí. Por lo tanto, al enviar un mensaje, es necesario indicar no solo la dirección IP de la máquina destinataria, sino también el «nombre» del proceso destinatario. Este nombre es, en realidad, un número, llamado número de puerto. Algunos números están reservados para aplicaciones estándar: el puerto 69 para la aplicación tftp (protocolo trivial de transferencia de archivos), por ejemplo.

Los paquetes gestionados por el protocolo UDP también se denominan datagramas. Tienen la siguiente forma:

Estos datagramas se encapsularán en paquetes IP y, posteriormente, en tramas físicas.

11.1.7.2. El protocolo TCP: Protocolo de control de transferencia

Para garantizar comunicaciones seguras, el protocolo UDP no es suficiente: el desarrollador de aplicaciones debe crear por su cuenta un protocolo que le permita detectar el enrutamiento correcto de los paquetes. El protocolo TCP (Protocolo de control de transferencia) evita estos problemas. Sus características son las siguientes:

  • El proceso que desea transmitir establece primero una conexión con el proceso destinatario de la información que va a enviar. Esta conexión se realiza entre un puerto de la máquina emisora y un puerto de la máquina receptora. Entre ambos puertos se crea así una ruta virtual que quedará reservada exclusivamente para los dos procesos que hayan establecido la conexión.
  • Todos los paquetes enviados por el proceso de origen siguen esta ruta virtual y llegan en el orden en que fueron enviados, lo cual no estaba garantizado en el protocolo UDP, ya que los paquetes podían seguir rutas diferentes.
  • La información transmitida tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta información no se envía necesariamente de inmediato: el protocolo TCP espera tener suficiente cantidad para enviarla. Se almacena en una estructura llamada segmento TCP. Una vez lleno, este segmento se transmitirá a la capa IP, donde se encapsulará en un paquete IP.
  • Cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP receptor verifica que reciba los segmentos en orden. Por cada segmento recibido correctamente, envía un acuse de recibo al remitente.
  • Cuando este último lo recibe, se lo indica al proceso emisor. De esta manera, este último puede saber que un segmento ha llegado a su destino, lo cual no era posible con el protocolo UDP.
  • Si, tras un cierto tiempo, el protocolo TCP que ha enviado un segmento no recibe un acuse de recibo, vuelve a transmitir el segmento en cuestión, garantizando así la calidad del servicio de transmisión de la información.
  • El circuito virtual establecido entre los dos procesos que se comunican es full-duplex: esto significa que la información puede transitar en ambos sentidos. De este modo, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen continúa enviando información. Esto permite, por ejemplo, que el protocolo de origen TCP envíe varios segmentos sin esperar un acuse de recibo. Si, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento específico n.º n, reanudará la transmisión de los segmentos a partir de ese punto.

11.1.8. La capa de aplicaciones

Por encima de los protocolos UDP y TCP, existen diversos protocolos estándar:

TELNET

Este protocolo permite que un usuario de una máquina A de la red se conecte a una máquina B (a menudo llamada máquina anfitriona). TELNET emula en la máquina A un terminal denominado universal. Por lo tanto, el usuario actúa como si tuviera un terminal conectado a la máquina B. Telnet se basa en el protocolo TCP.

FTP: (Protocolo de transferencia de archivos)

Este protocolo permite el intercambio de archivos entre dos máquinas remotas, así como operaciones con archivos, como la creación de directorios, por ejemplo. Se basa en el protocolo TCP.

TFTP: (Control trivial de transferencia de archivos)

Este protocolo es una variante de FTP. Se basa en el protocolo UDP y es menos sofisticado que FTP.

DNS: (Sistema de Nombres de Dominio)

Cuando un usuario desea intercambiar archivos con una máquina remota, por ejemplo, mediante FTP, debe conocer la dirección de Internet de dicha máquina. Por ejemplo, para realizar una conexión FTP en la máquina Lagaffe de la Universidad de Angers, habría que ejecutar FTP de la siguiente manera: FTP 193.49.144.1

Esto obliga a tener un directorio que establezca la correspondencia entre máquina <--> dirección IP. Probablemente, en este directorio las máquinas se designarían con nombres simbólicos tales como:

máquina DPX2/320 de la Universidad de Angers

máquina Sun de la ISERPA de Angers

Es evidente que sería más cómodo referirse a una máquina por su nombre en lugar de por su dirección IP. Esto plantea el problema de la unicidad del nombre: hay millones de máquinas interconectadas. Se podría imaginar que un organismo centralizado asignara los nombres. Sin duda, eso sería bastante engorroso. De hecho, el control de los nombres se ha distribuido en dominios. Cada dominio es administrado por un organismo, por lo general de estructura muy ágil, que tiene total libertad para elegir los nombres de las máquinas. Así, las máquinas en Francia pertenecen al dominio «fr», administrado por el Inria de París. Para seguir simplificando las cosas, se distribuye aún más el control: se crean dominios dentro del dominio «fr». De este modo, la Universidad de Angers pertenece al dominio «univ-Angers». El servicio que administra este dominio tiene total libertad para nombrar las máquinas de la red de la Universidad de Angers. Por el momento, este dominio no se ha subdividido. Pero en una gran universidad con muchas máquinas en red, podría subdividirse.

La máquina DPX2/320 de la Universidad de Angers se ha denominado Lagaffe, mientras que una máquina PC y otra 486DX50 se han denominado liny. ¿Cómo se pueden referenciar estas máquinas desde el exterior? Especificando la jerarquía de los dominios a los que pertenecen. Así, el nombre completo de la máquina Lagaffe será:

Lagaffe.univ-Angers.fr

Dentro de los dominios, se pueden utilizar nombres relativos. Así, dentro del dominio fr y fuera del dominio univ-Angers, se podrá hacer referencia a la máquina Lagaffe mediante

Lagaffe.univ-Angers

Por último, dentro del dominio univ-Angers, se podrá referenciar simplemente como

Lagaffe

Por lo tanto, una aplicación puede hacer referencia a una máquina por su nombre. Al final, sin embargo, hay que obtener la dirección de Internet de esa máquina. ¿Cómo se logra esto? Supongamos que desde una máquina A queremos comunicarnos con una máquina B.

  • Si la máquina B pertenece al mismo dominio que la máquina A, probablemente encontraremos su dirección IP en un archivo de la máquina A.
  • De lo contrario, la máquina A encontrará, en otro archivo o en el mismo que antes, una lista de varios servidores de nombres con sus direcciones IP. Un servidor de nombres se encarga de establecer la correspondencia entre el nombre de una máquina y su dirección IP. La máquina A enviará una consulta especial al primer servidor de nombres de su lista, denominada consulta DNS, que incluye, por lo tanto, el nombre de la máquina buscada. Si el servidor consultado tiene ese nombre en sus registros, enviará a la máquina A la dirección IP correspondiente. De lo contrario, el servidor también encontrará en sus archivos una lista de servidores de nombres a los que puede consultar. Entonces lo hará. De este modo, se consultará a varios servidores de nombres, no de manera desordenada, sino de forma que se minimicen las consultas. Si finalmente se encuentra la máquina, la respuesta llegará de vuelta a la máquina A.

XDR: (Representación de datos eXternal)

Creado por Sun MicroSystems, este protocolo especifica una representación estándar de los datos, independiente de las máquinas.

RPC: (Llamada a procedimiento remoto)

Definido también por Sun, es un protocolo de comunicación entre aplicaciones remotas, independiente de la capa de transporte. Este protocolo es importante: libera al programador de la necesidad de conocer los detalles de la capa de transporte y hace que las aplicaciones sean portables. Este protocolo se basa en el protocolo XDR

NFS: Sistema de archivos de red

También definido por Sun, este protocolo permite que una máquina «vea» el sistema de archivos de otra máquina. Se basa en el protocolo RPC mencionado anteriormente.

11.1.9. Conclusión

En esta introducción hemos presentado algunas líneas generales de los protocolos de Internet. Para profundizar en este tema, se puede leer el excelente libro de Douglas Comer:

Título TCP/IP: Arquitectura, protocolos, aplicaciones.

Autor Douglas COMER

Editor InterEditions

11.2. Las clases .NET de la gestión de direcciones IP

Una máquina en la red de Internet se identifica de manera única mediante una dirección IP (Protocolo de Internet) que puede adoptar dos formas:

  • IPv4: codificada en 32 bits y representada por una cadena de la forma «I1.I2.I3.I4», donde In es un número entre 1 y 254. Estas son las direcciones IP más comunes en la actualidad.
  • IPv6: codificada en 128 bits y representada por una cadena del tipo «[I1.I2.I3.I4.I5.I6.I7.I8]», donde In es una cadena de 4 dígitos hexadecimales. En este documento, no utilizaremos las direcciones IPv6.

Una máquina también puede definirse mediante un nombre igualmente único. Este nombre no es obligatorio, ya que las aplicaciones siempre terminan utilizando las direcciones IP de las máquinas. Están ahí para facilitarles la vida a los usuarios. Por lo tanto, es más fácil, con un navegador, solicitar el http://www.ibm.com (URL) que el URL http://129.42.17.99, aunque ambos métodos son posibles.

Una máquina puede tener varias direcciones IP si está conectada físicamente a varias redes al mismo tiempo. En ese caso, tiene una dirección IP en cada red.

Una dirección IP puede representarse de dos maneras en .NET:

  • como una cadena de caracteres «I1.I2.I3.I4» o «[I1.I2.I3.I4.I5.I6.I7.I8]»
  • en forma de un objeto de tipo IPAddress

La clase IPAddress

Entre los métodos M, propiedades P y constantes C de la clase IPAddress, se encuentran los siguientes:

AddressFamily AddressFamily
P
familia de la dirección IP. El tipo AddressFamily es una enumeración. Los dos valores habituales son:
AddressFamily.InterNetwork: para una dirección IPv4
AddressFamily.InterNetworkV6: para una dirección IPv6
IPAddress Any
C
la dirección IP «0.0.0.0». Cuando un servicio está asociado a esta dirección, significa que acepta clientes en todas las direcciones IP de la máquina en la que opera.
IPAddress LoopBack
C
la dirección IP «127.0.0.1». Se conoce como «dirección de bucle». Cuando un servicio está asociado a esta dirección, significa que solo acepta clientes que se encuentran en la misma máquina que él.
IPAdress None
C
la dirección IP «255.255.255.255». Cuando un servicio está asociado a esta dirección, significa que no acepta a ningún cliente.
bool TryParse(string ipString, out IPAddress address)
M
intenta convertir la dirección IP ipString de la forma «I1.I2.I3.I4» en un objeto de dirección IPAddress. Devuelve true si la operación se realizó con éxito.
bool IsLoopBack
M
devuelve «true» si la dirección IP es «127.0.0.1»
string ToString()
M
devuelve la dirección IP en el formato «I1.I2.I3.I4» o «[I1.I2.I3.I4.I5.I6.I7.I8]»

La asociación entre la dirección IP y nomMachine está garantizada por un servicio distribuido de Internet llamado DNS (Sistema de Nombres de Dominio). Los métodos estáticos de la clase Dns permiten establecer la asociación entre las direcciones IP <--> nomMachine:

GetHostEntry (string hostNameOrdAddress)
devuelve una dirección IPHostEntry a partir de una dirección IP en forma de cadena o a partir de un nombre de máquina. Lanza una excepción si no se puede encontrar la máquina.
GetHostEntry (IPAddress ip)
devuelve una dirección IPHostEntry a partir de una dirección IP del tipo IPAddress. Lanza una excepción si no se puede encontrar la máquina.
string GetHostName()
devuelve el nombre de la máquina en la que se ejecuta el programa que ejecuta esta instrucción
IPAddress[] GetHostAddresses(string hostNameOrdAddress)
devuelve las direcciones IP de la máquina identificada por su nombre o por una de sus direcciones IP.

Una instancia IPHostEntry encapsula las direcciones IP, los alias y el nombre de una máquina. El tipo IPHostEntry es el siguiente:

IPAddress[] AddressList
P
tabla de direcciones IP de la máquina
String[] Aliases
P
los alias DNS de la máquina. Estos son los nombres que corresponden a las diferentes direcciones IP de la máquina.
string HostName
P
el nombre de host principal de la máquina

Consideremos el siguiente programa, que muestra el nombre de la máquina en la que se ejecuta y, a continuación, de manera interactiva, presenta las correspondencias entre la dirección IP y el nombre de la máquina:


using System;
using System.Net;

namespace Chap9 {
    class Program {
        static void Main(string[] args) {
            // muestra el nombre de la máquina local
            // luego brinda información de manera interactiva sobre las máquinas de la red
            // identificadas por un nombre o una dirección IP

            // equipo local
            Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());

            // preguntas y respuestas interactivas
            string machine;
            IPHostEntry ipHostEntry;
            while (true) {
                // ingreso del nombre o la dirección IP del equipo buscado
                Console.Write("Machine recherchée (rien pour arrêter) : ");
                machine = Console.ReadLine().Trim().ToLower();
                // ¿Listo?
                if (machine == "") return;
                // gestión de excepciones
                try {
                    // búsqueda de máquina
                    ipHostEntry = Dns.GetHostEntry(machine);
                    // el nombre de la máquina
                    Console.WriteLine("Machine : " + ipHostEntry.HostName);
                    // las direcciones IP de la máquina
                    Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
                    for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
                        Console.Write(", {0}" , ipHostEntry.AddressList[i]);
                    }
                    Console.WriteLine();
                    // los alias de la 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 {
                    // la máquina no existe
                    Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
                }
            }
        }
    }
}

La ejecución arroja los siguientes resultados:

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

11.3. Fundamentos de la programación web

11.3.1. Generalidades

Consideremos la comunicación entre dos máquinas remotas A y B:

Cuando una aplicación AppA de la máquina A quiere comunicarse con una aplicación AppB de la máquina B en Internet, debe saber varias cosas:

  • la dirección IP o el nombre de la máquina B
  • el número del puerto con el que opera la aplicación AppB. De hecho, la máquina B puede albergar numerosas aplicaciones que operan en Internet. Cuando recibe información procedente de la red, debe saber a qué aplicación va dirigida dicha información. Las aplicaciones de la máquina B tienen acceso a la red a través de «ventanillas», también llamadas puertos de comunicación. Esta información se encuentra en el paquete recibido por la máquina B para que pueda entregarse a la aplicación correcta.
  • Los protocolos de comunicación que comprende la máquina B. En nuestro estudio, utilizaremos únicamente los protocolos TCP-IP.
  • El protocolo de comunicación aceptado por la aplicación AppB. De hecho, las máquinas A y B se «comunicarán» entre sí. Lo que se digan quedará encapsulado en los protocolos TCP-IP. Sin embargo, cuando, al final de la cadena, la aplicación AppB reciba la información enviada por la aplicación AppA, deberá ser capaz de interpretarla. Esto es similar a la situación en la que dos personas, A y B, se comunican por teléfono: su diálogo se transmite a través del teléfono. El habla será codificada en forma de señales por el teléfono A, transportada por las líneas telefónicas, llegará al teléfono B para ser decodificada allí. La persona B entonces oye palabras. Ahí es donde entra en juego el concepto de protocolo de diálogo: si A habla francés y B no entiende ese idioma, A y B no podrán dialogar de manera útil.

Por lo tanto, las dos aplicaciones que se comunican deben ponerse de acuerdo sobre el tipo de diálogo que van a adoptar. Por ejemplo, el diálogo con un servicio ftp no es el mismo que con un servicio pop: estos dos servicios no aceptan los mismos comandos. Tienen un protocolo de diálogo diferente.

11.3.2. Las características del protocolo TCP

Aquí solo analizaremos las comunicaciones de red que utilizan el protocolo de transporte TCP. Recordemos aquí sus características:

  • El proceso que desea transmitir establece primero una conexión con el proceso destinatario de la información que va a enviar. Esta conexión se realiza entre un puerto de la máquina emisora y un puerto de la máquina receptora. Entre ambos puertos se crea así una ruta virtual que quedará reservada exclusivamente para los dos procesos que han establecido la conexión.
  • Todos los paquetes enviados por el proceso de origen siguen esta ruta virtual y llegan en el orden en que fueron enviados
  • La información transmitida tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta información no se envía necesariamente de inmediato: el protocolo TCP espera tener suficiente cantidad para enviarla. Se almacena en una estructura llamada segmento TCP. Una vez que este segmento se llene, se transmitirá a la capa IP, donde se encapsulará en un paquete IP.
  • Cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP receptor verifica que reciba los segmentos en orden. Por cada segmento recibido correctamente, envía un acuse de recibo al remitente.
  • Cuando este último lo recibe, se lo indica al proceso emisor. De esta manera, este último puede saber que un segmento ha llegado a su destino.
  • Si, tras un cierto tiempo, el protocolo TCP que ha enviado un segmento no recibe un acuse de recibo, vuelve a transmitir el segmento en cuestión, garantizando así la calidad del servicio de transmisión de la información.
  • El circuito virtual establecido entre los dos procesos que se comunican es full-duplex: esto significa que la información puede transitar en ambos sentidos. De este modo, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen continúa enviando información. Esto permite, por ejemplo, que el protocolo de origen TCP envíe varios segmentos sin esperar un acuse de recibo. Si, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento específico n.º n, reanudará la transmisión de los segmentos a partir de ese punto.

11.3.3. La relación cliente-servidor

A menudo, la comunicación en Internet es asimétrica: la máquina A inicia una conexión para solicitar un servicio a la máquina B; especifica que desea establecer una conexión con el servicio SB1 de la máquina B. Esta última acepta o rechaza la solicitud. Si acepta, la máquina A puede enviar sus solicitudes al servicio SB1. Estas deben cumplir con el protocolo de comunicación que entiende el servicio SB1. De esta manera, se establece un diálogo de solicitud-respuesta entre la máquina A, a la que llamamos máquina cliente, y la máquina B, a la que llamamos máquina servidor. Uno de los dos participantes cerrará la conexión.

11.3.4. Arquitectura de un cliente

La arquitectura de un programa de red que solicita los servicios de una aplicación servidor será la siguiente:

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. Arquitectura de un servidor

La arquitectura de un programa que ofrece servicios será la siguiente:

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

El programa servidor procesa de manera diferente la solicitud de conexión inicial de un cliente y sus solicitudes posteriores para obtener un servicio. El programa no proporciona el servicio por sí mismo. Si lo hiciera, durante el tiempo que durara el servicio ya no estaría atento a las solicitudes de conexión y, por lo tanto, los clientes no recibirían el servicio. Por lo tanto, procede de otra manera: tan pronto como se recibe una solicitud de conexión en el puerto de escucha y esta es aceptada, el servidor crea una tarea encargada de prestar el servicio solicitado por el cliente. Este servicio se presta en otro puerto de la máquina servidora llamado puerto de servicio. De esta manera, se puede atender a varios clientes al mismo tiempo.

Una tarea de servicio tendrá la siguiente estructura:

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. Descubre los protocolos de comunicación de Internet:

11.4.1. Introducción

Cuando un cliente se conecta a un servidor, se establece un diálogo entre ambos. La naturaleza de este diálogo constituye lo que se conoce como el protocolo de comunicación del servidor. Entre los protocolos más comunes de Internet se encuentran los siguientes:

  • HTTP: Protocolo de transferencia HyperText — el protocolo de comunicación con un servidor web (servidor HTTP)
  • SMTP: Simple Mail Transfer Protocol —el protocolo de comunicación con un servidor de envío de correo electrónico (servidor SMTP)
  • POP: Protocolo de oficina postal (POP) — el protocolo de comunicación con un servidor de almacenamiento de correo electrónico (servidor POP). Se trata de recuperar los correos electrónicos recibidos, no de enviarlos.
  • FTP: Protocolo de transferencia de archivos (FTP) —el protocolo de comunicación con un servidor de almacenamiento de archivos (servidor FTP).

Todos estos protocolos tienen la particularidad de ser protocolos de líneas de texto: el cliente y el servidor intercambian líneas de texto. Si contamos con un cliente capaz de:

  • establecer una conexión con un servidor TCP
  • mostrar en la consola las líneas de texto que el servidor le envía
  • enviar al servidor las líneas de texto que un usuario ingresaría

entonces podemos comunicarnos con un servidor TCP que utilice un protocolo de líneas de texto, siempre y cuando conozcamos las reglas de dicho protocolo.

El programa telnet que se encuentra en las máquinas Unix o Windows es un cliente de este tipo. En las máquinas Windows, también existe una herramienta llamada putty y es la que vamos a utilizar aquí. putty se puede descargar en la dirección [http://www.putty.org/]. Es un ejecutable (.exe) que se puede utilizar directamente. Lo configuraremos de la siguiente manera:

  • [1]: la dirección IP del servidor TCP al que queremos conectarnos o su nombre
  • [2]: el puerto de escucha del servidor TCP
  • [3]: utiliza el modo Raw, que indica una conexión TCP sin procesar.
  • [4]: utilizar el modo Never para evitar que la ventana del cliente putty se cierre si el servidor cierra la conexión.
  • [6,7]: número de columnas y filas de la consola
  • [5]: el número máximo de líneas que se mantienen en memoria. Un servidor HTTP puede enviar muchas líneas. Es necesario poder desplazarse por ellas.
  • [8,9]: para conservar los parámetros anteriores, asigne un nombre a la configuración [8] y guárdela [9].
  • [11,12]: para recuperar una configuración guardada, selecciona [11] y cárgala como [12].

Con esta herramienta así configurada, veamos algunos protocolos TCP.

11.4.2. El protocolo HTTP (Protocolo de transferencia HyperText)

Conectemos nuestro cliente [1] al servidor web de la máquina istia.univ-angers.fr [2], puerto 80 [3]:

En la consola de putty, creamos el siguiente diálogo HTTP:

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

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

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0
  • las líneas 1 a 4 son la solicitud del cliente, ingresada con el teclado
  • las líneas 5 a 19 son la respuesta del servidor
  • línea 1: sintaxis GET UrlDocument HTTP/1.1 — solicitamos la URL /, c.a.d. La raíz del sitio web [istia.univ-angers.fr].
  • línea 2: sintaxis Host: máquina:puerto
  • línea 3: sintaxis Connection: [mode de la connexion]. El modo [close] le indica al servidor que cierre la conexión una vez que haya enviado su respuesta. El modo [Keep-Alive] solicita que se mantenga abierta.
  • línea 4: línea en blanco. Las líneas 1 a 3 se denominan encabezados HTTP. Puede haber otros además de los que se presentan aquí. El final de los encabezados HTTP se indica con una línea en blanco.
  • Líneas 5-13: los encabezados HTTP de la respuesta del servidor; también terminan aquí con una línea en blanco.
  • líneas 14-19: el documento enviado por el servidor, en este caso un documento HTML
  • línea 5: sintaxis HTTP/1.1 código msg; el código 200 indica que se encontró el documento solicitado.
  • línea 6: la fecha y la hora del servidor
  • línea 7: identificación del software que presta el servicio web —en este caso, un servidor Apache en un sistema Linux/Debian
  • línea 8: el documento fue generado dinámicamente por PHP
  • línea 9: cookie de identificación del cliente; si este desea ser reconocido en su próxima conexión, deberá reenviar esta cookie en sus encabezados HTTP.
  • línea 10: indica que, tras servir el documento solicitado, el servidor cerrará la conexión
  • línea 11: el documento se transmitirá por partes (chunked) y no de una sola vez.
  • línea 12: tipo de documento: en este caso, un documento HTML
  • línea 13: la línea vacía que indica el final de los encabezados HTTP del servidor
  • línea 14: número hexadecimal que indica el número de caracteres del primer bloque del documento. Cuando este número sea 0 (línea 19), el cliente sabrá que ha recibido todo el documento.
  • líneas 15-18: parte del documento recibido.

La conexión se ha cerrado y el cliente putty está inactivo. Volvamos a conectarnos a [1] y limpiemos la pantalla de las visualizaciones anteriores [2,3]:

El diálogo esta vez es el siguiente:

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

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

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

0
  • línea 1: se solicitó un documento inexistente
  • línea 5: el servidor HTTP respondió con el código 404, lo que significa que no se encontró el documento solicitado.

Si solicitamos este documento con el navegador Firefox:

Image

Si solicitamos ver el código fuente [Affichage/Code source]:

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

Obtenemos las líneas 13 a 22 recibidas por nuestro cliente putty. Lo interesante de este es que, además, nos muestra los encabezados HTTP de la respuesta. También es posible obtenerlos con Firefox.

11.4.3. El protocolo SMTP (Protocolo simple de transferencia de correo)

Los servidores SMTP suelen operar en el puerto 25 [2]. Nos conectamos al servidor [1]. Aquí, por lo general, hay que elegir un servidor

que pertenezca al mismo dominio IP que la máquina, ya que, en la mayoría de los casos, los servidores SMTP están configurados para aceptar únicamente solicitudes de máquinas que pertenezcan al mismo dominio que ellos. Además, con bastante frecuencia, los cortafuegos o antivirus de las computadoras personales están configurados para no aceptar conexiones al puerto 25 desde una computadora externa. En ese caso, puede ser necesario reconfigurar ese cortafuegos o antivirus.

El cuadro de diálogo SMTP en la ventana del cliente putty es el siguiente:

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

A continuación, (D) es una solicitud del cliente y (R) una respuesta del servidor.

  • línea 1: (R) mensaje de bienvenida del servidor SMTP
  • línea 2: (D) comando HELO para decir «hola»
  • línea 3: (R) respuesta del servidor
  • línea 4: (D) dirección del remitente, por ejemplo, correo de: someone@gmail.com
  • línea 5: (R) respuesta del servidor
  • línea 6: (D) dirección del destinatario, por ejemplo, rcpt to: someoneelse@gmail.com
  • línea 7: (R) respuesta del servidor
  • línea 8: (D) indica el inicio del mensaje
  • línea 9: (R) respuesta del servidor
  • líneas 10-12: (D) el mensaje a enviar, que termina con una línea que contiene únicamente un punto.
  • línea 13: (R) respuesta del servidor
  • línea 14: (D) el cliente indica que ha terminado
  • línea 15: (R) respuesta del servidor, que luego cierra la conexión

11.4.4. El protocolo POP (Post Office Protocol)

Los servidores POP suelen operar en el puerto 110 [2]. Nos conectamos al servidor [1]. El diálogo POP en la ventana del cliente putty es el siguiente:

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

ligne1
ligne2
.
quit
+OK Bye-bye.
  • línea 1: (R) mensaje de bienvenida del servidor POP
  • línea 2: (D) el cliente proporciona su identificador POP, c.a.d. El nombre de usuario con el que lee su correo
  • línea 3: (R) la respuesta del servidor
  • línea 4: (D) la contraseña del cliente
  • línea 5: (R) la respuesta del servidor
  • línea 6: (D) el cliente solicita la lista de sus mensajes
  • líneas 7-12: (R) la lista de mensajes en la bandeja de entrada del cliente, en el formato [N° du message taille en octets du message]
  • línea 13: (D) se solicita el mensaje n.º 64
  • líneas 14-25: (R) el mensaje n.º 64, con las líneas 15-22 correspondientes a los encabezados del mensaje y las líneas 23-24 al cuerpo del mensaje.
  • línea 26: (D) el cliente indica que ha terminado
  • línea 27: (R) respuesta del servidor, que luego cerrará la conexión.

11.4.5. El protocolo FTP (Protocolo de transferencia de archivos)

El protocolo FTP es más complejo que los presentados anteriormente. Para conocer las líneas de texto intercambiadas entre el cliente y el servidor, se puede utilizar una herramienta como FileZilla [http://www.filezilla.fr/].

FileZilla es un cliente FTP que ofrece una interfaz de Windows para realizar transferencias de archivos. Las acciones del usuario en la interfaz de Windows se traducen en comandos FTP que se registran en [1]. Es una buena forma de descubrir los comandos del protocolo FTP.

11.5. Las clases .NET de la programación web

11.5.1. Elegir la clase adecuada

El marco de trabajo .NET ofrece diferentes clases para trabajar con la red:

  • La clase Socket es la que opera más cerca de la red. Permite administrar la conexión de red con gran precisión. El término socket se refiere a un conector eléctrico. El término se ha ampliado para referirse a un conector de red de software. En una comunicación TCP-IP entre dos máquinas A y B, son dos sockets los que se comunican entre sí. Una aplicación puede trabajar directamente con los sockets. Este es el caso de la aplicación A mencionada anteriormente. Un socket puede ser un socket client o serveur.
  • Si se desea trabajar a un nivel menos detallado que el de la clase Socket, se pueden utilizar las clases
  • TcpClient para crear un cliente TCP
  • TcpListener para crear un servidor TCP

Estas dos clases ofrecen a la aplicación que las utiliza una visión más sencilla de la comunicación de red, ya que se encargan por ella de los detalles técnicos de la gestión de los sockets.

  • .NET ofrece clases específicas para ciertos protocolos:
  • la clase SmtpClient para gestionar el protocolo SMTP de comunicación con un servidor SMTP de envío de correos electrónicos
  • la clase WebClient para gestionar los protocolos HTTP o FTP de comunicación con un servidor web.

Cabe destacar que la clase Socket es suficiente por sí sola para gestionar toda la comunicación TCP/IP, pero se buscará ante todo utilizar las clases de mayor nivel para facilitar la programación de la aplicación TCP/IP.

11.5.2. La clase TcpClient

La clase TcpClient es la clase adecuada en la mayoría de los casos para crear el cliente de un servicio TCP. Entre sus constructores C, métodos M y propiedades P, cuenta con los siguientes:

TcpClient(string hostname, int port)
C
crea una conexión TCP con el servicio que opera en el puerto indicado (port) de la máquina indicada (hostname). Por ejemplo, new TcpClient("istia.univ-angers.fr",80) para conectarse al puerto 80 de la máquina istia.univ-angers.fr
Socket Client
P
el socket que utiliza el cliente para comunicarse con el servidor.
NetworkStream GetStream()
M
obtiene un flujo de lectura y escritura hacia el servidor. Este flujo es el que permite las comunicaciones entre el cliente y el servidor.
void Close()
M
cierra la conexión. El socket y el flujo NetworkStream también se cierran
bool Connected()
P
verdadero si se ha establecido la conexión

La clase NetworkStream representa el flujo de red entre el cliente y el servidor. Se deriva de la clase Stream. Muchas aplicaciones cliente-servidor intercambian líneas de texto que terminan con los caracteres de fin de línea «\r\n». Por lo tanto, resulta útil utilizar los objetos StreamReader y StreamWriter para leer y escribir estas líneas en el flujo de red. Así, si una máquina M1 ha establecido una conexión con una máquina M2 mediante un objeto TcpClient client1 y ambas intercambian líneas de texto, podrá crear sus flujos de lectura y escritura de la siguiente manera:

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

La instrucción

out1.AutoFlush=true;

significa que el flujo de escritura de client1 no pasará por un búfer intermedio, sino que irá directamente a la red. Este punto es importante. Por lo general, cuando client1 envía una línea de texto a su contraparte, espera una respuesta. Esta nunca llegará si la línea en realidad se almacenó en el búfer de la máquina M1 y nunca se envió a la máquina M2.

Para enviar una línea de texto a la máquina M2, se escribirá:

client1.WriteLine("un texte");

Para leer la respuesta de M2, se escribirá:

string réponse=client1.ReadLine();

Ahora contamos con los elementos para diseñar la arquitectura básica de un cliente de Internet que utilice el siguiente protocolo de comunicación básico con el servidor:

  • el cliente envía una solicitud contenida en una sola línea
  • el servidor envía una respuesta contenida en una sola línea

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

namespace ... {
    class ... {
        static void Main(string[] args) {
            ...
            try {
                // se está conectando al servicio
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flujo de salida sin almacenamiento en búfer
                                writer.AutoFlush = true;
                                // bucle de solicitud-respuesta
                                while (true) {
                                    // la solicitud proviene del teclado
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // ¿Terminado?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // se envía la solicitud al servidor
                                    writer.WriteLine(demande);
                                    // se lee la respuesta del servidor
                                    réponse = reader.ReadLine();
                                    // se procesa la respuesta
                                    ...
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // error
                ...
            }
        }
    }
}
  • línea 11: creación de la conexión del cliente; la cláusula using garantiza que los recursos relacionados con ella se liberarán al salir de using.
  • línea 12: apertura del flujo de red en una cláusula using
  • línea 13: creación y ejecución del flujo de lectura en una cláusula using
  • línea 14: creación y ejecución del flujo de escritura en una cláusula using
  • línea 16: no almacenar en búfer el flujo de salida
  • líneas 18-31: el ciclo de solicitud del cliente / respuesta del servidor
  • línea 26: el cliente envía su solicitud al servidor
  • línea 28: el cliente espera la respuesta del servidor. Se trata de una operación bloqueante, similar a la lectura del teclado. La espera finaliza con la llegada de una cadena que termina en «\n» o bien con el fin del flujo. Esto ocurrirá si el servidor cierra la conexión que había establecido con el cliente.

11.5.3. La clase TcpListener

La clase TcpListener es la clase adecuada en la mayoría de los casos para crear un servicio TCP. Entre sus constructores C, métodos M y propiedades P, cuenta con los siguientes:

TcpListener(int port)
C
crea un servicio TCP que esperará (escuchará) las solicitudes de los clientes en un puerto pasado como parámetro (port), denominado puerto de escucha. Si la máquina está conectada a varias redes IP, el servicio escuchará en cada una de ellas.
TcpListener(IPAddress ip, int port)
C
Lo mismo, pero la escucha solo se realiza en la dirección IP especificada.
void Start()
M
inicia la escucha de las solicitudes de los clientes
TcpClient AcceptTcpClient()
M
acepta la solicitud de un cliente. A continuación, abre una nueva conexión con este, denominada conexión de servicio. El puerto utilizado por el servidor es aleatorio y lo elige el sistema. Se le denomina puerto de servicio. AcceptTcpClient devuelve como resultado el objeto TcpClient asociado, por parte del servidor, a la conexión de servicio.
void Stop()
M
deja de escuchar las solicitudes de los clientes
Socket Server
P
El socket de escucha del servidor

La estructura básica de un servidor TCP que se comunicaría con sus clientes según el siguiente protocolo:

  • el cliente envía una solicitud contenida en una sola línea
  • el servidor envía una respuesta contenida en una sola línea

podría verse así:


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

namespace ... {
    public class ... {
            ...
            // se crea el servicio de escucha
            TcpListener ecoute = null;
            try {
                // se crea el servicio; este escuchará en todas las interfaces de red de la máquina
                ecoute = new TcpListener(IPAddress.Any, port);
                // se inicia
                ecoute.Start();
                // bucle del servicio
                TcpClient tcpClient = null;
                // bucle infinito: se detendrá con Ctrl-C
                while (true) {
                    // espera a un cliente
                    tcpClient = ecoute.AcceptTcpClient();
                    // el servicio lo proporciona otra tarea
                    ThreadPool.QueueUserWorkItem(Service, tcpClient);
                    // siguiente cliente
                }
            } catch (Exception ex) {
                // se reporta el error
                ...
            } finally {
                // fin del servicio
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // presta servicio a un cliente
        public static void Service(Object infos) {
            // se recupera el cliente al que se debe atender
            Client client = infos as Client;
            // explotación del enlace TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flujo de salida sin almacenamiento en búfer
                                writer.AutoFlush = true;
                                // bucle de lectura de solicitud/escritura de respuesta
                                bool fini=false;
                                while (! fini) != null) {
                                    // espera de solicitud del cliente: operación bloqueante
                                    demande=reader.ReadLine();
                                    // preparación de la respuesta
                                    réponse=...;
                                    // envío de respuesta al cliente
                                    writer.WriteLine(réponse);
                                    // siguiente solicitud
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // error
                ...
            } finally {
                // fin del cliente
                ...
            }
        }
    }
}
  • línea 14: se crea el servicio de escucha para un puerto determinado y una dirección IP determinada. Hay que recordar aquí que una máquina tiene al menos dos direcciones IP: la dirección «127.0.0.1», que es su dirección de bucle cerrado, y la dirección «I1.I2.I3.I4» que tiene en la red a la que está conectada. Puede tener otras direcciones IP si está conectada a varias redes IP. IPAddress.Any se refiere a todas las direcciones IP de una máquina.
  • línea 16: se inicia el servicio de escucha. Anteriormente ya se había creado, pero aún no estaba a la escucha. Estar a la escucha significa esperar las solicitudes de los clientes.
  • líneas 20-26: el ciclo de espera de solicitud del cliente / servicio al cliente se repite para cada nuevo cliente
  • línea 22: se acepta la solicitud de un cliente. El método AcceptTcpClient devuelve una instancia TcpClient denominada de servicio:
    • el cliente ha realizado su solicitud con su propia instancia TcpClient del lado del cliente, a la que llamaremos TcpClientDemande
    • el servidor acepta esta solicitud con AcceptTcpClient. Este método crea una instancia TcpClient del lado del servidor, a la que llamaremos TcpClientService. De este modo, se establece una conexión TCP abierta con las instancias TcpClientDemande <--> TcpClientService en ambos extremos.
    • La comunicación cliente/servidor que tiene lugar a continuación se realiza a través de esta conexión. El servicio de escucha ya no interviene.
  • línea 24: para que el servidor pueda atender a varios clientes al mismo tiempo, el servicio se lleva a cabo mediante subprocesos, un subproceso por cliente.
  • línea 32: se cierra el servicio de escucha
  • línea 38: el método ejecutado por el hilo de servicio para un cliente. Recibe como parámetro la instancia TcpClient, que ya está conectada al cliente al que se debe atender.
  • Líneas 38-71: aquí encontramos un código similar al del cliente TCP básico que analizamos anteriormente.

11.6. Ejemplos de clientes/servidores TCP

11.6.1. Un servidor de eco

Nos proponemos escribir un servidor de eco que se iniciará desde una ventana DOS mediante el comando:

ServeurEcho puerto

El servidor opera en el puerto pasado como parámetro. Simplemente devuelve al cliente la solicitud que este le envió. El programa es el siguiente:


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

// llamada: serveurEcho puerto
// servidor de eco
// devuelve al cliente la línea que este le envió

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

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

            // ¿hay algún argumento?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }
            // este argumento debe ser un número entero mayor que 0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port<=0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // se crea el servicio de escucha
            TcpListener ecoute = null;
            int numClient = 0;    // siguiente número de cliente
            try {
                // Se crea el servicio; este escuchará en todas las interfaces de red de la máquina
                ecoute = new TcpListener(IPAddress.Any, port);
                // se inicia
                ecoute.Start();
                // seguimiento
                Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
                // hilos de servicio
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);
                // bucle de servicio
                TcpClient tcpClient = null;
                // bucle infinito: se detendrá con Ctrl-C
                while (true) {
                    // esperando a un cliente
                    tcpClient = ecoute.AcceptTcpClient();
                    // el servicio lo proporciona otra tarea
                    ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // siguiente cliente
                    numClient++;
                }
            } catch (Exception ex) {
                // se reporta el error
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            } finally {
                // fin del servicio
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // presta servicio a un cliente del servidor de eco
        public static void Service(Object infos) {
            // se recupera el cliente al que se debe atender
            Client client = infos as Client;
            // presta el servicio al cliente
            Console.WriteLine("Début de service au client {0}", client.NumClient);
            // gestión de la conexión TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flujo de salida sin almacenamiento en búfer
                                writer.AutoFlush = true;
                                // bucle de lectura de solicitud/escritura de respuesta
                                string demande = null;
                                while ((demande = reader.ReadLine()) != null) {
                                    // monitoreo de consola
                                    Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    // eco de la solicitud al cliente
                                    writer.WriteLine("[{0}]", demande);
                                    // seguimiento de consola
                                    Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
                                    // el servicio se detiene cuando el cliente envía «bye»
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // error
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                // fin del cliente
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }
    }

    // información del cliente
    internal class Client {
        public TcpClient CanalTcp { get; set; }        // conexión con el cliente
        public int NumClient { get; set; }            // N.º de cliente
    }
}

La estructura del servidor de eco se ajusta a la arquitectura básica de los servidores TCP expuesta anteriormente. Solo comentaremos la parte de «servicio al cliente»:

  • línea 79: se lee la solicitud del cliente
  • línea 83: se devuelve al cliente entre corchetes
  • línea 79: el servicio se detiene cuando el cliente cierra la conexión

En una ventana de DOS, utilizamos el ejecutable del proyecto C#:

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

A continuación, iniciamos dos clientes putty que conectamos al puerto 100 de la máquina localhost:

 

La salida de la consola del servidor de eco queda así:

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

El cliente 1 y luego el cliente 0 envían los siguientes textos:

  • [1]: el cliente n.º 1
  • [2]: el cliente n.º 0
  • [3]: la consola del servidor de eco
  • en [4]: el cliente 1 se desconecta con el comando bye.
  • en [5]: el servidor lo detecta

El servidor se puede detener con Ctrl-C. El cliente n.º 0 lo detecta entonces como [6].

11.6.2. Un cliente para el servidor de eco

Ahora escribiremos un cliente para el servidor anterior. Se llamará de la siguiente manera:

ClientEcho nomServeur puerto

Se conecta a la máquina nomServeur en el puerto port y luego envía al servidor líneas de texto que este le devuelve como eco.


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

namespace Chap9 {
    // se conecta a un servidor de eco
    // cada línea que se escribe en el teclado se recibe como eco
    class ClientEcho {
        static void Main(string[] args) {
            // sintaxis
            const string syntaxe = "pg machine port";

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

            // se anota el nombre del servidor
            string serveur = args[0];

            // el puerto debe ser un número entero mayor que 0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }

            // se puede trabajar
            string demande = null;        // solicitud del cliente
            string réponse = null;        // respuesta del servidor
            try {
                // se conecta al servicio
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flujo de salida sin almacenamiento en búfer
                                writer.AutoFlush = true;
                                // bucle de solicitud-respuesta
                                while (true) {
                                    // la solicitud proviene del teclado
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // ¿Terminado?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // se envía la solicitud al servidor
                                    writer.WriteLine(demande);
                                    // se lee la respuesta del servidor
                                    réponse = reader.ReadLine();
                                    // se está procesando la respuesta
                                    Console.WriteLine("Réponse : {0}", réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // error
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

La estructura de este cliente se ajusta a la arquitectura general básica propuesta para los clientes Tcp. Estos son los resultados obtenidos con la siguiente configuración:

  • el servidor se ejecuta en el puerto 100 en una ventana de DOS
  • en la misma máquina se ejecutan dos clientes en otras dos ventanas de DOS

En la ventana del cliente A (n.º 0) se observan las siguientes pantallas:

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

En la del cliente B (n.º 1):

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

En la del servidor:

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

El cliente A n.º 0 se desconecta:

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

La consola del servidor:

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

11.6.3. Un cliente genérico TCP

Vamos a escribir un cliente TCP genérico que se iniciará de la siguiente manera: ClientTcpGenerique servidor puerto. Funcionará de manera similar al cliente PuTTY, pero tendrá una interfaz de consola y no presentará opciones de configuración.

En la aplicación anterior, el protocolo de comunicación era conocido: el cliente enviaba una sola línea y el servidor respondía con una sola línea. Cada servicio tiene su protocolo específico y también se dan las siguientes situaciones:

  • el cliente debe enviar varias líneas de texto antes de recibir una respuesta
  • la respuesta de un servidor puede contener varias líneas de texto

Por lo tanto, el ciclo de enviar una sola línea al servidor y recibir una sola línea enviada por el servidor no siempre es adecuado. Para manejar protocolos más complejos que el de eco, el cliente TCP genérico contará con dos subprocesos:

  • el hilo principal leerá las líneas de texto ingresadas desde el teclado y las enviará al servidor.
  • Un hilo secundario trabajará en paralelo y se dedicará a leer las líneas de texto enviadas por el servidor. Tan pronto como reciba una, la mostrará en la consola. El hilo solo se detiene cuando el servidor cierra la conexión. Por lo tanto, trabaja de manera continua.

El código es el siguiente:


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

namespace Chap9 {
    // recibe como parámetro las características de un servicio en el formato: servidor puerto
    // se conecta al servicio
    // envía al servidor cada línea que se escribe en el teclado
    // crea un hilo para leer continuamente las líneas de texto enviadas por el servidor
    class ClientTcpGenerique {
        static void Main(string[] args) {
            // sintaxis
            const string syntaxe = "pg serveur port";

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

            // se anota el nombre del servidor
            string serveur = args[0];

            // el puerto debe ser un número entero mayor que 0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // se conecta al servicio
            TcpClient tcpClient = null;
            try {
                tcpClient = new TcpClient(serveur, port);
            } catch (Exception ex) {
                // error
                Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
                // fin
                return;
            }

            // se inicia un hilo aparte para leer las líneas de texto enviadas por el servidor
            ThreadPool.QueueUserWorkItem(Receive, tcpClient);

            // la lectura de los comandos del teclado se realiza en el hilo principal
            Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
            string demande = null;        // solicitud del cliente
            try {
                // se procesa la conexión del cliente
                using (tcpClient) {
                    // se crea un flujo de escritura hacia el servidor
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamWriter writer = new StreamWriter(networkStream)) {
                            // flujo de salida sin búfer
                            writer.AutoFlush = true;
                            // bucle de solicitud-respuesta
                            while (true) {
                                demande = Console.ReadLine();
                                // ¿Terminado?
                                if (demande.Trim().ToLower() == "bye")
                                    break;
                                // se envía la solicitud al servidor
                                writer.WriteLine(demande);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // error
                Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
            }
        }

        // hilo de lectura cliente <-- servidor
        public static void Receive(object infos) {
            // datos locales
            string réponse = null;    // respuesta del servidor
            // creación del flujo de entrada
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // bucle de lectura continua de las líneas de texto del flujo de entrada
                            while ((réponse = reader.ReadLine()) != null) {
                                // visualización en la consola
                                Console.WriteLine("<-- {0}", réponse);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // error
                Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // se señala el fin del hilo de lectura
                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.");
            }
        }
    }
}
  • línea 34: el cliente se conecta al servidor
  • línea 43: se inicia un hilo para leer las líneas de texto del servidor. Debe ejecutar el método Receive de la línea 73. A este método se le pasa la instancia TcpClient que se conectó al servidor.
  • líneas 57-64: el bucle de entrada de comandos de teclado / envío de comandos al servidor. La entrada de los comandos de teclado la realiza el hilo principal.
  • líneas 75-98: el método Receive ejecutado por el hilo de lectura de las líneas de texto. Este método recibe como parámetro la instancia TcpClient que se ha conectado al servidor.
  • líneas 84-87: el bucle continuo de lectura de las líneas de texto enviadas por el servidor. Solo se detiene cuando el servidor cierra la conexión abierta con el cliente.

A continuación se presentan algunos ejemplos que retoman los utilizados con el cliente putty en el párrafo 11.4. El cliente se ejecuta en una consola de DOS.

Protocolo HTTP

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

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

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

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

Se invita al lector a volver a leer las explicaciones proporcionadas en el párrafo 11.4.2. Solo comentaremos lo que es específico de la aplicación:

  • línea 28: después de enviar la línea 27, el servidor HTTP cerró la conexión, lo que provocó el fin del hilo de lectura. El hilo principal, que lee los comandos ingresados desde el teclado, sigue activo. El comando de la línea 29, ingresado desde el teclado, lo detiene.

Protocolo SMTP

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

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

Se invita al lector a volver a leer las explicaciones del párrafo 11.4.3 y a probar los demás ejemplos utilizados con el cliente putty.

11.6.4. Un servidor TCP genérico

Ahora nos enfocamos en un servidor

  • que muestra en pantalla los comandos enviados por sus clientes
  • y les envía como respuesta las líneas de texto que un usuario escribe en el teclado. Por lo tanto, es este último quien actúa como servidor.

El programa se inicia en una ventana de DOS mediante: ServeurTcpGenerique portEcoute, donde portEcoute es el puerto al que deben conectarse los clientes. El servicio al cliente estará a cargo de dos subprocesos:

  • el hilo principal, que:
    • atenderá a los clientes uno tras otro y no en paralelo;
    • que leerá las líneas que el usuario escriba en el teclado y las enviará al cliente. El usuario indicará con el comando «bye» que cierra la conexión con el cliente. Debido a que la consola no puede utilizarse para dos clientes al mismo tiempo, nuestro servidor solo atiende a un cliente a la vez.
  • un hilo secundario dedicado exclusivamente a leer las líneas de texto enviadas por el cliente

El servidor, por su parte, nunca se detiene, salvo que el usuario presione Ctrl-C en el teclado.

Veamos algunos ejemplos. El servidor se inicia en el puerto 100 y se utiliza el cliente genérico de paragraphe11.6.3 para comunicarse con él. La ventana del cliente es la siguiente:

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

Las líneas que comienzan con <-- son las enviadas del servidor al cliente; las demás, las del cliente al servidor. La ventana del servidor es la siguiente:

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

Las líneas que comienzan con <-- son las enviadas del cliente al servidor; las demás, las enviadas por el servidor al cliente. La línea 9 indica que el hilo de lectura de las solicitudes del cliente se ha detenido. El hilo principal del servidor sigue esperando que se ingresen comandos desde el teclado para enviarlos al cliente. Por lo tanto, hay que ingresar el comando bye de la línea 10 para pasar al siguiente cliente. El servidor sigue activo aunque el cliente 1 ya haya terminado. Se inicia un segundo cliente para el mismo servidor:

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

La ventana del servidor se ve así:

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

Después de la línea 6 anterior, el servidor quedó en espera de un nuevo cliente. Se puede detener con Ctrl-C.

Ahora simulemos un servidor web iniciando nuestro servidor genérico en el puerto 88:

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

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

Ahora abramos un navegador y solicitemos la página http://localhost:88/exemple.html. El navegador se conectará entonces al puerto 88 de la máquina localhost y solicitará la página /exemple.html:

 

Ahora veamos la ventana de nuestro servidor:

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

Vemos los encabezados HTTP enviados por el navegador. Esto nos permite descubrir otros encabezados HTTP distintos a los que ya hemos visto. Elaboremos una respuesta para nuestro cliente. El usuario que escribe en el teclado es aquí el verdadero servidor y puede elaborar una respuesta manualmente. Recordemos la respuesta generada por un servidor web en un ejemplo anterior:

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

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

Intentemos dar una respuesta similar, limitándonos a lo estrictamente necesario:

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

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

En nuestra respuesta nos hemos limitado a los encabezados HTTP de las líneas 1 a 4. No indicamos el tamaño del documento que vamos a enviar (Content-Length), sino que nos limitamos a indicar que vamos a cerrar la conexión (Connection: close) después de enviarlo. Esto es suficiente para el navegador. Al ver que la conexión se ha cerrado, sabrá que la respuesta del servidor ha finalizado y mostrará la página HTML que se le ha enviado. Esta última corresponde a las líneas 6 a 9. A continuación, el usuario cierra la conexión con el cliente desde el teclado al escribir el comando bye, línea 10. Ante este comando del teclado, el hilo principal cierra la conexión con el cliente. Esto provoca la excepción de la línea 11. El hilo encargado de leer las líneas de texto del cliente fue interrumpido bruscamente por el cierre de la conexión con el cliente y lanzó una excepción. Después de la línea 12, el servidor queda en espera de un nuevo cliente.

El navegador del cliente muestra ahora lo siguiente:

Si en el ejemplo anterior escribimos Affichage/Source para ver qué recibió el navegador, obtenemos [2], es decir, exactamente lo que enviamos desde el servidor genérico.

El código del servidor genérico TCP es el siguiente:


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) {

            // ¿Hay algún argumento?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                Environment.Exit(1);
            }
            // Este argumento debe ser un número entero mayor 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);
            }
            // se crea el servicio de escucha
            TcpListener ecoute = null;
            try {
                // se crea el servicio
                ecoute = new TcpListener(IPAddress.Any, port);
                // se inicia
                ecoute.Start();
                // seguimiento
                Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
                while (true) {
                    // espera a un cliente
                    Console.WriteLine("Attente du client suivant...");
                    TcpClient tcpClient = ecoute.AcceptTcpClient();
                    Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
                    // se inicia un hilo aparte para leer las líneas de texto enviadas por el cliente
                    ThreadPool.QueueUserWorkItem(Receive, tcpClient);
                    // la lectura de los comandos del teclado se realiza en el hilo principal
                    Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
                    string réponse = null;        // respuesta del servidor
                    // se procesa la conexión del cliente
                    using (tcpClient) {
                        // se crea un flujo de escritura hacia el cliente
                        using (NetworkStream networkStream = tcpClient.GetStream()) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flujo de salida sin búfer
                                writer.AutoFlush = true;
                                // bucle de entrada de respuestas desde el teclado
                                while (true) {
                                    réponse = Console.ReadLine();
                                    // ¿Terminado?
                                    if (réponse.Trim().ToLower() == "bye")
                                        break;
                                    // se envía la solicitud al cliente
                                    writer.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // se notifica el error
                Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // fin de la escucha
                ecoute.Stop();
            }
        }

        // hilo de lectura servidor <-- cliente
        public static void Receive(object infos) {
            // datos locales
            string demande = null;    // solicitud del cliente
            string idClient=null;    // identidad del cliente

            // gestión de la conexión del cliente
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    // identidad del cliente
                    idClient = tcpClient.Client.RemoteEndPoint.ToString();
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // bucle de lectura continua de las líneas de texto del flujo de entrada
                            while ((demande = reader.ReadLine()) != null) {
                                // visualización en la consola
                                Console.WriteLine("<-- {0}", demande);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // error
                Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
            } finally {
                // se señala el fin del hilo de lectura
                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);
            }
        }
    }
}
  • línea 29: el servicio de escucha se crea, pero no se inicia. Escucha todas las interfaces de red de la máquina.
  • línea 31: se inicia el servicio de escucha
  • línea 34: bucle infinito de espera de clientes. El usuario detendrá el servidor con Ctrl-C.
  • línea 37: espera de un cliente — operación bloqueante. Cuando llega el cliente, la instancia TcpClient devuelta por el método AcceptTcpClient representa el lado del servidor de una conexión abierta con el cliente.
  • línea 40: el flujo de lectura de las solicitudes del cliente se asigna a un hilo independiente.
  • línea 45: uso de la conexión con el cliente en una cláusula using para garantizar que se cierre pase lo que pase.
  • línea 47: uso del flujo de red en una cláusula using
  • línea 48: creación, en una cláusula using, de un flujo de escritura sobre el flujo de red
  • línea 50: el flujo de escritura no se almacenará en búfer
  • líneas 52-59: bucle de entrada por teclado de los comandos que se enviarán al cliente
  • línea 69: fin del servicio de escucha. Esta instrucción nunca se ejecutará aquí, ya que el servidor se detiene con Ctrl-C.
  • línea 78: el método Receive, que muestra continuamente en la consola las líneas de texto enviadas por el cliente. Aquí se repite lo que se vio para el cliente genérico TCP.

11.6.5. Un cliente web de

En el ejemplo anterior vimos algunos de los encabezados HTTP que enviaba un navegador:

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

Vamos a escribir un cliente web al que se le pasaría como parámetro un URL y que mostraría en pantalla el texto enviado por el servidor. Supondremos que este es compatible con el protocolo HTTP 1.1. De los encabezados anteriores, solo utilizaremos los siguientes:

1
2
3
4
<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
<--
  • el primer encabezado indica el documento deseado
  • el segundo, el servidor al que se le consulta
  • el tercero, que deseamos que el servidor cierre la conexión después de habernos respondido.

Si en la línea 1 anterior reemplazamos GET por HEAD, el servidor solo nos enviará los encabezados HTTP y no el documento especificado en la línea 1.

Nuestro cliente web se llamará de la siguiente manera: ClientWeb URL cmd, donde URL es elURL deseada y cmd una de las dos palabras clave GET o HEAD para indicar si se desean solo los encabezados (HEAD) o también el contenido de la página (GET). Veamos un primer ejemplo:

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

...\Chap9\06\bin\Release>
  • línea 1: solo solicitamos los encabezados HTTP (HEAD)
  • líneas 2-9: la respuesta del servidor

Si usamos GET en lugar de HEAD en la llamada al cliente web, obtenemos el mismo resultado que con HEAD, además del cuerpo del documento solicitado.

El código del cliente web es el siguiente:


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

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

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

            // se anota el URI solicitado
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verificación de la validez del 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 incorrecto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verificación del pedido
            if (commande != "GET" && commande != "HEAD") {
                // pedido incorrecto
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // se está conectando al servicio
                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)) {
                                // flujo de salida sin almacenar en búfer
                                writer.AutoFlush = true;
                                // se solicita el URL - envío de las cabeceras HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // se lee la respuesta
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != null) {
                                    // se muestra la respuesta en la consola
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // se muestra la excepción
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

La única novedad en este programa es el uso de la clase Uri. El programa recibe un URL (Localizador Uniforme de Recursos) o un URI (Identificador Uniforme de Recursos) con el formato http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La clase Uri nos permite descomponer la cadena del URL en sus diferentes elementos.

  • Líneas 26-33: se crea un objeto Uri a partir de la cadena stringURI recibida como parámetro. Si la cadena URI recibida como parámetro no es un URI válido (falta del protocolo, del servidor, etc.), se lanza una excepción. Esto nos permite verificar la validez del parámetro recibido. Una vez construido el objeto Uri, tenemos acceso a los diferentes elementos de esta URI. Así, si el objeto uri del código anterior se construyó a partir de la cadena http://serveur:port/document?param1=val1&param2=val2;..., tendremos:
    • uri.Host=serveur,
    • uri.Port=port,
    • uri.Path = document,
    • uri.Query=param1=val1&param2=val2;...,
    • uri.pathAndQuery= cheminPageHTML?param1=val1&param2=val2;...,
    • uri.Scheme=http.

11.6.6. Un navegador web que maneja las redirecciones

El cliente web anterior no maneja una posible redirección de URL que él mismo solicitó. A continuación se muestra un ejemplo:

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

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ibm.com/us/">here</a>.</p>
</body></html>
  • línea 2: el código 302 Found indica una redirección. La dirección a la que debe redirigirse el navegador se encuentra en el cuerpo del documento, en la línea 16.

Un segundo ejemplo:

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

0
  • línea 2: el código 301 Moved Permanently indica una redirección. La dirección a la que debe redirigirse el navegador se indica en la línea 6, en el encabezado HTTP Location.

Un tercer ejemplo:

1
2
3
4
5
6
7
...\Chap9\06\bin\Release>ClientWeb http://www.gouv.fr GET
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/es/
Date: Sat, 03 May 2008 14:56:53 GMT
Connection: close
  • línea 2: el código 302 Moved Temporarily indica una redirección. La dirección a la que debe redirigirse el navegador se indica en la línea 5, en el encabezado HTTP Location.

Un cuarto ejemplo con un servidor IIS local en la máquina:

...\istia\Chap9\06\bin\Release>ClientWeb.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:16:56 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=FDJLADLCOLDHGKGNIPMLHIIA; path=/
Cache-control: private
  • línea 2: el código 302 «Object moved» indica una redirección. La dirección a la que debe redirigirse el navegador se indica en la línea 5, en el encabezado HTTP Location. Cabe señalar que, a diferencia de los ejemplos anteriores, la dirección de redirección es relativa. La dirección completa es, de hecho, http://localhost/localstart.asp.

Nos proponemos gestionar las redirecciones cuando la primera línea de los encabezados HTTP contenga la palabra clave moved (sin distinción de mayúsculas y minúsculas) y la dirección de redirección se encuentre en el encabezado HTTP Location.

Si retomamos los tres últimos ejemplos, obtenemos los siguientes resultados:

URL: http://www.bull.com

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


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

HTTP/1.1 200 OK
Date: Sun, 04 May 2008 10:22:49 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Connection: close
Content-Type: text/html
  • línea 11: la redirección se realiza a la dirección de la línea 6

URL: http://www.gouv.fr

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


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

HTTP/1.1 200 OK
Server: Apache
X-Powered-By: PHP/4.4.1
Last-Modified: Sun, 04 May 2008 10:29:48 GMT
Content-Type: text/html
Expires: Sun, 04 May 2008 10:40:38 GMT
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close
  • línea 11: la redirección se realiza a la dirección de la línea 6

URL: http://localhost

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


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

HTTP/1.1 401 Access Denied
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="localhost"
Connection: close
Content-Length: 4766
Content-Type: text/html
  • línea 13: la redirección se realiza a la dirección de la línea 6
  • línea 15: se nos negó el acceso a la página http://localhost/localstart.asp.

El programa que gestiona la redirección es el siguiente:


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

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

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

            // se observa que se solicita el URI
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verificación de la validez del 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 incorrecto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verificación del pedido
            if (commande != "GET" && commande != "HEAD") {
                // orden incorrecta
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            const int nbRedirsMax = 1;        // no se acepta más de una redirección
            int nbRedirs = 0;                            // número de redirecciones en curso

            // expresión regular para encontrar una redirección URL
            Regex location = new Regex(@"^Location: (.+?)$");
            try {
                // se pueden solicitar varios URL si hay redirecciones
                while (nbRedirs <= nbRedirsMax) {
                    // gestión de redirecciones
                    bool redir = false;
                    bool locationFound = false;
                    string locationString = null;
                    // se establece conexión con el servicio
                    using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                        using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
                            using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
                                // flujo de salida sin almacenamiento en búfer
                                writer.AutoFlush = true;
                                // se solicita el URL - envío de los encabezados HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // se lee la primera línea de la respuesta
                                string premièreLigne = reader.ReadLine();
                                // eco en pantalla
                                Console.WriteLine(premièreLigne);

                                // ¿redirección?
                                if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
                                    // hay una redirección
                                    redir = true;
                                    nbRedirs++;
                                }

                                // siguientes encabezados HTTP hasta encontrar la línea vacía que indica el final de los encabezados
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != "") {
                                    // se muestra la respuesta
                                    Console.WriteLine(réponse);
                                    // si hay redirección, se busca el encabezado Location
                                    if (redir && !locationFound) {
                                        // se compara la línea actual con la expresión relacional «location»
                                        Match résultat = location.Match(réponse);
                                        if (résultat.Success) {
                                            // si se ha encontrado, se anota el URL de redirección
                                            locationString = résultat.Groups[1].Value;
                                            // se registra que se ha encontrado
                                            locationFound = true;
                                        }
                                    }
                                }

                                // Se han agotado los encabezados HTTP; se escribe la línea vacía
                                Console.WriteLine(réponse);
                                // luego pasamos al cuerpo del documento
                                while ((réponse = reader.ReadLine()) != null) {
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                    // ¿Ya terminamos?
                    if (!locationFound || nbRedirs > nbRedirsMax)
                        break;
                    // Hay que realizar una redirección; se construye la nueva URI
                    try {
                        if (locationString.StartsWith("http")) {
                            // dirección http completa
                            uri = new Uri(locationString);
                        } else {
                            // dirección http relativa a la URI actual
                            uri = new Uri(uri, locationString);
                        }
                        // registro de consola
                        Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
                    } catch (Exception ex) {
                        // problema con la URI
                        Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
                    }
                }
            } catch (Exception e) {
                // se muestra la excepción
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

En comparación con la versión anterior, los cambios son los siguientes:

  • línea 46: la expresión regular para recuperar la dirección de redirección en el encabezado HTTP Location: dirección.
  • línea 49: el código que antes se ejecutaba para una sola URI ahora puede ejecutarse sucesivamente para varias URIs.
  • línea 66: se lee la primera línea de los encabezados HTTP enviados por el servidor. Es esta la que contiene la palabra clave moved si el documento solicitado ha sido reubicado.
  • líneas 71-75: se verifica si la primera línea contiene la palabra clave moved. Si es así, se anota.
  • líneas 79-93: se leen los demás encabezados HTTP hasta encontrar la línea vacía que indica su fin. Si la primera línea anunciaba una redirección, entonces se presta atención al encabezado HTTP Location: dirección para memorizar la dirección de redirección en locationString.
  • líneas 98-100: el resto de la respuesta del servidor HTTP se muestra en la consola.
  • líneas 105-106: la URI solicitada se ha procesado por completo y se ha mostrado. Si no hay que realizar ninguna redirección o si se excede el número de redirecciones permitidas, se sale del programa.
  • líneas 108-122: si hay una redirección, se calcula la nueva URI que se debe solicitar. Hay que realizar un pequeño ajuste dependiendo de si la dirección de redirección encontrada era absoluta (línea 111) o relativa (línea 114).

11.7. Las clases .NET especializadas en un protocolo específico de Internet

En los ejemplos anteriores del cliente web, el protocolo HTTP se manejaba con un cliente TCP. Por lo tanto, teníamos que manejar nosotros mismos el protocolo de comunicación específico utilizado. Podríamos haber creado, de manera análoga, un cliente SMTP o POP. El marco de trabajo .NET ofrece clases especializadas para los protocolos HTTP y SMTP. Estas clases conocen el protocolo de comunicación entre el cliente y el servidor y evitan que el desarrollador tenga que gestionarlo. A continuación, las presentamos.

11.7.1. La clase classeWebClient

Existe una clase WebClient capaz de comunicarse con un servidor web. Consideremos el ejemplo del cliente web del párrafo 11.6.5, tratado aquí con la clase WebClient.


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

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

            // se registra el URI solicitado
            string stringURI = args[0];

            // verificación de la validez del 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 incorrecto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }

            try {
                // creación de cliente web
                using (WebClient client = new WebClient()) {
                    // Agregación de un encabezado HTTP 
                    client.Headers.Add("user-agent", "st");
                    using (Stream stream = client.OpenRead(uri)) {
                        using (StreamReader reader = new StreamReader(stream)) {
                            // visualización de la respuesta del servidor web
                            Console.WriteLine(reader.ReadToEnd());
                            // visualización de los encabezados de respuesta del 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);
            }
        }
    }
}
  • línea 35: se crea el cliente web, pero aún no está configurado
  • línea 37: se agrega un encabezado HTTP a la solicitud HTTP que se va a realizar. Veremos que se enviarán otros encabezados de forma predeterminada.
  • línea 38: el cliente web solicita la URI proporcionada por el usuario y lee el documento enviado. [WebClient].OpenRead(URI) abre la conexión con Uri y lee la respuesta. Ahí radica la utilidad de la clase. Se encarga de la comunicación con el servidor web. El resultado del método OpenRead es de tipo Stream y representa el documento solicitado. Los encabezados HTTP enviados por el servidor, que preceden al documento en la respuesta, no forman parte de este.
  • línea 39: se utiliza un StreamReader y, en la línea 41, su método ReadToEnd para leer la respuesta completa.
  • Líneas 44-46: se muestran los encabezados HTTP de la respuesta del servidor. [WebClient].ResponseHeaders representa una colección con valores cuyas claves son los nombres de los encabezados HTTP y cuyos valores son las cadenas de caracteres asociadas a dichos encabezados.
  • línea 51: las excepciones que se generan durante un intercambio entre el cliente y el servidor son del tipo WebException.

Veamos algunos ejemplos.

Iniciamos el servidor genérico TCP creado en el párrafo 6.4.6:

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

Iniciamos el cliente web anterior de la siguiente manera:

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

La URI solicitada es la del servidor genérico. Este muestra entonces los encabezados HTTP que le envió el cliente web:

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

Así, vemos que:

  • que el cliente web envía 3 encabezados HTTP por defecto (líneas 3, 5, 6)
  • línea 4: el encabezado que generamos nosotros mismos (línea 37 del código)
  • que el cliente web utiliza por defecto el método GET (línea 3). Existen otros métodos, entre ellos POST y HEAD.

Ahora solicitemos un recurso que no existe:

1
2
3
4
5
...\Chap9\09\bin\Release>09 http://istia.univ-angers.fr/desconocido
L'exception suivante s'est produite : System.Net.WebException: The remote server returned an error: (404) Not Found.
   at System.Net.WebClient.OpenRead(Uri address)
   at System.Net.WebClient.OpenRead(String address)
   at Chap9.WebClient1.Main(String[] args) in C:\data\2007-2008\c# 2008\poly\istia\Chap9\09\Program.cs:línea 16
  • línea 2: se produjo una excepción de tipo WebException porque el servidor respondió con el código 404 Not Found para indicar que el recurso solicitado no existía.

Por último, terminemos solicitando un recurso que sí existe:

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

El archivo istia.univ-angers.txt generado por el comando es el siguiente:

<!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
...
</html>
---------------------
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1
Date: Sun, 04 May 2008 14:30:53 GMT
Set-Cookie: fe_typo_user=22eaaf283a; path=/
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
---------------------
  • línea 1: el documento HTML solicitado.
  • líneas 3-10: los encabezados de la respuesta HTTP en un orden que no es necesariamente el mismo en el que se enviaron.

La clase WebClient cuenta con métodos que permiten recibir un documento (métodos DownLoad) o enviarlo (métodos UpLoad):

DownLoadData
para descargar un recurso como una matriz de bytes (por ejemplo, una imagen)
DownLoadFile
para descargar un recurso y guardarlo en un archivo local
DownLoadString
para descargar un recurso y recuperarlo como una cadena de caracteres (por ejemplo, un archivo HTML)
OpenWrite
el equivalente a OpenRead, pero para enviar datos al servidor
UpLoadData
el equivalente a DownLoadData, pero hacia el servidor
UpLoadFile
el equivalente a DownLoadFile, pero hacia el servidor
UpLoadString
el equivalente a DownLoadString, pero hacia el servidor
UpLoadValues
para enviar al servidor los datos de un comando POST y recuperar los resultados en forma de una tabla de bytes. El comando POST solicita un documento al tiempo que transmite al servidor la información necesaria para determinar cuál es el documento real que se debe enviar. Esta información se envía como documento al servidor, de ahí el nombre UpLoad del método. Se envía después de la línea en blanco de los encabezados HTTP en el formato param1=valeur1&param2=valeur2&...:
POST /document HTTP/1.1
...
[ligne vide]
param1=valeur1&param2=valeur2&...
El mismo documento podría solicitarse mediante el método GET:
GET /document?param1=valeur1&param2=valeur2&...
...
[ligne vide]
La diferencia entre los dos métodos es que el navegador que muestra la URI solicitada mostrará /document en el caso de POST y /document?param1=valeur1&param2=valeur2&... en el caso de GET.

11.7.2. Las clases WebRequest / WebResponse

A veces, la clase WebClient no es lo suficientemente flexible para hacer lo que queremos. Retomemos el ejemplo del cliente web con redirección analizado en el párrafo 11.6.6. Necesitamos generar el encabezado HTTP:

HEAD /document HTTP/1.1

Hemos visto que los encabezados HTTP que emite por defecto el cliente web son los siguientes:

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

También hemos visto que era posible agregar encabezados HTTP a los anteriores con la colección [WebClient].Headers. Solo la línea 1 no es un encabezado que pertenezca a la colección Headers, ya que no tiene el formato clave:valor. No he encontrado la manera de cambiar el GET por HEAD en la línea 1 partiendo de la clase WebClient (¿tal vez no busqué bien?). Cuando la clase WebClient ha alcanzado sus límites, se puede pasar a las clases WebRequest / WebResponse:

  • WebRequest: representa la totalidad de la solicitud del cliente web.
  • WebResponse: representa la totalidad de la respuesta del servidor web

Ya mencionamos que la clase WebClient maneja los esquemas http:, https:, ftp: y file:. Las solicitudes y respuestas de estos distintos protocolos no tienen el mismo formato. Por lo tanto, es necesario manejar el tipo exacto de estos elementos en lugar de su tipo genérico WebRequest y WebResponse. Por eso, utilizaremos las clases:

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

Ahora abordamos con las clases HttpWebRequest y HttpWebresponse el ejemplo del cliente web con redirección analizado en el párrafo 11.6.6. El código es el siguiente:


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

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

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

            // se anota el URI solicitado
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verificación de la validez del URI
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI incorrecto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verificación del pedido
            if (commande != "GET" && commande != "HEAD") {
                // pedido incorrecto
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // se está configurando la solicitud
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = commande;
                httpWebRequest.Proxy = null;
                // se ejecuta
                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);
                // encabezados 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)) {
                        // se muestra la respuesta en la consola
                        Console.WriteLine(reader.ReadToEnd());
                    }
                }
            } catch (WebException e1) {
                // se recupera la respuesta
                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) {
                // se muestra la excepción
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
            }
        }
    }
}
  • línea 40: se crea un objeto de tipo WebRequest con el método estático WebRequest.Create(Uri uri), donde uri es la URI del documento que se va a descargar. Como sabemos que el protocolo de la URI es HTTP, el tipo del resultado se cambia a HttpWebRequest para poder acceder a los elementos específicos del protocolo HTTP.
  • Línea 41: establecemos el método GET / POST / HEAD de la primera línea de los encabezados HTTP. En este caso, será GET o HEAD.
  • línea 42: en una red privada corporativa, es común que las computadoras de la empresa estén aisladas de Internet por razones de seguridad. Para ello, la red privada utiliza direcciones de Internet que los enrutadores de Internet no enrutan. La red privada se conecta a Internet mediante equipos especiales llamados proxys, que están conectados tanto a la red privada de la empresa como a Internet. Este es un ejemplo de equipos con varias direcciones: IP. Un equipo de la red privada no puede establecer por sí mismo una conexión con un servidor de Internet, por ejemplo, un servidor web. Debe solicitarle a un equipo proxy que lo haga por él. Un equipo proxy puede albergar servidores proxy para diferentes protocolos. Se habla de proxy HTTP para referirse al servicio que se encarga de realizar las solicitudes HTTP en nombre de las máquinas de la red privada. Si existe un servidor proxy de este tipo (HTTP), debe indicarse en el campo [WebRequest].proxy. Por ejemplo, se escribiría:
[WebRequest].proxy=new WebProxy("pproxy.istia.uang:3128");

si el proxy HTTP opera en el puerto 3128 de la máquina pproxy.istia.uang. Se debe poner «null» en el campo [WebRequest].proxy si la máquina tiene acceso directo a Internet y no necesita pasar por un proxy.

  • línea 44: el método GetResponse() solicita el documento identificado por su URI y devuelve un objeto WebRequestResponse que aquí se transforma en un objeto HttpWebResponse. Este objeto representa la respuesta del servidor a la solicitud del documento.
  • línea 47:
    • [HttpWebResponse].ResponseUri: es la URI del servidor que envió el documento. En caso de redirección, esta puede ser diferente de la URI del servidor consultado inicialmente. Cabe señalar que el código no maneja la redirección. Esta es manejada automáticamente por el método GetResponse. Una vez más, esta es la ventaja de las clases de alto nivel frente a las clases básicas del protocolo TCP.
    • [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription representan la primera línea de la respuesta, por ejemplo: HTTP/1.1 200 OK. StatusCode es 200 y StatusDescription es OK.
  • línea 50: [HttpWebResponse].Headers es la colección de encabezados HTTP de la respuesta.
  • línea 55: [HttpWebResponse].GetResponseStream: es el flujo que permite obtener el documento contenido en la respuesta.
  • línea 61: puede ocurrir una excepción de tipo WebException
  • línea 63: [WebException].Response es la respuesta que provocó el lanzamiento de la excepción.

A continuación se muestra un ejemplo de ejecución:

...\Chap9\09B\bin\Release>09B http://www.gouv.fr HEAD
---------------------
Le serveur http://www.premier-ministre.gouv.fr/fr/ respondió: 200 OK
---------------------
Connection: keep-alive
Content-Type: text/html; charset=iso-8859-1
Date: Mon, 05 May 2008 13:02:29 GMT
Expires: Mon, 05 May 2008 13:07:20 GMT
Last-Modified: Mon, 05 May 2008 12:56:59 GMT
Server: Apache
X-Powered-By: PHP/4.4.1
---------------------
  • líneas 1 y 3: el servidor que respondió no es el mismo al que se le había enviado la consulta. Por lo tanto, hubo una redirección.
  • líneas 5-11: los encabezados HTTP enviados por el servidor

11.7.3. Aplicación: un cliente proxy de un servidor web de traducción

Ahora mostramos cómo las clases anteriores nos permiten aprovechar los recursos de la web.

11.7.3.1. L'application

Existen sitios de traducción en la web. El que se utilizará aquí es el sitio http://trans.voila.fr/traduction_voila.php:

El texto a traducir se inserta en [1], la dirección de traducción se selecciona en [2]. La traducción se solicita mediante [3] y se obtiene en [4].

Vamos a desarrollar una aplicación de Windows que sirva como cliente de la aplicación anterior. No hará nada más que la aplicación del sitio [trans.voila.fr]. Su interfaz será la siguiente:

11.7.3.2. La arquitectura de la aplicación

La aplicación tendrá la siguiente arquitectura de dos capas:

11.7.3.3. El proyecto de Visual Studio

El proyecto de Visual Studio será el siguiente:

  • En [1], la solución está compuesta por dos proyectos:
  • [2]: uno para la capa [dao] y las entidades que esta utiliza,
  • [3]: el otro para la interfaz de Windows

11.7.3.4. El proyecto [dao]

El proyecto [dao] está formado por los siguientes elementos:

  • IServiceTraduction.cs: la interfaz presentada a la capa [ui]
  • ServiceTraduction: la implementación de esta interfaz
  • WebTraductionsException: una excepción específica de la aplicación

La interfaz IServiceTraduction es la siguiente:


using System.Collections.Generic;

namespace dao {
    public interface IServiceTraduction {
        // idiomas utilizados
        IDictionary<string, string> LanguesTraduites { get; }
        // traducción
        string Traduire(string texte, string deQuoiVersQuoi);
    }
}
  • línea 6: la propiedad LanguesTraduites devuelve el diccionario de idiomas aceptados por el servidor de traducción. Este diccionario tiene entradas del formato ["fe","Français-Anglais"], donde el valor indica un sentido de traducción —en este caso, del francés al inglés— y la clave «fe» es un código utilizado por el servidor de traducción trans.voila.fr.
  • línea 8: el método Traduire es el método de traducción:
    • texte es el texto a traducir
    • deQuoiVersQuoi es una de las claves del diccionario de idiomas traducidos
    • el método devuelve la traducción del texto

ServiceTraduction es una clase de implementación de la interfaz IServiceTraduction. La detallamos en la siguiente sección.

WebTraductionsException es la siguiente clase de excepción:


using System;

namespace entites {
    public class WebTraductionsException : Exception {

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

        // fabricantes
        public WebTraductionsException() {
        }
        public WebTraductionsException(string message)
            : base(message) {
        }
        public WebTraductionsException(string message, Exception e)
            : base(message, e) {
        }
    }
}
  • línea 7: un código de error

11.7.3.5. El cliente web [ServiceTraduction]

Volvamos a la arquitectura de nuestra aplicación:

La clase [ServiceTraduction] que debemos escribir es un cliente del servicio web de traducción [trans.voila.fr]. Para escribirla, necesitamos entender

  • qué espera el servidor de traducción de su cliente
  • qué le devuelve a su cliente

Veamos, con un ejemplo, el diálogo entre el cliente y el servidor que tiene lugar durante una traducción. Retomemos el ejemplo presentado en la introducción de la aplicación:

El texto a traducir se inserta en [1]; la dirección de traducción se selecciona en [2]. La traducción se solicita mediante [3] y se obtiene en [4].

Para obtener la traducción [4], el navegador envió la siguiente solicitud GET (que se muestra en su barra de direcciones):

http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=mi+perro+está+enfermo

Es bastante fácil de entender:

  • http://trans.voila.fr/traduction_voila.php es la URL del servicio de traducción
  • isText=1 parece indicar que se trata de texto
  • translationDirection indica el sentido de la traducción, en este caso Français-Anglais
  • stext es el texto a traducir en un formato que se conoce como URL codificada. De hecho, ciertos caracteres no pueden aparecer en una URL. Este es el caso, por ejemplo, del espacio, que aquí se ha codificado con un +. El marco de trabajo .NET ofrece el método estático System.Web.HttpUtility.UrlEncode para realizar esta tarea de codificación.

De ello se deduce que, para consultar el servidor de traducción, nuestra clase [ServiceTraduction] podrá utilizar la cadena

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

donde los marcadores {0} y {1} serán reemplazados, respectivamente, por el sentido de la traducción y el texto a traducir.

¿Cómo sabemos cuáles son los sentidos de traducción aceptados por el servidor? En la captura de pantalla anterior, los idiomas traducidos aparecen en la lista desplegable. Si en el navegador miramos (Ver / Código fuente) el código HTML de la página, encontramos lo siguiente para la lista desplegable:

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

No es un código HTML muy limpio, ya que cada etiqueta <option> debería cerrarse normalmente con una etiqueta </option>. Dicho esto, los atributos «value» nos dan la lista de códigos de traducción que deben enviarse al servidor. En el diccionario LanguesTraduites de la interfaz IServiceTraduction, las claves serán los atributos «value» mencionados anteriormente y los valores, los textos que muestra la lista desplegable.

Ahora veamos (Ver / Código fuente) dónde se encuentra en la página HTML la traducción devuelta por el servidor de traducción:

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

La traducción se encuentra justo en medio de la página HTML devuelta. ¿Cómo encontrarla? Podemos usar una expresión regular con la secuencia <div class="txtTrad">...</div>, ya que la etiqueta <div class="txtTrad"> solo aparece en ese lugar de la página HTML. La expresión regular en C# que permite recuperar el texto traducido es la siguiente:

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

Ahora contamos con los elementos necesarios para escribir la clase de implementación ServiceTraduction de la interfaz 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 {
        // propiedades de configuración automática del servicio
        public IDictionary<string, string> LanguesTraduites { get; set; }
        public string UrlServeurTraduction { get; set; }
        public string ProxyHttp { get; set; }
        public String RegexTraduction { get; set; }

        // traducción
        public string Traduire(string texte, string deQuoiVersQuoi) {
            // ¿Es posible la traducción solicitada?
            if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
                throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
            }
            // texto a traducir
            string texteATraduire = HttpUtility.UrlEncode(texte);
            // URI a solicitar
            string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
            // expresión regular para encontrar la traducción en la respuesta
            Regex patternTraduction = new Regex(RegexTraduction);
            // excepción
            WebTraductionsException exception = null;
            // traducción
            string traduction = null;
            try {
                // se configura la consulta
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = "GET";
                httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
                // se ejecuta
                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 la traducción en la línea actual
                            MatchCollection résultats = patternTraduction.Matches(ligne);
                            // ¿Se encontró la traducción?
                            if (résultats.Count != 0) {
                                traduction = résultats[0].Groups[1].Value.Trim();
                                traductionTrouvée = true;
                            }
                        }
                        // ¿Se encontró la traducción?
                        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 };
            }
            // ¿Excepción?
            if (exception != null) {
                throw exception;
            } else {
                return traduction;
            }
        }
    }
}
  • línea 12: la propiedad LanguesTraduites de la interfaz IServiceTraduction, inicializada desde afuera
  • línea 13: la propiedad UrlServeurTraduction es la URL que se debe solicitar al servidor de traducción: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, donde el marcador {0} deberá sustituirse por el idioma de destino y el marcador {1} por el texto a traducir —inicializada desde el exterior—
  • línea 14: la propiedad ProxyHttp es el posible proxy HTTP que se utilizará, por ejemplo: pproxy.istia.uang:3128 —inicializada desde el exterior
  • línea 15: la propiedad RegexTraduction es la expresión regular que permite recuperar la traducción en el flujo HTML devuelto por el servidor de traducción, por ejemplo @"<div class=""txtTrad"">(.*?)</div>" —inicializada desde el exterior
  • Estas cuatro propiedades serán, en nuestra aplicación, inicializadas por Spring.
  • líneas 20-22: se verifica que la dirección de traducción solicitada exista efectivamente en el diccionario de idiomas traducidos. Si no es así, se lanza una excepción.
  • línea 24: el texto a traducir se codifica para que pueda formar parte de una URL
  • línea 26: se construye la URI del servicio de traducción. Si la propiedad UrlServeurTraduction es la cadena http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, el marcador {0} se reemplaza por el sentido de la traducción y el marcador {1} por el texto a traducir.
  • línea 28: se construye el patrón de búsqueda de la traducción en la respuesta HTML del servidor de traducción.
  • Líneas 33 y 60: la consulta al servidor de traducción se realiza dentro de un bloque try/catch
  • línea 35: se crea el objeto HttpWebRequest, que se utilizará para consultar al servidor de traducción, con la URI del documento solicitado.
  • línea 36: el método de consulta es GET. Se podría omitir esta instrucción, ya que GET es probablemente el método predeterminado del objeto HttpWebRequest.
  • línea 37: se establece la propiedad Proxy del objeto HttpWebRequest.
  • línea 39: se envía la solicitud al servidor de traducción y se recupera su respuesta, que es de tipo HttpWebResponse.
  • líneas 41-42: se utiliza un StreamReader para leer cada línea de la respuesta HTML del servidor.
  • líneas 45-53: en cada línea de la respuesta, se busca la traducción. Cuando se encuentra, se detiene la lectura de la respuesta HTML y se cierran todos los flujos que se hayan abierto.
  • líneas 55-57: si no se ha encontrado ninguna traducción en la respuesta HTML, se prepara una excepción de tipo WebTraductionsException para indicarlo.
  • líneas 60-62: si se produjo una excepción durante el intercambio entre el cliente y el servidor, se encapsula en una excepción de tipo WebTraductionsException para indicarlo.
  • líneas 64-68: si se ha registrado una excepción, se lanza; de lo contrario, se devuelve la traducción encontrada.

Nuestro ejemplo supone que el proxy HTTP no requiere autenticación. Si no fuera así, se escribiría algo como:


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

Aquí hemos utilizado WebRequest / WebResponse en lugar de WebClient porque no necesitamos procesar la respuesta HTML completa del servidor de traducción. Una vez que encontramos la traducción en esta respuesta, ya no necesitamos el resto de las líneas de la respuesta. La clase WebClient no permite hacer esto.

A continuación se muestra un programa de prueba de la clase ServiceTraduction:


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

namespace ui {
    class Program {
        static void Main(string[] args) {
            try {
                // creación del servicio de traducción
                ServiceTraduction serviceTraduction = new ServiceTraduction();
                // expresión regular para encontrar la traducción
                serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
                // URL del servidor de traducción
                serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
                // diccionario de idiomas traducidos
                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";
                // traducción
                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) {
                // error
                Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
            }
        }
    }
}

Los resultados obtenidos son los siguientes:

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

El proyecto [dao] de la solución se compila en un DLL HttpTraductions.dll:

 

11.7.3.6. La interfaz gráfica de la aplicación

Repasemos la arquitectura de nuestra aplicación:

Ahora escribimos la capa [ui]. Esta es el objeto del proyecto [ui] de la solución en desarrollo:

La carpeta [lib] [3] contiene algunas de las DLL a las que hace referencia el proyecto [4]:

  • las necesarias para Spring: Spring.Core, Common.Logging, antlr.runtime
  • la de la capa [dao]: HttpTraductions

El archivo [App.config] contiene la configuración de 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>
            <!-- el servicio de traducción -->
            <object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
                <property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&amp;translationDirection={0}&amp;stext={1}"/>
                <!--
                <property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
                -->
                <property name="RegexTraduction" value="&lt;div class=&quot;txtTrad&quot;&gt;(.*?)&lt;/div&gt;"/>
                <property name="LanguesTraduites">
                    <dictionary key-type="string" value-type="string">
                        <entry key="fe" value="Français-Anglais"/>
                        <entry key="ef" value="Anglais-Français"/>
...
                        <entry key="ei" value="Anglais-Italien"/>
                        <entry key="ie" value="Italien-Anglais"/>
                    </dictionary>
                </property>
            </object>
        </objects>
    </spring>
</configuration>
  • línea 15: los objetos que Spring debe instanciar. Solo habrá uno, el de la línea 18, que instancia el servicio de traducción con la clase ServiceTraduction, que se encuentra en DLL HttpTraductions.
  • línea 19: la propiedad UrlServeurTraduction de la clase ServiceTraduction. Existe un problema con el carácter & de la URL. Este carácter tiene un significado específico en un archivo XML. Por lo tanto, debe ser escaneado. Lo mismo ocurre con otros caracteres que encontraremos más adelante en el archivo. Deben ser reemplazados por una secuencia [&code;]: & por [&amp;], < por [&lt;], > por [&gt;], " por [&quot;].
  • línea 21: la propiedad ProxyHttp de la clase ServiceTraduction. Queda una propiedad sin inicializar en null. No definir esta propiedad equivale a indicar que no hay un proxy HTTP.
  • línea 23: la propiedad RegexTraduction de la clase ServiceTraduction. En la expresión regular, fue necesario reemplazar los caracteres [< > "] por sus equivalentes protegidos.
  • Líneas 24-33: la propiedad LanguesTraduites de la clase ServiceTraduction.

El programa [Program.cs] se ejecuta al iniciar la aplicación. Su código es el siguiente:


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

namespace ui {
    static class Program {
        /// <summary>
        /// El punto de entrada principal de la aplicación.
        /// </summary>
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
    
            // --------------- Código de desarrollador
            // instanciación del servicio de traducción
            IApplicationContext ctx = null;
            Exception ex = null;
            ServiceTraduction serviceTraduction = null;
            try {
                // contexto de Spring
                ctx = ContextRegistry.GetContext();
                // se solicita una referencia del servicio de traducción
                serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
            } catch (Exception e1) {
                // almacenamiento de la excepción
                ex = e1;
            }
            // formulario para mostrar
            Form form = null;
            // ¿Hubo alguna excepción?
            if (ex != null) {
                // sí: se crea el mensaje de error que se mostrará
                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;
                }
                // creación de la ventana de error a la que se pasa el mensaje de error que se debe mostrar
                Form2 form2 = new Form2();
                form2.MsgErreur = msgErreur.ToString();
                // esta será la ventana que se mostrará
                form = form2;
            } else {
                // Todo salió bien
                // creación de la interfaz gráfica [Form1] a la que se le pasa la referencia al servicio de traducción
                Form1 form1 = new Form1();
                form1.ServiceTraduction = serviceTraduction;
                // esta será la ventana que se mostrará
                form = form1;
            }
            // visualización de la ventana
            Application.Run(form);
        }
    }
}

Este código ya se utilizó en la aplicación Impuestos, versión 6, en el párrafo 7.6.2.

  • Spring crea el servicio de traducción en la línea 27. Si esta creación se ha realizado correctamente, se mostrará el formulario [Form1] (líneas 52-55); de lo contrario, se mostrará el formulario de error [Form2] (líneas 36-48).

El formulario [Form2] es el que se utiliza en la aplicación Impuestos versión 6 y se ha explicado en el párrafo 7.6.4.

El formulario [Form1] es el siguiente:

n.º
tipo
nombre
función
1
TextBox
textBoxTexteATraduire
cuadro de texto para ingresar el texto a traducir
MultiLine=true
2
ComboBox
comboBoxLangues
la lista de direcciones de traducción
3
Botón
buttonTraduire
para solicitar la traducción del texto [1] en la dirección [2]
4
TextBox
textBoxTraduction
la traducción del texto [1]

El código del formulario [Form1] es el siguiente:


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

namespace ui {
    public partial class Form1 : Form {
        // servicio de traducción
        public ServiceTraduction ServiceTraduction { get; set; }
        // diccionario de idiomas
        Dictionary<string, string> languesInversées = new Dictionary<string, string>();

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

        // carga inicial del formulario
        private void Form1_Load(object sender, EventArgs e) {
            // creación del diccionario inverso de idiomas
            foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
                // idiomas
                string langues = ServiceTraduction.LanguesTraduites[code];
                // agregación (idiomas, código) al diccionario inverso
                languesInversées[langues] = code;
            }
            // llenado del menú desplegable en orden alfabético de los idiomas
            string[] languesCombo = languesInversées.Keys.ToArray();
            Array.Sort<string>(languesCombo);
            foreach (string langue in languesCombo) {
                comboBoxLangues.Items.Add(langue);
            }
            // selección del primer idioma
            if (comboBoxLangues.Items.Count != 0) {
                comboBoxLangues.SelectedIndex = 0;
            }
        }

        private void buttonTraduire_Click(object sender, EventArgs e) {
            // ¿Hay algo que traducir?
            string texte = textBoxTexteATraduire.Text.Trim();
            if (texte == "") return;
            // traducción
            try {
                textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
            } catch (Exception ex) {
                textBoxTraduction.Text = ex.Message;
            }
        }
    }
}
  • línea 10: una referencia al servicio de traducción. Esta propiedad pública fue inicializada por [Program.cs], línea 53. Por lo tanto, cuando se ejecutan los métodos Form1_Load (línea 20) o buttonTraduire_Click (línea 40), este campo ya está inicializado.
  • línea 12: el diccionario de idiomas traducidos con entradas de tipo ["Français-Anglais","fe"], c.a.d. El inverso del diccionario LanguesTraduites devuelto por el servicio de traducción.
  • línea 20: el método Form1_Load se ejecuta al cargar el formulario.
  • líneas 22-27: se utilizan los diccionarios serviceTraduction.LanguesTraduites y ["fe","Français-Anglais"] para construir los diccionarios languesInversées y ["Français-Anglais", "fe"].
  • línea 29: languesCombo es la tabla de claves de los diccionarios languesInversées y c.a.d. Una tabla de elementos ["Français-Anglais"]
  • línea 30: esta tabla está ordenada para presentar en el menú desplegable los sentidos de traducción en orden alfabético
  • líneas 31-33: se completa el menú desplegable de idiomas.
  • línea 40: el método que se ejecuta cuando el usuario hace clic en el botón [Traduire]
  • línea 46: basta con llamar al método serviceTraduction.Traduire para solicitar la traducción. El primer parámetro es el texto a traducir; el segundo, el código de la dirección de traducción. Este código se encuentra en el diccionario languesInversées a partir del elemento seleccionado en el menú desplegable de idiomas.
  • línea 48: si se produce una excepción, esta se muestra en lugar de la traducción.

11.7.3.7. Conclusion

Esta aplicación ha demostrado que los clientes web del marco .NET nos permiten aprovechar los recursos de la web. La técnica es siempre similar:

  • determinar la URI a la que se va a realizar la consulta. Esta URI suele llevar parámetros.
  • consultarla
  • buscar en la respuesta del servidor lo que se busca mediante expresiones regulares

Esta técnica es aleatoria. De hecho, con el paso del tiempo, la URI consultada o la expresión regular que permite encontrar el resultado esperado pueden cambiar. Por lo tanto, conviene colocar esta información en un archivo de configuración. Pero esto puede resultar insuficiente. Veremos en el siguiente capítulo que existen recursos más estables en la web: los servicios web.

11.7.4. Un cliente SMTP (Protocolo simple de transporte de correo) con la clase SmtpClient

Un cliente SMTP es un cliente de un servidor SMTP, servidor de envío de correo. La clase .NET SmtpClient encapsula por completo las necesidades de dicho cliente. El desarrollador no necesita conocer los detalles del protocolo SMTP. Nosotros sí los conocemos. Se presentó en el párrafo 11.4.3.

Presentamos la clase SmtpClient en el marco de una aplicación básica de Windows que permite enviar correos electrónicos con archivos adjuntos. La aplicación se conectará al puerto 25 de un servidor SMTP. Recordamos que, en la mayoría de los sistemas Windows, los cortafuegos u otros antivirus bloquean las conexiones al puerto 25. Por lo tanto, es necesario desactivar esta protección para probar la aplicación:

El cliente SMTP tendrá una arquitectura de una sola capa:

El proyecto de Visual Studio es el siguiente:

  

La interfaz gráfica de usuario [SendMailForm.cs] de la aplicación es la siguiente:

N.º
tipo
nombre
función
1
TextBox
textBoxServeur
nombre del servidor SMTP al que conectarse
2
NumericUpDown
numericUpDownPort
el puerto al que conectarse
3
TextBox
textBoxExpediteur
dirección del remitente del mensaje
4
TextBox
textBoxTo
direcciones de los destinatarios en el formato: dirección1, dirección2, ...
5
TextBox
textBoxCc
direcciones de los destinatarios en copia (CC = Carbon Copy) en el formato: dirección1, dirección2, ...
6
TextBox
textBoxBcc
direcciones de los destinatarios en copia oculta (BCC = Blind Carbon Copy) en el formato: dirección1, dirección2, ... Todas las direcciones de estos tres campos de entrada recibirán el mismo mensaje con los mismos archivos adjuntos. Los destinatarios del mensaje podrán ver las direcciones que figuraban en los campos 4 y 5, pero no las del campo 6. Por lo tanto, la CCO es una forma de incluir a alguien en copia sin que los demás destinatarios del mensaje lo sepan.
7
Botón
buttonAjouter
para agregar un archivo adjunto al correo
8
ListBox
listBoxPiecesJointes
lista de archivos adjuntos al correo
9
TextBox
textBoxSujet
Asunto de la carta
10
TextBox
textBoxMessage
El texto del mensaje.
MultiLine=true
11
Botón
buttonEnvoyer
para enviar el mensaje y los posibles archivos adjuntos
12
TextBox
textBoxRésultat
muestra un resumen del mensaje enviado o un mensaje de error si se ha presentado algún problema
13
Botón
buttonEffacer
para borrar [12]
 
OpenfileDialog
openFileDialog1
control no visual que permite seleccionar un archivo adjunto en el sistema de archivos local

En el ejemplo anterior, el resumen que se muestra en [12] es el siguiente:

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

El código del formulario [SendMailForm.cs] es el siguiente:


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();
        }

        // Agregar un archivo adjunto
        private void buttonAjouter_Click(object sender, EventArgs e) {
            // Configuración del cuadro de diálogo openfileDialog1
            openFileDialog1.InitialDirectory = Application.ExecutablePath;
            openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
            openFileDialog1.FilterIndex = 0;
            openFileDialog1.FileName = "";
            // se muestra el cuadro de diálogo y se recupera su resultado
            if (openFileDialog1.ShowDialog() == DialogResult.OK) {
                // se recupera el nombre del archivo
                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() != "";
        }

        // eliminar un archivo adjunto
        private void buttonRetirer_Click(object sender, EventArgs e) {
            // ¿Archivo adjunto seleccionado?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // se elimina
                listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
                // se actualiza el botón «Eliminar»
                buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
            }
        }

        private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
            // ¿Archivo adjunto seleccionado?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // se actualiza el botón «Quitar»
                buttonRetirer.Enabled = true;
            }
        }

        // envío del mensaje con sus archivos adjuntos
        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 = "";
        }
    }
}

No comentaremos este código, ya que no presenta novedades. Para comprender el método buttonAjouter_Click de la línea 14, se invita al lector a volver a leer el párrafo 7.5.1.

El método buttonEnvoyer_Click de la línea 55, que envía el correo, es el siguiente:


private void buttonEnvoyer_Click(object sender, EventArgs e) {
            try {
                // reloj de arena
                Cursor = Cursors.WaitCursor;
                // el cliente SMTP
                SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
                // el mensaje
                MailMessage message = new MailMessage();
                // remitente
                message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
                message.From = message.Sender;
                // destinatarios
                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));
                    }
                }
                // asunto
                message.Subject = textBoxSujet.Text.Trim();
                // texto del mensaje
                message.Body = textBoxMessage.Text;
                // archivos adjuntos
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    message.Attachments.Add(new Attachment(attachement));
                }
                // envío del mensaje
                smtpClient.Send(message);
                // Ok: se muestra un resumen
                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) {
                // se muestra el error
                textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
            }
            // cursor normal
            Cursor = Cursors.Arrow;
        }
  • línea 6: se crea el cliente SMTP. Requiere dos parámetros: el nombre del servidor SMTP y el puerto en el que opera
  • línea 8: se crea un mensaje de tipo MailMessage. Este es el que encapsulará la totalidad del mensaje que se va a enviar.
  • línea 10: se ingresa la dirección de correo electrónico Sender del remitente. Una dirección de correo electrónico es una instancia del tipo MailAddress construida a partir de una cadena de caracteres «xx@yy.zz». Esta cadena debe tener el formato esperado para una dirección de correo electrónico; de lo contrario, se genera una excepción. En ese caso, se mostrará en el campo textBoxResultat (línea 63) en un formato poco intuitivo.
  • líneas 13-19: las direcciones de correo electrónico de los destinatarios se colocan en la lista «Para» del mensaje. Estas direcciones se obtienen del campo textBoxTo. La expresión regular de la línea 13 permite extraer las diferentes direcciones, que están separadas por una coma.
  • líneas 21-26: se repite el mismo proceso para inicializar el campo CC del mensaje con las direcciones en copia del campo textBoxCc.
  • líneas 28-33: se repite el mismo proceso para inicializar el campo «Cco» del mensaje con las direcciones en copia oculta del campo textBoxBcc.
  • línea 35: el campo Subject del mensaje se inicializa con el asunto del campo textBoxSujet.
  • línea 37: el campo «Body» del mensaje se inicializa con el texto del mensaje textBoxMessage.
  • Líneas 39-41: se adjuntan los archivos al mensaje. Cada archivo adjunto se agrega como un objeto Attachment al campo Attachments del mensaje. Se instancia un objeto Attachment a partir de la ruta completa del archivo que se va a adjuntar en el sistema de archivos local.
  • línea 43: el mensaje se envía mediante el método Send del cliente SMTP.
  • líneas 45-60: se escribe el resumen del envío en el campo textBoxResultat y se reinicia el formulario.
  • línea 63: visualización de un posible error

11.8. Un cliente TCP genérico asíncrono

11.8.1. Introducción

En todos los ejemplos de este capítulo, la comunicación cliente-servidor se realizaba en modo bloqueante, también conocido como modo síncrono:

  • cuando un cliente se conecta a un servidor, espera la respuesta del servidor a esa solicitud antes de continuar.
  • Cuando un cliente lee una línea de texto enviada por el servidor, queda bloqueado hasta que el servidor la haya enviado.
  • Del lado del servidor, los hilos de servicio que atienden al cliente funcionan de la misma manera que se describió anteriormente.

En las interfaces gráficas, a menudo es necesario evitar que el usuario quede bloqueado durante operaciones que tardan mucho tiempo. El caso que se suele citar es el de la descarga de un archivo grande. Durante esta descarga, es necesario permitir que el usuario siga interactuando libremente con la interfaz gráfica.

Aquí nos proponemos reescribir el cliente TCP genérico del párrafo 11.6.3 incorporando los siguientes cambios:

  • la interfaz será gráfica
  • la herramienta de comunicación con el servidor será un objeto Socket
  • el modo de comunicación será asíncrono:
    • el cliente iniciará una conexión con el servidor, pero no se quedará bloqueado esperando a que se establezca
    • el cliente iniciará un envío al servidor, pero no se quedará bloqueado esperando a que termine
    • el cliente iniciará la recepción de datos procedentes del servidor, pero no se quedará bloqueado esperando a que esta finalice.

Recordemos en qué nivel se encuentra el objeto Socket en la comunicación cliente/servidor TCP:

La clase Socket es la que opera más cerca de la red. Permite gestionar con precisión la conexión de red. El término socket se refiere a un conector eléctrico. El término se ha ampliado para referirse a un conector de red de software. En una comunicación TCP-IP entre dos máquinas A y B, son dos sockets los que se comunican entre sí. Una aplicación puede trabajar directamente con los sockets. Este es el caso de la aplicación A mencionada anteriormente. Un socket puede ser un socket client o serveur.

11.8.2. La interfaz gráfica del cliente TCP asíncrono

La aplicación de Visual Studio es la siguiente:

  

[ClientTcpAsynchrone.cs] es la interfaz gráfica. Esta es la siguiente:

N.º
tipo
nombre
función
1
TextBox
textBoxNomServeur
nombre del servidor TCP al que conectarse
2
NumericUpDown
numericUpDownPortServeur
el puerto al que conectarse
3
RadioButton
radioButtonLF
radioButtonRCLF
para indicar el carácter de fin de línea que debe usar el cliente: LF "\n" o RCLF "\r\n"
4
Botón
buttonConnexion
para conectarse al puerto [2] del servidor [1]. El botón muestra el texto [Connecter] cuando el cliente no está conectado a un servidor, y [Déconnecter] cuando sí lo está.
5
TextBox
textBoxMsgToServeur
mensaje que se enviará al servidor una vez establecida la conexión. Cuando el usuario presiona la tecla [Entrée], el mensaje se envía con el marcador de fin de línea seleccionado en [3]
6
ListBox
listBoxEvts
lista en la que se muestran los principales eventos de la conexión cliente/servidor: conexión, desconexión, cierre de flujo, errores de comunicación
7
ListBox
listBoxDialogue
lista en la que se muestran los mensajes del diálogo entre el cliente y el servidor
8
Botón
buttonRazEvts
para borrar la lista [6]
4
Botón
buttonRazDialogue
para borrar la lista [7]

Los principios de funcionamiento de esta interfaz son los siguientes:

  • el usuario conecta su cliente TCP gráfico a un servicio TCP mediante [1, 2, 3, 4].
  • Un hilo asíncrono recibe continuamente todos los datos enviados por el servidor TCP y los muestra en la lista [7]. Este hilo está separado de las demás actividades de la interfaz.
  • El usuario puede enviar mensajes al servidor a su propio ritmo mediante [5]. Cada mensaje se envía mediante un hilo asíncrono. A diferencia del hilo de recepción, que nunca se detiene, el hilo de transmisión finaliza tan pronto como se envía el mensaje. Se utilizará un nuevo hilo asíncrono para el siguiente mensaje.
  • La comunicación entre el cliente y el servidor finaliza cuando uno de los participantes cierra la conexión. El usuario puede tomar esta iniciativa con el botón [4], que, una vez establecida la conexión, muestra el texto [Déconnecter].

A continuación se muestra una captura de pantalla de una ejecución:

  • en [1]: conexión a un servicio POP
  • en [2]: visualización de los eventos que ocurrieron durante la conexión
  • en [3]: el mensaje enviado por el servidor POP al finalizar la conexión
  • en [4]: el botón [Connecter] se ha convertido en el botón [Déconnecter]
  • en [1], se envió el comando quit al servidor POP. El servidor respondió +OK goodbye y cerró la conexión
  • en [2], se detectó este cierre por parte del servidor. El cliente cerró entonces la conexión por su parte.
  • en [3], el botón [Déconnecter] volvió a ser un botón [Connecter]

11.8.3. Conexión asíncrona al servidor

Al presionar el botón [Connecter] se ejecuta el siguiente método:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // ¿Conectarse o desconectarse?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • línea 3: el botón puede tener el texto [Connecter] o [Déconnecter].

El método de conexión es el siguiente:


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() {
            // verificaciones de datos
            string nomServeur = textBoxNomServeur.Text.Trim();
            if (nomServeur == "") {
                logEvent("indiquez le nom du serveur");
                return;
            }
            // seguimiento
            logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
            try {
                 // creación de socket
                client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                // conexión así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;
            }
        }

         // se ha establecido la conexión
        private void connecté(IAsyncResult résultat) {
            // se recupera el socket del cliente
            Socket client = résultat.AsyncState as Socket;
    ...
        }


        // seguimiento del proceso
        private void logEvent(string msg) {
....
        }
    }
}
  • línea 1: la clase Socket forma parte del espacio de nombres System.Net.Sockets.

Hay ciertos datos que deben compartirse entre varios métodos del formulario. Estos son los siguientes:

  • línea 7: «client» es el socket de comunicación con el servidor
  • líneas 6 y 8: el cliente recibirá sus mensajes en un array de bytes llamado «data».
  • línea 9: «respuesta» es la respuesta enviada por el servidor.
  • línea 10: finLigne es el marcador de fin de línea que utiliza el cliente TCP; se inicializa por defecto en RCLF, pero el usuario puede modificarlo mediante los botones de opción [3].

El procedimiento connexion de la línea 19 establece la conexión con el servidor TCP:

  • líneas 21-25: se verifica que el nombre del servidor no esté vacío. Si no es así, el evento se registra en listBoxEvts mediante el método logEvent de la línea 49.
  • línea 27: se indica que la conexión se va a realizar
  • línea 30: se crea el objeto Socket necesario para la comunicación TCP/IP. El constructor admite tres parámetros:
    • AddressFamily addressFamily: la familia de direcciones IP del cliente y del servidor; en este caso, las direcciones IPv4 (AddressFamily.InterNetwork)
    • SocketType socketType: el tipo de socket. El tipo SocketType.Stream es adecuado para conexiones TCP/IP
    • ProtocolType protocolType: el tipo de protocolo de Internet utilizado; en este caso, el protocolo TCP
  • línea 32: la conexión se realiza de forma asíncrona. Se inicia la conexión, pero la ejecución continúa sin esperar a que esta finalice. El método [Socket].BeginConnect admite cuatro parámetros:
    • IPAddress ipAddress: la dirección IP de la máquina en la que se ejecuta el servicio al que hay que conectarse
    • Int32 port: el puerto del servicio
    • AsyncCallBack asyncCallBack: AsyncCallBack es un tipo delegado:
public void AsyncCallBack(IAsyncResult ar);

El método asyncCallBack, que se pasa como tercer parámetro del método BeginConnect, debe ser un método que acepte un tipo IAsyncCallBack y que no devuelva ningún resultado. Este es el método que se llamará una vez que se haya establecido la conexión. Aquí pasamos como tercer parámetro el método connecté de la línea 41.

  • (continuación)
    • Objeto state: un objeto que se debe pasar al método asyncCallBack. Este método recibe (véase el delegado anterior) un parámetro ar de tipo IAsyncResult. El objeto state se podrá recuperar en ar.AsyncState (línea 43). Aquí pasamos como cuarto parámetro el socket del cliente.
  • línea 38: el método ha finalizado. El usuario puede volver a interactuar con la interfaz gráfica. La conexión se realiza en segundo plano, en paralelo a la gestión de los eventos de la interfaz gráfica. También en paralelo, el método connecté de la línea 41 se llamará al final de la conexión, independientemente de si esta termina correctamente o no.

El código del método connecté es el siguiente:


// se ha establecido la conexión
        private void connecté(IAsyncResult résultat) {
            // se recupera el socket del cliente
            Socket client = résultat.AsyncState as Socket;
            try {
                // se finaliza la operación asíncrona
                client.EndConnect(résultat);
                // seguimiento
                logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
                // formulario
                buttonConnexion.Text = "Déconnecter";
                // lectura asíncrona de datos procedentes del 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;
            }
}

        // recepción de datos
        private void lecture(IAsyncResult résultat) {
            // se recupera el socket del cliente
            Socket client = résultat.AsyncState as Socket;
...
        }

  • línea 4: el socket del cliente se recupera del parámetro résultat recibido por el método. Cabe recordar que este objeto es el que se pasa como cuarto parámetro del método BeginConnect.
  • línea 7: el intento de conexión se finaliza mediante el método EndConnect, al que se debe pasar el parámetro résultat recibido por el método.
  • línea 9: el evento se registra en la lista de eventos
  • línea 11: el botón [Connecter] se convierte en un botón [Déconnecter] para que el usuario pueda solicitar la desconexión.
  • línea 13: se inicializa la respuesta del servidor. Esta se actualizará mediante llamadas repetidas al método asíncrono BeginReceive.
  • línea 14: primera llamada al método asíncrono BeginReceive. Este se invoca con los siguientes parámetros:
    • byte[] buffer: el búfer en el que se colocarán los datos que se recibirán; en este caso, el búfer es data
    • int offset: a partir de qué posición del búfer se colocarán los datos que se van a recibir; en este caso, el desplazamiento es 0, c.a.d, lo que significa que los datos se colocan a partir del primer byte del búfer.
    • int size: el tamaño en bytes del búfer; en este caso, el tamaño es tailleBuffer.
    • SocketFlags socketFlags: configuración del socket; en este caso, no hay configuración
    • AsyncCallBack asyncCallBack: el método de llamada de retorno que se ejecutará cuando finalice la recepción. Esto ocurrirá ya sea porque el búfer haya recibido datos o porque se haya cerrado la conexión. Aquí, el método de llamada de retorno es el método lecture de la línea 22.
    • Objeto state: el objeto que se debe pasar al método de llamada de retorno asyncCallBack. Aquí, se vuelve a pasar el socket del cliente.

Cabe señalar que todo esto ocurre sin ninguna acción por parte del usuario, salvo la solicitud inicial de conexión con el botón [Connecter]. Al final del método connecté, se ejecuta otro método en segundo plano: el método lecture, que analizaremos a continuación.


// recepción de datos
        private void lecture(IAsyncResult résultat) {
            // se recupera el socket del cliente
            Socket client = résultat.AsyncState as Socket;
            int nbOctetsReçus = 0;
            bool erreur = false;
            try {
                // número de bytes recibidos
                nbOctetsReçus = client.EndReceive(résultat);
                if (nbOctetsReçus == 0) {
                    // el servidor ya no responde
                    logEvent("le serveur a fermé la connexion");
                }
            } catch (Exception e) {
                // Hubo un problema de recepción
                logEvent(String.Format("erreur de réception : {0}", e.Message));
                erreur = true;
            }
            // ¿Terminado?
            if (nbOctetsReçus == 0 || erreur) {
                // se desconecta al cliente si es necesario
                déconnexion();
                // se muestra el final de la respuesta
                afficherRéponseServeur(réponse, true);
                // fin de lectura
                return;
            }
            // se recuperan los datos recibidos
            string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
            // se agregan a los datos ya recibidos
            réponse += données;
            // se muestra la respuesta
            afficherRéponseServeur(réponse, false);
            // se sigue leyendo
            client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
        }
  • línea 2: el método lecture se ejecuta en segundo plano cuando el búfer data ha recibido datos o cuando el servidor ha cerrado la conexión.
  • línea 9: la solicitud asíncrona de lectura se finaliza mediante EndReceive. Una vez más, este método debe llamarse con el parámetro recibido por la función de llamada de retorno. El método EndReceive devuelve el número de bytes recibidos en el búfer de lectura.
  • línea 10: si el número de bytes es cero, significa que el servidor ha cerrado la conexión.
  • línea 12: se registra el evento en la lista de eventos
  • línea 14: se maneja cualquier excepción
  • líneas 16-17: se registra el evento en la lista de eventos y se registra el error
  • línea 20: se verifica si se debe cerrar la conexión
  • línea 22: se cierra la conexión del lado del cliente con un método déconnexion que veremos más adelante.
  • línea 24: la respuesta del servidor, c.a.d. La variable global réponse se muestra en la lista de diálogo listBoxDialogue mediante un método privado afficherRéponseServeur.
  • línea 26: fin del método asíncrono lecture
  • línea 29: los bytes recibidos se colocan en una cadena de caracteres con el formato UTF8.
  • línea 31: se agregan a la respuesta que se está construyendo
  • línea 33: la respuesta se muestra en la lista listBoxDialogue.
  • línea 35: se vuelve a esperar datos del servidor

En definitiva, el método asíncrono lecture nunca se detiene. De manera continua, lee los datos procedentes del servidor y los muestra en la lista listBoxDialogue. Solo se detiene cuando la conexión es cerrada, ya sea por el servidor o por el propio usuario.

11.8.4. Desconexión del servidor

Al presionar el botón [Déconnecter] se ejecuta el siguiente método:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // ¿Conectarse o desconectarse?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • línea 3: el botón puede tener el texto [Connecter] o [Déconnecter].

El método déconnexion se encarga de desconectar al cliente:


private void déconnexion() {
            // cierre del socket
            if (client != null && client.Connected) {
                try {
                    // seguimiento
                    logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
                    // desconexión
                    client.Shutdown(SocketShutdown.Both);
                    client.Close();
                    // formulario
                    buttonConnexion.Text = "Connecter";
                } catch (Exception ex) {
                    // seguimiento
                    logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
                }
            }
        }
  • línea 3: si el cliente existe y está conectado
  • línea 6: se notifica la desconexión en listBoxEvts. La propiedad client.RemoteEndPoint proporciona el par (dirección IP, puerto) del otro extremo de la conexión; en este caso, c.a.d corresponde al servidor.
  • línea 8: el flujo de datos del socket se cierra con el método ShutDown. El flujo de datos de un socket es bidireccional: el socket envía y recibe datos. El parámetro del método ShutDown puede ser: ShutDown.Receive para cerrar el flujo de recepción, Shutdonw.Send para cerrar el flujo de transmisión o ShutDown.Both para cerrar ambos flujos.
  • línea 9: se liberan los recursos asociados al socket
  • línea 11: el botón [Déconnecter] pasa a ser el botón [Connecter]
  • líneas 12-15: manejo de una posible excepción

11.8.5. Envío asíncrono de datos al servidor

Cuando el usuario valida el mensaje del campo textBoxMsgToServeur, se ejecuta el siguiente método:


        private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
            // ¿tecla [Entrée]?
            if (e.KeyChar == 13 && client.Connected) {
                envoyerMessage();
            }
}
  • líneas 3-5: si el usuario ha presionado la tecla [Entrée] y el socket del cliente está conectado, entonces se envía el mensaje del campo textBoxMsgToServeur mediante el método envoyerMessage.

El método envoyerMessage es el siguiente:


        private void envoyerMessage() {
            // enviar un mensaje de forma asíncrona
            // el mensaje
            byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
            // se envía
            client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
            // diálogo
            logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
            // borrar mensaje
            textBoxMsgToServeur.Clear();
}
  • línea 4: se agrega al mensaje el marcador de fin de línea del cliente y se coloca en la matriz de bytes message.
  • línea 6: se inicia una transmisión asincrónica con el método BeginSend. Los parámetros de BeginSend son idénticos a los del método BeginReceive. Al finalizar la operación de envío asíncrono del mensaje, se llamará al método écriture.
  • línea 8: el mensaje enviado se agrega a la lista listBoxDialogue para poder dar seguimiento al diálogo cliente/servidor
  • línea 10: el mensaje enviado se elimina de la interfaz gráfica

El método de devolución de llamada écriture es el siguiente:


        private void écriture(IAsyncResult résultat) {
            // resultado del envío de un mensaje
            Socket client = résultat.AsyncState as Socket;
            try {
                client.EndSend(résultat);
            } catch (Exception e) {
                // Hubo un problema con el envío
                logEvent(String.Format("erreur d'émission : {0}", e.Message));
            }
}
  • línea 4: el método de devolución de llamada écriture recibe un parámetro de resultado de tipo IAsyncResult.
  • línea 3: en el parámetro résultat, se recupera el socket del cliente. Este socket era el quinto parámetro del método BeginSend.
  • línea 5: se finaliza la operación asíncrona de envío.

No se espera a que termine la transmisión de un mensaje para devolver el control al usuario. De esta manera, el usuario puede enviar un segundo mensaje aunque la transmisión del primero aún no haya finalizado.

11.8.6. Visualización de los eventos y del diálogo cliente/servidor

Los eventos se muestran mediante el método logEvents:


        // seguimiento del proceso
        private void logEvent(string msg) {
            listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
        }

        private void logEventCallBack(string msg) {
            // visualización del mensaje
            msg = msg.Replace(finLigne, " ");
            listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
  • línea 2: el método logEvents recibe como parámetro el mensaje que se debe agregar a la lista listBoxEvts.
  • línea 3: no se puede utilizar directamente el componente listBoxEvents. De hecho, el método logEvents es invocado por dos tipos de subprocesos:
    • el hilo principal, propietario de la interfaz gráfica, por ejemplo, cuando indica que se está realizando un intento de conexión
    • un hilo secundario que realiza una operación asíncrona. Este tipo de hilo no es propietario de los componentes y su acceso a un componente C debe ser controlado mediante una operación C.Invoke. Esta operación le indica al control C que un hilo desea realizar una operación sobre él. El método Invoke admite dos parámetros:
      • una función de devolución de llamada de tipo delegate. Esta función de devolución de llamada será ejecutada por el hilo propietario de la interfaz gráfica y no por el hilo que ejecuta el método C.Invoke.
      • un objeto que se pasará a la función de devolución de llamada.

En este caso, el primer parámetro que se pasa al método Invoke es una instancia del siguiente delegado:


        public delegate void writeLog(string log);

El delegado writeLog tiene un parámetro de tipo string y no devuelve ningún resultado. El parámetro será el mensaje que se registrará en listBoxEvts.

En la línea 3, el primer parámetro pasado al método Invoke es el método logEventCallBack de la línea 6. Este se corresponde correctamente con la firma del delegado writeLog. El segundo parámetro que se pasa al método Invoke es el mensaje que se pasará como parámetro al método logEventCallBack.

La operación Invoke es una operación síncrona. La ejecución del hilo secundario queda bloqueada hasta que el hilo propietario del control ejecute el método de devolución de llamada.

  • línea 6: el método de llamada de retorno ejecutado por el hilo de la interfaz gráfica recibe el mensaje que se mostrará en el control listBoxEvts.
  • línea 9: el evento se registra en la primera posición de la lista para que los eventos más recientes aparezcan al principio de la lista.

Los mensajes del diálogo cliente/servidor se muestran mediante el método logDialogue:


        // seguimiento del diálogo
        private void logDialogue(string msg) {
            listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
        }
        private void logDialogueCallBack(string msg) {
            // visualización del mensaje
            msg = msg.Replace(finLigne, " ");
            listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}

El principio es el mismo que en el método logEvent.

Los mensajes recibidos por el cliente se muestran mediante el método afficherRéponseServeur:


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

El primer parámetro es el mensaje que se mostrará. Este mensaje puede consistir en una secuencia de líneas. De hecho, el cliente lee los datos procedentes del servidor en bloques de tailleBuffer (1024) bytes. En estos 1024 bytes, pueden encontrarse diversas líneas que se reconocen por su marcador de fin de línea "\n". La última línea puede estar incompleta, ya que su marcador de fin de línea se encuentra en los siguientes 1024 bytes. El método identifica en el mensaje las líneas que terminan con «\n» y luego le pide a logDialogue que las muestre. El segundo parámetro del método indica si se debe mostrar la última línea encontrada o dejarla en el búfer para que se complete con el mensaje siguiente. El código es bastante complejo y no resulta relevante en este contexto. Por lo tanto, no se comentará.

11.8.7. Conclusión

El mismo ejemplo podría resolverse con operaciones sincrónicas. En este caso, el aspecto asíncrono de la interfaz gráfica aporta poco al usuario. Sin embargo, si el usuario se conecta y luego se da cuenta de que el servidor «ya no responde», tiene la posibilidad de desconectarse gracias a que la interfaz gráfica sigue respondiendo a los eventos durante la ejecución de las operaciones asincrónicas. Este ejemplo, bastante complejo, nos ha permitido presentar nuevos conceptos:

  • el uso de sockets
  • el uso de métodos asincrónicos. Lo que se ha visto forma parte de un estándar. Existen otros métodos asincrónicos que funcionan según el mismo modelo.
  • la actualización de controles de una interfaz gráfica mediante subprocesos secundarios.

La comunicación TCP/IP asíncrona ofrece ventajas más significativas para un servidor que las mostradas en el ejemplo anterior. Sabemos que el servidor atiende a sus clientes mediante subprocesos secundarios. Si su grupo de subprocesos tiene N subprocesos, esto significa que solo puede atender a N clientes simultáneamente. Si los N hilos realizan todos una operación bloqueante (sincrónica), ya no hay hilos disponibles para un nuevo cliente hasta que una de las operaciones bloqueantes finalice y libere un hilo. Si en los hilos se realizan operaciones asincrónicas en lugar de sincrónicas, un hilo nunca queda bloqueado y puede reutilizarse rápidamente para nuevos clientes.

11.9. Aplicación de ejemplo, versión 8: Servidor de cálculo de impuestos

11.9.1. La arquitectura de la nueva versión

Retomamos la aplicación de cálculo de impuestos que ya se ha tratado en diversas formas. Recordemos su última versión, la de la versión 7 del párrafo 9.8.

Los datos se encontraban en una base de datos y la capa [ui] era una interfaz gráfica:

 

Vamos a retomar esta arquitectura y distribuirla en dos máquinas:

  • una máquina [serveur] alojará las capas [metier] y [dao] de la versión 7. Se creará una capa TCP/IP [serveur] [1] para permitir que los usuarios de Internet consulten el servicio de cálculo de impuestos.
  • Una máquina [client] alojará la capa [ui] de la versión 7. Se creará una capa TCP/IP [client] [2] para permitir que la capa [ui] consulte el servicio de cálculo de impuestos.

La arquitectura cambia profundamente en este punto. La versión 7 era una aplicación de Windows para una sola estación de trabajo. La versión 8 se convierte en una aplicación cliente/servidor de Internet. El servidor podrá atender a varios clientes simultáneamente.

En primer lugar, escribiremos la parte [serveur] de la aplicación.

11.9.2. El servidor de cálculo de impuestos

11.9.2.1. El proyecto de Visual Studio

El proyecto de Visual Studio será el siguiente:

  • En [1], el proyecto. En él se encuentran los siguientes elementos:
  • [ServeurImpot.cs]: el servidor TCP/IP para el cálculo del impuesto en forma de aplicación de consola.
  • [dbimpots.sdf]: la base de datos SQL Server Compact de la versión 7, descrita en el párrafo 9.8.5.
  • [App.config]: el archivo de configuración de la aplicación.
  • En [2], la carpeta [lib] contiene los archivos DLL necesarios para el proyecto:
    • [ImpotsV7-dao]: la capa [dao] de la versión 7
    • [ImpotsV7-metier]: la capa [metier] de la versión 7
    • [antlr.runtime, CommonLogging, Spring.Core] para Spring
  • en [3], las referencias del proyecto

11.9.2.2. Configuración de la aplicación

El archivo [App.config] es utilizado por Spring. Su contenido es el siguiente:


<?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>
  • líneas 16-20: configuración de la capa [dao] asociada a la base SQL Server compact
  • líneas 21-23: configuración de la capa [metier].

Este es el archivo de configuración utilizado en la capa [ui] de la versión 7. Se presentó en el párrafo 9.8.4.

11.9.2.3. Funcionamiento del servidor

Al iniciar el servidor, la aplicación del servidor crea instancias de las capas [metier] y [dao] y, a continuación, muestra una interfaz de consola de administración:

  

La consola de administración acepta los siguientes comandos:

start port
para iniciar el servicio en un puerto determinado
stop
para detener el servicio. Posteriormente, se puede reiniciar en el mismo puerto o en otro.
echo start
para activar el eco del diálogo cliente/servidor en la consola
echo stop
para desactivar el eco
status
para mostrar el estado activo o inactivo del servicio
quit
para salir de la aplicación

Iniciemos el servidor:

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

Ahora ejecutemos el cliente TCP gráfico asíncrono que analizamos anteriormente en el párrafo 11.8.

Image

El cliente está conectado. Puede enviar los siguientes comandos al servidor de cálculo de impuestos:

aide
para obtener la lista de comandos permitidos
impot marié nbEnfants salaireAnnuel
para calcular el impuesto de una persona que tiene nbEnfants hijos y un salario de salaireAnnuel euros. marié es igual a 0 si la persona está casada, y a n en caso contrario.
aurevoir
para cerrar la conexión con el servidor

A continuación se muestra un ejemplo de diálogo:

Del lado del servidor, la consola muestra lo siguiente:

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

Activemos el eco y reiniciemos un nuevo diálogo desde el cliente gráfico:

 

La consola de administración muestra entonces lo siguiente:

1
2
3
4
5
6
7
echo start
Serveur de calcul d'impôt >Début du service au client 1
<--- Client 1 : aide
---> Client 1 : Commandes acceptées
1-aide
2-impot marié(O/N) nbEnfants salaireAnnuel
3-aurevoir
  • línea 1: el eco del diálogo cliente/servidor está activado
  • línea 2: ha llegado un cliente
  • línea 3: ha enviado el comando [aide]
  • líneas 4-7: la respuesta del servidor en 4 líneas.

Detengamos el servicio:

1
2
3
stop
L'erreur suivante s'est produite sur le serveur : Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall
Serveur de calcul d'impôt >
  • línea 1: se solicita detener el servicio (no la aplicación en sí)
  • línea 2: una excepción debida a que el servidor, que estaba bloqueado en una espera de cliente, fue interrumpido bruscamente debido al cierre del servicio de escucha.
  • línea 3: el servicio ahora puede reiniciarse mediante «start port» o detenerse mediante quit.

Antes de que se detuviera el servicio de escucha, se estaba atendiendo a un cliente en otra conexión. Esta conexión no se cierra al cerrar el socket de escucha. El cliente puede seguir enviando comandos: el hilo de servicio que se le había asignado antes de que se detuviera el servicio de escucha sigue respondiéndole:

Image

11.9.3. El código del servidor TCP para el cálculo de impuestos

1
  

El código del servidor [ServeurImpot.cs] es el siguiente:


...
namespace Chap9 {
    public class ServeurImpot {

        // datos compartidos entre subprocesos y 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) {
            // instancias de las capas [metier] y [dao]
            IApplicationContext ctx = null;
            metier = null;
            try {
                // contexto de Spring
                ctx = ContextRegistry.GetContext();
                // se solicita una referencia en la capa [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");

                // configuración del grupo de subprocesos
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);

                // lee los comandos de administración del servidor ingresados desde el teclado en un bucle infinito
                string commande = null;
                string[] champs = null;
                while (true) {
                    // invita
                    Console.Write("Serveur de calcul d'impôt >");
                    // lectura de comando
                    commande = Console.ReadLine().Trim().ToLower();
                    champs = Regex.Split(commande, @"\s+");
                    // ejecución de comando
                    switch (champs[0]) {
                        case "start":
                            // ¿activo?
                            if (actif) {
                                //error
                                Console.WriteLine("Le serveur est déjà actif");
                            } else {
                                // verificación del puerto
                                if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
                                    Console.WriteLine("Syntaxe : start port. Port incorrect");
                                } else {
                                    // se inicia el servicio de escucha
                                    ThreadPool.QueueUserWorkItem(doEcoute, null);
                                }
                            }
                            break;
                        case "echo":
                            // eco de inicio/parada
                            if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
                                Console.WriteLine("Syntaxe : echo start / stop");
                            } else {
                                echo = champs[1] == "start";
                            }
                            break;
                        case "stop":
                            // fin del servicio
                            if (actif) {
                                service.Stop();
                                actif = false;
                            }
                            break;
                        case "status":
                            // estado del 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":
                            // salida de la aplicación
                            Console.WriteLine("Fin du service");
                            Environment.Exit(0);
                            break;
                        default:
                            // comando incorrecto
                            Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
                            break;
                    }
                }
            } catch (Exception e1) {
                // visualización de excepción
                Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
                return;
            }
        }


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

....
    }
}
  • líneas 18-21: las capas [metier] y [dao] son instanciadas por Spring, configurado por [App.config]. A continuación, se inicializa la variable global metier de la línea 6.
  • líneas 24-25: se configura el grupo de subprocesos de la aplicación con un mínimo y un máximo de 10 subprocesos.
  • líneas 30-86: el bucle de entrada de los comandos de administración del servicio (start, stop, quit, echo, status).
  • línea 32: indicador del servidor para cada nuevo comando
  • línea 34: lectura del comando de administración
  • línea 35: el comando se divide en campos para su análisis
  • líneas 38-52: el comando «start port», cuyo objetivo es iniciar el servicio de escucha
    • línea 40: si el servicio ya está activo, no hay nada que hacer
    • línea 45: se verifica que el puerto exista y sea correcto. Si es así, se establece la variable global port de la línea 7.
    • línea 49: el servicio de escucha será gestionado por un hilo secundario para que el hilo principal pueda seguir ejecutando los comandos de la consola. Si el método doEcoute logra establecer la conexión, se inicializan las variables globales service de la línea 8 y actif de la línea 9.
  • líneas 53-60: el comando echo start / stop que activa / desactiva el eco del diálogo cliente/servidor en la consola
    • línea 58: se establece la variable global echo de la línea 7
  • líneas 61-67: el comando stop, que detiene el servicio de escucha.
    • línea 64: se detiene el servicio de escucha
  • líneas 68-75: el comando status, que muestra el estado activo o inactivo del servicio
  • líneas 76-80: el comando quit que detiene todo.

El hilo encargado de escuchar las solicitudes de los clientes ejecuta el siguiente método doEcoute:


        private static void doEcoute(Object data) {
            // hilo de escucha de solicitudes de los clientes
            try {
                // se crea el servicio
                service = new TcpListener(IPAddress.Any, port);
                // se inicia
                service.Start();
                // el servidor está activo
                actif = true;
                // seguimiento
                Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
                // ciclo de servicio a los clientes
                TcpClient tcpClient = null;
                // n.º de cliente
                int numClient = 0;
                // bucle infinito
                while (true) {
                    // esperando a un cliente
                    tcpClient = service.AcceptTcpClient();
                    // el servicio lo realiza otra tarea
                    ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // siguiente cliente
                    numClient++;
                }
            } catch (Exception ex) {
                // se reporta el error
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            }
        }

        // información del cliente
        internal class Client {
            public TcpClient CanalTcp { get; set; }        // conexión con el cliente
            public int NumClient { get; set; }            // N.º de cliente
}

Aquí tenemos un código similar al del servidor de eco analizado en el párrafo 11.6.1. Solo comentaremos lo que es diferente:

  • línea 7: se pone en marcha el servicio de atención telefónica
  • línea 9: se observa que el servicio ya está activo

Línea 21: los clientes son atendidos por subprocesos de servicio que ejecutan el siguiente método doService:


private static void doService(Object infos) {
            // se selecciona al cliente al que se debe atender
            Client client = infos as Client;
            // se atiende al cliente
            Console.WriteLine("Début du service au client {0}", client.NumClient);
            // procesamiento de la conexión TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flujo de salida sin almacenamiento en búfer
                                writer.AutoFlush = true;
                                // envío de un mensaje de bienvenida al cliente
                                writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
                                // bucle de lectura de solicitud/escritura de respuesta
                                string demande = null;
                                bool serviceFini = false;
                                while (!serviceFini && (demande = reader.ReadLine()) != null) {
                                    // seguimiento de la consola
                                    if (echo) {
                                        Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    }
                                    // análisis de la solicitud
                                    demande = demande.Trim().ToLower();
                                    // ¿Solicitud vacía?
                                    if (demande.Length == 0) {
                                        // ¿Solicitud errónea?
                                        writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
                                        return;
                                    }

                                    // desglosamos la solicitud en campos
                                    string[] champs = Regex.Split(demande, @"\s+");
                                    // análisis
                                    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":
                                            // se calcula el impuesto
                                            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) {
                // error
                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) {
            // ¿salida a la consola?
            if (echo) {
                Console.WriteLine("---> Client {0} : {1}", numClient, message);
            }
            // envío de mensaje al cliente
            writer.WriteLine(message);
}

Una vez más, tenemos aquí un código similar al del servidor de eco analizado en el párrafo 11.6.1. Solo comentaremos lo que es diferente:

  • línea 15: una vez que el cliente se conecta, el servidor le envía un mensaje de bienvenida.
  • líneas 19-52: el bucle de lectura de los comandos del cliente. El bucle se detiene cuando el cliente envía el comando «aurevoir».
  • línea 27: caso de un comando vacío
  • línea 34: la solicitud se descompone en campos para ser analizada
  • línea 37: comando aide: el cliente solicita la lista de comandos autorizados
  • línea 40: comando impot: el cliente solicita un cálculo de impuestos. Se responde con el mensaje devuelto por el método calculImpot, que detallaremos a continuación.
  • línea 44: comando aurevoir: el cliente indica que ha terminado.
  • línea 45: nos preparamos para salir del bucle de lectura de las solicitudes del cliente (líneas 19-52)
  • línea 46: se responde al cliente con un mensaje de despedida
  • línea 48: un comando incorrecto. Se envía al cliente un mensaje de error.

El procesamiento del comando impot se lleva a cabo mediante el siguiente método calculImpot:


private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
            // pregunta sobre estado civil (S/N) nbEnfants salaireAnnuel
            // se necesitan 4 campos
            if (champs.Length != 4) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campos [1]
            string marié = champs[1];
            if (marié != "o" && marié != "n") {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campos [2]
            int nbEnfants;
            if (!int.TryParse(champs[2], out nbEnfants)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campos [3]
            int salaireAnnuel;
            if (!int.TryParse(champs[3], out salaireAnnuel)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // Está bien, calculamos el impuesto
            int impot = 0;
            try {
                impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
                return impot.ToString();
            } catch (Exception ex) {
                return ex.Message;
            }
        }
  • línea 1: el método recibe como tercer parámetro la matriz de campos del pedido impot. Si este se ha formulado correctamente, tiene la forma «impot casado nbEnfants salaireAnnuel». El método devuelve como resultado la respuesta que se debe enviar al cliente.
  • línea 4: se verifica que el comando tenga 4 campos
  • línea 8: se verifica que el campo marié sea válido
  • línea 14: se verifica que el campo nbEnfants sea válido
  • línea 19: se verifica que el campo salaireAnnuel sea válido
  • línea 25: se calcula el impuesto utilizando el método CalculerImpot de la capa [metier]. Cabe recordar que esta capa está encapsulada en una DLL.
  • línea 26: si la capa [metier] ha devuelto un resultado, este se entrega al cliente.
  • línea 28: si la capa [metier] ha lanzado una excepción, el mensaje de la misma se envía al cliente.

11.9.4. El cliente gráfico del servidor TCP de cálculo de impuestos

11.9.4.1. El proyecto « » en Visual Studio

El proyecto de Visual Studio del cliente gráfico será el siguiente:

  • En [1], los dos proyectos de la solución, uno para cada una de las dos capas de la aplicación
  • en [2], el cliente TCP que actúa como capa [metier] para la capa [ui]. Por lo tanto, utilizaremos ambos términos.
  • en [3], la capa [ui] de la versión 7, con un pequeño detalle del que hablaremos

11.9.4.2. La capa [metier]

La interfaz IImpotMetier no ha cambiado. Sigue siendo la de la versión 7:


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

La implementación de esta interfaz es la siguiente clase [ImpotMetierTcp]:


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

        // información [serveur]
        private string Serveur { get; set; }
        private int Port { get; set; }

        // cálculo del impuesto
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
                // nos conectamos al servicio
                using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flujo de salida sin almacenamiento en búfer
                                writer.AutoFlush = true;
                                // se omite el mensaje de bienvenida
                                reader.ReadLine();
                                // solicitud
                                writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
                                // respuesta
                                return int.Parse(reader.ReadLine());
                            }
                        }
                    }
                }
            }
        }
    }
  • línea 7: el nombre o la dirección IP del servidor TCP de cálculo de impuestos
  • línea 8: el puerto de escucha de este servidor
  • Spring inicializará estas dos propiedades al instanciar la clase [ImpotMetierTcp].
  • línea 11: el método de cálculo del impuesto. Cuando se ejecuta, las propiedades Serveur y Port ya están inicializadas. En el código se observa el procedimiento clásico de un cliente TCP
  • línea 13: se abre la conexión con el servidor
  • líneas 14-16: se recupera (línea 14) el flujo de red asociado a esta conexión, del cual se extraen un flujo de lectura (línea 15) y un flujo de escritura (línea 16).
  • línea 18: el flujo de escritura debe estar sin búfer
  • línea 20: aquí hay que recordar que, al abrir la conexión, el servidor envía al cliente una primera línea que es el mensaje de bienvenida «Bienvenido al servidor de cálculo de impuestos». Este mensaje se lee y se ignora.
  • línea 22: se envía al servidor el comando del tipo: impuesto o 2 60000 para solicitarle que calcule el impuesto de una persona casada con dos hijos y un salario anual de 60 000 euros.
  • línea 24: el servidor responde con el monto del impuesto en formato «4282» o con un mensaje de error si la orden no estaba bien formada (eso no sucederá aquí) o si se presentó un problema al calcular el impuesto. En este caso, este último escenario no se maneja, pero sin duda hubiera sido más «limpio» hacerlo. De hecho, si la línea leída es un mensaje de error, se lanzará una excepción porque la conversión a un entero fallará. La excepción captada por la interfaz gráfica será un error de conversión, mientras que la excepción original es de una naturaleza completamente diferente. Se invita al lector a mejorar este código.
  • Líneas 25-28: liberación de todos los recursos utilizados con una cláusula «using».

La capa [metier] se compila en DLL ImpotsV8-metier.dll:

Image

11.9.4.3. La capa [ui]

La capa [ui] [1,3] es la que se analiza en la versión 7, en el párrafo 9.8.4, con tres diferencias:

  • la configuración de la capa [metier] en [App.config] es diferente porque su implementación ha cambiado
  • la interfaz gráfica [Form1.cs] se ha modificado para mostrar una posible excepción
  • la capa [metier] se encuentra dentro de DLL [ImpotsV8-metier.dll].

El archivo [App.config] es el siguiente:


<?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>
  • línea 16: instanciación de la capa [metier] con la clase Metier.ImpotMetierTcp de la DLL ImpotsV8-metier.dll
  • líneas 17-18: se inicializan las propiedades Servidor y Puerto de la clase Metier.ImpotMetierTcp. El servidor estará en la máquina localhost y operará en el puerto 27.

La interfaz gráfica que se muestra al usuario es la siguiente:

  • En [1], se ha agregado un TextBox para mostrar una posible excepción. Este campo no existía en la versión anterior.

Aparte de este detalle, el código del formulario es el mismo que ya se analizó en el párrafo 6.4.3. Se invita al lector a consultarlo. En [2], se muestra un ejemplo de ejecución obtenido con un servidor iniciado de la siguiente manera:

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

La captura de pantalla [2] del cliente corresponde a las líneas del cliente 9 mencionadas anteriormente.

11.9.5. Conclusión

Una vez más, hemos podido reutilizar código existente, sin modificaciones (capas [metier] y [dao] del servidor) o con muy pocas modificaciones (capa [ui] del cliente). Esto fue posible gracias a nuestro uso sistemático de interfaces y a su instanciación con Spring. Si en la versión 7 hubiéramos colocado el código de negocio directamente en los manejadores de eventos de la interfaz gráfica, ese código de negocio no habría sido reutilizable. Este es el principal inconveniente de las arquitecturas de una sola capa.

Por último, cabe señalar que la capa [ui] no tiene conocimiento alguno de que es un servidor remoto el que le calcula el monto del impuesto.