8. Programación TCP-IP
8.1. Generalidades
8.1.1. Los protocolos de Internet
A continuación ofrecemos una introducción a los protocolos de comunicación de Internet, también conocidos como 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 tenga 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 ordenadores heterogéneos tiene su origen en las investigaciones llevadas a cabo 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 denominada ARPAnet, red que posteriormente pasó a denominarse INTERNET. Los protocolos TCP/IP definen formatos y reglas de transmisión y recepción independientes de la organización de las redes y del hardware utilizado.
La red diseñada por el DARPA y gestionada por los protocolos TCP/IP es una red de conmutación de paquetes. Este tipo de red transmite la información por la red en pequeños fragmentos denominados paquetes. Así, si un ordenador transmite un archivo de gran tamaño, 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 situadas en máquinas A y B diferentes quieren comunicarse: lo hacen a nivel de la capa Application. No necesitan conocer todos los detalles del funcionamiento de la red: cada aplicación entrega la información que desea transmitir a la capa inferior: la capa Présentation. Por lo tanto, la aplicación solo tiene que conocer las reglas de interfaz con la capa Présentation.
Una vez que la información se encuentra en la capa Présentation, pasa, 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 al equipo de destino. Allí se someterá al proceso inverso al que se le aplicó en el equipo remitente.
En cada capa, el proceso emisor encargado de enviar la información la envía a un proceso receptor en la otra máquina que pertenece a la misma capa que él. Lo hace siguiendo ciertas reglas que se denominan «protocolo de la capa». Por lo tanto, el esquema de comunicación final es el siguiente:

La función de las diferentes capas es la siguiente:
Garantiza la transmisión de bits a través de un soporte físico. En esta capa se encuentran equipos terminales de procesamiento de datos (E.T.T.D), como terminales u ordenadores, así como equipos de terminación de circuitos de datos (E.T.C.D), como moduladores/demoduladores, multiplexores y concentradores. Los puntos de interés a este nivel son: . la elección de la codificación de la información (analógica o digital) . la elección del modo de transmisión (síncrono o asíncrono). | |
Oculta las características físicas de la capa física. Detecta y corrige los errores de transmisión. | |
Gestiona la ruta que debe seguir la información enviada por la red. A esto se le denomina 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 ofrece esta capa puede ser la multiplexación: la capa de transporte podrá utilizar una misma conexión de red (de máquina a máquina) para transmitir información perteneciente a varias aplicaciones. | |
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 uniformizar la representación de los datos en las diferentes máquinas. Así, los datos procedentes de una máquina A serán «formateados» por la capa Présentation de la máquina A, según un formato estándar, antes de ser enviados a la red. Una vez que llegan a la capa Présentation del equipo receptor B, que los reconocerá gracias a su formato estándar, se reformatearán de otra manera para que la aplicación del equipo B los reconozca. | |
En este nivel se encuentran las aplicaciones que suelen estar más cercanas al 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 materializado. El conjunto de protocolos TCP/IP se acerca a él de la siguiente forma:

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

- Red de difusión: un equipo emisor transmite información por el cable indicando la dirección del equipo destinatario. Todos los equipos conectados reciben entonces esta información, pero solo el destinatario la conserva.
- El método de acceso es el siguiente: el transmisor que desea emitir escucha el cable y detecta si hay o no una onda portadora, cuya presencia significaría que hay una transmisión en curso. Se trata de la técnica CSMA (Carrier Sense Multiple Access). En ausencia de portadora, un transmisor puede decidir transmitir a su vez. Puede haber varios que tomen esta decisión. Las señales emitidas se mezclan: se dice que se produce una colisión. El transmisor detecta esta situación: al mismo tiempo que emite por el cable, escucha lo que realmente circula por él. Si detecta que la información que transita por el cable no es la que él ha emitido, deduce que se ha producido una colisión y dejará de emitir. Los demás transmisores que estaban emitiendo harán lo mismo. Cada uno reanudará su transmisión tras un tiempo aleatorio que depende de cada transmisor. Esta técnica se denomina CD (detección de colisiones). El método de acceso se denomina, por 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á inscrita en la tarjeta que la conecta al cable. A esta dirección se la denomina «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 un equipo y el de otro equipo. Se trata, por tanto, de 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 de direcciones inversa) | establece la correspondencia entre la dirección física del equipo y la dirección de Internet del equipo |
Capas de transporte y sesión
En esta capa se encuentran los siguientes protocolos:
TCP (Protocolo de control de transmisión) | Garantiza una entrega fiable de información entre dos clientes |
UDP (Protocolo de datagramas de usuario) | Garantiza una entrega no fiable 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 a un equipo A conectarse a un equipo 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 equipo en la dirección de Internet de dicho equipo | |
creado por Sun MicroSystems, especifica una representación estándar de los datos, independiente de los equipos | |
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 un equipo «vea» el sistema de archivos de otro equipo. Se basa en el protocolo RPC anterior |
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 al equipo destinatario, donde volverá a atravesar las mismas capas, esta vez en sentido inverso, hasta llegar a la aplicación destinataria de la información enviada. El siguiente esquema muestra el recorrido de la información:

