Skip to content

9. Programación TCP-IP

9.1. Generalidades

9.1.1. Los protocolos de Internet

A continuación ofrecemos una introducción a los protocolos de comunicación de Internet, también denominados conjunto de protocolos TCP/IP (Protocolo de control de transferencia / Protocolo de Internet), por el nombre de los dos protocolos principales. Es recomendable que el lector tenga una comprensión general del funcionamiento de las redes y, en particular, de los protocolos TCP/IP antes de abordar la construcción de aplicaciones distribuidas.

El texto que sigue es una traducción parcial de un texto que se encuentra en el documento «Lan Workplace for Dos - Administrator's Guide» de NOVELL, documento de principios de los años 90.

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

El concepto general de crear una red de ordenadores heterogéneos 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 a máquinas heterogéneas comunicarse entre sí. Estos protocolos se probaron en una red denominada ARPAnet, red que posteriormente pasó a ser 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 hardware utilizado.

La red diseñada por el DARPA y gestionada 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 un ordenador 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 situadas en máquinas A y B diferentes quieren 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, pasa, según 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 sometió en la máquina remitente.

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 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
Garantiza la transmisión de bits sobre un soporte físico. En esta capa se encuentran equipos terminales de procesamiento de datos (E.T.T.D), como terminales u ordenadores, así como equipos de terminación de circuitos de datos (E.T.C.D), como moduladores/demoduladores, multiplexores y concentradores. Los puntos de interés a este nivel son:
. la elección de la codificación de la información (analógica o digital)
. la elección del modo de transmisión (sincrónico o asincrónico).
Liaison de données
Oculta las características físicas de la capa física. Detecta y corrige los errores de transmisión.
Réseau
Gestiona la ruta que debe seguir la información enviada a través de la red. A esto se le denomina routage: determinar la ruta que debe seguir la información para llegar 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 ofrece esta capa puede ser la multiplexación: la capa de transporte podrá 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 uniformizar 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. Una vez llegados a la capa Présentation de la máquina destinataria B, que los reconocerá gracias a su formato estándar, se formatearán de otra manera 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 materializado. La suite de protocolos TCP/IP se aproxima a él con la siguiente forma:

Capa física

En las redes locales, suele encontrarse tecnología Ethernet o Token-Ring. Aquí solo presentamos 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á constituida físicamente por un cable coaxial de aproximadamente 1,27 cm de diámetro y una longitud máxima de 500 m. Se puede ampliar mediante répéteurs, sin que dos equipos puedan estar separados por más de dos repetidores. El cable es pasivo: todos los elementos activos se encuentran en los equipos conectados al cable. Cada equipo está conectado 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 al ordenador para su procesamiento, o viceversa.

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

  • Capacidad de 10 megabits por segundo.
  • Topología en bus: todos los equipos están conectados al mismo cable
  • Red de difusión: un equipo emisor transmite información por el cable con la dirección del equipo destinatario. Todos los equipos conectados reciben entonces esta información y solo el destinatario la conserva.
  • El método de acceso es el siguiente: el transmisor que desea emitir escucha el cable y detecta si hay o no una onda portadora, cuya presencia indicaría que hay una transmisión en curso. Se trata de la técnica CSMA (Carrier Sense Multiple Access). En ausencia de 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 colisión. El transmisor detecta esta situación: al mismo tiempo que emite por el cable, escucha lo que realmente pasa por él. Si detecta que la información que transita por el cable no es la que él ha emitido, deduce que hay una colisión y dejará de emitir. Los demás transmisores que estaban emitiendo harán lo mismo. Cada uno reanudará su transmisión tras un tiempo aleatorio que depende de cada transmisor. Esta técnica se denomina CD (detección de colisión). El método de acceso se denomina así CSMA/CD.
  • Un direccionamiento de 48 bits. Cada máquina tiene una dirección, denominada aquí dirección física, que está inscrita en la tarjeta que la conecta al cable. A esta dirección se le denomina dirección Ethernet de la máquina.

Capa de red

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

IP (Internet Protocol)
Transmite paquetes entre dos nodos de la red
ICMP
(Internet Control Message Protocol)
ICMP establece la comunicación entre el programa del protocolo IP de una máquina y el de otra máquina. Se trata, por tanto, de 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 del equipo y la dirección de Internet del equipo

Capas de transporte/sesión

