Programación cliente

Práctica
3
Arquitectura de Redes y Servicios de
Telecomunicación
Programación cliente/Servidor: Servicio de echo.
Desarrollo de un cliente y un servidor del servicio de echo.
Objetivos
La programación cliente/servidor es fundamental para entender la interacción de las
aplicaciones Internet más comunes. El conocimiento del interfaz de los sockets y la
estructura básica de un cliente y un servidor permiten desarrollar con rapidez la
estructura de cualquier aplicación cliente-servidor, dependiendo de la complejidad del
servicio que se quiera implementar, la dificultad del cliente y/o el servidor se encuentre
al implementar el protocolo de aplicación. Por tanto, el objetivo de esta práctica se
centra en conocer cómo se construye la estructura básica de un aplicación clienteservidor, desarrollando un servicio muy simple, el servicio de “echo”.
Descripción
Con la interfaz de los sockets disponemos de un mecanismo estándar para utilizar la pila
de protocolos TCP/IP. Gracias a esta interfaz las aplicaciones pueden intercambiar
información de forma eficiente a través de Internet, independientemente del sistema
operativo y de la red local a la cual se encuentre conectado el host que las ejecuta. Sin
embargo, para la programación de aplicaciones sobre Internet esta capacidad de
comunicación punto a punto (peer-to-peer) es aún insuficiente. Resulta necesario
establecer otras normas de nivel superior que rijan el tráfico de información.
En la práctica, la mayoría de las aplicaciones sobre TCP/IP se organizan basándose en
el denominado paradigma cliente-servidor. Este tipo de interacción se encuentra tan
extendido que la mayoría de las aplicaciones Internet aplican este modelo.
El modelo cliente-servidor.
El modelo cliente-servidor surge como solución al problema de la sincronización entre
aplicaciones. Por ejemplo, consideremos dos aplicaciones que se ejecutan en diferentes
computadores, y que necesitan comunicarse para intercambiar información.
Supongamos que ambas comprueban si se encuentra presente el otro extremo y, en caso
contrario, abortan el proceso devolviendo un mensaje de error. Puesto que resulta
imposible que ambas aplicaciones se pongan en marcha simultáneamente, la primera de
ellas nunca encontrará a la otra activa (recordemos que la segunda aún no se ha
lanzado), y por lo tanto abortará la comunicación. Cuando entre la segunda no podrá
conectar con la primera que ya ha acabado su ejecución. Si repetimos el intento un
número muy elevado de ocasiones, quizá consigamos que ambas se encuentren activas
simultáneamente y conecten, pero resulta extremadamente difícil.
El modelo cliente-servidor soluciona el problema de la sincronización haciendo que una
de ellas comience su ejecución y espere (en principio de forma indefinida) a que la otra
contacte con ella. Puesto que TCP/IP no es capaz de responder por sí mismo al recibir
una petición de conexión (no es su función) es necesario que cuando ésta se produce ya
exista un proceso esperándola.
Para asegurar que un computador establezca una comunicación cuando se le solicita, la
mayoría de los sistemas operativos disponen de programas de comunicaciones que
comienzan su ejecución en la puesta en marcha del equipo. Cada uno de ellos se ejecuta
indefinidamente, esperando la llegada de peticiones al servicio que ofrecen.
El modelo cliente-servidor divide las aplicaciones en dos categorías, según comiencen
su ejecución esperando una petición de comunicación o iniciando la misma.
Generalmente, si un programa queda a la espera de solicitudes será considerado un
servidor, mientras que si por el contrario inicia la comunicación se considerará un
cliente. Es decir, la dirección de la solicitud de conexión determina el papel de cliente o
servidor de una aplicación.
Los usuarios utilizarán programas clientes cuando deseen acceder a servicios remotos.
Cada vez que se ejecuta un programa cliente, éste debe contactar con un servidor, enviar
la solicitud del servicio y esperar la respuesta. Cuando ésta llega, el cliente continúa su
proceso. Habitualmente los programas cliente resultan mucho más sencillos que los
servidores, y los usuarios no requieren privilegios especiales para su uso. Por el
contrario, los programas servidores esperarán la llegada de peticiones desde los clientes,
y cuando reciban una de ellas, realizarán las operaciones necesarias y devolverán el
resultado al cliente. En el diseño de servidores resulta fundamental contemplar aspectos
tales como la identificación del usuario que gobierna el cliente, la comprobación de que
dicho usuario tiene autorización para solicitar el servicio y proteger la seguridad de los
datos del servidor frente a accesos no autorizados, así como la integridad del sistema
frente a ataques externos.
Puertos y servicios.
Para que sea posible la comunicación entre clientes y servidores es necesario que,
cuando un cliente solicita un servicio, no sólo se dirija a una dirección IP, sino que
seleccione entre todos los servicios que ofrece el host remoto, aquél que desea. Para
ello, a la hora de solicitar una conexión, ésta hace referencia a un puerto dentro de la
máquina destino. El concepto de puerto está asociado al interfaz entre las aplicaciones y
TCP/IP (los sockets). Cuando se crea un socket se le asocia un número de puerto (bien
mediante la llamada bind o de forma automática por el sistema al hacer un connect).
Asimismo, al solicitar una conexión se especifica tanto la dirección IP de la máquina
destino como el puerto al cual queremos acceder.
Los números de puerto se distribuyen entre servicios estándar y servicios no estándar.
Los puertos "bien conocidos" se asignan a servicios tales como el terminal virtual
(TELNET), la transferencia de ficheros (FTP), etc., mientras que el resto es asignado
automáticamente por el sistema o pueden utilizarse para aplicaciones específicas.
2
Tipos de clientes
La primera clasificación que podemos establecer responde precisamente al tipo de
servicio al que acceden, y por tanto al número de puerto que utilizan para la solicitud
del servicio. Por este criterio, distinguiremos clientes estándar, como aquellos que
solicitan la conexión a puertos bien conocidos, y clientes no estándar, que utilizan otros
números de puerto y acceden a servicios particulares.
Por otro lado, las aplicaciones cliente-servidor pueden utilizar un servicio con conexión
o sin conexión. Estas dos aproximaciones corresponden a los dos niveles de transporte
que ofrece la pila de protocolos TCP/IP. Si la comunicación entre cliente y servidor
utiliza UDP, la interacción será, evidentemente, sin conexión, mientras que el uso de
TCP implica una conexión.
Desde nuestro punto de vista, la diferencia entre ambas aproximaciones será
fundamental puesto que determinan el nivel de fiabilidad proporcionada por la
arquitectura de protocolos. TCP proporciona toda la fiabilidad necesaria para la
interconexión a través de Internet. Por el contrario, los clientes y servidores que
emplean UDP no tienen garantías sobre la entrega de los datos. Cuando un cliente
realiza una petición, ésta puede perderse, llegar más de una vez al receptor o entregarse
en orden distinto al que fue enviada. Lo mismo puede suceder con la respuesta del
servidor. Por ello la aplicación debe tomar las medidas apropiadas para detectar y
corregir estos errores.
UDP resulta poco fiable puesto que propaga los errores de los niveles inferiores sin
comprobarlos ni corregirlos. Por tanto, no evitará los errores de envío que se produzcan
en el nivel de IP, que a su vez depende del hardware de red que tenga por debajo. Esto
permite que la utilización de UDP, sea más simple y más rápida que TCP (carece de
fase de establecimiento de la conexión), y que resulte recomendable, por ejemplo,
cuando el servicio se proporciona en la misma red local en que se encuentra el servidor,
donde los errores en la comunicación son muy poco frecuentes. Algunos programadores
cometen el error de implementar servicios sobre UDP y verificarlos en su red local,
dando por bueno el servicio sin comprobar su funcionamiento en Internet. Por ello, en
general, resulta más interesante el uso del servició con conexión de TCP. De esta forma
se consiguen programas más simples, ya que nuestra aplicación se desentiende del
control de errores en el transporte de los datos.
Normalmente sólo emplearemos UDP cuando la especificación del servicio a
implementar lo requiera, cuando el protocolo necesite realizar difusiones o bien cuando
el coste temporal y los recursos utilizados en TCP sean excesivamente elevados para la
aplicación.
3
Clases de servidores.
Se definen cuatro tipos distintos de servidores, dependiendo del tipo de protocolo de
transporte que utilicen y de que atiendan a sus clientes de forma secuencial o
simultánea:




Iterativo sin conexión (UDP).
Iterativo con conexión (TCP).
Concurrente sin conexión (UDP).
Concurrente con conexión (TCP).
Servidor iterativo sin conexión.
Los servidores iterativos resultan adecuados para aquellos servicios que requieran poco
tiempo de procesamiento, ya que en caso contrario la espera puede resultar incómoda
para los clientes que están guardando su turno para ser atendidos. Como el protocolo
TCP introduce una sobrecarga de procesamiento muy superior a la que provoca UDP, la
mayoría de los servidores iterativos utilizan este último.
El funcionamiento de un servidor iterativo UDP se ajusta al siguiente esquema:
Crear un socket de tipo
SOCK DGRAM
Asociarle una dirección de socket
bién -conocida (BIND)
Esperar la petición de servicio sobre
el socket creado (RECVFROM)
Procesar la petición de servicio y
enviar resultados (SENDTO)
En este caso en todo el proceso se utiliza un único socket, que sirve tanto para esperar
las peticiones de los clientes como para atender las mismas.
Servidor
Aplicación
S.O.
Socket BC
Estructura de un servidor iterativo sin conexión.
4
Servidor iterativo con conexión.
Aunque los servidores iterativos más frecuentes son sin conexión, también pueden
implementarse este tipo de servidores sobre TCP. A continuación, mostramos su
funcionamiento:
Crear un socket de tipo SOCK_STREAM
(SOCKET)
A diferencia del caso anterior, vemos
que ahora se emplean dos sockets: El
servidor utiliza el socket maestro para
recibir las peticiones de conexión por
parte de los clientes; al establecerse una
conexión se crea un nuevo socket (socket
esclavo) asociado a dicha conexión, que
es el que se usa para llevar a cabo el
diálogo con el cliente. Durante ese
período, mientras se están atendiendo las
solicitudes de ese cliente, no se aceptan
nuevas peticiones de conexión.
Asociarle una dirección de socket
bién -conocida (BIND)
Poner el socket en modo pasivo (LISTEN)
Esperar una petición de conexión sobre el
socket y obtener un nuevo socket cuando
se establece la conexión (ACCEPT)
Servidor
Diálogo cliente – servidor según el
protocolo de aplicación (READ, WRITE,
RECV, SEND)
Aplicación
S.O.
Maestro
Esclavo
Estructura de un servidor iterativo con
conexión
Cierre del socket asociado a la conexión
(CLOSE)
Servidor concurrente sin conexión.
El objetivo principal al introducir la concurrencia es disminuir el tiempo de respuesta
cuando existen varios clientes. Esto resulta especialmente interesante cuando:



El tiempo necesario para atender las peticiones de los clientes es variable o
siempre elevado.
Enviar una respuesta requiere un tiempo significativo de E/S.
El servidor se va a ejecutar en un entorno con múltiples procesadores.
La mayoría de los servidores concurrentes se basarán en el empleo de múltiples
procesos (en nuestro caso múltiples threads). Existirá un proceso maestro que se
encargará de crear un socket y asociarlo a un puerto bien-conocido, donde esperará las
peticiones de los clientes, y creará procesos esclavos que se encarguen de atender las
peticiones de dichos clientes. En el caso de los servidores concurrentes sin conexión,
cada petición se trata de forma independiente, provenga del mismo cliente que realizó la
petición anterior o de otro distinto.
5
Por lo tanto, para atender cada una de estas peticiones se generará un nuevo proceso,
que se destruirá una vez atendida la misma. Esto plantea una cuestión importante a la
hora de implementar un servidor como concurrente o iterativo: ¿Supera el coste de
atender una única petición de un servicio determinado al que le supone al sistema crear
un nuevo proceso y destruirlo después. Existen pocos servicios en los que el coste de
servicio sea mayor que el de gestión de procesos, por lo que la mayoría de los
servidores sin conexión tienen implementaciones iterativas. Una excepción es la
aplicación de transferencia de ficheros TFTP (Trivial File Transfer Protocol). Veamos
el esquema de funcionamiento de este tipo de servidores:
Proceso Maestro
Proceso esclavo
Crea un socket de tipo
SOCK_DGRAM (SOCKET)
Recoge la petición de servicio que
le pasa el maestro, así como la
dirección del cliente
Le asocia una dirección de socket
Bien-conocida (BIND)
Procesa la petición y envía la
respuesta al cliente, en uno o varios
mensajes (SENDTO)
Espera una petición de servicio sobre
el socket creado (RECVFROM)
Tras acabar el servicio el proceso
esclavo se autodestruye
Crea un proceso esclavo y le pasa la
petición de servicio recibida
Ahora podemos tener múltiples procesos en el servidor, pero todas las transferencias se
llevan a cabo a través de un único socket:
Maestro
...
Esc
Esclavo
Esclavo
Esclavo
Aplicación
S.O.
Socket BC
Estructura de un servidor concurrente sin conexión.
6
Servidor concurrente con conexión.
En este caso, tendremos un único proceso maestro, escuchando en un puerto bienconocido. Al establecerse una conexión, se crea un nuevo proceso que se encargará de
atender las peticiones del
Proceso Maestro
Proceso esclavo
cliente a través del socket
esclavo. Mientras tanto, el
proceso
maestro
sigue
Crear un socket de tipo
esperando
nuevas
solicitudes
SOCK_STREAM
Recoge el nuevo socket
de conexión. Al finalizar la
(SOCKET)
resultante del
conexión con el cliente, se
establecimiento de la
cierra el socket esclavo y el
conexión
proceso esclavo se destruye.
Asociarle una dirección
de socket
bien -conocida (BIND)
Poner el socket en modo
pasivo (LISTEN)
Intercambio de
mensajes entre cliente y
servidor según el
protocolo de aplicación
(READ, WRITE,
Según el funcionamiento descrito anteriormente, este tipo
de servidores pueden usar simultáneamente varios sockets
para la comunicación. Su estructura se muestra en la figura
siguiente:
Tras acabar el servicio
el proceso esclavo cierra
la conexión (CLOSE) y
se destruye
Esperar una petición de
conexión sobre el socket
creado (ACCEPT)
Slave
Slave
Master
Crear un proceso
esclavo, que atenderá
las peticiones del
servicio, y pasarle el
nuevo socket creado por
Aplicación
Slave
Maestro Esclavo_1 Esclavo_2
Esclavo_n
Estructura de un servidor concurrente con conexión
7
S.O
El servicio de ECHO
Permite a un usuario comprobar que la estación que actúa como servidor está activa y
que se tiene acceso a ella a través de la red. Al recibir una petición, el servidor de echo
envía como respuesta los mismos datos que envió el cliente en su solicitud. Este
servicio se encuentra disponible tanto sobre TCP como sobre UDP1.
Servicio de eco basado en TCP
El servidor escucha en el puerto 7 para recibir las peticiones de conexión de los clientes.
Una vez establecida la conexión el servidor devuelve al cliente todos los datos que este
le va enviando. Esta conexión permanece abierta hasta que el cliente solicita la
desconexión.
En concreto, los pasos que debe seguir el cliente de echo TCP son los siguientes:
1.
2.
3.
4.
5.
6.
Obtener dirección IP y número de puerto, y construir dirección de socket.
Crear un socket de tipo SOCK_STREAM mediante la llamada SOCKET.
Asignar al socket creado la dirección de socket local (BIND).*
Establecer conexión con servidor (CONNECT).
Protocolo de aplicación, utilizando READ, WRITE2, SEND y RECV
Cerrar la conexión, eliminando el socket (CLOSE, SHUTDOWN).
(*) La dirección local de socket es especificada por CONNECT, por lo que no resulta
obligatorio hacerlo de forma explícita.
Servicio de eco basado en UDP
En este caso, el servidor también espera la recepción de peticiones de los clientes en el
puerto 7. Cuando se recibe un datagrama, el servidor envía copia de los datos recibidos
al cliente que se los ha enviado. Cada petición se trata de forma independiente en el
servidor, incluso aunque se reciban varias procedentes del mismo cliente.
Los pasos a ejecutar por el cliente UDP son los siguientes:
1.
2.
3.
4.
5.
Obtener dirección IP y número de puerto, y construir dirección de socket.
Crear un socket de tipo SOCK_DGRAM mediante la llamada SOCKET.
Asignar la dirección de socket local al socket recién creado (BIND).
Especificar el servidor con el que queremos conectar (CONNECT). (*)
Protocolo de aplicación, utilizando SENDTO, RECVFROM, y opcionalmente
READ, WRITE, SEND y RECV.
6. Eliminar el socket (CLOSE).
(*) Opcional: No resulta necesario emplear CONNECT si posteriormente utilizamos
SENDTO y RECVFROM. Sí resulta necesario para utilizar SEND, etc.
1
Se podría equiparar a los mensajes ICMP ECHO, pero a nivel de aplicación.
A pesar de encontrarse ampliamente extendidas en los sockets de UNIX, en la versión Windows no se
utilizan las llamadas READ y WRITE
2
8
Trabajo a realizar
En esta práctica se proporciona la aplicación casi completa. Se suministran tres
módulos: Echo.c, echod.c y util.c. El primero y el segundo contienen el código principal
del cliente y servidor de echo, respectivamente. El último módulo se define como una
librería de funciones y procedimientos disponibles para los módulos anteriores.
También se ha suministrado el fichero “Makefile” que permite compilar y enlazar los
programas ejecutables tanto del cliente como del servidor (comando make).
Se ha suprimido el código correspondiente al procedimiento ConnectSock (módulo
util.c) y parte del código del cuerpo del programa principal (módulo echo.c) del cliente
de echo. Se pide que se completen estas funciones.
Una vez completadas estas funciones, se deberá comprobar el correcto funcionamiento
del cliente desarrollado, pudiendo utilizar para ello el servidor de echo local (localhost)
y/o el servidor de echo suministrado (echod). Para lanzar este último sin que entre en
conflicto con el que se está ejecutando en el sistema, se sugiere el uso de una dirección
de puerto alternativa, por ejemplo 50000+nº de usuario en el sistema. Así, si nuestro
usuario es 27 (nombre usuario  arst27), lanzaremos el servidor de echo así:
$ echod 50027
Sugerencias en el desarrollo y depuración de los programas:
(1) El servidor de desarrollo tiene la dirección IP: 10.1.32.64
(2) El acceso al servidor se hará a través de un cliente SSH. En el escritorio se
dispone de un cliente SSH (putty.exe)
(3) Se dispone de cuentas de usuario cuyo nombre será arstX y la clave de acceso
inicial X__arst (dónde XX es un número entre 1 y 55 y como separador con el
nombre se han empleado dos guiones bajos “_”). Así por ejemplo el usuario
arst7 tendrá la clave de acceso 7__arst. Cada alumno tendrá asignada una cuenta
de usuario. La asignación de cuentas se encuentra en un documento adjunto. Se
recomienda cambiar la clave de acceso la primera de vez que se acceda.
(4) Antes de finalizar la sesión se recomienda salvar el código desarrollado en otra
ubicación. No se garantiza la disponibilidad de los contenidos en dichas cuentas.
(5) Cuando se desarrolle un servidor, aseguraros de eliminarlo del sistema antes de
finalizar la sesión. Para ello, deberéis listar todos los procesos activos que tenéis
en vuestra terminal (comando ps) y eliminar (comando kill) el(los) proceso(s) de
servidor(es) que se encuentre(n) en ejecución:
Arst60@labatc:~/labfiles$ ps
PID TTY
TIME CMD
16985 pts/6
00:00:00 bash
17024 pts/6
00:00:00 echod
17025 pts/6
00:00:00 ps
Arss60@labatc:~/labfiles$ kill -9 17024
9
Cuestiones adicionales
1. Implementar el cliente y servidor de echo basados en UDP.
2. Modificar el cliente de echo para construir un cliente del servicio time basado en
UDP. Este servicio, accesible a través del puerto 37, existe tanto en versiones
TCP como UDP. El servidor devuelve una cadena que contiene la fecha y la
hora actual del sistema en cuanto se abre la conexión. La versión sobre UDP
requiere que el cliente envíe algún mensaje para que el servidor conteste, cuyo
contenido resulta indiferente.
3. Implementar un servidor multiprotocolo del servicio de echo. De esta forma, en
lugar de tener dos servidores independientes, uno sobre TCP y el otro sobre
UDP, para atender el mismo servicio, podemos realizar un único servidor que
atienda peticiones tanto sobre TCP como UDP. Para ello puede resultar útil el
empleo de las siguientes funciones:
Función SELECT: Permite atender la entrada/salida producida por
varios descriptores de sockets. Indica al sistema cuántos descriptores
desea escuchar y qué eventos hay que notificarle (lectura, escritura o
eventos de otro tipo)  Multiplexación de entrada/salida síncrona
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
donde:
n: Indica el número máximo de descriptores (sockets) a escuchar.
readfds, writefds y exceptfds: Definen conjuntos de descriptores
que se escucharán para lectura, escritura y eventos/excepciones,
respectivamente.
fd_set: Estructura de datos que define un conjunto de
descriptores:
timeout: Define el intervalo de tiempo que la llamada select
esperará a que se produzca alguna entrada/salida sobre los
descriptores especificados. Si es 0, la llamada retorna inmediatamente después de comprobar el estado de los descriptores
deseados.
struct timeval {
long
long
};
tv_sec;
tv_usec;
/* segundos */
/* microsegundos */
FD_ZERO: Inicializa un conjunto (set) de descriptores a cero.
fd_set set;
FD_ZERO(set)
FD_SET: Incluye un descriptor (fd) en un conjunto de descriptores (set).
FD_SET(fd,set)
10
FD_ISSET: Comprueba si un socket (fd) pertenece a un conjunto (set).
FD_ISSET(fd, set)
FD_CLR: Elimina un descriptor (fd) de un conjunto (set).
FD_ISSET(fd, set)
Mediante la función select el servidor puede esperar escuchando sobre varios
sockets. La función retornará cuando alguno de los sockets se active. Antes de
poner el servidor a la escucha mediante esta función, será necesario realizar
algunas inicializaciones: poner el conjunto de descriptores de sockets sobre el
que se va a trabajar a cero (FD_ZERO) e incluir en dicho conjunto los sockets
sobre los que el servidor va a escuchar (FD_SET). Los pasos necesarios pueden
resumirse en:
Abrir socket_pasivo_TCP tsock;
Abrir socket_pasivo_UDP usock;
Obtener el número máximo de descriptores a escuchar:
Nfds = MAX(tsock, usock) + 1;
Inicializar conjunto de descriptores (set) a cero (FD_ZERO)
Repetir
Incluir tsock en el conjunto set (FD_SET)
Incluir usock en el conjunto set (FD_SET)
Esperar a que se active alguno de los descriptores (SELECT)
Si usock activo responder a eco_UDP
Si tsock activo
Establecer conexión
Incluir socket_esclavo en el conjunto set
Si algún otro descriptor activo
Responder a eco_TCP
siempre
Bibliografía
[1] D. Comer, “Internetworking with TCP/IP, vol3”, 2ª ed., Prentice Hall, 1998.
[2] RFC 862: ECHO Protocol.
11
Informe
El informe o memoria de esta sesión de laboratorio deberá incluir los siguientes
aspectos:






Una página de presentación con el nombre de la asignatura, curso, titulación, tu
nombre y apellidos, número y título de la práctica y las fechas de realización y
envío la memoria.
Un resumen de los objetivos perseguidos en la práctica.
Metodología: Una breve descripción del proceso que has seguido a la hora de
definir los distintos escenarios, modelos, etc. contemplados en la práctica.
Exposición y análisis de los resultados obtenidos a lo largo de la práctica y, si
procede, una comparación de los mismos con lo que esperabas obtener.
La respuesta a las cuestiones o ejercicios planteados en la memoria de la
práctica. Si la respuesta incorpora nuevas gráficas, debe realizarse un breve
análisis de las mismas.
Una conclusión general en la que se describa lo que se ha aprendido con esta
práctica, las dificultades que se han encontrado en la realización de los
diferentes pasos y cualquier sugerencia, propuesta de ampliación o comentario
que permita mejorar la práctica propuesta.
12