Redes de Computadores: Relación de problemas 1. Sockets 1. Obtención de nombres y direcciones IP Uso de gethostbyname y gethostbyaddr. Bajo el sistema operativo Linux, esta información se puede obtener mediante el comando /usr/bin/nslookup al que se le puede pasar una dirección IP para obtener el nombre de una máquina o el nombre de una máquina para obtener todas las direcciones IP que tiene asignadas y sus alias. Una dirección IP es un entero de 32 bits (4 octetos) en formato big-endian (primero el octeto de más peso. Igual ocurre con los puertos TCP o UDP que son enteros de 16 bits. Unix dispone de cuatro funciones para traducir entre el formato empleado por TCP/IP y el empleado por nuestro computador. Todas tienen el formato XtoYT donde X e Y pueden ser h (host) o n(network), pero no iguales y T puede ser l (entero largo) o s(entero corto). Para más información ver las páginas man de estos comandos. Para manipular directamente direcciones IP, veanse las páginas man de los comandos inet addr, inet network, inet makeaddr, inet lnaof, inet netof e inet ntoa. Por ejemplo, inet addr traduce una string con la dirección IP de una máquina en formato a.b.c.d a un entero de 32 bits. El programa dirmem.c obtiene una estructura (struct hostent) (ver página man de gethostbyaddr) de una máquina a partir de su dirección IP en notación de punto y extrae de ella el nombre (o nombres) para mostrarlos en pantalla. /*dirnom.c: Obtenci’on de informaci’on a partir de una direcciona IP*/ #include #include #include #include #include #include #include <stdio.h> <string.h> <sys/types.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> <netdb.h> int main(int argc, const char *argv[]) { u_long addr; struct hostent *hp; char **p; if (argc != 2) { printf("Uso: %s direccion-IP\n",argv[0]); exit(1); } /*Cambia de formato a.b.c.d a formato binario*/ if ((addr=inet_addr(argv[1])) == -1 ) { printf("La direccion IP tiene que estar en notacion a.b.c.d\n"); exit(2); } /*Obtiene una estructura con informacion del host dado en binario.*/ hp=gethostbyaddr( (char *)&addr, sizeof(addr),AF_INET); if (hp==NULL) { printf("No encuentro la informacion sobre la maquina %s\n", argv[1]); exit(3); } /*Para cada uno de las posibles entradas en h_addr_list*/ for (p=hp->h_addr_list; *p != 0; p++) { struct in_addr in; /*Pasa el binario de la tabla a in.s_addr porque esa estructura la */ /*necesita inet_ntoa, para pasarla a formato a.b.c.d. */ memcpy(&in.s_addr, *p, sizeof(in.s_addr)); printf("%s\t%s \n", inet_ntoa(in),hp->h_name); } exit(0); } 1 Todos los programas mostrados en estas prácticas se encuentran en http://www.ace.ual.es/∼leo/ftp/Redes/Sockets y por lo tanto no hay que escribirlos ni dejarse la vista en el código anterior. Compilar el programa con gcc dirnom -o dirnom y ejecutarlo, por ejemplo: dirnom 148.88.2.6 El resultado será: 148.88.2.6 newton-if-c.mirror.ac.uk Practica 1: Resolución de nombres Crear un programa, nomdir.c que tomando como argumento de entrada el nombre de la máquina, escriba por pantalla las direccion(es) IP que tiene asignada(s) y sus alias, si los tiene. Probar el programa con el nombre unix.hensa.ac.uk 2. Datagramas en el dominio UNIX Los sockets se comunican usando una infraestructura de comunicaciones preexistente. Un Dominio de comunicación es una familia de protocolos. En un sistema UNIX los dominios más habituales son: PF UNIX: para comunicación entre procesos de una misma máquina. PF INET: para comunicación entre procesos en dos computadores conectados mediante los protocolos TCPUDP /IP. PF=Protocol Family. En cada dominio hay una forma de especificar las direcciones, por lo tanto existen dos familias de direcciones: AF UNIX: Una dirección de socket es como nombre de fichero. AF INET: Una dirección de socket precisa 3 campos: Una dirección IP. Un protocolo (TCP o UDP). Un puerto correspondiente a ese protocolo. AF= Address Family. En el fichero /etc/services se puede encontrar en cada lı́nea información de nombre estándar puerto/protocolo nombre alternativo1 nombre alternativo2 ... Desde un programa se puede ver en que puerto está un determinado servicio y el servicio asociado a un puerto, con las funciones getservbyname y getservbyport. Por lo tanto existen una serie de puertos reservados. Los puertos a partir de IPPORT RESERVED (usualmente 1024) pueden ser usados por los usuarios para definir otros servicios. Al crear un socket hay que indicar el estilo de comunicación que usará. Para que dos sockets se comuniquen deben haberse creado con el mismo estilo: SOCK STREAM: Orientado a conexión. Intercambio de datos fiable y orientado a octetos. SOCK DGRAM: No orientado a conexión. Los programas que se muestran a continuación, recunixd.c y emiunixd.c, se comunican mediante sockets con datagramas en el dominio UNIX. La dirección del socket del receptor es el fichero “sockdir”. En PF UNIX la dirección del socket se gestiona con la estructura sockaddr un definida en /usr/include/sys/un.h. Sus campos son: short sun family = AF UNIX char sun path[108] = pathname correspondiente a la dirección del socket. El programa recunixd: 2 Crea un un socket en el dominio PF UNIX, estilo SOCK DGRAM. Le asigna la dirección “socketdir”. Se bloquea a la espera de recepción de un mensaje. Cierra el socket después de recibir el mensaje. Borra la entrada “socketdir” del directorio. /*Ejemplo de receptor usando sockets con datagramas en el dominio UNIX*/ #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <string.h> #include <stdio.h> #define NAME "socketdir" /*Ejemplo de emisor usando sockets con datagramas en el dominio UNIX*/ #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <string.h> #include <stdio.h> #define DATA "Este es el mensaje ...." int main() { int sock, length, lon2; struct sockaddr_un name; char buf[1024]; int main(int argc, char *argv[]) { int sock; struct sockaddr_un name; char buf[1024]; /*Nos creamos un socket en el dominio PF_UNIX y del tipo datagrama*/ /*0 indica el protocolo de comunicaciones 0->protocolo por omision*/ sock=socket(PF_UNIX, SOCK_DGRAM,0); /*Nos creamos un socket en el dominio PF_UNIX y del tipo datagrama*/ /*0 indica el protocolo de comunicaciones 0->protocolo por omision*/ sock=socket(PF_UNIX, SOCK_DGRAM,0); if (sock<0) { perror("Abriendo socket de datagramas"); exit(1); } if (sock<0) { perror("Abriendo socket de datagramas"); exit(1); } /*Formato de la direccion*/ name.sun_family=AF_UNIX; /*Formato de la direccion*/ name.sun_family=AF_UNIX; /*Direcci’on del socket*/ strcpy(name.sun_path,NAME); /*Direcci’on del socket*/ strcpy(name.sun_path,argv[1]); /*sock = el socket a traves del cual se escribe*/ /*DATA = mensaje a enviar.*/ /*strlen(DATA)+1 = tamano del mansaje a enviar*/ /*0 = modo de comunicacion, 0-> normal*/ /*name = estructura que contiene la direccion del distino del datagrama*/ /*sizeof(name) = longitud en octetos de name.*/ if (sendto(sock,DATA,strlen(DATA)+1,0, (struct sockaddr *)&name, sizeof(name)) <0) perror("Enviando un datagrama"); /*Bind asocia un socket con su direcci’on*/ if (bind(sock, (struct sockaddr *) &name, sizeof(name))<0) { perror("Asociando nombre al socket"); exit(1); } printf("Direccion del socket --> %s\n",NAME); close(sock); exit(0); /*sock = el socket a traves del cual se lee*/ /*buf = Lugar donde hay que dejar la informacion leida*/ /*1024 = cuantos octetos se quieren recibir.*/ /*0 = modo en el que se quiere recibir, 0->recepcion normal*/ /*NULL = Almacenar la direccion del socket emisor. NULL-> no interesados*/ /*lon2 = Espacio necesario para almacenar la direccion del socket emisor*/ if (recvfrom(sock,buf,1024,0,NULL,&lon2) <0) perror("Recibiendo un datagrama"); printf("-->%s\n",buf); } close(sock); unlink(NAME); exit(0); } El programa emiunixd: Crea un un socket en el dominio PF UNIX, estilo SOCK DGRAM. Calcula la dirección del receptor (a partir del primer argumento de la lı́nea de comandos). Envı́a un datagrama a esa dirección. Cierra el socket. Ejecución Compilar los fuentes: gcc recunixd.c -o recunixd gcc emiunixd.c -o emiunixd Lanzar en background recunixd (recunixd &). Deberá escribir en pantalla la dirección del socket creado. Lanzar el emisor, facilitándole la dirección del socket del receptor. 3. Datagramas en el dominio internet Vamos a realizar la aplicación equivalente a la anterior, pero en el dominio de internet. El cambio fundamental radica en la gestión de las direcciones de los sockets. En AF INET se usa la estructura struct scokaddr in definida en /usr/include/netinet/in.h. Los campos son: 3 short sin family = AF INET unsigned short sin port = Número de puerto. struct in addr sin addr = dirección IP en orden de red. char sin zero[8] = Campo de relleno. El campo más complicado puede ser sin addr, pero puede usarse una operación de copia desde alguna estructura devuelta por funciones de manipulación de direcciones IP ( normalmente gethostbyname()). recuérdese que el puerto y la dirección IP están en orden de red y si se quieren visualizar hay que cambiarlos a orden de máquina. /*Ejemplo de sockets orientados a datagramas en internet. Receptor*/ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> /*Ejemplo de sockets orientados a datagramas en internet. Receptor*/ #include <sys/socket.h> #include<netdb.h> #include <netinet/in.h> #include <stdio.h> int main() { int sock, length, lon2; struct sockaddr_in name; char buf[1024]; #define DATA "Este es el mensaje ...." int main(int argc, char *argv[]) { int sock; struct sockaddr_in name; struct hostent *hp; sock=socket(PF_INET,SOCK_DGRAM,0); if (sock<0) { perror("Abriendo socket de datagramas"); exit(1); } sock=socket(PF_INET,SOCK_DGRAM,0); if (sock<0) { perror("Abriendo socket de datagramas"); exit(1); } /*Formato de la direccion*/ name.sin_family=AF_INET; /*Direccion IP, INADDR_ANY -> que cuando se use bind para asociar una*/ /*direccion a un socket, la direccion IP es la de la maquina donde esta*/ /*ejecutandose el programa.*/ name.sin_addr.s_addr=htonl(INADDR_ANY); /*devuelve una estructura hostent para el host especificado en argv[1]*/ /*para obtener la direccion IP a partir del nombre de la maquina*/ hp=gethostbyname(argv[1]); /*0-> cuando se utilice bind(), el puerto que se va a asociar al socket es */ /*uno libre asignado por el SO.*/ name.sin_port=htons(0); if (hp == 0) { fprintf(stderr,"%s: host desconocido",argv[1]); exit(2); } if (bind(sock,(struct sockaddr *)&name, sizeof(name))<0) { perror("Asociando nombre al socket"); exit(1); } /*Copiamos en la estructura name la direccion del ordenador al que */ /*vamos a conectarnos.*/ memcpy((char *)&name.sin_addr, (char *)hp->h_addr, hp->h_length); name.sin_family = AF_INET; name.sin_port = htons(atoi(argv[2])); /*Hasta despues del bind no sabremos la direccion del socket asignada.*/ /*=> usar getsockname()*/ if (sendto(sock,DATA,strlen(DATA)+1,0,(struct sockaddr *)&name,sizeof(name))<0) perror("Enviando un datagrama"); length=sizeof(name); /*sock = socket del que queremos saber la direccion*/ /*name = estructura en la que se va a dejar la direccion*/ /*length = tamano ocupado por la estructura.*/ if (getsockname(sock,(struct sockaddr *)&name,&length)<0) { perror("Averiguando el nombre del socket"); exit(1); } close(sock); exit(0); } /*Imprimimos el puerto para que el emisor mande a ese puerto.*/ printf("puerto del socket -->%d\n", ntohs(name.sin_port)); if (recvfrom(sock,buf,1024,0,(struct sockaddr *)NULL,&lon2)<0) perror("Recibiendo el datagrama"); printf("-->%s\n",buf); close(sock); exit(0); } Obsérvese en el receptor como se manipula la estructura name. Se usa INADDR ANY, para que la dirección IP que se asocie al socket con bind() sea la máquina actual. El campo name.sin port=htons(0) se usa para que sea el SO el que asigne un puerto libre al socket. Después del bind() se usa getsockname para saber toda la dirección del socket, entre la que se encuentra el puerto que nos ha correspondido. Ahora no es necesario usar unlink ya que en el dominio de internet una dirección de socket no deja rastro. El programa de ejemplo emisor consigue la dirección del socket del receptor a partir de los argumentos facilitados en la lı́nea de comandos (nombre de la máquina y el puerto de espera del receptor) y rellena la estructura name con esos datos en el formato correcto. El emisor no se molesta en asignar una dirección a su socket ya que el SO asigna al socket la IP de la máquina local y un puerto libre si se quiere realizar una conexión y no se ha invocado a bind(). Si se quiere saber que ha ocurrido se puede usar getsockname() después de sendto() para obtener la dirección del socket. También se podrı́a haber usado un número de puerto fijo para realizar la comunicación, tal como se verá cuando se realice una comunicación con conexión. 4 Ejecución Compilar los programas: gcc recinetd.c -o recinetd gcc eminetd.c -o eminetd Lanzar en background recinetd (recinetd &). Deberá escribir en pantalla la dirección del puerto del socket creado. Lanzar el emisor, facilitándole la dirección de la máquina a la que quiere conectarse (localhost si no estamos conectados a internet) y la dirección del puerto del receptor. Práctica 2: Comunicación bidireccional con UDP Modificar los programas anteriores para que realicen un intercambio de datagramas en cada sentido: primero eminetd envı́a un datagrama a recinetd, tal como está en el ejemplo y luego recinetd envı́a otro de respuesta a eminetd. Práctica 3: Identificación de emisores UDP Ampliar el programa de ejemplo de recepción de datagramas UDP, para que recinetd no solo escriba por pantalla el contenido del datagrama recibido, sino también la dirección IP (en notación punto) y el número de puerto del emisor. 4. Conexiones en el dominio de Internet Todo lo aplicable en el dominio INET será aplicable en el dominio UNIX, menos el formato de las direcciones socket, por lo que solo mostraremos el caso INET. El el siguiente ejemplo, el programa Clinetc.c: Crea un un socket en el dominio PF UNIX, estilo SOCK STREAM. Calcula la dirección del receptor, a partir del primer argumento de la lı́nea de comandos y un puerto conocido a priori. Se conecta, de forma activa, con el servidor Envı́a un mensaje. Cierra el socket y por lo tanto la conexión. El puerto del cliente no aparece explı́citamente en ninguna parte, pero es necesario, ya que una conexión TCP precisa de dos puertos. Al igual que en el caso UDP, el SO asigna dinámicamente un puerto si se intenta establecer una conexión y no ha habido un bind() previo. La conexión se establece mediante el comando connect (ver página man). La escritura por la conexión se hace mediante la llamada genérica para escribir por cualquier objeto de E/S: write(). Una alternativa a write es send(), especı́ficamente diseñada para sockets con conexión (ver página man). El programa Serinetc.c: Crea un un socket en el dominio PF UNIX, estilo SOCK STREAM. Calcula una dirección y se la asocia a sock. Marca el sock como socket de escucha. Se bloquea en el sock, a la espera de una petición de establecimiento de conexión por parte de algún cliente. Consigue una conexión a través de un socket nuevo, msgsock, que es un socket de dialogo. Recibe información a través del msgsock. Cierra msgsock (y con él la conexión). Se dispone a aceptar nuevas conexiones a través de sock. 5 /*Clinetc.c: Cliente TCP basado en sockets*/ /*Serinetc.c: Servidor iterativo TCP basado en sockets*/ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #define SERV_ADDR (IPPORT_RESERVED+1) #define DATA "##--##--##----***----##--##--##" #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #define STDOUT 1 #define SERV_ADDR (IPPORT_RESERVED+1) int main (int argc, char *argv[]) { int sock; struct sockaddr_in server; struct hostent *hp; int main() { int rval; int sock,length,msgsock; struct sockaddr_in server; char buf[1024]; if (argc<2) { printf("Uso: %s nombre_host\n",argv[0]); exit(1); } /*Creaci’on de un socket de escucha, de tipo "stream"*/ sock=socket(PF_INET, SOCK_STREAM,0); if (sock<0) { perror("No hay socket de escucha"); exit(1); } /*Creamos un socket orientado a conexi’on*/ sock=socket(PF_INET,SOCK_STREAM,0); if (sock<0) { perror("No se ha opdido conseguir el socket"); exit(1); } /*Voy a asignar ese socket a la direcci’on de transporte*/ server.sin_family=AF_INET; server.sin_addr.s_addr=htonl(INADDR_ANY); server.sin_port = htons(SERV_ADDR); /*El puerto es uno concreto y fijo*/ if (bind(sock,(struct sockaddr *)&server, sizeof(server))<0) { perror("Direccion no asignada"); exit(1); } /*Me dispongo a escuchar en el socket.*/ listen(sock,1); while (1) { /*Me bloqueo esperando peticion de conexion*/ /*Acepto y consigo un socket de dialogo "msgsock"*/ msgsock = accept(sock, (struct sockaddr *)0, (int *) 0); if (msgsock==-1) perror("Conexion no aceptada"); else do { /*Me dispongo a leer datos de la conexion*/ memset(buf,0,sizeof(buf)); rval=read(msgsock,buf,1024); if (rval<0) perror("Mensaje no leido"); else write(STDOUT,buf,rval); } while (rval>0); printf("\nCerrando la conexi’on...\n"); close(msgsock); } exit(0); } /*Nos conectamos con el socket de escucha del servidor.*/ /*Lo conseguimos calculando primero su direcci’on, a partir del nombre*/ server.sin_family =AF_INET; hp=gethostbyname(argv[1]); if (hp==0) { fprintf(stderr, "%s: No conozco ese computador\n",argv[1]); exit(2); } memcpy((char *)&server.sin_addr, (char *)hp->h_addr, hp->h_length); server.sin_port = htons (SERV_ADDR); if (connect(sock, (struct sockaddr *)&server, sizeof(server))<0) { perror("Conexion no aceptada!!!"); exit(1); } if (write(sock,DATA, strlen(DATA)+1)<0) perror("No he podido escribir el mensaje"); close(sock); exit(0); } El servidor gestiona dos sockets (sock y msgsock). En uno usa listen() y en el otro accept(). Para poder aceptar conexiones TCP de forma pasiva, el programa debe marcar un socket como de escucha, mediante el comando listen(), que tiene como argumento, además del socket, el tamaño de la cola de peticiones de conexión pendientes. En nuestro caso de tamaño 1, lo que implica que mientras se sirve una petición, solo puede haber una más esperando. Si hay una esperando y llega otra adicional, su petición fracasará. La llamada accept extrae la primera conexión pedida en la cola de conexiones pendientes, crea un nuevo socket de dialogo con las mismas propiedades del usado en listen(). Si no hay peticiones pendientes, accept() bloquea el servidor hasta que haya una. Si se quiere, con accept() se puede obtener información de la dirección del otro extremo. Esto se puede obtener en cualquier otro momento con la función getpeername() (ver página man). La lectura por el socket de dialogo se hace con la función genérica de UNIX: read(). Una alternativa a read es recv(), especı́ficamente diseñada para sockets con conexión (ver página man) que es simétrica a send(). Se ha hablado de close() sobre un socket para cerrar la conexión, pero lo que hace es eliminar el descriptor del socket y por lo tanto la conexión. Para realizar sólo el cierre de la conexión, sin eliminar el socket, está el comando shutdown() (ver página man). Ejecución Generar los ejecutables. Lanzar el servidor en background (Serinetc &). Podemos comprobar con el comando ps que el servido está funcionando. Ahora se puede ejecutar el cliente, indicándole la máquina en la que se encuentra el servidor (Ej: Clinetc localhost). Una vez conectados el cliente termina pero el servidor no (comprobarlo con ps). Se pueden realizar varias ejecuciones de Clinetc seguidas. El servidor habrá que matarlo explı́citamente con el comando kill (Ej: kill -9 <pid del Serinetc>). 6 Práctica 4: Identificación de clientes TCP A partir de los programas anteriores, modificar el servidor (Serinetc.c) para que durante su ejecución imprima por pantalla los siguientes datos de los clientes que a él se conectan: (1)número de puerto, (2) dirección IP en notación de punto y (3)nombre de la máquina correspondiente a esa dirección IP. 5. Conexiones simultáneas Es interesante poder dar servicio a varios clientes a la vez. Un servidor puede bloquearse intentando leer de un socket, cuando puede haber otras conexiones activas. Por otro lado, si el servidor es no bloqueante, debe atender a varias conexiones secuencialmente para ver que conexiones tienen datos, es decir, debe realizar iteraciones constantemente. Solución multiproceso Para resolver los problemas anteriores se pueden tener servidores concurrentes. La estrategia esta basada en tener un proceso atendiendo constantemente al socket de escucha. Cuando se establece una conexión el servidor lanza un proceso hijo con fork() que es el que realmente atiende la conexión a través del socket de dialogo. Se crearán tantos hijos como sea necesario. /*Serveco2.c : Un servidor de eco orientado a conexi’on concurrente*/ /* multiproceso basado en sockets.*/ /*----------------------------------------------------------------------------*/ int main() { struct sockaddr_in sin,fsin; int s,ssock,alen; #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include<signal.h> #include <errno.h> /*----------------------------------------------------------------------------*/ void do_echo(int); sin.sin_family=AF_INET; sin.sin_addr.s_addr =htonl(INADDR_ANY); sin.sin_port =htons(3500); if ((s=socket(PF_INET,SOCK_STREAM,0))<0) { perror("No se puede crear el socket"); exit(1); } if (bind (s,(struct sockaddr *)&sin,sizeof(sin))<0) { perror("No se puede asignar la direcci’on"); exit(2); } if (listen(s,5)<0) { perror("No puedo poner el socket en modo escucha"); exit(3); } signal(SIGCHLD, SIG_IGN); while (1) { alen=sizeof(fsin); if ((ssock =accept(s, (struct sockaddr *)&fsin,&alen))<0) { if (errno == EINTR) continue; perror("Fallo en accept"); exit(4); } switch (fork()) { case -1: perror("No puedo crear hijo"); exit(5); case 0 : close(s); /*Proceso hijo*/ do_echo(ssock); break; default: close(ssock); break; } } } /*----------------------------------------------------------------------------*/ extern int errno; /*----------------------------------------------------------------------------*/ void do_echo(int fd) { char buf[4096]; int cc,org,faltan,cc2; while (cc=read(fd,buf,sizeof(buf))) { if (cc<0) { perror("Read"); exit(6); } org=0; faltan=cc; /*Los que hay que mandar*/ while (faltan) { if ((cc2=write(fd,&buf[org],faltan))<0) { perror("Fallo al escribir"); exit(7); } org+=cc2; faltan-=cc2; } } close(fd); } El comando fork() crea una copia casi exacta del proceso que lo invoca devolviendo 0 para el proceso hijo y el pid del proceso hijo para el proceso padre (Ver página man). Esta solución funciona bien cuando los procesos hijos no necesitan compartir información (variables) puesto que cada uno de ellos se ejecuta en un espacio de memoria separado (Telnet, FTP, o WWW). El programa Sereco.c muestra una implementación de esta estrategia. Puede probarse usando: telnet máquina 3500, donde 3500 es el puerto. Aspectos del ejemplo: signal(SIGCHLD, SIG IN); El servidor crea hijos, uno por cada conexión. Cuando la conexión se cierra el proceso hijo se muere. El padre deberı́a realizar un wait() que le bloquea hasta que el hijo muere. Si no se realiza, los hijos se quedan en 7 estado zombie. Con la instrucción anterior se ignoran las señales que indican la muerte de los hijos y no los deja en estado zombie. Por último, la función do echo escribe mediante un bucle de llamadas write ya que write no garantiza que se envı́e por la conexión el número de bytes solicitados. Práctica 5: Cliente del servicio “eco” Realizar un cliente TCP para el servidor de eco implementado. Usar después el servidor de eco del sistema, (si está accesible) comprobando primero, a través del fichero /etc/services, el número del puerto asociado a ese servicio. Modificar después el programa para que acceda a dicho servidor, sin conocer previamente el número de puerto, usando la función getservbyname() para obtenerlo. 5.1. Solución monoproceso Vamos a implementar un único proceso que atienda a todas las conexiones gracias a la función select() (Ver página man). Con select() se puede: (i) bloquear varios sockets a la vez, para despertarlos cuando se pueda operar en uno de ellos, (ii) hacer una encuesta simultánea a varios sockets, sin bloqueo y (iii) bloquearnos por un tiempo limitado. Select() tiene cinco argumentos. Los argumentos segundo, tercero y cuarto son del tipo fd set (objetos de E/S, incluidos sockets). El primer argumento indica el tamaño máximo de esos conjuntos (usualmente FD SETSIZE). El último es una estructura de tiempo con segundos y microsegundos. Los tres conjuntos fd set contienen lo siguiente: Primero: Los objetos a examinar para ver si se puede realizar una lectura sobre ellos. Segundo: Los objetos a examinar para ver si se puede escribir sobre ellos. Tercero: Los objetos a examinar para ver si se puede realizar una operación especial sobre ellos. Select monitoriza todos los objetos a la vez durante el tiempo máximo especificado en su último argumento. Dependiendo de este valor, los comportamientos de select() pueden ser: NULL : Select funciona de forma totalmente bloqueante hasta realizar una operación sobre alguno o alguno de los descriptores. 0: Se limita ha hacer una encuesta , retornando inmediatamente. >0 : Select bloquea a sus descriptores y retorna cuando , o bien, expira el temporizador, o bien, se ha realizado una operación sobre algún descriptor. Select devuelve -1 en caso de error y en caso contrario el número de descriptores preparados para realizar una operación. Para saber que descriptores se pueden utilizar hay que examinar el conjunto de descriptores que habrán sido previamente modificados. Para ello existen una serie de macros (ver página man de select para ver sus argumentos): FD FD FD FD SET(): incluye un descriptor en un conjunto de descriptores. CLR(): borra un descriptor de un conjunto. ISSET(): Devuelve 1 si el descriptor está en el conjunto. ZERO(): vacı́a el conjunto. Como ejemplo de utilización ver el programa Sereco3.c. El servidor de eco Sereco3 gestiona las conexiones de dos en dos. Se usan los descriptores msgocka y msgsockb que en cada iteración se incluyen en el conjunto de descriptores de lectura, para que select los modifique para ver que entradas están activas. Un cliente que se conecte al servidor no puede hacer nada hasta que otro cliente se conecte también. Gracias al funcionamiento de select, si un cliente cierra la conexión el otro puede seguir trabajando. Chequear este comportamiento usando clientes telnet. En un servidor más convencional, el select se hace sobre un conjunto de sockets de escucha cuyo número de elementos puede variar durante la vida del servidor. Sobre este conjunto se realizan las operaciones select(). Si hay alguno activo, se crea una nueva conexión y se introduce el nuevo socket de dialogo en el conjunto. Mientras se da servicio a una conexión se puede detectar el fin de la misma, cerrándola y eliminando el socket asociado del conjunto. 8 /*Serveco3.c : Un servidor de eco orientado a conexi’on concurrente*/ /* monoproceso basado en sockets.*/ #include #include #include #include #include #include #include #include <stdlib.h> <stdio.h> <string.h> <sys/time.h> <sys/types.h> <unistd.h> <sys/socket.h> <netinet/in.h> #define STDOUT 1 #define SERV_ADDR (IPPORT_RESERVED+1) int main() { int rvala, rvalb; int sock, length, msgsocka, msgsockb; int dispuestos; struct sockaddr_in server; fd_set lecturas; char buf[1024]; if ((sock=socket(PF_INET,SOCK_STREAM,0))<0) { perror("No se puede crear el socket"); exit(1); } server.sin_family=AF_INET; server.sin_addr.s_addr =htonl(INADDR_ANY); server.sin_port =htons(SERV_ADDR); if (bind (sock,(struct sockaddr *)&server,sizeof(server))<0) { perror("No se puede asignar la direcci’on"); exit(2); } listen(sock,1); do { msgsocka=accept(sock,(struct sockaddr *)0,(int *)0); if (msgsocka== -1) { perror("Conexion no aceptada!!!"); exit(-1); } msgsockb=accept(sock,(struct sockaddr *)0,(int *)0); if (msgsockb== -1) { perror("Conexion no aceptada!!!"); exit(-1); } do { FD_ZERO(&lecturas); FD_SET(msgsocka, &lecturas); FD_SET(msgsockb, &lecturas); dispuestos = select (FD_SETSIZE, &lecturas, (fd_set *)NULL, (fd_set *)NULL,NULL); if (FD_ISSET(msgsocka,&lecturas)) { rvala = read(msgsocka,buf,1024); if (rvala<0) perror("Mensaje no leido (1)"); else write(STDOUT,buf,rvala); } if (FD_ISSET(msgsockb,&lecturas)) { rvalb = read(msgsockb,buf,1024); if (rvalb<0) perror("Mensaje no leido (1)"); else write(STDOUT,buf,rvalb); } } while ((rvala>0)||(rvalb>0)); printf("\n Cerrando las conexiones....\n"); close(msgsocka); close(msgsockb); } while (1); exit(0); } 6. El Super-servidor inetd Para evitar tener corriendo tantos servidores (daemons) como servicios ofrece una máquina, se ha diseñado en UNIX un servidor especial llamado inetd que permite poner en ejecución otros servidores, pero solo si son necesarios. También permite que cualquier filtro (programa que lee de la entrada estándar y escribe en la salida estándar) se convierta en servidor. Inetd atiende simultáneamente a los puertos especificados en /etc/inetd.conf. Crea tantos sockets como puertos a atender y después usa select para monitorizar los puertos. Cuando recibe una petición de comunicación por un puerto ejecuta una llamada al sistema fork()seguida de una llamada exec() para poner en marcha el programa apropiado al servicio deseado que se configura de según lo establecido en /etc/inetd.conf. Lo interesante de inetd es que el proceso hijo creado tiene como entrada estándar y como salida estándar el socket a través del cual se comunica el cliente. Cada lı́nea de /etc/inetd.conf contiene lo siguiente: 9 Nombre del servicio (como en /etc/services). estilo de comunicación (stream, datagram). Protocolo a emplear (tcp,udp,..) wait/nowait : Nowait implica que tras lanzar el servidor, inetd sigue escuchando, creando servidores adicionales si es necesario. Wait no permite esto. Normalmente se usa nowait para TCP y wait para UDP. Usuario que ejecutará el servidor. Ruta de acceso al servidor. Internal significa que el servicio lo da directamente inetd. Argumentos para el programa servidor. Por lo tanto inetd nos permite incluir servidores de una forma fácil, sin tener que preocuparnos de sockets, direcciones, etc, nuestro programa solo debe leer de la entrada estándar y escribir en la salida estándar, usando si se desean la biblioteca de E/S estándar (hay que usar fflush() cuando sea necesario para evitar los efectos de doble buffering; el de E/S más el de TCP). 7. Aspectos de interés Sockets de datagramas conectados Usar connect() con socket de datagramas simplemente establece un destino por omisión para los datagramas enviados. De esta forma se pueden usar las funciones write(), read(), send(), recv() o cualquier otra que opere sobre un descriptor, sin necesidad de dar la dirección de destino del datagrama. telnet como cliente universal Se puede usar telnet máquina puerto como cliente para cualquier servidor que use TCP, siempre que esté basado en el intercambio de mensajes de texto. Todo lo que escribamos por teclado será entregado al servidor y todo lo que el servidor responda será mostrado por pantalla. Estado de comunicación de nuestros procesos ps: Para ver los procesos que se están ejecutando. Útil para no dejarse, sin desearlo, servidores en ejecución. kill: Para matar procesos que no deseamos que sigan en ejecución. El argumento es el identificador de proceso (PID) mostrado mediante ps. netstat: Muestra el estado de las conexiones de nuestra máquina. Nos indica qué conexiones hay en marcha y las direcciones (IP y puerto) de cada extremo. También da información sobre la actividad en el dominio UNIX. Uso de funciones de E/S estándar con sockets La funciones de entrada salida que se encuentra en stdio.h están diseñadas para manipular objetos del tipo (FILE *), por lo que hay que crear un objeto de tipo FILE asociado al socket usando la función fdopen (Ver página man), cuyo primer argumento es un descriptor de fichero UNIX, obtenido, por ejemplo, mediante open(), pipe() o socket(). El segundo argumento es una cadena que indica el modo de apertura. El uso de FILE lleva asociado un buffer, y puesto que TCP ya utiliza también un buffer, podemos eliminar el de FILE mediante el comando setbuff(), pasándole NULL como segundo argumento. Práctica 6: Sistema sencillo de transferencia de ficheros Basándonos en TCP, crear un servidor, serfichd que no necesita ningún argumento adicional. El cliente, traer se podrá lanzar desde cualquier máquina. Los parámetros de traer son: máquina servidor ficero remoto [fichero local]. El diseño de servfichd se implementará como un servidor iterativo, dejando como opción implementar el concurrente. 10