En esta capa se encuentran los siguientes protocolos:

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

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

Aquí se encuentran diversos protocolos:

TELNET
Emulador de terminal que permite a una máquina A conectarse 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 del conocimiento de 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 a una máquina «ver» 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 al equipo destinatario, 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 esquema muestra el recorrido de la información:

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

  • La aplicación envía una secuencia de bytes que se transmitirá a la capa transport.
  • La capa transport divide esta secuencia de bytes en segments y TCP, y añade al principio de cada segmento su número. 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. En la cabecera de este paquete, coloca las direcciones de Internet de los equipos de origen y destino. También determina la dirección física del equipo destinatario. Todo ello se pasa a la capa de enlace de datos y enlace físico, es decir, a la tarjeta de red que conecta el equipo 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 el equipo de destino, 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 comprueba 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 declara correcto, la capa IP desencapsula el segmento TCP que contiene y lo pasa a la capa superior transport.
  • La capa transport, 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 comprobación para el segmento TCP. Si se considera correcta, 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 un ordenador, una impresora inteligente, un servidor de archivos, cualquier cosa, de hecho, 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 hardware y de la red utilizada. Se trata de una dirección de 4 bytes que identifica tanto una red local como un nodo de dicha red. La dirección de Internet se representa habitualmente en forma de 4 números, valores de los 4 bytes, separados por un punto. Así, la dirección del equipo Lagaffe de la Facultad de Ciencias de Angers es 193.49.144.1 y la del equipo Liny, 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, un equipo de una red A puede comunicarse con un equipo de una 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.

Las direcciones IP deben ser todas diferentes. En Francia, es el INRIA el que se encarga de asignar las direcciones IP. De hecho, este organismo asigna una dirección a 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 entonces asignar las direcciones IP 193.49.144.1 a 193.49.144.254 como considere oportuno. Esta dirección suele registrarse en un archivo específico de cada máquina conectada a la red.

9.1.5.1. Las clases de direcciones IP

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

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

Según el tamaño de estos dos campos, las direcciones IP se dividen en 3 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 un equipo en esa red

Más concretamente, 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 224 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 un equipo de esa red

Más concretamente, 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, podemos tener 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 un equipo en esa red

Más concretamente, 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, podemos tener 221 redes de clase C con hasta 256 nodos.

Dado que la dirección de la máquina Lagaffe de la Facultad de Ciencias de Angers es 193.49.144.1, vemos que el byte de peso mayor 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 de la red. Son aquellas en las que la dirección del nodo se pone a 0. Así, la dirección 193.49.144.0 es la dirección IP de la red de la Facultad de Ciencias de Angers. En consecuencia, ningún nodo de una red puede tener la dirección cero.
  • Cuando en una dirección IP la dirección del nodo solo contiene 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 Dirección de Internet <--> Dirección física

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

Por lo tanto, el paquete IP contiene las direcciones de Internet de los equipos de origen y destino. Cuando este paquete se transmite a la capa encargada de enviarlo a la red física, se le añade 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 encuentra la dirección física de los equipos de origen y destino. ¿Cómo se obtienen?

El equipo remitente, que conoce la dirección IP del equipo con el que desea comunicarse, obtiene la dirección física de este último utilizando un protocolo específico denominado ARP (Address Resolution Protocol).

  • 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 encargado 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 puede hacerlo? Ha encontrado en el paquete las direcciones IP y la dirección física del remitente.
  • El remitente recibe así la dirección física que buscaba. La almacena en memoria para poder utilizarla posteriormente si hay que enviar otros paquetes al mismo destinatario.

La dirección IP de una máquina suele estar 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 una memoria de la tarjeta de red y no se puede cambiar.

Cuando un administrador desea organizar su red de otra manera, puede verse obligado a cambiar las direcciones IP de todos los nodos y, por lo tanto, a editar los distintos archivos de configuración de los distintos nodos. 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: en ese caso, se introduce 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 denominado RARP (Reverse Address Resolution Protocol). A continuación, envía a la red un paquete especial denominado paquete RARP, análogo al paquete ARP anterior, en el que incluye su dirección física. Este paquete se envía a todos los nodos, que reconocen entonces un paquete RARP. Uno de ellos, denominado servidor RARP, posee 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 debe tener normalmente 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 denominada 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 durante su emisión o recepción. Este tipo de paquete concreto 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 los equipos de origen y destino. De este modo, el equipo destinatario sabe quién le envía un mensaje.

