Skip to content

9. Programación TCP-IP

9.1. Generalidades

9.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. Es recomendable que el lector cuente con una comprensión general del funcionamiento de las redes y, en particular, de los protocolos TCP/IP antes de abordar el desarrollo 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 proviene 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

9.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 del tipo de 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 uniformar 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.

9.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 las redes locales, por lo general se utiliza la 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 que transmite envía información por el cable 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; detecta entonces si hay o no una onda portadora, cuya presencia significarí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 equipo tiene una dirección, denominada aquí dirección física, que está grabada en la tarjeta que lo conecta al cable. A esta dirección se le llama la dirección Ethernet del equipo.

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

9.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 de identificación. 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 a partir de 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.

9.1.5. El direccionamiento en Internet

Un noeud de una red puede ser una computadora, una impresora inteligente, un servidor de archivos, o 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.

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

9.1.5.2. Los protocolos de conversión entre dirección de Internet y dirección física

Hemos visto que, al transmitir información de una máquina a otra, esta información, al atravesar la capa IP, se encapsula en paquetes. Estos 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, es posible que tenga que cambiar las direcciones IP de todos los nodos y, por lo tanto, 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.

9.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 surge 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.

9.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 la dirección 193.49.145.3 dentro de la red n.º 2.

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.

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

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

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

9.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 si los paquetes se están enrutando correctamente.

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

9.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 conveniente designar una máquina por un nombre en lugar de por su dirección IP. Entonces surge 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, es necesario 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án 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á hasta la máquina A.

XDR: (Representación de datos de 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.

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

9.2. Administración de direcciones de red

Una máquina en la red de Internet se identifica de manera única mediante una dirección IP (Protocolo de Internet) con el formato I1.I2.I3.I4, donde In es un número entre 1 y 254. También puede identificarse mediante un nombre igualmente único. Este nombre no es obligatorio, ya que las aplicaciones siempre terminan utilizando las direcciones IP de los equipos. Su función es facilitar la vida de los usuarios. Así, es más fácil, con un navegador, solicitar la dirección http://www.ibm.com (URL) que la dirección URL http://129.42.17.99, aunque ambos métodos son posibles. 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). La plataforma .NET ofrece la clase Dns para administrar las direcciones de Internet:

Image

La mayoría de los métodos de la clase son estáticos. Veamos los que nos interesan:

Overloads Public Shared Function
 GetHostByAddress(ByVal address As
 String) As IPHostEntry
Devuelve una dirección IPHostEntry a partir de una dirección IP en el formato «I1.I2.I3.I4». Lanza una excepción si no se puede encontrar la máquina address.
Public Shared Function
 GetHostByName(ByVal hostName As
 String) As IPHostEntry
devuelve una dirección IPHostEntry a partir de un nombre de máquina. Lanza una excepción si no se puede encontrar la máquina name.
Public Shared Function 
GetHostName() As String
devuelve el nombre de la máquina en la que se ejecuta el programa que ejecuta esta instrucción

Las direcciones de red del tipo IPHostEntry tienen la siguiente forma:

Las propiedades que nos interesan:

Public Property AddressList 
As IPAddress ()
Lista de direcciones IP de una máquina. Si una dirección IP designa a una y solo una máquina física, ¿puede una máquina física tener varias direcciones IP? Este será el caso si cuenta con varias tarjetas de red que la conectan a diferentes redes.
Public Property Aliases 
As String ()
Lista de alias de una máquina, que puede identificarse mediante un nombre principal y varios alias
Public Property HostName 
As String
el nombre de la máquina, si tiene uno

De la clase IPAddress destacaremos el constructor, las propiedades y los métodos siguientes:

Image

Un objeto [IPAddress] puede transformarse en una cadena I1.I2.I3.I4 con el método ToString(). A la inversa, se puede obtener un objeto IPAddress a partir de una cadena I1.I2.I3.I4 mediante el método estático IPAddress.Parse("I1.I2.I3.I4"). 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:

dos>address1
Machine Locale=tahe

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

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

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

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

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

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

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

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

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

El programa es el siguiente:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System
Imports System.Net
Imports System.Text.RegularExpressions

' módulo de prueba
Public Module adresses

    Sub Main()
        ' muestra el nombre de la máquina local
        ' y luego proporciona información de manera interactiva sobre las máquinas de la red
        ' identificadas por un nombre o una dirección IP
        ' equipo local
        Dim localHost As String = Dns.GetHostName()
        Console.Out.WriteLine(("Machine Locale=" + localHost))

        ' preguntas y respuestas interactivas
        Dim machine As String
        Dim adresseMachine As IPHostEntry
        While True
            ' ingreso del nombre del equipo buscado
            Console.Out.Write("Machine recherchée (fin pour arrêter) : ")
            machine = Console.In.ReadLine().Trim().ToLower()
            ' ¿Listo?
            If machine = "fin" Then
                Exit While
            End If

            ' ¿Dirección I1.I2.I3.I4 o nombre de la máquina?
            Dim isIPV4 As Boolean = Regex.IsMatch(machine, "^\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s*$")
            ' gestión de excepciones
            Try
                If isIPV4 Then
                    adresseMachine = Dns.GetHostByAddress(machine)
                Else
                    adresseMachine = Dns.GetHostByName(machine)
                End If
                ' el nombre
                Console.Out.WriteLine(("Machine : " + adresseMachine.HostName))
                ' las direcciones IP
                Console.Out.Write(("Adresses IP : " + adresseMachine.AddressList(0).ToString))
                Dim i As Integer
                For i = 1 To adresseMachine.AddressList.Length - 1
                    Console.Out.Write(("," + adresseMachine.AddressList(i).ToString))
                Next i
                Console.Out.WriteLine()
                ' los alias
                If adresseMachine.Aliases.Length <> 0 Then
                    Console.Out.Write(("Alias : " + adresseMachine.Aliases(0)))
                    For i = 1 To adresseMachine.Aliases.Length - 1
                        Console.Out.Write(("," + adresseMachine.Aliases(i)))
                    Next i
                    Console.Out.WriteLine()
                End If
            Catch
                ' la máquina no existe
                Console.Out.WriteLine("Impossible de trouver la machine [" + machine + "]")
            End Try
        End While
    End Sub
End Module

9.3. Programación TCP-IP

9.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 conocer varios datos:

  • 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 está contenida 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.

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

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

9.3.4. Arquitectura de un cliente

La arquitectura de un programa de red que solicita los servicios de una aplicación de 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

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

9.3.6. La clase TcpClient

La clase TcpClient es la clase adecuada para representar al cliente de un servicio TCP. Se define de la siguiente manera:

Image

Los constructores, métodos y propiedades que nos interesan son los siguientes:

Public Sub New(ByVal hostname
 As String,ByVal port As Integer)
crea una conexión TCP con el servidor 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
Public Sub Close()
cierra la conexión con el servidor TCP
Public Function GetStream()
 As NetworkStream
obtiene un flujo NetworkStream de lectura y escritura hacia el servidor. Este flujo es el que permite las comunicaciones entre el cliente y el servidor.

9.3.7. La clase NetworkStream

La clase NetworkStream representa el flujo de red entre el cliente y el servidor. La clase se define de la siguiente manera:

Image

La clase NetworkStream 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. Cuando dos máquinas se comunican, hay un objeto TcpClient en cada extremo de la conexión. El método GetStream de este objeto permite acceder al flujo de red (NetworkStream) que conecta a las dos máquinas. 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:

Dim in1 as StreamReader=new StreamReader(client1.GetStream())
Dim out1 as StreamWriter=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 respuesta nunca llegará si la línea se ha almacenado en el búfer de la máquina M1 y nunca se ha enviado. 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á:

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

9.3.8. Arquitectura básica de un cliente de Internet

Ahora contamos con los elementos necesarios para definir la arquitectura básica de un cliente de Internet:


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

    Try
      ' se conecta al servicio que opera en el puerto P de la máquina M
      client = New TcpClient(nomServeur, port)

      ' se crean los flujos de entrada y salida del cliente TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' ciclo de solicitud-respuesta
      While True
        ' se prepara la solicitud
        demande = ...
        ' se envía al servidor
        OUT.WriteLine(demande)
        ' se lee la respuesta del servidor
        réponse = [IN].ReadLine()
        ' se procesa la respuesta
        ...
            End While
            ' se ha terminado
            client.Close()
        Catch ex As Exception
      ' se maneja la excepción
...
        End Try

9.3.9. La clase TcpListener

La clase TcpListener es la clase adecuada para representar un servicio TCP. Se define de la siguiente manera:

Image

Los constructores, métodos y propiedades que nos interesan son los siguientes:

Public Sub New(ByVal localaddr 
As IPAddress,ByVal port As Integer)
crea un servicio TCP que esperará (listen) las solicitudes de los clientes en un puerto pasado como parámetro (port), denominado puerto de escucha de la máquina local con la dirección IP localadr.
Public Function AcceptTcpClient() 
As TcpClient
acepta la solicitud de un cliente. Devuelve como resultado un objeto TcpClient asociado a otro puerto, denominado puerto de servicio.
Public Sub Start()
inicia la escucha de las solicitudes de los clientes
Public Sub Stop()
deja de escuchar las solicitudes de los clientes

9.3.10. Arquitectura básica de un servidor de Internet

De lo visto anteriormente, se puede deducir la estructura básica de un servidor:


    ' se crea el servicio de escucha
    Dim ecoute As TcpListener = Nothing
    Dim port As Integer = ...
    Try
      ' se crea el servicio
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' se inicia
      ecoute.Start()
      ' bucle del servicio
      Dim liaisonClient As TcpClient = Nothing
            While not fini
                ' espera a un cliente
                liaisonClient = ecoute.AcceptTcpClient()
                ' el servicio lo presta otra tarea
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf [méthode]))
                tache.Start()
            End While
        Catch ex As Exception
            ' se reporta el error
