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
© Copyright 2024