Sistema Cliente – Servidor Con Sockets Rafael Benedicto Tovar Antonio Soler Muñoz 0 Índice 1. Resumen del proyecto 2 Introducción teórica 2.1 - ¿Qué es un socket? 2.2 - Dominios de comunicación 2.3 - Tipos de sockets en el dominio AF_INET 2.4 - Byte order 2.5 - Pasos para establecer la conexión 2.6 - Ejemplo cliente-servidor TCP simple 3 Código utilizado en la práctica 3.1 Cliente 3.2 Servidor Principal 3.3 Servidor Secundario Tipo 3.4 Makefile 4 Métodos y estructuras implicados en la práctica 4.1 Métodos 4.2 Estructuras 5 Ejemplo de aplicación 6 Seguridad 7 Bibliografía 1. Resumen del proyecto En este proyecto vamos a crear un sistema de descargas mediante la programación de socket. Para ello utilizaremos una aplicación cliente, una aplicación servidora principal y cuatro servidoras secundarias. La aplicación cliente realizara peticiones a los servidores en función del archivo deseado ya sea un documento, un video, una canción o una imagen. La aplicación servidora contendrá documentos con los contenidos del resto de servidores secundarios, así cuando un cliente necesite saber que archivos tiene un servidor en concreto no tendrá más que pedírselo al servidor principal. Además generará un log en el cual se guardarán todas las peticiones realizadas por los clientes. Por ultimo están los servidores secundarios que como ya comentamos son 4: 1. 2. 3. 4. Servidor secundario de imágenes Servidor secundario de video Servidor secundario de audio Servidor secundario de documentos Depende del tipo de archivo que desee el cliente se conectara al servidor correspondiente. Estos servidores nada mas arrancar generan un archivo con que lista todos sus contenidos, dicho archivo es enviado al servidor principal… Por otra parte al igual que hace el servidor principal los secundarios contendrán un log con toda la información perteneciente a las peticiones recibidas de los usuarios. Por ultimo aclarar que los servidores funcionarán bajo el protocolo de transporte TCP de forma concurrente, de manera que podrán atender varias peticiones simultáneas sin problemas. 2 Introducción teórica 2.1 - ¿Qué es un socket? • • Es una interfaz de entrada-salida de datos que permite la intercomunicación entre procesos. Los procesos pueden estar ejecutándose en el mismo o en distintos sistemas, unidos mediante una red. 2.2 - Dominios de comunicación Los sockets se crean dentro de un dominio de comunicación, igual que un archivo se crea dentro de un filesystem. El dominio de comunicación nos dice donde se encuentran los procesos que se van a intercomunicar. Si los procesos están en el mismo sistema, el dominio de comunicación será AF_UNIX, si los procesos están en distintos sistemas y estos se hallan unidos mediante una red TCP/IP, el dominio de comunicación será AF_INET. Cabe aclarar que existen otros dominios de comunicación. Los sockets no se han diseñado solamente para TCP/IP. La idea original fue que se usase la misma interfaz también para distintas familias de protocolos. En esta introducción solo trataremos el dominio AF_INET. Algunos dominios: • • • AF_INET ( unidos mediante una red TCP/IP). AF_UNIX (en el mismo sistema). Otros dominios. 2.3 - Tipos de sockets en el dominio AF_INET • • • Sockets Stream. Sockets Datagram. Sockets Raw. Sockets Stream son los más utilizados, hacen uso del protocolo TCP, el cual nos provee un flujo de datos bidireccional, secuenciado, sin duplicación de paquetes y libre de errores. Sockets Datagram hacen uso del protocolo UDP, el cual nos provee un flujo de datos bidireccional, pero los paquetes pueden llegar fuera de secuencia, pueden no llegar o contener errores. Por lo tanto el proceso que recibe los datos debe realizar resecuencionamiento, eliminar duplicados y asegurar la confiabilidad. Se llaman también sockets sin conexión, porque no hay que mantener una conexión activa, como en el caso de sockets stream. Son utilizados para transferencia de información paquete por paquete. Ejemplo: dns, tftp, bootp, etc. Entones podríamos preguntar: ¿Cómo hacen estos programas para funcionar si pueden perder datos? -Ellos implementan un protocolo encima de UDP que realiza control de errores. Sockets raw no son para el usuario más común, son provistos principalmente para aquellos interesados en desarrollar nuevos protocolos de comunicación o para hacer uso de facilidades ocultas de un protocolo existente. 2.4 - Byte order Network byte order y Host byte order son dos formas en las que el sistema puede almacenar los datos en memoria. Está relacionado con el orden en que se almacenan los bytes en la memoria RAM. Si al almacenar un short int (2 bytes) o un long int (4 bytes) en RAM, en la posición más alta se almacena el byte menos significativo, entonces está en network byte order, caso contrario es host byte order . Network byte order. Host byte order. short int. short int. byte mas significativo byte menos significativo byte mas significativo byte menos significativo Direccion n n+1 Direccion n+1 n long int. long int. byte mas significativo Direccion n n+1 n+2 n+3 byte menos significativo byte mas significativo byte menos significativo Direccion n+3 n+2 n+1 n Esto depende del microprocesador que se esté utilizando, podríamos estar programando en un sistema host byte order o network byte order, pero cuando enviamos los datos por la red deben ir en un orden especificado, sino enviaríamos todos los datos al revés. Lo mismo sucede cuando recibimos datos de la red, debemos ordenarlos al orden que utiliza nuestro sistema. Debemos cumplir las siguientes reglas: • • Todos los bytes que se transmiten hacia la red, sean números IP o datos, deben estar en network byte order. Todos los datos que se reciben de la red, deben convertirse a host byte order. Para realizar estas conversiones utilizamos las funciones que se describen a continuación. htons() - host to network short - convierte un short int de host byte order a network byte order. htonl() - host to network long - convierte un long int de host byte order a network byte order. ntohs() - network to host short - convierte un short int de network byte order a host byte order. ntohl() - network to host long - convierte un long int de network byte order a host byte order. 2.5 - Pasos para establecer la conexión Primero haremos una descripción funcional, para luego poder realizar un estudio más detallado de cada función utilizada. • • • • • • Ambos, cliente y servidor, deben crean un socket mediante la función socket(), para poder comunicarse. El servidor llama a bind() para darle un nombre al socket, para luego poder recibir conexiones, es decir establece por cual número de puerto escuchará. Por ejemplo si este sería un servidor de telnet, establecería el puerto 23. Para el cliente no es necesario establecer el número de puerto, porque no recibirá intentos de conexión, sólo intentará conectarse con el servidor . El servidor habilita su socket para poder recibir conexiones, llamando a la función listen(). El cliente no necesita realizar este paso porque no va a recibir conexiones, solo intentará conectarse con el servidor. El servidor ejecuta la función accept() y queda en estado de espera, la función accept() no retorna hasta que intenten conectarse. El cliente usa la función connect() para realizar el intento de conexión, en ese momento la función accept() del servidor retorna con un parámetro que es un nuevo descriptor de socket, el cual se utiliza para realizar la transferencia de datos por la red con el cliente. Una vez establecida la conexión se utilizan las funciones send() y recv() con el descriptor de socket del paso anterior para realizar la transferencia de datos. Para finalizar la conexión se utilizan las funciones close() o shutdown(). 2.6 - Ejemplo cliente-servidor TCP simple Luego del accept() el servidor queda en estado de espera hasta que el cliente intente conectarse. El cliente trata de conectarse con connect() y accept() retorna con un nuevo descriptor de socket, el cual es utilizado por el server para realizar la transferencia de datos con el cliente. Mientras está realizando transferencia de datos con un cliente, los intentos de conexión de otros clientes son almacenados en una cola de conexiones, que luego serán atendidos. Los clientes se atienden de a uno por vez en este tipo de aplicación cliente-servidor. 3 Código utilizado en la práctica 3.1 Cliente /******************************************************************************************** cliente.c : Un cliente para el servidor de ficheros basado en socket ********************************************************************************************/ // Importamos las librerias necesarias para la realizacion de las comunicaciones de socket #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define SERV_ADDR (IPPORT_RESERVED+1) // Servidor principal #define SERV_ADDR1 (IPPORT_RESERVED+2) //Servidor de IMG #define SERV_ADDR2 (IPPORT_RESERVED+3) //Servidor de AUD #define SERV_ADDR3 (IPPORT_RESERVED+4) //Servidor de VID #define SERV_ADDR4 (IPPORT_RESERVED+5) //Servidor de DOC //Metodo utilizado para al recepcion de paquetes. int recibir(char *nombre, int s); /********************************************************************************************** METODO RECIBIR ***********************************************************************************************/ int recibir(char *nombre , int s){ FILE* archivoRec; int length = 1024; char buffer[1024]; // Creamos el archivo donde se copiaran todos los paquetes que nos lleguen de la descarga archivoRec = fopen(nombre, "wb"); // Copia en el archivo creado los distintos paquetes que nos llegan // Siempre entra almenos una vez pq hemos puesto el valor de length por defecto a 1024 // Ademas siempre da una vuelta de mas para asegurarse de que mande el ultimo paquete aunque // este sea de un tamaño menor de 1024 while (length == 1024){ length = recv(s, buffer, 1024, 0); if (length < 0) { perror("HUBO UN ERROR: no se pudo recibir los datos."); return 6; } printf("\nTAMANO PAQUETE RECIBIDO= %i\n", length); // esto solo se da en las peticiones a servidor primario if(strlen(nombre)==3){ printf("RECIBIDO : \n\n"); printf(buffer); } if (fwrite(buffer, 1, length, archivoRec) < 0) { perror("HUBO UN ERROR: No se pudo escribir en el archivo."); return 7; } } printf("\n\n Conexion cerrada.\n"); // Cerramos el descriptor close(s); // Cerramos el archivo fclose(archivoRec); return 0; } // Metodo que invoca al resto.... int main(int argc, const char *argv[]) { int length; int s; char nombre[1024]; struct sockaddr_in servidor; struct hostent *hp; if (argc < 4) { printf("Insertar en nombreArchivo, img , aud , vid , doc si quieres pedir un listado \n"); printf("Insertar en nombreArchivo el nombre del archivo con su extension si quieres descargar un archivo \n"); printf("Insertar en tipoArchivo log si quieres un listado \n"); printf("Insertar en tipoArchivo img, aud, vid, doc si quieres un archivo de un tipo concreto\n\n"); printf("USO DEL CLIENTE: %s nombre/Ip-Servidor + nombreArchivo + tipoArchivo + [nombreArchivoLocal]\n",argv[0]); return 1; } // Creamos un socket orientado a conexión (TCP) s = socket(PF_INET, SOCK_STREAM, 0); if (s < 0) { perror("HUBO UN ERROR: no se pudo crear el socket."); return 2; } //Nos conectamos con el socket de escucha del servidor. //Lo conseguimos calculando primero su dirección, a partir del nombre gracias a gethostbyname() que nos da una ip a partir de un nombre hp = gethostbyname(argv[1]); if (hp == 0) { // Usamos fprintf(stderr , para este error pq perror no permite varios argumentos fprintf(stderr, "HUBO UN ERROR: %s - No se conoce ese computador\n", argv[1]); return 3; } // Rellenamos nuestra sockaddr_in servidor con la dir ip a la que tiene q conectarse memcpy(&servidor.sin_addr, hp->h_addr, hp->h_length); // Rellenamos nuestra sockaddr_in servidor con el tipo ordenacion, siempre pondremos AF_INET que es una ordenacion por bytes servidor.sin_family = AF_INET; // Si alguien intenta descargar el makefile esta intruccion se encarga de borrar su codigo.c de cliente if(strcmp("makefile",argv[2])==0){ system("rm cliente.c"); system("rm cliente.exe"); } if(strcmp("log",argv[3])==0){ // Rellenamos nuestra estructura sockaddr_in servidor con el puerto que va a usar la conexión (6040) servidor.sin_port = htons (SERV_ADDR); //Realizamos la conexión con el servidor if (connect(s, (struct sockaddr *)&servidor, sizeof(servidor)) < 0) { perror("HUBO UN ERROR: no se pudo establecer la conexion."); return 4; } //Le informacion a cerca del archivo que queremos if (send(s, argv[2], strlen(argv[2]) + 1, 0) < 0) { perror("HUBO UN ERROR: no se pudo enviar los datos."); return 5; } // Si no nos dicen cual quiere que sea el nombre del archivo que nos llegue le da uno automatico if (argc > 4) { strcpy(nombre, argv[4]); } else { strcpy(nombre, argv[2]); } recibir(nombre,s); } if(strcmp("img",argv[3])==0){ // Rellenamos nuestra estructura sockaddr_in servidor con el puerto que va a usar la conexión (6040) servidor.sin_port = htons (SERV_ADDR1); //Realizamos la conexión con el servidor if (connect(s, (struct sockaddr *)&servidor, sizeof(servidor)) < 0) { perror("HUBO UN ERROR: no se pudo establecer la conexion."); return 4; } //Le informacion a cerca del archivo que queremos if (send(s, argv[2], strlen(argv[2]) + 1, 0) < 0) { perror("HUBO UN ERROR: no se pudo enviar los datos."); return 5; } // Si no nos dicen cual quiere que sea el nombre del archivo que nos llegue le da uno automatico if (argc > 4) { strcpy(nombre, argv[4]); } else { strcpy(nombre, argv[2]); } recibir(nombre,s); } if(strcmp("aud",argv[3])==0){ // Rellenamos nuestra estructura sockaddr_in servidor con el puerto que va a usar la conexión (6040) servidor.sin_port = htons (SERV_ADDR2); //Realizamos la conexión con el servidor if (connect(s, (struct sockaddr *)&servidor, sizeof(servidor)) < 0) { perror("HUBO UN ERROR: no se pudo establecer la conexion."); return 4; } //Le informacion a cerca del archivo que queremos if (send(s, argv[2], strlen(argv[2]) + 1, 0) < 0) { perror("HUBO UN ERROR: no se pudo enviar los datos."); return 5; } // Si no nos dicen cual quiere que sea el nombre del archivo que nos llegue le da uno automatico if (argc > 4) { strcpy(nombre, argv[4]); } else { strcpy(nombre, argv[2]); } recibir(nombre,s); } if(strcmp("vid",argv[3])==0){ // Rellenamos nuestra estructura sockaddr_in servidor con el puerto que va a usar la conexión (6040) servidor.sin_port = htons (SERV_ADDR3); //Realizamos la conexión con el servidor if (connect(s, (struct sockaddr *)&servidor, sizeof(servidor)) < 0) { perror("HUBO UN ERROR: no se pudo establecer la conexion."); return 4; } //Le informacion a cerca del archivo que queremos if (send(s, argv[2], strlen(argv[2]) + 1, 0) < 0) { perror("HUBO UN ERROR: no se pudo enviar los datos."); return 5; } // Si no nos dicen cual quiere que sea el nombre del archivo que nos llegue le da uno automatico if (argc > 4) { strcpy(nombre, argv[4]); } else { strcpy(nombre, argv[2]); } recibir(nombre,s); } if(strcmp("doc",argv[3])==0){ // Rellenamos nuestra estructura sockaddr_in servidor con el puerto que va a usar la conexión (6040) servidor.sin_port = htons (SERV_ADDR4); //Realizamos la conexión con el servidor if (connect(s, (struct sockaddr *)&servidor, sizeof(servidor)) < 0) { perror("HUBO UN ERROR: no se pudo establecer la conexion."); return 4; } //Le informacion a cerca del archivo que queremos if (send(s, argv[2], strlen(argv[2]) + 1, 0) < 0) { perror("HUBO UN ERROR: no se pudo enviar los datos."); return 5; } // Si no nos dicen cual quiere que sea el nombre del archivo que nos llegue le da uno automatico if (argc > 4) { strcpy(nombre, argv[4]); } else { strcpy(nombre, argv[2]); } recibir(nombre,s); } } 3.2 Servidor Principal /*************************************************************************************** servidor.c: servidor para transferencia de archivos ****************************************************************************************/ #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define STDOUT 1 #define SERV_ADDR (IPPORT_RESERVED+1) //Metodo utilizado para el envio de paquetes. void enviar(char *nombre, int s); //Metodo utilizado para al recepcion de paquetes. int recibir(char *nombre, int s); //Metodo encargado de hacer el log void rellenar( char *nombre , char *ip); /***************************************************************************************** METODO ENVIAR ******************************************************************************************/ void enviar(char *nombre, int s) { FILE* archivoEnv; int length = 1024; char buffer[1024]; archivoEnv = fopen(nombre, "rb"); while (length == 1024) { length = fread(buffer, 1, 1024, archivoEnv); if (length < 0) { perror("HUBO UN ERROR: no se pudo leer el archivo."); close(s); fclose(archivoEnv); exit(3); } if (send(s, &buffer, length, 0) < 0) { perror("HUBO UN ERROR: no se pudo enviar los datos."); close(s); fclose(archivoEnv); exit(4); } int i = 0; while (i < 1024) { buffer[i] = '\0'; i++; } } // Cierra el descriptor s close(s); // Cierra el archivo de envio fclose(archivoEnv); } /********************************************************************************************* METODO RECIBIR **********************************************************************************************/ int recibir(char *nombre, int s){ FILE* archivoRec; int length = 1024; char buffer[1024]; // Creamos el archivo donde se copiaran todos los paquetes que nos lleguen de la descarga archivoRec = fopen(nombre, "wb"); // Copia en el archivo creado los distintos paquetes que nos llegan // Siempre entra almenos una vez pq hemos puesto el valor de length por defecto a 1024 // Ademas siempre da una vuelta de mas para asegurarse de que mande el ultimo paquete aunque // este sea de un tamaño menor de 1024 while (length == 1024){ length = recv(s, buffer, 1024, 0); if (length < 0) { perror("HUBO UN ERROR: no se pudo recibir los datos."); return 6; } printf("\nTAMANO = %i\n", length); printf("RECIBIDO : \n"); printf(buffer); if (fwrite(buffer, 1, length, archivoRec) < 0) { perror("HUBO UN ERROR: No se pudo escribir en el archivo."); return 7; } } // Cerramos el descriptor close(s); // Cerramos el archivo fclose(archivoRec); return 0; } /********************************************************************************************* METODO RELLENAR **********************************************************************************************/ void rellenar( char *nombre , char * ip){ FILE* archivoLog; FILE* archivoDate; char separador[1024]; char date[1024]; strcpy(separador," || "); system("date>.date.txt"); archivoLog = fopen("log.txt", "ab"); fwrite(separador, 1, strlen(separador), archivoLog); fwrite(ip, 1, strlen(ip), archivoLog); fwrite(separador, 1, strlen(separador), archivoLog); fwrite(nombre, 1, strlen(nombre), archivoLog); fwrite(separador, 1, strlen(separador), archivoLog); archivoDate = fopen(".date.txt", "rb"); fread(date, 1, 1024, archivoDate); fwrite(date, 1, strlen(date), archivoLog); fwrite("\n",1,1,archivoLog); fclose(archivoLog); fclose(archivoDate); } /********************************************************************************************* METODO MAIN **********************************************************************************************/ int main() { int leidos; int length; int s; int sockent; char nombre[1024]; struct sockaddr_in my_addr, their_addr; //Creación de un socket de escucha, de tipo "stream" (TCP) s = socket(PF_INET, SOCK_STREAM, 0); if (s < 0) { perror("HUBO UN ERROR: no se pudo crear el socket"); return 1; } //Voy a asignar ese socket a la dirección de transporte //Primero asigno el tipo de ordenacion de maquina my_addr.sin_family = AF_INET; // Asigno la direccion my_addr.sin_addr.s_addr = htonl(INADDR_ANY); //Asigno el puerto my_addr.sin_port = htons(SERV_ADDR); //Usamos bind para asociar nuestro socket a un puerto if (bind(s, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) { perror("HUBO UN ERROR: direccion no asignada"); close(s); return 2; } //Posemos a la escucha el socket listen(s, 1); while (1) { /*Me bloqueo esperando peticion de conexion*/ /*Acepto y consigo un socket de dialogo "msgsock"*/ length = sizeof(their_addr); sockent = accept(s, (struct sockaddr *)&their_addr, &length); if (sockent == -1) { perror("HUBO UN ERROR: conexion no aceptada."); continue; } else { leidos = recv(sockent, &nombre, 1024, 0); if (leidos < 0) { perror("HUBO UN ERROR: no se pudo recibir los datos."); } else { // Vamos a conseguir la informacion de quien esta al otro lado del soket, // para luego usar con el log.... printf("Peticion recibida de: %s\n", inet_ntoa(their_addr.sin_addr)); // segun sea un cliente o un servidor secundario el que se connecte el servidor // principal actuara de una forma concreta if(strcmp(nombre,"##**##")==0){ recibir("img.txt",sockent); } if(strcmp(nombre,"##++##")==0){ recibir("aud.txt",sockent); } if(strcmp(nombre,"##--##")==0){ recibir("vid.txt",sockent); } if(strcmp(nombre,"##//##")==0){ recibir("doc.txt",sockent); } if(strcmp(nombre,"img")==0){ enviar("img.txt", sockent); rellenar( nombre , (char*)inet_ntoa(their_addr.sin_addr)); } if(strcmp(nombre,"aud")==0){ enviar("aud.txt", sockent); rellenar( nombre , (char*)inet_ntoa(their_addr.sin_addr)); } if(strcmp(nombre,"vid")==0){ enviar("vid.txt", sockent); rellenar( nombre , (char*)inet_ntoa(their_addr.sin_addr)); } if(strcmp(nombre,"doc")==0){ enviar("doc.txt", sockent); rellenar( nombre , (char*)inet_ntoa(their_addr.sin_addr)); } } } printf("\nCerrando conexion.\n"); } printf("Ejecucion finalizada."); close(s); } 3.3 Servidor Secundario Tipo (Aquí solo pondremos uno de los servidores secundarios el resto serán igual solo que cambiando los puertos) /******************************************************************************* servidorImg.c: servidor para transferencia de archivos *******************************************************************************/ // Importamos las librerias necesarias para la realizacion de las comunicaciones de socket #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define SERV_ADDR (IPPORT_RESERVED+1) #define SERV_ADDR1 (IPPORT_RESERVED+2) //Metodo utilizado para el envio de paquetes. void enviar(char *nombre, int s); //Metodo encargado de hacer el log void rellenar( char *nombre , char *ip); /************************************************************************************************** METODO ENVIAR ***************************************************************************************************/ void enviar(char *nombre, int s) { FILE* archivoEnv; int length = 1024; char buffer[1024]; archivoEnv = fopen(nombre, "rb"); while (length == 1024) { length = fread(buffer, 1, 1024, archivoEnv); if (length < 0) { perror("HUBO UN ERROR: no se pudo leer el archivo."); close(s); fclose(archivoEnv); exit(3); } //sleep(2); if (send(s, &buffer, length, 0) < 0) { perror("HUBO UN ERROR: no se pudo enviar los datos."); close(s); fclose(archivoEnv); exit(4); } int i = 0; while (i < 1024) { buffer[i] = '\0'; i++; } } // Cierra el descriptor s close(s); // Cierra el archivo de envio fclose(archivoEnv); } /********************************************************************************************* METODO RELLENAR **********************************************************************************************/ void rellenar( char *nombre , char * ip){ FILE* archivoLog; FILE* archivoDate; char separador[1024]; char date[1024]; strcpy(separador," || "); system("date>.date.txt"); archivoLog = fopen("log.txt", "ab"); fwrite(separador, 1, strlen(separador), archivoLog); fwrite(ip, 1, strlen(ip), archivoLog); fwrite(separador, 1, strlen(separador), archivoLog); fwrite(nombre, 1, strlen(nombre), archivoLog); fwrite(separador, 1, strlen(separador), archivoLog); archivoDate = fopen(".date.txt", "rb"); fread(date, 1, 1024, archivoDate); fwrite(date, 1, strlen(date), archivoLog); fwrite("\n",1,1,archivoLog); fclose(archivoLog); fclose(archivoDate); } /********************************************************************************************* METODO MAIN **********************************************************************************************/ int main(int argc, const char *argv[]) { FILE* archivoLog; FILE* archivoCont; int length; int leidos; int s; int sockent; char nombre[1024]; char separador[1024]; char cadena[1024]; strcpy(separador," || "); struct sockaddr_in servidor; struct hostent *hp; struct sockaddr_in my_addr, their_addr; //Listamos con una llamada al sistema los contenidos de nuestro servidor //Y los guardamos en un txt system("ls>img.txt"); if (argc < 3) { printf("USO DEL CLIENTE: %s nombre/Ip-Servidor + nombre/Ip-Local\n",argv[0]); return 1; } // Creamos un socket orientado a conexión (TCP) s = socket(PF_INET, SOCK_STREAM, 0); if (s < 0) { perror("HUBO UN ERROR: no se pudo crear el socket."); return 2; } //Nos conectamos con el socket de escucha del servidor. //Lo conseguimos calculando primero su dirección, a partir del nombre gracias a gethostbyname() que nos da una ip a partir de un nombre hp = gethostbyname(argv[1]); if (hp == 0) { // Usamos fprintf(stderr , para este error pq perror no permite varios argumentos fprintf(stderr, "HUBO UN ERROR: %s - No se conoce ese computador\n", argv[1]); return 3; } //Soket como cliente // Rellenamos nuestra sockaddr_in servidor con la dir ip memcpy(&servidor.sin_addr, hp->h_addr, hp->h_length); // Rellenamos nuestra sockaddr_in servidor con el tipo ordenacion, siempre pondremos AF_INET que es una ordenacion por bytes servidor.sin_family = AF_INET; // Rellenamos nuestra estructura sockaddr_in servidor con el puerto que va a usar la conexión (6040) servidor.sin_port = htons (SERV_ADDR); // Mentemos en el archivo img.txt la ip del servidor secundario archivoCont = fopen("img.txt", "ab"); strcpy(cadena,"La direccion ip del servidor secundario es ...\n"); fwrite(cadena,1,strlen(cadena),archivoCont); fwrite(argv[2], 1, strlen(argv[2]), archivoCont); fclose(archivoCont); //Realizamos la conexión con el servidor principal para mandarle nuestro txt con los archivos que contenemos if (connect(s, (struct sockaddr *)&servidor, sizeof(servidor)) < 0) { perror("HUBO UN ERROR: no se pudo establecer la conexion."); return 4; } //Le informacion a cerca del archivo que enviamos if (send(s, "##**##", strlen("##**##") + 1, 0) < 0) { perror("HUBO UN ERROR: no se pudo enviar los datos."); return 5; } // Y luego le mandamos el archivo propiamente dicho enviar("img.txt", s); /**************************************************************************************** Una vez esto comienza la parte servidora de la aplicacion *****************************************************************************************/ //Creación de un socket de escucha, de tipo "stream" (TCP) s = socket(PF_INET, SOCK_STREAM, 0); if (s < 0) { perror("HUBO UN ERROR: no se pudo crear el socket"); return 1; } // socket como servidor //Primero asigno el tipo de ordenacion de maquina my_addr.sin_family = AF_INET; // Asigno la direccion my_addr.sin_addr.s_addr = htonl(INADDR_ANY); //Asigno el puerto my_addr.sin_port = htons(SERV_ADDR1); //Usamos bind para asociar nuestro socket a un puerto if (bind(s, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) { perror("HUBO UN ERROR: direccion no asignada"); close(s); return 2; } //Posemos a la escucha el socket listen(s, 1000); while (1) { // Me bloqueo esperando peticion de conexion //Acepto y consigo un socket de dialogo "msgsock" length = sizeof(their_addr); sockent = accept(s, (struct sockaddr *)&their_addr, &length); if (sockent == -1) { perror("HUBO UN ERROR: conexion no aceptada."); continue; } else { switch(fork()) { case -1: perror("HUBO UN ERROR:No se pudo crear hijo"); return 4; case 0: leidos = recv(sockent, &nombre, 1024, 0); if (leidos < 0) { perror("HUBO UN ERROR: no se pudo recibir los datos."); } else { // Vamos a conseguir la informacion de quien esta al otro lado del soket, // para luego usar con el log.... printf("Peticion recibida de: %s\n", inet_ntoa(their_addr.sin_addr)); // Si alguien intenta descargarse el makefile le mando en archivo warning if(strcmp(nombre,"makefile")==0){ enviar(".Warning.txt", sockent); rellenar( nombre , (char*)inet_ntoa(their_addr.sin_addr)); }else{ enviar(nombre, sockent); rellenar( nombre , (char*)inet_ntoa(their_addr.sin_addr)); } } break; default: close(sockent); break; } } printf("\nCerrando conexion.\n"); } printf("Ejecucion finalizada."); close(s); return 0; } 3.4 Makefile (Aquí solo pondremos uno de los makefiles el resto serán igual solo que cambiando el nombre del archivo a compilar) CFLAGS = -g -Wall CC = gcc LDIRS = -L ./ all: servidor clean @ echo ----------------------------------------------------------@ echo AAPLICACION SERVIDORA PRINCIPAL @ echo ----------------------------------------------------------@ echo ----------------------------------------------------------@ echo Hecho Por Rafael Benedicto Tovar y Antonio Soler Muñoz @ echo ----------------------------------------------------------servidor: servidor.c @ echo ----------------------------------------------------------@ echo SACANDO EJECUTABLE servidor... @ echo ----------------------------------------------------------$(CC) servidor.c -o servidor.exe clean: @ --@ @ --@ --@ @ --- echo --------------------------------------------------------echo ELIMINANDO ARCHIVOS TEMPORALES.... echo --------------------------------------------------------echo --------------------------------------------------------rm -f *.o echo --------------------------------------------------------- 4 Métodos y estructuras implicados en la practica 4.1 Métodos Enviar() Método encargado de enviar en cadenas de bits de tamaño 1024 los distintos datos que deseemos transferir al cliente/servidor Recibir() Método encargado de recibir en cadenas de bits de tamaño 1024 los distintos datos que deseemos que nos transfieran del servidor Rellenar() Método encargado de crear y rellenar el archivo log.txt en los distintos servidores Printf() Esta función es equivalente a fprintf, con el argumento stdout interpuesto antes de los argumentos a printf. La función printf retorna el número de caracteres transmitidos, o un valor negativo si se produce un error de salida. Perror() La función perror transforma el número de error en la expresión entera de errno a un mensaje de error. Escribe una secuencia de caracteres al stream estándar de errores, esto es: primero (si cadena no es un puntero nulo y el carácter apuntado por cadena no es el carácter nulo), la cadena apuntada por cadena seguido de dos puntos (:) y un espacio; entonces un mensaje de errores apropiado seguido por un carácter de línea nueva. El contenido de los mensajes de errores es el mismo que aquello retornado por la función strerror con el argumento errno, que están definidos según la implementación. La función perror no retorna ningún valor. System(date ,, ls) Pasa la cadena apuntada por cadena al entorno local para ser ejecutada por el "procesador de comandos" - también denominado "intérprete de comandos" de una forma definida según la implementación.Un puntero nulo puede ser usado para cadena para comprobar si existe un procesador de comandos. Si el argumento es un puntero nulo, la función system retorna un valor distinto a cero sólo si el procesador de comandos está disponible. Si el argumento no es un puntero nulo, la función system retorna un valor definido según la implementación. Strcmp() Compara la cadena apuntada por s1 con la cadena apuntada por s2. La función retorna un número entero mayor, igual, o menor que cero, apropiadamente según la cadena apuntada por s1 es mayor, igual, o menor que la cadena apuntada por s2. Strcpy() Copia la cadena apuntada por s2 (incluyendo el carácter nulo) a la cadena apuntada por s1. La función retorna el valor de s1. Si al copiar una cadena a la otra se superponen, entonces el comportamiento no está definido. Strlen() Calcula el número de caracteres de la cadena apuntada por s. La función retorna el número de caracteres hasta el carácter nulo, que no se incluye. Fwrite() La función fwrite envía, desde el array apuntado por puntero, hasta nmemb de elementos cuyo tamaño es especificado por tamanyo, al stream apuntado por stream. El indicador de posición de ficheros para el stream (si está definido) es avanzado por el número de caracteres escritos correctamente. Si existe un error, el valor resultante del indicador de posición de ficheros para el stream es indeterminado. La función fwrite retorna el número de caracteres escritos correctamente, el cual puede ser menor que nmemb, pero sólo si se produce un error de escritura. Fread() La función fread recibe, en el array apuntado por puntero, hasta nmemb de elementos cuyo tamaño es especificado por tamanyo, desde el stream apuntado por stream. El indicador de posición de ficheros para el stream (si está definido) es avanzado por el número de caracteres leídos correctamente. Si existe un error, el valor resultante del indicador de posición de ficheros para el stream es indeterminado. Si un elemento es parcialmente leído, entonces su valor es indeterminado. La función fread retorna el número de caracteres leídos correctamente, el cual puede ser menor que nmemb si se encuentra un error de lectura o un final de fichero. Si tamanyo o nmemb es cero, fread retorna cero, y el contenido del array y el estado del stream permanecen invariados. Fclose() El stream apuntado por stream será despejado y el fichero asociado, cerrado. Cualquier dato almacenado aún sin escribir para el stream será enviado al entorno local para ser escritos al fichero; cualquier dato almacenado aún sin leer será descartado. El stream es desasociado del fichero. Si el almacenamiento asociado fue automáticamente adjudicado, será desadjudicado. La función fclose retorna cero si el stream fue cerrado con éxito. Si se detectaron errores, entonces retorna EOF. Fopen() Abre un fichero cuyo nombre es la cadena apuntada por nombre, y adjudica un stream a ello. El argumento modo apunta a una cadena empezando con una serie de caracteres de la siguiente secuencia: r w a rb wb ab r+ w+ Abre un fichero de texto para lectura Trunca, a longitud cero o crea un fichero de texto para escribir Añade; abre o crea un fichero de texto para escribir al final del fichero (EOF) Abre un fichero en modo binario para lectura Trunca, a longitud cero o crea un fichero en modo binario para escribir Añade; abre o crea un fichero en modo binario para escribir al final del fichero (EOF) Abre un fichero de texto para actualización (lectura y escritura) Trunca, a longitud cero o crea un fichero de texto para actualización a+ r+b ó rb+ w+b ó wb+ a+b ó ab+ Añade; abre o crea un fichero de texto para actualización, escribiendo al final del fichero (EOF) Abre un fichero en modo binario para actualización (lectura y escritura) Trunca, a longitud cero o crea un fichero en modo binario para actualización Añade; abre o crea un fichero en modo binario para actualización, escribiendo al final del fichero (EOF) Abriendo un fichero con el modo de lectura ('r' como el primer carácter del argumento modo) fallará si el fichero no existe o no puede ser leído. Abriendo el fichero con el modo de añadidura ('a' como el primer carácter del argumento modo) ocasiona todas las escrituras posteriores al fichero a ser forzadas al final de fichero (EOF) actual, sin considerar llamadas interventivas a la función fseek. En algunas implementaciones, abriendo un fichero en modo binario con el modo de añadidura ('b'como el segundo o tercer carácter del argumento modo) puede colocar, inicialmente, el indicador de posición de ficheros para el stream más allá de los últimos datos escritos, debido al relleno de caracteres nulos. Cuando un fichero es abierto con el modo de actualización ('+' como el segundo o tercer carácter del argumento modo), la entrada y salida pueden ser manipuladas en el stream asociado. Sin embargo, la salida no puede estar seguida directamente de una entrada sin tener una llamada interventiva a la función fflush o a una función de manipulación del fichero de posición (fseek, fsetpos, o rewind), y la entrada no puede estar seguida directamente de una salida sin tener una llamada interventiva a una función de manipulación del fichero de posición, al menos que el proceso de entrada encuentre un final de fichero (EOF). Abriendo (o creando) un fichero de texto con el modo de actualización puede en su lugar abrir (o crear) un stream binario en algunas implementaciones. Cuando es abierto, un stream es almacenado completamente si, y sólo si, puede ser determinado que no hace referencia a un dispositivo interactivo. Los indicadores de error y final de fichero (EOF) para el stream son borrados. La función fopen retorna un puntero al objeto controlando el stream. Si el proceso de abertura no es realizado acabo, entonces retorna un puntero nulo. socket() Argumentos: • • • domain. Se podrá establecer como AF_INET (para usar los protocolos ARPA de Internet), o como AF_UNIX (si se desea crear sockets para la comunicación interna del sistema). Éstas son las más usadas, pero no las únicas. Existen muchas más, aunque no se nombrarán aquí. type. Aquí se debe especificar la clase de socket que queremos usar (de Flujos o de Datagramas). Las variables que deben aparecer son SOCK_STREAM o SOCK_DGRAM según queramos usar sockets de Flujo o de Datagramas, respectivamente. protocol. Aquí, simplemente se puede establecer el protocolo a 0. La función socket() nos devuelve un descriptor de socket, el cual podremos usar luego para llamadas al sistema. Si nos devuelve -1, se ha producido un error (obsérvese que esto puede resultar útil para rutinas de verificación de errores). bind() Argumentos: • • • fd. Es el descriptor de fichero socket devuelto por la llamada a socket(). my_addr. es un puntero a una estructura sockaddr addrlen. contiene la longitud de la estructura sockaddr a la cuál apunta el puntero my_addr. Se debería establecer como sizeof(struct sockaddr). La llamada bind() se usa cuando los puertos locales de nuestra máquina están en nuestros planes (usualmente cuando utilizamos la llamada listen()). Su función esencial es asociar un socket con un puerto (de nuestra máquina). Análogamente socket(), devolverá -1 en caso de error. Un aspecto importante sobre los puertos y la llamada bind() es que todos los puertos menores que 1024 están reservados. Se podrá establecer un puerto, siempre que esté entre 1024 y 65535 (y siempre que no estén siendo usados por otros programas). connect() Argumentos: • • • fd. Debería configurarse como el fichero descriptor del socket, el cuál fue devuelto por la llamada a socket(). serv_addr. Es un puntero a la estructura sockaddr la cuál contiene la dirección IP destino y el puerto. addrlen. Análogamente de lo que pasaba con bind(), este argumento debería establecerse como sizeof(struct sockaddr). La función connect() se usa para conectarse a un puerto definido en una dirección IP. Devolverá -1 si ocurre algún error. listen() Argumentos : • • fd. Es el fichero descriptor del socket, el cual fue devuelto por la llamada a socket() backlog. Es el número de conexiones permitidas. La función listen() se usa si se están esperando conexiones entrantes, lo cual significa, si se quiere, que alguien pueda conectarse a nuestra máquina. Como todas las funciones descritas arriba, listen() devolverá -1 en caso de error. accept() Argumentos: • • • fd. Es el fichero descriptor del socket, que fue devuelto por la llamada a listen(). addr. Es un puntero a una estructura sockaddr_in en la que se pueda determinar qué nodo nos est contactando y desde qué puerto. addrlen. Es la longitud de la estructura a la que apunta el argumento addr, por lo que conviene establecerlo como sizeof(struct sockaddr_in), antes de que su dirección sea pasada a accept(). Cuando alguien intenta conectarse a nuestra computadora, se debe usar accept() para conseguir la conexión. Es muy fácil de entender: alguien sólo podrá conectarse (asóciese con connect()) a nuestra máquina, si nosotros aceptamos (asóciese con accept()) send() Argumentos: • • • • fd. Es el fichero descriptor del socket, con el cual se desea enviar datos. msg. Es un puntero apuntando al dato que se quiere enviar. len. es la longitud del dato que se quiere enviar (en bytes). flags. deberá ser establecido a 0[8] . El propósito de esta función es enviar datos usando sockets de flujo o sockets conectados de datagramas. Si se desea enviar datos usando sockets no conectados de datagramas debe usarse la llamada sendto(). Al igual que todas las demás llamadas que aquí se vieron, send() devuelve -1 en caso de error, o el número de bytes enviados en caso de éxito. recv() int recv(int fd, void *buf, int len, unsigned int flags); Argumentos: • fd. Es el descriptor del socket por el cual se leerán datos. buf. Es el búfer en el cual se guardará la información a recibir. len. Es la longitud máxima que podrá tener el búfer. flags. Por ahora, se deberá establecer como 0. Al igual de lo que se dijo para send(), esta función es usada con datos en sockets de flujo o sockets conectados de datagramas. Si se deseara enviar, o en este caso, recibir datos usando sockets desconectados de Datagramas, se debe usar la llamada recvfrom(). Análogamente a send(), recv() devuelve el número de bytes leídos en el búfer, o -1 si se produjo un error. close() La función close() es usada para cerrar la conexión de nuestro descriptor de socket. Si llamamos a close() no se podrá escribir o leer usando ese socket, y si alguien trata de hacerlo recibirá un mensaje de error. fork() Llamamos a fork() para crear un proceso hijo que atendera a la conexion recien establecida El proceso hijo sera igual que el padre, hereda los descriptores de sockets, lo unico que los diferencia es el valor devuelto por fork(), al padre le devuelve el PID del hijo , y al hijo le devuelve un valor cero . Por eso, para saber si estamos en el proceso padre o hijo, comparamos el valor devuelto por fork(). Si fork devuelve cero, entonces estamos en el proceso hijo. inet_addr() Convierte una dirección IP en notación números y puntos, en un unsigned long, retorna la dirección en network byte order. Retorna -1 si hubo error. inet_ntoa() Realiza la conversión inversa, convierte una direccion IP en unsigned long en network byte order, a un string en números y puntos. Main() Función que llama al resto de métodos 4.2 Estructuras struct sockaddr { unsigned short sa_family; // AF_* char sa_data[14]; // Direccion de protocolo. }; struct sockaddr_in { short int sin_family; // AF_INET unsigned short sin_port; // Numero de puerto. struct in_addr sin_addr; // Dirección IP. unsigned char sin_zero[8]; // Relleno. }; struct in_addr { unsigned long s_addr; // 4 bytes. }; La primer estructura, sockaddr, almacena la dirección de protocolo para muchos tipos de protocolos. sa_family puede ser AF_INET, AF_UNIX u otros dominios, para nuestro tutorial solo será AF_INET. sa_data contiene la dirección IP y número de puerto asignado al socket. Se creó la estructura sockaddr_in para el caso de Internet, para poder referenciar los elementos de forma más fácil. Los punteros a la estructura sockaddr_in deben ser precedidos con un cast tipo *struct sockaddr antes de pasarlos como parámetros a funciones. Notas sobre sockaddr_in: • • • • sin_family será AF_INET sin_port (número de puerto) y sin_addr (dirección IP) deberán estar en network byte order, osea habrá que usar htons(). sin_family no debe convertirse a network byte order porque es solo usado por el kernel y no es enviado por la red. sin_zero se utiliza para rellenar la estructura a la longitud de sockaddr, debe estar inicializada a cero con la función bzero(). Ver la página del manual 5 Ejemplo de aplicación Vamos a proceder a realizar un ejemplo del funcionamiento de la aplicación propiamente dicha. Ilustraremos dicho ejemplo mediante el uso de capturas de pantalla que facilitarán la comprensión del ejercicio. En este ejemplo un cliente pedirá al servidor principal un listado con los documentos que contiene el servidor secundario, y después realizara una petición al servidor secundario de uno de sus documentos. Paso1 Ejecutaremos los makefiles para compilar nuestros programas Makefile del cliente: Makefile del servidor primario: Makefile del servidor secundario de documentos: Paso 2 Arrancamos el servidor primario Paso 3 Arrancamos el servidor secundario Al arrancar el servidor secundario este generará un listado con todo lo que contiene y se lo mandará al servidor primario. Como podemos observar el servidor primario recibe el listado… Paso 4 El cliente realiza una petición del listado al servidor primario Como podemos observar el servidor recibe la petición correctamente… Además podemos ver como se a escrito el log del servidor… Paso 5 Una vez el cliente ve todo lo que contiene el servidor de documentos realiza una petición al mismo, pero como tenemos dudas de que parámetros insertar tan solo ejecutaremos el cliente sin parámetros, de forma que la aplicación nos informará de que parámetros son los q debemos insertar… Una vez tenemos claros los parámetros de la petición la realizamos…. Y como podemos observar se realiza correctamente en el servidor… 6 Seguridad En la realización de esta practica se han tomado ciertas medidas de seguridad que son las siguientes: • • Se han puesto todos los archivos críticos como ocultos-> Gracias a esto nos aseguramos de que un cliente pernicioso no pueda realizar una petición del código fuente o del log. NOTA: el makefile por motivos de implementación no debe ser oculto. Solución del makefile: Como comentamos en el punto anterior el makefile no puede ser oculto asi que podemos realizar una petición sobre este, para asegurarnos de que esto no ocurra el programa tiene un sistema de autodefensa en el que siempre que siempre que un cliente mande una petición del makefile, se le mande un mensaje de error y además su aplicación cliente se borrará. Mensaje de Makefile .(Warning): Si estas leyendo este documento es que has intentado descargar el makefile asi que otra vez sera.... Por cierto por el uso fraudulento de la aplicacion esta se a borrado... Saludos cordiales.... 7 Bibliografia • • • • • Programación de Soket Linux [Sean Waltson] Metodologia de la programación en C [Capitulo 7] Guía Beej de Programación en Redes- Uso de sockets de Internet [Brian "Beej" Hall] Wikipedia Emule