....
        End Try
        ' fin del servicio
        ecoute.Stop()

La clase Service es un thread que podría tener el siguiente aspecto:


Public Class Service

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

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

    ' método run
    Public Sub Run()
        ' devuelve el servicio al cliente
        Try
            ' flujo de entrada
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' flujo de salida
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' bucle de lectura de solicitud/escritura de respuesta
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' se procesa la solicitud
                ...
                ' se envía la respuesta
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' siguiente solicitud
                demande = [IN].ReadLine
            End While
            ' fin de la conexión
            liaisonClient.Close()
        Catch e As Exception
            ...
        End Try
        ' fin del servicio
    End Sub

9.4. Ejemplos

9.4.1. Servidor de eco

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

serveurEcho port

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:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

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

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

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

    ' ¿hay algún argumento?
    If args.Length <> 1 Then
      erreur(syntaxe, 1)
    End If
    ' este argumento debe ser un número entero mayor que 0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(0))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
    ' se crea el servicio de escucha
    Dim ecoute As TcpListener = Nothing
    Dim nbClients As Integer = 0 ' nbre de clients traités
    Try
      ' se crea el servicio
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' se inicia
      ecoute.Start()
      ' seguimiento
      Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & port))
      Console.Out.WriteLine(ecoute.LocalEndpoint)

      ' bucle del servicio
      Dim liaisonClient As TcpClient = Nothing
            While True
                ' bucle infinito: se detendrá con Ctrl-C
                ' esperando a un cliente
                liaisonClient = ecoute.AcceptTcpClient()

                ' el servicio lo proporciona otra tarea
                nbClients += 1
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf New traiteClientEcho(liaisonClient, nbClients).Run))
                tache.Start()
            End While
            ' se vuelve a escuchar las solicitudes
        Catch ex As Exception
            ' se reporta el error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
        ' fin del servicio
        ecoute.Stop()
    End Sub

    ' visualización de errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' cierre con error
        Environment.Exit(exitCode)
    End Sub
End Class

' -------------------------------------------------------
' presta servicio a un cliente del servidor de eco
Public Class traiteClientEcho

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

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

    ' método run
    Public Sub Run()
        ' presta servicio al cliente
        Console.Out.WriteLine(("Début de service au client " & numClient))
        Try
            ' flujo de entrada
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' flujo de salida
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' bucle de lectura de solicitud/escritura de respuesta
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' seguimiento
                Console.Out.WriteLine(("Client " & numClient & " : " & demande))
                ' el servicio se detiene cuando el cliente envía un marcador de fin de archivo
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' el servicio se detiene cuando el cliente envía «fin»
                If demande.Trim().ToLower() = "fin" Then
                    Exit While
                End If
                ' siguiente solicitud
                demande = [IN].ReadLine
            End While
            ' fin de la conexión
            liaisonClient.Close()
        Catch e As Exception
            erreur("Erreur lors de la fermeture de la liaison client (" + e.ToString + ")", 2)
        End Try
        ' fin del servicio
        Console.Out.WriteLine(("Fin de service au client " & numClient))
    End Sub

    ' visualización de errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización de error
        System.Console.Error.WriteLine(msg)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub
End Class

La estructura del servidor se ajusta a la arquitectura general de los servidores TCP.

9.4.2. Un cliente para el servidor de eco

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

clientEcho nomServeur port

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.


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

Public Class clientEcho

  ' se conecta a un servidor de eco
  ' cualquier línea que se escriba en el teclado se recibe entonces como eco
  Public Shared Sub Main(ByVal args() As String)
    ' sintaxis
    Const syntaxe As String = "pg machine port"

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

    ' el puerto debe ser un número entero mayor que 0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(1))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If

        ' se puede trabajar
    Dim client As TcpClient = Nothing ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
    Dim demande As String = Nothing ' demande du client
    Dim réponse As String = Nothing ' réponse du serveur
    Try
      ' se conecta al servicio que opera en el puerto P de la máquina M
      client = New TcpClient(nomServeur, port)

      ' se crean los flujos de entrada y salida del cliente TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' bucle de solicitud-respuesta
      While True
        ' la solicitud proviene del teclado
        Console.Out.Write("demande (fin pour arrêter) : ")
        demande = Console.In.ReadLine()
        ' se envía al servidor
        OUT.WriteLine(demande)
        ' se lee la respuesta del servidor
        réponse = [IN].ReadLine()
        ' se procesa la respuesta
        Console.Out.WriteLine(("Réponse : " + réponse))
        ' ¿Listo?
        If demande.Trim().ToLower() = "fin" Then
          Exit While
                End If
            End While
            ' ya terminó
            client.Close()
        Catch ex As Exception
      ' se maneja la excepción
      erreur(ex.Message, 3)
        End Try
    End Sub

    ' se muestran los errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' cese con error
        Environment.Exit(exitCode)
    End Sub
End Class

La estructura de este cliente se ajusta a la arquitectura general de los clientes tcp.Voici; los resultados obtenidos en 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 1 se obtienen los siguientes resultados:

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

En la del cliente 2:

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

En la del servidor:

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

Cabe destacar que el servidor pudo atender a dos clientes simultáneamente.

9.4.3. Un cliente genérico TCP

Muchos de los servicios creados en los inicios de Internet funcionan según el modelo del servidor de eco analizado anteriormente: las comunicaciones entre cliente y servidor se realizan mediante el intercambio de líneas de texto. Vamos a escribir un cliente TCP genérico que se iniciará de la siguiente manera: cltgen servidor puerto

Este cliente TCP se conectará al puerto port del servidor serveur. Una vez hecho esto, creará dos subprocesos:

  1. un hilo encargado de leer los comandos ingresados desde el teclado y enviarlos al servidor
  2. un hilo encargado de leer las respuestas del servidor y mostrarlas en la pantalla

¿Por qué dos subprocesos si en la aplicación anterior no se había sentido esa necesidad? En esta última, el protocolo de diálogo 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 bucle que consiste en enviar una sola línea al servidor y recibir una sola línea enviada por el servidor no siempre es adecuado. Por lo tanto, crearemos dos bucles independientes:

  • un bucle para leer los comandos ingresados en el teclado que se enviarán al servidor. El usuario indicará el fin de los comandos con la palabra clave fin.
  • un bucle para recibir y mostrar las respuestas del servidor. Este será un bucle infinito que solo se interrumpirá cuando el servidor cierre la conexión de red o cuando el usuario, desde el teclado, escriba el comando fin.

Para que estos dos bucles estén separados, necesitamos dos subprocesos independientes. Veamos un ejemplo de ejecución en el que nuestro cliente TCP genérico se conecta a un servicio SMTP (Protocolo de transferencia SendMail). Este servicio se encarga de enviar el correo electrónico a los destinatarios. Funciona en el puerto 25 y utiliza un protocolo de comunicación basado en el intercambio de líneas de texto.

dos>cltgen istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test

ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Analicemos estos intercambios entre el cliente y el servidor:

  • el servicio SMTP envía un mensaje de bienvenida cuando un cliente se conecta a él:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • Algunos servicios cuentan con un comando help que proporciona información sobre los comandos que se pueden utilizar con el servicio. En este caso, no es así. Los comandos SMTP utilizados en el ejemplo son los siguientes:
    • mail from: expéditeur, para indicar la dirección de correo electrónico del remitente del mensaje
    • rcpt to: destinataire, para indicar la dirección de correo electrónico del destinatario del mensaje. Si hay varios destinatarios, se repite el comando rcpt to: tantas veces como sea necesario para cada uno de ellos.
    • data, que le indica al servidor SMTP que se va a enviar el mensaje. Como se indica en la respuesta del servidor, este consiste en una secuencia de líneas que termina con una línea que contiene únicamente el carácter punto. Un mensaje puede tener encabezados separados del cuerpo del mensaje por una línea en blanco. En nuestro ejemplo, hemos incluido un asunto con la palabra clave Subject:
  • Una vez enviado el mensaje, se le puede indicar al servidor que hemos terminado con el comando quit. El servidor entonces cierra la conexión réseau.Le; el hilo de lectura puede detectar este evento y detenerse.
  • A continuación, el usuario escribe «fin» en el teclado para detener también el hilo de lectura de los comandos ingresados por el teclado.

Si revisamos el correo recibido, tenemos lo siguiente (Outlook):

Image

Cabe señalar que el servicio SMTP no puede detectar si un remitente es válido o no. Por lo tanto, nunca se puede confiar en el campo from de un mensaje. En este caso, el remitente machin@univ-angers.fr no existía. Este cliente TCP genérico nos permite descubrir el protocolo de comunicación de los servicios de Internet y, a partir de ahí, crear clases especializadas para los clientes de dichos servicios. Descubramos el protocolo de comunicación del servicio POP (Post Office Protocol), que permite recuperar los correos electrónicos almacenados en un servidor. Funciona en el puerto 110.

