8. Programación TCP-IP
8.1. Generalidades
8.1.1. Los protocolos de Internet
Aquí ofrecemos una introducción a los protocolos de comunicación de Internet, también conocidos como el conjunto de protocolos TCP/IP (Protocolo de Control de Transmisión / Protocolo de Internet), en referencia a los dos protocolos principales. Es recomendable que el lector cuente con una comprensión general del funcionamiento de las redes y, en particular, de los protocolos TCP/IP antes de abordar el desarrollo de aplicaciones distribuidas.
El texto que sigue es una traducción parcial de un texto que se encuentra en el documento «Lan Workplace for Dos - Administrator's Guide» de NOVELL, un documento de principios de los años 90.
El concepto general de crear una red de computadoras heterogéneas proviene de investigaciones realizadas por la DARPA (Agencia de Proyectos de Investigación Avanzada de Defensa) en Estados Unidos. La DARPA desarrolló el conjunto de protocolos conocido como TCP/IP, que permite que máquinas heterogéneas se comuniquen entre sí. Estos protocolos se probaron en una red llamada ARPAnet, red que posteriormente se convirtió en la red INTERNET. Los protocolos TCP/IP definen formatos y reglas de transmisión y recepción independientes de la organización de las redes y del equipo utilizado.
La red diseñada por el DARPA y administrada por los protocolos TCP/IP es una red de conmutación de paquetes. Este tipo de red transmite la información por la red en pequeños fragmentos llamados paquetes. Así, si una computadora transmite un archivo grande, este se dividirá en pequeños fragmentos que se enviarán por la red para ser recomponidos en el destino. TCP/IP define el formato de estos paquetes, a saber:
- origen del paquete
- destino
- longitud
- tipo
8.1.2. El modelo OSI
Los protocolos TCP/IP siguen aproximadamente el modelo de red abierta denominado OSI (Modelo de Referencia de Interconexión de Sistemas Abiertos) definido por la ISO (Organización Internacional de Normalización). Este modelo describe una red ideal en la que la comunicación entre máquinas puede representarse mediante un modelo de siete capas:

Cada capa recibe servicios de la capa inferior y ofrece los suyos a la capa superior. Supongamos que dos aplicaciones ubicadas en máquinas A y B diferentes desean comunicarse: lo hacen a nivel de la capa Application. No necesitan conocer todos los detalles del funcionamiento de la red: cada aplicación entrega la información que desea transmitir a la capa inferior: la capa Présentation. Por lo tanto, la aplicación solo tiene que conocer las reglas de interfaz con la capa Présentation.
Una vez que la información se encuentra en la capa Présentation, pasa, siguiendo otras reglas, a la capa Session y así sucesivamente, hasta que la información llega al soporte físico y se transmite físicamente a la máquina de destino. Allí, se someterá al proceso inverso al que se le aplicó en la máquina de origen.
En cada capa, el proceso emisor encargado de enviar la información la envía a un proceso receptor en la otra máquina que pertenece a la misma capa que él. Lo hace siguiendo ciertas reglas que se denominan el protocolo de la capa. Por lo tanto, tenemos el siguiente esquema de comunicación final:

La función de las diferentes capas es la siguiente:
Asegura la transmisión de bits a través de un medio físico. En esta capa se encuentran equipos terminales de procesamiento de datos (E.T.T.D), tales como terminales u computadoras, así como equipos de terminación de circuitos de datos (E.T.C.D), tales como moduladores/demoduladores, multiplexores y concentradores. Los puntos de interés en este nivel son: . la elección de la codificación de la información (analógica o digital) . la elección del modo de transmisión (sincrónico o asincrónico). | |
Oculta las características físicas de la capa física. Detecta y corrige los errores de transmisión. | |
Administra la ruta que debe seguir la información enviada por la red. A esto se le llama routage: determinar la ruta que debe seguir la información para que llegue a su destinatario. | |
Permite la comunicación entre dos aplicaciones, mientras que las capas anteriores solo permitían la comunicación entre máquinas. Un servicio que brinda esta capa puede ser el multiplexado: la capa de transporte puede utilizar una misma conexión de red (de máquina a máquina) para transmitir información perteneciente a varias aplicaciones. | |
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. | |
Su objetivo es estandarizar la representación de los datos en las diferentes máquinas. Así, los datos procedentes de una máquina A serán «formateados» por la capa Présentation de la máquina A, según un formato estándar, antes de ser enviados a la red. Al llegar a la capa Présentation de la máquina destinataria B, que los reconocerá gracias a su formato estándar, se les dará otro formato para que la aplicación de la máquina B los reconozca. | |
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. |
8.1.3. El modelo TCP/IP
El modelo OSI es un modelo ideal que aún no se ha implementado. El conjunto de protocolos TCP/IP se acerca a él de la siguiente manera:

Capa física
En las redes locales, por lo general se utiliza la tecnología Ethernet o Token-Ring. Aquí solo presentaremos la tecnología Ethernet.
Ethernet
Es el nombre que se le da a una tecnología de redes locales de conmutación de paquetes inventada en PARC Xerox a principios de la década de 1970 y estandarizada por Xerox, Intel y Digital Equipment en 1978. La red está compuesta físicamente por un cable coaxial de aproximadamente 1,27 cm de diámetro y una longitud máxima de 500 m. Se puede extender mediante répéteurs, sin que dos equipos puedan estar separados por más de dos repetidores. El cable es pasivo: todos los componentes activos se encuentran en las máquinas conectadas al cable. Cada máquina está conectada al cable mediante una tarjeta de acceso a la red que incluye:
- un transmisor (transceiver) que detecta la presencia de señales en el cable y convierte las señales analógicas en digitales y viceversa.
- un acoplador que recibe las señales digitales del transmisor y las transmite a la computadora para su procesamiento, o viceversa.
Las características principales de la tecnología Ethernet son las siguientes:
- Capacidad de 10 megabits por segundo.
- Topología de bus: todas las máquinas están conectadas al mismo cable

- Red de difusión: una máquina que transmite envía información por el cable con la dirección de la máquina destinataria. Todas las máquinas conectadas reciben entonces esta información y solo la máquina a la que va dirigida la conserva.
- El método de acceso es el siguiente: el transmisor que desea transmitir escucha el cable; detecta entonces si hay o no una onda portadora, cuya presencia significaría que hay una transmisión en curso. Esta es la técnica CSMA (Carrier Sense Multiple Access). Si no hay portadora, un transmisor puede decidir transmitir a su vez. Puede haber varios que tomen esta decisión. Las señales emitidas se mezclan: se dice que hay una colisión. El transmisor detecta esta situación: al mismo tiempo que transmite por el cable, escucha lo que realmente está pasando por él. Si detecta que la información que transita por el cable no es la que él mismo transmitió, deduce que hay una colisión y dejará de transmitir. Los demás transmisores que estaban transmitiendo harán lo mismo. Cada uno reanudará su transmisión después de un tiempo aleatorio que depende de cada transmisor. Esta técnica se denomina CD (Detección de colisión). El método de acceso se denomina, por lo tanto, CSMA/CD.
- un sistema de direccionamiento de 48 bits. Cada máquina tiene una dirección, denominada aquí «dirección física», que está grabada en la tarjeta que la conecta al cable. A esta dirección se le conoce como la dirección Ethernet de la máquina.
Capa de red
En esta capa encontramos los protocolos IP, ICMP, ARP y RARP.
IP (Protocolo de Internet) | Transmite paquetes entre dos nodos de la red |
ICMP (Protocolo de mensajes de control de Internet) | ICMP establece la comunicación entre el programa del protocolo IP de una máquina y el de otra máquina. Por lo tanto, es un protocolo de intercambio de mensajes dentro del propio protocolo IP. |
ARP (Protocolo de resolución de direcciones) | establece la correspondencia entre la dirección de Internet de una máquina y su dirección física |
RARP (Protocolo de resolución inversa de direcciones) | establece la correspondencia entre la dirección física de la máquina y la dirección de Internet de la máquina |
Capas de transporte/sesión
En esta capa se encuentran los siguientes protocolos:
TCP (Protocolo de control de transmisión) | Garantiza la entrega confiable de información entre dos clientes |
UDP (Protocolo de datagramas de usuario) | Garantiza una entrega no confiable de información entre dos clientes |
Capas de aplicación, presentación y sesión
Aquí se encuentran diversos protocolos:
Emulador de terminal que permite que una máquina A se conecte a una máquina B como terminal | |
permite la transferencia de archivos | |
permite la transferencia de archivos | |
permite el intercambio de mensajes entre usuarios de la red | |
convierte un nombre de máquina en la dirección de Internet de la máquina | |
creado por Sun MicroSystems, especifica una representación estándar de los datos, independiente de las máquinas | |
definido también por Sun, es un protocolo de comunicación entre aplicaciones remotas, independiente de la capa de transporte. Este protocolo es importante: libera al programador de la necesidad de conocer los detalles de la capa de transporte y hace que las aplicaciones sean portables. Este protocolo se basa en el protocolo XDR | |
también definido por Sun; este protocolo permite que una máquina «vea» el sistema de archivos de otra máquina. Se basa en el protocolo RPC mencionado anteriormente |
8.1.4. Funcionamiento de los protocolos de Internet
Las aplicaciones desarrolladas en el entorno TCP/IP suelen utilizar varios de los protocolos de este entorno. Un programa de aplicación se comunica con la capa más alta de los protocolos. Esta transmite la información a la capa inferior y así sucesivamente hasta llegar al soporte físico. Allí, la información se transfiere físicamente a la máquina destinataria, donde volverá a atravesar las mismas capas, esta vez en sentido inverso, hasta llegar a la aplicación destinataria de la información enviada. El siguiente diagrama muestra el recorrido de la información:

Tomemos un ejemplo: la aplicación FTP, definida en el nivel de la capa Application y que permite la transferencia de archivos entre máquinas.
- La aplicación entrega una secuencia de bytes para que se transmita a la capa transport.
- La capa transport divide esta secuencia de bytes en segments y TCP, y agrega al inicio de cada segmento su número correspondiente. Los segmentos se pasan a la capa de red, que se rige por el protocolo IP.
- La capa IP crea un paquete que encapsula el segmento TCP recibido. Al principio de este paquete, coloca las direcciones de Internet de las máquinas de origen y destino. También determina la dirección física de la máquina destinataria. Todo esto se pasa a la capa de enlace de datos y enlace físico, es decir, a la tarjeta de red que conecta la máquina a la red física.
- Allí, el paquete IP se encapsula a su vez en una trama física y se envía a su destinatario por el cable.
- En la máquina destinataria, la capa de enlace de datos y enlace físico hace lo contrario: desencapsula el paquete IP de la trama física y lo pasa a la capa IP.
- La capa IP verifica que el paquete sea correcto: calcula una suma a partir de los bits recibidos (checksum), suma que debe coincidir con la que figura en la cabecera del paquete. Si no es así, el paquete se rechaza.
- Si el paquete se considera correcto, la capa IP desencapsula el segmento TCP que contiene y lo pasa a la capa superior, transport.
- La capa transport —la capa TCP en nuestro ejemplo— revisa el número del segmento para restaurar el orden correcto de los segmentos.
- También calcula una suma de verificación para el segmento TCP. Si se verifica que es correcto, la capa TCP envía un acuse de recibo a la máquina de origen; de lo contrario, se rechaza el segmento TCP.
- A la capa TCP solo le queda transmitir la parte de datos del segmento a la aplicación destinataria de los mismos en la capa superior.
8.1.5. Los problemas de direccionamiento en Internet
Un noeud de una red puede ser una computadora, una impresora inteligente, un servidor de archivos, o cualquier dispositivo que pueda comunicarse mediante los protocolos TCP/IP. Cada nodo tiene una dirección física cuyo formato depende del tipo de red. En una red Ethernet, la dirección física se codifica en 6 bytes. Una dirección de una red X25 es un número de 14 dígitos.
La dirección de Internet de un nodo es una dirección lógica: es independiente del equipo y de la red que se utilice. Es una dirección de 4 bytes que identifica tanto a una red local como a un nodo de dicha red. La dirección de Internet suele representarse en forma de 4 números, que son los valores de los 4 bytes, separados por un punto. Así, la dirección de la computadora Lagaffe de la Facultad de Ciencias de Angers se escribe como 193.49.144.1 y la de la computadora Liny como 193.49.144.9. De ello se deduce que la dirección de Internet de la red local es 193.49.144.0. Esta red puede tener hasta 254 nodos.
Dado que las direcciones de Internet o direcciones IP son independientes de la red, una máquina de la red A puede comunicarse con una máquina de la red B sin preocuparse por el tipo de red en la que se encuentra: basta con que conozca su dirección IP. El protocolo IP de cada red se encarga de realizar la conversión entre la dirección IP y la dirección física, en ambos sentidos.
Todas las direcciones IP deben ser diferentes. Hay organismos oficiales encargados de distribuirlas. De hecho, estos organismos asignan una dirección para redes locales, por ejemplo, 193.49.144.0 para la red de la Facultad de Ciencias de Angers. El administrador de esta red puede luego asignar las direcciones IP 193.49.144.1 a 193.49.144.254 como lo considere conveniente. Esta dirección suele registrarse en un archivo específico de cada equipo conectado a la red.
8.1.5.1. Las clases de direcciones IP
Una dirección IP es una secuencia de 4 bytes que a menudo se escribe como I1.I2.I3.I4, y que en realidad contiene dos direcciones:
- la dirección de la red
- la dirección de un nodo de esa red
Según el tamaño de estos dos campos, las direcciones IP se dividen en tres clases: clases A, B y C.
Clase A
La dirección IP: I1.I2.I3.I4 tiene la forma R1.N1.N2.N3, donde
R1 es la dirección de la red
N1.N2.N3 es la dirección de una máquina en esa red
Más precisamente, la forma de una dirección IP de clase A es la siguiente:

La dirección de red ocupa 7 bits y la dirección del nodo, 24 bits. Por lo tanto, pueden existir 127 redes de clase A, cada una con hasta 2²⁴ nodos.
Clase B
En este caso, la dirección IP: I1.I2.I3.I4 tiene el formato R1.R2.N1.N2, donde
R1.R2 es la dirección de la red
N1.N2 es la dirección de una máquina en esa red
Más precisamente, la forma de una dirección IP de clase B es la siguiente:

La dirección de la red ocupa 2 bytes (14 bits exactamente), al igual que la del nodo. Por lo tanto, pueden existir 2¹⁴ redes de clase B, cada una con hasta 2¹⁶ nodos.
Clase C
En esta clase, la dirección IP: I1.I2.I3.I4 tiene la forma R1.R2.R3.N1, donde
R1.R2.R3 es la dirección de la red
N1 es la dirección de una máquina en esa red
Más precisamente, la forma de una dirección IP de clase C es la siguiente:

La dirección de red ocupa 3 bytes (menos 3 bits) y la dirección del nodo, 1 byte. Por lo tanto, pueden existir 2²¹ redes de clase C con hasta 256 nodos.
Dado que la dirección de la computadora Lagaffe de la Facultad de Ciencias de Angers es 193.49.144.1, vemos que el byte de mayor peso es 193, es decir, en binario 11000001. De ello se deduce que la red es de clase C.
Direcciones reservadas
. Algunas direcciones IP son direcciones de red en lugar de direcciones de nodos dentro de la red. Son aquellas en las que la dirección del nodo se establece en 0. Así, la dirección 193.49.144.0 es la dirección IP de la red de la Facultad de Ciencias de Angers. Por lo tanto, ningún nodo de una red puede tener la dirección cero.
. Cuando en una dirección IP la dirección del nodo contiene únicamente 1, 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 admite 2⁸ = 256 nodos, si se eliminan las dos direcciones prohibidas, solo quedan 254 direcciones autorizadas.
8.1.5.2. Los protocolos de conversión de dirección de Internet <--> dirección física
Hemos visto que, al transmitir información de una máquina a otra, esta información, al atravesar la capa IP, se encapsula en paquetes. Estos tienen la siguiente forma:

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

En la trama final, se encuentran las direcciones físicas de los equipos de origen y destino. ¿Cómo se obtienen?
La máquina remitente, que conoce la dirección IP de la máquina con la que desea comunicarse, obtiene la dirección física de esta última utilizando un protocolo específico llamado ARP (Protocolo de resolución de direcciones).
- Envía un paquete de un tipo especial llamado paquete ARP que contiene la dirección IP de la máquina cuya dirección física se busca. También se ha asegurado de incluir en él su propia dirección IP, así como su dirección física.
- Este paquete se envía a todos los nodos de la red.
- Estos reconocen la naturaleza especial del paquete. El nodo que reconoce su dirección IP en el paquete responde enviando al remitente del paquete su dirección física. ¿Cómo lo hace? Ha encontrado en el paquete las direcciones IP y la dirección física del remitente.
- Así, el remitente recibe la dirección física que buscaba. La almacena en memoria para poder usarla más adelante si hay que enviar otros paquetes al mismo destinatario.
La dirección IP de una máquina normalmente está registrada en uno de sus archivos, por lo que puede consultarlo para conocerla. Esta dirección se puede cambiar: basta con editar el archivo. La dirección física, por su parte, está registrada en la memoria de la tarjeta de red y no se puede cambiar.
Cuando un administrador desea organizar su red de otra manera, puede verse en la necesidad de cambiar las direcciones IP de todos los nodos y, por lo tanto, de editar los distintos archivos de configuración de cada uno de ellos. Esto puede resultar tedioso y dar lugar a errores si hay muchas máquinas. Un método consiste en no asignar una dirección IP a las máquinas: para ello, se inserta un código especial en el archivo en el que la máquina debería encontrar su dirección IP. Al descubrir que no tiene una dirección IP, la máquina la solicita mediante un protocolo llamado RARP (Protocolo de resolución inversa de direcciones). A continuación, envía a la red un paquete especial llamado paquete RARP, similar al paquete ARP anterior, en el que incluye su dirección física. Este paquete se envía a todos los nodos, los cuales reconocen entonces un paquete RARP. Uno de ellos, llamado servidor RARP, cuenta con un archivo que indica la correspondencia entre la dirección física y la dirección IP de todos los nodos. A continuación, responde al remitente del paquete RARP, reenviándole su dirección IP. Por lo tanto, un administrador que desee reconfigurar su red solo tiene que editar el archivo de correspondencias del servidor RARP. Este, por lo general, debe tener una dirección fija IP que debe poder conocer sin tener que utilizar él mismo el protocolo RARP.
8.1.6. La capa de red conocida como capa IP de Internet
El protocolo IP (Protocolo de Internet) define la forma que deben adoptar los paquetes y la manera en que deben gestionarse al enviarlos o recibirlos. Este tipo específico de paquete se denomina datagrama IP. Ya lo hemos presentado:

Lo importante es que, además de los datos que se van a transmitir, el datagrama IP contiene las direcciones de Internet de las máquinas de origen y destino. De esta manera, la máquina destinataria sabe quién le envía un mensaje.
A diferencia de una trama de red, cuya longitud está determinada por las características físicas de la red por la que transita, la longitud del datagrama IP la establece el software y, por lo tanto, será la misma en diferentes redes físicas. Hemos visto que, al descender de la capa de red a la capa física, el datagrama IP se encapsulaba en una trama física. Hemos dado el ejemplo de la trama física de una red Ethernet:

Las tramas físicas circulan de nodo en nodo hacia su destino, el cual puede no estar en la misma red física que la máquina remitente. Por lo tanto, el paquete IP puede encapsularse sucesivamente en diferentes tramas físicas en los nodos que conectan dos redes de tipo diferente. También es posible que el paquete IP sea demasiado grande para encapsularse en una trama física. El software IP del nodo donde se presenta este problema descompone entonces el paquete IP en fragments siguiendo reglas precisas, y cada uno de ellos se envía posteriormente a la red física. Solo se volverán a ensamblar en su destino final.
8.1.6.1. El enrutamiento
El enrutamiento es el método para dirigir los paquetes IP a su destino. Existen dos métodos: el enrutamiento directo y el enrutamiento indirecto.
Enrutamiento directo
El enrutamiento directo se refiere al envío de un paquete IP directamente del remitente al destinatario dentro de la misma red:
- La máquina que envía un datagrama IP cuenta con la dirección IP del destinatario.
- Obtiene la dirección física de este último mediante el protocolo ARP o en sus tablas, si ya se ha obtenido dicha dirección.
- Envía el paquete a través de la red a esa dirección física.
Enrutamiento indirecto
El enrutamiento indirecto se refiere al envío de un paquete IP a un destino que se encuentra en una red distinta a aquella a la que pertenece el remitente. En este caso, las partes de dirección de red de las direcciones IP de las máquinas de origen y destino son diferentes. La máquina de origen reconoce este hecho. A continuación, envía el paquete a un nodo especial llamado enrutador (router), nodo que conecta una red local con otras redes y cuya dirección IP encuentra en sus tablas; esta dirección se obtuvo inicialmente ya sea de un archivo, de una memoria permanente o mediante información que circula por la red.
Un enrutador está conectado a dos redes y tiene una dirección IP dentro de ambas redes.

En nuestro ejemplo anterior:
- La red n.º 1 tiene la dirección de Internet 193.49.144.0 y la red n.º 2, la dirección 193.49.145.0.
- Dentro de la red n.º 1, el enrutador tiene la dirección 193.49.144.6 y la dirección 193.49.145.3 dentro de la red n.º 2.
La función del enrutador es convertir el paquete IP que recibe —y que está contenido en una trama física típica de la red n.º 1— en una trama física que pueda circular por la red n.º 2. Si la dirección IP del destinatario del paquete se encuentra en la red n.º 2, el enrutador le enviará el paquete directamente; de lo contrario, lo enviará a otro enrutador, que conecta la red n.º 2 con la red n.º 3, y así sucesivamente.
8.1.6.2. Mensajes de error y de control
Siempre en la capa de red, es decir, al mismo nivel que el protocolo IP, existe el protocolo ICMP (Protocolo de mensajes de control de Internet). Sirve para enviar mensajes sobre el funcionamiento interno de la red: nodos en falla, congestión en un enrutador, etc. Los mensajes ICMP se encapsulan en paquetes IP y se envían a la red. Las capas IP de los distintos nodos toman las medidas adecuadas según los mensajes ICMP que reciben. De este modo, una aplicación en sí misma nunca percibe estos problemas propios de la red.
Un nodo utilizará la información ICMP para actualizar sus tablas de enrutamiento.
8.1.7. La capa de transporte: los protocolos UDP y TCP
8.1.7.1. El protocolo UDP: Protocolo de datagramas de usuario
El protocolo UDP permite un intercambio no confiable de datos entre dos puntos, es decir, no se garantiza que un paquete llegue correctamente a su destino. La aplicación, si así lo desea, puede gestionar esto por sí misma, por ejemplo, esperando un acuse de recibo después de enviar un mensaje antes de enviar el siguiente.
Hasta ahora, a nivel de red, hemos hablado de direcciones IP de máquinas. Sin embargo, en una misma máquina pueden coexistir al mismo tiempo diferentes procesos, todos los cuales pueden comunicarse entre sí. Por lo tanto, al enviar un mensaje, es necesario indicar no solo la dirección IP de la máquina destinataria, sino también el «nombre» del proceso destinatario. Este nombre es, en realidad, un número, llamado número de puerto. Algunos números están reservados para aplicaciones estándar: el puerto 69 para la aplicación tftp (protocolo trivial de transferencia de archivos), por ejemplo.
Los paquetes gestionados por el protocolo UDP también se denominan datagramas. Tienen la siguiente forma:

Estos datagramas se encapsularán en paquetes IP y, posteriormente, en tramas físicas.
8.1.7.2. El protocolo TCP: Protocolo de control de transferencia
Para garantizar comunicaciones seguras, el protocolo UDP no es suficiente: el desarrollador de aplicaciones debe crear por sí mismo un protocolo que le permita detectar si los paquetes se están enrutando correctamente.
El protocolo TCP (Protocolo de control de transferencia) evita estos problemas. Sus características son las siguientes:
- El proceso que desea transmitir establece primero una conexión con el proceso destinatario de la información que va a enviar. Esta conexión se realiza entre un puerto de la máquina emisora y un puerto de la máquina receptora. Entre ambos puertos se crea así una ruta virtual que quedará reservada exclusivamente para los dos procesos que hayan establecido la conexión.
- Todos los paquetes enviados por el proceso de origen siguen esta ruta virtual y llegan en el orden en que fueron enviados, lo cual no estaba garantizado en el protocolo UDP, ya que los paquetes podían seguir rutas diferentes.
- La información transmitida tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta información no se envía necesariamente de inmediato: el protocolo TCP espera tener suficiente cantidad para enviarla. Se almacena en una estructura llamada segmento TCP. Una vez lleno, este segmento se transmitirá a la capa IP, donde se encapsulará en un paquete IP.
- Cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP receptor verifica que reciba los segmentos en orden. Por cada segmento recibido correctamente, envía un acuse de recibo al remitente.
- Cuando este último lo recibe, se lo indica al proceso emisor. De esta manera, este último puede saber que un segmento ha llegado a su destino, lo cual no era posible con el protocolo UDP.
- Si, tras un cierto tiempo, el protocolo TCP que ha enviado un segmento no recibe un acuse de recibo, vuelve a transmitir el segmento en cuestión, garantizando así la calidad del servicio de transmisión de la información.
- El circuito virtual establecido entre los dos procesos que se comunican es full-duplex: esto significa que la información puede transitar en ambos sentidos. De este modo, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen continúa enviando información. Esto permite, por ejemplo, que el protocolo de origen TCP envíe varios segmentos sin esperar un acuse de recibo. Si, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento específico n.º n, reanudará la transmisión de los segmentos a partir de ese punto.
8.1.8. La capa de aplicaciones
Por encima de los protocolos UDP y TCP, existen varios protocolos estándar:
TELNET
Este protocolo permite que un usuario de una máquina A de la red se conecte a una máquina B (a menudo llamada máquina anfitriona). TELNET emula en la máquina A un terminal denominado universal. Por lo tanto, el usuario actúa como si tuviera un terminal conectado a la máquina B. Telnet se basa en el protocolo TCP.
FTP: (Protocolo de transferencia de archivos)
Este protocolo permite el intercambio de archivos entre dos máquinas remotas, así como operaciones con archivos, como la creación de directorios, por ejemplo. Se basa en el protocolo TCP.
TFTP: (Control trivial de transferencia de archivos)
Este protocolo es una variante de FTP. Se basa en el protocolo UDP y es menos sofisticado que FTP.
DNS: (Sistema de Nombres de Dominio)
Cuando un usuario desea intercambiar archivos con una máquina remota, por ejemplo, mediante FTP, debe conocer la dirección de Internet de dicha máquina. Por ejemplo, para realizar una conexión FTP en la máquina Lagaffe de la Universidad de Angers, habría que ejecutar FTP de la siguiente manera: FTP 193.49.144.1
Esto obliga a tener un directorio que establezca la correspondencia entre máquina <--> dirección IP. Probablemente, en este directorio las máquinas se designarían con nombres simbólicos tales como:
máquina DPX2/320 de la Universidad de Angers
máquina Sun de la ISERPA de Angers
Es evidente que sería más conveniente designar una máquina por un nombre en lugar de por su dirección IP. Entonces surge el problema de la unicidad del nombre: hay millones de máquinas interconectadas. Se podría imaginar que un organismo centralizado asignara los nombres. Sin duda, eso sería bastante engorroso. De hecho, el control de los nombres se ha distribuido en dominios. Cada dominio es administrado por un organismo, por lo general de estructura muy ágil, que tiene total libertad para elegir los nombres de las máquinas. Así, las máquinas en Francia pertenecen al dominio «fr», administrado por el Inria de París. Para seguir simplificando las cosas, se distribuye aún más el control: se crean dominios dentro del dominio «fr». De este modo, la Universidad de Angers pertenece al dominio «univ-Angers». El servicio que administra este dominio tiene total libertad para nombrar las máquinas de la red de la Universidad de Angers. Por el momento, este dominio no se ha subdividido. Pero en una gran universidad con muchas máquinas en red, podría subdividirse.
La máquina DPX2/320 de la Universidad de Angers se ha denominado Lagaffe, mientras que una PC y una 486DX50 se han denominado liny. ¿Cómo se pueden referenciar estas máquinas desde el exterior? Especificando la jerarquía de los dominios a los que pertenecen. Así, el nombre completo de la máquina Lagaffe será:
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
Por último, dentro del dominio univ-Angers, se podrá referenciar simplemente como
Por lo tanto, una aplicación puede hacer referencia a una máquina por su nombre. Al final, sin embargo, es necesario obtener la dirección de Internet de esa máquina. ¿Cómo se logra esto? Supongamos que desde una máquina A queremos comunicarnos con una máquina B.
- Si la máquina B pertenece al mismo dominio que la máquina A, probablemente se encontrará su dirección IP en un archivo de la máquina A.
- De lo contrario, la máquina A encontrará, en otro archivo o en el mismo que antes, una lista de varios servidores de nombres con sus direcciones IP. Un servidor de nombres se encarga de establecer la correspondencia entre el nombre de una máquina y su dirección IP. La máquina A enviará una consulta especial al primer servidor de nombres de su lista, denominada consulta DNS, que incluye, por lo tanto, el nombre de la máquina buscada. Si el servidor consultado tiene ese nombre en sus registros, enviará a la máquina A la dirección IP correspondiente. De lo contrario, el servidor también encontrará en sus archivos una lista de servidores de nombres a los que puede consultar. Entonces lo hará. De este modo, se consultarán varios servidores de nombres, no de manera desordenada, sino de forma que se minimicen las consultas. Si finalmente se encuentra la máquina, la respuesta llegará hasta la máquina A.
XDR: (Representación de datos eXternal)
Creado por Sun MicroSystems, este protocolo especifica una representación estándar de los datos, independiente de las máquinas.
RPC: (Llamada a procedimiento remoto)
Definido también por Sun, es un protocolo de comunicación entre aplicaciones remotas, independiente de la capa de transporte. Este protocolo es importante: libera al programador de la necesidad de conocer los detalles de la capa de transporte y hace que las aplicaciones sean portables. Este protocolo se basa en el protocolo XDR
NFS: Sistema de archivos de red
También definido por Sun, este protocolo permite que una máquina «vea» el sistema de archivos de otra máquina. Se basa en el protocolo RPC mencionado anteriormente.
8.1.9. Conclusión
En esta introducción hemos presentado algunas ideas generales sobre los protocolos de Internet. Para profundizar en este tema, se recomienda leer el excelente libro de Douglas Comer:
Título: TCP/IP: Arquitectura, protocolos, aplicaciones.
Autor: Douglas COMER
Editorial: InterEditions
8.2. Gestión de direcciones de red en Java
8.2.1. Definición
Cada equipo en Internet se identifica mediante una dirección o un nombre únicos. Estas dos entidades se administran en Java mediante la clase InetAddress, de la cual a continuación se presentan algunos métodos:
devuelve los 4 bytes de la dirección IP de la instancia InetAddress actual | |
devuelve la dirección IP de la instancia actual InetAddress | |
proporciona el nombre de Internet de la instancia actual InetAddress | |
proporciona la identidad de la dirección IP/nombre de Internet de la instancia actual InetAddress | |
crea la instancia InetAddress de la máquina designada por Host. Genera una excepción si Host es desconocido. Host puede ser el nombre de Internet de una máquina o su dirección IP en la forma I1.I2.I3.I4 | |
crea la instancia InetAddress de la máquina en la que se ejecuta el programa que contiene esta instrucción. |
8.2.2. Algunos ejemplos
8.2.2.1. Identificar la máquina local
import java.net.*;
public class localhost{
public static void main (String arg[]){
try{
InetAddress adresse=InetAddress.getLocalHost();
byte[] IP=adresse.getAddress();
System.out.print("IP=");
int i;
for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
System.out.println(IP[i]);
System.out.println("adresse="+adresse.getHostAddress());
System.out.println("nom="+adresse.getHostName());
System.out.println("identité="+adresse);
} catch (UnknownHostException e){
System.out.println ("Erreur getLocalHost : "+e);
}// fin de try
}// fin de main
}// fin de la clase
Los resultados de la ejecución son los siguientes:
Cada máquina tiene una dirección interna IP, que es 127.0.0.1. Cuando un programa utiliza esta dirección de red, está utilizando la máquina en la que se ejecuta. La ventaja de esta dirección es que no requiere una tarjeta de red. Por lo tanto, se pueden probar programas de red sin estar conectado a una red. Otra forma de referirse a la máquina local es utilizar el nombre «localhost».
8.2.2.2. Identificar cualquier máquina
import java.net.*;
public class getbyname{
public static void main (String arg[]){
String nomMachine;
// se recupera el argumento
if(arg.length==0)
nomMachine="localhost";
else nomMachine=arg[0];
// intentamos obtener la dirección de la máquina
try{
InetAddress adresse=InetAddress.getByName(nomMachine);
System.out.println("IP : "+ adresse.getHostAddress());
System.out.println("nom : "+ adresse.getHostName());
System.out.println("identité : "+ adresse);
} catch (UnknownHostException e){
System.out.println ("Erreur getByName : "+e);
}// fin de try
}// fin de main
}// fin de la clase
Con la llamada **java getbyname**, se obtienen los siguientes resultados:
Con la llamada java getbyname shiva.istia.univ-angers.fr, se obtiene:
Con la llamada Java getbyname www.ibm.com, se obtiene:
8.3. Comunicaciones TCP-IP
8.3.1. Información general

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 opera la aplicación AppB. De hecho, la máquina B puede albergar numerosas aplicaciones que operan en Internet. Cuando recibe información procedente de la red, debe saber a qué aplicación va dirigida dicha información. Las aplicaciones de la máquina B tienen acceso a la red a través de «ventanillas», también llamadas puertos de comunicación. Esta información está contenida en el paquete recibido por la máquina B para que pueda entregarse a la aplicación correcta.
- Los protocolos de comunicación que comprende la máquina B. En nuestro estudio, utilizaremos únicamente los protocolos TCP-IP.
- El protocolo de comunicación aceptado por la aplicación AppB. De hecho, las máquinas A y B se «comunicarán» entre sí. Lo que se digan quedará encapsulado en los protocolos TCP-IP. Sin embargo, cuando, al final de la cadena, la aplicación AppB reciba la información enviada por la aplicación AppA, deberá ser capaz de interpretarla. Esto es similar a la situación en la que dos personas, A y B, se comunican por teléfono: su diálogo se transmite a través del teléfono. El habla será codificada en forma de señales por el teléfono de A, transmitida por las líneas telefónicas, llegará al teléfono de B para ser decodificada allí. La persona B entonces escucha 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 manera efectiva.
Por lo tanto, las dos aplicaciones que se comunican deben ponerse de acuerdo sobre el tipo de diálogo que van a adoptar. Así, 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 comunicación diferente.
8.3.2. Las características del protocolo TCP
Aquí solo analizaremos las comunicaciones de red que utilizan el protocolo de transporte TCP. Recordemos las características de este protocolo:
- El proceso que desea transmitir establece primero una conexión con el proceso destinatario de la información que va a enviar. Esta conexión se realiza entre un puerto de la máquina emisora y un puerto de la máquina receptora. Entre ambos puertos se crea así una ruta virtual que quedará reservada exclusivamente para los dos procesos que han establecido la conexión.
- Todos los paquetes enviados por el proceso de origen siguen esta ruta virtual y llegan en el orden en que fueron enviados
- La información transmitida tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta información no se envía necesariamente de inmediato: el protocolo TCP espera tener suficiente cantidad para enviarla. Se almacena en una estructura llamada segmento TCP. Una vez que este segmento se llene, se transmitirá a la capa IP, donde se encapsulará en un paquete IP.
- Cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP receptor verifica que reciba los segmentos en orden. Por cada segmento recibido correctamente, envía un acuse de recibo al remitente.
- Cuando este último lo recibe, se lo indica al proceso emisor. De esta manera, este último puede saber que un segmento ha llegado a su destino.
- Si, tras un cierto tiempo, el protocolo TCP que ha enviado un segmento no recibe un acuse de recibo, vuelve a transmitir el segmento en cuestión, garantizando así la calidad del servicio de transmisión de la información.
- El circuito virtual establecido entre los dos procesos que se comunican es full-duplex: esto significa que la información puede transitar en ambos sentidos. De este modo, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen continúa enviando información. Esto permite, por ejemplo, que el protocolo de origen TCP envíe varios segmentos sin esperar un acuse de recibo. Si, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento específico n.º n, reanudará la transmisión de los segmentos a partir de ese punto.
8.3.3. La relación cliente-servidor
A menudo, la comunicación en Internet es asimétrica: la máquina A inicia una conexión para solicitar un servicio a la máquina B; especifica que desea establecer una conexión con el servicio SB1 de la máquina B. Esta última acepta o rechaza la solicitud. Si acepta, la máquina A puede enviar sus solicitudes al servicio SB1. Estas deben cumplir con el protocolo de comunicación que entiende el servicio SB1. De esta manera, se establece un diálogo de solicitud-respuesta entre la máquina A, a la que llamamos máquina cliente, y la máquina B, a la que llamamos máquina servidor. Uno de los dos participantes cerrará la conexión.
8.3.4. Arquitectura de un cliente
La arquitectura de un programa de red que solicita los servicios de una aplicación servidor será la siguiente:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
8.3.5. Arquitectura de un servidor
La arquitectura de un programa que ofrece servicios será la siguiente:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
El programa servidor procesa de manera diferente la solicitud de conexión inicial de un cliente y sus solicitudes posteriores para obtener un servicio. El programa no proporciona el servicio por sí mismo. Si lo hiciera, durante el tiempo que durara el servicio ya no estaría atento a las solicitudes de conexión y, por lo tanto, los clientes no recibirían el servicio. Por lo tanto, procede de otra manera: tan pronto como se recibe una solicitud de conexión en el puerto de escucha y esta es aceptada, el servidor crea una tarea encargada de prestar el servicio solicitado por el cliente. Este servicio se presta en otro puerto de la máquina servidora llamado puerto de servicio. De esta manera, se puede atender a varios clientes al mismo tiempo.
Una tarea de servicio tendrá la siguiente estructura:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
8.3.6. La clase Socket
8.3.6.1. Définition
La herramienta básica que usan los programas para comunicarse por Internet es el socket. Esta palabra en inglés significa «enchufe». Aquí se amplía su significado para referirse a un «enchufe de red». Para que una aplicación pueda enviar y recibir información a través de Internet, necesita un conector de red, un socket. Esta herramienta se creó inicialmente en las versiones de Unix de la Universidad de Berkeley. Desde entonces, se ha adaptado a todos los sistemas Unix, así como al entorno Windows. También existe en las máquinas virtuales Java en dos formas: la clase Socket para aplicaciones cliente y la clase ServerSocket para aplicaciones servidor. A continuación explicamos algunos de los constructores y métodos de la clase Socket:
abre una conexión remota con el puerto port de la máquina host |
devuelve el número del puerto local utilizado por el socket | |||
devuelve el número del puerto remoto al que está conectado el socket | |||
devuelve la dirección local InetAddress a la que está vinculado el socket | |||
devuelve la dirección remota InetAddress a la que está vinculado el socket | |||
devuelve un flujo de entrada que permite leer los datos enviados por el socio remoto | |||
devuelve un flujo de salida que permite enviar datos al socio remoto | |||
cierra el flujo de entrada del socket | |||
cierra el flujo de salida del socket | |||
cierra el socket y sus flujos de E/S | |||
devuelve una cadena de caracteres que «representa» el socket | |||
8.3.6.2. Apertura de una conexión con una máquina servidor
Hemos visto que para que una máquina A abra una conexión con un servicio de una máquina B, necesita dos datos:
- la dirección IP o el nombre de la máquina B
- el número de puerto en el que opera el servicio deseado
El constructor
crea un socket y lo conecta a la máquina host en el puerto port. Este constructor genera una excepción en diferentes casos:
- dirección incorrecta
- puerto incorrecto
- solicitud rechazada
- …
Debemos manejar esta excepción:
Socket sClient=null;
try{
sClient=new Socket(host,port);
} catch(Exception e){
// la conexión falló: se procesa el error
….
}
Si la solicitud de conexión se realiza con éxito, al cliente se le asigna localmente un puerto para comunicarse con la máquina B. Una vez establecida la conexión, se puede conocer este puerto mediante el método:
Si la conexión se realiza con éxito, hemos visto que, por su parte, el servidor delega el servicio a otra tarea que opera en un puerto denominado «puerto de servicio». Este número de puerto se puede obtener mediante el método:
8.3.6.3. Enviar información por la red
Se puede obtener un flujo de escritura en el socket y, por lo tanto, en la red mediante el método:
Todo lo que se envíe en este flujo se recibirá en el puerto de servicio de la máquina servidora. Muchas aplicaciones tienen un diálogo en forma de líneas de texto que terminan con un salto de línea. Por eso, el método println resulta muy práctico en estos casos. Entonces, se transforma el flujo de salida OutputStream en el flujo PrintWriter, que cuenta con el método println. La escritura puede generar una excepción.
8.3.6.4. Leer información proveniente de la red
Se puede obtener un flujo de lectura de la información que llega al socket con el método:
Todo lo que se lea en este flujo proviene del puerto de servicio de la máquina servidora. Para las aplicaciones que tienen un diálogo en forma de líneas de texto terminadas con un salto de línea, se recomienda utilizar el método readLine. Para ello, se transforma el flujo de entrada InputStream en el flujo BufferedReader, que cuenta con el método readLine(). La lectura puede generar una excepción.
8.3.6.5. Cierre de la conexión
Se realiza con el método:
El método puede generar una excepción. Se liberan los recursos utilizados, en particular el puerto de red.
8.3.6.6. La arquitectura del cliente
Ahora contamos con los elementos necesarios para describir la arquitectura básica de un cliente de Internet:
Socket sClient=null;
try{
// se conecta al servicio que opera en el puerto P de la máquina M
sClient=new Socket(M,P);
// se crean los flujos de entrada y salida del socket de cliente
BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);
// bucle de solicitud-respuesta
boolean fini=false;
String demande;
String réponse;
while (! fini){
// Se prepara la solicitud
demande=…
// se envía
out.println(demande);
// se lee la respuesta
réponse=in.readLine();
// se procesa la respuesta
…
}
// se ha completado
sClient.close();
} catch(Exception e){
// se maneja la excepción
….
}
No hemos tratado de manejar los diferentes tipos de excepciones generadas por el constructor Socket ni por los métodos readline, getInputStream, getOutputStream y close, para no complicar el ejemplo. Todo se ha agrupado en una sola excepción.
8.3.7. La clase ServerSocket
8.3.7.1. Définition
Esta clase está diseñada para la gestión de sockets del lado del servidor. A continuación explicamos algunos de los constructores y métodos de esta clase:
crea un socket de escucha en el puerto port | |
lo mismo, pero establece en count el tamaño de la cola, y en c.a.d el número máximo de conexiones de clientes en espera si el servidor está ocupado cuando llega la conexión del cliente. |
devuelve el número del puerto de escucha utilizado por el socket | |
devuelve la dirección local InetAddress a la que está vinculada la socket | |
pone al servidor en espera de una conexión (operación bloqueante). Al recibir una conexión de un cliente, devuelve un socket desde el cual se prestará el servicio al cliente. | |
cierra el socket y sus flujos de E/S | |
devuelve una cadena de caracteres que «representa» el socket | |
cierra el socket de servicio y libera los recursos asociados a él |
8.3.7.2. Apertura del servicio
Se realiza con los dos constructores:
port es el puerto de escucha del servicio: aquel al que los clientes envían sus solicitudes de conexión. count es el tamaño máximo de la fila de espera del servicio (50 por defecto), la cual almacena las solicitudes de conexión de los clientes a las que el servidor aún no ha respondido. Cuando la fila de espera está llena, las solicitudes de conexión que llegan son rechazadas. Ambos constructores generan una excepción.
8.3.7.3. Aceptación de una solicitud de conexión
Cuando un cliente realiza una solicitud de conexión en el puerto de escucha del servicio, este la acepta mediante el método:
Este método devuelve una instancia de Socket: es el socket de servicio, a través del cual se prestará el servicio, por lo general mediante otra tarea. El método puede generar una excepción.
8.3.7.4. Lectura/escritura a través del socket de servicio
Dado que el socket de servicio es una instancia de la clase Socket, se recomienda consultar las secciones anteriores donde se ha tratado este tema.
8.3.7.5. Identificar al cliente
Una vez obtenido el socket de servicio, se puede identificar al cliente con el método
de la clase Socket. De esta manera, se tendrá acceso a la dirección IP y al nombre del cliente.
8.3.7.6. Cerrar el servicio
Esto se realiza con el método
de la clase ServerSocket. Esto libera los recursos ocupados, en particular el puerto de escucha. El método puede generar una excepción.
8.3.7.7. Arquitectura básica de un servidor
A partir de lo anterior, podemos describir la estructura básica de un servidor:
SocketServer sEcoute=null;
try{
// se inicia el servicio
int portEcoute=…
int maxConnexions=…
sEcoute=new ServerSocket(portEcoute,maxConnexions);
// procesamiento de las solicitudes de conexión
boolean fini=false;
Socket sService=null;
while( ! fini){
// espera y aceptación de una solicitud
sService=sEcoute.accept();
// el servicio lo presta otra tarea a la que se le pasa el socket de servicio
new Service(sService).start();
// se vuelve a poner en espera de solicitudes de conexión
}
// Se ha terminado: se cierra el servicio
sEcoute.close();
} catch (Exception e){
// se procesa la excepción
…
}
La clase Service es un thread que podría tener el siguiente aspecto:
public class Service extends Thread{
Socket sService; // el socket de servicio
// constructor
public Service(Socket S){
sService=S;
}
// ejecución
public void run(){
try{
// se crean los flujos de entrada-salida
BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);
// bucle de solicitud-respuesta
boolean fini=false;
String demande;
String réponse;
while (! fini){
// se lee la solicitud
demande=in.readLine();
// se procesa
…
// se prepara la respuesta
réponse=…
// se envía
out.println(réponse);
}
// se ha completado
sService.close();
} catch(Exception e){
// se maneja la excepción
….
}// try
} // ejecutar
8.4. Aplicaciones
8.4.1. Servidor de eco
Nos proponemos escribir un servidor de eco que se iniciará desde una ventana DOS mediante el comando:
El servidor opera en el puerto pasado como parámetro. Simplemente devuelve al cliente la solicitud que este le envió, acompañada de su identidad (IP+nombre). Acepta dos conexiones en su lista de espera. Aquí tenemos todos los componentes de un servidor TCP. El programa es el siguiente:
// llamada: serveurEcho puerto
// servidor de eco
// devuelve al cliente la línea que este le envió
import java.net.*;
import java.io.*;
public class serveurEcho{
public final static String syntaxe="Syntaxe : serveurEcho port";
public final static int nbConnexions=2;
// programa principal
public static void main (String arg[]){
// ¿hay algún argumento?
if(arg.length != 1)
erreur(syntaxe,1);
// este argumento debe ser un número entero mayor que 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(arg[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// se crea el socket de escucha
ServerSocket ecoute=null;
try{
ecoute=new ServerSocket(port,nbConnexions);
} catch (Exception e){
erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
}
// seguimiento
System.out.println("Serveur d'écho lancé sur le port " + port);
// bucle de servicio
boolean serviceFini=false;
Socket service=null;
while (! serviceFini){
// espera a un cliente
try{
service=ecoute.accept();
} catch (IOException e){
erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
}
// se identifica la conexión
try{
System.out.println("Client ["+identifie(service.getInetAddress())+","+
service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
+ "," + service.getLocalPort() + "]");
} catch (Exception e) {
erreur("identification liaison",1);
}
// el servicio lo proporciona otra tarea
new traiteClientEcho(service).start();
}// fin de while
}// fin de main
// visualización de errores
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
// identifica
private static String identifie(InetAddress Host){
// identificación del host
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// fin de la clase
// presta servicio a un cliente del servidor de eco
class traiteClientEcho extends Thread{
private Socket service; // socket de servicio
private BufferedReader in; // flujo de entrada
private PrintWriter out; // flujo de salida
// constructor
public traiteClientEcho(Socket service){
this.service=service;
}
// método run
public void run(){
// creación de los flujos de entrada y salida
try{
in=new BufferedReader(new InputStreamReader(service.getInputStream()));
} catch (IOException e){
erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
}// fin de try
try{
out=new PrintWriter(service.getOutputStream(),true);
} catch (IOException e){
erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
}// fin de try
// se envía la identificación de la conexión al cliente
try{
out.println("Client ["+identifie(service.getInetAddress())+","+
service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
+ "," + service.getLocalPort() + "]");
} catch (Exception e) {
erreur("identification liaison",1);
}
// bucle de lectura de solicitud/escritura de respuesta
String demande,reponse;
try{
// el servicio se detiene cuando el cliente envía un marcador de fin de archivo
while ((demande=in.readLine())!=null){
// eco de la solicitud
reponse="["+demande+"]";
out.println(reponse);
// el servicio se detiene cuando el cliente envía «fin»
if(demande.trim().toLowerCase().equals("fin")) break;
}// fin del while
} catch (IOException e){
erreur("Erreur lors des échanges client/serveur ("+e+")",3);
}// fin de try
// se cierra el socket
try{
service.close();
} catch (IOException e){
erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
}// fin de try
}// fin de ejecución
// visualización de errores
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}// fin del error
// identifica
private String identifie(InetAddress Host){
// identificación del host
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// fin de clase
Las dos clases necesarias para el servicio se han reunido en un mismo archivo fuente. Solo una de ellas, la que tiene la función main, cuenta con el atributo public. La estructura del servidor se ajusta a la arquitectura general de los servidores TCP. Se ha agregado un método (identifie) que permite identificar la conexión entre el servidor y un cliente. A continuación se muestran algunos resultados:
El servidor se inicia con el comando
A continuación, muestra en la ventana de control el siguiente mensaje:
Para probar este servidor, se utiliza el programa telnet, que está disponible tanto en Unix como en Windows. Telnet es un cliente TCP universal adecuado para todos los servidores que aceptan líneas de texto que terminan con un carácter de fin de línea en su comunicación. Este es el caso de nuestro servidor de eco. Iniciamos un primer cliente telnet en Windows (2000 en este ejemplo) al escribir telnet en una ventana DOS:
DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1
Le caractère d'échappement est 'CTRL+$'
Microsoft Telnet> help
Les commandes peuvent être abrégées. Les commandes prises en charge sont :
close ferme la connexion en cours
display affiche les paramètres d'opération
open ouvre une connexion à un site
quit quitte telnet
set définit les options (entrez 'set ?' pour afficher la liste)
status affiche les informations d'état
unset annule les options (entrez 'unset ?' pour afficher la liste)
? ou help affiche des informations d'aide
Microsoft Telnet> set ?
NTLM Active l'authentification NTLM.
LOCAL_ECHO Active l'écho local.
TERM x (où x est ANSI, VT100, VT52 ou VTNT))
CRLF Envoi de CR et de LF
Microsoft Telnet> set local_echo
Microsoft Telnet> open localhost 187
El programa telnet, por defecto, no muestra el eco de los comandos que se escriben en el teclado. Para obtener este eco, se ejecuta el comando:
Para establecer una conexión con el servidor, indicándole el puerto del servicio de eco (187) y la dirección de la máquina en la que se encuentra (localhost), se ejecuta el comando:
En la ventana de DOS del cliente, se recibe entonces el mensaje:
En la ventana del servidor, aparece el mensaje:
Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Aquí, tahe y localhost se refieren a la misma máquina. En la ventana del cliente telnet, se pueden escribir líneas de texto. El servidor las devuelve como eco:
Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]
Cabe señalar que el puerto del cliente (1059) se detecta correctamente, pero que el puerto del servicio (187) es idéntico al puerto de escucha (187), lo cual es inesperado. De hecho, cabría esperar obtener el puerto del socket de servicio y no el puerto de escucha. Habría que verificar si se obtienen los mismos resultados en Unix. Ahora, iniciemos un segundo cliente telnet. La ventana del servidor queda así:
Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
En la ventana del segundo cliente, también podemos escribir líneas de texto:
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]
Así se observa que el servidor de eco puede atender a varios clientes al mismo tiempo. Los clientes telnet pueden cerrarse al cerrar la ventana de DOS en la que se ejecutan.
8.4.2. Un cliente Java para el servidor de eco
En la sección anterior, utilizamos un cliente telnet para probar el servicio de eco. Ahora escribiremos nuestro propio cliente:
// llamada: clientEcho puerto de la máquina
// cliente del servidor de eco
// envía líneas al servidor, que se las devuelve como eco
import java.net.*;
import java.io.*;
public class clientEcho{
public final static String syntaxe="Syntaxe : clientEcho machine port";
// programa principal
public static void main (String arg[]){
// ¿hay dos argumentos?
if(arg.length != 2)
erreur(syntaxe,1);
// el primer argumento debe ser el nombre de una máquina existente
String machine=arg[0];
InetAddress serveurAddress=null;
try{
serveurAddress=InetAddress.getByName(machine);
} catch (Exception e){
erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
}
// el puerto debe ser un número entero mayor que 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(arg[1]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\nPort incorrect ("+E+")",3);
// se conecta al servidor
Socket sClient=null;
try{
sClient=new Socket(machine,port);
} catch (Exception e){
erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
}
// se identifica la conexión
try{
System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
+ "," + sClient.getPort() + "]");
} catch (Exception e) {
erreur("identification liaison ("+e+")",5);
}
// se crea el flujo de lectura de las líneas tecleadas
BufferedReader IN=null;
try{
IN=new BufferedReader(new InputStreamReader(System.in));
} catch (Exception e){
erreur("Création du flux d'entrée clavier ("+e+")",6);
}
// creación del flujo de entrada asociado al socket del cliente
BufferedReader in=null;
try{
in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
} catch (Exception e){
erreur("Création du flux d'entrée de la socket client("+e+")",7);
}
// creación del flujo de salida asociado al socket del cliente
PrintWriter out=null;
try{
out=new PrintWriter(sClient.getOutputStream(),true);
} catch (Exception e){
erreur("Création du flux de sortie de la socket ("+e+")",8);
}
// bucle de solicitudes y respuestas
boolean serviceFini=false;
String demande=null;
String reponse=null;
// se lee el mensaje enviado por el servidor inmediatamente después de la conexión
try{
reponse=in.readLine();
} catch (IOException e){
erreur("Lecture réponse ("+e+")",4);
}
// visualización de la respuesta
System.out.println("Serveur : " +reponse);
while (! serviceFini){
// lectura de una línea escrita en el teclado
System.out.print("Client : ");
try{
demande=IN.readLine();
} catch (Exception e){
erreur("Lecture ligne ("+e+")",9);
}
// envío de una solicitud a la red
try{
out.println(demande);
} catch (Exception e){
erreur("Envoi demande ("+e+")",10);
}
// espera/lectura de la respuesta
try{
reponse=in.readLine();
} catch (IOException e){
erreur("Lecture réponse ("+e+")",4);
}
// visualización de la respuesta
System.out.println("Serveur : " +reponse);
// ¿Ya terminó?
if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
}
// Ya terminó
try{
sClient.close();
} catch(Exception e){
erreur("Fermeture socket ("+e+")",11);
}
}// mano
// visualización de errores
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
// identifica
private static String identifie(InetAddress Host){
// identificación del host
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// fin de clase
La estructura de este cliente se ajusta a la arquitectura general de los clientes tcp. Aquí se han manejado las diferentes excepciones posibles, una por una, lo que hace que el programa sea más pesado. Estos son los resultados obtenidos al probar este cliente:
Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]
Las líneas que comienzan con Client son las enviadas por el cliente, y las que comienzan con Serveur son las que el servidor devolvió como respuesta.
8.4.3. Un cliente genérico TCP
Muchos de los servicios creados en los inicios de Internet funcionan según el modelo del servidor de eco analizado anteriormente: las comunicaciones entre cliente y servidor se realizan mediante el intercambio de líneas de texto. Vamos a escribir un cliente TCP genérico que se iniciará de la siguiente manera: java cltTCPgenerique servidor puerto
Este cliente TCP se conectará al puerto port del servidor serveur. Una vez hecho esto, creará dos subprocesos:
- un hilo encargado de leer los comandos ingresados desde el teclado y enviarlos al servidor
- un hilo encargado de leer las respuestas del servidor y mostrarlas en la pantalla
¿Por qué dos subprocesos si en la aplicación anterior no se había sentido esa necesidad? En esta última, el protocolo de comunicación era conocido: el cliente enviaba una sola línea y el servidor respondía con una sola línea. Cada servicio tiene su protocolo específico y también se dan las siguientes situaciones:
- el cliente debe enviar varias líneas de texto antes de recibir una respuesta
- la respuesta de un servidor puede contener varias líneas de texto
Por lo tanto, el bucle que consiste en enviar una sola línea al servidor y recibir una sola línea enviada por el servidor no siempre es adecuado. Por lo tanto, crearemos dos bucles independientes:
- un ciclo para leer los comandos ingresados en el teclado para enviarlos al servidor. El usuario indicará el fin de los comandos con la palabra clave fin.
- un bucle para recibir y mostrar las respuestas del servidor. Este será un bucle infinito que solo se interrumpirá cuando el servidor cierre la conexión de red o cuando el usuario, desde el teclado, escriba el comando fin.
Para que estos dos bucles estén separados, necesitamos dos subprocesos independientes. Veamos un ejemplo de ejecución en el que nuestro cliente TCP genérico se conecta a un servicio SMTP (Protocolo de transferencia SendMail). Este servicio se encarga de enviar el correo electrónico a sus destinatarios. Funciona en el puerto 25 y utiliza un protocolo de comunicación basado en el intercambio de líneas de texto.
Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test
ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Analicemos estos intercambios entre el cliente y el servidor:
- el servicio SMTP envía un mensaje de bienvenida cuando un cliente se conecta a él:
- Algunos servicios cuentan con un comando help que proporciona información sobre los comandos que se pueden utilizar con el servicio. En este caso, no es así. Los comandos SMTP utilizados en el ejemplo son los siguientes:
- mail from: expéditeur, para indicar la dirección de correo electrónico del remitente del mensaje
- rcpt to: destinataire, para indicar la dirección de correo electrónico del destinatario del mensaje. Si hay varios destinatarios, se repite el comando rcpt to: tantas veces como sea necesario para cada uno de ellos.
- data, que le indica al servidor SMTP que se va a enviar el mensaje. Como se indica en la respuesta del servidor, este consiste en una secuencia de líneas que termina con una línea que contiene únicamente el carácter punto. Un mensaje puede tener encabezados separados del cuerpo del mensaje por una línea en blanco. En nuestro ejemplo, hemos incluido un asunto con la palabra clave Subject:
- Una vez enviado el mensaje, se le puede indicar al servidor que hemos terminado con el comando quit. El servidor entonces cierra la conexión de red. El hilo de lectura puede detectar este evento y detenerse.
- El usuario luego escribe «fin» en el teclado para detener también el hilo de lectura de los comandos ingresados en el teclado.
Si revisamos el correo recibido, tenemos lo siguiente (Outlook):

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> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<-- by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<-- Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<-- by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<-- Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2 Tl. : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 , Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Los comandos principales son los siguientes:
- user login, donde se ingresa el nombre de usuario en la máquina que aloja nuestros correos electrónicos
- contraseña password, donde se ingresa la contraseña asociada al inicio de sesión anterior
- list, para obtener la lista de mensajes en formato de número y tamaño en bytes
- retr i, para leer el mensaje n.º i
- quit, para salir de la sesión.
Ahora veamos el protocolo de comunicación entre un cliente y un servidor web, que por lo general opera en el puerto 80:
Dos> java clientTCPgenerique 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:
El servidor web solo responde después de recibir la línea en blanco. En el ejemplo, solo utilizamos un comando:
que solicita al servidor el URL /index.html e indica que está trabajando con el protocolo HTTP versión 1.0. La versión más reciente de este protocolo es la 1.1. El ejemplo muestra que el servidor respondió enviando el contenido del archivo index.html y luego cerró la conexión, ya que se observa que el hilo de lectura de las respuestas finaliza. Antes de enviar el contenido del archivo index.html, el servidor web envió una serie de encabezados que terminaban con una línea en blanco:
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
La línea <html> es la primera línea del archivo /index.html. Lo anterior se conoce como encabezados HTTP (Protocolo de transferencia HyperText). No vamos a detallar aquí estas cabeceras, pero recordemos que nuestro cliente genérico permite acceder a ellas, lo cual puede ser útil para comprenderlas. La primera línea, por ejemplo:
indica que el servidor web contactado comprende el protocolo HTTP/1.1 y que ha encontrado correctamente el archivo solicitado (200 OK), siendo 200 un código de respuesta HTTP. Las líneas
le indican al cliente que recibirá 11 251 bytes que representan el texto HTML (HyperText Markup Language) y que, al finalizar el envío, se cerrará la conexión.
Así que aquí tenemos un cliente TCP muy práctico. Sin duda hace menos que el programa telnet que usamos anteriormente, pero fue interesante escribirlo nosotros mismos. El programa del cliente TCP genérico es el siguiente:
// paquetes importados
import java.io.*;
import java.net.*;
public class clientTCPgenerique{
// recibe como parámetro las características de un servicio en el formato
// servidor y puerto
// se conecta al servicio
// crea un hilo para leer los comandos ingresados desde el teclado
// estas se enviarán al servidor
// crea un hilo para leer las respuestas del servidor
// estas se mostrarán en la pantalla
// todo termina con el comando «fin» escrito en el teclado
// variable de instancia
private static Socket client;
public static void main(String[] args){
// sintaxis
final String syntaxe="pg serveur port";
// número de argumentos
if(args.length != 2)
erreur(syntaxe,1);
// se anota el nombre del servidor
String serveur=args[0];
// el puerto debe ser un número entero mayor que 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[1]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
client=null;
// pueden surgir problemas
try{
// se conecta al servicio
client=new Socket(serveur,port);
}catch(Exception ex){
// error
erreur("Impossible de se connecter au service ("+ serveur
+","+port+"), erreur : "+ex.getMessage(),3);
// fin
return;
}//catch
// se crean los hilos de lectura/escritura
new ClientSend(client).start();
new ClientReceive(client).start();
// fin del hilo principal
return;
}// main
// visualización de errores
public static void erreur(String msg, int exitCode){
// visualización de error
System.err.println(msg);
// cierre con error
System.exit(exitCode);
}//error
}//clase
class ClientSend extends Thread {
// clase encargada de leer los comandos ingresados desde el teclado
// y enviarlos a un servidor a través de un cliente TCP pasado como parámetro
private Socket client; // el cliente TCP
// constructor
public ClientSend(Socket client){
// se indica el cliente TCP
this.client=client;
}//constructor
// método Run del hilo
public void run(){
// datos locales
PrintWriter OUT=null; // flujo de escritura de red
BufferedReader IN=null; // flujo del teclado
String commande=null; // comando leído del teclado
// gestión de errores
try{
// creación del flujo de escritura de red
OUT=new PrintWriter(client.getOutputStream(),true);
// creación del flujo de entrada del teclado
IN=new BufferedReader(new InputStreamReader(System.in));
// bucle de entrada y envío de comandos
System.out.println("Commandes : ");
while(true){
// lectura del comando ingresado por teclado
commande=IN.readLine().trim();
// ¿Terminado?
if (commande.toLowerCase().equals("fin")) break;
// envío del comando al servidor
OUT.println(commande);
// siguiente comando
}//while
}catch(Exception ex){
// error
System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// fin: se cierran los flujos
try{
OUT.close();client.close();
}catch(Exception ex){}
// se notifica el fin del hilo
System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
}//ejecución
}//clase
class ClientReceive extends Thread{
// clase encargada de leer las líneas de texto destinadas a un
// cliente TCP pasado como parámetro
private Socket client; // el cliente TCP
// constructor
public ClientReceive(Socket client){
// se registra el cliente TCP
this.client=client;
}//constructor
// método Run del hilo
public void run(){
// datos locales
BufferedReader IN=null; // flujo de lectura de red
String réponse=null; // respuesta del servidor
// gestión de errores
try{
// creación del flujo de lectura de red
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
// 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==null) break;
// visualización
System.out.println("<-- "+réponse);
}//while
}catch(Exception ex){
// error
System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// fin: se cierran los flujos
try{
IN.close();client.close();
}catch(Exception ex){}
// se notifica el fin del hilo
System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
}//ejecución
}//clase
8.4.4. Un servidor TCP genérico
Ahora nos enfocamos en un servidor
- que muestra en pantalla los comandos enviados por sus clientes
- y les envía como respuesta las líneas de texto que un usuario escribe en el teclado. Por lo tanto, es este último quien actúa como servidor.
El programa se inicia con: java serveurTCPgenerique portEcoute, donde portEcoute es el puerto al que deben conectarse los clientes. El servicio al cliente estará a cargo de dos subprocesos:
- un hilo dedicado exclusivamente a leer las líneas de texto enviadas por el cliente
- un hilo dedicado exclusivamente a leer las respuestas que el usuario escribe en el teclado. Este indicará, mediante el comando «fin», que cierra la conexión con el cliente.
El servidor crea dos subprocesos por cliente. Si hay n clientes, habrá 2n subprocesos activos al mismo tiempo. El servidor, por su parte, nunca se detiene, salvo que el usuario presione Ctrl-C en el teclado. Veamos algunos ejemplos.
El servidor se inicia en el puerto 100 y se utiliza el cliente genérico para comunicarse con él. La ventana del cliente es la siguiente:
E:\data\serge\MSNET\c#\red\cliente tcp genérico> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]
Las líneas que comienzan con <-- son las enviadas del servidor al cliente; las demás, las del cliente al servidor. La ventana del servidor es la siguiente:
Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Las líneas que comienzan con <-- son las enviadas del cliente al servidor. Las líneas N: son las enviadas del servidor al cliente n.º N. El servidor anterior sigue activo, mientras que el cliente 1 ya ha terminado. Se inicia un segundo cliente para el mismo servidor:
Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]
La ventana del servidor se ve así:
Dos> java serveurTCPgenerique 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> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Ahora abramos un navegador y solicitemos la página http://localhost:88/exemple.html. El navegador se conectará entonces al puerto 88 de la máquina localhost y luego solicitará la página /exemple.html:

Ahora veamos la ventana de nuestro servidor:
Dos>java serveurTCPgenerique 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
<--
Así 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 propósito es indicarle al servidor qué tipo de cliente tiene frente a él. También vemos que los encabezados HTTP terminan con una línea en blanco.
Elaboremos una respuesta para nuestro cliente. El usuario que escribe en el teclado es aquí el verdadero servidor y puede elaborar una respuesta manualmente. Recordemos la respuesta que dio un servidor web en un ejemplo anterior:
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
Intentemos dar una respuesta similar:
...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 : <head><title>Serveur generique</title></head>
2 : <body>
2 : <center>
2 : <h2>Reponse du serveur generique</h2>
2 : </center>
2 : </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]
Las líneas que comienzan con 2: se envían desde el servidor al cliente n.º 2. El comando fin cierra la conexión del servidor con el cliente. En nuestra respuesta nos hemos limitado a los siguientes encabezados HTTP:
HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
No indicamos el tamaño del archivo que vamos a enviar (Content-Length), sino que simplemente indicamos que vamos a cerrar la conexión (Connection: close) después de enviarlo. Esto es suficiente para el navegador. Al ver que la conexión se ha cerrado, sabrá que la respuesta del servidor ha finalizado y mostrará la página HTML que se le envió. Esta última es la siguiente:
2 : <html>
2 : <head><title>Serveur generique</title></head>
2 : <body>
2 : <center>
2 : <h2>Reponse du serveur generique</h2>
2 : </center>
2 : </body>
2 : </html>
A continuación, el usuario cierra la conexión con el cliente al escribir el comando fin. El navegador sabe entonces que la respuesta del servidor ha finalizado y puede mostrarla:

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