Veamos un ejemplo: la aplicación FTP, definida en la capa Application y que permite la transferencia de archivos entre equipos.
- La aplicación envía una secuencia de bytes que debe transmitirse a la capa transport.
- La capa transport divide esta secuencia de bytes en segments y TCP, y añade al principio de cada segmento su número correspondiente. Los segmentos se pasan a la capa de red, regulada por el protocolo IP.
- La capa IP crea un paquete que encapsula el segmento TCP recibido. En la cabecera de este paquete, coloca las direcciones de Internet de los equipos de origen y destino. También determina la dirección física del equipo destinatario. Todo ello se pasa a la capa de enlace de datos y enlace físico, es decir, a la tarjeta de red que conecta el equipo a la red física.
- Allí, el paquete IP se encapsula a su vez en una trama física y se envía a su destinatario a través del cable.
- En el equipo de destino, la capa de enlace de datos y enlace físico realiza el proceso inverso: desencapsula el paquete IP de la trama física y lo pasa a la capa IP.
- La capa IP comprueba que el paquete sea correcto: calcula una suma a partir de los bits recibidos (checksum), suma que debe coincidir con la que figura en la cabecera del paquete. Si no es así, el paquete se rechaza.
- Si el paquete se considera correcto, la capa IP desencapsula el segmento TCP que contiene y lo pasa a la capa superior, la transport.
- La capa transport —la capa TCP en nuestro ejemplo— examina el número del segmento para restablecer el orden correcto de los segmentos.
- También calcula una suma de comprobación para el segmento TCP. Si se considera correcta, la capa TCP envía un acuse de recibo a la máquina de origen; de lo contrario, se rechaza el segmento TCP.
- A la capa TCP solo le queda transmitir la parte de datos del segmento a la aplicación destinataria de los mismos en la capa superior.
8.1.5. Los problemas de direccionamiento en Internet
Un noeud de una red puede ser un ordenador, una impresora inteligente, un servidor de archivos o, de hecho, cualquier dispositivo capaz de comunicarse mediante los protocolos TCP/IP. Cada nodo tiene una dirección física cuyo formato depende del tipo de red. En una red Ethernet, la dirección física se codifica en 6 bytes. Una dirección de una red X25 es un número de 14 dígitos.
La dirección de Internet de un nodo es una dirección lógica: es independiente del hardware y de la red utilizada. Se trata de una dirección de 4 bytes que identifica tanto una red local como un nodo de dicha red. La dirección de Internet suele representarse en forma de 4 números, que corresponden a los valores de los 4 bytes, separados por un punto. Así, la dirección del ordenador Lagaffe de la Facultad de Ciencias de Angers es 193.49.144.1 y la del ordenador Liny, 193.49.144.9. De ello se deduce que la dirección de Internet de la red local es 193.49.144.0. Esta red puede tener hasta 254 nodos.
Dado que las direcciones de Internet o direcciones IP son independientes de la red, un equipo de la red A puede comunicarse con un equipo de la red B sin tener en cuenta el tipo de red en la que se encuentra: basta con que conozca su dirección IP. El protocolo IP de cada red se encarga de realizar la conversión entre la dirección IP y la dirección física, en ambos sentidos.
Las direcciones IP deben ser todas diferentes. Hay organismos oficiales encargados de distribuirlas. De hecho, estos organismos asignan una dirección a las redes locales; por ejemplo, 193.49.144.0 para la red de la Facultad de Ciencias de Angers. El administrador de esta red puede, a continuación, asignar las direcciones IP 193.49.144.1 a 193.49.144.254 como considere oportuno. Esta dirección suele figurar 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 dicha red
En función del 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 un equipo de esa red
Más concretamente, la forma de una dirección IP de clase A es la siguiente:

La dirección de red ocupa 7 bits y la dirección del nodo, 24 bits. Por lo tanto, pueden existir 127 redes de clase A, cada una de las cuales puede contener 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 un equipo de esa red
Más concretamente, la forma de una dirección IP de clase B es la siguiente:

Tanto la dirección de red como la del nodo ocupan 2 bytes (14 bits exactamente). Por lo tanto, pueden existir 2¹⁴ redes de clase B, cada una de las cuales puede contener hasta 2¹⁶ nodos.
Clase C
En esta clase, la dirección IP: I1.I2.I3.I4 tiene el formato R1.R2.R3.N1, donde
R1.R2.R3 es la dirección de la red
N1 es la dirección de un equipo de esa red
Más concretamente, la forma de una dirección IP de clase C es la siguiente:

La dirección de red ocupa 3 bytes (menos 3 bits) y la dirección del nodo, 1 byte. Por lo tanto, pueden existir 2²¹ redes de clase C con hasta 256 nodos.
Dado que la dirección del ordenador 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 consiguiente, ningún nodo de una red puede tener la dirección cero.
. Cuando en una dirección IP la dirección del nodo solo contiene unos, se trata de una dirección de difusión: esta dirección designa a todos los nodos de la red.
. En una red de clase C, que teóricamente 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 entre dirección de Internet y dirección física
Hemos visto que, al enviar información de un equipo a otro, esta se encapsula en paquetes al atravesar la capa IP. Estos paquetes tienen la siguiente forma:

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

En la trama final figuran las direcciones físicas de los equipos de origen y destino. ¿Cómo se obtienen?
El equipo remitente, que conoce la dirección IP del equipo con el que desea comunicarse, obtiene la dirección física de este último utilizando un protocolo específico denominado ARP (Address Resolution Protocol).
- Envía un paquete de un tipo especial denominado paquete ARP que contiene la dirección IP del equipo cuya dirección física se busca. También se ha encargado de incluir en él su propia dirección IP, así como su dirección física.
- Este paquete se envía a todos los nodos de la red.
- Estos reconocen la naturaleza especial del paquete. El nodo que reconoce su dirección IP en el paquete responde enviando al remitente del paquete su dirección física. ¿Cómo puede hacerlo? Ha encontrado en el paquete las direcciones IP y la dirección física del remitente.
- Así pues, el remitente recibe la dirección física que buscaba. La almacena en memoria para poder utilizarla posteriormente si hay que enviar otros paquetes al mismo destinatario.
La dirección IP de un equipo suele estar registrada en uno de sus archivos, por lo que puede consultarlo para conocerla. Esta dirección se puede cambiar: basta con editar el archivo. La dirección física, por su parte, está registrada en una memoria de la tarjeta de red y no se puede modificar.
Cuando un administrador desea organizar su red de otra manera, puede verse obligado a cambiar las direcciones IP de todos los nodos y, por lo tanto, a editar los distintos archivos de configuración de 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 ninguna dirección IP a los equipos: para ello, se introduce un código especial en el archivo en el que el equipo debería encontrar su dirección IP. Al descubrir que no tiene una dirección IP, la máquina la solicita mediante un protocolo denominado RARP (Protocolo de resolución inversa de direcciones). A continuación, envía a la red un paquete especial denominado paquete RARP, análogo al paquete ARP anterior, en el que incluye su dirección física. Este paquete se envía a todos los nodos, que reconocen entonces un paquete RARP. Uno de ellos, denominado servidor RARP, dispone de un archivo que recoge 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, normalmente, 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 denominada capa IP de Internet
El protocolo IP (Protocolo de Internet) define la forma que deben adoptar los paquetes y la manera en que deben gestionarse durante su envío o recepción. Este tipo concreto 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 los equipos de origen y destino. De este modo, el equipo destinatario sabe quién le envía un mensaje.
A diferencia de una trama de red, cuya longitud viene determinada por las características físicas de la red por la que transita, la longitud del datagrama IP la fija el software y, por lo tanto, será la misma en diferentes redes físicas. Hemos visto que, al descender de la capa de red a la capa física, el datagrama IP se encapsulaba en una trama física. Hemos puesto como ejemplo la trama física de una red Ethernet:

Las tramas físicas circulan de nodo en nodo hacia su destino, que puede no encontrarse en la misma red física que el equipo 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 en el que se produce este problema descompone entonces el paquete IP en fragments siguiendo unas reglas precisas, y cada uno de ellos se envía a continuación 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 utilizado para dirigir los paquetes IP hacia su destino. Existen dos métodos: el enrutamiento directo y el enrutamiento indirecto.
Enrutamiento directo
El enrutamiento directo se refiere al envío de un paquete IP directamente del remitente al destinatario dentro de la misma red:
- El equipo remitente de un datagrama IP dispone de 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 dispone de dicha dirección.
- Envía el paquete a través de la red a dicha 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 los equipos de origen y destino son diferentes. El equipo de origen detecta esta circunstancia. A continuación, envía el paquete a un nodo especial denominado enrutador (router), nodo que conecta una red local con otras redes y cuya dirección, IP, encuentra en sus tablas; dicha dirección se obtuvo inicialmente bien de un archivo, bien de una memoria permanente, o bien a través de la información que circula por la red.
Un enrutador está conectado a dos redes y posee una dirección IP dentro de ambas redes.

En nuestro ejemplo anterior:
- La red n.º 1 tiene la dirección de Internet 193.49.144.0 y la red n.º 2, la dirección 193.49.145.0.
- Dentro de la red n.º 1, el router tiene la dirección 193.49.144.6 y, dentro de la red n.º 2, la dirección 193.49.145.3.
La función del router es convertir el paquete IP que recibe —y que está contenido en una trama física típica de la red n.º 1— en una trama física que pueda circular por la red n.º 2. Si la dirección IP del destinatario del paquete se encuentra en la red n.º 2, el router le enviará el paquete directamente; de lo contrario, lo enviará a otro router, que conecta la red n.º 2 con la red n.º 3, y así sucesivamente.
8.1.6.2. Mensajes de error y de control
También en la capa de red, es decir, al mismo nivel que el protocolo IP, existe el protocolo ICMP (Internet Control Message Protocol). Sirve para enviar mensajes sobre el funcionamiento interno de la red: nodos averiados, atascos en un router, etc. Los mensajes ICMP se encapsulan en paquetes IP y se envían a la red. Las capas IP de los distintos nodos toman las medidas adecuadas en función de los mensajes ICMP que reciben. De este modo, una aplicación en sí misma nunca 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 fiable de datos entre dos puntos, es decir, no se garantiza que un paquete llegue correctamente a su destino. La aplicación, si lo desea, puede gestionar esto por sí misma, por ejemplo, esperando un acuse de recibo tras el envío de un mensaje antes de enviar el siguiente.
Por el momento, a nivel de red, hemos hablado de direcciones IP de máquinas. Sin embargo, en una misma máquina pueden coexistir al mismo tiempo diferentes procesos, todos los cuales pueden comunicarse entre sí. Por lo tanto, al enviar un mensaje, hay que indicar no solo la dirección IP del equipo destinatario, sino también el «nombre» del proceso destinatario. Este nombre es, en realidad, un número, denominado número de puerto. Algunos números están reservados para aplicaciones estándar: el puerto 69 para la aplicación tftp (trivial file transfer protocol), por ejemplo.
Los paquetes gestionados por el protocolo UDP también se denominan datagramas. Tienen la siguiente forma:

Estos datagramas se encapsularán en paquetes IP y, a continuación, en tramas físicas.
8.1.7.2. El protocolo TCP: Protocolo de control de transferencia
Para garantizar la seguridad de las comunicaciones, el protocolo UDP resulta insuficiente: el desarrollador de aplicaciones debe diseñar por sí mismo un protocolo que le permita detectar si los paquetes se enrutan correctamente.
El protocolo TCP (Protocolo de control de transferencia) evita estos problemas. Sus características son las siguientes:
- El proceso que desea enviar datos establece primero una conexión con el proceso destinatario de la información que va a enviar. Esta conexión se establece entre un puerto del equipo emisor y un puerto del equipo receptor. 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 se enviaron, algo que no estaba garantizado en el protocolo UDP, ya que los paquetes podían seguir rutas diferentes.
- La información enviada tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta información no se envía necesariamente de inmediato: el protocolo TCP espera a tener suficiente para enviarla. Se almacena en una estructura denominada segmento TCP. Una vez completado, este segmento se transmitirá a la capa IP, donde se encapsulará en un paquete IP.
- Cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP destinatario comprueba que recibe los segmentos en 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 este modo, este último puede saber que un segmento ha llegado a su destino, algo que no era posible con el protocolo UDP.
- Si, transcurrido un tiempo determinado, 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 circular en ambos sentidos. De este modo, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen sigue enviando información. Esto permite, por ejemplo, que el protocolo TCP de origen envíe varios segmentos sin esperar el acuse de recibo. Si, transcurrido un tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento concreto con el 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 a un usuario de un equipo A de la red conectarse a un equipo B (denominado a menudo «equipo host»). TELNET emula en el equipo A un terminal denominado «universal». De este modo, el usuario actúa como si dispusiera de un terminal conectado al equipo 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 un equipo remoto, por ejemplo, mediante FTP, debe conocer la dirección de Internet de dicho equipo. Por ejemplo, para realizar una conexión FTP con el equipo Lagaffe de la Universidad de Angers, habría que ejecutar FTP de la siguiente manera: FTP 193.49.144.1
Esto obliga a disponer de un directorio que establezca la correspondencia entre máquina y 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 resultaría más cómodo 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, sería bastante engorroso. De hecho, el control de los nombres se ha distribuido en dominios. Cada dominio está gestionado por un organismo, por lo general muy ágil, que tiene total libertad a la hora de elegir los nombres de los equipos. Así, los equipos de Francia pertenecen al dominio «fr», gestionado por el Inria de París. Para seguir simplificando las cosas, se distribuye aún más el control: se crean dominios dentro del dominio «fr». Así, la Universidad de Angers pertenece al dominio «univ-Angers». El servicio que gestiona este dominio tiene total libertad para nombrar los equipos de la red de la Universidad de Angers. Por el momento, este dominio no se ha subdividido. Pero en una gran universidad con muchos equipos en red, podría subdividirse.
El equipo DPX2/320 de la Universidad de Angers ha recibido el nombre de Lagaffe, mientras que una máquina PC y otra 486DX50 han recibido el nombre de liny. ¿Cómo se pueden referenciar estos equipos desde el exterior? Especificando la jerarquía de los dominios a los que pertenecen. Así, el nombre completo del equipo 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 al equipo «Lagaffe» mediante
Por último, dentro del dominio univ-Angers, se podrá referenciar simplemente como
Por lo tanto, una aplicación puede hacer referencia a un equipo por su nombre. Al fin y al cabo, hay que obtener la dirección de Internet de ese equipo. ¿Cómo se hace esto? Supongamos que desde un equipo A queremos comunicarnos con un equipo B.
- Si la máquina B pertenece al mismo dominio que la máquina A, probablemente encontraremos su dirección IP en un archivo de la máquina A.
- De lo contrario, el equipo A encontrará, en otro archivo o en el mismo que el anterior, una lista de varios servidores de nombres con sus direcciones IP. Un servidor de nombres se encarga de establecer la correspondencia entre el nombre de un equipo y su dirección IP. El equipo A enviará una consulta especial al primer servidor de nombres de su lista, denominada consulta DNS, que incluirá, por tanto, el nombre del equipo buscado. Si el servidor consultado tiene ese nombre en sus registros, enviará al equipo A la dirección IP correspondiente. De lo contrario, el servidor también encontrará en sus archivos una lista de servidores de nombres a los que puede consultar. Y así lo hará. De este modo, se consultará a varios servidores de nombres, no de forma desordenada, sino de manera que se minimicen las consultas. Si finalmente se encuentra la máquina, la respuesta 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 en red
Definido también por Sun, este protocolo permite que un equipo «vea» el sistema de archivos de otro equipo. Se basa en el protocolo anterior RPC.
8.1.9. Conclusión
En esta introducción hemos presentado algunas líneas generales de los protocolos de Internet. Para profundizar en este tema, se puede consultar 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 de Internet se identifica mediante una dirección o un nombre únicos. En Java, estos dos elementos se gestionan mediante la clase InetAddress, de la que a continuación se enumeran algunos métodos:
devuelve los 4 bytes de la dirección IP de la instancia InetAddress actual | |
proporciona la dirección IP de la instancia InetAddress actual | |
proporciona el nombre de Internet de la instancia actual InetAddress | |
proporciona la identidad (dirección IP) y el 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 el equipo 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 al equipo local es utilizar el nombre «localhost».
8.2.2.2. Identificar cualquier equipo
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 un equipo A quiere comunicarse con una aplicación AppB de un equipo B de Internet, debe conocer varios datos:
- la dirección IP o el nombre del equipo B
- el número de puerto con el que trabaja la aplicación AppB. De hecho, el equipo 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 del equipo B acceden a la red a través de «ventanas», también denominadas puertos de comunicación. Esta información figura en el paquete recibido por el equipo B para que pueda entregarse a la aplicación correcta.
- Los protocolos de comunicación que comprende la máquina B. En nuestro estudio, utilizaremos únicamente los protocolos TCP-IP.
- El protocolo de diálogo aceptado por la aplicación AppB. De hecho, las máquinas A y B van a «comunicarse» entre sí. Lo que se digan quedará encapsulado en los protocolos TCP-IP. No obstante, cuando, al final de la cadena, la aplicación AppB reciba la información enviada por la aplicación AppA, deberá ser capaz de interpretarla. Esto es análogo a la situación en la que dos personas, A y B, se comunican por teléfono: su diálogo se transmite a través del teléfono. El habla se codifica en forma de señales en el teléfono A, se transmite por las líneas telefónicas y llega al teléfono B para ser descodificada. La persona B oye entonces las palabras. Aquí es donde entra en juego el concepto de protocolo de diálogo: si A habla francés y B no entiende ese idioma, A y B no podrán mantener un diálogo útil.
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. Características del protocolo TCP
Aquí solo estudiaremos las comunicaciones de red que utilizan el protocolo de transporte TCP. Recordemos las características de este protocolo:
- El proceso que desea transmitir establece en primer lugar una conexión con el proceso destinatario de la información que va a transmitir. Esta conexión se establece entre un puerto del equipo emisor y un puerto del equipo receptor. 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 se han enviado
- La información enviada tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta información no se envía necesariamente de inmediato: el protocolo TCP espera a tener suficiente cantidad para enviarla. Se almacena en una estructura denominada segmento TCP. Una vez completado, este segmento se transmitirá a la capa IP, donde se encapsulará en un paquete IP.
- Cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP destinatario comprueba que recibe los segmentos en 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 este modo, este último puede saber que un segmento ha llegado a su destino.
- Si, transcurrido un tiempo determinado, 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 circular en ambos sentidos. De este modo, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen sigue enviando información. Esto permite, por ejemplo, que el protocolo TCP de origen envíe varios segmentos sin esperar un acuse de recibo. Si, transcurrido un tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento concreto con el 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, especificando 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 ajustarse al protocolo de diálogo que comprende el servicio SB1. De este modo, se establece un diálogo de solicitud-respuesta entre la máquina A, denominada máquina cliente, y la máquina B, denominada máquina servidor. Uno de los dos interlocutores cerrará la conexión.
8.3.4. Arquitectura de un cliente
La arquitectura de un programa de red que solicita los servicios de una aplicación de servidor será la siguiente:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
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 gestiona de forma diferente la solicitud de conexión inicial de un cliente y sus solicitudes posteriores para obtener un servicio. El programa no presta el servicio por sí mismo. Si lo hiciera, mientras durara el servicio dejaría de estar a la escucha de 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 del servidor denominado «puerto de servicio». De este modo, se puede atender a varios clientes al mismo tiempo.
Una tarea de servicio tendrá la siguiente estructura:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
8.3.6. La clase Socket
8.3.6.1. Définition
La herramienta básica que utilizan los programas que se comunican a través de Internet es el socket. Esta palabra inglesa significa «enchufe». Aquí se amplía su significado para referirse a «enchufe de red». Para que una aplicación pueda enviar y recibir información a través de Internet, necesita un «enchufe 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 interlocutor remoto | |||
devuelve un flujo de salida que permite enviar datos al interlocutor 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 un servidor
Hemos visto que, para que un equipo A establezca una conexión con un servicio de un equipo B, necesita dos datos:
- la dirección IP o el nombre del equipo 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
- …
Tenemos que gestionar esta excepción:
Socket sClient=null;
try{
sClient=new Socket(host,port);
} catch(Exception e){
// la conexión ha fallado: se gestiona 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 establece correctamente, hemos visto que, por su parte, el servidor hace que otra tarea se encargue del servicio, trabajando en un puerto denominado «puerto de servicio». Este número de puerto se puede conocer mediante el método:
8.3.6.3. Enviar información a través de 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 por este flujo se recibirá en el puerto de servicio del servidor. Muchas aplicaciones presentan un diálogo en forma de líneas de texto que terminan con un salto de línea. Por ello, el método println resulta muy práctico en estos casos. A continuación, se transforma el flujo de salida OutputStream en el flujo PrintWriter, que utiliza el método println. La escritura puede generar una excepción.
8.3.6.4. Leer información procedente 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 procede del puerto de servicio del servidor. Para las aplicaciones con un diálogo en forma de líneas de texto terminadas con un salto de línea, es recomendable utilizar el método readLine. Para ello, se transforma el flujo de entrada InputStream en el flujo BufferedReader, que dispone del 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 disponemos de los elementos necesarios para describir la arquitectura básica de un cliente de Internet:
Socket sClient=null;
try{
// se establece la conexión con el servicio que opera en el puerto P del equipo 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 gestiona la excepción
….
}
No hemos intentado gestionar los diferentes tipos de excepción generados 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 única excepción.
8.3.7. La clase ServerSocket
8.3.7.1. Définition
Esta clase está destinada a 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 cliente 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á vinculado el socket | |
pone al servidor en espera de una conexión (operación bloqueante). Cuando llega una conexión de cliente, devuelve un socket desde el que 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 cola del servicio (50 por defecto), que almacena las solicitudes de conexión de los clientes a las que el servidor aún no ha respondido. Cuando la cola está llena, las solicitudes de conexión que llegan se rechazan. 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: se trata del socket de servicio, a través del cual se prestará el servicio, normalmente 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 remite al lector a las secciones anteriores en las que 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 este modo, se tendrá acceso a la dirección IP y al nombre del cliente.
8.3.7.6. Cerrar el servicio
Esto se hace 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{
// apertura del 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 completado: se cierra el servicio
sEcoute.close();
} catch (Exception e){
// se gestiona la excepción
…
}
La clase Service es una 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 y 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 gestiona 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 indicado como parámetro. Se limita a devolver al cliente la solicitud que este le ha enviado, 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 ha enviado
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 presta otra tarea
new traiteClientEcho(service).start();
}// fin del bucle «while»
}// fin de main
// visualización de los 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 al cliente la identificación de la conexión
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 bucle «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
// identificación
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 añadido 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 mediante el comando
A continuación, muestra en la ventana de control el siguiente mensaje:
Para probar este servidor, se utiliza el programa telnet, disponible tanto para Unix como para Windows. Telnet es un cliente TCP universal compatible con todos los servidores que aceptan líneas de texto terminadas con un carácter de fin de línea en su diálogo. Este es el caso de nuestro servidor de eco. Se inicia un primer cliente telnet en Windows (2000 en este ejemplo) escribiendo 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 teclean. Para que aparezca este eco, hay que introducir el comando:
Para establecer una conexión con el servidor, indicándole el puerto del servicio de eco (187) y la dirección del equipo en el 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]
En este caso, tahe y localhost hacen referencia al mismo equipo. En la ventana del cliente telnet, se pueden escribir líneas de texto. El servidor las devuelve tal cual:
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 del servicio y no el puerto de escucha. Habría que comprobar 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 se pueden 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í pues, se puede observar que el servidor de eco puede atender a varios clientes a la vez. Los clientes telnet pueden cerrarse cerrando 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, hemos utilizado un cliente telnet para probar el servicio de eco. Ahora vamos a escribir nuestro propio cliente:
// llamada: clientEcho puerto de 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 un equipo 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 establece la conexión con el 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);
}
// creación del 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 justo 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 tecleada
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);
// ¿Ha terminado?
if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
}
// Ya ha terminado
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);
}
// identificar
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. En este caso, se han gestionado las diferentes excepciones posibles, una por una, lo que ralentiza el programa. 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 por Client son las enviadas por el cliente, y las que comienzan por Serveur son las que el servidor ha devuelto.
8.4.3. Un cliente TCP genérico
Muchos de los servicios creados en los inicios de Internet funcionan según el modelo de 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 ejecutará 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 introducidos mediante el teclado y enviarlos al servidor
- un hilo encargado de leer las respuestas del servidor y mostrarlas en pantalla
¿Por qué dos subprocesos si en la aplicación anterior no se había planteado esta necesidad? En aquella, el protocolo de diálogo era conocido: el cliente enviaba una sola línea y el servidor respondía con una sola línea. Cada servicio tiene su protocolo específico y también se dan las siguientes situaciones:
- el cliente debe enviar varias líneas de texto antes de recibir una respuesta
- la respuesta de un servidor puede contener varias líneas de texto
Por lo tanto, el bucle que consiste en enviar una única línea al servidor y recibir una única línea enviada por el servidor no siempre resulta adecuado. Por lo tanto, crearemos dos bucles independientes:
- un bucle para leer los comandos introducidos con el teclado que se enviarán al servidor. El usuario indicará el final 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 introduzca en el teclado 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]
Comentemos estos intercambios entre cliente y 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 indica al servidor SMTP que se va a enviar el mensaje. Tal y como se indica en la respuesta del servidor, este consiste en una secuencia de líneas que termina con una línea que contiene únicamente el carácter punto. Un mensaje puede tener encabezados separados del cuerpo del mensaje por una línea en blanco. En nuestro ejemplo, hemos incluido un asunto con la palabra clave Subject:
- una vez enviado el mensaje, se puede indicar al servidor que se ha terminado con el comando quit. El servidor cierra entonces la conexión de red. El hilo de lectura puede detectar este evento y detenerse.
- A continuación, el usuario pulsa «fin» en el teclado para detener también el hilo de lectura de los comandos introducidos mediante el teclado.
Si comprobamos el correo recibido, tenemos lo siguiente (Outlook):

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 introduce el nombre de usuario en el servidor que aloja nuestros correos electrónicos
- introducir password, donde se indica 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 finalizar la sesión.
Veamos ahora el protocolo de comunicación entre un cliente y un servidor web, que suele funcionar 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 siguiendo el siguiente esquema:
El servidor web solo responde tras recibir la línea vacía. En el ejemplo solo hemos utilizado un comando:
que solicita al servidor el URL /index.html e indica que trabaja con el protocolo HTTP versión 1.0. La versión más reciente de este protocolo es la 1.1. El ejemplo muestra que el servidor respondió enviando el contenido del archivo index.html y, a continuación, 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 denomina encabezados HTTP (Protocolo de transferencia HyperText). No vamos a detallar aquí estas cabeceras, pero recordaremos que nuestro cliente genérico permite acceder a ellas, lo que puede resultar útil para comprenderlas. La primera línea, por ejemplo:
indica que el servidor web contactado entiende el protocolo HTTP/1.1 y que ha encontrado correctamente el archivo solicitado (200 OK), siendo 200 un código de respuesta HTTP. Las líneas
indican al cliente que va a 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í pues, tenemos aquí un cliente TCP muy práctico. Sin duda, hace menos cosas que el programa telnet que hemos utilizado anteriormente, pero ha sido 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 introducidos mediante el teclado
// estas se enviarán al servidor
// crea un hilo para leer las respuestas del servidor
// estas se mostrarán en pantalla
// Todo finaliza con el comando «fin» introducido mediante 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 indica 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 establece la conexión con el 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 subprocesos 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 del error
System.err.println(msg);
// parada con error
System.exit(exitCode);
}//error
}//clase
class ClientSend extends Thread {
// clase encargada de leer los comandos introducidos mediante 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 en el 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 introducción y envío de comandos
System.out.println("Commandes : ");
while(true){
// lectura del comando introducido mediante el 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 centramos en un servidor
- que muestra en pantalla los comandos enviados por sus clientes
- y les envía como respuesta las líneas de texto que el usuario escribe en el teclado. Por lo tanto, es este último quien actúa como servidor.
El programa se ejecuta con: java serveurTCPgenerique portEcoute, donde portEcoute es el puerto al que deben conectarse los clientes. El servicio al cliente lo prestarán 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 teclea 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 pulse Ctrl-C en el teclado. Veamos algunos ejemplos.
El servidor se ejecuta 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 por <-- son las enviadas desde el servidor al cliente; las demás, las enviadas desde el 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 por <-- son las enviadas desde el cliente al servidor. Las líneas N: son las enviadas desde el servidor al cliente n.º N. El servidor anterior sigue activo, mientras que el cliente 1 ha finalizado. Se inicia un segundo cliente para el mismo servidor:
Dos> 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 queda 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, a continuación, solicitará la página /exemple.html:

Veamos ahora 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
<--
De este modo, descubrimos los encabezados HTTP enviados por el navegador. Esto nos permite ir desentrañando poco a poco el protocolo HTTP. En un ejemplo anterior, habíamos creado un cliente web que solo enviaba el comando GET. Eso había sido suficiente. Aquí vemos que el navegador envía otra información al servidor. Su finalidad es indicar al servidor qué tipo de cliente tiene delante. También vemos que los encabezados HTTP terminan con una línea en blanco.
Elaboremos una respuesta para nuestro cliente. El usuario que teclea es aquí el verdadero servidor y puede elaborar una respuesta a mano. Recordemos la respuesta dada por un servidor web en un ejemplo anterior:
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
Intentemos dar una respuesta similar:
...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 : <head><title>Serveur generique</title></head>
2 : <body>
2 : <center>
2 : <h2>Reponse du serveur generique</h2>
2 : </center>
2 : </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]
Las líneas que comienzan por 2: se envían desde el servidor al cliente n.º 2. El comando fin cierra la conexión del servidor con el cliente. En nuestra respuesta nos hemos limitado a los siguientes encabezados HTTP:
HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
No indicamos el tamaño del archivo que vamos a enviar (Content-Length), sino que nos limitamos a indicar que vamos a cerrar la conexión (Connection: close) tras enviarlo. Esto es suficiente para el navegador. Al ver que la conexión se ha cerrado, sabrá que la respuesta del servidor ha finalizado y mostrará la página HTML que se le ha enviado. Esta última es la siguiente:
2 : <html>
2 : <head><title>Serveur generique</title></head>
2 : <body>
2 : <center>
2 : <h2>Reponse du serveur generique</h2>
2 : </center>
2 : </body>
2 : </html>
A continuación, el usuario cierra la conexión con el cliente introduciendo el comando fin. El navegador sabe entonces que la respuesta del servidor ha finalizado y puede mostrarla:

Si, en el ejemplo anterior, introducimos View/Source para ver lo que ha recibido el navegador, obtenemos:

es decir, exactamente lo que se ha enviado 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 pantalla
// crea un hilo para leer los comandos introducidos con el teclado
// estas se enviarán como respuesta al cliente
// todo ello termina con el comando «fin» introducido con 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 >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 independientes
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 bucle «while»
}catch(Exception ex){
// se notifica 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 introducidas mediante 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.º de cliente
// constructor
public ServeurSend(Socket client, int numClient){
// se anota 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 en el 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 introducción y envío de comandos
while(true){
// identificación del cliente
System.out.print("--> " + numClient + " : ");
// lectura de la respuesta introducida mediante el 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
}//while
}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+ "]");
}//ejecución
}//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 anota 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 hemos visto algunos de los encabezados HTTP que enviaba un navegador:
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
Vamos a escribir un cliente web al que se le pasaría como parámetro un URL y que mostraría en pantalla el 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:
- La primera cabecera indica qué página queremos
- el segundo, a qué servidor nos dirigimos
- el tercero, que queremos que el servidor cierre la conexión tras habernos respondido.
Si en el ejemplo anterior sustituimos 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, a continuación, 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 (por lo tanto, de URL). La nueva URL viene indicada 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 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 anota el URI solicitado
String URLString=args[0];
String commande=args[1].toUpperCase();
// comprobació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 establece la conexión con el 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 completado
client.close();
} catch(Exception e){
// se gestiona 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);
// parada con error
System.exit(exitCode);
}//error
}//clase
La única novedad de este programa es el uso de la clase URL. El programa recibe un URL (Uniform Resource Locator) o un URI (Uniform Resource Identifier) con el formato http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La clase URL nos permite descomponer la cadena de URL en sus diferentes elementos. Se crea 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 comprobar la validez del parámetro recibido. Una vez creado el objeto URL, se puede acceder a sus distintos elementos. Así, si el objeto url del código anterior se ha creado 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 ninguna solicitud
uri.getProtocol()=http
8.4.6. Cliente web que gestiona las redirecciones
El cliente web anterior no gestiona una posible redirección de la solicitud URL que ha realizado. El siguiente cliente sí la gestiona.
- Lee la primera línea de los encabezados HTTP enviados por el servidor para comprobar si contiene la cadena «302 Object moved», que indica una redirección
- Lee los encabezados siguientes. Si hay redirección, busca la línea «Location: url», que proporciona la nueva URL de la página solicitada, y anota esta URL.
- Muestra el resto de la respuesta del servidor. Si hay redirección, se repiten los pasos del 1 al 3 con la nueva URL. El programa no admite más de una redirección. Este límite viene definido por una constante que se puede modificar.
He aquí 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 su contenido en 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 URL de redirección
Pattern location=Pattern.compile("^Location: (.+?)$");
// gestión de errores
try{
// puede haber varias cadenas URL que consultar si hay redireccionamientos
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 establece la conexión con el 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 las cabeceras 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
// cabezas HTTP siguientes hasta encontrar la línea vacía que indica el final de las cabezas
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 anota que se ha encontrado
locationFound=true;
}//si
}//si
// siguiente encabezado
}//mientras
// líneas siguientes 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();
// ¿Hemos terminado?
if ( ! locationFound || nbRedirs>nbRedirsMax)
break;
// hay que realizar una redirección: se crea 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 gestiona 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);
// finalización con error
System.exit(exitCode);
}//error
}//clase
8.4.7. Servidor de cálculo de impuestos
Retomamos el ejercicio IMPOTS, que ya se ha tratado de diversas formas. Recordemos la última versión:
Se ha creado una clase base para impuestos. Sus atributos son tres matrices 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 «fabricante» 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 las tres matrices de datos necesarias para calcular el impuesto
- un constructor sin parámetros que solo pueden utilizar las clases hijas
A partir de esta clase se ha derivado la clase «impotsJDBC», que permite rellenar los tres tableros «limites», «coeffR» y «coeffN» a partir del contenido de una base de datos:
public class impotsJDBC extends impots{
// se añade 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 escrito una aplicación gráfica. La aplicación utilizaba un objeto de la clase impotsJDBC. Tanto la aplicación como dicho objeto se encontraban en el mismo equipo. Nos proponemos colocar el programa de prueba y el objeto impotsJDBC en equipos diferentes. Tendremos una aplicación cliente-servidor en la que el objeto remoto impotsJDBC actuará como 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 de 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 introducidos mediante el teclado
// el servidor se gestionará 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 de 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 introducidos con el teclado. Por ello, se crea un hilo para leer dichos comandos. Habrá dos posibles: start para iniciar el servicio y stop para detenerlo definitivamente. El método admin que gestiona estos comandos es el siguiente:
public void admin() throws IOException{
// lee los comandos de administración del servidor introducidos mediante 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");
// continuamos
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 subprocesos de ejecución
System.exit(0);
}//si
else {
// error
System.out.println("Commande incorrecte. Utilisez (start,stop)");
}//si
}//while
}//admin
Si el comando introducido en el teclado es start, se inicia un hilo de escucha de las solicitudes de los clientes. Si el comando introducido es stop, se detienen todos los hilos. El hilo de escucha ejecuta el método ecoute:
public void ecoute(){
// hilo de escucha de las 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 presta otra tarea
new traiteClientImpots(liaisonClient,this).start();
// se vuelve a escuchar las solicitudes
}// fin del bucle «while»
}catch(Exception ex){
// se notifica 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 se gestionan mediante el método run del hilo traiteCientImpots, al constructor del cual se 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 gestiona las solicitudes de los clientes. Se trata de líneas de texto que pueden adoptar dos formas:
- cálculo de matrimonio (s/n) nbEnfants salaireAnnuel
- fincalculs
La forma 1 permite calcular un impuesto, mientras que 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){
// Desglose de 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;
}//if
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
}//while
}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 comprueba la validez de la solicitud y, en su caso, se calcula el impuesto y se envía 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 necesitan 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 necesitan 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 a ejecutarlo por primera vez:
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 escucha de las solicitudes de los clientes. Es el comando «start», introducido mediante el teclado, el que inicia esta escucha. El comando «stop» detiene el servidor. Ahora utilicemos un cliente. Utilizaremos el cliente genérico creado anteriormente. El servidor está en marcha:
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 ejecuta en otra ventana de DOS:
Se puede ver que el cliente ha recibido correctamente el mensaje de bienvenida del servidor. Enviamos otros comandos:
x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<-- syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Volvemos a la ventana del servidor para detenerlo:
dos>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 «cliente TCP genérico». Una vez comprendida esta aplicación, nos daremos cuenta de 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 del equipo 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 cliente/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 —situado 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 copyright |
8.5.1.2. FONCTIONNEMENT DE El APPLICATION
Cuando se carga la hoja principal de la aplicación, se llevan a cabo 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 ese momento. Al hacer clic en esta opción, se llevan a cabo las siguientes operaciones:
- se comprueba 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 que el usuario pueda interrumpir la conexión en curso
- Se desactivan todas las opciones del menú, excepto Quitter y Auteur
La conexión puede finalizar de varias formas:
- El usuario ha pulsado 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 ha cerrado la conexión.
- La conexión finaliza 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.
Solo se puede acceder a esta opción si se cumplen las siguientes condiciones:
-
se ha establecido la conexión con el servidor
-
hay un mensaje que enviar
Si se cumplen estas condiciones, se envía al servidor el texto que figura en el campo TxtSend (3), terminado por la secuencia RCLF si se ha marcado la opción RCLF; en caso contrario, por la secuencia LF. Cualquier error que se produzca durante el envío se indica en la lista de seguimiento.
Vuelven a poner a cero, respectivamente, las listas LstSuivi y LstDialogue. Estas opciones quedan desactivadas cuando las listas correspondientes están vacías.
Este botón, situado en la parte inferior del formulario, solo aparece cuando el cliente está intentando conectarse al servidor. Es posible que la conexión no se establezca porque el servidor no responde o responde incorrectamente. El botón Annuler ofrece 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 producirse 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 indican 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 se produce 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 dispone de varios potentes servidores de cálculo accesibles a través de Internet. Cualquier equipo 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 elegido lo utiliza y devuelve los resultados al cliente en forma de archivo de texto.
Las ventajas de este tipo de 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 informáticos están optimizados: solo se necesitan unas pocas máquinas potentes. De este modo, una pequeña organización sin recursos informáticos puede utilizar este servicio a cambio de una contribución económica calculada en función del tiempo de cálculo utilizado.
A pesar de la potencia de los equipos, un cálculo puede durar en ocasiones varias horas: en ese caso, el servidor no está disponible para otros clientes. Entonces, el cliente se enfrenta al problema de encontrar un servidor de cálculo disponible. Para ello, se utiliza un «gestor de recursos de cálculo», denominado en lo sucesivo servidor GRC. Este servicio se aloja en una única máquina y funciona 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 dispone de la lista completa de servidores de cálculo, le responde enviándole el nombre de un servidor que se encuentra inactivo en ese momento. A continuación, 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 gráfica será la siguiente:

La interfaz presenta dos listas de servidores:
- a la izquierda, la lista de servidores inactivos y, por 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 sobre 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 rellena con la lista de nombres de los servidores de cálculo gestionados 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 rellenar 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 del equipo
- desactiva el menú «Iniciar»
- habilita el menú «Detener»
Esta opción interrumpe el servicio:
- se vacía la lista de servidores ocupados
- la lista de servidores libres se rellena con el contenido del archivo Servidores
- se habilita el menú «Iniciar»
- se desactiva el menú «Cerrar»
La aplicación se cierra.
La comunicación 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 algún servidor de cálculo disponible para él.
El servidor GRC toma entonces el primer servidor que encuentra en su lista de servidores libres y devuelve su nombre al cliente en el formato:
Además, añade el servidor asignado al cliente a la lista de servidores ocupados con el formato:
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 comprueba 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, está 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, tras responder al cliente, el servidor GRC cierra la conexión con él 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 comprueba primero que el cliente sea efectivamente uno de los que estaba atendiendo. Para ello, comprueba que la dirección IP del cliente figure 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 pasa a ser:

Tras 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 caso, queremos crear 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 «conversación» con el servicio smtp de 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 guion (-).
Un número igual o superior a 500 indica un mensaje de error.
// fin de los 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 comprueba la validez de la dirección del remitente: la acepta tal y como se le ha facilitado
// 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 del equipo en el que funciona el servidor smtp, este comprueba que existe; de lo contrario, no realiza ninguna comprobación. Si se ha realizado la comprobación y se ha detectado un error, este se indicará con un número >= 500.
Se pueden enviar tantos comandos «rcpt to» como se desee: esto permite enviar un mensaje a varias personas.
// fin de los 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».
A continuación, el mensaje se envía al destinatario indicado por el comando rcpt.
// fin de los comentarios
// respuesta del servidor SMTP
// texto del mensaje escrito con 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 crear un programa con la siguiente interfaz gráfica:

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 con el 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 | Vacía 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 sobre derechos de autor |
8.5.3.4. FONCTIONNEMENT DE APPLICATION
Este menú hace que aparezca la siguiente ventana:

Es necesario rellenar ambos campos para que el botón OK esté activo. Ambos datos deben almacenarse en variables globales para que estén disponibles para otros módulos.
Solo se puede acceder a esta opción si se cumplen las siguientes condiciones:
- se haya realizado la configuración
- hay un mensaje que 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 la comunicación entre el cliente y el servidor
- se establece una conexión en el puerto 25 del servidor especificado en la configuración
- A continuación, 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 figuran en los campos 1, 3 y 4
- En las líneas enviadas tras el comando data, aparecerán los siguientes textos:
- una línea Subject:: texto del asunto de la comprobación 2
- una línea Cc:: direcciones de la comprobación 3
- una línea Bcc:: direcciones de la comprobación 4
- el texto del mensaje de la comprobación 5
- el punto final
Este botón, situado 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 establezca porque el servidor smtp no responde o responde incorrectamente. El botón Annuler ofrece 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 aparece 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. Por otra parte, «Ocultar seguimiento» hace invisible la opción RazSuivi que se encuentra debajo. |
Borrar seguimiento | Vacía la lista de seguimiento 6 |
Ocultar cuadro de diálogo | Oculta la lista de diálogo 7, el texto que aparece encima 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 (quizá el botón Annuler) y se reduce el tamaño de la ventana. |
Borrar cuadro de diálogo | Vacía la lista de diálogo 7 |
Este menú abre una ventana denominada «Copyright»:

Los errores de conexión se indican 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 avisa al usuario mediante un cuadro de error y se muestra la lista con la causa del error, si estaba oculta anteriormente. 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 programa 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 los dos datos introducidos mediante la opción Opciones/Configurar. Las líneas Suivi y Dialogue indican el estado de las listas de Seguimiento y 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 actúa como si existiera:
Si el archivo sendmail.ini existe pero está incompleto (faltan líneas), la línea que falta se sustituye por la línea correspondiente de las anteriores. Así, si falta la línea Suivi=..., se actúa 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: da igual.
En la opción Options/Configurer, se muestran los valores SmtpServer y ReplyAddress actualmente en uso. El usuario puede modificarlos si lo desea.
8.5.3.6. TRAVAIL A FAIRE
Realice el trabajo descrito anteriormente. Se recomienda dejar la gestión del archivo de configuración para el 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 funciona en el puerto 106. Este servicio permite cambiar la contraseña en un equipo 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 por números de tres cifras: 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 esta se acepta; de lo contrario, devuelve un error
- El servidor responde confirmando que se ha aceptado la nueva contraseña; 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 interrumpirla. |
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 copyright |
8.5.4.4. Funcionamiento de la aplicación
Cuando se carga la hoja principal de la aplicación, se producen las siguientes acciones:
- la pestaña 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 rellenado los campos del 1 al 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 que el usuario pueda interrumpir la conexión en curso
- se desactivan todas las opciones del menú excepto Quitter y Auteur
A continuación, la secuencia de eventos es la siguiente:
- El usuario ha pulsado 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 que el usuario pueda cerrar la conexión si lo desea.
- Si durante la comunicación se produce algún error, se cierra la conexión y se muestra la causa del error en la lista de seguimiento LstSuivi.
Esta opción cierra cualquier conexión activa con el servidor y finaliza la aplicación.
Vuelven a poner a cero, respectivamente, las listas LstSuivi y LstDialogue. Estas opciones están desactivadas cuando las listas correspondientes están vacías.
Este botón, situado en la parte inferior del formulario, solo aparece cuando el cliente se está conectando o está conectado al servidor. El botón Annuler ofrece 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 producirse 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 indican en la lista de seguimiento 6, mientras que los relacionados con el diálogo cliente/servidor aparecen en la lista de diálogo 7. Cuando se produce 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
Llevar a cabo el trabajo descrito anteriormente en forma de aplicación independiente y, a continuación, de applet.