dos>cltgen istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<--     by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<--     Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<--     by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<--     Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2                  Tl.  : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 ,  Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Los comandos principales son los siguientes:

  • user login, donde se ingresa el nombre de usuario en la máquina que aloja nuestros correos
  • pass password, donde se ingresa la contraseña asociada al nombre de usuario anterior
  • list, para obtener la lista de mensajes en formato de número y tamaño en bytes
  • retr i, para leer el mensaje n.º i
  • quit, para salir de la sesión.

Ahora veamos el protocolo de comunicación entre un cliente y un servidor web, que por lo general opera en el puerto 80:

dos>cltgen istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise  jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Un cliente web envía sus comandos al servidor según el siguiente esquema:

commande1
commande2
...
commanden
[ligne vide]

El servidor web solo responde después de recibir la línea en blanco. En el ejemplo, solo hemos utilizado un comando:

GET /index.html HTTP/1.0

que solicita al servidor el URL /index.html e indica que está trabajando con el protocolo HTTP versión 1.0. La versión más reciente de este protocolo es la 1.1. El ejemplo muestra que el servidor respondió enviando el contenido del archivo index.html y luego cerró la conexión, ya que se observa que el hilo de lectura de respuestas finaliza. Antes de enviar el contenido del archivo index.html, el servidor web envió una serie de encabezados que terminaban con una línea en blanco:

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

La línea <html> es la primera línea del archivo /index.html. Lo anterior se conoce como encabezados HTTP (Protocolo de transferencia HyperText). No vamos a detallar aquí estas cabeceras, pero recordemos que nuestro cliente genérico permite acceder a ellas, lo cual puede ser útil para comprenderlas. La primera línea, por ejemplo:

<-- HTTP/1.1 200 OK

indica que el servidor web contactado comprende el protocolo HTTP/1.1 y que ha encontrado correctamente el archivo solicitado (200 OK), siendo 200 un código de respuesta HTTP. Las líneas

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

le indican al cliente que recibirá 11 251 bytes que representan el texto HTML (lenguaje de marcado HyperText) y que, al finalizar el envío, se cerrará la conexión. Así que aquí tenemos un cliente TCP muy práctico. De hecho, este cliente ya existe en las máquinas, donde se llama telnet, pero era interesante escribirlo nosotros mismos. El programa del cliente TCP genérico es el siguiente:


' espacios de nombres
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

' la clase
Public Class clientTcpGénérique
   
    
   ' recibe como parámetro las características de un servicio en el formato
   ' servidor puerto
   ' se conecta al servicio
   ' crea un hilo para leer los comandos ingresados desde el teclado
   ' estas se enviarán al servidor
   ' crea un hilo para leer las respuestas del servidor
   ' estas se mostrarán en la pantalla
   ' todo termina con el comando «fin» escrito en el teclado
  Public Shared Sub Main(ByVal args() As String)

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

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

    ' el puerto debe ser un número entero mayor que 0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(1))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
    Dim client As TcpClient = Nothing
    ' pueden surgir problemas
    Try
      ' se conecta al servicio
      client = New TcpClient(serveur, port)
    Catch ex As Exception
      ' error
      Console.Error.WriteLine(("Impossible de se connecter au service (" & serveur & "," & port & "), erreur : " & ex.Message))
      ' fin
      Return
        End Try
        ' se crean los hilos de lectura/escritura
        Dim thReceive As New Thread(New ThreadStart(AddressOf New clientReceive(client).Run))
        Dim thSend As New Thread(New ThreadStart(AddressOf New clientSend(client).Run))

        ' se inicia la ejecución de los dos hilos
        thSend.Start()
        thReceive.Start()

        ' fin del hilo principal
        Return
    End Sub

    ' visualización de errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' se muestra un error
        System.Console.Error.WriteLine(msg)
        ' Se detiene con error
        Environment.Exit(exitCode)
    End Sub
End Class

Public Class clientSend
    ' clase encargada de leer los comandos ingresados desde el teclado
    ' y enviarlos a un servidor a través de un cliente TCP pasado al constructor
    Private client As TcpClient    ' le client tcp

    ' constructor
    Public Sub New(ByVal client As TcpClient)
        ' se registra el cliente TCP
        Me.client = client
    End Sub

    ' método Run del hilo
    Public Sub Run()

        ' datos locales
        Dim OUT As StreamWriter = Nothing        ' flux d'écriture réseau
        Dim commande As String = Nothing        ' commande lue au clavier
        ' gestión de errores
        Try
            ' creación del flujo de escritura de red
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' bucle de entrada y envío de comandos
            Console.Out.WriteLine("Commandes : ")
            While True
                ' lectura del comando ingresado por teclado
                commande = Console.In.ReadLine().Trim()
                ' ¿Terminado?
                If commande.ToLower() = "fin" Then
                    Exit While
                End If
                ' envío del comando al servidor
                OUT.WriteLine(commande)
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fin: se cierran los flujos
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' se notifica el fin del hilo
        Console.Out.WriteLine("[fin du thread d'envoi des commandes au serveur]")
    End Sub
End Class


Public Class clientReceive
    ' clase encargada de leer las líneas de texto destinadas a un 
    ' cliente TCP pasado al constructor
    Private client As TcpClient    ' le client tcp

    ' constructor
    Public Sub New(ByVal client As TcpClient)
        ' se registra el cliente TCP
        Me.client = client
    End Sub

    'constructor
    ' método Run del hilo
    Public Sub Run()

        ' datos locales
        Dim [IN] As StreamReader = Nothing        ' flux lecture réseau
        Dim réponse As String = Nothing        ' réponse serveur
        ' gestión de errores
        Try
            ' creación del flujo de lectura de red
            [IN] = New StreamReader(client.GetStream())
            ' bucle de lectura de líneas de texto del flujo IN
            While True
                ' lectura del flujo de red
                réponse = [IN].ReadLine()
                ' ¿Flujo cerrado?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' visualización
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fin: se cierran los flujos
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' se notifica el fin del hilo
        Console.Out.WriteLine("[fin du thread de lecture des réponses du serveur]")
    End Sub
End Class

9.4.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 con: srvgen portEcoute, donde portEcoute es el puerto al que deben conectarse los clientes. El servicio al cliente estará a cargo de dos subprocesos:

  • un hilo dedicado exclusivamente a leer las líneas de texto enviadas por el cliente
  • un hilo dedicado exclusivamente a leer las respuestas que el usuario escribe en el teclado. Este indicará, mediante el comando fin, que cierra la conexión con el cliente.

