UNIVERSIDAD NACIONAL AUTÓNOMA DE MÉXICO Facultad de Ingeniería Arquitecturas Cliente/Servidor “Sockets en C” Alumnos: Arellano Santiago Víctor Manuel Girón Capistrán Aldo Iván Guerrero Ramirez Eduardo Daniel Rosas Peña Ramiro Fecha: Entrega 08/septiembre/2009 Objetivos. Implementar Sockets en el lenguaje de programación “C”. Introducción. A la hora de comunicar dos programas, existen varias posibilidades para establecer la conexión inicialmente. Una de ellas es la que utiliza el modelo cliente y un servidor,un modelo estándar de ejecución de aplicaciones en una red. Un servidor es un proceso que se está ejecutando en un nodo de la red y que gestiona el acceso a un determinado recurso. Un cliente es un proceso que se ejecuta en el mismo o en diferente nodo y que realiza peticiones de servicio al servidor. Su nombre se debe a que es el que solicita información al servidor, es decir, las peticiones están originadas por la necesidad de acceder al recurso que gestiona el servidor. El servidor está continuamente esperando peticiones de servicio. Cuando se produce una petición el servidor despierta y atiende al cliente. Cuando el servicio concluye, el servidor vuelve al estado de espera. Resumiendo, servidor es el programa que permanece pasivo a la espera de que alguien solicite conexión con él, normalmente, para pedirle algún dato. Cliente es el programa que solicita la conexión para, normalmente, pedir datos al servidor. Desarrollo. 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. Los procesos pueden estar ejecutándose en el mismo o en distintos sistemas, unidos mediante una red. Los sockets permiten la comunicación entre procesos, como los teléfonos permiten la comunicación entre las personas. Los sockets se crean dentro de un dominio de comunicación, igual que un archivo se crea de un filesystem. El dominio de comunicación nos dice dónde 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. Una forma de conseguir que dos programas se transmitan datos, basada en el protocolo TCP/IP, es la programación de sockets. Un socket no es más que un "canal de comunicación" entre dos programas que corren sobre ordenadores distintos o incluso en el mismo ordenador. Desde el punto de vista de programación, un socket no es más que un "fichero" que se abre de una manera especial. Una vez abierto se pueden escribir y leer datos de él con las habituales funciones de read() y write() del lenguaje C. Tipos de sockets. Tenemos dos categorías de sockets: Sockets Stream son los más utilizados, hacen uso del protocolo TCP ( figura 4-1), el cual nos provee un flujo de datos bidireccional, secuenciado, sin duplicación de paquetes y libre de errores. Son llamados sockets orientados a conexión. La especificación del protocolo TCP se puede leer en la RFC-793 . 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. Se llaman también sockets no orientados a 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. La especificacion del protocolo UDP se puede leer en la RFC-768. 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. Implementación de Sockets en C. Existen diversos lenguajes de programación para implementar sockets como PHP, Java, Visual Basic, Python, C, C++ y otros. En esta ocasión emplearemos al lenguaje C para lograr nuestro objetivo. Para lograr el objetivo del trabajo primero conoceremos cómo es el almacenamiento de datos en la memoria y qué funciones de conversión se requieren cuando se crean sockets, también daremos un vistazo a la creación y conexión de sockets y las funciones implicadas en el proceso para los dos tipos de sockets estudiados. Almacenamiento de datos en la memoria y funciones de conversión. Network byte order y Host byte order son dos formas en las que el sistema puede almacenar los datos en memoria. Dependiendo 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. Las conversiones se realizarán con las siguientes conversiones: 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. Estructuras a utilizar. En los dos sockets emplearemos las estructuras siguientes: 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. }; 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 nuestros ejemlos solo será AF_INET. sa_data contiene la dirección IP y número de puerto asignado al socket. Se creó la esctuctura sockaddr_in para el caso de internet, para poder referenciar los elementos de forma más fácil. Los apuntadores a la estructura sockaddr_in deben ser precedidos con un cast tipo *struct sockaddr antes de pasarlos como parámetros a funciones. : sin_family sera 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 longuitud de sockaddr, debe estar inicializada a cero con la función bzero(). Ver la página del manual Debemos asignarle valores a una variable tipo sockaddr_in antes de llamara las función bind(). El siguiente ejemplo lo aclara perfectamente: struct sockaddr_in my_addr; ... my_addr.sin_family=AF_INET; my_addr.sin port=htones(3490); //Número de puerto por donde escuchará el servidor. my_addr.sin_addr.s_addr=inet_addr(\“132.241.5.10\”); //IP de la interface por donde escuchará el servidor. bzero(&(my_addr.sin_zero),8); //Rellenos con ceros. Notas: Si se asigna el valor cero a sin_port, el sistema nos dará automáticamente un puerto disponible. Se puede automatizar, la asignación de la IP, si ponemos el valor INADDR_ANY, el sistema asignará la dirección IP local (my_addr.sin_addr.s_addr=htonl(INADDR_ANY); ). Las variables my_addr.sin_port y my_addr.sin_addr.s_addr deben estar en network byte order ya que son valroes que viajan por la red pero my_addr.sin_family no porque solo es utilizado por el kernel para saber qué tipo de dirección contiene la estructura. La función inet_addr( ) convierte una dirección IP en notación números y puntos en un dato de tipo unsigned long, retorna la dirección en network byte order. Retorna 1 si hubo error. Creación de socket. socket( ) Para poder realizar una conexión utilizaremos un socket. Los sockets se crean llamando a la función socket(), esta función retorna un descritpor de socket, que es tipo int. Si hubo algún error, socket() retorna -1 y la variable global errno se establece con un valor que indica el error que se produjo sockf= socket ( int dominio, int tipo, int protocolo ); sockfd: es el descriptor de socket devuelto. Luego se utilizará para conectarse, recibir conexiones, enviar y recibir datos, etc. dominio: dominio donde se realiza la conexión. Nosotros manejaremos AF_INET. Tipo: podrá ser SOCK_STREAM o SOCK_DGRAM o SOCK_RAW. Protocolo: 0 (cero, selecciona el protocolo más apropiado ). Si se especifica al protocolo como cero, el sistema selecciona el protocolo apropiado de uno de los protocolos disponibles, dependiendo del tipo de socket requerido. bind( ) . Para poder recibir llamadas se debe tener un número telefónico, para poder recibir conexiones se le debe asignar un nombre al socket. El socket se crea sin nobre, debemos asignarle uno para poder recibir conexiones. bind () se utiliza para darle un nombre al socket, osea una dirección IP y número de puerto del host local por donde escuchará, al especificar una IP del host local le estamos diciendo por cual interfaz física escuchará (el sistema puede tener varias interfaces ethernet, ppp, etc). Es necesario llamar a bind() cuando se está programando un servidor. Cuando se está programando un cliente generalmente no se utiliza esta función, el kernel le asignará al socket la dirección IP y número de puerto disponible al llamar a ciertas funciones, por ejemplo cuando llamamos a connect( ) para conectarnos con un sistema remoto. En el servidor es necesario llamar a bind() debido a que el número de puerto debe ser conocido para que los clientes puedan conectarse. Por ejemplo si estamos programando un servidor de telnet debemos llamar a bind() para asignarle al socket el puerto 23. En la aplicación cliente se puede llamar a bind() y asignarle un número de puerto, pero no es necesario porque nadie va a tratar ni podra conectarse con él. int bind(int sockfd, struct sockaddr *my_addr, int addrlen) sockfd : es el descriptor de socket devuelto por la función socket(). my_addr: es un puntero a una estuctura sockaddr que contiene la IP del host local y el número depuerto que se va a asignar al socket. addrlen :debe ser establecido al tamaño de la estuctura sockaddr. sizeof(struct sockaddr). Pasos para establecer la comunicación. En sockets orientados a conexión el procedimiento es el siguiente: 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 hablilita 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á conectarsecon el servidor. El servidor ejecuta la f unció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(). Cuando se programan sockets no orientados a conexión la metodología es esta: Ambos, cliente y servidor, crean un socket mediante la función socket( ). El servidor debe establecer por qué número de puerto recibirá los datos, en este caso no existe la conexión, los datos se envían como si fueran mensajes. Para realizar transferencia de datos, utilizan las funciones sendto( ) y recvfrom( ). No es necesario que el servidor llame a la función listen( ), ni tampoco el paso connect( ) o accept( ). Funciones que figuran en la conexión. listen() Se llama desde el servidor, habilita el socket para que pueda recibir conexiones.Sólo se aplica a sockets tipo SOCK_STREAM. int listen ( int sockfd, int backlog) sockfd :es el descriptor de socket devuelto por la función socket() que será utilizado para recibir conexiones. backlog :es el número máximo de conexiones en la cola de entrada de conexiones. Las conexiones entrantes quedan en estado de espera en esta cola hasta que se aceptan ( accept () ). accept( ) Se utiliza en el servidor, con un socket habilitado para recibir conexiones ( listen() ). Esta función retorna un nuevo descriptor de socket al recibir la conexión del cliente en el puerto configurado. La llamada a accept() no retornará hasta que se produce una conexión o es interrumpida por una señal. int accept ( int sockfd, void *addr, int *addrlen) sockfd :e s el descriptor de socket habilitado para recibir conexiones. addr :es un apuntador u a una estructura sockadd_in. Aquí se almacenará informacion de la conexión entrante. Se utiliza para determinar que host está llamando y desde qué número de puerto. addrlen : debe ser establecido al tamaño de la estuctura sockaddr. sizeof(struct sockaddr): accept() no escribirá más de addrlen bytes en addr . Si escribe menos bytes, modifica el valor de addrlen a la cantidad de bytes escritos. connect() Inicia la conexión con el servidor remoto, lo utiliza el cliente para conectarse. int connect ( int sockfd, struct sockaddr *serv_addr, int addrlen ) sockfd : es el descriptor de socket devuelto por la función socket(). serv_addr : es una estructura sockaddr que contine la dirección IP y número de puerto destino. addrlen : debe ser inicializado al tamaño de struct sockaddr send() y recv() Después de establecer la conexión, se puede comenzar con la transferencia de datos. Estas dos funciones son para realizar transferencia de datos sobre sockets stream. send() y recv() son identicas a write() y read(), exepto que se agrega un parámetro flags. send ( int sockfd, const void *msg, int len, int flags ) sockfd :e el descriptor socket por donde se enviarán los datos. msg :apuntador a los datos a ser enviados. len :ongitud de los datos en bytes. send() retorna la cantidad de datos enviados, la cual podrá ser menor que la cantidad de datos que se escribieron en el buffer para enviar. send() enviará la máxima cantidad de datos que pueda manejar y retorna la cantidad de datos enviados, es responsabilidad del programador comparar la cantidad de datos enviandos con len y si no se enviaron todos los datos, enviarlos en la próxima llamada a send(). recv ( int sockfd, void *buf, int len, unsigned int flags ) sockfd : es el descriptor socket por donde se recibirán los datos. buf :apuntador a un buffer donde se almacenarán los datos recibidos. len : es la longitud del buffer buf. Si no hay datos a recibir en el socket , la llamada a recv() no retorna (bloquea) hasta que llegan datos, se puede establecer al socket como no bloqueante de manera que cuando no hay datos para recibir la función retorna -1 y establece la variable errno=EWOULDBLOCK. recv() retorna el número de bytes recibidos. sendto() y recvfrom() Funciones para realizar transferencia de datos sobre sockets datagram. int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen) sockfd : descriptor socket por donde se enviarán los datos. Msg : apuntador a los datos a ser enviados. len :es la longitud de los datos en bytes. to :es un apuntador a una estructura sockaddr que contiene la dirección IP y número de puerto destino. tolen :debe ser inicializado al tamaño de struct sockaddr sendto() retorna el número de bytes enviados, el cual puede ser menor que len, igual que en send(). int recvfrom ( int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen ) sockfd :es el descriptor socket por donde se recibirán los datos. buf :un apuntador a un buffer donde se almacenarán los datos recibidos. len :es la longitud del buffer buf. from :es un apuntador a una estructura sockaddr que contiene la dirección IP y número de puerto del host origen de los datos. fromlen :debe ser inicializado al tamaño de struct sockaddr. Si no hay datos a recibir en el socket , la llamada a recvfrom() no retorna (bloquea) hasta que llegan datos, se puede establecer al socket como no bloqueante de manera que cuando no hay datos para recibir la función retorna -1 y establece la variable errno=EWOULDBLOCK. recvfrom() retorna el numero de bytes recibidos, igual que recv(). close () y shutdown (). Finalizan la conexión del descriptor de socket. close ( sockfd): despues de utilizar close, el socket queda desabilitado para realizar lecturas o escrituras. shutdown (sockfd, int how):permite desabilitar la comunicación en una determinada dirección o en ambas direcciones. Código fuente, compilación y ejecución de los sockets. Ya tenemos conocimiento de las funciones, estructuras y conversiones que requerimos para implementar sockets así que en las páginas siguientes se mostrarán en primer lugar el código fuente para los dos tipos de socket que hemos decidido exponer: No orientado a conexión: que está formado por los archivos clienteEco.c y servidorEco.c . Orientado a conexión: consta de dos archivos que son clienteTCP.c y servidorTCP.c . Para llevar realzar las dos últimas tarea utilizamos el sistema operativo Fedora 9 para linux. clienteEco.c #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> /**********************************************************/ /* función MAIN */ /* Orden Parametros: IP destino, puerto , mensaje */ /* */ /**********************************************************/ main(int argc, char *argv[]) { int s; struct sockaddr_in bs,des; char resp[255]; int *sd; if (argc == 4) { // Creamos el socket s = socket(AF_INET,SOCK_DGRAM,0); if (s != -1) { bs.sin_family = AF_INET; bs.sin_port = htons(0); //Coge cualquier puerto disponible en la máquina bs.sin_addr.s_addr = htonl(INADDR_ANY); //Coge cualquier IP de la máquina //Asigna un nombre local al socket if( bind(s,(struct sockaddr*)&bs, sizeof(bs)) != -1) { des.sin_family = AF_INET; des.sin_addr.s_addr = inet_addr(argv[1]); des.sin_port = htons(atoi(argv[2])); } //Envia el string sendto(s,argv[3],strlen(argv[3])+1,0,(struct sockaddr*)&des,sizeof(des)); printf("\n\n->Enviando: %s, a: %s en el puerto: %s \n",argv[3], argv[1], argv[2]); //Recibe el string del servidor recvfrom(s,resp, sizeof(resp) ,0,(struct sockaddr*)&des, sd); printf("<-Recibido: %s\n",resp); //Cierra el socket close(s); } else { printf("ERROR al nombrar el socket\n"); } } else { printf("ERROR: El socket no se ha creado correctamente!\n"); } } else { printf("\n\n\aEl número de parámetros es incorrecto\n\n"); } servidorEco.c #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> /**********************************************************/ /* función MAIN */ /* Orden Parametros: Puerto */ /* */ /**********************************************************/ main(int argc, char *argv[]) { int s; struct sockaddr_in bs, in; char entrada[255]; int sd; if (argc == 2) { // Creamos el socket s = socket(AF_INET,SOCK_DGRAM,0); if (s != -1) { bs.sin_family = AF_INET; bs.sin_port = htons(atoi(argv[1])); //Asigna el puerto especificado por la línea de comandos bs.sin_addr.s_addr = htonl(INADDR_ANY); //IP cualquiera de la máquina //Asigna un nombre local al socket if( bind(s,(struct sockaddr*)&bs, sizeof(bs)) != -1) { printf("\n\aServidor ACTIVO escuchando en el puerto: %s\n",argv[1]); //El while permite atender a múltiples clientes while (1) { //Recibe la cadena del cliente if ( recvfrom(s,entrada, sizeof(entrada) ,0,(struct sockaddr*) &in, &sd)== -1) perror("Error en recvfrom"); //Devuelve la cadena al cliente if (sendto(s,strcat(entrada,"\0"),strlen(entrada)+1,0,(struct sockaddr*) &in, sizeof(in)) == -1) } perror("Error en sendto"); } //Se cierra el socket close(s); } else { printf("ERROR al nombrar el socket\n"); } } else { printf("ERROR: El socket no se ha creado correctamente!\n"); } } else { printf("\n\n\aEl número de parámetros es incorrecto\n\n"); } Compilación y ejecución de estos sockets: Servidor: Cliente: clienteTCP.c #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> /**********************************************************/ /* función MAIN */ /* Orden Parametros: IP destino, puerto , mensaje */ /* */ /**********************************************************/ main(int argc, char *argv[]) { int s; struct sockaddr_in bs,des; char resp[255]; int *sd; if (argc == 4) { // Creamos el socket s = socket(AF_INET,SOCK_STREAM,0); if (s != -1) { bs.sin_family = AF_INET; bs.sin_port = htons(0); //Asigna un puerto disponible dela máquina bs.sin_addr.s_addr = htonl(INADDR_ANY); //Asigna una IP de la máquina //Asigna un nombre local al socket if( bind(s,(struct sockaddr*)&bs, sizeof(bs)) != -1) { //Se prepara el nombre de la máquina remota des.sin_family = AF_INET; des.sin_addr.s_addr = inet_addr(argv[1]); des.sin_port = htons(atoi(argv[2])); //Establece la conexión con la máquina remota connect(s,(struct sockaddr*)&des,sizeof(des)); } //Envía el mensaje send(s,argv[3],strlen(argv[3])+1,0); printf("\n\n->Enviando: %s, a: %s en el puerto: %s \n",argv[3], argv[1], argv[2]); //Recibe la respuesta recv(s,resp, sizeof(resp) ,0); printf("<-Recibido: %s\n",resp); //Se cierra la conexión (socket) close(s); } else { printf("ERROR al nombrar el socket\n"); } } else { printf("ERROR: El socket no se ha creado correctamente!\n"); } } else { printf("\n\n\aEl número de parámetros es incorrecto\n\n"); } servidorTCP.c #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAX_CONN 10 //Nº máximo de conexiones en espera /**********************************************************/ /* función MAIN */ /* Orden Parametros: Puerto */ /* */ /**********************************************************/ main(int argc, char *argv[]) { int s,s_aux; struct sockaddr_in bs,in, out_s; char entrada[255]; int sd; if (argc == 2) { // Creamos el socket s = socket(AF_INET, SOCK_STREAM,0); if (s != -1) { bs.sin_family = AF_INET; bs.sin_port = htons(atoi(argv[1])); bs.sin_addr.s_addr = htonl(INADDR_ANY); //Asigna un nombre local al socket if( bind(s,(struct sockaddr*)&bs, sizeof(bs)) != -1) { printf("\n\aServidor ACTIVO escuchando en el puerto: %s\n",argv[1]); //Espera al establecimiento de alguna conexión listen(s, MAX_CONN); //Permite atender a múltiples usuarios while (1) { //Establece una conexión s_aux = accept (s,(struct sockaddr*) &in, &sd); //Recibe el mensaje del cliente if ( recv(s_aux,entrada, sizeof(entrada) ,0)== -1) perror("Error en recvfrom"); //Envia el mensaje al cliente if (send(s_aux,strcat(entrada,"\0"),strlen(entrada)+1,0) == -1) perror("Error en sendto"); //Cierra la conexión con el cliente actual close(s_aux); } //Cierra el servidor close(s); } else { printf("ERROR al nombrar el socket\n"); } } else { printf("ERROR: El socket no se ha creado correctamente!\n"); } } else { printf("\n\n\aEl número de parámetros es incorrecto\n\n"); } } Compilación y ejecución de estos sockets. Servidor TCP Cliente TCP Conclusiones. Gracias al desarrollo de la investigación, hemos podido comprender de mejor manera la construcción o implementación de sockets en el lenguaje C así como visualizar el funcionamiento de los mismos. Otro punto a resaltar es que también se comprendió la diferencia entre sockets orientados y no orientados a conexión. Bibliografía Márquez García, Francisco Manuel.Programación Avanzada en Unix.México.3era Edición., 2004. Mesografía. http://www.eslinux.com/articulos/8591/programacion-sockets-lenguaje-c http://www.tutorial-enlace.net/tutorial-Programacion_de_sockets_con_C_y_linux-1841.html http://www.eslinux.com/articulos/8591/programacion-sockets-lenguaje-c http://www.agapea.com/libros/TCP-IP-Sockets-in-C-2nd-Edition-isbn-0123745403-i.htm http://foro.elhacker.net/programacion_cc/introduccion_al_uso_de_sockets_en_c-t12480.0.html