A diferencia de una trama de red, cuya longitud viene determinada por las características físicas de la red por la que transita, la longitud del datagrama IP la fija 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 puesto como ejemplo la trama física de una red Ethernet:

Las tramas físicas circulan de nodo en nodo hacia su destino, que puede no estar en la misma red física que la máquina remitente. Por lo tanto, el paquete IP puede encapsularse sucesivamente en diferentes tramas físicas en los nodos que conectan dos redes de tipo diferente. También es posible que el paquete IP sea demasiado grande para encapsularse en una trama física. El software IP del nodo donde se plantea este problema descompone entonces el paquete IP en fragments según reglas precisas, enviándose cada uno de ellos a continuación 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 de encaminamiento de los paquetes IP hacia 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 remitente de un datagrama IP tiene 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 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 denominado enrutador (router), nodo que conecta una red local con otras redes y cuya dirección IP encuentra en sus tablas, dirección obtenida inicialmente ya sea en un archivo, en una memoria permanente o mediante información que circula por la red.

Un enrutador está conectado a dos redes y posee 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 router 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 router 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 router le enviará el paquete directamente; de lo contrario, lo enviará a otro router, conectando la red n.º 2 con una red n.º 3, y así sucesivamente.

9.1.6.2. Mensajes de error y de control

También 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 averiados, 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 en función de los mensajes ICMP que reciben. De este modo, una aplicación en sí misma nunca ve 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 fiable de datos entre dos puntos, es decir, no se garantiza el correcto enrutamiento de un paquete hasta su destino. La aplicación, si lo desea, puede gestionar esto por sí misma, por ejemplo, esperando un acuse de recibo tras el envío de un mensaje antes de enviar el siguiente.

Por el momento, a nivel de red, hemos hablado de direcciones IP de máquinas. Sin embargo, en una máquina pueden coexistir al mismo tiempo diferentes procesos que pueden comunicarse entre sí. Por lo tanto, al enviar un mensaje, hay que 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, denominado número de puerto. Algunos números están reservados para aplicaciones estándar: el puerto 69 para la aplicación tftp (trivial file transfer protocol), 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, a continuación, en tramas físicas.

9.1.7.2. El protocolo TCP: Protocolo de control de transferencia

Para unas comunicaciones seguras, el protocolo UDP es insuficiente: el desarrollador de aplicaciones debe elaborar por sí mismo un protocolo que le permita detectar el correcto enrutamiento de los paquetes.

El protocolo TCP (Protocolo de control de transferencia) evita estos problemas. Sus características son las siguientes:

  • El proceso que desea transmitir establece primero una conexión con el proceso destinatario de la información que va a transmitir. 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 emitidos por el proceso de origen siguen esta ruta virtual y llegan en el orden en que fueron emitidos, lo que no estaba garantizado en el protocolo UDP, ya que los paquetes podían seguir rutas diferentes.
  • La información enviada tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta no se envía necesariamente de inmediato: el protocolo TCP espera a tener suficiente para enviarla. Se almacena en una estructura denominada 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 destinatario comprueba que recibe los segmentos en secuencia. 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 este modo, este puede saber que un segmento ha llegado a su destino, lo que no era posible con el protocolo UDP.
  • Si, tras un cierto tiempo, el protocolo TCP que ha emitido 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. Así, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen sigue enviando información. Esto permite, por ejemplo, que el protocolo TCP de origen envíe varios segmentos sin esperar el acuse de recibo. Si, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un determinado segmento 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 varios protocolos estándar:

TELNET

Este protocolo permite a un usuario de una máquina A de la red conectarse a una máquina B (a menudo denominada máquina host). TELNET emula en la máquina A un terminal denominado universal. Por lo tanto, el usuario actúa como si dispusiera de 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 de transferencia de archivos trivial)

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 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 disponer de un directorio que establezca la correspondencia entre máquina <--> dirección IP. Probablemente, en este directorio las máquinas se designarían con nombres simbólicos tales como:

máquina DPX2/320 de la Universidad de Angers

máquina Sun de la ISERPA de Angers