El servidor crea dos subprocesos por cliente. Si hay n clientes, habrá 2n subprocesos activos al mismo tiempo. 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 para comunicarse con él. La ventana del cliente es la siguiente:

dos>cltgen localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

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:

dos>srvgen 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]

Las líneas que comienzan con <-- son las enviadas del cliente al servidor. Las líneas N: son las enviadas del servidor al cliente n.º N. El servidor anterior sigue activo, mientras que el cliente 1 ya ha terminado. Se inicia un segundo cliente para el mismo servidor:

dos>cltgen localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

La ventana del servidor se ve así:

dos>srvgen 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C

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

dos>srvgen 88
Serveur générique lancé sur le port 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 luego solicitará la página /exemple.html:

Image

Ahora veamos la ventana de nuestro servidor:

dos>srvgen 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

De esta manera, descubrimos los encabezados HTTP enviados por el navegador. Esto nos permite ir desentrañando poco a poco el protocolo HTTP. En un ejemplo anterior, habíamos creado un cliente web que solo enviaba el comando GET. Eso había sido suficiente. Aquí vemos que el navegador envía otra información al servidor. Su propósito es indicarle al servidor qué tipo de cliente tiene frente a él. También vemos que los encabezados HTTP terminan con una línea en blanco. 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 dada por un servidor web en un ejemplo anterior:

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

Intentemos dar una respuesta similar:

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

Las líneas que comienzan con 2: se envían desde el servidor al cliente n.º 2. El comando fin cierra la conexión del servidor con el cliente. En nuestra respuesta nos hemos limitado a los siguientes encabezados HTTP:

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

No indicamos el tamaño del archivo que vamos a enviar (Content-Length), sino que simplemente informamos que cerraremos 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 envió. Esta última es la siguiente:

2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>

A continuación, el usuario cierra la conexión con el cliente al escribir el comando fin. El navegador sabe entonces que la respuesta del servidor ha finalizado y puede mostrarla:

Image

Si en el ejemplo anterior ejecutamos Affichage/Source para ver qué recibió el navegador, obtenemos:

Image

es decir, exactamente lo que se envió desde el servidor genérico. El código del servidor genérico TCP es el siguiente:


' espacios de nombres
Imports System
Imports System.Net
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

Public Class serveurTcpGénérique

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

        ' recibe el puerto de escucha de las solicitudes de los clientes
        ' crea un hilo para leer las solicitudes del cliente
        ' estas se mostrarán en la pantalla
        ' crea un hilo para leer los comandos ingresados con el teclado
        ' estas se enviarán como respuesta al cliente
        ' todo termina con el comando «fin» ingresado desde el teclado

        Const syntaxe As String = "Syntaxe : pg port"

        ' ¿Hay algún argumento?
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' este argumento debe ser un número entero mayor que 0
        Dim port As Integer = 0
        Dim erreurPort As Boolean = False
        Dim E As Exception = Nothing
        Try
            port = Integer.Parse(args(0))
        Catch ex As Exception
            E = ex
            erreurPort = True
        End Try
        erreurPort = erreurPort Or port <= 0
        If erreurPort Then
            erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
        End If
        ' se crea el servicio de escucha
        Dim ecoute As TcpListener = Nothing
        Dim nbClients As Integer = 0     ' nbre de clients traités
        Try
            ' se crea el servicio
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
            ' se inicia
            ecoute.Start()
            ' seguimiento
            Console.Out.WriteLine(("Serveur générique lancé sur le port " & port))

            ' bucle de servicio a los clientes
            Dim client As TcpClient = Nothing
            While True        ' boucle infinie - sera arrêtée par Ctrl-C
                ' espera de un cliente
                client = ecoute.AcceptTcpClient()

                ' el servicio se ejecuta en subprocesos separados
                nbClients += 1
                ' hilo de lectura de las solicitudes de los clientes
                Dim thReceive As New Thread(New ThreadStart(AddressOf New serveurReceive(client, nbClients).Run))
                ' hilo de lectura de las respuestas que el usuario escribe en el teclado
                Dim thSend As New Thread(New ThreadStart(AddressOf New serveurSend(client, nbClients).Run))

                ' se inicia la ejecución de los dos hilos
                thSend.Start()
                thReceive.Start()
            End While
            ' se vuelve a escuchar las solicitudes
        Catch ex As Exception
            ' se reporta el error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub

    ' visualización de los errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' se muestra el error
        System.Console.Error.WriteLine(msg)
        ' se detiene con error
        Environment.Exit(exitCode)
    End Sub
End Class


Public Class serveurSend
    ' clase encargada de leer las respuestas ingresadas con el teclado
    ' y enviarlas a un cliente a través de un cliente TCP pasado al constructor
    Private client As TcpClient    ' le client tcp
    Private numClient As Integer    ' n° de client

    ' constructor
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' se registra el cliente TCP
        Me.client = client
        ' y su número
        Me.numClient = numClient
    End Sub

    ' método Run del hilo
    Public Sub Run()

        ' datos locales
        Dim OUT As StreamWriter = Nothing        ' flux d'écriture réseau
        Dim réponse As String = Nothing        ' réponse lue au clavier
        ' seguimiento
        Console.Out.WriteLine(("Thread de lecture des réponses du serveur au client " & numClient & " lancé"))
        ' gestión de errores
        Try
            ' creación del flujo de escritura de red
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' bucle de entrada y envío de comandos
            While True
                ' identificación del cliente
                Console.Out.Write((numClient & " : "))
                ' lectura de la respuesta escrita con el teclado
                réponse = Console.In.ReadLine().Trim()
                ' ¿Ya terminaste?
                If réponse.ToLower() = "fin" Then
                    Exit While
                End If
                ' envío de la respuesta al servidor
                OUT.WriteLine(réponse)
            End While
            ' respuesta siguiente
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fin: se cierran los flujos
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' se notifica el fin del hilo
        Console.Out.WriteLine(("[fin du Thread de lecture des réponses du serveur au client " & numClient & "]"))
    End Sub
End Class

