Sistema Cliente – Servidor Con Sockets

Anuncio
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
Descargar