Es evidente que sería más cómodo designar una máquina por un nombre en lugar de por su dirección IP. Se plantea entonces 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, sería bastante engorroso. El control de los nombres se ha distribuido, de hecho, en dominios. Cada dominio es gestionado por un organismo, generalmente muy ágil, que tiene total libertad para elegir los nombres de las máquinas. Así, las máquinas en Francia pertenecen al dominio fr, gestionado 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. Así, la Universidad de Angers pertenece al dominio univ-Angers. El servicio que gestiona este dominio tiene total libertad para nombrar los equipos de la red de la Universidad de Angers. Por el momento, este dominio no se ha subdividido. Pero en una gran universidad con muchos equipos en red, podría subdividirse.

La máquina DPX2/320 de la Universidad de Angers se ha denominado Lagaffe, mientras que una PC 486DX50 se ha 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, hay que obtener la dirección de Internet de esa máquina. ¿Cómo se hace 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 el anterior, 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 solicitud especial al primer servidor de nombres de su lista, denominada solicitud DNS, que incluye, por 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. Y así lo hará. De este modo, se consultará a varios servidores de nombres, no de forma desordenada, sino de manera que se minimicen las consultas. Si finalmente se encuentra la máquina, la respuesta volverá a la máquina A.

XDR: (Representación de datos eXternal)

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

RPC: (Llamada a procedimiento remoto)

Definido también por Sun, es un protocolo de comunicación entre aplicaciones remotas, independiente de la capa de transporte. Este protocolo es importante: libera al programador del conocimiento de 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

Definido también por Sun, este protocolo permite a una máquina «ver» el sistema de archivos de otra máquina. Se basa en el protocolo RPC anterior.

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. Gestión de direcciones de red