Public Class serveurReceive
    ' clase encargada de leer las líneas de texto enviadas al servidor 
    ' a través de un cliente TCP pasado al constructor
    Private client As TcpClient     ' le client tcp
    Private numClient As Integer    ' n° de client

    ' constructor
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' se registra el cliente TCP
        Me.client = client
        ' y su número
        Me.numClient = numClient
    End Sub

    ' método Run del hilo
    Public Sub Run()
        ' datos locales
        Dim [IN] As StreamReader = Nothing        ' flux lecture réseau
        Dim réponse As String = Nothing        ' réponse serveur
        ' seguimiento
        Console.Out.WriteLine(("Thread de lecture des demandes du client " & numClient & " lancé"))
        ' gestión de errores
        Try
            ' creación del flujo de lectura de red
            [IN] = New StreamReader(client.GetStream())
            ' bucle de lectura de líneas de texto del flujo IN
            While True
                ' lectura del flujo de red
                réponse = [IN].ReadLine()
                ' ¿Flujo cerrado?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' visualización
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fin: se cierran los flujos
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' se notifica el fin del hilo
        Console.Out.WriteLine(("[fin du Thread de lecture des demandes du client " & numClient & "]"))
    End Sub
End Class

9.4.5. Un cliente web

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/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

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

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • El primer encabezado indica qué página queremos
  • el segundo, a qué servidor le enviamos la solicitud
  • el tercero, que queremos que el servidor cierre la conexión después de respondernos.

Si en el ejemplo anterior reemplazamos GET por HEAD, el servidor solo nos enviará los encabezados HTTP y no la página HTML.

Nuestro cliente web se llamará de la siguiente manera: clientweb URL cmd, donde URL es laURL deseada y cmd una de las dos palabras clave GET o HEAD para indicar si solo queremos los encabezados (HEAD) o también el contenido de la página (GET). Veamos un primer ejemplo. Iniciamos el servidor IIS y luego el navegador web en la misma máquina:

dos>clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private

La respuesta

HTTP/1.1 302 Object moved

significa que la página solicitada ha cambiado de ubicación (es decir, de URL). La nueva ubicación, URL, se indica en el encabezado Location:

Location: /IISSamples/Default/welcome.htm

Si utilizamos GET en lugar de HEAD en la llamada al cliente web:

dos>clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private

<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

Obtenemos el mismo resultado que con HEAD, además del cuerpo de la página HTML. El programa es el siguiente:


' espacios de nombres
Imports System
Imports System.Net.Sockets
Imports System.IO


Public Class clientWeb1

    ' solicita un URL
    ' muestra el contenido de esta en la pantalla
    Public Shared Sub Main(ByVal args() As String)
        ' sintaxis
        Const syntaxe As String = "pg URI GET/HEAD"

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

        ' verificación de la validez del URI
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI incorrecto
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try
        ' verificación del pedido
        If commande <> "GET" And commande <> "HEAD" Then
            ' pedido incorrecto
            erreur("Le second paramètre doit être GET ou HEAD", 3)
        End If

        ' se puede trabajar
        Dim client As TcpClient = Nothing        ' le client
        Dim [IN] As StreamReader = Nothing        ' le flux de lecture du client
        Dim OUT As StreamWriter = Nothing        ' le flux d'écriture du client
        Dim réponse As String = Nothing        ' réponse du serveur
        Try
            ' nos conectamos al servidor
            client = New TcpClient(uri.Host, uri.Port)

            ' se crean los flujos de entrada y salida del cliente TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True

            ' se solicita el URL - envío de los encabezados HTTP
            OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
            OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
            OUT.WriteLine("Connection: close")
            OUT.WriteLine()
            ' se lee la respuesta
            réponse = [IN].ReadLine()
            While Not (réponse Is Nothing)
                ' se procesa la respuesta
                Console.Out.WriteLine(réponse)
                ' se lee la respuesta
                réponse = [IN].ReadLine()
            End While
            ' se ha completado
            client.Close()
        Catch e As Exception
            ' se maneja la excepción
            erreur(e.Message, 4)
        End Try
    End Sub

    ' se muestran los errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' cese con error
        Environment.Exit(exitCode)
    End Sub
End Class

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. Se construye un objeto Uri a partir de la cadena URIstring recibida como parámetro:


        ' verificación de la validez del URI
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI incorrecto
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

