Práctica3: Programación POSIX Programación y Administración de Sistemas 2011-2012 Javier Sánchez Monedero [email protected] 2o curso de Grado en Ingeniería Informática Departamento de Informática y Análisis Numérico Escuela Politécnica Superior Universidad de Córdoba 26 de octubre de 2012 Resumen Entre otras cosas, el estándar POSIX define una serie de funciones (comportamiento, parámetros y valores devueltos) que deben proporcionar todos los sistemas operativos que quieran satisfacer el estándar. De esta forma, si una aplicación hace un buen uso de estas funciones deberá compilar y ejecutarse sin problemas en cualquier sistema operativo POSIX. En esta práctica vamos a conocer qué es el estándar POSIX, y una implementación de la biblioteca C de POSIX, la biblioteca libre GNU C Library o glibc. En Moodle se adjunta el fichero pas-practica3.tgz que contiene código de ejemplo de las funciones que iremos viendo en clase. Índice 1. Introducción a POSIX 2 2. Objetivos 4 3. Entrega de prácticas 4 4. Documentación de POSIX y las bibliotecas 4 5. Procesado de línea de comandos tipo POSIX 5.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 6. Variables de entorno 6.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 6 1 2 Programación y Administración de Sistemas– 2011-2012 7. Obtención de información de un usuario/a 7.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 6 8. Ejercicio resumen 1 (0.5 punto) 7 9. Creación de procesos Fork y Exec 9.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.2. Ejercicios y ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 8 10. Señales entre procesos 10.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.2. Ejercicios y ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 11 11. sockets POSIX 11.1. Introducción y conceptos básicos . . . . . . . 11.2. Algoritmo básico Cliente-Servidor . . . . . . 11.3. Pasos de programación C en el lado Servidor 11.4. Pasos de programación C en el lado Cliente . . . . . 12 12 13 14 14 12. Todo junto: un servidor web básico en C 12.1. Introducción y código . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 16 13. Ejercicio resumen 2 (1 punto) 17 14. Trabajo futuro 18 Referencias 18 1. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introducción a POSIX POSIX es el acrónimo de Portable Operating System Interface; la X viene de UNIX como seña de identidad de la API (Application Programming Interface, interfaz de programación de aplicaciones). Son una familia de estándares de llamadas al sistema operativo definidos por el IEEE (Institute of Electrical and Electronics Engineers, Instituto de Ingenieros Eléctricos y Electrónicos) y especificados formalmente en el IEEE 1003. Persiguen generalizar las interfaces de los sistemas operativos para que una misma aplicación pueda ejecutarse en distintas plataformas. Estos estándares surgieron de un proyecto de normalización de las API y describen un conjunto de interfaces de Figura 1: Mascota del aplicación adaptables a una gran variedad de implementaciones de proyecto GNU (http: sistemas operativos [1]. La última versión de la especificación POSIX es del año 2008, se cono//www.gnu.org/) ce por “POSIX.1-2008”, “IEEE Std 1003.1-2008” y por “The Open Group Technical Standard Base Specifications, Issue 7”[2]. 1. Introducción a POSIX 3 Figura 2: Dennis MacAlistair Ritchie. Colaboró en el diseño y desarrollo de los sistemas operativos Multics y Unix, así como el desarrollo de varios lenguajes de programación como el C, tema sobre el cual escribió un célebre clásico de las ciencias de la computación junto a Brian Wilson Kernighan: El lenguaje de programación C [4]. GNU C Library, comúnmente conocida como glibc es la biblioteca estándar de lenguaje C de GNU. Se distribuye bajo los términos de la licencia GNU LGPL. Esta biblioteca sigue todos los estándares más relevantes como ISO C99 y POSIX.1-2008 [3]. gnulib, también conocida como “Biblioteca de portabilidad de GNU” es una colección de subrutinas diseñadas para usarse en distintos sistemas operativos y arquitecturas. El objetivo de esta segunda biblioteca es facilitar el desarrollo multi-plataforma de aplicaciones de software libre. En los sistemas en los que se usan, estas bibliotecas de C proporcionan y definen las llamadas al sistema y otras funciones básicas, y son utilizadas por casi todos los programas. Sobre todo, es muy usada en los sistemas GNU y en el kernel de Linux. Cuando hablamos de Linux como sistema operativo completo debemos referirnos a él como Figura 3: GNU + Linux = GNU/Linux “GNU/Linux” para reconocer que el sistema lo compone tanto el núcleo Linux como las bibliotecas de C y otras herramientas de GNU que hacen posible Linux1 . glibc y gnulib son muy portables y soportan gran cantidad de plataformas de hardware [5]. En los sistemas Linux se instalan normalmente con el nombre de libc6 y gnulib respectivamente. No debe confundirse con GLib2 , otra biblioteca que proporciona estructuras de datos avanzadas como árboles, listas enlazadas, tablas hash, etc. y un entorno de orientación a objetos en C. En estas prácticas utilizaremos la biblioteca glibc como implementación de programa1 2 http://es.wikipedia.org/wiki/Controversia_por_la_denominaci%C3%B3n_GNU/Linux http://library.gnome.org/devel/glib/ 4 Programación y Administración de Sistemas– 2011-2012 ción del API POSIX. Algunas distribuciones de GNU/Linux, como Debian o Ubuntu utilizan una variante de la glibc llamada eglibc3 , pero a efectos de programación no debería haber diferencias. 2. Objetivos Conocer algunas rutinas POSIX y su implementación glibc. Aprender a utilizar bibliotecas externas en nuestros programas. Aprender cómo funcionan internamente algunas partes de GNU/Linux. Mejorar la programación viendo ejemplos hechos por los desarrolladores de las bibliotecas. Aprender a comunicar aplicaciones en red y conocer brevemente la arquitectura ClienteServidor. 3. Entrega de prácticas Se pedirá la entrega y defensa de algunos ejercicios resúmenes que integren varios de los conceptos y funciones estudiadas en clase. Las prácticas deben realizarse a nivel individual y se utilizarán mecanismos para comprobar que se han realizado y entendido. 4. Documentación de POSIX y las bibliotecas Documentación POSIX.1-2008: Especificación del estándar POSIX. Dependiendo de la parte que documente es más o menos pedagógica4 . Documentación GNU C Library: Esta documentación incluye muchos de los conceptos que ya habéis trabajado en asignaturas de introducción a la programación o de sistemas operativos. Es una guía completa de programación en el lenguaje C, pero sobre todo incluye muchas funciones que son esenciales para programar, útiles para ahorrar tiempo trabajando o para garantizar la portabilidad del código entre sistemas POSIX5 . 5. Procesado de línea de comandos tipo POSIX 5.1. Introducción y documentación Los parámetros que procesa un programa en sistemas POSIX deben seguir un estándar de formato y respuesta esperada6 . Un resumen de lo definido en el estándar es lo siguiente: 3 http://www.eglibc.org/home http://pubs.opengroup.org/onlinepubs/9699919799/ 5 http://www.gnu.org/software/libc/manual/ 6 12.1 Utility Argument Syntax, http://pubs.opengroup.org/onlinepubs/9699919799/ basedefs/V1_chap12.html 4 6. Variables de entorno 5 Una opción es un guión ’-’ seguido de un carácter alfanumérico, por ejemplo, -o. Una opción requiere un parámetro que debe aparecer inmediatamente después de la opción, por ejemplo, para -o lo siguiente: -o parámetro o -oparámetro. Las opciones que no requieren parámetros pueden agruparse detrás de un guión, por ejemplo, -lst es equivalente a -t -l -s. Las opciones puede aparecer en cualquier orden, así -lst es equivalente a -tls. Las opciones pueden aparecer muchas veces. El parámetro -- indica el fin de las opciones en cualquier caso. La opción - a menudo se indica para representar alguna entrada estándar. La función getopt()7 del estándar ayuda a desarrollar el manejo de las opciones siguiendo las directrices POSIX.1-2008. getopt() está implementada en libc89 . Puedes ver código de ejemplo comentado en el fichero ejemplo-getopt.c. El fichero ejemplo-getoptlong.c contiene un ejemplo de procesado de órdenes al estilo de GNU (por ejemplo, --help y -h como órdenes compatibles). Este segundo ejemplo no lo veremos en clase. 5.2. Ejercicios Descarga el código de ejemplo de getopt() de la documentación de glibc10 . Lee el código, compílalo, ejecútalo para comprobar que admite las opciones de parámetros POSIX. Trata de entender el código y añade más opciones (por ejemplo una sin parámetros y otra con parámetros). Debes consultar la sección Using the getopt function de la documentación11 para saber cómo funciona getopt, qué valores espera y qué comportamiento tiene. 6. Variables de entorno 6.1. Introducción y documentación Una variable de entorno es un objeto designado para contener información usada por una o más aplicaciones. La variables de entorno se asociación a toda la máquina, pero también a usuarios individuales. Si utilizas bash, puedes consultar las variables de entorno de tu sesión con el comando env. También puedes consultar o modificar el valor de una variable de forma individual: $ env $ ... $ echo $LANG es_ES.UTF-8 $ export LANG=en_GB.UTF-8 7 http://pubs.opengroup.org/onlinepubs/9699919799/functions/getopt.html http://www.gnu.org/software/libc/manual/html_node/Getopt.html 9 Notas sobre portabilidad en http://www.gnu.org/software/gnulib/manual/gnulib.html# getopt 10 http://www.gnu.org/software/libc/manual/html_node/Example-of-Getopt.html 11 http://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html 8 6 Programación y Administración de Sistemas– 2011-2012 6.2. Ejercicios Utilizando la función getenv()12 , haz un programa que dependiendo del idioma de la sesión de usuario, imprima un mensaje con su carpeta personal en castellano o en inglés. Puedes ver código de ejemplo en el fichero ejemplo-getenv.c. 7. 7.1. Obtención de información de un usuario/a Introducción y documentación En los sistemas operativos la base de datos de usuarios puede ser local y/o remota. Por ejemplo, en GNU/Linux puedes ver los usuarios y grupos locales en los ficheros /etc/passwd y /etc/group. Si los usuarios no son locales normalmente se encuentran en una máquina remota a la que se accede por un protocolo específico. Algunos ejemplos son el servicio de información de red (NIS, Network Information Service) o el protocolo ligero de acceso a directorios (LDAP, Lightweight Directory Access Protocol). En la actualizad NIS apenas se usa y LDAP es el estándar para autenticar usuarios tanto en sistemas Unix o GNU/Linux como en sistemas Windows. En el caso de GNU/Linux, la autenticación de usuarios la realizan los módulos de autenticación PAM (Pluggable Authentication Module). PAM es un mecanismo de autenticación flexible que permite abstraer las aplicaciones del proceso de identificación. La búsqueda de su información asociada la realiza el servicio NSS (Name Service Switch), que provee una interfaz para configurar y acceder a diferentes bases de datos de cuentas de usuarios y claves como /etc/passwd, /etc/group, /etc/hosts, LDAP, etc. POSIX presenta una interfaz para el acceso a la información de usuarios que abstrae al programador de dónde se encuentran los usuarios (en bases de datos locales y/o remotas, con distintos formatos, etc.). Por ejemplo, la llamada getpwuid() devuelve una estructura con información de un usuario. El sistema POSIX se encarga de intercambiar información con NSS para conseguir la información del usuario. NSS leerá ficheros en el disco duro o realizará consultas a través de la red para conseguir esa información. 7.2. Ejercicios Puedes ver las funciones y estructuras de acceso a la información de usuarios/as y grupos en los siguientes ficheros de cabecera: /usr/include/pwd.h /usr/include/grp.h. La función getpwnam()13 permite obtener información de un usuario/a. Mira el programa de ejemplo de getpwnam() y amplíalo para utilizar getgrgid()14 para obtener el nombre del grupo del usuario a través del identificador del grupo. Puedes ver código de ejemplo en el fichero ejemplo-infousuario.c. 12 http://pubs.opengroup.org/onlinepubs/009604599/functions/getenv.html http://pubs.opengroup.org/onlinepubs/009604599/functions/getpwnam.html http://www.gnu.org/software/libc/manual/html_node/User-Database.html 14 http://www.gnu.org/software/libc/manual/html_node/Group-Database.html 13 9. Creación de procesos Fork y Exec 8. 7 Ejercicio resumen 1 (0.5 punto) Realizar un programa que obtenga e imprima la información de un usuario (todos los campos de la estructura passwd) pasado por el parámetro -u. Si además se le pasa la opción -g deberá buscar e imprimir la información del grupo del usuario (número ID del grupo y nombre). Si se le pasa la opción -e imprimirá el mensaje en castellano, y la opción -s hace que lo imprima en inglés. Si no se le pasa ni -e ni -s se mirará la variable de entorno LANG para mostrar la información. Por ejemplo, las siguientes llamadas serían válidas: # # $ # # $ # # $ Obtener la información de un usuario usando el idioma configurado en LANG infousuario -u i02samoj Obtener la información de un usuario forzando el idioma en castellano infousuario -u i02samoj -e Obtener la información de un usuario añadiendo la información de su grupo principal e imprimiendo todo en inglés infousuario -u i02samoj -s -g El control de errores debe ser el siguiente: Asegurar que se pasa el nombre del usuario (tienes un ejemplo de cómo controlar que se pasa el parámetro de una opción en el ejemplo-getopt.c). Las opciones -e y -s no pueden activarse a la vez. Sugerencia: empezar por el código de ejemplo ejemplo-getopt.c. Primero debe procesarse la entrada de comandos para asegurarse que no falta ninguna opción esencial (por ejemplo el nombre de usuario). Después debe ir la lógica del programa dependiendo de las opciones que se hayan reconocido. Se pueden hacer funciones para simplificar la función main. Por ejemplo, una función para imprimir la información del usuario en castellano/inglés a la que se le pase la estructura struct passwd y una opción. Una sugerencia para el esquema general puede ser: 1. Procesar opciones de entrada. 2. Usar la variable de entorno LANG si las “flags” de las opciones -e o -s están a cero. 3. Llamar a una función que imprima la información de un usuario en inglés o castellano con las opciones (login, grupo si/no, idioma). 9. 9.1. Creación de procesos Fork y Exec Introducción y documentación En general, en sistemas operativos y lenguajes de programación, se llama bifurcación o fork a la creación de un subproceso copia del proceso que llama a la función. El subproceso creado, o “proceso hijo”, proviene del proceso originario, o “proceso padre”. Los procesos 8 Programación y Administración de Sistemas– 2011-2012 pid=fork() case 0: // hijo default: // padre Figura 4: Esquema de llamadas y procesos generados por fork() en el ejemplo. resultantes son idénticos, salvo que tienen distinto número de proceso (PID)15 . En UNIX otra forma de crear subprocesos es utilizar tuberías o pipes, las cuáles son esenciales para la comunicación inter-procesos, por ejemplo: $ find . -name "*.c" -print | wc -l En C se crea un subproceso con llamando a la función fork()16 . Tienes un pequeño manual y mucho código de ejemplo en [6]. Puedes ver un código con muchos comentarios en la entrada de Wikipedia17 . El nuevo proceso hereda varias propiedades del proceso padre (variables de entorno, descriptores de ficheros, etc. ). En esta asignatura no entraremos en qué sucede internamente a nivel del sistema operativo. Después de una llamada exitosa a fork habrá dos copias del código original ejecutándose a la vez. En el proceso original, el valor devuelto de fork será el identificador del proceso hijo. En cambio, en el proceso hijo el valor devuelto por fork será 0. 9.2. Ejercicios y ejemplos El listado siguiente (ejemplo-fork.c) es un ejemplo de uso de fork que controla qué tipo de proceso es el que ejecuta el código utilizando el valor devuelto por la función, así como otras funciones POSIX para obtener información de los procesos (puedes ver un esquema de las subprocesos creados en la Figura 4): #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t rf; rf = fork(); switch (rf) 15 http://www.gnu.org/software/libc/manual/html_node/Processes.html http://pubs.opengroup.org/onlinepubs/9699919799/ 17 http://es.wikipedia.org/wiki/Bifurcaci%C3%B3n_%28sistema_operativo%29 16 9. Creación de procesos Fork y Exec 9 { case -1: printf ("No he podido crear el proceso hijo \n"); exit(-1); case 0: printf ("Soy el hijo, mi PID es %d y mi PPID es %d \n", getpid(), getppid()); sleep (5); break; default: printf ("Soy el padre, mi PID es %d y el PID de mi hijo es %d \n", getpid(), rf); sleep (10); } printf ("Final de ejecución de %d \n", getpid()); exit (0); } Un ejemplo de llamada a este código sería: $ ./ejemplo-fork Soy el padre, mi PID es 23455 y el PID de mi hijo es 23456 Soy el hijo, mi PID es 23456 y mi PPID es 23455 Final de ejecución de 23456 Final de ejecución de 23455 En este ejemplo el proceso padre no queda bloqueado esperando al hijo, prueba a poner un valor menor de espera (sleep) para el padre que para el hijo: $ ./ejemplo-fork Soy el padre, mi PID es 23437 y el PID de mi hijo es 23438 Soy el hijo, mi PID es 23438 y mi PPID es 23437 Final de ejecución de 23437 $ Final de ejecución de 23438 Si quisiéramos que el proceso padre esperase a que el proceso (o los procesos) hijos terminasen, debemos utilizar la función wait18 . El valor devuelto por la función wait es el PID del proceso hijo que se está esperando que termine, si se le ha pasado un PID como argumento. Si sólo se le pasa &status, wait() espera a que terminen todos los procesos hijos y devuelve el PID del último proceso en terminar. Un ejemplo: #include <unistd.h> #include <sys/wait.h> int main(){ pid_t pid; int status, died; 18 www.gnu.org/software/libc/manual/html_node/Process-Completion.html 10 Programación y Administración de Sistemas– 2011-2012 switch(pid=fork()){ case -1: printf("can’t fork\n"); exit(-1); case 0 : sleep(2); // código que ejecuta el hijo exit(3); default: died= wait(&status); // código que ejecuta el padre } } En ocasiones puede interesar ejecutar un programa distinto, no diferentes partes de él, y se quiere iniciar este segundo proceso diferente desde el programa principal. La familia de funciones exec() permiten iniciar un programa dentro de otro programa. En lugar de crear una copia del proceso, como fork(), exec() provoca el reemplazo total del programa que llama a la función por el programa llamado. Por ese motivo se suele utilizar exec() junto con fork(), de forma que sea un proceso hijo el que cree el nuevo proceso para que el proceso padre no sea destruido. Podemos ver este mecanismo en el siguiente código de ejemplo: #include #include #include #include <unistd.h> <sys/wait.h> <stdio.h> <stdlib.h> int main() { pid_t pid; int status, died; char *args[] = {"/bin/ls", "-r", "-t", "-l", (char *) 0 }; switch(pid=fork()) { case -1: printf("can’t fork\n"); exit(-1); case 0 : printf("child\n"); // código que ejecuta el hijo execv("/bin/ls", args); default: died= wait(&status); // código que ejecuta el padre } return(0); } 11 10. Señales entre procesos 10. 10.1. Señales entre procesos Introducción y documentación Las señales entre programas son interrupciones software que se generan para informar a un proceso de la ocurrencia de un evento. Otras formas alternativas o complementarias de comunicación entre procesos son las tuberías POSIX (funciones popen y relacionadas) y las las colas de mensajes POSIX o POSIX message queues (funciones mq_open, mq_send. . . ). Los programas pueden diseñarse para capturar una o varias señales proporcionando una función que las maneje. Este tipo de funciones se llaman técnicamente callbacks o retrollamadas. Una callback es una referencia a un trozo de código ejecutable, normalmente una función, que se pasa como parámetro a otro código. Esto permite, por ejemplo, que una capa de bajo nivel del software llame a la subrutina o función definida en una capa superior (ver Figura 5, fuente Wikipedia19 ). Application program Main program Callback function calls calls Library function Software library Figura 5: Esquema del funcionamiento de las callbacks o retrollamadas. Por ejemplo, cuando se apaga GNU/Linux, se envía la señal SIGTERM a todos los procesos, así los procesos pueden capturar esta señal y terminar de forma adecuada, por ejemplo liberando recursos, cerrando ficheros abiertos, etc. La función signal20 permite asociar una determinada función (a través de un puntero a función) a una señal identificada por un entero (SIGTERM, SIGKILL, etc.). #include <signal.h> sighandler_t signal(int signum, sighandler_t handler); ... 10.2. Ejercicios y ejemplos El código de ejemplo ejemplo-signals.c21 contiene ejemplos captura de señales POSIX enviadas a un programa. Recuerda que la función signal() no llama a ninguna función, 19 http://en.wikipedia.org/wiki/Callback_%28computer_science%29 http://pubs.opengroup.org/onlinepubs/9699919799/functions/signal.html 21 Fuente http://www.amparo.net/ce155/signals-ex.html 20 12 Programación y Administración de Sistemas– 2011-2012 lo que hace es asociar una función del programador a eventos que se generan en el sistema, esto es, pasar un puntero a una función. $ ./ejemplo-signal Cannot handle SIGKILL! I caught the SIGHUP signal! I caught the SIGTERM signal! Terminado (killed) # En otra terminal: obtener el PID del proceso $ ps -e|grep signal 31188 pts/0 00:00:05 ejemplo-signal $ kill -SIGHUP 31188 $ killall ejemplo-signal # mandamos señal SIGTERM $ killall ejemplo-signal -9 # Forzamos parar el programa con SIGKILL 11. 11.1. sockets POSIX Introducción y conceptos básicos En esta sección haremos una pequeña introducción a los sockets POSIX, una herramienta que permite desarrollar aplicaciones que se comuniquen entre sí a través de una red utilizando diferentes protocolos o facilitando el desarrollo de otros protocolos sobre TCP/IP. Como esta no es una asignatura específica de redes, aprenderemos algunos conceptos y algoritmos básicos para la comunicación pero sin entrar en detalle del funcionamiento y de todas las opciones disponibles. Si tienes duda sobre alguna función, hay un buen resumen en castellano con los pasos y funciones para utilizar sockets en la web chuidiang.com [7], nosotros usaremos algunas descripciones de este manual por ser muy sencillas y claras. A continuación daremos algunas definiciones. Protocolo: En una red de ordenadores hay varios ordenadores que están conectados entre si por un medio físico (cable, ondas inalámbricas, etc.) a través del cuál puede transmitirse información. Para que exista esta comunicación debe haber un “idioma” o protocolo común entre los equipos. Hay muchísimos protocolos de comunicación, entre los cuales el más extendido es el TCP/IP. El más extendido porque es el que se utiliza en Internet. socket: Un socket no es más que un “canal de comunicación” entre dos programas que corren sobre ordenadores distintos o incluso en el mismo ordenador. Desde el punto de vista de programación, un socket no es más que un “fichero” que se abre de una manera especial. Una vez abierto se pueden escribir y leer datos de él con las habituales funciones de read() y write() del lenguaje C. Existen básicamente dos tipos de “canales de comunicación” o sockets, los orientados a conexión (por ejemplo TCP) y los no orientados a conexión (por ejemplo UDP). Nosotros veremos un ejemplo de funcionamiento de sockets orientados a la conexión. arquitectura cliente-servidor: La arquitectura cliente-servidor es un modelo de aplicación distribuida en el que las tareas se reparten entre los proveedores de recursos o servicios, 13 11. sockets POSIX llamados servidores, y los demandantes, llamados clientes. Por ejemplo, la World Wide Web (WWW) se implementa con este modelo. El servidor es el programa que permanece pasivo a la espera de que alguien solicite conexión con él, normalmente, para pedirle algún dato. Cliente es el programa que solicita la conexión para, normalmente, pedir datos al servidor. 11.2. Algoritmo básico Cliente-Servidor La Figura 6 muestra el diagrama de flujo general de la comunicación Cliente-Servidor utilizando sockets. Diagrama de flujo de un socket TCP implementando el modelo Cliente-Servidor Servidor socket() bind() Cliente listen() socket() accept() connect() send() recv() closeSocket() El cliente envía datos y el servidor los recive El servidor envía datos y el cliente los recive El cliente manda un mensaje para finalizar la conexión recv() send() recv() closeSocket() Figura 6: Diagrama de flujo de la implementación del modelo Cliente-Servidor orientado a la conexión con sockets POSIX. 14 Programación y Administración de Sistemas– 2011-2012 11.3. Pasos de programación C en el lado Servidor Con C en Unix/Linux, los pasos que debe seguir un programa servidor son los siguientes [7]: Apertura de un socket, mediante la función socket(). Esta función devuelve un descriptor de fichero normal, como puede devolverlo open() o fopen(). La función socket() no hace absolutamente nada respecto a iniciar la comunicación, salvo devolvernos y preparar un descriptor de fichero que el sistema posteriormente asociará a una conexión en red. Avisar al sistema operativo de que hemos abierto un socket y queremos que asocie nuestro programa a dicho socket. Se consigue mediante la función bind(). El sistema todavía no atenderá a las conexiones de clientes, simplemente anota que cuando empiece a hacerlo, tendrá que avisarnos a nosotros. Es en esta llamada cuando se debe indicar el número de servicio (o puerto) al que se quiere atender. Avisar al sistema de que comience a atender dicha conexión de red. Se consigue mediante la función listen(). A partir de este momento el sistema operativo anotará la conexión de cualquier cliente para pasárnosla cuando se lo pidamos. Si llegan clientes más rápido de lo que somos capaces de atenderlos, el sistema operativo hace una “cola” con ellos y nos los irá pasando según vayamos pidiéndolo. Pedir y aceptar las conexiones de clientes al sistema operativo. Para ello hacemos una llamada a la función accept(). Esta función le indica al sistema operativo que nos dé al siguiente cliente de la cola. Si no hay clientes se quedará bloqueada hasta que algún cliente se conecte. Escribir y recibir datos del cliente, por medio de las funciones write() y read(), que son exactamente las mismas que usamos para escribir o leer de un fichero. Obviamente, tanto cliente como servidor deben saber qué datos esperan recibir, qué datos deben enviar y en qué formato, por ejemplo el protocolo HTTP y el formato HTML o XHTML. Cierre de la comunicación y del socket, por medio de la función close(), que es la misma que sirve para cerrar un fichero. 11.4. Pasos de programación C en el lado Cliente Nosotros no veremos la parte del cliente, ya que cualquier navegador web será el cliente de nuestro servidor. Los pasos que debe seguir un programa cliente son los siguientes [7]: Apertura de un socket, como el servidor, por medio de la función socket(). Solicitar conexión con el servidor por medio de la función connect(). Dicha función quedará bloqueada hasta que el servidor acepte nuestra conexión o bien si no hay servidor en el sitio indicado, saldrá dando un error. En esta llamada se debe facilitar la dirección IP del servidor y el número de servicio que se desea. Escribir y recibir datos del servidor por medio de las funciones write() y read(). Cerrar la comunicación por medio de close(). 12. Todo junto: un servidor web básico en C 15 Figura 7: Ejemplo de ejecución del servidor web con la llamada ejemplo-webserver -p 8080 -r htdocs 12. 12.1. Todo junto: un servidor web básico en C Introducción y código En esta sección vamos a ver un ejemplo que combina muchas de las funciones POSIX que hemos visto en las prácticas. Esta sección la trabajaremos directamente sobre el código de ejemplo ejemplo-webserver.c. Este ejemplo es un código ampliado descargado de internet con algunas funciones auxiliares que se han añadido para facilitar los ejercicios. Pregunta todas las dudas que tengas durante la explicación del código. El objetivo es tratar de reconocer los pasos del modelo Cliente-Servidor de la Figura 6 a grandes rasgos y sin perderse en los detalles, ya que no se pedirá ninguna modificación de las partes de código relativas a la comunicación.. La Figura 8 muestra el funcionamiento general del código del servidor web combinando el esquema de la Figura 6 con fork() para poder atender varias conexiones de manera simultánea. 16 Programación y Administración de Sistemas– 2011-2012 Servidor Diagrama de flujo del ejemplo de servidor web básico El socket creado por el proceso principal debería ser cerrado al finalizar el programa, por ejemplo capturando las señales SIGTERM, SIGINT... socket() startServer() bind() listen() Cliente La llamada a accept() crea un nuevo socket para atender la conexión que deberá ser cerrado al finalizar el intercambio de datos. socket() accept() connect() while(1) { ... fork() ... } fork() GET /hola.html HTTP/1.1 recv() send() respond() HTTP/1.0 200 OK .... Datos send() recv() closeSocket() El cliente manda un mensaje para finalizar la conexión recv() closeSocket() Figura 8: Diagrama de flujo de la implementación del modelo Cliente-Servidor orientado a la conexión con sockets POSIX. 12.2. Ejercicios Arranca y para el servidor siguiendo la ayuda de la línea de comandos. Prueba a conectarte a él a través del navegador y prueba a cambiar las opciones. Si no sabes HTML, mira los ficheros de ejemplo del directorio htdocs del paquete de prácticas. Para arrancar el servidor, que escuche en el puerto 8080 y que utilice el directorio htdocs como raíz para servir ficheros web: $ ./ejemplo-webserver -p 8080 -r htdocs ahora visita la dirección http://localhost:8080/ y comprueba que puedes navegar por las páginas alojadas en el servidor. En el navegador deberías obtener un resultado similar al de la Figura 7. Puedes probar a ejecutar el servidor en ts.uco.es, en este caso la dirección 17 13. Ejercicio resumen 2 (1 punto) sería http://ts.uco.es:8080/. Si esto lo hacéis varias personas, tal vez tengáis que cambiar el puerto porque sólo una aplicación puede escuchar en un puerto, este mensaje de error os indicaría que ya hay una aplicación asociada al puerto que pretendéis usar: $ ./ejemplo-webserver -p 8080 -r htdocs $ socket() or bind(): Address already in use Este mensaje de error puede obtenerse aún cuando haya finalizado la ejecución de programa que escucha el puerto. Aunque el documento es antiguo y la explicación tal vez no sea precisa, puedes leer una explicación de por qué pasa esto en [8] 13. Ejercicio resumen 2 (1 punto) Para este ejercicio partiremos del código ejemplo-webserver.c. El Makefile del paquete de prácticas ya incluye una regla para compilar el fichero webserverex.c. Ejercicios: 1. La función webServerLog() implementa un pequeño sistema de registro o log (no óptimo, por supuesto). Esta función es similar a printf(). Utilizándola se pueden registrar mensajes de inicio o parada del servidor o los accesos al servidor en el fichero access.log y los errores en el fichero error.log. Por ejemplo, puedes hacer las siguientes llamadas: webServerLog(WS_LOG_ACCESS, \ "Servidor iniciado en el puerto %s", PORT); webServerLog(WS_LOG_ERROR, "Error en la conexión"); No necesitas reemplazar todos los mensajes, pero si los más significativos: cuando no se puede iniciar el servidor por estar el puerto ocupado, el inicio o parada del servidor y las peticiones HTTP que llegan al servidor. 2. Modifica el código del servidor web para que tenga una nueva opción -f. Cuando se pase esta opción, el servidor deberá incluir una foto del Fary en todas las páginas web que sirva. Una imagen se incluye en HTML con el código <img src="fichero.jpg" alt="Texto alternativo"> insertado entre las etiquetas <BODY> y </BODY>, que es donde va el contenido en si de una página HTML. Figura 9: El Fary 18 Programación y Administración de Sistemas– 2011-2012 Ten en cuenta que el navegador hará una petición por cada fichero .html, y otra(s) por cada imagen (u otros datos) contenidos en la web. Así, si una web tiene una imagen, el navegador pedirá primero el fichero .html con un GET /hola.html HTTP/1.1 y cuando lo abra y detecte la etiqueta <img ...> hará otra petición GET /fichero.jpg HTTP/1.1 para que el servidor le envíe el fichero de la imagen22 . Ayúdate y modifica si lo necesitas las funciones auxiliares strcasestr()23 y copyfile(). Acuérdate de actualizar la ayuda del programa. 3. Captura algunas llamadas al sistema (SIGTERM, SIGINT y SIGHUP) para gestionar el fin del programa adecuadamente. Puedes asociar estas tres señales con la misma función de parar el servidor. Se deberían finalizar y cerrar adecuadamente las conexiones y ficheros abiertos. Opcionalmente puedes hacer un script de bash que arranque y pare el servidor ejecutando el servidor y enviándole la señal SIGTERM respectivamente. Por ejemplo, start-server.sh y stop-server.sh. 4. Captura la URL solicitada por el cliente. Si se pide la URL http://servidor:puerto/ infousuario/i02samoj el servidor debe generar una web con la información del usuario. Con el código del ejercicio resumen 1 y usando un fichero temporal no debería ser muy difícil. Para el segundo ejercicio, una llamada similar a $ ./ejemplo-webserver -p 8080 -r htdocs/ -f debería producir un resultado similar en el navegador al de la Figura 10. 14. Trabajo futuro Algunas ideas que no ha dado tiempo a hacer: Utilizar el patrón de diseño singleton para guardar la configuración del programa. Controlar la destrucción de subprocesos del servidor pasado un tiempo. Comunicación inter-procesos con diferentes alternativas (tuberías, POSIX MQ, etc.). Capturar cómo ha muerto un proceso WEXITSTATUS, WTERMSIG... Hacer que el servidor sirva código generado con PHP (por ejemplo recogiendo la salida de php ejemplo.php) 22 La petición de estos ficheros auxiliares se hace al servidor que sirve la web, pero también se puede hacer a otro si el fichero se encuentra en otro sitio web. Esto ocurre por ejemplo cuando se inserta un vídeo de youtube o similares en una página. 23 En el código del servidor web se proporciona la función strcasestr(cadena, subcadena) para facilitar uno de los ejercicios. Esta función busca una subcadena dentro de una cadena. Devuelve un 0 si no encuentra la subcadena o la posición de la subcadena si la encuentra, es decir, un valor mayor que cero. 14. Trabajo futuro 19 Figura 10: Ejemplo del resultado de la ejecución del servidor web para implementar la opción -f. 20 Programación y Administración de Sistemas– 2011-2012 Referencias [1] Wikipedia. Posix – wikipedia, la enciclopedia libre, 2012. [Internet; descargado 12abril-2012]. Available from: http://es.wikipedia.org/w/index.php?title= POSIX&oldid=53746603. [2] The IEEE and The Open Group. Posix.1-2008 – the open group base specifications issue 7, 2008. Available from: http://pubs.opengroup.org/onlinepubs/9699919799/. [3] Proyecto GNU. Gnu c library, 2012. software/libc/libc.html. Available from: http://www.gnu.org/ [4] Brian W. Kernighan, Dennis Ritchie, and Dennis M. Ritchie. C Programming Language (2nd Edition). Pearson Educación, 2 edition, 1991. [5] Wikipedia. Glibc – wikipedia, la enciclopedia libre, 2012. [Internet; descargado 12abril-2012]. Available from: http://es.wikipedia.org/w/index.php?title= Glibc&oldid=53229698. [6] Tim Love. Fork and exec, 2008. Available from: http://www-h.eng.cam.ac.uk/ help/tpl/unix/fork.html. [7] chuidiang.com. Programación de sockets en c de unix/linux, 2007. Available from: http://www.chuidiang.com/clinux/sockets/sockets_simp.php. [8] Andrew Gierth Vic Metcalfe and other contributers. Programming UNIX Sockets in C - Frequently Asked Questions. 4.2 Why don’t my sockets close?, 1996. Available from: http://www.softlab.ntua.gr/facilities/documentation/unix/ unix-socket-faq/unix-socket-faq-4.html#ss4.2. [9] Wikipedia. Dennis ritchie – wikipedia, la enciclopedia libre, 2012. Available from: http: //es.wikipedia.org/wiki/Dennis_Ritchie.