11. Programación en Internet
11.1. Généralités
11.1.1. Los protocolos de Internet
A continuación ofrecemos una introducción a los protocolos de comunicación de Internet, también denominados conjunto de protocolos TCP/IP (Protocolo de Control de Transmisión / Protocolo de Internet), por el nombre de los dos protocolos principales. Puede resultar útil que el lector tenga una comprensión general del funcionamiento de las redes y, en particular, de los protocolos TCP/IP antes de abordar la creación de aplicaciones distribuidas. El texto que sigue es una traducción parcial de un texto que se encuentra en el documento «Lan Workplace for Dos - Administrator's Guide» de NOVELL, un documento de principios de los años 90.
El concepto general de crear una red de 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
11.1.2. El modelo OSI
Los protocolos TCP/IP siguen aproximadamente el modelo de red abierta denominado OSI (Modelo de Referencia de Interconexión de Sistemas Abiertos), definido por la ISO (Organización Internacional de Normalización). Este modelo describe una red ideal en la que la comunicación entre máquinas puede representarse mediante un modelo de siete capas:
![]() |
Cada capa recibe servicios de la capa inferior y ofrece los suyos a la capa superior. Supongamos que dos aplicaciones 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 sometió 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 aspectos más destacados a este nivel son:
| |
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 de la máquina destinataria B, que los reconocerá gracias a su formato estándar, se reformatearán de otra manera para que la aplicación de la máquina 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. |
11.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). Si no hay portadora, un transmisor puede decidir transmitir a su vez. Puede haber varios que tomen esta decisión. Las señales emitidas se mezclan: se dice que 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 colisión). El método de acceso se denomina, por tanto, CSMA/CD.
- Un direccionamiento de 48 bits. Cada máquina tiene una dirección, denominada aquí dirección física, que está inscrita en la tarjeta que la conecta al cable. A esta dirección se la denomina dirección Ethernet de la máquina.
Capa de red
En esta capa encontramos los protocolos IP, ICMP, ARP y RARP.
Transmite paquetes entre dos nodos de la red | |
ICMP establece la comunicación entre el programa del protocolo IP de una máquina y el de otra máquina. Se trata, por tanto, de un protocolo de intercambio de mensajes dentro del propio protocolo IP. | |
establece la correspondencia entre la dirección de Internet de la máquina y la dirección física de la máquina | |
establece la correspondencia entre la dirección física del equipo y la dirección de Internet del equipo |
Capas de transporte/sesión
En esta capa se encuentran los siguientes protocolos:
Garantiza una entrega fiable de la información entre dos clientes | |
Garantiza una entrega no fiable de información entre dos clientes |
Capas de aplicación/presentación/sesión
Aquí se encuentran diversos protocolos:
Emulador de terminal que permite a una máquina A conectarse a una máquina B como terminal | |
permite la transferencia de archivos | |
permite la transferencia de archivos | |
permite el intercambio de mensajes entre usuarios de la red | |
convierte un nombre de 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 |
11.1.4. Funcionamiento de los protocolos de Internet
Las aplicaciones desarrolladas en el entorno TCP/IP suelen utilizar varios de los protocolos de este entorno. Un programa de aplicación se comunica con la capa más alta de los protocolos. Esta transmite la información a la capa inferior y así sucesivamente hasta llegar al soporte físico. Allí, la información se transfiere físicamente 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. Al principio 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.
11.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 ordenador de una red A puede comunicarse con un ordenador de una 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.
Todas las direcciones IP deben ser diferentes. En Francia, es el INRIA el que se encarga de asignar las direcciones IP. De hecho, este organismo asigna una dirección a su red local, por ejemplo, 193.49.144.0 para la red de la Facultad de Ciencias de Angers. A continuación, el administrador de esta red puede 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.
11.1.5.1. Las clases de direcciones IP
Una dirección IP es una secuencia de 4 bytes que a menudo se escribe como I1.I2.I3.I4 y que, en realidad, contiene dos direcciones:
- la dirección de la red
- la dirección de un nodo de 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 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 permitidas.
11.1.5.2. Los protocolos de conversión entre dirección de Internet y dirección física
Hemos visto que, al transmitir información de 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.
11.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.
11.1.6.1. El enrutamiento
El enrutamiento es el método utilizado para dirigir los paquetes IP a su destino. Existen dos métodos: el enrutamiento directo y el enrutamiento indirecto.
Enrutamiento directo
El enrutamiento directo consiste en el 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 situado 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, dirección obtenida inicialmente bien de un archivo, bien de una memoria permanente o incluso a través de la información que circula por la red.
Un enrutador está conectado a dos redes y tiene una dirección IP dentro de ambas redes.
![]() |
En nuestro ejemplo anterior:
. La red n.º 1 tiene la dirección de Internet 193.49.144.0 y la red n.º 2, la dirección 193.49.145.0.
. Dentro de la red n.º 1, el 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 conecte la red n.º 2 con la red n.º 3, y así sucesivamente.
11.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.
11.1.7. La capa de transporte: los protocolos UDP y TCP
11.1.7.1. El protocolo UDP: Protocolo de datagramas de usuario
El protocolo UDP permite un intercambio no 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.
11.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 el correcto enrutamiento de los paquetes. El protocolo TCP (Protocolo de control de transferencia) evita estos problemas. Sus características son las siguientes:
- El proceso que desea enviar datos establece en primer lugar 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 lleno, este segmento se transmitirá a la capa IP, donde se encapsulará en un paquete IP.
- Cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP destinatario comprueba que recibe los segmentos en 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 un acuse de recibo. Si, transcurrido un tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento concreto n.º n, reanudará la transmisión de los segmentos a partir de ese punto.
11.1.8. La capa de aplicaciones
Por encima de los protocolos UDP y TCP, existen diversos protocolos estándar:
TELNET
Este protocolo permite 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 Universidad de Angers ISERPA
Es evidente que sería más cómodo referirse a un equipo por su nombre en lugar de por su dirección IP. Esto plantea el problema de la unicidad del nombre: hay millones de equipos interconectados. Se podría imaginar que un organismo centralizado se encargara de asignar los nombres. Sin duda, sería un proceso 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á:
Lagaffe.univ-Angers.fr
Dentro de los dominios, se pueden utilizar nombres relativos. Así, dentro del dominio «fr» y fuera del dominio «univ-Angers», se podrá hacer referencia al equipo «Lagaffe» mediante
Lagaffe.univ-Angers
Por último, dentro del dominio univ-Angers, se podrá referenciar simplemente como
Lagaffe
Por lo tanto, una aplicación puede hacer referencia a 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 solicitud especial al primer servidor de nombres de su lista, denominada solicitud DNS, que incluye, 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 el equipo, la respuesta llegará hasta el equipo A.
XDR: (Representación de datos eXternal)
Creado por Sun MicroSystems, este protocolo especifica una representación estándar de los datos, independiente de los equipos.
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.
11.1.9. Conclusión
En esta introducción hemos presentado algunas líneas generales de los protocolos de Internet. Para profundizar en este tema, se puede leer el excelente libro de Douglas Comer:
Título TCP/IP: Arquitectura, protocolos, aplicaciones.
Autor Douglas COMER
Editorial InterEditions
11.2. Las clases .NET de la gestión de direcciones IP
Un equipo conectado a Internet se identifica de forma única mediante una dirección IP (Protocolo de Internet), que puede adoptar dos formas:
- IPv4: codificada en 32 bits y representada por una cadena con el formato «I1.I2.I3.I4», donde In es un número comprendido entre 1 y 254. Estas son las direcciones IP más habituales en la actualidad.
- IPv6: codificada en 128 bits y representada por una cadena con el formato «[I1.I2.I3.I4.I5.I6.I7.I8]», donde In es una cadena de 4 dígitos hexadecimales. En este documento no utilizaremos las direcciones IPv6.
Una máquina también puede definirse mediante un nombre igualmente único. Este nombre no es obligatorio, ya que las aplicaciones siempre utilizan, en última instancia, las direcciones IP de las máquinas. Su finalidad es facilitar la vida a los usuarios. Así, resulta más fácil, con un navegador, solicitar el http://www.ibm.com (URL) que el URL http://129.42.17.99, aunque ambos métodos son posibles.
Un equipo puede tener varias direcciones IP si está conectado físicamente a varias redes al mismo tiempo. En ese caso, tiene una dirección IP en cada red.
Una dirección IP puede representarse de dos formas en .NET:
- en forma de cadena de caracteres «I1.I2.I3.I4» o «[I1.I2.I3.I4.I5.I6.I7.I8]»
- en forma de un objeto de tipo IPAddress
La clase IPAddress
Entre los métodos M, propiedades P y constantes C de la clase IPAddress, se encuentran los siguientes:
P | familia de la dirección IP. El tipo AddressFamily es una enumeración. Los dos valores habituales son: AddressFamily.InterNetwork: para una dirección IPv4 AddressFamily.InterNetworkV6: para una dirección IPv6 | |
C | la dirección IP «0.0.0.0». Cuando un servicio está asociado a esta dirección, significa que acepta clientes en todas las direcciones IP del equipo en el que opera. | |
C | la dirección IP «127.0.0.1». Se denomina «dirección de bucle». Cuando un servicio está asociado a esta dirección, significa que solo acepta clientes que se encuentran en la misma máquina que él. | |
C | la dirección IP «255.255.255.255». Cuando un servicio está asociado a esta dirección, significa que no acepta a ningún cliente. | |
M | intenta convertir la dirección IP ipString de la forma «I1.I2.I3.I4» en un objeto de tipo dirección IPAddress. Devuelve true si la operación se ha realizado correctamente. | |
M | devuelve «true» si la dirección IP es «127.0.0.1» | |
M | convierte la dirección IP en «I1.I2.I3.I4» o «[I1.I2.I3.I4.I5.I6.I7.I8]» |
La asociación entre la dirección IP y nomMachine está garantizada por un servicio distribuido de Internet denominado DNS (Sistema de Nombres de Dominio). Los métodos estáticos de la clase Dns permiten establecer la asociación entre las direcciones IP <--> nomMachine:
devuelve una dirección IPHostEntry a partir de una dirección IP en forma de cadena o a partir de un nombre de máquina. Lanza una excepción si no se encuentra la máquina. | |
devuelve una dirección IPHostEntry a partir de una dirección IP del tipo IPAddress. Lanza una excepción si no se encuentra la máquina. | |
devuelve el nombre del equipo en el que se ejecuta el programa que ejecuta esta instrucción | |
devuelve las direcciones IP de la máquina identificada por su nombre o por una de sus direcciones IP. |
Una instancia IPHostEntry encapsula las direcciones IP, los alias y el nombre de una máquina. El tipo IPHostEntry es el siguiente:
P | tabla de direcciones IP de la máquina | |
P | los alias DNS de la máquina. Estos son los nombres que corresponden a las diferentes direcciones IP de la máquina. | |
P | el nombre de host principal del equipo |
Consideremos el siguiente programa, que muestra el nombre del equipo en el que se ejecuta y, a continuación, de forma interactiva, proporciona las correspondencias entre la dirección IP y el nombre del equipo:
using System;
using System.Net;
namespace Chap9 {
class Program {
static void Main(string[] args) {
// muestra el nombre del equipo local
// y, a continuación, proporciona información de forma interactiva sobre los equipos de la red
// identificadas por un nombre o una dirección IP
// ordenador local
Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());
// preguntas y respuestas interactivas
string machine;
IPHostEntry ipHostEntry;
while (true) {
// introducción del nombre o la dirección IP del equipo buscado
Console.Write("Machine recherchée (rien pour arrêter) : ");
machine = Console.ReadLine().Trim().ToLower();
// ¿He terminado?
if (machine == "") return;
// gestión de excepciones
try {
// búsqueda de máquina
ipHostEntry = Dns.GetHostEntry(machine);
// el nombre de la máquina
Console.WriteLine("Machine : " + ipHostEntry.HostName);
// las direcciones IP de la máquina
Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
Console.Write(", {0}" , ipHostEntry.AddressList[i]);
}
Console.WriteLine();
// los alias de la máquina
if (ipHostEntry.Aliases.Length != 0) {
Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
Console.Write(", {0}" , ipHostEntry.Aliases[i]);
}
Console.WriteLine();
}
} catch {
// la máquina no existe
Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
}
}
}
}
}
La ejecución ofrece los siguientes resultados:
11.3. Fundamentos de la programación web
11.3.1. Aspectos generales
Consideremos la comunicación entre dos máquinas remotas A y B:
![]() |
Cuando una aplicación AppA de un equipo A quiere comunicarse con una aplicación AppB de un equipo B en 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 funcionan en Internet. Cuando recibe información procedente de la red, debe saber a qué aplicación va destinada 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 será codificada en forma de señales por el teléfono A, transportada a través de las líneas telefónicas, llegará al teléfono B para ser descodificada allí. La persona B oirá entonces las palabras. Aquí es donde entra en juego el concepto de protocolo de diálogo: si A habla francés y B no entiende ese idioma, A y B no podrán 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. Por ejemplo, el diálogo con un servicio ftp no es el mismo que con un servicio pop: estos dos servicios no aceptan los mismos comandos. Tienen un protocolo de diálogo diferente.
11.3.2. Características del protocolo TCP
Aquí solo estudiaremos las comunicaciones de red que utilizan el protocolo de transporte TCP. Recordemos aquí sus características:
- El proceso que desea transmitir establece, en primer lugar, una conexión con el proceso destinatario de la información que va a transmitir. Esta conexión se establece entre un puerto 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 enviaron
- 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 lleno, este segmento se transmitirá a la capa IP, donde se encapsulará en un paquete IP.
- Cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP destinatario comprueba que recibe los segmentos en 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, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento concreto n.º n, reanudará la transmisión de los segmentos a partir de ese punto.
11.3.3. La relación cliente-servidor
A menudo, la comunicación en Internet es asimétrica: el equipo A inicia una conexión para solicitar un servicio al equipo B, especificando que desea establecer una conexión con el servicio SB1 del equipo B. Este último 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.
11.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
fermer la connexion
11.3.5. Arquitectura de un servidor
La arquitectura de un programa que ofrece servicios será la siguiente:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
El programa servidor 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
11.4. Descubre los protocolos de comunicación de Internet:
11.4.1. Introducción
Cuando un cliente se conecta a un servidor, se establece un diálogo entre ambos. La naturaleza de dicho diálogo constituye lo que se conoce como protocolo de comunicación del servidor. Entre los protocolos más habituales de Internet se encuentran los siguientes:
- HTTP: HyperText Transfer Protocol —el protocolo de comunicación con un servidor web (servidor HTTP)—
- SMTP: Simple Mail Transfer Protocol —el protocolo de comunicación con un servidor de envío de correo electrónico (servidor SMTP)
- POP: Protocolo de oficina postal (POP) —el protocolo de comunicación con un servidor de almacenamiento de correo electrónico (servidor POP). Su función es recuperar los correos electrónicos recibidos, no enviarlos.
- FTP: Protocolo de transferencia de archivos (FTP): el protocolo de comunicación con un servidor de almacenamiento de archivos (servidor FTP).
Todos estos protocolos tienen la particularidad de ser protocolos de líneas de texto: el cliente y el servidor intercambian líneas de texto. Si disponemos de un cliente capaz de:
- establecer una conexión con un servidor TCP
- mostrar en la consola las líneas de texto que el servidor le envía
- enviar al servidor las líneas de texto que un usuario introduciría
entonces podremos comunicarnos con un servidor TCP que utilice un protocolo de líneas de texto, siempre que conozcamos las reglas de dicho protocolo.
El programa telnet que se encuentra en los equipos Unix o Windows es un cliente de este tipo. En los equipos Windows también existe una herramienta llamada putty y es la que vamos a utilizar aquí. putty se puede descargar en la dirección [http://www.putty.org/]. Se trata de un ejecutable (.exe) que se puede utilizar directamente. Lo configuraremos de la siguiente manera:
![]() |
- [1]: la dirección IP del servidor TCP al que queremos conectarnos o su nombre
- [2]: el puerto de escucha del servidor TCP
- [3]: utilizar el modo Raw, que designa una conexión TCP sin procesar.
- [4]: seleccionar el modo Never para evitar que se cierre la ventana del cliente putty si el servidor cierra la conexión.
- [6,7]: número de columnas y filas de la consola
- [5]: número máximo de líneas que se mantienen en memoria. Un servidor HTTP puede enviar muchas líneas. Es necesario poder desplazarse por ellas.
![]() |
- [8,9]: para conservar los parámetros anteriores, asigna un nombre a la configuración [8] y guárdala [9].
- [11,12]: para recuperar una configuración guardada, selecciónela ([11]) y cárguela ([12]).
Con esta herramienta así configurada, veamos algunos protocolos TCP.
11.4.2. El protocolo HTTP (Protocolo de transferencia HyperText)
Conectemos nuestro cliente [1] al servidor web de la máquina istia.univ-angers.fr [2], puerto 80 [3]:
![]() |
En la consola de putty, creamos el siguiente diálogo HTTP:
- las líneas 1-4 son la solicitud del cliente, introducida mediante el teclado
- las líneas 5-19 son la respuesta del servidor
- línea 1: sintaxis GET UrlDocument HTTP/1.1: solicitamos la URL «/», c.a.d. La raíz del sitio web es [istia.univ-angers.fr].
- Línea 2: sintaxis Host: máquina:puerto
- línea 3: sintaxis Connection: [mode de la connexion]. El modo [close] indica al servidor que cierre la conexión una vez que haya enviado su respuesta. El modo [Keep-Alive] solicita que se mantenga abierta.
- línea 4: línea en blanco. Las líneas 1-3 se denominan encabezados HTTP. Puede haber otros además de los que se muestran aquí. El final de los encabezados HTTP se indica con una línea en blanco.
- Líneas 5-13: los encabezados HTTP de la respuesta del servidor; también terminan aquí con una línea en blanco.
- líneas 14-19: el documento enviado por el servidor, en este caso un documento HTML
- línea 5: sintaxis HTTP/1.1 código msg; el código 200 indica que se ha encontrado el documento solicitado.
- línea 6: la fecha y la hora del servidor
- línea 7: identificación del software que presta el servicio web; en este caso, un servidor Apache en un sistema Linux/Debian
- línea 8: el documento ha sido generado dinámicamente por PHP
- línea 9: cookie de identificación del cliente; si este desea que se le reconozca en su próxima conexión, deberá reenviar esta cookie en sus encabezados HTTP.
- línea 10: indica que, tras servir el documento solicitado, el servidor cerrará la conexión
- línea 11: el documento se transmitirá por fragmentos (chunked) y no de una sola vez.
- línea 12: tipo de documento: en este caso, un documento HTML
- línea 13: la línea vacía que indica el final de los encabezados HTTP del servidor
- línea 14: número hexadecimal que indica el número de caracteres del primer bloque del documento. Cuando este número sea 0 (línea 19), el cliente sabrá que ha recibido todo el documento.
- líneas 15-18: parte del documento recibido.
La conexión se ha cerrado y el cliente putty está inactivo. Volvamos a conectarnos a [1] y borremos de la pantalla las visualizaciones anteriores de [2,3]:
![]() |
El cuadro de diálogo que aparece esta vez es el siguiente:
- línea 1: se ha solicitado un documento inexistente
- línea 5: el servidor HTTP ha respondido con el código 404, lo que significa que no se ha encontrado el documento solicitado.
Si solicitamos este documento con el navegador Firefox:

Si solicitamos ver el código fuente [Affichage/Code source]:
Obtenemos las líneas 13-22 recibidas por nuestro cliente putty. El interés de este último es mostrarnos, además, los encabezados HTTP de la respuesta. También es posible obtenerlos con Firefox.
11.4.3. El protocolo SMTP (Protocolo simple de transferencia de correo)
![]() |
Los servidores SMTP suelen funcionar en el puerto 25 [2]. Nos conectamos al servidor [1]. En este caso, normalmente hay que elegir un servidor
que pertenezca al mismo dominio IP que el equipo, ya que, en la mayoría de los casos, los servidores SMTP están configurados para aceptar únicamente las solicitudes de equipos que pertenezcan al mismo dominio que ellos. Por otra parte, también es bastante frecuente que los cortafuegos o antivirus de los ordenadores personales estén configurados para no aceptar conexiones al puerto 25 procedentes de un ordenador externo. En ese caso, puede ser necesario reconfigurar [3] dicho cortafuegos o antivirus.
El cuadro de diálogo SMTP en la ventana del cliente putty es el siguiente:
A continuación, (D) es una solicitud del cliente y (R) una respuesta del servidor.
- línea 1: (R) mensaje de bienvenida del servidor SMTP
- línea 2: (D) comando HELO para decir «hola»
- línea 3: (R) respuesta del servidor
- línea 4: (D) dirección del remitente, por ejemplo, correo de: someone@gmail.com
- línea 5: (R) respuesta del servidor
- línea 6: (D) dirección del destinatario, por ejemplo, «rcpt to: someoneelse@gmail.com»
- línea 7: (R) respuesta del servidor
- línea 8: (D) indica el inicio del mensaje
- línea 9: (R) respuesta del servidor
- líneas 10-12: (D) el mensaje que se va a enviar, terminado por una línea que contiene únicamente un punto.
- línea 13: (R) respuesta del servidor
- línea 14: (D) el cliente indica que ha terminado
- línea 15: (R) respuesta del servidor, que a continuación cierra la conexión
11.4.4. El protocolo POP (Post Office Protocol)
![]() |
Los servidores POP suelen funcionar en el puerto 110 [2]. Nos conectamos al servidor [1]. El diálogo POP en la ventana del cliente putty es el siguiente:
- línea 1: (R) mensaje de bienvenida del servidor POP
- línea 2: (D) el cliente proporciona su identificador POP, c.a.d. El nombre de usuario con el que lee su correo
- línea 3: (R) la respuesta del servidor
- línea 4: (D) la contraseña del cliente
- línea 5: (R) la respuesta del servidor
- línea 6: (D) el cliente solicita la lista de sus mensajes
- líneas 7-12: (R) la lista de mensajes del buzón del cliente, en formato [N° du message taille en octets du message]
- línea 13: (D) se solicita el mensaje n.º 64
- líneas 14-25: (R) el mensaje n.º 64, con las líneas 15-22 correspondientes a los encabezados del mensaje y las líneas 23-24 al cuerpo del mensaje.
- línea 26: (D) el cliente indica que ha terminado
- línea 27: (R) respuesta del servidor, que a continuación cerrará la conexión.
11.4.5. El protocolo FTP (Protocolo de transferencia de archivos)
El protocolo FTP es más complejo que los presentados anteriormente. Para conocer las líneas de texto intercambiadas entre el cliente y el servidor, se puede utilizar una herramienta como FileZilla [http://www.filezilla.fr/].
![]() |
FileZilla es un cliente FTP que ofrece una interfaz de Windows para realizar transferencias de archivos. Las acciones del usuario en la interfaz de Windows se traducen en comandos FTP que se registran en [1]. Es una buena forma de descubrir los comandos del protocolo FTP.
11.5. Las clases .NET de la programación web
11.5.1. Elegir la clase adecuada
El marco de trabajo .NET ofrece diferentes clases para trabajar con la red:
![]() |
- La clase Socket es la que opera más cerca de la red. Permite gestionar con precisión la conexión de red. El término socket hace referencia a una toma de corriente. El término se ha ampliado para designar una toma de red de software. En una comunicación TCP-IP entre dos máquinas A y B, son dos sockets los que se comunican entre sí. Una aplicación puede trabajar directamente con los sockets. Este es el caso de la aplicación A mencionada anteriormente. Un socket puede ser un socket client o serveur.
- Si se desea trabajar a un nivel menos detallado que el de la clase Socket, se pueden utilizar las clases
- TcpClient para crear un cliente TCP
- TcpListener para crear un servidor TCP
Estas dos clases ofrecen a la aplicación que las utiliza una visión más sencilla de la comunicación de red, ya que se encargan de gestionar por ella los detalles técnicos de la gestión de sockets.
- .NET ofrece clases específicas para determinados protocolos:
- la clase SmtpClient para gestionar el protocolo SMTP de comunicación con un servidor SMTP de envío de correos electrónicos
- la clase WebClient para gestionar los protocolos HTTP o FTP de comunicación con un servidor web.
Cabe destacar que la clase Socket es suficiente por sí sola para gestionar cualquier comunicación TCP/IP, pero se intentará, ante todo, utilizar las clases de nivel superior para facilitar la programación de la aplicación TCP/IP.
11.5.2. La clase TcpClient
La clase TcpClient es la clase adecuada en la mayoría de los casos para crear el cliente de un servicio TCP. Entre sus constructores C, métodos M y propiedades P, cuenta con los siguientes:
C | crea una conexión TCP con el servicio que opera en el puerto indicado (port) de la máquina indicada (hostname). Por ejemplo, new TcpClient("istia.univ-angers.fr",80) para conectarse al puerto 80 de la máquina istia.univ-angers.fr | |
P | el socket utilizado por el cliente para comunicarse con el servidor. | |
M | obtiene un flujo de lectura y escritura hacia el servidor. Este flujo es el que permite las comunicaciones entre el cliente y el servidor. | |
M | cierra la conexión. El socket y el flujo NetworkStream también se cierran | |
P | verdadero si se ha establecido la conexión |
La clase NetworkStream representa el flujo de red entre el cliente y el servidor. Se deriva de la clase Stream. Muchas aplicaciones cliente-servidor intercambian líneas de texto que terminan con los caracteres de fin de línea «\r\n». Por ello, resulta interesante utilizar los objetos StreamReader y StreamWriter para leer y escribir estas líneas en el flujo de red. Así, si una máquina M1 ha establecido una conexión con una máquina M2 mediante un objeto TcpClient client1 y ambas intercambian líneas de texto, podrá crear sus flujos de lectura y escritura de la siguiente manera:
StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=new StreamWriter(client1.GetStream());
out1.AutoFlush=true;
La instrucción
significa que el flujo de escritura de client1 no pasará por un búfer intermedio, sino que irá directamente a la red. Este punto es importante. Por lo general, cuando client1 envía una línea de texto a su interlocutor, espera una respuesta. Esta nunca llegará si la línea se ha almacenado en realidad en el búfer de la máquina M1 y nunca se ha enviado a la máquina M2.
Para enviar una línea de texto a la máquina M2, se escribirá:
Para leer la respuesta de M2, se escribirá:
Ahora disponemos de los elementos necesarios para diseñar la arquitectura básica de un cliente de Internet que utilice el siguiente protocolo de comunicación básico con el servidor:
- el cliente envía una solicitud contenida en una sola línea
- el servidor envía una respuesta contenida en una sola línea
using System;
using System.IO;
using System.Net.Sockets;
namespace ... {
class ... {
static void Main(string[] args) {
...
try {
// se establece conexión con el servicio
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin búfer
writer.AutoFlush = true;
// bucle de solicitud-respuesta
while (true) {
// la solicitud procede del teclado
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// ¿Terminado?
if (demande.Trim().ToLower() == "bye")
break;
// se envía la solicitud al servidor
writer.WriteLine(demande);
// se lee la respuesta del servidor
réponse = reader.ReadLine();
// se procesa la respuesta
...
}
}
}
}
}
} catch (Exception e) {
// error
...
}
}
}
}
- línea 11: creación de la conexión del cliente; la cláusula using garantiza que los recursos asociados a ella se liberarán al salir de using.
- línea 12: apertura del flujo de red en una cláusula using
- línea 13: creación y ejecución del flujo de lectura en una cláusula using
- línea 14: creación y ejecución del flujo de escritura en una cláusula using
- línea 16: no almacenar en búfer el flujo de salida
- líneas 18-31: el ciclo de solicitud del cliente / respuesta del servidor
- línea 26: el cliente envía su solicitud al servidor
- línea 28: el cliente espera la respuesta del servidor. Se trata de una operación bloqueante, como la lectura desde el teclado. La espera finaliza con la llegada de una cadena que termina en «\n» o bien con el fin del flujo. Esto ocurrirá si el servidor cierra la conexión que ha establecido con el cliente.
11.5.3. La clase TcpListener
La clase TcpListener es la clase adecuada en la mayoría de los casos para crear un servicio TCP. Entre sus constructores C, métodos M y propiedades P, cuenta con los siguientes:
C | crea un servicio TCP que esperará (escuchará) las solicitudes de los clientes en un puerto pasado como parámetro (port), denominado puerto de escucha. Si el equipo está conectado a varias redes IP, el servicio escuchará en cada una de ellas. | |
C | Lo mismo, pero la escucha solo se realiza en la dirección IP especificada. | |
M | inicia la escucha de las solicitudes de los clientes | |
M | Acepta la solicitud de un cliente. A continuación, abre una nueva conexión con él, denominada «conexión de servicio». El puerto utilizado por el servidor es aleatorio y lo elige el sistema. Se denomina «puerto de servicio». AcceptTcpClient devuelve como resultado el objeto TcpClient asociado, por parte del servidor, a la conexión de servicio. | |
M | deja de escuchar las solicitudes de los clientes | |
P | El socket de escucha del servidor |
La estructura básica de un servidor TCP que se comunicaría con sus clientes según el siguiente protocolo:
- el cliente envía una solicitud contenida en una sola línea
- el servidor envía una respuesta contenida en una sola línea
podría tener este aspecto:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
namespace ... {
public class ... {
...
// se crea el servicio de escucha
TcpListener ecoute = null;
try {
// se crea el servicio; este escuchará en todas las interfaces de red del equipo
ecoute = new TcpListener(IPAddress.Any, port);
// se inicia
ecoute.Start();
// bucle del servicio
TcpClient tcpClient = null;
// bucle infinito: se detendrá con Ctrl-C
while (true) {
// espera a un cliente
tcpClient = ecoute.AcceptTcpClient();
// el servicio lo presta otra tarea
ThreadPool.QueueUserWorkItem(Service, tcpClient);
// siguiente cliente
}
} catch (Exception ex) {
// se notifica el error
...
} finally {
// fin del servicio
ecoute.Stop();
}
}
// -------------------------------------------------------
// presta servicio a un cliente
public static void Service(Object infos) {
// se recupera el cliente al que hay que atender
Client client = infos as Client;
// explotación del enlace TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin almacenamiento en búfer
writer.AutoFlush = true;
// bucle de lectura de solicitud/escritura de respuesta
bool fini=false;
while (! fini) != null) {
// espera de solicitud del cliente: operación bloqueante
demande=reader.ReadLine();
// preparación de la respuesta
réponse=...;
// envío de la respuesta al cliente
writer.WriteLine(réponse);
// siguiente solicitud
}
}
}
}
}
} catch (Exception e) {
// error
...
} finally {
// fin del cliente
...
}
}
}
}
- línea 14: se crea el servicio de escucha para un puerto determinado y una dirección IP determinada. Hay que recordar aquí que un equipo tiene al menos dos direcciones IP: la dirección «127.0.0.1», que es su dirección de bucle cerrado, y la dirección «I1.I2.I3.I4» que tiene en la red a la que está conectado. Puede tener otras direcciones IP si está conectada a varias redes IP. IPAddress.Any hace referencia a todas las direcciones IP de un equipo.
- línea 16: se inicia el servicio de escucha. Se había creado anteriormente, pero aún no estaba a la escucha. Estar a la escucha significa esperar las solicitudes de los clientes.
- líneas 20-26: el bucle de espera de solicitud del cliente / servicio al cliente se repite para cada nuevo cliente
- línea 22: se acepta la solicitud de un cliente. El método AcceptTcpClient devuelve una instancia TcpClient denominada de servicio:
- el cliente ha realizado su solicitud con su propia instancia TcpClient del lado del cliente, a la que llamaremos TcpClientDemande
- el servidor acepta esta solicitud con AcceptTcpClient. Este método crea una instancia TcpClient del lado del servidor, a la que llamaremos TcpClientService. De este modo, se establece una conexión TCP abierta con las instancias TcpClientDemande <--> TcpClientService en ambos extremos.
- La comunicación cliente/servidor que tiene lugar a continuación se realiza a través de esta conexión. El servicio de escucha ya no interviene.
- Línea 24: para que el servidor pueda atender a varios clientes a la vez, el servicio se gestiona mediante subprocesos, un subproceso por cliente.
- línea 32: se cierra el servicio de escucha
- línea 38: el método ejecutado por el hilo de servicio para un cliente. Recibe como parámetro la instancia TcpClient, ya conectada al cliente al que se debe atender.
- Líneas 38-71: encontramos un código similar al del cliente TCP básico estudiado anteriormente.
11.6. Ejemplos de clientes/servidores TCP
11.6.1. Un servidor de eco
Nos proponemos escribir un servidor de eco que se iniciará desde una ventana DOS mediante el comando:
ServeurEcho puerto
El servidor opera en el puerto pasado como parámetro. Se limita a devolver al cliente la solicitud que este le ha enviado. El programa es el siguiente:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
// llamada: serveurEcho puerto
// servidor de eco
// devuelve al cliente la línea que este le ha enviado
namespace Chap9 {
public class ServeurEcho {
public const string syntaxe = "Syntaxe : [serveurEcho] port";
// programa principal
public static void Main(string[] args) {
// ¿hay algún argumento?
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// este argumento debe ser un número entero mayor que 0
int port = 0;
if (!int.TryParse(args[0], out port) || port<=0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
return;
}
// se crea el servicio de escucha
TcpListener ecoute = null;
int numClient = 0; // siguiente número de cliente
try {
// Se crea el servicio; este escuchará en todas las interfaces de red del equipo
ecoute = new TcpListener(IPAddress.Any, port);
// se inicia
ecoute.Start();
// seguimiento
Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
// hilos de servicio
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// bucle del servicio
TcpClient tcpClient = null;
// bucle infinito: se detendrá con Ctrl-C
while (true) {
// esperando a un cliente
tcpClient = ecoute.AcceptTcpClient();
// el servicio lo presta otra tarea
ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// siguiente cliente
numClient++;
}
} catch (Exception ex) {
// se notifica el error
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
} finally {
// fin del servicio
ecoute.Stop();
}
}
// -------------------------------------------------------
// presta servicio a un cliente del servidor de eco
public static void Service(Object infos) {
// se recupera el cliente al que hay que atender
Client client = infos as Client;
// presta el servicio al cliente
Console.WriteLine("Début de service au client {0}", client.NumClient);
// gestión de la conexión TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin búfer
writer.AutoFlush = true;
// bucle de lectura de solicitud/escritura de respuesta
string demande = null;
while ((demande = reader.ReadLine()) != null) {
// seguimiento de la consola
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
// eco de la solicitud al cliente
writer.WriteLine("[{0}]", demande);
// seguimiento de consola
Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
// el servicio se detiene cuando el cliente envía «bye»
if (demande.Trim().ToLower() == "bye")
break;
}
}
}
}
}
} catch (Exception e) {
// error
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
// fin del cliente
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
}
// información del cliente
internal class Client {
public TcpClient CanalTcp { get; set; } // conexión con el cliente
public int NumClient { get; set; } // n.º de cliente
}
}
La estructura del servidor de eco se ajusta a la arquitectura básica de los servidores TCP expuesta anteriormente. Solo comentaremos la parte relativa al «servicio al cliente»:
- línea 79: se lee la solicitud del cliente
- línea 83: se devuelve al cliente entre corchetes
- línea 79: el servicio se detiene cuando el cliente cierra la conexión
En una ventana de DOS, utilizamos el ejecutable del proyecto C#:
A continuación, iniciamos dos clientes putty que conectamos al puerto 100 del equipo localhost:
![]() |
La salida de la consola del servidor de eco queda así:
El cliente 1 y, a continuación, el cliente 0 envían los siguientes textos:
![]() |
- [1]: el cliente n.º 1
- [2]: el cliente n.º 0
- [3]: la consola del servidor de eco
![]() |
- en [4]: el cliente 1 se desconecta con el comando bye.
- en [5]: el servidor lo detecta
El servidor se puede detener con Ctrl-C. El cliente n.º 0 lo detecta entonces con [6].
11.6.2. Un cliente para el servidor de eco
Ahora vamos a escribir un cliente para el servidor anterior. Se llamará de la siguiente manera:
ClientEcho nomServeur puerto
Se conecta al equipo nomServeur en el puerto port y, a continuación, envía al servidor líneas de texto que este le devuelve como eco.
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
// se conecta a un servidor de eco
// cada línea tecleada se recibe como eco
class ClientEcho {
static void Main(string[] args) {
// sintaxis
const string syntaxe = "pg machine port";
// número de argumentos
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// se anota el nombre del servidor
string serveur = args[0];
// el puerto debe ser un número entero mayor que 0
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// se puede trabajar
string demande = null; // solicitud del cliente
string réponse = null; // respuesta del servidor
try {
// se establece la conexión con el servicio
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin almacenamiento en búfer
writer.AutoFlush = true;
// bucle de solicitud-respuesta
while (true) {
// la solicitud procede del teclado
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// ¿Terminado?
if (demande.Trim().ToLower() == "bye")
break;
// se envía la solicitud al servidor
writer.WriteLine(demande);
// se lee la respuesta del servidor
réponse = reader.ReadLine();
// se está procesando la respuesta
Console.WriteLine("Réponse : {0}", réponse);
}
}
}
}
}
} catch (Exception e) {
// error
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
La estructura de este cliente se ajusta a la arquitectura general básica propuesta para los clientes Tcp. Estos son los resultados obtenidos con la siguiente configuración:
- el servidor se ejecuta en el puerto 100 en una ventana de DOS
- en el mismo equipo se ejecutan dos clientes en otras dos ventanas de DOS
En la ventana del cliente A (n.º 0) aparecen los siguientes mensajes:
En la del cliente B (n.º 1):
En la del servidor:
El cliente A n.º 0 se desconecta:
La consola del servidor:
11.6.3. Un cliente genérico TCP
Vamos a escribir un cliente TCP genérico que se iniciará de la siguiente manera: ClientTcpGenerique servidor puerto. Su funcionamiento será similar al del cliente PuTTY, pero tendrá una interfaz de consola y no ofrecerá opciones de configuración.
En la aplicación anterior, el protocolo de comunicación era conocido: el cliente enviaba una sola línea y el servidor respondía con una sola línea. Cada servicio tiene su protocolo específico y también se dan las siguientes situaciones:
- el cliente debe enviar varias líneas de texto antes de recibir una respuesta
- la respuesta de un servidor puede constar de varias líneas de texto
Por lo tanto, el ciclo de envío de una única línea al servidor / recepción de una única línea enviada por el servidor no siempre resulta adecuado. Para gestionar protocolos más complejos que el de eco, el cliente TCP genérico contará con dos subprocesos:
- el hilo principal leerá las líneas de texto introducidas con el teclado y las enviará al servidor.
- Un hilo secundario trabajará en paralelo y se dedicará a leer las líneas de texto enviadas por el servidor. En cuanto reciba una, la mostrará en la consola. El hilo solo se detiene cuando el servidor cierra la conexión. Por lo tanto, trabaja de forma continua.
El código es el siguiente:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
// recibe como parámetro las características de un servicio en el formato: servidor puerto
// se conecta al servicio
// envía al servidor cada línea tecleada
// crea un hilo para leer de forma continua las líneas de texto enviadas por el servidor
class ClientTcpGenerique {
static void Main(string[] args) {
// sintaxis
const string syntaxe = "pg serveur port";
// número de argumentos
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// se anota el nombre del servidor
string serveur = args[0];
// el puerto debe ser un número entero mayor que 0
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// se establece la conexión con el servicio
TcpClient tcpClient = null;
try {
tcpClient = new TcpClient(serveur, port);
} catch (Exception ex) {
// error
Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
// fin
return;
}
// se inicia un hilo independiente para leer las líneas de texto enviadas por el servidor
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// la lectura de los comandos del teclado se realiza en el hilo principal
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string demande = null; // solicitud del cliente
try {
// se gestiona la conexión del cliente
using (tcpClient) {
// se crea un flujo de escritura hacia el servidor
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin búfer
writer.AutoFlush = true;
// bucle de solicitud-respuesta
while (true) {
demande = Console.ReadLine();
// ¿Terminado?
if (demande.Trim().ToLower() == "bye")
break;
// Se envía la solicitud al servidor
writer.WriteLine(demande);
}
}
}
}
} catch (Exception e) {
// error
Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
}
}
// hilo de lectura cliente <-- servidor
public static void Receive(object infos) {
// datos locales
string réponse = null; // respuesta del servidor
// creación del flujo de entrada
try {
using (TcpClient tcpClient = infos as TcpClient) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// bucle de lectura continua de las líneas de texto del flujo de entrada
while ((réponse = reader.ReadLine()) != null) {
// visualización en la consola
Console.WriteLine("<-- {0}", réponse);
}
}
}
}
} catch (Exception ex) {
// error
Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// se señala el final del hilo de lectura
Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
}
}
}
}
- línea 34: el cliente se conecta al servidor
- línea 43: se inicia un hilo para leer las líneas de texto del servidor. Debe ejecutar el método Receive de la línea 73. A este método se le pasa la instancia TcpClient que se ha conectado al servidor.
- líneas 57-64: el bucle de introducción de comandos de teclado / envío de comandos al servidor. La introducción de los comandos de teclado la gestiona el hilo principal.
- líneas 75-98: el método Receive ejecutado por el hilo de lectura de las líneas de texto. Este método recibe como parámetro la instancia TcpClient que se ha conectado al servidor.
- líneas 84-87: el bucle continuo de lectura de las líneas de texto enviadas por el servidor. Solo se detiene cuando el servidor cierra la conexión abierta con el cliente.
A continuación se muestran algunos ejemplos que retoman los utilizados con el cliente putty en el apartado 11.4. El cliente se ejecuta en una consola DOS.
Protocolo HTTP
Se invita al lector a volver a leer las explicaciones que figuran en el apartado 11.4.2. Solo comentaremos lo que es específico de la aplicación:
- línea 28: tras el envío de la línea 27, el servidor HTTP cerró la conexión, lo que provocó la finalización del hilo de lectura. El hilo principal, que lee los comandos introducidos mediante el teclado, sigue activo. El comando de la línea 29, introducido mediante el teclado, lo detiene.
Protocolo SMTP
Se recomienda al lector que vuelva a leer las explicaciones del apartado 11.4.3 y que pruebe los demás ejemplos utilizados con el cliente putty.
11.6.4. Un servidor TCP genérico « »
Ahora nos centramos en un servidor
- que muestra en pantalla los comandos enviados por sus clientes
- y les envía como respuesta las líneas de texto tecleadas por un usuario. Por lo tanto, es este último quien actúa como servidor.
El programa se ejecuta en una ventana de DOS mediante: ServeurTcpGenerique portEcoute, donde portEcoute es el puerto al que deben conectarse los clientes. El servicio al cliente lo prestarán dos subprocesos:
- el hilo principal, que:
- atenderá a los clientes uno tras otro y no en paralelo;
- que leerá las líneas tecleadas por el usuario y las enviará al cliente. El usuario indicará mediante el comando «bye» que cierra la conexión con el cliente. Dado que la consola no puede utilizarse para dos clientes simultáneamente, nuestro servidor solo atiende a un cliente a la vez.
- un hilo secundario dedicado exclusivamente a leer las líneas de texto enviadas por el cliente
El servidor, por su parte, nunca se detiene, salvo que el usuario pulse Ctrl-C en el teclado.
Veamos algunos ejemplos. El servidor se ejecuta en el puerto 100 y utilizamos el cliente genérico de paragraphe11.6.3 para comunicarnos con él. La ventana del cliente es la siguiente:
Las líneas que comienzan por <-- son las enviadas del servidor al cliente; las demás, las del cliente al servidor. La ventana del servidor es la siguiente:
Las líneas que comienzan por <-- son las enviadas del cliente al servidor; las demás, las enviadas por el servidor al cliente. La línea 9 indica que el hilo de lectura de las solicitudes del cliente se ha detenido. El hilo principal del servidor sigue a la espera de comandos introducidos mediante el teclado para enviarlos al cliente. Por lo tanto, hay que introducir mediante el teclado el comando bye de la línea 10 para pasar al siguiente cliente. El servidor sigue activo aunque el cliente 1 haya finalizado. Se inicia un segundo cliente para el mismo servidor:
La ventana del servidor queda entonces así:
Tras la línea 6 anterior, el servidor ha pasado a estar a la espera de un nuevo cliente. Se puede detener pulsando Ctrl-C.
Simulemos ahora un servidor web iniciando nuestro servidor genérico en el puerto 88:
Ahora abramos un navegador y solicitemos la página http://localhost:88/exemple.html. El navegador se conectará entonces al puerto 88 de la máquina localhost y solicitará la página /exemple.html:
![]() |
Veamos ahora la ventana de nuestro servidor:
Vemos los encabezados HTTP enviados por el navegador. Esto nos permite descubrir otros encabezados HTTP distintos de los que ya hemos visto. Elaboremos una respuesta para nuestro cliente. El usuario que teclea es aquí el verdadero servidor y puede elaborar una respuesta manualmente. Recordemos la respuesta que dio un servidor web en un ejemplo anterior:
Intentemos dar una respuesta similar ciñéndonos a lo estrictamente necesario:
En nuestra respuesta nos hemos limitado a los encabezados HTTP de las líneas 1 a 4. No indicamos el tamaño del documento que vamos a enviar (Content-Length), sino que nos limitamos a indicar que vamos a cerrar la conexión (Connection: close) 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 corresponde a las líneas 6-9. A continuación, el usuario cierra la conexión con el cliente desde el teclado introduciendo el comando bye, línea 10. Ante este comando de teclado, el hilo principal cierra la conexión con el cliente. Esto provoca la excepción de la línea 11. El hilo encargado de leer las líneas de texto del cliente se ha interrumpido bruscamente al cerrarse la conexión con el cliente y ha lanzado una excepción. Tras la línea 12, el servidor queda a la espera de un nuevo cliente.
El navegador del cliente muestra ahora lo siguiente:
![]() |
Si, en el ejemplo anterior, introducimos Affichage/Source para ver qué ha recibido el navegador, obtenemos [2], es decir, exactamente lo que se ha enviado desde el servidor genérico.
El código del servidor genérico TCP es el siguiente:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
public class ServeurTcpGenerique {
public const string syntaxe = "Syntaxe : ServeurGénérique Port";
// programa principal
public static void Main(string[] args) {
// ¿Hay algún argumento?
if (args.Length != 1) {
Console.WriteLine(syntaxe);
Environment.Exit(1);
}
// Este argumento debe ser un número entero mayor que 0
int port = 0;
if (!int.TryParse(args[0], out port) || port <= 0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
Environment.Exit(2);
}
// Se crea el servicio de escucha
TcpListener ecoute = null;
try {
// se crea el servicio
ecoute = new TcpListener(IPAddress.Any, port);
// se inicia
ecoute.Start();
// seguimiento
Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
while (true) {
// espera a un cliente
Console.WriteLine("Attente du client suivant...");
TcpClient tcpClient = ecoute.AcceptTcpClient();
Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
// se inicia un hilo independiente para leer las líneas de texto enviadas por el cliente
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// la lectura de los comandos del teclado se realiza en el hilo principal
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string réponse = null; // respuesta del servidor
// se gestiona la conexión del cliente
using (tcpClient) {
// Se crea un flujo de escritura hacia el cliente
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin búfer
writer.AutoFlush = true;
// bucle de introducción de respuestas desde el teclado
while (true) {
réponse = Console.ReadLine();
// ¿Terminado?
if (réponse.Trim().ToLower() == "bye")
break;
// se envía la solicitud al cliente
writer.WriteLine(réponse);
}
}
}
}
}
} catch (Exception ex) {
// se notifica el error
Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// fin de la escucha
ecoute.Stop();
}
}
// hilo de lectura servidor <-- cliente
public static void Receive(object infos) {
// datos locales
string demande = null; // solicitud del cliente
string idClient=null; // identidad del cliente
// gestión de la conexión del cliente
try {
using (TcpClient tcpClient = infos as TcpClient) {
// identidad del cliente
idClient = tcpClient.Client.RemoteEndPoint.ToString();
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// bucle de lectura continua de las líneas de texto del flujo de entrada
while ((demande = reader.ReadLine()) != null) {
// visualización en consola
Console.WriteLine("<-- {0}", demande);
}
}
}
}
} catch (Exception ex) {
// error
Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
} finally {
// se notifica el fin del hilo de lectura
Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
}
}
}
}
- línea 29: el servicio de escucha se crea, pero no se inicia. Escucha todas las interfaces de red del equipo.
- línea 31: se inicia el servicio de escucha
- línea 34: bucle infinito de espera de clientes. El usuario detendrá el servidor con Ctrl-C.
- línea 37: espera de un cliente —operación bloqueante—. Cuando llega el cliente, la instancia TcpClient devuelta por el método AcceptTcpClient representa el lado del servidor de una conexión abierta con el cliente.
- línea 40: el flujo de lectura de las solicitudes del cliente se asigna a un hilo independiente.
- línea 45: uso de la conexión con el cliente en una cláusula using para garantizar que se cierre pase lo que pase.
- línea 47: uso del flujo de red en una cláusula using
- línea 48: creación, en una cláusula using, de un flujo de escritura sobre el flujo de red
- línea 50: el flujo de escritura no se almacenará en búfer
- líneas 52-59: bucle de introducción mediante el teclado de los comandos que se enviarán al cliente
- línea 69: fin del servicio de escucha. Esta instrucción nunca se ejecutará aquí, ya que el servidor se detiene con Ctrl-C.
- línea 78: el método Receive, que muestra de forma continua en la consola las líneas de texto enviadas por el cliente. Aquí se repite lo que se ha visto para el cliente genérico TCP.
11.6.5. Un cliente web « »
En el ejemplo anterior hemos visto algunos de los encabezados HTTP que enviaba un navegador:
Vamos a escribir un cliente web al que se le pasaría como parámetro un URL y que mostraría en pantalla el texto enviado por el servidor. Supondremos que este es compatible con el protocolo HTTP 1.1. De los encabezados anteriores, solo utilizaremos los siguientes:
- el primer encabezado indica el documento deseado
- el segundo, el servidor al que se realiza la consulta
- el tercero, que queremos que el servidor cierre la conexión tras habernos respondido.
Si en la línea 1 anterior sustituimos GET por HEAD, el servidor solo nos enviará los encabezados HTTP y no el documento especificado en la línea 1.
Nuestro cliente web se llamará de la siguiente manera: ClientWeb URL cmd, donde URL es elURL deseada y «cmd» una de las dos palabras clave «GET» o «HEAD» para indicar si solo se desean los encabezados (HEAD) o también el contenido de la página (GET). Veamos un primer ejemplo:
- línea 1: solo solicitamos los encabezados HTTP (HEAD)
- líneas 2-9: la respuesta del servidor
Si utilizamos GET en lugar de HEAD en la llamada al cliente web, obtenemos el mismo resultado que con HEAD, además del cuerpo del documento solicitado.
El código del cliente web es el siguiente:
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
class ClientWeb {
static void Main(string[] args) {
// sintaxis
const string syntaxe = "pg URI GET/HEAD";
// número de argumentos
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// se anota el URI solicitado
string stringURI = args[0];
string commande = args[1].ToUpper();
// verificación de la validez del URI
if(! stringURI.StartsWith("http://")){
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI incorrecto
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// Comprobación del pedido
if (commande != "GET" && commande != "HEAD") {
// pedido incorrecto
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// Conectándonos al servicio
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin almacenar en búfer
writer.AutoFlush = true;
// se solicita el URL - envío de las cabeceras HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// se lee la respuesta
string réponse = null;
while ((réponse = reader.ReadLine()) != null) {
// se muestra la respuesta en la consola
Console.WriteLine(réponse);
}
}
}
}
}
} catch (Exception e) {
// se muestra la excepción
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
La única novedad en este programa es el uso de la clase Uri. El programa recibe un URL (Uniform Resource Locator) o un URI (Uniform Resource Identifier) con el formato http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La clase Uri nos permite descomponer la cadena del URL en sus diferentes elementos.
- Líneas 26-33: se crea un objeto Uri a partir de la cadena stringURI recibida como parámetro. Si la cadena «URI» recibida como parámetro no es una «URI» válida (falta del protocolo, del servidor, etc.), se lanza una excepción. Esto nos permite verificar la validez del parámetro recibido. Una vez creado el objeto Uri, se tiene acceso a los distintos elementos de esta URI. Así, si el objeto uri del código anterior se ha creado a partir de la cadena http://serveur:port/document?param1=val1¶m2=val2;..., tendremos:
- uri.Host=serveur,
- uri.Port=port,
- uri.Path = document,
- uri.Query=param1=val1¶m2=val2;...,
- uri.pathAndQuery= cheminPageHTML?param1=val1¶m2=val2;...,
- uri.Scheme=http.
11.6.6. Un cliente web que gestiona las redirecciones
El cliente web anterior no gestiona una posible redirección de URL que él mismo ha solicitado. He aquí un ejemplo:
- línea 2: el código 302 Found indica una redirección. La dirección a la que debe redirigirse el navegador se encuentra en el cuerpo del documento, en la línea 16.
Un segundo ejemplo:
- línea 2: el código 301 Moved Permanently indica una redirección. La dirección a la que debe redirigirse el navegador se indica en la línea 6, en el encabezado HTTP Location.
Un tercer ejemplo:
- línea 2: el código 302 «Moved Temporarily» indica una redirección. La dirección a la que debe redirigirse el navegador se indica en la línea 5, en el encabezado HTTP Location.
Un cuarto ejemplo con un servidor IIS local en el equipo:
- línea 2: el código 302 «Object moved» indica una redirección. La dirección a la que debe redirigirse el navegador se indica en la línea 5, en el encabezado «HTTP Location». Cabe señalar que, a diferencia de los ejemplos anteriores, la dirección de redirección es relativa. La dirección completa es, de hecho, http://localhost/localstart.asp.
Nos proponemos gestionar las redirecciones cuando la primera línea de los encabezados HTTP contenga la palabra clave moved (sin distinción entre mayúsculas y minúsculas) y la dirección de redirección se encuentre en el encabezado HTTP Location.
Si retomamos los tres últimos ejemplos, obtenemos los siguientes resultados:
URL: http://www.bull.com
- línea 11: la redirección se realiza a la dirección de la línea 6
URL: http://www.gouv.fr
- línea 11: la redirección se realiza a la dirección de la línea 6
URL: http://localhost
- línea 13: la redirección se realiza a la dirección de la línea 6
- línea 15: se nos ha denegado el acceso a la página http://localhost/localstart.asp.
El programa que gestiona la redirección es el siguiente:
using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;
namespace Chap9 {
class ClientWebAvecRedirection {
static void Main(string[] args) {
// sintaxis
const string syntaxe = "pg URI GET/HEAD";
// número de argumentos
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// se observa que se solicita el URI
string stringURI = args[0];
string commande = args[1].ToUpper();
// verificación de la validez del URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI incorrecto
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// Comprobación del pedido
if (commande != "GET" && commande != "HEAD") {
// pedido incorrecto
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
const int nbRedirsMax = 1; // no se acepta más de una redirección
int nbRedirs = 0; // número de redirecciones en curso
// expresión regular para encontrar una redirección URL
Regex location = new Regex(@"^Location: (.+?)$");
try {
// se pueden solicitar varios URL si hay redirecciones
while (nbRedirs <= nbRedirsMax) {
// gestión de redirecciones
bool redir = false;
bool locationFound = false;
string locationString = null;
// Se establece conexión con el servicio
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
// flujo de salida sin almacenamiento en búfer
writer.AutoFlush = true;
// se solicita el URL: envío de las cabeceras HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// se lee la primera línea de la respuesta
string premièreLigne = reader.ReadLine();
// eco en pantalla
Console.WriteLine(premièreLigne);
// ¿redirección?
if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
// hay una redirección
redir = true;
nbRedirs++;
}
// siguientes encabezados HTTP hasta encontrar la línea vacía que indica el final de los encabezados
string réponse = null;
while ((réponse = reader.ReadLine()) != "") {
// se muestra la respuesta
Console.WriteLine(réponse);
// si hay redirección, se busca el encabezado «Location»
if (redir && !locationFound) {
// se compara la línea actual con la expresión relacional «location»
Match résultat = location.Match(réponse);
if (résultat.Success) {
// si se ha encontrado, se anota el URL de redirección
locationString = résultat.Groups[1].Value;
// se anota que se ha encontrado
locationFound = true;
}
}
}
// Se han agotado los encabezados HTTP: se escribe la línea vacía
Console.WriteLine(réponse);
// luego pasamos al cuerpo del documento
while ((réponse = reader.ReadLine()) != null) {
Console.WriteLine(réponse);
}
}
}
}
// ¿Hemos terminado?
if (!locationFound || nbRedirs > nbRedirsMax)
break;
// Hay que realizar una redirección: se crea la nueva URI
try {
if (locationString.StartsWith("http")) {
// dirección http completa
uri = new Uri(locationString);
} else {
// Dirección HTTP relativa a la URI actual
uri = new Uri(uri, locationString);
}
// registro de la consola
Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
} catch (Exception ex) {
// problema con la URI
Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
}
}
} catch (Exception e) {
// se muestra la excepción
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
En comparación con la versión anterior, los cambios son los siguientes:
- línea 46: la expresión regular para recuperar la dirección de redirección en el encabezado HTTP Location: dirección.
- línea 49: el código que antes se ejecutaba para una única URI ahora puede ejecutarse sucesivamente para varias URIs.
- línea 66: se lee la primera línea de los encabezados HTTP enviados por el servidor. Es esta la que contiene la palabra clave moved si el documento solicitado se ha movido.
- líneas 71-75: se comprueba si la primera línea contiene la palabra clave moved. En caso afirmativo, se anota.
- líneas 79-93: lectura de los demás encabezados HTTP hasta encontrar la línea vacía que indica su fin. Si la primera línea anunciaba una redirección, se presta atención al encabezado HTTP Location: dirección para memorizar la dirección de redirección en locationString.
- líneas 98-100: el resto de la respuesta del servidor HTTP se muestra en la consola.
- líneas 105-106: la URI solicitada se ha procesado por completo y se ha mostrado. Si no hay que realizar ninguna redirección o si se supera el número de redirecciones permitidas, se sale del programa.
- líneas 108-122: si hay redirección, se calcula la nueva URI que se debe solicitar. Hay que realizar un pequeño ajuste dependiendo de si la dirección de redirección encontrada era absoluta (línea 111) o relativa (línea 114).
11.7. Las clases .NET especializadas en un protocolo concreto de Internet
En los ejemplos anteriores del cliente web, el protocolo HTTP se gestionaba con un cliente TCP. Por lo tanto, teníamos que gestionar nosotros mismos el protocolo de comunicación específico utilizado. Podríamos haber creado, de forma análoga, un cliente SMTP o POP. El marco de trabajo .NET ofrece clases especializadas para los protocolos HTTP y SMTP. Estas clases conocen el protocolo de comunicación entre el cliente y el servidor y evitan que el desarrollador tenga que gestionarlas. A continuación las presentamos.
11.7.1. La clase classeWebClient
Existe una clase WebClient capaz de comunicarse con un servidor web. Consideremos el ejemplo del cliente web del apartado 11.6.5, tratado aquí con la clase WebClient.
using System;
using System.IO;
using System.Net;
namespace Chap9 {
public class Program {
public static void Main(string[] args) {
// sintaxis: [prog] URI
const string syntaxe = "pg URI";
// número de argumentos
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// se registra el URI solicitado
string stringURI = args[0];
// verificación de la validez del URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI incorrecto
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
try {
// creación de un cliente web
using (WebClient client = new WebClient()) {
// Añadido de un encabezado HTTP
client.Headers.Add("user-agent", "st");
using (Stream stream = client.OpenRead(uri)) {
using (StreamReader reader = new StreamReader(stream)) {
// visualización de la respuesta del servidor web
Console.WriteLine(reader.ReadToEnd());
// Visualización de los encabezados de la respuesta del servidor
Console.WriteLine("---------------------");
foreach (string clé in client.ResponseHeaders.Keys) {
Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
}
Console.WriteLine("---------------------");
}
}
}
} catch (WebException e1) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
} catch (Exception e2) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
}
}
}
}
- línea 35: se crea el cliente web, pero aún no está configurado
- línea 37: se añade un encabezado HTTP a la solicitud HTTP que se va a realizar. Veremos que se enviarán otros encabezados de forma predeterminada.
- Línea 38: el cliente web solicita la URI indicada por el usuario y lee el documento enviado. [WebClient].OpenRead(Uri) establece la conexión con Uri y lee la respuesta. Ahí radica el interés de la clase. Se encarga de la comunicación con el servidor web. El resultado del método OpenRead es de tipo Stream y representa el documento solicitado. Las cabeceras HTTP enviadas por el servidor y que preceden al documento en la respuesta no forman parte de este.
- Línea 39: se utiliza un StreamReader y, en la línea 41, su método ReadToEnd para leer la respuesta completa.
- Líneas 44-46: se muestran los encabezados HTTP de la respuesta del servidor. [WebClient].ResponseHeaders representa una colección con valores cuyas claves son los nombres de los encabezados HTTP y cuyos valores son las cadenas de caracteres asociadas a dichos encabezados.
- línea 51: las excepciones que se producen durante un intercambio entre el cliente y el servidor son de tipo WebException.
Veamos algunos ejemplos.
Iniciamos el servidor genérico TCP creado en el apartado 6.4.6:
Iniciamos el cliente web anterior de la siguiente manera:
La URI solicitada es la del servidor genérico. Este muestra entonces los encabezados HTTP que le ha enviado el cliente web:
Así, vemos que:
- que el cliente web envía 3 encabezados HTTP por defecto (líneas 3, 5 y 6)
- línea 4: el encabezado que hemos generado nosotros mismos (línea 37 del código)
- que el cliente web utiliza por defecto el método GET (línea 3). Existen otros métodos, entre los que se encuentran POST y HEAD.
Ahora solicitemos un recurso que no existe:
- línea 2: se ha producido una excepción de tipo WebException porque el servidor ha respondido con el código 404 Not Found para indicar que el recurso solicitado no existía.
Por último, terminemos solicitando un recurso que sí existe:
El archivo istia.univ-angers.txt generado por el comando es el siguiente:
- línea 1: el documento HTML solicitado.
- líneas 3-10: los encabezados de la respuesta HTTP en un orden que no tiene por qué ser el mismo en el que se enviaron.
La clase WebClient dispone de métodos que permiten recibir un documento (métodos DownLoad) o enviarlo (métodos UpLoad):
para descargar un recurso como matriz de bytes (por ejemplo, una imagen) | |
para descargar un recurso y guardarlo en un archivo local | |
para descargar un recurso y recuperarlo como cadena de caracteres (por ejemplo, un archivo HTML) | |
el equivalente a OpenRead, pero para enviar datos al servidor | |
el equivalente a DownLoadData, pero hacia el servidor | |
el equivalente a DownLoadFile, pero hacia el servidor | |
el equivalente a DownLoadString, pero hacia el servidor | |
para enviar al servidor los datos de un comando POST y recuperar los resultados en forma de matriz de bytes. El comando POST solicita un documento al tiempo que transmite al servidor la información necesaria para determinar el documento real que debe enviarse. Esta información se envía como documento al servidor, de ahí el nombre UpLoad del método. Se envía tras la línea en blanco de los encabezados HTTP en el formato param1=valeur1¶m2=valeur2&...:
El mismo documento podría solicitarse mediante el método GET:
La diferencia entre ambos métodos es que el navegador que muestre la URI solicitada mostrará «/document» en el caso de «POST» y «/document?param1=valeur1¶m2=valeur2&...» en el caso de «GET». |
11.7.2. Las clases WebRequest / WebResponse
A veces, la clase WebClient no es lo suficientemente flexible como para hacer lo que queremos. Volvamos al ejemplo del cliente web con redirección analizado en el apartado 11.6.6. Necesitamos enviar el encabezado HTTP:
Hemos visto que los encabezados HTTP emitidos por defecto por el cliente web eran los siguientes:
También hemos visto que era posible añadir encabezados HTTP a los anteriores con la colección [WebClient].Headers. Solo la línea 1 no es un encabezado perteneciente a la colección Headers, ya que no tiene el formato clave:valor. No he encontrado la forma de cambiar el «GET» por «HEAD» en la línea 1 partiendo de la clase «WebClient» (¿quizás no he buscado bien?). Cuando la clase WebClient ha alcanzado sus límites, se puede pasar a las clases WebRequest / WebResponse:
- WebRequest: representa la totalidad de la solicitud del cliente web.
- WebResponse: representa la totalidad de la respuesta del servidor web
Hemos dicho que la clase WebClient gestiona los esquemas http:, https:, ftp: y file:. Las solicitudes y respuestas de estos distintos protocolos no tienen el mismo formato. Por ello, es necesario manejar el tipo exacto de estos elementos en lugar de su tipo genérico WebRequest y WebResponse. Por lo tanto, utilizaremos las clases:
- HttpWebRequest, HttpWebResponse para un cliente HTTP
- FtpWebRequest y FtpWebResponse para un cliente FTP
Ahora tratamos, con las clases HttpWebRequest y HttpWebresponse, el ejemplo del cliente web con redirección estudiado en el apartado 11.6.6. El código es el siguiente:
using System;
using System.IO;
using System.Net.Sockets;
using System.Net;
namespace Chap9 {
class WebRequestResponse {
static void Main(string[] args) {
// sintaxis
const string syntaxe = "pg URI GET/HEAD";
// número de argumentos
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// se anota el URI solicitado
string stringURI = args[0];
string commande = args[1].ToUpper();
// verificación de la validez del URI
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI incorrecto
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// Comprobación del pedido
if (commande != "GET" && commande != "HEAD") {
// pedido incorrecto
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// Se está configurando la solicitud
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = commande;
httpWebRequest.Proxy = null;
// se ejecuta
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// resultado
Console.WriteLine("---------------------");
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
// encabezados HTTP
Console.WriteLine("---------------------");
foreach (string clé in httpWebResponse.Headers.Keys) {
Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
}
Console.WriteLine("---------------------");
// documento
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
// se muestra la respuesta en la consola
Console.WriteLine(reader.ReadToEnd());
}
}
} catch (WebException e1) {
// se recupera la respuesta
HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
} catch (Exception e2) {
// se muestra la excepción
Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
}
}
}
}
- línea 40: se crea un objeto de tipo WebRequest con el método estático WebRequest.Create(Uri uri), donde uri es la URI del documento que se va a descargar. Dado que sabemos que el protocolo de la URI es HTTP, el tipo del resultado se cambia a HttpWebRequest para poder acceder a los elementos específicos del protocolo HTTP.
- Línea 41: establecemos el método GET / POST / HEAD de la primera línea de los encabezados HTTP. En este caso, será GET o HEAD.
- línea 42: en una red privada de empresa, es habitual que los equipos de la empresa estén aislados de Internet por motivos de seguridad. Para ello, la red privada utiliza direcciones de Internet que los routers de Internet no enrutan. La red privada está conectada a Internet a través de equipos especiales denominados «proxy», que están conectados tanto a la red privada de la empresa como a Internet. Este es un ejemplo de equipos con varias direcciones: IP. Un equipo de la red privada no puede establecer por sí mismo una conexión con un servidor de Internet, por ejemplo, un servidor web. Debe solicitar a un equipo proxy que lo haga por él. Un equipo proxy puede albergar servidores proxy para diferentes protocolos. Se habla de proxy HTTP para referirse al servicio que se encarga de realizar las solicitudes HTTP en nombre de los equipos de la red privada. Si existe un servidor proxy de este tipo (HTTP), hay que indicarlo en el campo [WebRequest].proxy. Por ejemplo, se escribiría:
si el proxy HTTP opera en el puerto 3128 del equipo pproxy.istia.uang. Se introduce «null» en el campo [WebRequest].proxy si el equipo tiene acceso directo a Internet y no tiene que pasar por un proxy.
- línea 44: el método GetResponse() solicita el documento identificado por su URI y devuelve un objeto WebRequestResponse que aquí se transforma en un objeto HttpWebResponse. Este objeto representa la respuesta del servidor a la solicitud del documento.
- línea 47:
- [HttpWebResponse].ResponseUri: es la URI del servidor que ha enviado el documento. En caso de redirección, esta puede ser diferente de la URI del servidor al que se realizó la consulta inicialmente. Cabe señalar que el código no gestiona la redirección. Esta se gestiona automáticamente mediante el método GetResponse. Una vez más, esta es la ventaja de las clases de alto nivel frente a las clases básicas del protocolo TCP.
- [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription representan la primera línea de la respuesta, por ejemplo: HTTP/1.1 200 OK. StatusCode es 200 y StatusDescription es OK.
- línea 50: [HttpWebResponse].Headers es la colección de encabezados HTTP de la respuesta.
- línea 55: [HttpWebResponse].GetResponseStream: es el flujo que permite obtener el documento contenido en la respuesta.
- línea 61: puede producirse una excepción de tipo WebException
- línea 63: [WebException].Response es la respuesta que ha provocado la excepción.
A continuación se muestra un ejemplo de ejecución:
- líneas 1 y 3: el servidor que ha respondido no es el mismo al que se había enviado la consulta. Por lo tanto, se ha producido una redirección.
- líneas 5-11: los encabezados HTTP enviados por el servidor
11.7.3. Aplicación: un cliente proxy de un servidor web de traducción
A continuación mostramos cómo las clases anteriores nos permiten aprovechar los recursos de la web.
11.7.3.1. L'application
En la web existen sitios de traducción. El que se utilizará aquí es el sitio http://trans.voila.fr/traduction_voila.php:
![]() | El texto que se va a traducir se introduce en [1], y la dirección de traducción se selecciona en [2]. La traducción se solicita mediante [3] y se obtiene en [4]. |
Vamos a escribir una aplicación cliente para Windows de la aplicación anterior. No hará nada más que la aplicación del sitio [trans.voila.fr]. Su interfaz será la siguiente:
![]() |
11.7.3.2. La arquitectura de la aplicación
La aplicación tendrá la siguiente arquitectura de dos capas:
![]() |
11.7.3.3. El proyecto de Visual Studio
El proyecto de Visual Studio será el siguiente:
![]() |
- En [1], la solución se compone de dos proyectos:
- [2]: uno para la capa [dao] y las entidades que esta utiliza,
- [3]: el otro para la interfaz de Windows
11.7.3.4. El proyecto [dao]
El proyecto [dao] está formado por los siguientes elementos:
- IServiceTraduction.cs: la interfaz presentada a la capa [ui]
- ServiceTraduction: la implementación de esta interfaz
- WebTraductionsException: una excepción específica de la aplicación
La interfaz IServiceTraduction es la siguiente:
using System.Collections.Generic;
namespace dao {
public interface IServiceTraduction {
// idiomas utilizados
IDictionary<string, string> LanguesTraduites { get; }
// traducción
string Traduire(string texte, string deQuoiVersQuoi);
}
}
- línea 6: la propiedad LanguesTraduites devuelve el diccionario de idiomas aceptados por el servidor de traducción. Este diccionario contiene entradas con el formato ["fe","Français-Anglais"], donde el valor indica una dirección de traducción —en este caso, del francés al inglés— y la clave «fe» es un código utilizado por el servidor de traducción trans.voila.fr.
- línea 8: el método Traduire es el método de traducción:
- texte es el texto que se va a traducir
- deQuoiVersQuoi es una de las claves del diccionario de idiomas traducidos
- el método devuelve la traducción del texto
ServiceTraduction es una clase de implementación de la interfaz IServiceTraduction. La detallamos en la sección siguiente.
WebTraductionsException es la siguiente clase de excepción:
using System;
namespace entites {
public class WebTraductionsException : Exception {
// código de error
public int Code { get; set; }
// fabricantes
public WebTraductionsException() {
}
public WebTraductionsException(string message)
: base(message) {
}
public WebTraductionsException(string message, Exception e)
: base(message, e) {
}
}
}
- línea 7: un código de error
11.7.3.5. El cliente web [ServiceTraduction]
Volvamos a la arquitectura de nuestra aplicación:
![]() |
La clase [ServiceTraduction] que debemos escribir es un cliente del servicio web de traducción [trans.voila.fr]. Para escribirla, debemos comprender
- qué espera el servidor de traducción de su cliente
- qué le devuelve a su cliente
Veamos con un ejemplo el diálogo entre cliente y servidor que tiene lugar en una traducción. Retomemos el ejemplo presentado en la introducción de la aplicación:
![]() | El texto que se va a traducir se inserta en [1], y la dirección de traducción se selecciona en [2]. La traducción se solicita mediante [3] y se obtiene en [4]. |
Para obtener la traducción [4], el navegador ha enviado la siguiente solicitud GET (que aparece en su barra de direcciones):
http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=el+perro+está+enfermo
Es bastante fácil de entender:
- http://trans.voila.fr/traduction_voila.php es la URL del servicio de traducción
- isText=1 parece indicar que se trata de texto
- translationDirection indica el sentido de la traducción, en este caso Français-Anglais
- stext es el texto que hay que traducir en un formato denominado «URL codificada». De hecho, hay ciertos caracteres que no pueden aparecer en una URL. Es el caso, por ejemplo, del espacio, que aquí se ha codificado con un +. El framework .NET ofrece el método estático System.Web.HttpUtility.UrlEncode para realizar esta tarea de codificación.
De ello se deduce que, para consultar el servidor de traducción, nuestra clase [ServiceTraduction] podrá utilizar la cadena
, en la que los marcadores {0} y {1} se sustituirán, respectivamente, por el sentido de la traducción y el texto a traducir.
¿Cómo se saben los sentidos de traducción aceptados por el servidor? En la captura de pantalla anterior, los idiomas traducidos aparecen en la lista desplegable. Si en el navegador se consulta (Ver / Código fuente) el código HTML de la página, se encuentra lo siguiente para la lista desplegable:
No es un código HTML muy limpio, ya que cada etiqueta <option> debería cerrarse normalmente con una etiqueta </option>. Dicho esto, los atributos «value» nos proporcionan la lista de códigos de traducción que deben enviarse al servidor. En el diccionario LanguesTraduites de la interfaz IServiceTraduction, las claves serán los atributos «value» mencionados anteriormente y los valores, los textos que muestra el menú desplegable.
Ahora veamos (Ver / Código fuente) dónde se encuentra en la página HTML la traducción devuelta por el servidor de traducción:
La traducción se encuentra justo en medio de la página HTML devuelta. ¿Cómo podemos localizarla? Podemos utilizar una expresión regular con la secuencia ...</div>, ya que la etiqueta solo aparece en ese punto de la página HTML. La expresión regular en C# que permite recuperar el texto traducido es la siguiente:
Ahora disponemos de los elementos necesarios para escribir la clase de implementación ServiceTraduction de la interfaz IServiceTraduction:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;
namespace dao {
public class ServiceTraduction : IServiceTraduction {
// propiedades de configuración automática del servicio
public IDictionary<string, string> LanguesTraduites { get; set; }
public string UrlServeurTraduction { get; set; }
public string ProxyHttp { get; set; }
public String RegexTraduction { get; set; }
// traducción
public string Traduire(string texte, string deQuoiVersQuoi) {
// ¿Es posible la traducción solicitada?
if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
}
// texto a traducir
string texteATraduire = HttpUtility.UrlEncode(texte);
// URI a solicitar
string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
// expresión regular para encontrar la traducción en la respuesta
Regex patternTraduction = new Regex(RegexTraduction);
// excepción
WebTraductionsException exception = null;
// traducción
string traduction = null;
try {
// se configura la consulta
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = "GET";
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
// se ejecuta
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// documento
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
bool traductionTrouvée = false;
string ligne = null;
while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
// ¿Se busca la traducción en la línea actual?
MatchCollection résultats = patternTraduction.Matches(ligne);
// ¿Se ha encontrado la traducción?
if (résultats.Count != 0) {
traduction = résultats[0].Groups[1].Value.Trim();
traductionTrouvée = true;
}
}
// ¿Se ha encontrado la traducción?
if (!traductionTrouvée) {
exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
}
}
}
} catch (Exception e) {
exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
}
// ¿Excepción?
if (exception != null) {
throw exception;
} else {
return traduction;
}
}
}
}
- línea 12: la propiedad LanguesTraduites de la interfaz IServiceTraduction, inicializada desde el exterior
- línea 13: la propiedad UrlServeurTraduction es la URL que se debe solicitar al servidor de traducción: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, donde el marcador {0} deberá sustituirse por el idioma de destino y el marcador {1} por el texto que se va a traducir —inicializada desde el exterior—
- línea 14: la propiedad ProxyHttp es el posible proxy HTTP que se va a utilizar, por ejemplo: pproxy.istia.uang:3128 —inicializada desde el exterior—
- línea 15: la propiedad RegexTraduction es la expresión regular que permite recuperar la traducción en el flujo HTML devuelto por el servidor de traducción, por ejemplo @"<div class=""txtTrad"">(.*?)</div>" —inicializada desde el exterior—
- Estas cuatro propiedades serán, en nuestra aplicación, inicializadas por Spring.
- líneas 20-22: se comprueba que la dirección de traducción solicitada existe efectivamente en el diccionario de idiomas traducidos. Si no es así, se lanza una excepción.
- línea 24: el texto que se va a traducir se codifica para que pueda formar parte de una URL
- línea 26: se construye la URI del servicio de traducción. Si la propiedad UrlServeurTraduction es la cadena http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, el marcador {0} se sustituye por el sentido de la traducción y el marcador {1} por el texto a traducir.
- línea 28: se construye la plantilla de búsqueda de la traducción en la respuesta HTML del servidor de traducción.
- Líneas 33 y 60: la operación de consulta al servidor de traducción se realiza dentro de un bloque «try / catch».
- línea 35: se crea el objeto HttpWebRequest, que se utilizará para consultar el servidor de traducción, con la URI del documento solicitado.
- Línea 36: el método de consulta es GET. Se podría prescindir de esta instrucción, ya que GET es probablemente el método por defecto del objeto HttpWebRequest.
- línea 37: se establece la propiedad Proxy del objeto HttpWebRequest.
- línea 39: se envía la solicitud al servidor de traducción y se recupera su respuesta, que es de tipo HttpWebResponse.
- Líneas 41-42: se utiliza un StreamReader para leer cada línea de la respuesta HTML del servidor.
- Líneas 45-53: en cada línea de la respuesta, se busca la traducción. Cuando se encuentra, se deja de leer la respuesta HTML y se cierran todos los flujos que se hayan abierto.
- líneas 55-57: si no se ha encontrado ninguna traducción en la respuesta HTML, se prepara una excepción de tipo WebTraductionsException para indicarlo.
- líneas 60-62: si se ha producido una excepción durante la comunicación entre el cliente y el servidor, se encapsula en una excepción de tipo WebTraductionsException para indicarlo.
- líneas 64-68: si se ha registrado una excepción, se lanza; de lo contrario, se devuelve la traducción encontrada.
Nuestro ejemplo supone que el proxy HTTP no requiere autenticación. Si no fuera así, se escribiría algo como:
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");
Aquí hemos utilizado WebRequest / WebResponse en lugar de WebClient porque no necesitamos procesar la respuesta HTML completa del servidor de traducción. Una vez encontrada la traducción en esta respuesta, ya no necesitamos el resto de las líneas de la respuesta. La clase WebClient no permite hacer esto.
A continuación se muestra un programa de prueba de la clase ServiceTraduction:
using System;
using System.Collections.Generic;
using dao;
using entites;
namespace ui {
class Program {
static void Main(string[] args) {
try {
// creación del servicio de traducción
ServiceTraduction serviceTraduction = new ServiceTraduction();
// expresión regular para buscar la traducción
serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
// URL del servidor de traducción
serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
// diccionario de idiomas traducidos
Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
languesTraduites["fe"]= "Français-Anglais";
languesTraduites["fs"]= "Français-Espagnol";
languesTraduites["ef"]= "Anglais-Français";
serviceTraduction.LanguesTraduites = languesTraduites;
// proxy
//serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
// traducción
string texte = "ce chien est perdu";
string deQuoiVersQuoi = "fe";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "l'été sera chaud";
deQuoiVersQuoi = "fs";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "my tailor is rich";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "xx";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
} catch (WebTraductionsException e) {
// error
Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
}
}
}
}
Los resultados obtenidos son los siguientes:
El proyecto [dao] de la solución se compila en un DLL HttpTraductions.dll:
![]() |
11.7.3.6. La interfaz gráfica de la aplicación
Volvamos a la arquitectura de nuestra aplicación:
![]() |
Ahora vamos a escribir la capa [ui]. Esta es el objeto del proyecto [ui] de la solución en desarrollo:
![]() |
La carpeta [lib] [3] contiene algunas de las DLL a las que hace referencia el proyecto [4]:
- las necesarias para Spring: Spring.Core, Common.Logging, antlr.runtime
- la de la capa [dao]: HttpTraductions
El archivo [App.config] contiene la configuración de Spring:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<description>Traductions sur le web</description>
<!-- el servicio de traducción -->
<object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
<property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"/>
<!--
<property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
-->
<property name="RegexTraduction" value="<div class="txtTrad">(.*?)</div>"/>
<property name="LanguesTraduites">
<dictionary key-type="string" value-type="string">
<entry key="fe" value="Français-Anglais"/>
<entry key="ef" value="Anglais-Français"/>
...
<entry key="ei" value="Anglais-Italien"/>
<entry key="ie" value="Italien-Anglais"/>
</dictionary>
</property>
</object>
</objects>
</spring>
</configuration>
- línea 15: los objetos que Spring debe instanciar. Solo habrá uno, el de la línea 18, que instancia el servicio de traducción con la clase ServiceTraduction, que se encuentra en DLL HttpTraductions.
- Línea 19: la propiedad UrlServeurTraduction de la clase ServiceTraduction. Hay un problema con el carácter «&» de la URL. Este carácter tiene un significado en un archivo XML. Por lo tanto, debe protegerse. Lo mismo ocurre con otros caracteres que encontraremos más adelante en el archivo. Deben sustituirse por una secuencia [&code;]: & por [&], < por [<], > por [>], " por ["].
- línea 21: la propiedad ProxyHttp de la clase ServiceTraduction. Queda una propiedad sin inicializar: null. No definir esta propiedad equivale a indicar que no hay ningún proxy HTTP.
- línea 23: la propiedad RegexTraduction de la clase ServiceTraduction. En la expresión regular, ha sido necesario sustituir los caracteres [< > "] por sus equivalentes protegidos.
- Líneas 24-33: la propiedad LanguesTraduites de la clase ServiceTraduction.
El programa [Program.cs] se ejecuta al iniciar la aplicación. Su código es el siguiente:
using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;
namespace ui {
static class Program {
/// <summary>
/// El punto de entrada principal de la aplicación.
/// </summary>
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// --------------- Código de desarrollador
// instanciación del servicio de traducción
IApplicationContext ctx = null;
Exception ex = null;
ServiceTraduction serviceTraduction = null;
try {
// contexto de Spring
ctx = ContextRegistry.GetContext();
// se solicita una referencia al servicio de traducción
serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
} catch (Exception e1) {
// almacenamiento de la excepción
ex = e1;
}
// formulario que se va a mostrar
Form form = null;
// ¿Se ha producido alguna excepción?
if (ex != null) {
// Sí: se crea el mensaje de error que se va a mostrar
StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
Exception e = ex;
while (e != null) {
msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
e = e.InnerException;
}
// creación de la ventana de error a la que se pasa el mensaje de error que se va a mostrar
Form2 form2 = new Form2();
form2.MsgErreur = msgErreur.ToString();
// esta será la ventana que se mostrará
form = form2;
} else {
// Todo ha salido bien
// creación de la interfaz gráfica [Form1] a la que se pasa la referencia al servicio de traducción
Form1 form1 = new Form1();
form1.ServiceTraduction = serviceTraduction;
// esta será la ventana que se mostrará
form = form1;
}
// Visualización de la ventana
Application.Run(form);
}
}
}
Este código ya se ha utilizado en la aplicación «Impuestos», versión 6, en el apartado 7.6.2.
- Spring crea el servicio de traducción en la línea 27. Si la creación se ha realizado correctamente, se mostrará el formulario [Form1] (líneas 52-55); de lo contrario, se mostrará el formulario de error [Form2] (líneas 36-48).
El formulario [Form2] es el que se utiliza en la aplicación Impuestos, versión 6, y se ha explicado en el apartado 7.6.4.
El formulario [Form1] es el siguiente:
![]() |
n.º | tipo | nombre | función |
1 | TextBox | textBoxTexteATraduire | cuadro de texto para introducir el texto a traducir MultiLine=true |
2 | ComboBox | comboBoxLangues | la lista de sentidos de traducción |
3 | Botón | buttonTraduire | para solicitar la traducción del texto [1] en la dirección [2] |
4 | TextBox | textBoxTraduction | la traducción del texto [1] |
El código del formulario [Form1] es el siguiente:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;
namespace ui {
public partial class Form1 : Form {
// servicio de traducción
public ServiceTraduction ServiceTraduction { get; set; }
// diccionario de idiomas
Dictionary<string, string> languesInversées = new Dictionary<string, string>();
// constructor
public Form1() {
InitializeComponent();
}
// carga inicial del formulario
private void Form1_Load(object sender, EventArgs e) {
// creación del diccionario inverso de idiomas
foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
// idiomas
string langues = ServiceTraduction.LanguesTraduites[code];
// adición (idiomas, código) al diccionario inverso
languesInversées[langues] = code;
}
// Relleno del menú desplegable en orden alfabético de los idiomas
string[] languesCombo = languesInversées.Keys.ToArray();
Array.Sort<string>(languesCombo);
foreach (string langue in languesCombo) {
comboBoxLangues.Items.Add(langue);
}
// selección del primer idioma
if (comboBoxLangues.Items.Count != 0) {
comboBoxLangues.SelectedIndex = 0;
}
}
private void buttonTraduire_Click(object sender, EventArgs e) {
// ¿Hay algo que traducir?
string texte = textBoxTexteATraduire.Text.Trim();
if (texte == "") return;
// traducción
try {
textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
} catch (Exception ex) {
textBoxTraduction.Text = ex.Message;
}
}
}
}
- línea 10: una referencia al servicio de traducción. Esta propiedad pública ha sido inicializada por [Program.cs], línea 53. Por lo tanto, cuando se ejecutan los métodos Form1_Load (línea 20) o buttonTraduire_Click (línea 40), este campo ya está inicializado.
- línea 12: el diccionario de idiomas traducidos con entradas de tipo ["Français-Anglais","fe"], c.a.d. El inverso del diccionario LanguesTraduites devuelto por el servicio de traducción.
- línea 20: el método Form1_Load se ejecuta al cargar el formulario.
- líneas 22-27: se utilizan los diccionarios serviceTraduction.LanguesTraduites y ["fe","Français-Anglais"] para construir los diccionarios languesInversées y ["Français-Anglais", "fe"].
- línea 29: languesCombo es la tabla de claves de los diccionarios languesInversées y c.a.d. Una tabla de elementos ["Français-Anglais"]
- línea 30: esta tabla está ordenada para mostrar en el menú desplegable los sentidos de traducción por orden alfabético
- líneas 31-33: se rellena el menú desplegable de idiomas.
- línea 40: el método que se ejecuta cuando el usuario hace clic en el botón [Traduire]
- línea 46: basta con llamar al método serviceTraduction.Traduire para solicitar la traducción. El primer parámetro es el texto que se va a traducir; el segundo, el código de la dirección de traducción. Este código se encuentra en el diccionario languesInversées a partir del elemento seleccionado en el menú desplegable de idiomas.
- línea 48: si se produce una excepción, se muestra en lugar de la traducción.
11.7.3.7. Conclusion
Esta aplicación ha demostrado que los clientes web del marco .NET nos permiten aprovechar los recursos de la web. La técnica es siempre similar:
- determinar la URI a la que se va a realizar la consulta. Esta URI suele llevar parámetros.
- consultarla
- buscar en la respuesta del servidor lo que se busca mediante expresiones regulares
Esta técnica es aleatoria. De hecho, con el paso del tiempo, la URI consultada o la expresión regular que permite encontrar el resultado esperado pueden cambiar. Por lo tanto, conviene guardar esta información en un archivo de configuración. Pero esto puede resultar insuficiente. Veremos en el siguiente capítulo que existen recursos más estables en la web: los servicios web.
11.7.4. Un cliente SMTP (Protocolo simple de transporte de correo) con la clase SmtpClient
Un cliente SMTP es un cliente de un servidor SMTP, servidor de envío de correo. La clase .NET SmtpClient encapsula por completo las necesidades de dicho cliente. El desarrollador no tiene por qué conocer los detalles del protocolo SMTP. Nosotros sí los conocemos. Se ha presentado en el apartado 11.4.3.
Presentamos la clase SmtpClient en el marco de una aplicación básica de Windows que permite enviar correos electrónicos con archivos adjuntos. La aplicación se conectará al puerto 25 de un servidor SMTP. Recordamos que, en la mayoría de los sistemas Windows, los cortafuegos u otros antivirus bloquean las conexiones al puerto 25. Por lo tanto, es necesario desactivar esta protección para probar la aplicación:
![]() |
El cliente SMTP tendrá una arquitectura de una sola capa:
![]() |
El proyecto de Visual Studio es el siguiente:
![]() |
La interfaz gráfica de usuario [SendMailForm.cs] de la aplicación es la siguiente:
![]() |
n.º | tipo | nombre | función |
1 | TextBox | textBoxServeur | nombre del servidor SMTP al que conectarse |
2 | NumericUpDown | numericUpDownPort | el puerto al que conectarse |
3 | TextBox | textBoxExpediteur | dirección del remitente del mensaje |
4 | TextBox | textBoxTo | direcciones de los destinatarios en el formato: dirección1, dirección2, ... |
5 | TextBox | textBoxCc | direcciones de los destinatarios en copia (CC = Carbon Copy) en el formato: dirección1, dirección2, ... |
6 | TextBox | textBoxBcc | direcciones de los destinatarios en copia oculta (BCC = Blind Carbon Copy) en el formato: dirección1, dirección2, ... Todas las direcciones de estos tres campos de entrada recibirán el mismo mensaje con los mismos archivos adjuntos. Los destinatarios del mensaje podrán ver las direcciones que figuraban en los campos 4 y 5, pero no las del campo 6. Por lo tanto, la CCO es una forma de incluir a alguien en copia sin que los demás destinatarios del mensaje lo sepan. |
7 | Botón | buttonAjouter | para añadir un archivo adjunto al correo |
8 | ListBox | listBoxPiecesJointes | lista de archivos que se deben adjuntar al correo |
9 | TextBox | textBoxSujet | Asunto de la carta |
10 | TextBox | textBoxMessage | El texto del mensaje. MultiLine=true |
11 | Botón | buttonEnvoyer | para enviar el mensaje y los posibles archivos adjuntos |
12 | TextBox | textBoxRésultat | muestra un resumen del mensaje enviado o un mensaje de error si se ha producido algún problema |
13 | Botón | buttonEffacer | para borrar [12] |
OpenfileDialog | openFileDialog1 | control no visual que permite seleccionar un archivo adjunto en el sistema de archivos local |
En el ejemplo anterior, el resumen que se muestra en [12] es el siguiente:
Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc :
Bcc :
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,
Vous trouverez ci-joint le dossier de candidature à l'ISTIA.
Cordialement,
ST
El código del formulario [SendMailForm.cs] es el siguiente:
using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;
namespace Chap9 {
public partial class SendMailForm : Form {
public SendMailForm() {
InitializeComponent();
}
// Añadir un archivo adjunto
private void buttonAjouter_Click(object sender, EventArgs e) {
// Configuración del cuadro de diálogo openfileDialog1
openFileDialog1.InitialDirectory = Application.ExecutablePath;
openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
openFileDialog1.FilterIndex = 0;
openFileDialog1.FileName = "";
// se muestra el cuadro de diálogo y se obtiene su resultado
if (openFileDialog1.ShowDialog() == DialogResult.OK) {
// se recupera el nombre del archivo
listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
}
}
private void textBoxServeur_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void setStatutEnvoyer() {
buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
}
// eliminar un archivo adjunto
private void buttonRetirer_Click(object sender, EventArgs e) {
// ¿Archivo adjunto seleccionado?
if (listBoxPiecesJointes.SelectedIndex != -1) {
// se elimina
listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
// se actualiza el botón «Eliminar»
buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
}
}
private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
// ¿Archivo adjunto seleccionado?
if (listBoxPiecesJointes.SelectedIndex != -1) {
// se actualiza el botón «Eliminar»
buttonRetirer.Enabled = true;
}
}
// envío del mensaje con sus archivos adjuntos
private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
}
private void textBoxTo_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void textBoxSujet_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void buttonEffacer_Click(object sender, EventArgs e) {
textBoxResultat.Text = "";
}
}
}
No comentaremos este código, ya que no presenta novedades. Para comprender el método buttonAjouter_Click de la línea 14, se invita al lector a volver a leer el apartado 7.5.1.
El método buttonEnvoyer_Click de la línea 55, que envía el correo, es el siguiente:
private void buttonEnvoyer_Click(object sender, EventArgs e) {
try {
// reloj de arena
Cursor = Cursors.WaitCursor;
// el cliente SMTP
SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
// el mensaje
MailMessage message = new MailMessage();
// remitente
message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
message.From = message.Sender;
// destinatarios
Regex marqueur = new Regex("\\s*,\\s*");
string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
foreach (string destinataire in destinataires) {
if (destinataire.Trim() != "") {
message.To.Add(new MailAddress(destinataire));
}
}
// CC
string[] copies = marqueur.Split(textBoxCc.Text.Trim());
foreach (string copie in copies) {
if (copie.Trim() != "") {
message.CC.Add(new MailAddress(copie));
}
}
// BCC
string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
foreach (string blindCopie in blindCopies) {
if (blindCopie.Trim() != "") {
message.Bcc.Add(new MailAddress(blindCopie));
}
}
// asunto
message.Subject = textBoxSujet.Text.Trim();
// texto del mensaje
message.Body = textBoxMessage.Text;
// archivos adjuntos
foreach (string attachement in listBoxPiecesJointes.Items) {
message.Attachments.Add(new Attachment(attachement));
}
// envío del mensaje
smtpClient.Send(message);
// Vale: se muestra un resumen
StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
textBoxSujet.Clear();
msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
textBoxTo.Clear();
msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
textBoxCc.Clear();
msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
textBoxBcc.Clear();
msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
foreach (string attachement in listBoxPiecesJointes.Items) {
msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
}
msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
listBoxPiecesJointes.Items.Clear();
textBoxResultat.Text = msg.ToString();
} catch (Exception ex) {
// se muestra el error
textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
}
// cursor normal
Cursor = Cursors.Arrow;
}
- línea 6: se crea el cliente SMTP. Necesita dos parámetros: el nombre del servidor SMTP y el puerto en el que opera
- línea 8: se crea un mensaje de tipo MailMessage. Este es el que encapsulará la totalidad del mensaje que se va a enviar.
- línea 10: se introduce la dirección de correo electrónico «Sender» del remitente. Una dirección de correo electrónico es una instancia del tipo MailAddress construida a partir de una cadena de caracteres «xx@yy.zz». Esta cadena debe tener el formato esperado para una dirección de correo electrónico; de lo contrario, se lanza una excepción. En ese caso, se mostrará en el campo textBoxResultat (línea 63) en un formato poco intuitivo.
- líneas 13-19: las direcciones de correo electrónico de los destinatarios se colocan en la lista «Para» del mensaje. Estas direcciones se recuperan del campo textBoxTo. La expresión regular de la línea 13 permite recuperar las diferentes direcciones, que están separadas por una coma.
- líneas 21-26: se repite el mismo proceso para inicializar el campo CC del mensaje con las direcciones en copia del campo textBoxCc.
- líneas 28-33: se repite el mismo proceso para inicializar el campo «Cco» del mensaje con las direcciones en copia oculta del campo textBoxBcc.
- línea 35: el campo «Subject» del mensaje se inicializa con el asunto del campo textBoxSujet.
- línea 37: el campo «Body» del mensaje se inicializa con el texto del mensaje textBoxMessage.
- Líneas 39-41: los archivos adjuntos se añaden al mensaje. Cada archivo adjunto se añade como un objeto «Attachment» al campo «Attachments» del mensaje. Se crea un objeto «Attachment» a partir de la ruta completa del archivo que se va a adjuntar en el sistema de archivos local.
- línea 43: el mensaje se envía mediante el método Send del cliente SMTP.
- líneas 45-60: se escribe el resumen del envío en el campo textBoxResultat y se reinicia el formulario.
- línea 63: visualización de cualquier error
11.8. Un cliente TCP genérico asíncrono
11.8.1. Introducción
En todos los ejemplos de este capítulo, la comunicación cliente/servidor se realizaba en modo bloqueante, también denominado modo síncrono:
- cuando un cliente se conecta a un servidor, espera la respuesta del servidor a dicha solicitud antes de continuar.
- Cuando un cliente lee una línea de texto enviada por el servidor, queda bloqueado hasta que el servidor la haya enviado.
- Por parte del servidor, los hilos de servicio que atienden al cliente funcionan de la misma manera que se ha descrito anteriormente.
En las interfaces gráficas, a menudo es necesario evitar que el usuario quede bloqueado durante operaciones largas. El caso que se suele citar es el de la descarga de un archivo de gran tamaño. Durante dicha descarga, hay que permitir que el usuario siga interactuando libremente con la interfaz gráfica.
Nos proponemos aquí reescribir el cliente TCP genérico del apartado 11.6.3 introduciendo los siguientes cambios:
- la interfaz será gráfica
- la herramienta de comunicación con el servidor será un objeto Socket
- el modo de comunicación será asíncrono:
- el cliente iniciará una conexión con el servidor, pero no se quedará bloqueado esperando a que se establezca
- el cliente iniciará un envío al servidor, pero no se quedará bloqueado esperando a que finalice
- el cliente iniciará la recepción de datos procedentes del servidor, pero no permanecerá bloqueado a la espera de que esta finalice.
Recordemos en qué nivel se sitúa el objeto Socket en la comunicación cliente/servidor TCP:
![]() |
La clase Socket es la que opera más cerca de la red. Permite gestionar con precisión la conexión de red. El término socket hace referencia a una toma de corriente. El término se ha ampliado para designar una toma de red de software. En una comunicación TCP-IP entre dos máquinas A y B, son dos sockets los que se comunican entre sí. Una aplicación puede trabajar directamente con los sockets. Este es el caso de la aplicación A mencionada anteriormente. Un socket puede ser un socket client o serveur.
11.8.2. La interfaz gráfica del cliente TCP asíncrono
La aplicación de Visual Studio es la siguiente:
![]() |
[ClientTcpAsynchrone.cs] es la interfaz gráfica. Esta es la siguiente:
![]() |
n.º | tipo | nombre | función |
1 | TextBox | textBoxNomServeur | nombre del servidor TCP al que conectarse |
2 | NumericUpDown | numericUpDownPortServeur | el puerto al que conectarse |
3 | RadioButton | radioButtonLF radioButtonRCLF | para indicar el carácter de fin de línea que debe utilizar el cliente: LF «\n» o RCLF «\r\n» |
4 | Botón | buttonConnexion | para conectarse al puerto [2] del servidor [1]. El botón muestra el texto [Connecter] cuando el cliente no está conectado a ningún servidor, y [Déconnecter] cuando sí lo está. |
5 | TextBox | textBoxMsgToServeur | mensaje que se enviará al servidor una vez establecida la conexión. Cuando el usuario pulsa la tecla [Entrée], el mensaje se envía con el carácter de fin de línea seleccionado en [3] |
6 | ListBox | listBoxEvts | Lista en la que se muestran los principales eventos de la conexión cliente/servidor: conexión, desconexión, cierre de flujo, errores de comunicación |
7 | ListBox | listBoxDialogue | Lista en la que se muestran los mensajes del diálogo cliente/servidor |
8 | Botón | buttonRazEvts | Para borrar la lista [6] |
4 | Botón | buttonRazDialogue | para borrar la lista [7] |
Los principios de funcionamiento de esta interfaz son los siguientes:
- el usuario conecta su cliente TCP gráfico a un servicio TCP mediante [1, 2, 3, 4].
- Un hilo asíncrono acepta de forma continua todos los datos enviados por el servidor TCP y los muestra en la lista [7]. Este hilo está independiente de las demás actividades de la interfaz.
- El usuario puede enviar mensajes al servidor a su propio ritmo mediante [5]. Cada mensaje se envía mediante un hilo asíncrono. A diferencia del hilo de recepción, que nunca se detiene, el hilo de envío finaliza en cuanto se ha enviado el mensaje. Se utilizará un nuevo hilo asíncrono para el siguiente mensaje.
- La comunicación cliente/servidor finaliza cuando uno de los interlocutores cierra la conexión. El usuario puede tomar esta iniciativa con el botón [4], que, una vez establecida la conexión, pasa a denominarse [Déconnecter].
A continuación se muestra una captura de pantalla de una ejecución:
![]() |
- en [1]: conexión a un servicio POP
- en [2]: visualización de los eventos que han tenido lugar durante la conexión
- en [3]: el mensaje enviado por el servidor POP al finalizar la conexión
- en [4]: el botón [Connecter] se ha convertido en el botón [Déconnecter]
![]() |
- en [1], se envió el comando quit al servidor POP. El servidor respondió +OK goodbye y cerró la conexión
- en [2], se detectó este cierre por parte del servidor. A continuación, el cliente cerró la conexión por su parte.
- En [3], el botón [Déconnecter] volvió a convertirse en un botón [Connecter]
11.8.3. Conexión asíncrona al servidor
Al pulsar el botón [Connecter] se ejecuta el siguiente método:
private void buttonConnexion_Click(object sender, EventArgs e) {
// ¿Conectar o desconectar?
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- línea 3: el botón puede tener el texto [Connecter] o [Déconnecter].
El método de conexión es el siguiente:
using System.Net.Sockets;
...
namespace Chap9 {
public partial class ClientTcp : Form {
const int tailleBuffer = 1024;
private Socket client = null;
private byte[] data = new byte[tailleBuffer];
private string réponse = null;
private string finLigne = "\r\n";
// Delegados
public delegate void writeLog(string log);
public ClientTcp() {
InitializeComponent();
}
....................................
private void connexion() {
// comprobaciones de datos
string nomServeur = textBoxNomServeur.Text.Trim();
if (nomServeur == "") {
logEvent("indiquez le nom du serveur");
return;
}
// seguimiento
logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
try {
// creación de socket
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// conexión asíncrona
client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);
} catch (Exception ex) {
logEvent(String.Format("erreur de connexion : {0}", ex.Message));
return;
}
}
// se ha establecido la conexión
private void connecté(IAsyncResult résultat) {
// se recupera el socket del cliente
Socket client = résultat.AsyncState as Socket;
...
}
// seguimiento del proceso
private void logEvent(string msg) {
....
}
}
}
- línea 1: la clase Socket forma parte del espacio de nombres System.Net.Sockets.
Hay una serie de datos que deben compartirse entre varios métodos del formulario. Son los siguientes:
- línea 7: «client» es el socket de comunicación con el servidor
- líneas 6 y 8: el cliente recibirá sus mensajes en un array de bytes llamado «data».
- línea 9: «respuesta» es la respuesta enviada por el servidor.
- línea 10: «finLigne» es el marcador de fin de línea utilizado por el cliente TCP; se inicializa por defecto en «RCLF», pero el usuario puede modificarlo mediante los botones de opción «[3]».
El procedimiento connexion de la línea 19 establece la conexión con el servidor TCP:
- líneas 21-25: se comprueba que el nombre del servidor no esté vacío. Si no es así, el evento se registra en listBoxEvts mediante el método logEvent de la línea 49.
- línea 27: se notifica que la conexión va a tener lugar
- línea 30: se crea el objeto Socket necesario para la comunicación TCP/IP. El constructor admite tres parámetros:
- AddressFamily addressFamily: la familia de direcciones IP del cliente y del servidor; en este caso, las direcciones IPv4 (AddressFamily.InterNetwork)
- SocketType socketType: el tipo de socket. El tipo SocketType.Stream es adecuado para conexiones TCP/IP
- ProtocolType protocolType: el tipo de protocolo de Internet utilizado; en este caso, el protocolo TCP
- línea 32: la conexión se establece de forma asíncrona. Se inicia la conexión, pero la ejecución continúa sin esperar a que esta finalice. El método [Socket].BeginConnect admite cuatro parámetros:
- IPAddress ipAddress: la dirección IP del equipo en el que se ejecuta el servicio al que hay que conectarse
- Int32 port: el puerto del servicio
- AsyncCallBack asyncCallBack: AsyncCallBack es un tipo delegado:
El método asyncCallBack, que se pasa como tercer parámetro del método BeginConnect, debe ser un método que acepte un tipo IAsyncCallBack y que no devuelva ningún resultado. Este es el método al que se llamará una vez establecida la conexión. Aquí pasamos como tercer parámetro el método connecté de la línea 41.
- (continuación)
- Objeto state: un objeto que se debe pasar al método asyncCallBack. Este método recibe (véase el delegado anterior) un parámetro ar de tipo IAsyncResult. El objeto state se podrá recuperar en ar.AsyncState (línea 43). Aquí pasamos como cuarto parámetro el socket del cliente.
- línea 38: el método ha finalizado. El usuario puede volver a interactuar con la interfaz gráfica. La conexión se realiza en segundo plano, en paralelo a la gestión de los eventos de la interfaz gráfica. También en paralelo, el método connecté de la línea 41 se llamará al final de la conexión, independientemente de si esta finaliza correctamente o no.
El código del método connecté es el siguiente:
// se ha establecido la conexión
private void connecté(IAsyncResult résultat) {
// se recupera el socket del cliente
Socket client = résultat.AsyncState as Socket;
try {
// se finaliza la operación asíncrona
client.EndConnect(résultat);
// seguimiento
logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
// formulario
buttonConnexion.Text = "Déconnecter";
// lectura asíncrona de datos procedentes del servidor
réponse = "";
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
} catch (SocketException e) {
logEvent(String.Format("erreur de connexion : {0}", e.Message));
return;
}
}
// recepción de datos
private void lecture(IAsyncResult résultat) {
// se recupera el socket del cliente
Socket client = résultat.AsyncState as Socket;
...
}
- línea 4: el socket del cliente se recupera del parámetro résultat recibido por el método. Recordemos que este objeto es el que se pasa como cuarto parámetro del método BeginConnect.
- línea 7: el intento de conexión se finaliza mediante el método EndConnect, al que se debe pasar el parámetro résultat recibido por el método.
- línea 9: el evento se registra en la lista de eventos
- línea 11: el botón [Connecter] se convierte en un botón [Déconnecter] para que el usuario pueda solicitar la desconexión.
- línea 13: se inicializa la respuesta del servidor. Se actualizará mediante llamadas repetidas al método asíncrono BeginReceive.
- línea 14: primera llamada al método asíncrono BeginReceive. Este se invoca con los siguientes parámetros:
- byte[] buffer: el búfer en el que se colocarán los datos que se van a recibir; en este caso, el búfer es data
- int offset: a partir de qué posición del búfer se colocarán los datos que se van a recibir; en este caso, el desplazamiento es 0, c.a.d, lo que significa que los datos se colocan a partir del primer byte del búfer.
- int size: el tamaño en bytes del búfer; en este caso, el tamaño es tailleBuffer.
- SocketFlags socketFlags: configuración del socket; en este caso, no hay configuración
- AsyncCallBack asyncCallBack: el método de llamada de retorno que se ejecutará cuando finalice la recepción. Esto ocurrirá bien porque el búfer haya recibido datos, bien porque se haya cerrado la conexión. En este caso, el método de llamada de retorno es el método lecture de la línea 22.
- Objeto state: el objeto que se debe pasar al método de llamada de retorno asyncCallBack. En este caso, se vuelve a pasar el socket del cliente.
Cabe señalar que todo esto ocurre sin que el usuario tenga que hacer nada, salvo la solicitud inicial de conexión mediante el botón [Connecter]. Al final del método connecté, se ejecuta otro método en segundo plano: el método lecture, que vamos a examinar ahora.
// recepción de datos
private void lecture(IAsyncResult résultat) {
// se recupera el socket del cliente
Socket client = résultat.AsyncState as Socket;
int nbOctetsReçus = 0;
bool erreur = false;
try {
// número de bytes recibidos
nbOctetsReçus = client.EndReceive(résultat);
if (nbOctetsReçus == 0) {
// el servidor ya no responde
logEvent("le serveur a fermé la connexion");
}
} catch (Exception e) {
// se ha producido un problema de recepción
logEvent(String.Format("erreur de réception : {0}", e.Message));
erreur = true;
}
// ¿Terminado?
if (nbOctetsReçus == 0 || erreur) {
// Se desconecta al cliente si es necesario
déconnexion();
// se muestra el final de la respuesta
afficherRéponseServeur(réponse, true);
// fin de lectura
return;
}
// se recogen los datos recibidos
string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
// se añaden a los datos ya recibidos
réponse += données;
// se muestra la respuesta
afficherRéponseServeur(réponse, false);
// se sigue leyendo
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
}
- línea 2: el método lecture se ejecuta en segundo plano cuando el búfer data ha recibido datos o cuando el servidor ha cerrado la conexión.
- línea 9: la solicitud asíncrona de lectura finaliza mediante EndReceive. Una vez más, este método debe invocarse con el parámetro recibido por la función de devolución de llamada. El método EndReceive devuelve el número de bytes recibidos en el búfer de lectura.
- línea 10: si el número de bytes es cero, significa que el servidor ha cerrado la conexión.
- línea 12: se registra el evento en la lista de eventos
- línea 14: se gestiona cualquier excepción que pueda producirse
- líneas 16-17: se registra el evento en la lista de eventos y se registra el error
- línea 20: se comprueba si hay que cerrar la conexión
- línea 22: se cierra la conexión por parte del cliente con un método déconnexion que veremos más adelante.
- línea 24: la respuesta del servidor, c.a.d. La variable global réponse se muestra en la lista de diálogo listBoxDialogue mediante un método privado afficherRéponseServeur.
- línea 26: fin del método asíncrono lecture
- línea 29: los bytes recibidos se colocan en una cadena de caracteres con el formato UTF8.
- línea 31: se añaden a la respuesta que se está construyendo
- línea 33: la respuesta se muestra en la lista listBoxDialogue.
- línea 35: se vuelve a esperar a recibir datos del servidor
En definitiva, el método asíncrono lecture nunca se detiene. De forma continua, lee los datos procedentes del servidor y los muestra en la lista listBoxDialogue. Solo se detiene cuando se cierra la conexión, ya sea por parte del servidor o por parte del propio usuario.
11.8.4. Desconexión del servidor
Al pulsar el botón [Déconnecter] se ejecuta el siguiente método:
private void buttonConnexion_Click(object sender, EventArgs e) {
// ¿Conexión o desconexión?
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- línea 3: el botón puede tener el texto [Connecter] o [Déconnecter].
El método déconnexion se encarga de desconectar al cliente:
private void déconnexion() {
// cierre del socket
if (client != null && client.Connected) {
try {
// seguimiento
logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
// desconexión
client.Shutdown(SocketShutdown.Both);
client.Close();
// formulario
buttonConnexion.Text = "Connecter";
} catch (Exception ex) {
// seguimiento
logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
}
}
}
- línea 3: si el cliente existe y está conectado
- línea 6: se notifica la desconexión en listBoxEvts. La propiedad client.RemoteEndPoint proporciona el par (dirección IP, puerto) del otro extremo de la conexión; en este caso, c.a.d corresponde al servidor.
- línea 8: el flujo de datos del socket se cierra con el método ShutDown. El flujo de datos de un socket es bidireccional: el socket envía y recibe datos. El parámetro del método ShutDown puede ser: ShutDown.Receive para cerrar el flujo de recepción, Shutdonw.Send para cerrar el flujo de transmisión o ShutDown.Both para cerrar ambos flujos.
- línea 9: se liberan los recursos asociados al socket
- línea 11: el botón [Déconnecter] pasa a ser el botón [Connecter]
- líneas 12-15: gestión de una posible excepción
11.8.5. Envío asíncrono de datos al servidor
Cuando el usuario valida el mensaje del campo textBoxMsgToServeur, se ejecuta el siguiente método:
private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
// ¿tecla [Entrée]?
if (e.KeyChar == 13 && client.Connected) {
envoyerMessage();
}
}
- líneas 3-5: si el usuario ha pulsado la tecla [Entrée] y el socket del cliente está conectado, se envía el mensaje del campo textBoxMsgToServeur mediante el método envoyerMessage.
El método envoyerMessage es el siguiente:
private void envoyerMessage() {
// enviar un mensaje de forma asíncrona
// el mensaje
byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
// se envía
client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
// diálogo
logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
// borrar mensaje
textBoxMsgToServeur.Clear();
}
- línea 4: se añade al mensaje el marcador de fin de línea del cliente y se coloca en la matriz de bytes message.
- línea 6: se inicia una emisión asíncrona con el método BeginSend. Los parámetros de BeginSend son idénticos a los del método BeginReceive. Al finalizar la operación de envío asíncrono del mensaje, se llamará al método écriture.
- línea 8: el mensaje enviado se añade a la lista listBoxDialogue para poder realizar un seguimiento del diálogo cliente/servidor
- línea 10: el mensaje enviado se elimina de la interfaz gráfica
El método de devolución de llamada écriture es el siguiente:
private void écriture(IAsyncResult résultat) {
// resultado del envío de un mensaje
Socket client = résultat.AsyncState as Socket;
try {
client.EndSend(résultat);
} catch (Exception e) {
// se ha producido un problema en el envío
logEvent(String.Format("erreur d'émission : {0}", e.Message));
}
}
- línea 4: el método de devolución de llamada écriture recibe un parámetro de resultado de tipo IAsyncResult.
- línea 3: en el parámetro résultat, se recupera el socket del cliente. Este socket era el quinto parámetro del método BeginSend.
- Línea 5: se finaliza la operación asíncrona de envío.
No se espera a que finalice el envío de un mensaje para devolver el control al usuario. De este modo, el usuario puede enviar un segundo mensaje aunque el envío del primero aún no haya finalizado.
11.8.6. Visualización de los eventos y del diálogo cliente/servidor
Los eventos se muestran mediante el método logEvents:
// seguimiento del proceso
private void logEvent(string msg) {
listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
}
private void logEventCallBack(string msg) {
// visualización del mensaje
msg = msg.Replace(finLigne, " ");
listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
- línea 2: el método logEvents recibe como parámetro el mensaje que se va a añadir a la lista listBoxEvts.
- línea 3: no se puede utilizar directamente el componente listBoxEvents. De hecho, el método logEvents es invocado por dos tipos de subprocesos:
- el hilo principal propietario de la interfaz gráfica, por ejemplo, cuando indica que se está realizando un intento de conexión
- un hilo secundario que realiza una operación asíncrona. Este tipo de hilo no es propietario de los componentes y su acceso a un componente C debe controlarse mediante una operación C.Invoke. Esta operación indica al control C que un hilo desea realizar una operación sobre él. El método Invoke admite dos parámetros:
- una función de devolución de llamada de tipo delegate. Esta función de devolución de llamada será ejecutada por el hilo propietario de la interfaz gráfica y no por el hilo que ejecuta el método C.Invoke.
- un objeto que se pasará a la función de devolución de llamada.
En este caso, el primer parámetro que se pasa al método Invoke es una instancia del siguiente delegado:
public delegate void writeLog(string log);
El delegado writeLog tiene un parámetro de tipo string y no devuelve ningún resultado. El parámetro será el mensaje que se va a registrar en listBoxEvts.
En la línea 3, el primer parámetro pasado al método Invoke es el método logEventCallBack de la línea 6. Este se corresponde efectivamente con la firma del delegado writeLog. El segundo parámetro que se pasa al método Invoke es el mensaje que se pasará como parámetro al método logEventCallBack.
La operación Invoke es una operación síncrona. La ejecución del hilo secundario queda bloqueada hasta que el hilo propietario del control ejecute el método de devolución de llamada.
- línea 6: el método de devolución ejecutado por el hilo de la interfaz gráfica recibe el mensaje que se va a mostrar en el control listBoxEvts.
- línea 9: el evento se registra en la primera posición de la lista para que los eventos más recientes aparezcan en la parte superior de la misma.
Los mensajes del diálogo cliente/servidor se muestran mediante el método logDialogue:
// seguimiento del diálogo
private void logDialogue(string msg) {
listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
}
private void logDialogueCallBack(string msg) {
// visualización del mensaje
msg = msg.Replace(finLigne, " ");
listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
El principio es el mismo que en el método logEvent.
Los mensajes recibidos por el cliente se muestran mediante el método afficherRéponseServeur:
private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}
El primer parámetro es el mensaje que se va a mostrar. Este mensaje puede estar formado por varias líneas. De hecho, el cliente lee los datos procedentes del servidor en bloques de tailleBuffer (1024) bytes. En estos 1024 bytes pueden encontrarse varias líneas, que se reconocen por su marcador de fin de línea «\n». La última línea puede estar incompleta, ya que su carácter de fin de línea se encuentra en los 1024 bytes siguientes. El método busca en el mensaje las líneas que terminan en «\n» y, a continuación, solicita a logDialogue que las muestre. El segundo parámetro del método indica si hay que mostrar la última línea encontrada o dejarla en el búfer para que se complete con el mensaje siguiente. El código es bastante complejo y no reviste interés en este contexto. Por lo tanto, no se comentará.
11.8.7. Conclusión
El mismo ejemplo podría tratarse con operaciones sincrónicas. En este caso, el carácter asíncrono de la interfaz gráfica aporta poco al usuario. No obstante, si el usuario se conecta y luego se da cuenta de que el servidor «ya no responde», tiene la posibilidad de desconectarse gracias a que la interfaz gráfica sigue respondiendo a los eventos mientras se ejecutan las operaciones asíncronas. Este ejemplo, bastante complejo, nos ha permitido presentar nuevos conceptos:
- el uso de sockets
- el uso de métodos asíncronos. Lo que se ha visto forma parte de un estándar. Existen otros métodos asíncronos que funcionan según el mismo modelo.
- la actualización de los controles de una interfaz gráfica mediante subprocesos secundarios.
La comunicación TCP/IP asíncrona presenta ventajas más significativas para un servidor que las mostradas en el ejemplo anterior. Sabemos que el servidor atiende a sus clientes mediante subprocesos secundarios. Si su grupo de subprocesos tiene N subprocesos, esto significa que solo puede atender a N clientes simultáneamente. Si los N subprocesos realizan todos una operación bloqueante (sincrónica), no quedan subprocesos disponibles para un nuevo cliente hasta que una de las operaciones bloqueantes finalice y libere un subproceso. Si en los subprocesos se realizan operaciones asíncronas en lugar de sincrónicas, un subproceso nunca queda bloqueado y puede reutilizarse rápidamente para nuevos clientes.
11.9. Aplicación de ejemplo, versión 8: Servidor de cálculo de impuestos
11.9.1. La arquitectura de la nueva versión
Retomamos la aplicación de cálculo de impuestos que ya hemos tratado en diversas formas. Recordemos su última versión, la de la versión 7 del apartado 9.8.
![]() |
Los datos se encontraban en una base de datos y la capa [ui] era una interfaz gráfica:
![]() |
Vamos a retomar esta arquitectura y distribuirla en dos máquinas:
![]() |
- una máquina [serveur] alojará las capas [metier] y [dao] de la versión 7. Se creará una capa TCP/IP [serveur] [1] para permitir que los usuarios de Internet consulten el servicio de cálculo de impuestos.
- Una máquina [client] alojará la capa [ui] de la versión 7. Se creará una capa TCP/IP [client] [2] para permitir que la capa [ui] consulte el servicio de cálculo de impuestos.
La arquitectura cambia profundamente en este punto. La versión 7 era una aplicación de Windows para un solo usuario. La versión 8 se convierte en una aplicación cliente/servidor de Internet. El servidor podrá atender a varios clientes simultáneamente.
En primer lugar, vamos a escribir la parte [serveur] de la aplicación.
11.9.2. El servidor de cálculo de impuestos
11.9.2.1. El proyecto de Visual Studio
![]() |
El proyecto de Visual Studio será el siguiente:
![]() |
- en [1], el proyecto. En él se encuentran los siguientes elementos:
- [ServeurImpot.cs]: el servidor TCP/IP para el cálculo de impuestos en forma de aplicación de consola.
- [dbimpots.sdf]: la base de datos SQL Server Compact de la versión 7, descrita en el apartado 9.8.5.
- [App.config]: el archivo de configuración de la aplicación.
- En [2], la carpeta [lib] contiene los archivos DLL necesarios para el proyecto:
- [ImpotsV7-dao]: la capa [dao] de la versión 7
- [ImpotsV7-metier]: la capa [metier] de la versión 7
- [antlr.runtime, CommonLogging, Spring.Core] para Spring
- en [3], las referencias del proyecto
11.9.2.2. Configuración de la aplicación
El archivo [App.config] es utilizado por Spring. Su contenido es el siguiente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
<constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
<constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
<constructor-arg index="2" value="select data1, data2, data3 from data"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- líneas 16-20: configuración de la capa [dao] asociada a la base SQL Server compact
- líneas 21-23: configuración de la capa [metier].
Este es el archivo de configuración utilizado en la capa [ui] de la versión 7. Se ha presentado en el apartado 9.8.4.
11.9.2.3. Funcionamiento del servidor
Al iniciar el servidor, la aplicación del servidor crea instancias de las capas [metier] y [dao] y, a continuación, muestra una interfaz de consola de administración:
La consola de administración admite los siguientes comandos:
para iniciar el servicio en un puerto determinado | |
para detener el servicio. Posteriormente, se puede reiniciar en el mismo puerto o en otro. | |
para activar el eco del diálogo cliente/servidor en la consola | |
para desactivar el eco | |
para mostrar el estado activo o inactivo del servicio | |
para salir de la aplicación |
Iniciemos el servidor:
Ahora vamos a ejecutar el cliente TCP gráfico asíncrono que hemos analizado anteriormente en el apartado 11.8.

El cliente está conectado. Puede enviar los siguientes comandos al servidor de cálculo de impuestos:
para obtener la lista de comandos permitidos | |
para calcular los impuestos de una persona que tiene nbEnfants hijos y un salario de salaireAnnuel euros. marié es igual a 0 si la persona está casada, y a n en caso contrario. | |
para cerrar la conexión con el servidor |
A continuación se muestra un ejemplo de diálogo:
![]() |
En el lado del servidor, la consola muestra lo siguiente:
Activemos el eco y reiniciemos un nuevo diálogo desde el cliente gráfico:
![]() |
La consola de administración muestra entonces lo siguiente:
- línea 1: el eco del diálogo cliente/servidor está activado
- línea 2: ha llegado un cliente
- línea 3: ha enviado el comando [aide]
- líneas 4-7: la respuesta del servidor en 4 líneas.
Detengamos el servicio:
- línea 1: se solicita la detención del servicio (no de la propia aplicación)
- línea 2: se produce una excepción debido a que el servidor, bloqueado en una espera de cliente, se ha interrumpido bruscamente a causa del cierre del servicio de escucha.
- línea 3: el servicio puede reiniciarse ahora mediante «start port» o detenerse mediante quit.
Antes de que se detuviera el servicio de escucha, se estaba atendiendo a un cliente en otra conexión. Esta conexión no se cierra al cerrar el socket de escucha. El cliente puede seguir enviando comandos: el hilo de servicio que se le había asignado antes del cierre del servicio de escucha sigue respondiéndole:

11.9.3. El código del servidor TCP para el cálculo de impuestos
![]() |
1 ![]() |
El código del servidor [ServeurImpot.cs] es el siguiente:
...
namespace Chap9 {
public class ServeurImpot {
// Datos compartidos entre subprocesos y métodos
private static IImpotMetier metier = null;
private static int port;
private static TcpListener service;
private static bool actif = false;
private static bool echo = false;
// programa principal
public static void Main(string[] args) {
// instancias de las capas [metier] y [dao]
IApplicationContext ctx = null;
metier = null;
try {
// contexto de Spring
ctx = ContextRegistry.GetContext();
// se solicita una referencia en la capa [metier]
metier = (IImpotMetier)ctx.GetObject("metier");
// configuración del grupo de subprocesos
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// lee los comandos de administración del servidor introducidos mediante el teclado en un bucle infinito
string commande = null;
string[] champs = null;
while (true) {
// solicita
Console.Write("Serveur de calcul d'impôt >");
// lectura del comando
commande = Console.ReadLine().Trim().ToLower();
champs = Regex.Split(commande, @"\s+");
// ejecución del comando
switch (champs[0]) {
case "start":
// ¿activo?
if (actif) {
//error
Console.WriteLine("Le serveur est déjà actif");
} else {
// comprobación del puerto
if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
Console.WriteLine("Syntaxe : start port. Port incorrect");
} else {
// se inicia el servicio de escucha
ThreadPool.QueueUserWorkItem(doEcoute, null);
}
}
break;
case "echo":
// echo start / stop
if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
Console.WriteLine("Syntaxe : echo start / stop");
} else {
echo = champs[1] == "start";
}
break;
case "stop":
// fin del servicio
if (actif) {
service.Stop();
actif = false;
}
break;
case "status":
// estado del servidor
if (actif) {
Console.WriteLine("Le service est lancé sur le port {0}", port);
} else {
Console.WriteLine("Le service n'est pas lancé}");
}
break;
case "quit":
// salida de la aplicación
Console.WriteLine("Fin du service");
Environment.Exit(0);
break;
default:
// comando incorrecto
Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
break;
}
}
} catch (Exception e1) {
// visualización de una excepción
Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
return;
}
}
private static void doEcoute(Object data) {
...
}
....
}
}
- líneas 18-21: las capas [metier] y [dao] son instanciadas por Spring, configurado mediante [App.config]. A continuación, se inicializa la variable global metier de la línea 6.
- líneas 24-25: se configura el grupo de subprocesos de la aplicación con un mínimo y un máximo de 10 subprocesos.
- líneas 30-86: el bucle de introducción de los comandos de administración del servicio (start, stop, quit, echo, status).
- línea 32: indicador del servidor para cada nuevo comando
- línea 34: lectura del comando de administración
- línea 35: el comando se divide en campos para su análisis
- líneas 38-52: el comando «start port», cuyo objetivo es iniciar el servicio de escucha
- línea 40: si el servicio ya está activo, no hay nada que hacer
- línea 45: se comprueba que el puerto existe y es correcto. Si es así, se establece la variable global port de la línea 7.
- línea 49: el servicio de escucha se gestionará mediante un hilo secundario para que el hilo principal pueda seguir ejecutando los comandos de la consola. Si el método doEcoute establece la conexión con éxito, se inicializan las variables globales service de la línea 8 y actif de la línea 9.
- líneas 53-60: el comando echo start / stop, que activa o desactiva el eco del diálogo cliente/servidor en la consola
- línea 58: se establece la variable global echo de la línea 7
- líneas 61-67: el comando stop, que detiene el servicio de escucha.
- línea 64: parada del servicio de escucha
- líneas 68-75: el comando status, que muestra el estado activo o inactivo del servicio
- líneas 76-80: el comando quit, que lo detiene todo.
El hilo encargado de escuchar las solicitudes de los clientes ejecuta el siguiente método doEcoute:
private static void doEcoute(Object data) {
// hilo de escucha de solicitudes de los clientes
try {
// se crea el servicio
service = new TcpListener(IPAddress.Any, port);
// se inicia
service.Start();
// el servidor está activo
actif = true;
// seguimiento
Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
// bucle de servicio a los clientes
TcpClient tcpClient = null;
// n.º de cliente
int numClient = 0;
// bucle infinito
while (true) {
// a la espera de un cliente
tcpClient = service.AcceptTcpClient();
// el servicio lo presta otra tarea
ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// siguiente cliente
numClient++;
}
} catch (Exception ex) {
// se notifica el error
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
}
}
// información del cliente
internal class Client {
public TcpClient CanalTcp { get; set; } // conexión con el cliente
public int NumClient { get; set; } // N.º de cliente
}
Aquí tenemos un código similar al del servidor de eco analizado en el apartado 11.6.1. Solo comentaremos las diferencias:
- línea 7: se pone en marcha el servicio de atención telefónica
- línea 9: se indica que el servicio ya está activo
Línea 21: los clientes son atendidos por subprocesos de servicio que ejecutan el siguiente método doService:
private static void doService(Object infos) {
// se selecciona al cliente al que hay que atender
Client client = infos as Client;
// se atiende al cliente
Console.WriteLine("Début du service au client {0}", client.NumClient);
// gestión de la conexión TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin almacenamiento en búfer
writer.AutoFlush = true;
// envío de un mensaje de bienvenida al cliente
writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
// bucle de lectura de solicitud/escritura de respuesta
string demande = null;
bool serviceFini = false;
while (!serviceFini && (demande = reader.ReadLine()) != null) {
// seguimiento de la consola
if (echo) {
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
}
// análisis de la solicitud
demande = demande.Trim().ToLower();
// ¿Solicitud vacía?
if (demande.Length == 0) {
// ¿Solicitud errónea?
writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
return;
}
// desglose de la solicitud por campos
string[] champs = Regex.Split(demande, @"\s+");
// análisis
switch (champs[0].ToLower()) {
case "aide":
writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
break;
case "impot":
// se calcula el impuesto
writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
break;
case "aurevoir":
serviceFini = true;
writeClient(writer, client.NumClient, "Au revoir...");
break;
default:
writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
break;
}
}
}
}
}
}
} catch (Exception e) {
// error
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
private static void writeClient(StreamWriter writer, int numClient, string message) {
// ¿Salida a la consola?
if (echo) {
Console.WriteLine("---> Client {0} : {1}", numClient, message);
}
// envío de mensaje al cliente
writer.WriteLine(message);
}
De nuevo, nos encontramos ante un código similar al del servidor de eco analizado en el apartado 11.6.1. Solo comentaremos las diferencias:
- línea 15: una vez que el cliente se ha conectado, el servidor le envía un mensaje de bienvenida.
- líneas 19-52: el bucle de lectura de los comandos del cliente. El bucle se detiene cuando el cliente envía el comando «aurevoir».
- línea 27: caso de un comando vacío
- línea 34: la solicitud se descompone en campos para su análisis
- línea 37: comando aide: el cliente solicita la lista de comandos autorizados
- línea 40: comando impot: el cliente solicita un cálculo de impuestos. Se responde con el mensaje devuelto por el método calculImpot, que detallaremos más adelante.
- línea 44: comando aurevoir: el cliente indica que ha terminado.
- línea 45: nos preparamos para salir del bucle de lectura de las solicitudes del cliente (líneas 19-52)
- línea 46: se responde al cliente con un mensaje de despedida
- línea 48: un comando incorrecto. Se envía al cliente un mensaje de error.
El procesamiento del comando impot se lleva a cabo mediante el siguiente método calculImpot:
private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
// pregunta sobre si está casado (S/N) nbEnfants salaireAnnuel
// se necesitan 4 campos
if (champs.Length != 4) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// campos [1]
string marié = champs[1];
if (marié != "o" && marié != "n") {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// campos [2]
int nbEnfants;
if (!int.TryParse(champs[2], out nbEnfants)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// campos [3]
int salaireAnnuel;
if (!int.TryParse(champs[3], out salaireAnnuel)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// Ya está, se calcula el impuesto
int impot = 0;
try {
impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
return impot.ToString();
} catch (Exception ex) {
return ex.Message;
}
}
- línea 1: el método recibe como tercer parámetro la matriz de campos del pedido impot. Si este se ha formulado correctamente, tiene el formato «impot casado nbEnfants salaireAnnuel». El método devuelve como resultado la respuesta que se debe enviar al cliente.
- línea 4: se comprueba que el comando tenga 4 campos
- línea 8: se comprueba que el campo marié sea válido
- línea 14: se comprueba que el campo nbEnfants sea válido
- línea 19: se comprueba que el campo salaireAnnuel sea válido
- línea 25: se calcula el impuesto mediante el método CalculerImpot de la capa [metier]. Cabe recordar que esta capa está encapsulada en una DLL.
- línea 26: si la capa [metier] ha devuelto un resultado, este se devuelve al cliente.
- línea 28: si la capa [metier] ha lanzado una excepción, el mensaje de dicha excepción se envía al cliente.
11.9.4. El cliente gráfico del servidor TCP de cálculo de impuestos
11.9.4.1. El proyecto « » en Visual Studio
![]() |
El proyecto de Visual Studio del cliente gráfico será el siguiente:
![]() |
- En [1], los dos proyectos de la solución, uno para cada una de las dos capas de la aplicación
- en [2], el cliente TCP que actúa como capa [metier] para la capa [ui]. Por lo tanto, utilizaremos ambos términos.
- en [3], la capa [ui] de la versión 7, con un pequeño detalle del que hablaremos
11.9.4.2. La capa [metier]
La interfaz IImpotMetier no ha cambiado. Sigue siendo la de la versión 7:
namespace Metier {
public interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
La implementación de esta interfaz es la siguiente clase [ImpotMetierTcp]:
using System.Net.Sockets;
using System.IO;
namespace Metier {
public class ImpotMetierTcp : IImpotMetier {
// información [serveur]
private string Serveur { get; set; }
private int Port { get; set; }
// cálculo del impuesto
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
// nos conectamos al servicio
using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// flujo de salida sin búfer
writer.AutoFlush = true;
// se omite el mensaje de bienvenida
reader.ReadLine();
// solicitud
writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
// respuesta
return int.Parse(reader.ReadLine());
}
}
}
}
}
}
}
- línea 7: el nombre o la dirección IP del servidor TCP de cálculo de impuestos
- línea 8: el puerto de escucha de este servidor
- Spring inicializará estas dos propiedades al instanciar la clase [ImpotMetierTcp].
- línea 11: el método de cálculo del impuesto. Cuando se ejecuta, las propiedades Serveur y Port ya están inicializadas. En el código se aprecia el procedimiento clásico de un cliente TCP
- línea 13: se abre la conexión con el servidor
- líneas 14-16: se recupera (línea 14) el flujo de red asociado a esta conexión, del que se extraen un flujo de lectura (línea 15) y un flujo de escritura (línea 16).
- línea 18: el flujo de escritura debe estar sin búfer
- línea 20: aquí hay que recordar que, al abrir la conexión, el servidor envía al cliente una primera línea que es el mensaje de bienvenida «Bienvenido al servidor de cálculo de impuestos». Este mensaje se lee y se ignora.
- línea 22: se envía al servidor el comando del tipo: impuesto o 2 60000 para pedirle que calcule el impuesto de una persona casada con dos hijos y un salario anual de 60 000 euros.
- Línea 24: el servidor responde con el importe del impuesto en formato «4282» o bien con un mensaje de error si la orden no se ha formulado correctamente (lo cual no ocurrirá aquí) o si ha surgido algún problema al calcular el impuesto. En este caso, este último caso no se gestiona, pero sin duda habría sido más «limpio» hacerlo. De hecho, si la línea leída es un mensaje de error, se lanzará una excepción porque la conversión a un entero fallará. La excepción captada por la interfaz gráfica será un error de conversión, mientras que la excepción original es de naturaleza totalmente diferente. Se invita al lector a mejorar este código.
- Líneas 25-28: liberación de todos los recursos utilizados con una cláusula «using».
La capa [metier] se compila en DLL ImpotsV8-metier.dll:

11.9.4.3. La capa [ui]
![]() |
La capa [ui] [1,3] es la que se analiza en la versión 7, en el apartado 9.8.4, salvo por tres detalles:
- la configuración de la capa [metier] en [App.config] es diferente porque ha cambiado su implementación
- la interfaz gráfica [Form1.cs] se ha modificado para mostrar una posible excepción
- la capa [metier] se encuentra dentro de DLL [ImpotsV8-metier.dll].
El archivo [App.config] es el siguiente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
<property name="Serveur" value="localhost"/>
<property name="Port" value="27"/>
</object>
</objects>
</spring>
</configuration>
- línea 16: instanciación de la capa [metier] con la clase Metier.ImpotMetierTcp de la DLL ImpotsV8-metier.dll
- líneas 17-18: se inicializan las propiedades Servidor y Puerto de la clase Metier.ImpotMetierTcp. El servidor estará en la máquina localhost y funcionará en el puerto 27.
La interfaz gráfica que se muestra al usuario es la siguiente:
![]() |
- En [1], se ha añadido un TextBox para mostrar una posible excepción. Este campo no existía en la versión anterior.
Aparte de este detalle, el código del formulario es el que ya se ha analizado en el apartado 6.4.3. Se invita al lector a consultarlo. En [2], se muestra un ejemplo de ejecución obtenido con un servidor iniciado de la siguiente manera:
La captura de pantalla [2] del cliente se corresponde con las líneas del cliente 9 anteriores.
11.9.5. Conclusión
Una vez más, hemos podido reutilizar código existente, sin modificaciones (capas [metier] y [dao] del servidor) o con muy pocas modificaciones (capa [ui] del cliente). Esto ha sido posible gracias a nuestro uso sistemático de interfaces y a su instanciación con Spring. Si en la versión 7 hubiéramos colocado el código de negocio directamente en los gestores de eventos de la interfaz gráfica, dicho código no habría sido reutilizable. Este es el principal inconveniente de las arquitecturas de una sola capa.
Por último, cabe señalar que la capa [ui] no tiene conocimiento alguno de que es un servidor remoto el que le calcula el importe del impuesto.























