es decir, exactamente lo que se envió desde el servidor genérico.
El código del servidor TCP genérico es el siguiente:
// paquetes
import java.io.*;
import java.net.*;
public class serveurTCPgenerique{
// programa principal
public static void main (String[] args){
// recibe las solicitudes de los clientes en el puerto de escucha
// crea un hilo para leer las solicitudes del cliente
// estas se mostrarán en la pantalla
// crea un hilo para leer los comandos ingresados desde el teclado
// estas se enviarán como respuesta al cliente
// todo termina con el comando «fin» escrito en el teclado
final String syntaxe="Syntaxe : pg port";
// variable de instancia
// ¿hay algún argumento?
if(args.length != 1)
erreur(syntaxe,1);
// el puerto debe ser un número entero mayor que 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// se crea el servicio de escucha
ServerSocket ecoute=null;
int nbClients=0; // número de clientes atendidos
try{
// Se crea el servicio
ecoute=new ServerSocket(port);
// seguimiento
System.out.println("Serveur générique lancé sur le port " + port);
// bucle de servicio a los clientes
Socket client=null;
while (true){ // bucle infinito: se detendrá con Ctrl-C
// espera a un cliente
client=ecoute.accept();
// el servicio se ejecuta en subprocesos separados
nbClients++;
// se crean los hilos de lectura/escritura
new ServeurSend(client,nbClients).start();
new ServeurReceive(client,nbClients).start();
// se vuelve a escuchar las solicitudes
}// fin del while
}catch(Exception ex){
// se reporta el error
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
}//catch
}// fin de main
// visualización de los errores
public static void erreur(String msg, int exitCode){
// visualización del error
System.err.println(msg);
// parada con error
System.exit(exitCode);
}//error
}//clase
class ServeurSend extends Thread{
// clase encargada de leer las respuestas ingresadas con el teclado
// y enviarlas a un cliente a través de un cliente TCP pasado al constructor
Socket client; // el cliente TCP
int numClient; // número de cliente
// constructor
public ServeurSend(Socket client, int numClient){
// se registra el cliente TCP
this.client=client;
// y su n.º
this.numClient=numClient;
}//fabricante
// método Run del hilo
public void run(){
// datos locales
PrintWriter OUT=null; // flujo de escritura de red
String réponse=null; // respuesta leída del teclado
BufferedReader IN=null; // flujo del teclado
// seguimiento
System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
// gestión de errores
try{
// creación del flujo de escritura de red
OUT=new PrintWriter(client.getOutputStream(),true);
// creación del flujo de teclado
IN=new BufferedReader(new InputStreamReader(System.in));
// bucle de entrada y envío de comandos
while(true){
// identificación del cliente
System.out.print("--> " + numClient + " : ");
// lectura de la respuesta ingresada por teclado
réponse=IN.readLine().trim();
// ¿Terminado?
if (réponse.toLowerCase().equals("fin")) break;
// envío de la respuesta al servidor
OUT.println(réponse);
// respuesta siguiente
}//mientras
}catch(Exception ex){
// error
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// fin: se cierran los flujos
try{
OUT.close();client.close();
}catch(Exception ex){}
// se notifica el fin del hilo
System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
}//ejecutar
}//clase
class ServeurReceive extends Thread{
// clase encargada de leer las líneas de texto enviadas al servidor
// a través de un cliente TCP pasado al constructor
Socket client; // el cliente TCP
int numClient; // n.º de cliente
// constructor
public ServeurReceive(Socket client, int numClient){
// se registra el cliente TCP
this.client=client;
// y su n.º
this.numClient=numClient;
}//fabricante
// método Run del hilo
public void run(){
// datos locales
BufferedReader IN=null; // flujo de lectura de red
String réponse=null; // respuesta del servidor
// seguimiento
System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
// gestión de errores
try{
// creación del flujo de lectura de red
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
// 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==null) break;
// visualización
System.out.println("<-- "+réponse);
}//while
}catch(Exception ex){
// error
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// fin: se cierran los flujos
try{
IN.close();client.close();
}catch(Exception ex){}
// se notifica el fin del hilo
System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
}//ejecución
}//clase
8.4.5. Un cliente web
En el ejemplo anterior vimos algunos de los encabezados HTTP que enviaba un navegador:
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
Vamos a escribir un cliente web al que se le pasaría como parámetro un URL y que mostraría en pantalla el contenido de ese URL. Supondremos que el servidor web al que se contacta para el URL es compatible con el protocolo HTTP 1.1. De los encabezados anteriores, solo utilizaremos los siguientes:
- El primer encabezado indica qué página queremos
- el segundo, a qué servidor le hacemos la consulta
- el tercero, que queremos que el servidor cierre la conexión después de habernos respondido.
Si en el ejemplo anterior reemplazamos GET por HEAD, el servidor solo nos enviará los encabezados HTTP y no la página HTML.
Nuestro cliente web se invocará de la siguiente manera: java 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>java 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
significa que la página solicitada ha cambiado de ubicación (es decir, de URL). La nueva ubicación, URL, se indica en el encabezado Location:
Si utilizamos GET en lugar de HEAD en la llamada al cliente web:
dos>java 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:
// paquetes importados
import java.io.*;
import java.net.*;
public class clientweb{
// solicita un URL
// muestra el contenido de esta en la pantalla
public static void main(String[] args){
// sintaxis
final String syntaxe="pg URI GET/HEAD";
// número de argumentos
if(args.length != 2)
erreur(syntaxe,1);
// se registra el URI solicitado
String URLString=args[0];
String commande=args[1].toUpperCase();
// verificación de la validez del URI
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI incorrecto
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//captura
// verificación del pedido
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// pedido incorrecto
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// se extrae la información útil del URL
String path=url.getPath();
if(path.equals("")) path="/";
String query=url.getQuery();
if(query!=null) query="?"+query; else query="";
String host=url.getHost();
int port=url.getPort();
if(port==-1) port=url.getDefaultPort();
// se puede trabajar
Socket client=null; // el cliente
BufferedReader IN=null; // el flujo de lectura del cliente
PrintWriter OUT=null; // el flujo de escritura del cliente
String réponse=null; // respuesta del servidor
try{
// se conecta al servidor
client=new Socket(host,port);
// se crean los flujos de entrada y salida del cliente TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// se solicita el URL - envío de los encabezados HTTP
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// se lee la respuesta
while((réponse=IN.readLine())!=null){
// se procesa la respuesta
System.out.println(réponse);
}//mientras
// se ha terminado
client.close();
} catch(Exception e){
// se maneja la excepción
erreur(e.getMessage(),4);
}//catch
}//main
// visualización de errores
public static void erreur(String msg, int exitCode){
// visualización del error
System.err.println(msg);
// cierre con error
System.exit(exitCode);
}//error
}//clase
La única novedad en este programa es el uso de la clase URL. El programa recibe un URL (Localizador Uniforme de Recursos) o un URI (Identificador Uniforme de Recursos) con el formato http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La clase URL nos permite descomponer la cadena del URL en sus diferentes elementos. Se construye un objeto URL a partir de la cadena URLstring recibida como parámetro:
// verificación de la validez del URL
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI incorrecto
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//captura
Si la cadena URL recibida como parámetro no es un URL válido (falta el protocolo, el servidor, etc.), se lanza una excepción. Esto nos permite verificar la validez del parámetro recibido. Una vez creado el objeto URL, se tiene acceso a sus distintos elementos. Así, si el objeto url del código anterior se creó a partir de la cadena
, tendremos:
url.getHost() = serveur
url.getPort() = port o -1 si no se indica el puerto
url.getPath()=cheminPageHTML o la cadena vacía si no hay ruta
url.getQuery() = param1=val1;param2=val2;... o null si no hay solicitud
uri.getProtocol()=http
8.4.6. Cliente web que maneja las redirecciones
El cliente web anterior no maneja una posible redirección de la solicitud URL que él mismo realizó. El siguiente cliente sí la maneja.
- Lee la primera línea de los encabezados HTTP enviados por el servidor para verificar si contiene la cadena «302 Object moved», que indica una redirección
- Lee los encabezados siguientes. Si hay una redirección, busca la línea «Location: url», que proporciona la nueva URL de la página solicitada, y toma nota de esta URL.
- Muestra el resto de la respuesta del servidor. Si hay redirección, se repiten los pasos 1 a 3 con la nueva URL. El programa no acepta más de una redirección. Este límite está definido por una constante que se puede modificar.
A continuación se muestra un ejemplo:
Dos>java 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:
// paquetes importados
import java.io.*;
import java.net.*;
import java.util.regex.*;
public class clientweb2{
// solicita un URL
// muestra el contenido de esta en la pantalla
public static void main(String[] args){
// sintaxis
final String syntaxe="pg URL GET/HEAD";
// número de argumentos
if(args.length != 2)
erreur(syntaxe,1);
// se anota el URI solicitado
String URLString=args[0];
String commande=args[1].toUpperCase();
// verificación de la validez del URI
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI incorrecto
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//captura
// verificación del pedido
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// pedido incorrecto
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// se puede trabajar
Socket client=null; // el cliente
BufferedReader IN=null; // el flujo de lectura del cliente
PrintWriter OUT=null; // el flujo de escritura del cliente
String réponse=null; // respuesta del servidor
final int nbRedirsMax=1; // no se acepta más de una redirección
int nbRedirs=0; // número de redirecciones en curso
String premièreLigne; // primera línea de la respuesta
boolean redir=false; // indica si hay redirección o no
String locationString=""; // la cadena URL de una posible redirección
// expresión regular para encontrar una cadena de redirección URL
Pattern location=Pattern.compile("^Location: (.+?)$");
// manejo de errores
try{
// pueden existir varias cadenas URL que se deben consultar si hay redirecciones
while(nbRedirs<=nbRedirsMax){
// se extrae la información útil del URL
String protocol=url.getProtocol();
String path=url.getPath();
if(path.equals("")) path="/";
String query=url.getQuery();
if(query!=null) query="?"+query; else query="";
String host=url.getHost();
int port=url.getPort();
if(port==-1) port=url.getDefaultPort();
// se conecta al servidor
client=new Socket(host,port);
// se crean los flujos de entrada y salida del cliente TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// se solicita el URL - envío de los encabezados HTTP
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// se lee la primera línea de la respuesta
premièreLigne=IN.readLine();
// eco en pantalla
System.out.println(premièreLigne);
// ¿redirección?
if(premièreLigne.endsWith("302 Object moved")){
// hay una redirección
redir=true;
nbRedirs++;
}//si
// las siguientes cabeceras HTTP hasta encontrar la línea vacía que indica el final de las cabeceras
boolean locationFound=false;
while(!(réponse=IN.readLine()).equals("")){
// se muestra la respuesta
System.out.println(réponse);
// si hay redirección, se busca el encabezado Location
if(redir && ! locationFound){
// se compara la línea con la expresión relacional «location»
Matcher résultat=location.matcher(réponse);
if(résultat.find()){
// si se ha encontrado, se anota el URL de redirección
locationString=résultat.group(1);
// se registra que se ha encontrado
locationFound=true;
}//si
}//si
// siguiente encabezado
}//mientras
// las siguientes líneas de la respuesta
System.out.println(réponse);
while((réponse=IN.readLine())!=null){
// se muestra la respuesta
System.out.println(réponse);
}//mientras
// se cierra la conexión
client.close();
// ¿Ya terminamos?
if ( ! locationFound || nbRedirs>nbRedirsMax)
break;
// hay que realizar una redirección; se genera la nueva URL
URLString=protocol +"://"+host+":"+port+locationString;
url=new URL(URLString);
// seguimiento
System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
}//mientras
} catch(Exception e){
// se maneja la excepción
erreur(e.getMessage(),4);
}//catch
}//main
// visualización de errores
public static void erreur(String msg, int exitCode){
// visualización de error
System.err.println(msg);
// cierre con error
System.exit(exitCode);
}//error
}//clase
8.4.7. Servidor de cálculo de impuestos
Retomamos el ejercicio IMPOTS, que ya se ha abordado de diversas formas. Recordemos la última versión:
Se ha creado una clase base de impuestos. Sus atributos son tres tablas de números:
public class impots{
// los datos necesarios para el cálculo del impuesto
// provienen de una fuente externa
protected double[] limites=null;
protected double[] coeffR=null;
protected double[] coeffN=null;
// campo «constructor» vacío
protected impots(){}
// constructor
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
La clase impots tiene dos constructores:
- un constructor al que se le pasan los tres arreglos de datos necesarios para calcular el impuesto
- un constructor sin parámetros que solo pueden utilizar las clases hijas
A partir de esta clase se derivó la clase impotsJDBC, que permite llenar los tres tableros limites, coeffR y coeffN a partir del contenido de una base de datos:
public class impotsJDBC extends impots{
// se agrega un constructor que permite crear
// las tablas «limites», «coeffr» y «coeffn» a partir de la tabla
// impuestos de una base de datos
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
// dsnIMPOTS: nombre DSN de la base de datos
// userIMPOTS, mdpIMPOTS: nombre de usuario/contraseña de acceso a la base de datos
Se había desarrollado una aplicación gráfica. La aplicación utilizaba un objeto de la clase impotsJDBC. Tanto la aplicación como este objeto se encontraban en la misma máquina. Nos proponemos colocar el programa de prueba y el objeto impotsJDBC en máquinas diferentes. Tendremos una aplicación cliente-servidor en la que el objeto remoto impotsJDBC será el servidor. La nueva clase se llama ServeurImpots y deriva de la clase impotsJDBC:
// paquetes importados
import java.net.*;
import java.io.*;
import java.sql.*;
public class ServeurImpots extends impotsJDBC {
// atributos
int portEcoute; // el puerto de escucha para las solicitudes de los clientes
boolean actif; // estado del servidor
// constructor
public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
throws IOException, SQLException, ClassNotFoundException {
// construcción principal
super(DSNimpots, USERimpots, MDPimpots);
// se indica el puerto de escucha
this.portEcoute=portEcoute;
// por el momento inactivo
actif=false;
// crea e inicia un hilo para leer los comandos ingresados desde el teclado
// el servidor se administrará a partir de estos comandos
Thread admin=new Thread(){
public void run(){
try{
admin();
}catch (Exception ignored){}
}
};
admin.start();
}//ServeurImpots
El único parámetro nuevo en el constructor es el puerto de escucha para las solicitudes de los clientes. Los demás parámetros se pasan directamente a la clase base impotsJDBC. El servidor de impuestos se controla mediante comandos ingresados desde el teclado. Por lo tanto, se crea un hilo para leer estos comandos. Habrá dos posibles: start para iniciar el servicio y stop para detenerlo definitivamente. El método admin que maneja estos comandos es el siguiente:
public void admin() throws IOException{
// lee los comandos de administración del servidor ingresados desde el teclado
// en un bucle infinito
String commande=null;
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
while(true){
// solicita
System.out.print("Serveur d'impôts>");
// lectura del comando
commande=IN.readLine().trim().toLowerCase();
// ejecución del comando
if(commande.equals("start")){
// ¿activo?
if(actif){
//error
System.out.println("Le serveur est déjà actif");
// seguimos
continue;
}//si
// se crea y se inicia el servicio de escucha
Thread ecoute=new Thread(){
public void run(){
ecoute();
}
};
ecoute.start();
}//si
else if(commande.equals("stop")){
// fin de todos los hilos de ejecución
System.exit(0);
}//si
else {
// error
System.out.println("Commande incorrecte. Utilisez (start,stop)");
}//si
}//mientras
}//admin
Si el comando ingresado en el teclado es start, se inicia un hilo para escuchar las solicitudes de los clientes. Si el comando ingresado es stop, se detienen todos los hilos. El hilo de escucha ejecuta el método ecoute:
public void ecoute(){
// hilo de escucha de solicitudes de los clientes
// se crea el servicio de escucha
ServerSocket ecoute=null;
try{
// se crea el servicio
ecoute=new ServerSocket(portEcoute);
// seguimiento
System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);
// bucle de servicio
Socket liaisonClient=null;
while (true){ // bucle infinito
// espera de un cliente
liaisonClient=ecoute.accept();
// el servicio lo realiza otra tarea
new traiteClientImpots(liaisonClient,this).start();
// se vuelve a escuchar las solicitudes
}// fin del while
}catch(Exception ex){
// se reporta el error
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
}//catch
}//hilo de escucha
Encontramos un servidor TCP clásico que escucha en el puerto portEcoute. Las solicitudes de los clientes son procesadas por el método run del hilo traiteCientImpots, al constructor del cual se le pasan dos parámetros:
- el objeto Socket liaisonClient, que permitirá acceder al cliente
- el objeto impotsJDBC this, que dará acceso al método this.calculer para el cálculo del impuesto.
// -------------------------------------------------------
// presta servicio a un cliente del servidor de impuestos
class traiteClientImpots extends Thread{
private Socket liaisonClient; // conexión con el cliente
private BufferedReader IN; // flujo de entrada
private PrintWriter OUT; // flujo de salida
private impotsJDBC objImpots; // objeto Impuesto
// constructor
public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
this.liaisonClient=liaisonClient;
this.objImpots=objImpots;
}//constructor
El método run procesa las solicitudes de los clientes. Se trata de líneas de texto que pueden tener dos formas:
- cálculo de matrimonio (s/n) nbEnfants salaireAnnuel
- fincalculs
La forma 1 permite calcular un impuesto; la forma 2 cierra la conexión entre el cliente y el servidor.
// método de ejecución
public void run(){
// presta el servicio al cliente
try{
// flujo de entrada
IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
// flujo de salida
OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
// envío de un mensaje de bienvenida al cliente
OUT.println("Bienvenue sur le serveur d'impôts");
// bucle de lectura de solicitud/escritura de respuesta
String demande=null;
String[] champs=null; // los elementos de la solicitud
String commande=null; // la orden del cliente: cálculo o fin de cálculos
while ((demande=IN.readLine())!=null){
// se desglosa la solicitud en campos
champs=demande.trim().toLowerCase().split("\\s+");
// dos solicitudes aceptadas: cálculo y fin de cálculos
commande=champs[0];
if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
// error del cliente
OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
// siguiente pedido
continue;
}//si
if(commande.equals("calcul")) calculerImpôt(champs);
if(commande.equals("fincalculs")){
// mensaje de despedida al cliente
OUT.println("Au revoir...");
// liberación de recursos
try{ OUT.close();IN.close();liaisonClient.close();}
catch(Exception ex){}
// fin
return;
}//si
//siguiente solicitud
}//mientras
}catch (Exception e){
erreur("L'erreur suivante s'est produite ("+e+")",2);
}// fin de try
}// fin de ejecución
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 verifica la validez de la solicitud y, si corresponde, se calcula el impuesto y se le devuelve al cliente.
// cálculo de impuestos
public void calculerImpôt(String[] champs){
// procesa la solicitud: cálculo para casados nbEnfants salaireAnnuel
// desglosada en campos en la tabla de campos
String marié=null;
int nbEnfants=0;
int salaireAnnuel=0;
// validez de los argumentos
try{
// se requieren al menos 4 campos
if(champs.length!=4) throw new Exception();
// casado
marié=champs[1];
if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
// hijos
nbEnfants=Integer.parseInt(champs[2]);
// salario
salaireAnnuel=Integer.parseInt(champs[3]);
}catch (Exception ignored){
// error de formato
OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
// fin
return;
}//si
// se puede calcular el impuesto
long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
// se envía la respuesta al cliente
OUT.println(""+impot);
}//calcular
Un programa de prueba podría ser el siguiente:
// llamada: serveurImpots puerto dsnImpots userImpots mdpImpots
import java.io.*;
public class testServeurImpots{
public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";
// programa principal
public static void main (String[] args){
// se requieren 4 argumentos
if(args.length != 4)
erreur(syntaxe,1);
// el puerto debe ser un número entero mayor que 0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// se crea el servidor de impuestos
try{
new ServeurImpots(port,args[1],args[2],args[3]);
}catch(Exception ex){
//error
System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
}//catch
}//Main
// visualización de errores
public static void erreur(String msg, int exitCode){
// visualización de error
System.err.println(msg);
// parada con error
System.exit(exitCode);
}//error
}// fin de clase
Introducimos en el programa de prueba los datos necesarios para crear un objeto ServeurImpots y, a partir de ahí, el programa crea dicho objeto.
Probemos una primera ejecución:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop
El comando
crea un objeto ServeurImpots que aún no está a la espera de solicitudes de los clientes. Es el comando «start», ingresado desde el teclado, el que inicia esta escucha. El comando «stop» detiene el servidor. Ahora utilicemos un cliente. Usaremos el cliente genérico creado anteriormente. El servidor está en funcionamiento:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
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:
Se observa que el cliente ha recibido correctamente el mensaje de bienvenida del servidor. Enviamos otros comandos:
x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<-- syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Regresamos a la ventana del servidor para detenerlo:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop
8.5. Ejercicios
8.5.1. Ejercicio 1 - Cliente TCP genérico gráfico
8.5.1.1. Presentación de la aplicación
Nos proponemos crear un programa capaz de comunicarse a través de Internet con los principales servicios TCP. Lo llamaremos un cliente TCP genérico. Una vez que se comprende esta aplicación, se observa que todos los clientes TCP son similares. La ventana del programa es la siguiente:

El significado de los distintos controles es el siguiente:
n.º | nombre | tipo | función |
1 | TxtRemoteHost | JTextField | nombre de la máquina que ofrece el servicio deseado |
2 | TxtPort | JTextField | puerto del servicio solicitado |
3 | TxtSend | JTextField | texto del mensaje que el cliente enviará al servidor |
4 | OptRCLF OptLF | JCheckBox | Botones que permiten indicar cómo terminan las líneas en el diálogo entre el cliente y el servidor RCLF: retorno de carro (#13) + salto de línea (#10) LF: salto de línea (#10) |
5 | LstSuivi | JList | muestra mensajes sobre el estado de la comunicación entre el cliente y el servidor |
6 | LstDialogue | JList | muestra los mensajes intercambiados por el cliente (->) y el servidor (<-) |
7 | CmdAnnuler | JButton | oculto —ubicado debajo de la lista de diálogo—. Aparece cuando la conexión está en curso y permite interrumpirla si el servidor no responde |
Las opciones de menú disponibles son las siguientes:
opción | subopciones | función |
Conexión | Conectar | conecta al cliente con el servidor |
Desconectar | cierra la conexión | |
Salir | Cierra el programa | |
Mensajes | Enviar | Envía el mensaje del control TxtSend al servidor |
RazSuivi | Borra la lista LstSuivi | |
RazDialogue | Borra la lista LstDialogue | |
Autor | Muestra un cuadro de derechos de autor |
8.5.1.2. FONCTIONNEMENT DE El APPLICATION
Cuando se carga la hoja principal de la aplicación, se realizan las siguientes acciones:
- la hoja se centra en la pantalla
- solo están activas las opciones de menú Connexion/Quitter y Auteur
- el botón Annuler queda oculto
- las listas LstSuivi y LstDialogue están vacías
Esta opción solo está disponible cuando los campos «Host remoto» y «N.º de puerto» no están vacíos y no hay ninguna conexión activa en este momento. Al hacer clic en esta opción, se realizan las siguientes operaciones:
- se verifica la validez del puerto: debe ser un número entero mayor que 0
- se inicia un hilo para establecer la conexión con el servidor
- aparece el botón Annuler para permitir que el usuario interrumpa la conexión en curso
- Se desactivan todas las opciones del menú, excepto Quitter y Auteur
La conexión puede finalizar de varias maneras:
- El usuario presionó el botón Annuler: se detiene el hilo de conexión y se restablece el menú a su estado inicial. En el registro se indica que el usuario cerró la conexión.
- La conexión termina con un error: se hace lo mismo que anteriormente y, además, en el registro se indica la causa del error.
- La conexión finaliza correctamente: se elimina el botón Annuler, se indica en el registro que se ha establecido la conexión, se habilita el menú RazSuivi, se deshabilita el menú Connecter y se habilita el menú Déconnecter
Esta opción solo está disponible cuando existe una conexión con el servidor. Cuando se activa, cierra la conexión con el servidor y restablece el menú a su estado inicial. En el registro se indica que el cliente ha cerrado la conexión.
Esta opción cierra cualquier conexión activa con el servidor y cierra la aplicación.
Esta opción solo está disponible si se cumplen las siguientes condiciones:
-
se ha establecido la conexión con el servidor
-
hay un mensaje para enviar
Si se cumplen estas condiciones, se envía al servidor el texto que se encuentra en el campo TxtSend (3), terminado con la secuencia RCLF si se ha marcado la opción RCLF; de lo contrario, con la secuencia LF. Cualquier error que se produzca durante el envío se indica en la lista de seguimiento.
Borran, respectivamente, las listas LstSuivi y LstDialogue. Estas opciones se desactivan cuando las listas correspondientes están vacías.
Este botón, ubicado en la parte inferior del formulario, solo aparece cuando el cliente se está conectando al servidor. Es posible que esta conexión no se realice porque el servidor no responde o responde incorrectamente. El botón Annuler le da entonces al usuario la posibilidad de interrumpir la solicitud de conexión.
La lista LstSuivi (5) realiza el seguimiento de la conexión. Indica los momentos clave de la conexión:
-
su apertura por parte del cliente
-
su cierre por parte del servidor o del cliente
-
todos los errores que puedan ocurrir mientras la conexión esté activa
La lista LstDialogue (6) realiza el seguimiento del diálogo que se establece entre el cliente y el servidor. Un hilo supervisa en segundo plano lo que ocurre en el socket de comunicación del cliente y lo muestra en la lista 6.
Este menú abre una ventana denominada «Copyright»:

Los errores de conexión se reportan en la lista de seguimiento 6, mientras que los relacionados con el diálogo cliente/servidor se muestran en la lista de diálogo 7. Cuando ocurre un error de conexión, se cierra el diálogo cliente/servidor y el formulario vuelve a su estado inicial, listo para una nueva conexión.
8.5.1.3. TRAVAIL A FAIRE
Llevar a cabo el trabajo descrito anteriormente de dos formas:
- aplicación independiente
- applet
8.5.2. Ejercicio 2: Un servidor de recursos
8.5.2.1. INTRODUCTION
Una institución cuenta con varios servidores de cálculo potentes accesibles a través de Internet. Cualquier máquina que desee utilizar estos servicios de cálculo envía un archivo de datos al puerto 756 de uno de los servidores. Este archivo contiene diversa información: nombre de usuario, contraseña, comandos que indican el tipo de cálculo deseado y los datos sobre los que se debe realizar el cálculo. Si el archivo de datos es correcto, el servidor de cálculo seleccionado lo utiliza y devuelve los resultados al cliente en forma de un archivo de texto.
Las ventajas de esta organización son múltiples:
- Cualquier tipo de cliente (PC, Mac, Unix,...) puede utilizar este servicio
- el cliente puede estar en cualquier lugar de Internet
- los recursos de cómputo están optimizados: solo se necesitan unas pocas máquinas potentes. De este modo, una organización pequeña sin recursos de cómputo puede utilizar este servicio a cambio de una contribución financiera calculada en función del tiempo de cómputo utilizado.
A pesar de la potencia de las máquinas, un cálculo puede durar a veces varias horas: en ese caso, el servidor no está disponible para otros clientes. Entonces, para un cliente surge el problema de encontrar un servidor de cálculo disponible. Para ello, se utiliza un «administrador de recursos de cálculo», denominado en adelante servidor GRC. Este servicio se encuentra en una sola máquina y opera en el puerto 864 en modo TCP. Es a él a quien se dirige un cliente que desea acceder a un servidor de cálculo. El servidor GRC, que cuenta con la lista completa de servidores de cálculo, le responde enviándole el nombre de un servidor que actualmente se encuentra inactivo. Entonces, el cliente solo tiene que enviar sus datos al servidor que se le haya asignado.
Nos proponemos escribir el servidor GRC.
8.5.2.2. El INTERFACE VISUELLE
La interfaz visual será la siguiente:

La interfaz presenta dos listas de servidores:
- a la izquierda, la lista de servidores inactivos y, por lo tanto, disponibles para realizar cálculos
- a la derecha, la lista de servidores ocupados con los cálculos de un cliente.
La estructura del menú es la siguiente:
Menú principal | Menú secundario | Función |
Servicio | Iniciar | Inicia el servicio TCP en el puerto 864 |
Detener | Detiene el servicio | |
Salir | Cierra la aplicación | |
Autor | Información de derechos de autor |
La estructura de los controles presentes en el formulario es la siguiente:
Nombre | Tipo | Función |
listLibres | JList | Lista de servidores disponibles |
listOccupés | JList | Lista de servidores ocupados |
8.5.2.3. FONCTIONNEMENT DE APPLICATION
Al cargar la aplicación, la lista listLibres se completa con la lista de nombres de los servidores de cálculo administrados por el GRC. Estos se definen en un archivo «Servidores» que se pasa como parámetro. Este archivo contiene una lista de nombres de servidores, uno por línea, y por lo tanto se utiliza para completar la lista listLibres. El menú «Iniciar» está habilitado, mientras que el menú «Detener» está deshabilitado.
Esta opción
- inicia el servicio de escucha en el puerto 864 de la máquina
- desactiva el menú Iniciar
- habilita el menú «Detener»
Esta opción detiene el servicio:
- se vacía la lista de servidores ocupados
- la lista de servidores disponibles se llena con el contenido del archivo Servidores
- se habilita el menú Iniciar
- se desactiva el menú «Detener»
La aplicación se cierra.
El diálogo cliente/servidor se realiza mediante el intercambio de líneas de texto que terminan con la secuencia RCLF. El servidor GRC reconoce dos comandos: getservidor y finservicio. A continuación detallamos la función de estos dos comandos:
- 1-getserveur
El cliente pregunta si hay un servidor de cálculo disponible para él.
El servidor GRC toma entonces el primer servidor que encuentra en su lista de servidores libres y le envía su nombre al cliente en el formato:
Además, incluye el servidor asignado al cliente en la lista de servidores ocupados de la siguiente forma:
como se muestra en el siguiente ejemplo, en el que el servidor calcul1.istia.univ-angers.fr está atendiendo al cliente con la dirección IP 193.52.43.5:

Un cliente no puede enviar un comando getservidor si ya se le ha asignado un servidor de cálculo. Por lo tanto, antes de responder al cliente, el servidor GRC verifica que la dirección IP del cliente no figure ya entre las registradas en la lista de servidores ocupados. Si es así, el servidor GRC responde:
Por último, existe el caso en el que no hay ningún servidor de cálculo disponible: la lista de servidores libres está vacía. En este caso, el servidor GRC responde:
En todos los casos, después de responder al cliente, el servidor GRC cierra la conexión con este para poder atender a otros clientes.
- 2-finservice
El cliente indica que ya no necesita el servidor de cálculo que estaba utilizando.
El servidor GRC verifica primero que el cliente sea efectivamente uno de los que estaba atendiendo. Para ello, verifica que la dirección IP del cliente se encuentre entre las registradas en la lista de servidores ocupados. Si no es así, el servidor GRC responde:
Si se reconoce al cliente, el servidor GRC le responde:
y pasa el servidor de cálculo asignado a ese cliente a la lista de servidores libres. Retomando el ejemplo anterior, si el cliente envía el comando finservice, la visualización del servidor GRC queda así:

Después de enviar la respuesta, sea cual sea, el servidor GRC cierra la conexión.
8.5.2.4. TRAVAIL A FAIRE
Escribe la aplicación como un programa independiente que se pueda probar, por ejemplo, con un cliente telnet o con el cliente TCP genérico del ejercicio anterior.
8.5.3. Ejercicio 3: un cliente SMTP
8.5.3.1. INTRODUCTION
En este ejercicio queremos desarrollar un cliente para el servicio SMTP (Protocolo de Transferencia SendMail) que permita enviar correo. En Unix o Windows, el programa telnet es un cliente que funciona con el protocolo tcp. Puede «comunicarse» con cualquier servicio tcp que acepte comandos en formato de texto que terminen con la secuencia RCLF, es decir, los caracteres de código ASCII 13 y 10. A continuación se muestra un ejemplo de comunicación con el servicio smtp para el envío de correo:
// respuesta del servidor SMTP
Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here
// comentarios --------------
El programa telnet puede llamar a cualquier servicio mediante la sintaxis
telnet machine_service port_service
Las comunicaciones entre el cliente y el servidor se realizan mediante líneas de texto que terminan con la secuencia RCLF.
Las respuestas del servicio smtp tienen el siguiente formato:
número-Mensaje o
número de mensaje
El servidor SMTP puede enviar varias líneas de respuesta. La última línea de la respuesta se identifica con un número seguido de un espacio, mientras que en las líneas anteriores de la respuesta, el número va seguido de un guión (-).
Un número mayor o igual a 500 indica un mensaje de error.
// fin de comentarios
// respuesta del servidor SMTP
214-Commands:
214- HELO EHLO MAIL RCPT DATA
214- RSET NOOP QUIT HELP VRFY
214- EXPN VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214- sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info
// comentarios ---------
El comando «mail» tiene la siguiente sintaxis:
mail from: dirección de correo electrónico del remitente del mensaje
// fin de comentarios
// respuesta del servidor SMTP
// comentarios
El servidor smtp no verifica la validez de la dirección del remitente: la acepta tal como se le proporcionó
// fin de comentarios
// comentarios ---------
El comando rcpt tiene la siguiente sintaxis:
rcpt to: dirección de correo electrónico del destinatario del mensaje
Si la dirección de correo electrónico es una dirección de la máquina en la que opera el servidor smtp, este verifica que exista; de lo contrario, no realiza ninguna verificación. Si se ha realizado la verificación y se ha detectado un error, este se indicará con un número >= 500.
Se pueden emitir tantos comandos «rcpt to» como se desee: esto permite enviar un mensaje a varias personas.
// fin de comentarios
// respuesta del servidor SMTP
// comentarios ---------
El comando data tiene la siguiente sintaxis:
data
línea1
línea2
...
.
A continuación, van las líneas de texto que componen el mensaje, el cual debe terminar con una línea que contenga únicamente el carácter «punto».
El mensaje se envía entonces al destinatario indicado por el comando rcpt.
// fin de comentarios
// respuesta del servidor SMTP
// texto del mensaje escrito en el teclado
subject: essai smtp
essai smtp a partir de telnet
.
// comentarios
En las líneas de texto del comando «data», se puede incluir una línea «subject:» para especificar el asunto del correo. Esta línea debe ir seguida de una línea en blanco.
// respuesta del servidor SMTP
// comentarios
El comando «quit» cierra la conexión con el servicio smtp
// fin de comentarios
// respuesta del servidor SMTP
8.5.3.2. El INTERFACE VISUELLE
Nos proponemos desarrollar un programa con la siguiente interfaz visual:

Los controles tienen las siguientes funciones:
Número | Tipo | Función |
1 | JTextField | Secuencia de direcciones de correo electrónico separadas por una coma |
2 | JTextField | Texto del asunto del mensaje |
3 | JTextField | Secuencia de direcciones de correo electrónico separadas por una coma |
4 | JTextField | Secuencia de direcciones de correo electrónico separadas por una coma |
5 | JTextArea | Texto del mensaje |
6 | JList | lista de seguimiento |
7 | JList | lista de diálogo |
8 | JButton | Botón «Cancelar» no representado, que aparece cuando el cliente solicita la conexión al servidor SMTP. Permite al usuario interrumpir esta solicitud si el servidor no responde. |
8.5.3.3. LES MENUS
La estructura de los menús de la aplicación es la siguiente:
Menú principal | Menú secundario | Función |
Correo | ||
Enviar | Envía el mensaje del control 5 | |
Salir | Salir de la aplicación | |
Opciones | ||
Ocultar seguimiento | Oculta el control 6 | |
Borrar seguimiento | Borra la lista de seguimiento 6 | |
Ocultar diálogo | Oculta la lista de diálogo 7 | |
Borrar diálogo | Borra la lista de diálogo 7 | |
Configurar | Permite al usuario especificar - la dirección del servidor SMTP que utiliza el programa - su dirección de correo electrónico | |
Guardar... | Guarda la configuración anterior en un archivo .ini | |
Autor | Información de derechos de autor |
8.5.3.4. FONCTIONNEMENT DE APPLICATION
Este menú hace que aparezca la siguiente ventana:

Ambos campos deben completarse para que el botón OK esté activo. Ambos datos deben almacenarse en variables globales para que estén disponibles para otros módulos.
Esta opción solo está disponible si se cumplen las siguientes condiciones:
- se haya realizado la configuración
- hay un mensaje para enviar
- hay un asunto
- hay al menos un destinatario en los campos 1, 3 y 4
Si se cumplen estas condiciones, la secuencia de eventos es la siguiente:
- el formulario pasa a un estado en el que se desactivan todas las acciones que puedan interferir en el diálogo entre el cliente y el servidor
- se establece una conexión en el puerto 25 del servidor especificado en la configuración
- luego, el cliente se comunica con el servidor SMTP según el protocolo descrito anteriormente
- el campo «mail from:» utiliza la dirección de correo electrónico del remitente indicada en la configuración
- el campo «rcpt to:» se utiliza para cada una de las direcciones de correo electrónico que se encuentran en los campos 1, 3 y 4
- En las líneas enviadas después del comando data, se encontrarán los siguientes textos:
- una línea Subject:: texto del asunto de la verificación 2
- una línea Cc:: direcciones de la verificación 3
- una línea Bcc:: direcciones de la verificación 4
- el texto del mensaje de la verificación 5
- el punto final
Este botón, ubicado en la parte inferior del formulario, solo aparece cuando el cliente se está conectando al servidor smtp. Es posible que esta conexión no se realice porque el servidor smtp no responde o responde incorrectamente. El botón Annuler le da entonces al usuario la posibilidad de interrumpir la solicitud de conexión.
La lista (6) realiza el seguimiento de la conexión. Indica los momentos clave de la conexión:
- su apertura por parte del cliente
- su cierre por parte del servidor o del cliente
- todos los errores de conexión
La lista (7) realiza el seguimiento del diálogo smtp que se establece entre el cliente y el servidor.
Estas dos listas están asociadas a opciones del menú:
Ocultar seguimiento | Oculta la lista de seguimiento 6, así como el texto que se encuentra encima. Si la altura que ocupan estos dos controles es H, todos los controles situados debajo se desplazan hacia arriba una altura de H y el tamaño total del formulario se reduce en H. Además, «Ocultar seguimiento» hace invisible la opción RazSuivi que se encuentra debajo. |
Borrar seguimiento | Borra la lista de seguimiento 6 |
Ocultar cuadro de diálogo | Oculta la lista de diálogo 7, el texto que aparece encima de ella y la opción de menú RazDialogue que se encuentra debajo. Al igual que con «Ocultar seguimiento», se recalcula la posición de los controles situados debajo (tal vez el botón Annuler) y se reduce el tamaño de la ventana. |
Borrar diálogo | Vacía la lista de diálogo 7 |
Este menú abre una ventana denominada «Copyright»:

Los errores de conexión se muestran en la lista de seguimiento 6, mientras que los relacionados con la comunicación cliente/servidor aparecen en la lista de diálogo 7. Cuando se produce un error, se le avisa al usuario mediante un cuadro de error y se muestra la lista con la causa del error, si antes estaba oculta. Además, se cierra el diálogo cliente/servidor y el formulario vuelve a su estado inicial.
8.5.3.5. GESTION de UN FICHIER DE CONFIGURATION
Es recomendable que el usuario no tenga que volver a configurar el software cada vez que lo utilice. Para ello, si la opción Opciones/Guardar la configuración al salir está marcada, al cerrar el programa se guardan los dos datos obtenidos mediante la opción Options/Configurer, así como el estado de las dos listas de seguimiento, en un archivo sendmail.ini ubicado en el mismo directorio que el archivo .exe del programa. Este archivo tiene el siguiente formato:
SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1
Las líneas SmtpServer y ReplyAddress recogen la información obtenida mediante la opción Opciones/Configurar. Las líneas Suivi y Dialogue indican el estado de las listas de Seguimiento y de Diálogo: 1 (presente), 0 (ausente).
Al cargar el programa, se lee el archivo sendmail.ini si existe y el formulario se configura en consecuencia. Si el archivo sendmail.ini no existe, se procede como si existiera:
Si el archivo sendmail.ini existe pero está incompleto (faltan líneas), la línea que falta se reemplaza por la línea correspondiente de las anteriores. Por ejemplo, si falta la línea Suivi=..., se trata como si tuviéramos Suivi=1.
Todas las líneas que no se ajusten a la plantilla:
se ignoran, al igual que aquellas en las que la palabra clave no es válida. La palabra clave puede estar en mayúsculas o minúsculas: no importa.
En la opción Options/Configurer, se muestran los valores SmtpServer y ReplyAddress que están actualmente en uso. El usuario puede modificarlos si lo desea.
8.5.3.6. TRAVAIL A FAIRE
Realice el trabajo descrito anteriormente. Se recomienda ocuparse de la gestión del archivo de configuración al final.
8.5.4. Ejercicio 4 - cliente POPPASS
8.5.4.1. Introduction
Nos proponemos crear un cliente TCP capaz de comunicarse con el servidor POPPASSD que opera en el puerto 106. Este servicio permite cambiar la contraseña en una máquina UNIX. El protocolo de comunicación cliente-servidor es el siguiente:
1 - Las comunicaciones se realizan mediante el intercambio de mensajes que terminan con la secuencia RCLF
2 - El cliente envía comandos al servidor
- El servidor responde con mensajes que comienzan con números de tres dígitos: XXX. Si XXX = 200, el comando se ha ejecutado correctamente; de lo contrario, se ha producido un error.
3 - La secuencia de los intercambios es la siguiente:
- el servidor responde con un mensaje de bienvenida
- El servidor responde solicitando la contraseña si se acepta el inicio de sesión; de lo contrario, devuelve un error
- el servidor responde solicitando la nueva contraseña; si la contraseña es aceptada, responde con un error; de lo contrario, no
- el servidor responde confirmando que la nueva contraseña ha sido aceptada; de lo contrario, devuelve un error
- el servidor envía un mensaje de fin y cierra la conexión
8.5.4.2. El formulario del cliente

El significado de los distintos controles es el siguiente:
n.º | nombre | tipo | función |
1 | txtRemoteHost | JTextField | nombre del servidor |
2 | txtLogin | JTextField | nombre de usuario |
3 | txtMdp | JTextField | Contraseña del usuario |
4 | txtNewMdp | JTextField | Nueva contraseña del usuario |
5 | txtConfirmation | JTextField | Confirmación de la nueva contraseña |
6 | lstSuivi | JList | Mensajes de seguimiento de la conexión |
7 | lstDialogue | JList | Mensajes del diálogo Cliente/Servidor |
10 | cmdAnnuler | JButton | No se muestra: botón que aparece cuando se está estableciendo la conexión con el servidor. Permite detenerla. |
8.5.4.3. Los menús
Título | Nombre del control | Función |
Conexión | mnuconexión | |
Conectar | mnuconnecter | inicia la conexión con el servidor |
Salir | mnuQuitter | cierra la aplicación |
Mensajes | mnuMessages | |
RazSuivi | mnuRazSuivi | borra la lista lstSuivi |
RazDialogue | mnuRazDialogue | borra la lista lstDialogue |
Autor | mnuAuteur | muestra el cuadro de derechos de autor |
8.5.4.4. Funcionamiento de la aplicación
Cuando se carga la hoja principal de la aplicación, ocurren lo siguiente:
- la hoja se centra en la pantalla
- solo están activas las opciones de menú Connexion/Quitter y Auteur
- el botón Annuler queda oculto
- las listas LstSuivi y LstDialogue están vacías
Esta opción solo está disponible cuando se han completado los campos 1 a 5. Al hacer clic en esta opción, se realizan las siguientes operaciones:
- se inicia un hilo para garantizar la conexión con el servidor
- aparece el botón Annuler para permitir que el usuario interrumpa la conexión en curso
- se desactivan todas las opciones del menú, excepto Quitter y Auteur
La secuencia de eventos es entonces la siguiente:
- El usuario presionó el botón Annuler: se detiene el hilo de conexión y se restablece el menú a su estado inicial. Se indica en el registro que el usuario ha cerrado la conexión.
- El servidor acepta la solicitud de conexión. A continuación, se inicia el diálogo con el servidor para cambiar la contraseña. Los intercambios de este diálogo se registran en la lista LstDialogue. Una vez finalizado el diálogo, se cierra la conexión con el servidor y el menú del formulario vuelve a su estado inicial.
- Mientras el diálogo esté activo, el botón Cancelar permanece visible para permitir que el usuario cierre la conexión si así lo desea.
- Si durante la comunicación se produce algún error, se cierra la conexión y la causa del error se muestra en la lista de seguimiento LstSuivi.
Esta opción cierra cualquier conexión activa con el servidor y cierra la aplicación.
Vuelven a vaciar, respectivamente, las listas LstSuivi y LstDialogue. Estas opciones están desactivadas cuando las listas correspondientes están vacías.
Este botón, ubicado en la parte inferior del formulario, solo aparece cuando el cliente se está conectando o ya está conectado al servidor. El botón Annuler le da al usuario la posibilidad de interrumpir la comunicación con el servidor.
La lista LstSuivi (5) realiza el seguimiento de la conexión. Indica los momentos clave de la conexión:
-
su apertura por parte del cliente
-
su cierre por parte del servidor o del cliente
-
todos los errores que puedan ocurrir mientras la conexión esté activa
La lista LstDialogue (6) realiza el seguimiento del diálogo que se establece entre el cliente y el servidor.
Este menú abre una ventana denominada «Copyright»:

Los errores de comunicación se reportan en la lista de seguimiento 6, mientras que los relacionados con el diálogo cliente/servidor se muestran en la lista de diálogo 7. Cuando ocurre un error de conexión, se cierra el diálogo cliente/servidor y el formulario vuelve a su estado inicial, listo para una nueva conexión.
8.5.4.5. TRAVAIL A FAIRE
Realizar el trabajo descrito anteriormente en forma de aplicación independiente y luego de applet.