Si la cadena URI recibida como parámetro no es una URI válida (falta el protocolo, el 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 distintos elementos de este URI. Así, si el objeto uri del código anterior se construyó a partir de la cadena http://serveur:port/cheminPageHTML?param1=val1;param2=val2;... tendremos:

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

9.4.6. Cliente web que maneja las redirecciones

El cliente web anterior no maneja una posible redirección de la solicitud URL que él mismo realizó. El siguiente cliente sí la maneja.

  1. Lee la primera línea de los encabezados HTTP enviados por el servidor para verificar si contiene la cadena «302 Object moved», que indica una redirección
  2. Lee los encabezados siguientes. Si hay una redirección, busca la línea «Location: url», que proporciona la nueva URL de la página solicitada, y toma nota de esta URL.
  3. Muestra el resto de la respuesta del servidor. Si hay redirección, se repiten los pasos 1 a 3 con la nueva URL. El programa no acepta más de una redirección. Este límite está definido por una constante que se puede modificar.

A continuación se muestra un ejemplo:

dos>clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private

<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781

<html>

<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>

El programa es el siguiente:


' espacios de nombres
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

' clase de cliente web
Public Class clientWeb
   
  ' solicita un URL y muestra su contenido en pantalla
  Public Shared Sub Main(ByVal args() As String)
    ' sintaxis
    Const syntaxe As String = "pg URI GET/HEAD"

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

    ' verificación de la validez del URI
    Dim uri As Uri = Nothing
    Try
      uri = New Uri(URIstring)
    Catch ex As Exception
      ' URI incorrecto
      erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
    End Try 'catch
    ' verificación del pedido
    If commande <> "GET" And commande <> "HEAD" Then
      ' orden incorrecta
      erreur("Le second paramètre doit être GET ou HEAD", 3)
    End If

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

        ' manejo de errores
    Try
      ' se pueden solicitar varios URL si hay redirecciones
      While nbRedirs <= nbRedirsMax
        ' se establece conexión con el servidor
        client = New TcpClient(uri.Host, uri.Port)

        ' se crean los flujos de entrada y salida del cliente TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' se envían los encabezados HTTP para solicitar el URL
        OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
        OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
        OUT.WriteLine("Connection: close")
        OUT.WriteLine()

        ' se lee la primera línea de la respuesta
        premièreLigne = [IN].ReadLine()
        ' salida en pantalla
        Console.Out.WriteLine(premièreLigne)

        ' ¿redirección?
        If Regex.IsMatch(premièreLigne, "302 Object moved$") Then
          ' hay una redirección
          redir = True
          nbRedirs += 1
                End If

                ' siguientes encabezados HTTP hasta encontrar la línea vacía que indica el final de los encabezados
                Dim locationFound As Boolean = False
                réponse = [IN].ReadLine()
                While réponse <> ""
                    ' se muestra la respuesta
                    Console.Out.WriteLine(réponse)
                    ' si hay redirección, se busca el encabezado Location
                    If redir And Not locationFound Then
                        ' se compara la línea con la expresión relacional «location»
                        Dim résultat As Match = location.Match(réponse)
                        If résultat.Success Then
                            ' 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
                        End If
                    End If
                    ' línea siguiente
                    réponse = [IN].ReadLine()
                End While

                ' líneas siguientes de la respuesta
                Console.Out.WriteLine(réponse)
                réponse = [IN].ReadLine()
                While Not (réponse Is Nothing)
                    ' se muestra la respuesta
                    Console.Out.WriteLine(réponse)
                    ' línea siguiente
                    réponse = [IN].ReadLine()
                End While

                ' se cierra la conexión
                client.Close()
                ' ¿Ya terminamos?
                If Not locationFound Or nbRedirs > nbRedirsMax Then
                    Exit While
                End If

                ' Hay que realizar una redirección; se genera la nueva URI
                URIstring = uri.Scheme + "://" & uri.Host & ":" & uri.Port & locationString
                uri = New Uri(URIstring)
                ' seguimiento
                Console.Out.WriteLine((ControlChars.Lf + "<--Redirection vers l'URL " + URIstring + "-->" + ControlChars.Lf))
            End While
        Catch e As Exception
      ' se maneja la excepción
      erreur(e.Message, 4)
        End Try
    End Sub

    ' Visualización de errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' Visualización del error
        System.Console.Error.WriteLine(msg)
        ' cancelación por error
        Environment.Exit(exitCode)
    End Sub
End Class

9.4.7. Servidor de cálculo de impuestos

Retomamos el ejercicio IMPOTS, que ya se ha abordado de diversas formas. Recordemos la última versión. Se ha creado una clase de impuesto. Sus atributos son tres tablas de números:


Public Class impôt
    ' los datos necesarios para el cálculo del impuesto
    ' provienen de una fuente externa
    Private limites(), coeffR(), coeffN() as double

La clase tiene dos constructores:

  • un constructor al que se le pasan los tres tableros de datos necesarios para calcular el impuesto
     // constructor 1
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
         ' inicializa las tres tablas de límites, coeffR, coeffN a partir de
         ' los parámetros pasados al constructor
  • un generador al que se le pasa el nombre DSN de una base de datos ODBC

    ' constructor 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' inicializa los tres arreglos de límites, coeffR, coeffN a partir de
        ' del contenido de la tabla Timpots de la base de datos ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
        ' puede generar una excepción

Se había escrito un programa de prueba:

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

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

En este caso, el programa de prueba y el objeto impôt se encontraban en la misma máquina. Nos proponemos colocar el programa de prueba y el objeto impôt en máquinas diferentes. Tendremos una aplicación cliente-servidor en la que el objeto remoto impôt será el servidor. La nueva clase se llama ServeurImpots y deriva de la clase impôt:


Public Class ServeurImpots
    Inherits impôt

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

    ' constructor
    Public Sub New(ByVal portEcoute As Integer, ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        MyBase.New(DSNimpots, Timpots, colLimites, colCoeffR, colCoeffN)
        ': se observa el puerto de escucha
        Me.portEcoute = portEcoute
        ' por el momento inactivo
        actif = False
        ' crea y ejecuta un hilo para leer los comandos ingresados desde el teclado
        ' el servidor se administrará a partir de estos comandos
        Dim threadLecture As Thread = New Thread(New ThreadStart(AddressOf admin))
        threadLecture.Start()
    End Sub

El único parámetro nuevo en el constructor es el puerto de escucha para las solicitudes de los clientes. Los demás parámetros se pasan directamente a la clase base impôt. El servidor de impuestos se controla mediante comandos ingresados desde el teclado. Por lo tanto, creamos un hilo para leer estos comandos. Habrá dos posibles: start para iniciar el servicio y stop para detenerlo definitivamente. El método admin que maneja estos comandos es el siguiente:


    Public Sub admin()
        ' lee los comandos de administración del servidor ingresados desde el teclado
        ' en un bucle infinito
        Dim commande As String = Nothing
        While True
            ' invita
            Console.Out.Write("Serveur d'impôts>")
            ' lectura del comando
            commande = Console.In.ReadLine().Trim().ToLower()
            ' ejecución de comando
            If commande = "start" Then
                ' ¿activo?
                If actif Then
                    'error
                    Console.Out.WriteLine("Le serveur est déjà actif")
                Else
                    ' se inicia el servicio de escucha
                    Dim threadEcoute As Thread = New Thread(New ThreadStart(AddressOf ecoute))
                    threadEcoute.Start()
                End If
            Else
                If commande = "stop" Then
                    ' fin de todos los subprocesos de ejecución
                    Environment.Exit(0)
                Else
                    ' error
                    Console.Out.WriteLine("Commande incorrecte. Utilisez (start,stop)")
                End If
            End If
        End While
    End Sub

Si el comando ingresado en el teclado es start, se inicia un hilo para escuchar las solicitudes de los clientes. Si el comando ingresado es stop, se detienen todos los hilos. El hilo de escucha ejecuta el método ecoute:


    Public Sub ecoute()
        ' hilo de escucha de solicitudes de los clientes
        ' se crea el servicio de escucha
        Dim ecoute As TcpListener = Nothing
        Try
            ' se crea el servicio
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), portEcoute)
            ' se inicia
            ecoute.Start()
            ' seguimiento
            Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & portEcoute))

            ' bucle de servicio
            Dim liaisonClient As TcpClient = Nothing
            While True            ' boucle infinie
                ' espera de un cliente
                liaisonClient = ecoute.AcceptTcpClient()
                ' el servicio lo realiza otra tarea
                Dim threadClient As Thread = New Thread(New ThreadStart(AddressOf New traiteClientImpots(liaisonClient, Me).Run))
                threadClient.Start()
            End While
            ' se vuelve a escuchar las solicitudes
        Catch ex As Exception
            ' se reporta el error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub

    ' visualización de los errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' cierre con error
        Environment.Exit(exitCode)
    End Sub