Una máquina en la red de Internet se define de forma ú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 definirse mediante un nombre igualmente único. Este nombre no es obligatorio, ya que las aplicaciones siempre utilizan en última instancia las direcciones IP de los equipos. Su finalidad es facilitar la vida a los usuarios. Así, resulta más fácil, con un navegador, solicitar la dirección http://www.ibm.com que la URL http://129.42.17.99, aunque ambos métodos son posibles. La asociación de direcciones IP <--> nomMachine está garantizada por un servicio distribuido de Internet denominado DNS (Sistema de Nombres de Dominio). La plataforma .NET ofrece la clase Dns para gestionar 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 encuentra 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 encuentra 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 designarse mediante un nombre principal y alias
Public Property HostName
As String
el nombre de la máquina, si lo tiene

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 con el método estático IPAddress.Parse("I1.I2.I3.I4"). Consideremos el siguiente programa, que muestra el nombre del equipo en el que se ejecuta y, a continuación, de forma interactiva, proporciona las correspondencias entre la dirección IP y el nombre del equipo:

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 del equipo local
        ' y, a continuación, proporciona información de forma interactiva sobre las máquinas de la red
        ' identificadas por un nombre o una dirección IP
        ' máquina 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
            ' introducción del nombre del equipo buscado
            Console.Out.Write("Machine recherchée (fin pour arrêter) : ")
            machine = Console.In.ReadLine().Trim().ToLower()
            ' ¿Terminado?
            If machine = "fin" Then
                Exit While
            End If

            ' ¿Dirección I1.I2.I3.I4 o nombre de 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 una máquina A quiere comunicarse con una aplicación AppB de una 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 trabaja 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 destinada dicha información. Las aplicaciones de la máquina B acceden a la red a través de ventanas, también denominadas puertos de comunicación. Esta información se encuentra en el paquete recibido por la máquina B para que pueda entregarse a la aplicación correcta.
  • Los protocolos de comunicación que comprende la máquina B. En nuestro estudio, utilizaremos únicamente los protocolos TCP-IP.
  • El protocolo de diálogo aceptado por la aplicación AppB. De hecho, las máquinas A y B van a «comunicarse». Lo que van a decir se encapsulará en los protocolos TCP-IP. No obstante, 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 análogo 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 oirá entonces las palabras. Aquí 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 forma ú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. Características del protocolo TCP

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

  • El proceso que desea transmitir establece en primer lugar una conexión con el proceso destinatario de la información que va a transmitir. Esta conexión se establece 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 a los dos procesos que han establecido la conexión.
  • Todos los paquetes emitidos por el proceso de origen siguen esta ruta virtual y llegan en el orden en que fueron emitidos
  • 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 a tener suficiente para enviarla. Se almacena en una estructura denominada 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 destinatario comprueba que recibe los segmentos en secuencia. 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 este modo, este 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, reenvía 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. Así, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen sigue enviando información. Esto permite, por ejemplo, que el protocolo TCP de origen envíe varios segmentos sin esperar el acuse de recibo. Si, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un determinado segmento 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 quiere abrir una conexión con el servicio SB1 de la máquina B. Esta acepta o rechaza. Si acepta, la máquina A puede enviar sus solicitudes al servicio SB1. Estas deben ajustarse al protocolo de diálogo que comprende el servicio SB1. De este modo, se establece un diálogo de solicitud-respuesta entre la máquina A, denominada máquina cliente, y la máquina B, denominada máquina servidor. Uno de los dos interlocutores 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 trata de forma diferente la solicitud de conexión inicial de un cliente y sus solicitudes posteriores destinadas a obtener un servicio. El programa no presta el servicio por sí mismo. Si lo hiciera, durante la duración del servicio ya no estaría a la escucha de las solicitudes de conexión y, por lo tanto, los clientes no serían atendidos. Por lo tanto, procede de otra manera: tan pronto como se recibe una solicitud de conexión en el puerto de escucha y se acepta, el servidor crea una tarea encargada de prestar el servicio solicitado por el cliente. Este servicio se presta en otro puerto del servidor denominado puerto de servicio. De este modo, 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 interesante 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 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 estas 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. En general, cuando client1 envía una línea de texto a su interlocutor, espera una respuesta. Esta nunca llegará si la línea se ha almacenado en realidad 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 disponemos de los elementos necesarios para escribir 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-salida del cliente TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' bucle 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 completado
            client.Close()
        Catch ex As Exception
      ' se gestiona 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 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 solicitudes de 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 de servicio
      Dim liaisonClient As TcpClient = Nothing
            While not fini
                ' espera de 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 notifica 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. Se limita a devolver al cliente la solicitud que este le ha enviado. 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 ha enviado

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 entero >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 de servicio
      Dim liaisonClient As TcpClient = Nothing
            While True
                ' bucle infinito: se detendrá con Ctrl-C
                ' espera de un cliente
                liaisonClient = ecoute.AcceptTcpClient()

                ' el servicio lo presta otra tarea
                nbClients += 1
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf New traiteClientEcho(liaisonClient, nbClients).Run))
                tache.Start()
            End While
            ' volvemos a escuchar las solicitudes
        Catch ex As Exception
            ' se informa del 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)
        ' parada 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 del 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 escribimos 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, a continuación, 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 tecleada 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 >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-salida del cliente TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' bucle 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))
        ' ¿Terminado?
        If demande.Trim().ToLower() = "fin" Then
          Exit While
                End If
            End While
            ' se ha terminado
            client.Close()
        Catch ex As Exception
      ' se gestiona la excepción
      erreur(ex.Message, 3)
        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)
        ' parada 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 inicia en el puerto 100 en una ventana de DOS
  • en la misma máquina se inician 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 ha sido capaz de 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 estudiado anteriormente: las comunicaciones cliente-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 subproceso encargado de leer los comandos tecleados y enviarlos al servidor
  2. un subproceso encargado de leer las respuestas del servidor y mostrarlas en pantalla

¿Por qué dos subprocesos cuando en la aplicación anterior no se había planteado esta 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 particular 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 de envío de una sola línea al servidor y recepción de una sola línea enviada por el servidor no siempre es adecuado. Por lo tanto, crearemos dos bucles independientes:

  • un bucle de lectura de los comandos tecleados para enviarlos al servidor. El usuario indicará el final de los comandos con la palabra clave fin.
  • un bucle de recepción y visualización de las respuestas del servidor. Este será un bucle infinito que solo se interrumpirá cuando el servidor cierre el flujo de red o cuando el usuario teclee el comando fin.

Para tener estos dos bucles 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 (SendMail Transfer Protocol). Este servicio se encarga de enviar el correo electrónico a los destinatarios. Funciona en el puerto 25 y tiene un protocolo de comunicación de tipo 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]

Comentemos estos intercambios entre cliente y 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 tienen un comando help que proporciona indicaciones sobre los comandos que se pueden utilizar con el servicio. Aquí no es el caso. 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 reenvía tantas veces como sea necesario el comando rcpt to: para cada uno de los destinatarios.
    • datos que indican al servidor SMTP que se va a enviar el mensaje. Tal y 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 puede indicar al servidor que se ha terminado con el comando quit. El servidor cierra entonces 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 introducidos mediante el teclado.

