PRÁCTICA 2: Cliente-servidor UDP El objetivo de esta práctica es la familiarización con aplicaciones elementales que usan los servicios de transporte de los sockets UDP. A lo largo de la práctica se realizarán diversas pruebas con una aplicación sencilla cliente/servidor escrita en lenguaje C. El servicio que realiza la aplicación distribuida consiste en una especie de servidor de echo; es decir, el servidor recibe un mensaje y lo reenvía de nuevo al cliente que se lo envió. Recuerda que cuando la comunicación entre dos procesos de aplicación usa los servicios de transporte UDP, cada extremo de la comunicación identifica a su socket UDP por la dirección y puerto local (por lo tanto, un paquete que llega a un equipo será entregado al socket cuyo identificador local coincida con la dirección IP destino y puerto destino del segmento UDP). INFORMACIÓN SOBRE LAS FUNCIONES DE SOCKETS Durante esta práctica se usarán las funciones más comunes para trabajar con sockets UDP. Pueden encontrar información detallada de ellas en la ayuda del manual de Linux. Esto lo puede encontrar: En Linux, escribiendo en el terminal: man <sección> función Donde sección es opcional. La ayuda del man está dividida en secciones, según la temática. Es posible que la palabra que se busque esté en varias secciones y a veces hay que indicar a cuál nos referimos. Todas las funciones que vamos a utilizar están en las secciones 2 y 3. En http://ait08.us.es/dwww/ hay un buscador de ayuda. En http://trajano.us.es/docencia/laboratoriodecomunicaciondedatos/recursos/m an-html/html/manpageindex.html están recopiladas todas las páginas del manual de Linux. Haciendo una búsqueda con cualquier buscador de Internet. REALIZACIÓN Una vez que sabes dónde buscar información sobre sockets, puedes comenzar buscando información sobre la función socket() en el manual (p.e. Puedes buscar en ait08.us.es con http://ait08.us.es/cgi-bin/dwww?type=runman&location=socket/2 ). Responde a las siguientes preguntas: 1. qué hace la llamada a socket () y qué devuelve? 2. cuántos servicios de transporte diferentes pueden usar las aplicaciones a través de los sockets? cómo se llama el tipo de sockets que ofrece el servicio de transporte de UDP? y el de TCP? Pág. 1
BLOQUE 1: FUNCIONAMIENTO BÁSICO DE LA PROGRAMACIÓN CON SOCKETS (I) El cliente solicita al usuario que escriba un mensaje por teclado (menos de 256 caracteres) y lo envía al servidor. Éste lo devuelve al cliente. Al ejecutar el cliente se le deben indicar por línea de comandos 3 parámetros: el identificador del socket UDP asociado al proceso con el que nos queremos comunicarnos; es decir, (a) la dirección IP o el nombre de la máquina destino y (b) el puerto asociado al proceso servidor../cliente_udp maquina_destino puerto El servidor escucha continuamente a través de su socket UDP cuyo puerto es especificado como parámetro en línea de comandos, y responde a todo el que le envíe un mensaje con el mismo mensaje. El servidor se ejecutaría con:./servidor_udp puerto Los códigos del cliente y el servidor se puede descargar desde la página web http://masai.us.es/practica2/bloque1. Ambos códigos se describen a continuación. CÓDIGO DEL CLIENTE Abre el código del cliente y tómate 10 minutos para estudiarlo y comprender el uso de las funciones que se utilizan. /******************************************************************* * Cliente de eco remoto sobre UDP: * * cliente dir_ip_maquina puerto * * manda mensaje a maquina, quien responde un solo mensaje * que el cliente imprime. ********************************************************************/ #include <sys/types.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #define TAMANO 256 /* tamano del buffer de recepcion */ void error_fatal(char *); /* Imprimer un mensaje de error y sale del programa */ int main(int argc, char *argv[]) int sock; /* descriptor del socket del cliente */ int length, n; struct sockaddr_in server, from; /* direcciones del socket del servidor y cliente */ struct hostent *hp; /* estructura para el nombre del servidor (ver gethostbyname) */ char buffer[tamano]; /* buffer de recepcion y envio del mensaje */ if (argc!= 3) fprintf(stderr,"uso: servidor puerto\n"); exit(1); /* (1) creacion del socket UDP del cliente */ sock= socket(af_inet, SOCK_DGRAM, 0); if (sock < 0) error_fatal("socket"); Pág. 2
server.sin_family = AF_INET; /*dominio de Internet*/ /* (2) averigua la direccion IP a partir del nombre del servidor*/ hp = gethostbyname(argv[1]); if (hp==0) error_fatal("host desconocido"); /* (3) copia la IP resuelta anteriormente en la direccion del socket del servidor */ memcpy((char *)&server.sin_addr, (char *)hp->h_addr, hp->h_length); /* (4) copia el puerto destino en la direccion del socket del servidor */ server.sin_port = htons(atoi(argv[2])); length=sizeof(struct sockaddr_in); printf("por favor, introduce el mensaje: "); memset(buffer,0,tamano); /*limpio el buffer*/ fgets(buffer,tamano-1,stdin); /* (5) envia al socket del servidor el mensaje almacenado en el buffer*/ n=sendto(sock,buffer, strlen(buffer),0,&server,length); if (n < 0) error_fatal("sendto"); /* (6) lee del socket el mensaje de respuesta del servidor*/ n = recvfrom(sock,buffer,tamano,0,&from, &length); if (n < 0) error_fatal("recvfrom"); buffer[n]='\0'; /* para poder imprimirlo con prinft*/ printf("recibido en el cliente: %s\0", buffer); /*cerramos el socket*/ if(close(sock) < 0) error_fatal("close"); void error_fatal(char *msg) perror(msg); exit(1); Listado 1. Programa cliente (cliente.c) La función de error_fatal imprime un mensaje y el código de error de la última operación efectuada, mediante la función predefinida perror. Seguidamente abandona el programa. Esta función se utilizará cuando se produzca un error irrecuperable del programa y se quiera salir inmediatamente de él. La función main crea un socket, prepara la dirección del socket UDP destinatario del mensaje (la IP y el puerto que identifican al socket en el servidor), y solicita que el usuario introduzca un mensaje. Una vez introducido el mensaje, se lo envía al servidor y se queda a la espera de la respuesta. Una vez recibida la respuesta del servidor la imprime en pantalla. Por último se cierra el socket abierto para liberar todos los recursos asociados a él en el sistema. Es importante que los sockets no se queden abiertos. Si el programa termina usando la función exit, ésta se encarga de cerrar todos los ficheros y sockets abiertos. Observe que cuando un número entero ocupa más de un byte en memoria, sus bytes se pueden almacenar bien escribiendo primero el byte más significativo (orden Big Endian) o escribiendo primero el byte menos significativo (orden Little Endian). Cuando se transmiten enteros, muchos protocolos, por convención, requieren que estos se transmitan en Big Endian (orden de bytes de red), mientras que en las máquinas estos no tienen que estar almacenados así (por ejemplo, los microprocesadores Intel usan el Pág. 3
orden Little Endian). Las direcciones IP y los puertos UDP y TCP se transmiten en orden de bytes de red, así que tenemos que convertirlos si nuestra máquina no usa ese orden. Para ello están las funciones htons (host to network short 16 bits), htonl (host to network long - 32 bits) y sus inversas ntohs (network to host) y ntohl. Una dirección IPv4 ocupa 4 bytes (32 bits) mientras que un número de puerto ocupa 2 bytes (16 bits). Estudie atentamente cada uno de los pasos indicados en el código y responda a las siguientes preguntas: SERVIDOR 3. qué campos tiene, y para qué se usan, la variable server? y la variable hp? (si no encuentras mejores fuentes, puedes mirar en http://es.tldp.org/tutoriales/prog-sockets/prog-sockets.html) 4. Describa el funcionamiento de las principales funciones relativas al uso de sockets utilizadas en cada uno de los pasos anteriores (cada paso esta indicado con un número en el comentario correspondiente). Busque en las fuentes mencionadas anteriormente. En su respuesta indique claramente la utilidad de cada función y los principales parámetros que recibe y devuelve. Algunas de las funciones que debe buscar son: socket, gethostbyname, sendto, recvfrom. 5. Compile el programa cliente.c y llame al ejecutable cliente. ha obtenido algún aviso?(si/no) Estudie el código del servidor. /************************************************ * Crea un servidor UDP. Recibe como argumento * en linea de comandos el numero de puerto. El servidor * nunca termina su ejecucion ***************************************/ #include <sys/types.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #define TAMANO 256 void error_fatal(char *msg) perror(msg); exit(1); int main(int argc, char *argv[]) int sock, length, fromlen, n; struct sockaddr_in server; struct sockaddr_in from; char buffer[tamano]; if (argc < 2) fprintf(stderr, "ERROR, no se ha indicado el puerto \n"); exit(1); /* (1) creacion del socket del servidor*/ Pág. 4
sock=socket(af_inet, SOCK_DGRAM, 0); if (sock < 0) error_fatal("creando el socket"); length = sizeof(server); memset(&server,0,length); /*limpio la direccion del socket del servidor*/ /* (2) vinculo la direccion IP y puerto local al socket creado anteriormente */ server.sin_family=af_inet; server.sin_addr.s_addr=inaddr_any; server.sin_port=htons(atoi(argv[1])); if (bind(sock,(struct sockaddr *)&server,length)<0) error_fatal("binding"); fromlen = sizeof(struct sockaddr_in); /* (3) bucle principal. Pongo a recibir y responder mensajes a traves del socket*/ while (1) n = recvfrom(sock,buffer,tamano,0,(struct sockaddr *)&from,&fromlen); if (n < 0) error_fatal("recvfrom"); /*datagrama recibido*/ buffer[n]='\0'; /* para poder imprimirlo con prinft*/ printf("recibido en el servidor: %s", buffer); /*enviar respuesta*/ n = sendto(sock,"recibi tu mensaje\n",17, 0,(struct sockaddr *)&from,fromlen); if (n < 0) error_fatal("sendto"); Listado 2. Programa servidor (servidor.c) El servidor crea un socket UDP asociado al puerto indicado por línea de comandos. Para ello asocia al descriptor del socket creado una dirección de socket con la función bind. La constante INADDR_ANY es una dirección comodín e indica al sistema que la dirección IP será cualquier dirección IP propia que elegirá el sistema. Responda a la siguiente pregunta: 6. cómo funciona la función bind?(parámetros de la función) por qué piensa que el cliente no la ha usado? Una vez vinculado el socket del servidor, entra en un bucle infinito donde se reciben las peticiones de los clientes y se devuelven las respuestas correspondientes. Ahora compile el programa servidor server.c y llame al ejecutable servidor. Realice las siguientes pruebas. Abra un nuevo terminal y ejecute en él el servidor en modo background (p.e. %>./servidor 20000 & ) En otro terminal, ejecute el cliente y dirija sus peticiones al servidor (use la dirección de bucle local 127.0.0.1). (p.e. %> cliente 127.0.0.1 20000 ) 7. qué se ha mostrado en el terminal donde ejecuta el cliente? y donde se ejecuta el servidor? 8. Ahora busque la dirección IP de su ordenador, y ponga dicha dirección como dirección IP del servidor al ejecutar el cliente. obtiene el mismo resultado?. 9. Pruebe a realizar una comunicación entre dos equipos con algún compañero. Recuerde que debe averiguar la dirección IP del ordenador que ejecuta el Pág. 5
servidor antes de ejecutar el cliente. qué dirección IP y puertos tienen el cliente? y el servidor? (puede mirarlo con el comando netstat) o mostrarlos por pantalla modificando el código del programa. Nota, si no encuentras un compañero puedes usar (desde fuera del CdC) la máquina masai.us.es como servidor (el programa servidor se ejecuta asociado al puerto 20000). BLOQUE 2. FUNCIONES BÁSICAS DE MANEJO DE SOCKETS (II) En este segundo bloque vamos a examinar una nueva versión del programa anterior más completa donde se realizarán las siguientes mejoras: El mensaje se introduce como parte de la línea de comandos en el cliente El servidor se cierra si no recibe mensajes en un tiempo máximo (temporizador) Se utilizan funciones avanzadas y actualizadas de traducción de nombres de máquinas y servicios (consultas a bases de datos de red). Puedes descargar el código en http://masai.us.es/practica2/bloque2. Tómate 15 minutos para examinar el código del cliente (llamado reco) y del servidor (llamado recod), y Responde a las siguiente pregunta: 10. cuáles son, a tu juicio, las principales diferencias entre esta aplicación y la del bloque 1? A continuación se explican algunas de las nuevas funciones utilizadas en este nuevo código. FUNCIONES DE TRADUCCIÓN DE NOMBRES DE MÁQUINAS Y SERVICIOS Consulta de bases de datos de red Ahora, para hallar la dirección de la máquina destino a partir de su nombre, vamos a utilizar una función diferente a gethostbyname (considerada obsoleta en la actualidad): la función getaddrinfo. Esta función también intentará averiguar la dirección IP usando el servicio DNS (Domain Name System), buscando previamente en el archivo hosts del sistema (donde se han almacenado los nombres ya resueltos). El archivo hosts esta localizado en diferentes lugares según el sistema operativo que se utilice (ver http://en.wikipedia.org/wiki/hosts_(file) ). Como ya sabes, si ejecutas netstat -a en un terminal obtendrás una lista de sockets abiertos junto con su identificador. En dicha lista a veces aparece el nombre de un servicio en lugar de su número de puerto asociado al socket del proceso servidor. El fichero services del sistema nos permite asociar un nombre a un puerto de un servidor (p.e. el nombre http al puerto 80). Para hallar el puerto del servicio a partir de su nombre también se puede usar la función getaddrinfo. Para la traducción inversa, de dirección IP a nombre de máquina o de puerto a nombre de servicio, se puede utilizar la función getnameinfo. Compruebe que el nombre y Pág. 6
dirección IP de los equipos involucrados en sus pruebas de laboratorio aparecen en el fichero hosts de su equipo. Si no es así, introdúzcalos a mano. En este segundo bloque se han usado la funciones getaddrinfo y getnameinfo ya que son compatibles con IPv4 y IPv6 (gethostbyname sólo es válida para IPv4, y es considerada obsoleta). Para evitar tener que llamarlas directamente, se han creado dos funciones en el fichero traduccion.c que nos abstrae de su complejidad: traduce_a_direccion e imprime_extremo_conexion. Uso de getaddrinfo en traduce_a_direccion A la función getaddrinfo hay que pasarle el nombre o dirección de la máquina como cadena de texto (por ejemplo, localhost o 127.0.0.1 ), el nombre del servicio o el número de puerto como cadena de texto (por ejemplo, http o 80 ) y una serie de sugerencias o pistas (hints) para hacer la búsqueda: Familia de protocolos: IPv4, IPv6, ambos, Tipo de socket: datagrama, flujo, Etc. Para las prácticas solo vamos a utilizar 3 tipos de sockets: UDP para esta práctica y TCP de escucha (o pasivo) y TCP normal (de cliente o activo) para la práctica siguiente. Se han creado constantes para indicar cada uno de estos tipos: SOCKET_UDP, SOCKET_TCP y SOCKET_TCP_PASIVO. A cambio, getaddrinfo nos devuelve una lista de todas las direcciones que cumplen los requisitos indicados como parámetros, y un código de error. Esta lista hay que liberarla posteriormente con la función freeaddrinfo. Desde un punto de vista estricto habría que probar la conexión con cada una de las direcciones que nos devuelve hasta que una de ellas tenga éxito, pero para simplificar la práctica solo utilizaremos la primera dirección que nos devuelva y el resto solo las mostraremos por pantalla. Toda esta información se podrá usar luego para crear el socket. A continuación se muestra el listado de la función traduce_a_direccion. Busque información sobre las funciones getaddrinfo y freeaddrinfo. /* Convierte una direccion de Internet y un puerto de servicio (ambos cadena de caracteres) a valores numericos para poder ser utilizados en otras funciones, como bind y connect. La informacion tambien se imprimira por pantalla. Parametros de entrada: - maquina - cadena de caracteres con la direccion de Internet - puerto - cadena de caracteres con el puerto de servicio - tipo - SOCKET_UDP, SOCKET_TCP o SOCKET_TCP_PASIVO (escucha) Parametros de salida: - info - estructura addrinfo con el primer valor encontrado Devuelve: - Verdadero, si ha tenido exito. */ int traduce_a_direccion(const char *maquina, const char *puerto, int tipo, struct addrinfo *info) struct addrinfo hints; /* Estructura utilizada para afinar la Pág. 7
busqueda */ struct addrinfo *result, *rp; /*rp, variable usada para recorrer la lista de direcciones encontradas */ int error = 0; /* Obtiene las direcciones que coincidan con maquina/puerto */ /* Ponemos a 0 la estructura hints */ memset(&hints, 0, sizeof(struct addrinfo)); /*Inicializamos la estructura */ hints.ai_family = AF_INET; /* AF_UNSPEC Permite IPv4 o IPv6 AF_INET solo IPv4 */ if (SOCKET_UDP == tipo) hints.ai_socktype = SOCK_DGRAM; /* Socket de datagramas */ else hints.ai_socktype = SOCK_STREAM; /* Socket de flujo */ hints.ai_protocol = 0; /* Cualquier protocolo */ if (SOCKET_TCP_PASIVO == tipo) hints.ai_flags = AI_PASSIVE; /* Cualquier direccion IP */ /*Llamamos a la funcion de busqueda de nombres */ error = getaddrinfo(maquina, puerto, &hints, &result); if (error!= 0) //Mostramos informacion del error usando la funcion gai_strerror fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(error)); else /* getaddrinfo() devuelve una lista de estructuras addrinfo. Vamos a imprimirlas todas, aunque solo devolveremos la primera */ printf("resultado de la resolucion de nombre:\n"); for (rp = result; rp!= NULL; rp = rp->ai_next) printf("-> "); imprime_extremo_conexion(rp->ai_addr, rp->ai_addrlen, tipo); printf("\n"); if (result == NULL) /* No se ha devuelto ninguna direccion */ fprintf(stderr, "No se han encontrado direcciones.\n"); error = 1; //Hay error else //Copiamos solo los campos del primer resultado que interesan. info->ai_family = result->ai_family; info->ai_socktype = result->ai_socktype; info->ai_protocol = result->ai_protocol; *info->ai_addr = *result->ai_addr; //Copiamos contenido del //puntero info->ai_addrlen = result->ai_addrlen; freeaddrinfo(result); /* Liberamos los datos */ return!error; Listado 3. Función traduce_a_direccion del fichero traduccion.c Pág. 8
Uso de getnameinfo en imprime_extremo_conexion A veces querremos imprimir por pantalla información sobre la dirección de un socket. Para ellos vamos a utilizar la función imprime_extremo_conexion que a su vez hace uso de la función getnameinfo. A la función getnameinfo se le pasa el identificador de un socket (dirección IP y puerto), y nos rellena dos tablas de caracteres, una con información de la máquina y otra con información del puerto. Mediante opciones podemos indicarle si queremos esa información en formato numérico o como nombre de máquina o servicio. Como un mismo número de puerto puede corresponder a servicios distintos en UDP y TCP, es necesario indicar el tipo de socket que estamos usando. A continuación se muestra el listado de la función imprime_extremo_conexion. Busque información sobre la función getnameinfo. /* Funcion que imprime el nombre de la maquina asociada a una direccion de internet y el puerto de una conexion. Hace uso de la funcion getnameinfo. Parametros de entrada: - direccion - estructura sockaddr con informacion de un extremo del socket. - len - longitud de la estructura direccion - tipo - SOCKET_UDP o SOCKET_TCP Devuelve: - Nada */ void imprime_extremo_conexion(const struct sockaddr *direccion, socklen_t len, int tipo) char hbufnum[ni_maxhost]; //cadena de la maquina (numerico) char hbufnombre[ni_maxhost]; //cadena de la maquina (nombre) char sbuf[ni_maxserv]; //cadena del servicio int opciones = NI_NUMERICHOST NI_NUMERICSERV; //Opciones para getnameinfo int error = 0; if (tipo == SOCKET_UDP) opciones = NI_DGRAM; //Convertimos a cadena de caracteres error = getnameinfo(direccion, len, hbufnum, sizeof(hbufnum), sbuf, sizeof(sbuf), opciones); if (error == 0) //Obtenemos tambien el nombre asociado a esa direccion IP if (tipo == SOCKET_TCP_PASIVO) printf("escuchando en "); else if (getnameinfo(direccion, len, hbufnombre, sizeof(hbufnombre), NULL, 0, NI_NAMEREQD)) //Error obteniendo el nombre printf("maquina=(desconocida) "); else printf("maquina=%s ", hbufnombre); //Imprimimos valores numericos. printf("(%s), Puerto=%s", hbufnum, sbuf); if (tipo == SOCKET_UDP) Pág. 9
printf(" UDP"); else printf(" TCP"); else //Mostramos informacion del error usando la funcion gai_strerror fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(error)); Listado 4. Función imprime_extremo_conexion del fichero traduccion.c CLIENTE reco El cliente consta de tres funciones: recibido_en_plazo, error_fatal y main. /******************************************************************* * Cliente de eco remoto sobre UDP: * * reco dir_ip_maquina puerto mensaje... * * manda mensaje a maquina, quien responde un solo mensaje * que el cliente imprime. ********************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include "traduccion.h" #define TAMANO 1000 /* tamanio de tampones */ #define PLAZO 20 /* plazo de recepcion */ /* Función error_fatal */ /* Función recibido_en_plazo (ver listado 6.) */ /* Función main del cliente eco (ver listado 7.) */ Listado 5. Programa cliente de eco (reco.c) La función booleana recibido_en_plazo permite esperar a que llegue algo por un descriptor de E/S en un plazo máximo de segundos, pasado el cual abandona (resultado falso). Busque información sobre la función select, que se utiliza para realizar esta tarea. int recibido_en_plazo(int descriptor, int segundos) struct timeval plazo = segundos, 0 ; /* plazo de recepcion */ fd_set readfds; //Conjunto de descriptores a monitorizar FD_ZERO(&readfds); //Limpiamos fds FD_SET(descriptor, &readfds); //Insertamos descriptor en el conjunto return select(descriptor + 1, &readfds, (fd_set *) NULL, (fd_set *) NULL, &plazo)!= 0; Listado 6. Función recibido_en_plazo (reco.c) La función main abre un socket y manda los mensajes que se indican en los parámetros. int main(int argc, char *argv[]) char buf[tamano]; /* tampon de recepcion */ Pág. 10
int s; /* descriptor del socket */ struct addrinfo infoserv; /* informacion extremo remoto */ struct sockaddr dirserv; /* direccion internet remota */ struct sockaddr from; /* direccion internet remota auxiliar */ socklen_t len; /* tamanio de la estructura sockaddr_in */ int i, cc; int cod_error = 0; /* 0 no hay error, 1 hay error */ /* Comprueba que reco tiene cuatro argumentos (ver listado 8) */ /* Prepara nombre remoto y crea socket de UDP (ver listado 9) */ /* Envía mensajes y recibe respuesta (ver listado 10) */ /* Cierra socket (ver listado 11) */ return cod_error; Listado 7. Función main del cliente eco (reco.c) Los argumentos del cliente deben ser cuatro (incluido su nombre): if (argc < 4) fprintf(stderr, "Uso: reco <direccion> <puerto> <mensaje>...\n"); cod_error = 1; else /* resto de la funcion */ Listado 8. Comprueba que reco tiene cuatro argumentos (reco.c) El tipo de socket debe ser de datagramas de internet (UDP). El nombre remoto se obtiene haciendo uso de la funcion traduce_a_direccion antes vista. A esta función hay que pasarle una estructura addrinfo (previamente iniciada a 0 con memset) para que nos la rellene con la información encontrada. La información devuelta se utiliza para crear el socket. /* Ponemos a 0 la estructura infoserv */ memset(&infoserv, 0, sizeof(struct addrinfo)); infoserv.ai_addr = &dirserv; //aqui se guardara la direccion //obtenemos datos del extremo al que queremos llamar if (!traduce_a_direccion(argv[1], argv[2], SOCKET_UDP, &infoserv)) //error traduciendo cod_error = 1; else //no ha habido error if ((s = socket(infoserv.ai_family, infoserv.ai_socktype, infoserv.ai_protocol)) < 0) error_fatal("socket"); Listado 9. Prepara nombre remoto y crea socket de UDP (reco.c) Tras esta preparación se pueden enviar datagramas a la máquina remota a través del socket y esperar las respuestas. Siempre se comprueba el valor devuelto por la función para detectar posibles fallos. for (i = 3; i < argc; i++) /* envia */ cc = sendto(s, argv[i], strlen(argv[i]), 0, infoserv.ai_addr, infoserv.ai_addrlen); if (cc < 0) error_fatal("sendto"); len = 0; Pág. 11
/* espera */ if (recibido_en_plazo(s, PLAZO)) len = sizeof(from); /* recibe */ cc = recvfrom(s, buf, sizeof(buf) - 1, 0, &from, &len); if (cc < 0) error_fatal("recvfrom"); buf[cc] = '\0'; /* Para poder usarlo con printf */ printf("recibido: %s\n", buf); else printf("*** plazo terminado ***\n"); Listado 10. Envía mensajes y recibe respuesta (reco.c) Por último se cierra el socket abierto para liberar todos los recursos asociados a él en el sistema. Es importante que los sockets no se queden abiertos. Si el programa termina usando la función exit, ésta se encarga de cerrar todos los ficheros y sockets abiertos. if (close(s) < 0) /*Cerramos el socket */ error_fatal("close"); /* fin del else de comprobacion de argumentos */ Listado 11. Cierra socket (reco.c) SERVIDOR recod El servidor de eco prepara un socket en un puerto de UDP y se pone a esperar mensajes de un cliente a dicho puerto. A continuación mostraremos la estructura del servidor y aquellas partes que difieran de lo mostrado en la versión vista en el bloque1. /******************************************************************* * Servidor de eco remoto sobre UDP: * recod puerto * recibe mensajes de clientes por el puerto especificado, * los presenta en pantalla y responde un mensaje igual. * Termina si lleva mas de 20 segundos sin recibir nada ********************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include "traduccion.h" #define TAMANO 1000 /* tamanio de tampones */ #define PLAZO 20 /* plazo de recepcion */ /* Función error_fatal */ /* Función recibido_en_plazo (ver listado 6) */ int main(int argc, char *argv[]) char buf[tamano]; /* tampon de recepcion */ int s; /* descriptor del socket */ struct sockaddr_in iyo; /* direccion internet local */ struct sockaddr iclnt; /* direccion internet remota */ socklen_t len; /* longitud de la estructura sockaddr */ Pág. 12
int cc; /* numero de caracteres leidos o escritos */ int cod_error = 0; /* 0 no hay error, 1 hay error */ /* Comprueba que recod tiene dos argumentos (ver listado 13.) */ /* Crea socket de UDP */ /* Prepara nombre local en puerto fijo */ /* Asigna direccion local al socket */ /* Recibe mensajes y envía respuesta (ver listado 14.) */ /* Cierra socket */ return cod_error; Listado 12. Programa servidor de eco (recod.c) La comprobación de argumentos es similar a la de reco: if (argc!= 2) fprintf(stderr, "Uso: recod <puerto>\n"); cod_error = 1; else /* resto de la funcion main */ Listado 13. Comprueba que recod tiene dos argumentos (recod.c) En el servidor no vamos a utilizar la función traduce_a_direccion, ya que no necesita traducir nada (el puerto se le pasa en formato numérico). El servidor se queda esperando la recepción de mensajes en el puerto correspondiente y cuando llegan mensajes los reenvía tal cual. La espera se hace en un plazo prudencial, tras el cual el servidor termina. For (;;) /* El servidor solo finaliza cuando pasa el plazo sin recibir nada, por eso a partir de ahora no se usa la función error_fatal */ if (recibido_en_plazo(s, PLAZO)) /* Recibe y responde lo mismo (ver listado 15) */ else printf("*** no oigo nada ***\n"); break; /*Salimos del bucle */ Listado 14. Recibe mensajes y envía respuesta (recod.c) Los mensajes que recibe los devuelve al que los envió, uno a uno. len = sizeof(iclnt); /* recibe */ cc = recvfrom(s, buf, sizeof(buf) - 1, 0, (struct sockaddr *) &iclnt, &len); if (cc < 0) perror("recvfrom"); /*Ignoramos este mensaje y esperamos otro */ else /* inserta terminador para poder imprimir con printf */ buf[cc] = '\0'; /* convierte direccion del cliente usando Pág. 13
imprime_extremo_conexion */ printf("desde "); imprime_extremo_conexion((struct sockaddr *) &iclnt, len, SOCKET_UDP); printf(" recibido: %s\n", buf); /* envia */ cc = sendto(s, buf, strlen(buf), 0, (struct sockaddr *) &iclnt, len); if (cc < 0) perror("sendto"); /*Informamos del error */ Listado 15. Recibe y responde lo mismo (recod.c) PRUEBAS DEL BLOQUE 2 P1) Busque ayuda en el manual sobre las funciones utilizadas. (a) Qué devuelve la llamada a la función socket si se produce algún error en la ejecución? Cómo podemos comprobar cuál de los posibles errores se ha producido? (b) Cómo podemos saber el número de resultados obtenidos tras una llamada a getaddrinfo? (c) Una vez utilizados los valores obtenidos con getaddrinfo, qué debemos hacer con la información devuelta por dicha función? Por qué? P2)Compile el cliente y servidor usando el comando make. En la mayoría de las máquinas con UNIX (Linux) hay un servidor de eco permanentemente instalado en el puerto 7 (este servicio se llama echo, y no escribe en la pantalla del servidor ni termina nunca). En el laboratorio, la imagen de Linux está configurada así, busque un ordenador que esté ejecutando Linux y compruebe que el cliente puede comunicarse con él, con: reco diripomaquina echo hola P3)Pruebe ahora cliente y servidor en el mismo equipo. Para ello abra dos ventanas de terminal y ejecute un programa en cada una. Por qué el cliente no tiene que usar el mismo número de puerto que el servidor? Puede usar los siguientes comandos (un comando en cada terminal): recod 5000 reco localhost 5000 uno dos tres P4)Seguidamente póngase de acuerdo con un compañero para probar cliente y servidor entre ordenadores personales. Por supuesto, habrá que arrancar el servidor antes de efectuar la consulta especificando el número de puerto donde va a escuchar. Intente también que varios clientes se comuniquen simultáneamente con un mismo servidor. Capture con el analizador de protocolos Wireshark una comunicación entre dos equipos (uno de ellos el suyo) y observe los datagramas UDP transmitidos. coinciden con lo esperable?. P5) Pruebe a generar errores y anote los resultados: Pág. 14
(a) Utilizando direcciones IP inexistentes o donde no exista ningún servidor a la escucha. (b) Indicando una dirección IP incorrecta. (c) Ejecutando dos servidores en el mismo puerto. P6. OPCIONAL) Una duda que suele surgir a menudo es qué es lo que pasa si al recibir un datagrama con la función recvfrom, el buffer pasado a la función es más pequeño que el datagrama. Cambie la constante que define el tamaño del buffer de recepción del cliente a un valor pequeño, de tal manera que pueda escribir una palabra que no quepa en él. Llame al fichero modificado reco6.c. Ejecute el nuevo cliente. ocurre? P7. OPCIONAL) Modifique el código del cliente para que este también utilice la función bind. Qué conseguimos con esto? Llame al fichero modificado reco7.c. P8. OPCIONAL) Aunque el protocolo UDP no está orientado a conexión, en la ayuda de la función connect se dice que esta función puede utilizarse con dicho protocolo. Qué efecto tendría? Modifique el código del cliente para que haga uso de la función connect y llame al fichero modificado reco8.c. El funcionamiento debe seguir siendo el mismo. FICHEROS A ENTREGAR Memoria de la práctica. (documento de texto con las respuestas y resultados de las pruebas) Ficheros modificados en esta práctica de las pruebas opcional es P6, P7 y P8. (en caso de haber realizado la parte opcional) Para subir a la tarea más de un fichero puede comprimir y empaquetar (con rar o zip) los ficheros a entregar. Pág. 15