Encontramos un servidor TCP convencional que escucha en el puerto portEcoute. Las solicitudes de los clientes son procesadas por el método Run de un objeto al que se le pasan dos parámetros:

  1. el objeto TcpClient, que permitirá llegar al cliente
  2. el objeto impôt this, que dará acceso al método this.calculer para el cálculo del impuesto.

' -------------------------------------------------------
' presta servicio a un cliente del servidor de impuestos
Public Class traiteClientImpots

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

    ' creador
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal objImpôt As impôt)
        Me.liaisonClient = liaisonClient
        Me.objImpôt = objImpôt
    End Sub

El método Run procesa las solicitudes de los clientes. Estas pueden presentarse de dos formas:

  1. cálculo para casados (s/n) nbEnfants salaireAnnuel
  2. cálculos

La forma 1 permite calcular un impuesto; la forma 2 cierra la conexión entre el cliente y el servidor.


    ' método Run
    Public Sub Run()
        ' presta servicio al cliente
        Try
            ' flujo de entrada
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' flujo de salida
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' envío de un mensaje de bienvenida al cliente
            OUT.WriteLine("Bienvenue sur le serveur d'impôts")

            ' bucle de lectura de solicitud/escritura de respuesta
            Dim demande As String = Nothing
            Dim champs As String() = Nothing            ' les éléments de la demande
            Dim commande As String = Nothing            ' la commande du client : calcul ou fincalculs
            demande = [IN].ReadLine()
            While Not (demande Is Nothing)
                ' se descompone la solicitud en campos
                champs = Regex.Split(demande.Trim().ToLower(), "\s+")
                ' dos solicitudes aceptadas: cálculo y fin de cálculos
                commande = champs(0)
                Dim erreur As Boolean = False
                If commande <> "calcul" And commande <> "fincalculs" Then
                    ' error del cliente
                    OUT.WriteLine("Commande incorrecte. Utilisez (calcul,fincalculs).")
                End If
                If commande = "calcul" Then
                    calculerImpôt(champs)
                End If
                If commande = "fincalculs" Then
                    ' mensaje de despedida al cliente
                    OUT.WriteLine("Au revoir...")
                    ' liberación de recursos
                    Try
                        OUT.Close()
                        [IN].Close()
                        liaisonClient.Close()
                    Catch
                    End Try
                    ' fin
                    Return
                End If
                ' nueva solicitud
                demande = [IN].ReadLine()
            End While
        Catch e As Exception
            erreur("L'erreur suivante s'est produite (" + e.ToString + ")", 2)
        End Try
    End Sub

El cálculo del impuesto se realiza mediante el método calculerImpôt, que recibe como parámetro la tabla de campos de la solicitud enviada por el cliente. Se verifica la validez de la solicitud y, si corresponde, se calcula el impuesto y se devuelve al cliente.


    ' cálculo de impuestos
    Public Sub calculerImpôt(ByVal champs() As String)
        ' procesa la solicitud: cálculo para casados nbEnfants salaireAnnuel
        ' desglosada en campos en la tabla de campos
        Dim marié As String = Nothing
        Dim nbEnfants As Integer = 0
        Dim salaireAnnuel As Integer = 0

        ' validez de los argumentos
        Try
            ' se requieren al menos 4 campos
            If champs.Length <> 4 Then
                Throw New Exception
            End If
            ' casado
            marié = champs(1)
            If marié <> "o" And marié <> "n" Then
                Throw New Exception
            End If
            ' hijos
            nbEnfants = Integer.Parse(champs(2))
            ' salario
            salaireAnnuel = Integer.Parse(champs(3))
        Catch
            OUT.WriteLine(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel")
            ' fin
            Exit Sub
        End Try
        ' se puede calcular el impuesto
        Dim impot As Long = objImpôt.calculer(marié = "o", nbEnfants, salaireAnnuel)
        ' se envía la respuesta al cliente
        OUT.WriteLine(impot.ToString)
    End Sub

    ' visualización de errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización de error
        System.Console.Error.WriteLine(msg)
        ' se detiene con error
        Environment.Exit(exitCode)
    End Sub

Esta clase es compilada por

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

donde impots.dll contiene el código de la clase impôt. Un programa de prueba podría ser el siguiente:


' espacios de nombres
Imports System
Imports System.IO
Imports Microsoft.VisualBasic

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

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

        ' se requieren 6 argumentos
        If args.Length <> 6 Then
            erreur(syntaxe, 1)
        End If
        ' el puerto debe ser un número entero mayor que 0
        Dim port As Integer = 0
        Dim erreurPort As Boolean = False
        Dim E As Exception = Nothing
        Try
            port = Integer.Parse(args(0))
        Catch ex As Exception
            E = ex
            erreurPort = True
        End Try
        erreurPort = erreurPort Or port <= 0
        If erreurPort Then
            erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
        End If
        ' se crea el servidor de impuestos
        Try
            Dim srvimots As ServeurImpots = New ServeurImpots(port, args(1), args(2), args(3), args(4), args(5))
        Catch ex As Exception
            'error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
    End Sub

    ' visualización de errores
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' se detuvo con error
        Environment.Exit(exitCode)
    End Sub
End Class

Se le pasan al programa de prueba los datos necesarios para construir un objeto ServeurImpots y, a partir de ahí, este crea dicho objeto. Este programa de prueba se compila mediante:

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

Esta es una primera prueba:

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

La línea

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

crea un objeto ServeurImpots que aún no está a la espera de solicitudes de los clientes. Es el comando «start», ingresado desde el teclado, el que inicia esta escucha. El comando «stop» detiene el servidor. Ahora utilicemos un cliente. Usaremos el cliente genérico creado anteriormente. El servidor está en funcionamiento:

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

El cliente genérico se inicia en otra ventana de DOS:

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

Se observa que el cliente ha recibido correctamente el mensaje de bienvenida del servidor. Enviamos otros comandos:

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

Regresamos a la ventana del servidor para detenerlo:

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