Si comprobamos 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 introduce el nombre de usuario en la máquina que aloja nuestros correos
  • pass password, donde se introduce 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.

Veamos ahora el protocolo de diálogo entre un cliente y un servidor web que suele funcionar 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 tras recibir la línea vacía. 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 trabaja 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ó devolviendo el contenido del archivo index.html y, a continuación, 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 en 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 denomina encabezados HTTP (Protocolo de transferencia HyperText). No vamos a detallar aquí estas cabeceras, pero recordaremos que nuestro cliente genérico permite acceder a ellas, lo que puede resultar útil para comprenderlas. La primera línea, por ejemplo:

<-- HTTP/1.1 200 OK

indica que el servidor web contactado entiende 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

indican al cliente que va a recibir 11 251 bytes que representan texto HTML (HyperText Markup Language) y que, al finalizar el envío, se cerrará la conexión. Así pues, tenemos aquí un cliente TCP muy práctico. De hecho, este cliente ya existe en los equipos, 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 forma de
   ' servidor puerto
   ' se conecta al servicio
   ' crea un subproceso para leer los comandos introducidos mediante el teclado
   ' estas se enviarán al servidor
   ' crea un subproceso para leer las respuestas del servidor
   ' estas se mostrarán en pantalla
   ' todo termina con el comando «fin» introducido mediante 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 >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 subprocesos 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 subprocesos
        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)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub
End Class

Public Class clientSend
    ' clase encargada de leer los comandos introducidos mediante 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 introducción y envío de comandos
            Console.Out.WriteLine("Commandes : ")
            While True
                ' lectura del comando introducido con el 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 señala 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

    ' fabricante
    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 señala 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 centramos en un servidor

  • que muestra en pantalla los comandos enviados por sus clientes
  • y les envía como respuesta las líneas de texto tecleadas por un usuario. 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 subproceso dedicado exclusivamente a la lectura de las líneas de texto enviadas por el cliente
  • un subproceso dedicado exclusivamente a leer las respuestas tecleadas por el usuario. 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 pulse 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 por <-- son las enviadas del servidor al cliente, las demás son 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 por <-- son las enviadas desde el cliente al servidor. Las líneas N: son las enviadas desde el servidor al cliente n.º N. El servidor anterior sigue activo, mientras que el cliente 1 ha finalizado. 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 es entonces esta:

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 solicitará la página /exemple.html:

Image

Veamos ahora 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 este modo, descubrimos los encabezados HTTP enviados por el navegador. Esto nos permite ir descubriendo 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 objetivo es indicar al servidor qué tipo de cliente tiene delante. También vemos que los encabezados HTTP terminan con una línea en blanco. Elaboremos una respuesta para nuestro cliente. El usuario que teclea es aquí el verdadero servidor y puede elaborar una respuesta a mano. 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 por 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 nos limitamos a decir que vamos a cerrar la conexión (Connection: close) tras enviarlo. Esto es suficiente para el navegador. Al ver que la conexión se ha cerrado, sabrá que la respuesta del servidor ha finalizado y mostrará la página HTML que se le ha enviado. Esta última 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 introduciendo el comando fin. El navegador sabe entonces que la respuesta del servidor ha finalizado y puede mostrarla:

Image

Si en el ejemplo anterior se ejecuta Affichage/Source para ver lo que ha recibido el navegador, se obtiene:

Image

es decir, exactamente lo que se ha enviado 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 pantalla
        ' crea un subproceso para leer los comandos introducidos con el teclado
        ' estas se enviarán como respuesta al cliente
        ' todo termina con el comando «fin» introducido mediante 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 entero >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 garantiza mediante subprocesos separados
                nbClients += 1
                ' hilo de lectura de solicitudes de clientes
                Dim thReceive As New Thread(New ThreadStart(AddressOf New serveurReceive(client, nbClients).Run))
                ' hilo de lectura de las respuestas tecleadas por el usuario
                Dim thSend As New Thread(New ThreadStart(AddressOf New serveurSend(client, nbClients).Run))

                ' se inicia la ejecución de los dos subprocesos
                thSend.Start()
                thReceive.Start()
            End While
            ' se vuelve a escuchar las solicitudes
        Catch ex As Exception
            ' se notifica 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)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub
End Class


Public Class serveurSend
    ' clase encargada de leer las respuestas tecleadas
    ' 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.º
        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 en red
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' bucle de introducción y envío de comandos
            While True
                ' identificación del cliente
                Console.Out.Write((numClient & " : "))
                ' lectura de la respuesta tecleada
                réponse = Console.In.ReadLine().Trim()
                ' ¿Terminado?
                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 señala 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 anota el cliente TCP
        Me.client = client
        ' y su n.º
        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, hemos visto 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 nos dirigimos
  • el tercero, que queremos que el servidor cierre la conexión después de habernos respondido.

Si en lo anterior sustituimos 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 cliente 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 (por lo tanto, de URL). La nueva 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 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
        ' 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-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 gestiona 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)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub
End Class

La única novedad de este programa es el uso de la clase Uri. El programa recibe un URL (Uniform Resource Locator) o un URI (Uniform Resource Identifier) 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 de protocolo, de servidor, etc.), se lanza una excepción. Esto nos permite verificar la validez del parámetro recibido. Una vez construido el objeto Uri, se tiene acceso a los diferentes elementos de esta Uri. Así, si el objeto uri del código anterior se ha construido 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 gestiona las redirecciones

El cliente web anterior no gestiona una posible redirección de URL que él mismo ha solicitado. El siguiente cliente sí la gestiona.

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

He aquí 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 de 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
      ' 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
    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 URL de redireccionamiento
    Dim location As New Regex("^Location: (.+?)$") '

        ' gestión de errores
    Try
      ' se pueden tener varios URL que consultar si hay redireccionamientos
      While nbRedirs <= nbRedirsMax
        ' se conecta al servidor
        client = New TcpClient(uri.Host, uri.Port)

        ' se crean los flujos de entrada-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 a 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 redireccionamiento, 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 redireccionamiento
                            locationString = résultat.Groups(1).Value
                            ' se anota 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()
                ' ¿Hemos terminado?
                If Not locationFound Or nbRedirs > nbRedirsMax Then
                    Exit While
                End If

                ' hay que realizar una redirección: se construye 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 gestiona 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)
        ' parada con 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 tratado en diversas formas. Recordemos la última versión. Se ha creado una clase de impuestos. 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 las tres tablas de datos necesarias para el cálculo del 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
        ' los parámetros pasados al constructor
  • un constructor 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 las tres tablas límite, coeffR, coeffN a partir de
        ' del contenido de la tabla Timpots de la base ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
        ' puede lanzar 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 indica el puerto de escucha
        Me.portEcoute = portEcoute
        ' por ahora inactivo
        actif = False
        ' crea y lanza un hilo para leer los comandos introducidos mediante el teclado
        ' el servidor se gestionará 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 de 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 introducidos con el teclado. Por lo tanto, creamos un subproceso para leer estos comandos. Habrá dos posibles: start para iniciar el servicio y stop para detenerlo definitivamente. El método admin que gestiona estos comandos es el siguiente:


    Public Sub admin()
        ' lee los comandos de administración del servidor introducidos mediante 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 del 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 introducido en el teclado es start, se inicia un hilo de escucha de las solicitudes de los clientes. Si el comando introducido 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 presta 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 notifica 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)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub

Encontramos un servidor TCP clásico que escucha en el puerto portEcoute. Las solicitudes de los clientes se procesan mediante el método Run de un objeto al que se 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 de 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

    ' constructor
    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 tener dos formas:

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

La forma 1 permite el cálculo de un impuesto, la forma 2 cierra la conexión cliente-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)
                ' descomposición de 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 realizada por el cliente. Se comprueba la validez de la solicitud y, en su caso, 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 necesitan 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 del error
        System.Console.Error.WriteLine(msg)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub

Esta clase se compila mediante

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 necesitan 6 argumentos
        If args.Length <> 6 Then
            erreur(syntaxe, 1)
        End If
        ' el puerto debe ser un número entero >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)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub
End Class

Se pasan al programa de prueba los datos necesarios para la construcción de 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

He aquí 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 escucha de las solicitudes de los clientes. Es el comando «start», introducido 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 se inicia:

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]

Volvemos 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