sockets en windows

52
1 Programación con sockets para Windows Índice 1 Introducción............................................................................................................... 2 1.1 Arquitectura Cliente/Servidor.............................................................................. 2 1.2 Concepto y tipos de sockets .............................................................................. 2 1.3 La API de Windows............................................................................................ 5 2 Operaciones básicas con sockets ............................................................................ 5 2.1 Inicialización de Ias DLLs ................................................................................ 5 2.2 Función socket ................................................................................................ 7 2.3 Utilidades para las funciones ........................................................................... 8 2.4 Función bind .................................................................................................... 14 3 Operaciones para comunicaciones con UDP ........................................................... 16 3.1 Función sendto ................................................................................................. 16 3.2 Función recvfrom .......................................................................................... 17 3.3 Función closesocket ......................................................................................... 17 3.4 Esquema cliente/servidor con UDP.................................................................... 18 3.5 Un ejemplo con UDP ......................................................................................... 19 4 Operaciones para comunicaciones multicast .......................................................... 23 4.1 Función setsockopt .......................................................................................... 23 4.2 Función closesocket ........................................................................................ 26 4.3 Esquema cliente/servidor con multicast ............................................................ 27 4.4 Un ejemplo con multicast .................................................................................. 28 5 Operaciones para comunicaciones con TCP ........................................................... 32 5.1 Función connect ............................................................................................... 32 5.2 Función listen..................................................................................................... 34 5.3 Función accept ................................................................................................. 34 5.4 Función send .................................................................................................... 37 5.5 Función recv ................................................................................................. 37 5.6 Funciones closesocket y shutdown .................................................................. 38 5.7 Cliente con TCP................................................................................................. 39 5.7.1 Ejemplo de un cliente con TCP .............................................................. 39 5.8 Servidor iterativo con TCP ........................................................................... 42 5.8.1 Esquema cliente/servidor con servidor iterativo con TCP ..................... 42 5.8.2 Un ejemplo con servidor iterativo con TCP .......................................... 43 5.9 Servidor concurrente con TCP ...................................................................... 46 5.9.1 Función _beginthread ........................................................................... 46 5.9.2 Esquema cliente/servidor con servidor concurrente con TCP................ 47 5.9.3 Un ejemplo con servidor concurrente con TCP..................................... 49

Upload: lapivot

Post on 03-Jul-2015

3.483 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Sockets en Windows

1

Programación con sockets para Windows Índice 1 Introducción............................................................................................................... 2

1.1 Arquitectura Cliente/Servidor.............................................................................. 2 1.2 Concepto y tipos de sockets .............................................................................. 2 1.3 La API de Windows............................................................................................ 5

2 Operaciones básicas con sockets ............................................................................ 5 2.1 Inicialización de Ias DLLs ................................................................................ 5 2.2 Función socket ................................................................................................ 7 2.3 Utilidades para las funciones ........................................................................... 8 2.4 Función bind .................................................................................................... 14

3 Operaciones para comunicaciones con UDP ........................................................... 16 3.1 Función sendto ................................................................................................. 16 3.2 Función recvfrom ….......................................................................................... 17 3.3 Función closesocket ......................................................................................... 17 3.4 Esquema cliente/servidor con UDP.................................................................... 18 3.5 Un ejemplo con UDP ......................................................................................... 19

4 Operaciones para comunicaciones multicast .......................................................... 23 4.1 Función setsockopt .......................................................................................... 23 4.2 Función closesocket ........................................................................................ 26 4.3 Esquema cliente/servidor con multicast ............................................................ 27 4.4 Un ejemplo con multicast .................................................................................. 28

5 Operaciones para comunicaciones con TCP ........................................................... 32 5.1 Función connect ............................................................................................... 32 5.2 Función listen..................................................................................................... 34 5.3 Función accept ................................................................................................. 34 5.4 Función send .................................................................................................... 37 5.5 Función recv …................................................................................................. 37 5.6 Funciones closesocket y shutdown .................................................................. 38 5.7 Cliente con TCP................................................................................................. 39

5.7.1 Ejemplo de un cliente con TCP .............................................................. 39 5.8 Servidor iterativo con TCP ........................................................................... 42

5.8.1 Esquema cliente/servidor con servidor iterativo con TCP ..................... 42 5.8.2 Un ejemplo con servidor iterativo con TCP .......................................... 43

5.9 Servidor concurrente con TCP ...................................................................... 46 5.9.1 Función _beginthread ........................................................................... 46 5.9.2 Esquema cliente/servidor con servidor concurrente con TCP................ 47 5.9.3 Un ejemplo con servidor concurrente con TCP..................................... 49

Page 2: Sockets en Windows

2

Capítulo 1. Introducción

En este capítulo 1 se desea presentar una serie de conceptos necesarios para poder utilizar las funciones proporcionadas por la librería de sockets para Windows (winsock).

1.1 Arquitectura Cliente/Servidor El modelo (o paradigma) cliente-servidor establece que en cualquier comunicación entre

un par de aplicaciones, una aplicación debe comenzar la ejecución y esperar a que la otra contacte con ella. El paradigma cliente-servidor divide las aplicaciones de comunicaciones en dos categorías, dependiendo de si la aplicación espera la comunicación o la inicia. A la aplicación que inicia la comunicación se la denomina cliente. A la aplicación con la que contacta el cliente se la denomina servidor. La comunicación entre el cliente y el servidor se puede resumir, en la mayoría de los casos, de la siguiente manera: el cliente envía una (o varias) petición(es) y espera una (o varias) respuesta(s) del servidor. Las peticiones y respuestas se realizan a través de las operaciones proporcionadas por una conjunto de librerías denominadas API (son las siglas inglesas de Interfaz de Programación de Aplicaciones)

PROCESO CLIENTE

cliente SOLICITA UN SERVICIO AL SERVIDOR. LA/S PETICIÓN/ES SE HACEN UTILIZANDO LAS OPERACIONES DE UN API PROCESO

SERVIDOR

servidor ESPERA LA PETICIÓN DE UN CLIENTE, PROCESA DICHA PETICIÓN, Y, EN LA MAYORÍA DE LOS CASOS, CONTESTA AL CLIENTE

Figura 1: Paradigma cliente/servidor.

1.2 Concepto y tipos de sockets El identificador por el que el cliente (o el servidor) envía o recibe datos a través de la

red se denomina socket ("enchufe"). Un socket representa simplemente un punto de conexión entre la aplicación y la red de comunicaciones. Es aquí donde aparece el famoso principio cliente/servidor, por el cual uno de los sockets (o puntos de conexión) actúa como servidor, atendiendo las peticiones del otro socket, que adopta el papel de cliente, enviando peticiones al

Page 3: Sockets en Windows

3

servidor y recibiendo a su vez el resultado de dichas solicitudes. Desde el punto de vista de los programadores, los sockets son los únicos identificadores de la red de comunicaciones y es a través de ellos por donde se enviarán o se recibirán los datos. Desde el punto de vista de la red, un socket debe ser implementado de forma que se le identifique de forma unívoca con respecto a todas las posibles aplicaciones que puedan existir en la red. Para realizar esa identificación dependerá de cuál sea la red que vamos a utilizar. Hoy en día la red que se emplea en la inmensa mayoría de los casos es la red Internet, también llamada arquitectura TCP/IP. En todo este tema vamos a centrar nuestro estudio en la comunicación con sockets utilizando siempre la arquitectura TCP/IP. Un socket, desde el punto de vista de la arquitectura TCP/IP, está representado por dos elementos fundamentales: la <dirección IP del equipo> y por el <número de puerto>. La <dirección IP del equipo> identifica la ubicación del ordenador donde se encuentra la aplicación con el socket. El <número de puerto> identifica uno de los distintos procesos que pueden tener lugar en la máquina <dirección IP del equipo>. En la siguiente figura 2 podemos ver que la aplicación cliente (y servidora) utiliza en el código la variable s_cli (s_serv) para poder acceder a la red Internet. El cliente envía los datos por el socket s_cli con la función de la librería del API de sockets send() (más adelante se estudiará con detalle). En el caso del servidor, los datos se reciben por el socket s_serv con la función de la librería del API de sockets recv() (también más adelante se estudiará esta función con detalle). Obsérvese también en la siguiente figura que, desde el punto de vista del nivel de transporte, el enchufe s_cli se implementa mediante la concatenación de la dirección IP 199.33.22.12 y el número de puerto 3333. En el caso de s_serv es mediante la concatenación de la dirección IP 130.40.50.10, y del número de puerto 80.

Servidor … SOCKET s_serv; … recv(s_serv, …)

s serv

<199.33.22.12, 3333> <130.40.50.10, 80>

interfaz de red

IP

Cliente … SOCKET s_cli; … send(s_cli, …)

s cli

TCP o UDP

Nivel Aplicación

Nivel Transporte

Nivel Red

Figura 2: Identificación de los sockets en la arquitectura TCP/IP.

Page 4: Sockets en Windows

4

Se puede decir que hay dos clases de aplicaciones clientes: aquellos que invocan servicios estándar TCP/IP y aquellos que invocan servicios a definir. Los servicios estándar son aquellos servicios ya definidos por TCP/IP, y que por lo tanto tienen ya asignado un número de puerto (llamado puerto bien-conocido o “well-known”). Por ejemplo, 80 es el número de puerto para el servidor web (http). Los puertos bien-conocidos están en el rango de 1 a 1024. Consideramos al resto como servicios a definir, y su rango será superior a 1024. En la mayoría de sistemas operativos hay que tener permisos especiales para poder ejecutar los servidores que implementan los servicios estándar (puertos por debajo del 1024). Por ejemplo en UNIX, sólo los puede ejecutar el super-usuario (o también llamado usuario root)

Tipos de sockets Cuando los programadores diseñan las aplicaciones cliente-servidor, deben elegir entre

dos tipos de interacción: orientada a conexión y no orientada a conexión. Los dos tipos de interacción corresponden directamente a los dos protocolos de nivel de transporte que suministra la familia TCP/IP. Si el cliente y el servidor se comunican usando UDP, la interacción es no orientada a conexión. Si utilizan TCP, la interacción es orientada a conexión. Véase el tema anterior para un conocimiento más exhaustivo de ambos protocolos. TCP proporciona toda la fiabilidad necesaria para la comunicación a través de la Internet. Para ello, verifica que los datos llegan y automáticamente retransmite los segmentos que no llegan. Computa un checksum sobre los datos para garantizar que no se corrompen durante la transmisión. Usa números de secuencia para asegurar que los datos llegan en orden, y automáticamente elimina segmentos duplicados. Proporciona control de flujo para asegurar que el emisor no transmite datos más rápidos que el receptor puede consumir. Finalmente, TCP informa tanto al cliente como al servidor si la red es inoperante por algún motivo. Los clientes y servidores que utilizan UDP no tienen garantía acerca de una entrega fiable. Cuando un cliente envía una petición, la petición se puede perder, duplicar, retardar o entregar fuera de orden. Las aplicaciones del cliente y servidor tienen que tomar las acciones oportunas para detectar y corregir tales errores (si quieren hacerlo). Como se puede observar, un protocolo orientado a conexión hace más fácil la tarea del programador al liberarle de la tarea de detectar y corregir errores. Desde el punto de vista del programador, UDP funciona bien si la red que hay por debajo funciona bien, o no le preocupa que se produzcan errores. Por ejemplo, en una LAN el protocolo UDP suele funcionar muy bien, ya que la tasa de errores es muy baja. Los principales tipos de sockets son:

• “Sockets de flujo” (stream sockets): Utilizan el protocolo de transporte TCP. • “Sockets de datagramas” (datagram sockets): Utilizan el protocolo de transporte UDP.

Page 5: Sockets en Windows

5

1.3 La API de Windows En la mayoría de las implementaciones, el protocolo TCP/IP reside en el sistema

operativo. Por tanto si un programa de aplicación usa TCP/IP para comunicarse, debe interactuar con el sistema operativo para pedir un servicio. Desde el punto de vista del programador, las rutinas que el sistema operativo suministra definen el interfaz entre la aplicación y el protocolo en concreto de Internet. La arquitectura TCP/IP no especifica los detalles de como la aplicación debe interactuar con la pila de protocolos de la arquitectura TCP/IP. Es decir, la arquitectura TCP/IP no define un determinado API. Varias APIs han sido creadas para poder utilizar los protocolos de la arquitectura TCP/IP. La más famosa y ampliamente utilizada es la API de sockets. El diseño original de esta API partió de un grupo de diseñadores de Berkeley allá por los años 80. Estas funciones de la API de sockets se implementaron, en el caso de la pila de protocolos de Internet, sobre una plataforma con el sistema operativo UNIX (la primera versión que incorporó esta API fue la 4.3BSD). Esta definición del API hecha por los diseñadores de Berkeley se ha venido incorporando desde entonces en todas las versiones con UNIX y LINUX hasta nuestros días. En este tema también vamos a centrar nuestro estudio en la API de sockets, pero implementada sobre el sistema operativo de Windows. A esta API de sockets para windows se la denomina Winsock. Es importante resaltar que aunque la API de sockets de Berkeley y Winsock son muy parecidas, no son totalmente iguales, y por tanto las aplicaciones no son portables directamente entre sí. Capítulo 2. Operaciones básicas con sockets

En este capítulo se van a presentar todas las funciones y estructuras de datos que van a necesitarse para poder manejar los sockets, independientemente de si son sockets de flujo (stream sockets) o sockets de datagramas (datagram sockets). Es decir, se presentarán las operaciones comunes a los sockets tanto si emplean el protocolo UDP o el protocolo TCP. 2.1 Inicialización de Ias DLLs

Antes de poder utilizar ninguna función de la API, un proceso debe inicializar la DLL de Winsock (ws2_32.dll). El prototipo en C de la función es:

#include <winsock2.h> int WSAStartup(WORD version, WSADATA *wsa_datos);

Page 6: Sockets en Windows

6

El primer parámetro determina el número de versión de Winsock más alto que nuestro programa puede manejar (en nuestro caso usamos la 2.2). Se puede poner la versión utilizando la macro MAKEWORD. El segundo parámetro es un puntero a una estructura de tipo WSADATA, que recibirá información sobre la implementación de Winsock que tengamos en nuestro ordenador: su número de versión, una descripción y el estado actual de la misma, etc. Si la llamada tiene éxito, ya podremos usar el resto de las funciones de sockets. Un ejemplo de utilización de la función WSAStartup() es: #include <winsock2.h>

... int error; WSADATA wsa_datos; ... error = WSAStartup(MAKEWORD( 2, 2 ), &wsa_datos); if ( error != 0 ) exit(1); // error al iniciar la DLL if ( LOBYTE( wsa_datos.wVersion ) != 2 || HIBYTE( wsa_datos.wVersion ) != 2 ) { WSACleanup( ); exit(2); //error en version DLL } ... Para compilar hay que decirle al compilador que enlace la biblioteca Winsock (ws2_32.dll). Con Visual Studio esto debe hacerse desde el propio proyecto (en menú Proyecto -> Propiedades -> Vinculador -> Entrada, y se añade "ws2_32.lib" ). Para una explicación más detallada, ver el tema de herramientas gráficas. Por último, al finalizar la utilización de todas las funciones de la API Winsock hay que ejecutar la función WSACleanup() para descargar correctamente todas estructuras asignadas por la DLL. No obstante, si se nos olvida utilizarla, el sistema descarga la correspondiente DLL de forma automática al finalizar la ejecución de cualquier programa. Esto es así porque, como veremos más adelante, muchos servidores no pueden invocan a esta función al tener que ejecutarse en un bucle permanente.

Page 7: Sockets en Windows

7

2.2 Función socket

Una vez inicializada la DLL, para que una aplicación pueda realizar operaciones de E/S en red para comunicarse con otra aplicación remota, lo primero que tiene que hacer es crear un socket al cual pueda dirigirse. Obviamente, esto es necesario tanto en el cliente como en servidor. El prototipo en C de la función es: #include <winsock2.h> SOCKET socket(int familia_protos, int tipo, int proto); El parámetro familia_protos especifica la familia de protocolos que usaremos. Los diseñadores de la API de Berkeley pensaron que podrían coexistir muchas arquitecturas de comunicaciones que soportaran las operaciones proporcionadas por el interfaz. Hoy en día en la práctica totalidad de los casos se utiliza la arquitectura TCP/IP (o también llamada internet). En el caso de internet que es el que nos concierne en este tema, la constante empleada será PF_INET. Por su parte, tipo indica si el tipo de socket que vamos a crear es de flujo o de datagramas, es decir, si usa TCP o UDP. Se emplea el valor SOCK_STREAM para crear un socket de flujo, y SOCK_DGRAM para crear el socket de datagramas. Por último, el parámetro proto establece el protocolo que se usará en este socket dentro de la familia de protocolos familia_protos. Con el valor cero el protocolo es asignado automáticamente por Winsock. Cualquier valor distinto de cero da la posibilidad de utilizar otros protocolos que no sean el estándar. Si todo va bien, obtendremos un valor de tipo SOCKET, que representa el nuevo punto de conexión obtenido y que tendremos que usar en las siguientes funciones para llegar a establecer una comunicación completa. En caso de error, la función devolverá la constante INVALID_SOCKET. Un ejemplo de utilización de la función socket() es: #include <winsock2.h>

#include <stdio.h> ... SOCKET s; ... s = socket(PF_INET, SOCK_DGRAM, 0); if (s = = INVALID_SOCKET) { printf("ERROR AL CREAR EL SOCKET: %d\n",WSAGetLastError()); exit(1); } ...

Page 8: Sockets en Windows

8

Nótese en el ejemplo que en caso de error al crear el socket se emplea la función WSAGetLastError(). Esta función se puede utilizar siempre que se produzca un error al invocar cualquier función del API Winsock. 2.3 Utilidades para las funciones

Para que un socket pueda ser útil debe estar asociado a una determinada dirección en internet que lo convierta en un punto único dentro de Internet. Esto se conseguirá asignándole al socket el par formado por la dirección IP de la máquina donde se ejecuta la aplicación, y un número de puerto no ocupado ya por otro socket. Para hacer esto el lenguaje C proporciona una serie de estructuras de datos. La primera es una estructura genérica pensada para poder trabajar con múltiples arquitecturas de protocolos.

struct sockaddr { u_short sa_family; char sa_data[14]; }; Debido a esta generalidad no es muy usada. Pensada para Internet existe la siguiente estructura de datos: struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; El campo sin_family indica la familia o formato de direcciones. En el caso de la arquitectura TCP/IP es el valor de la constante AF_INET el que hay que utilizar. El campo sin_port contendrá un número con el puerto elegido. En el campo sin_addr debe ponerse una dirección IP en binario (cada uno de los cuatro bytes se ponen en binario por separado). La estructura in_addr tiene la siguiente estructura:

Page 9: Sockets en Windows

9

struct in_addr{ union { struct {u_char s_b1, s_b2, s_b3,s_b4;} S_un_b; struct { u_short s_w1, s_w2;} S_un_w; u_long S_addr; }S_un; }; #define s_addr S_un.S_addr Normalmente sólo la definición s_addr del campo S_un.S_addr va a ser utilizado, como veremos más adelante. No hay que hacer nada más. No obstante, en el campo sin_zero de la estructura sockaddr_in debe encontrarse con todos sus campos a cero. Para asegurarnos de ello, normalmente se utiliza la función memset(). Tanto la estructura sockaddr como sockaddr_in se encuentran declaradas en <winsock2.h>. Posteriormente se van a presentar múltiples ejemplos de uso de esta estructura sockaddr_in. Conversiones Para poder trabajar con los datos de la estructura sockaddr_in (básicamente una dirección IP y un número de puerto) debemos tener en cuenta un aspecto muy importante: el orden de almacenamiento de los bytes dentro de las variables. Los campos sin_addr.s_addr y sin_port de la estructura sockaddr_in deben tener almacenados sus valores en el formato ”network byte order". El problema es que los ordenadores almacenan los datos en el formato “host byte order”, y ambos formatos no siempre coinciden. Para evitar esta posible disparidad, existen funciones que aseguren el buen almacenamiento de la información. Estas funciones son: • Para el almacenamiento de un número de puerto (que tiene 16 bits) pasándolo del “host

byte order” al “network byte order”: htons(). • Para el almacenamiento de una dirección IP (que tiene 32 bits) pasándola del “host byte

order” al “network byte order”: htonl(). Recuérdese que con estas funciones se garantiza el orden que deben tener los datos en los campos sin_addr.s_addr y sin_port de la estructura sockaddr_in. En algunas ocasiones nos ocurrirá lo contrario, tenemos datos en los campos sin_addr.s_addr y sin_port de la estructura sockaddr_in y queremos pasarlos a alguna variable de la aplicación (que obviamente debe ser almacenada en el formato “host byte order”). Para ello contamos con las siguientes funciones:

Page 10: Sockets en Windows

10

• Para el almacenamiento de un número de puerto (que tiene 16 bits) pasándolo del “network byte order” al “host byte order”: ntohs().

• Para el almacenamiento de una dirección IP (que tiene 32 bits) pasándola del “network

byte order” al “host byte order”: ntohl(). En el siguiente ejemplo presentamos un posible caso. #include <winsock2.h> ... u_short puerto1, puerto2; //unsigned short es igual que u_short struct sockaddr_in direccion1, dirección2; ... puerto1=80; // valor almacenado en “host byte order” direccion1.sin_port=htons(puerto); //valor almacenado en //“network byte order” ... puerto2=ntohs(direccion2.sin_port); //valor almacenado en //“host byte order” Vemos en el ejemplo que la variables puerto1 y puerto2 deben almacenar sus valores en el “host byte order”, mientras que las variables direccion1 y direccion2 deben hacerlo en el “network byte order” Direcciones IP

Para poder manejar de forma correcta las direcciones IP, el API Winsock proporciona las siguientes operaciones: • La función inet_addr() convierte una dirección IP en un entero largo sin signo (u_long). Es

importante resaltar que esta función devuelve el valor en el formato “network byte order”, por lo que no hay que utilizar la función htonl().

• La función inet_ntoa() convierte un entero largo sin signo a una cadena de caracteres.

Un ejemplo de utilización de direcciones IP: #include <winsock2.h> #include <stdio.h> ... struct sockaddr_in direccion; char * cadena; ... direccion.sin_addr.s_addr = inet_addr("138.100.152.2"); cadena=inet_ntoa(direccion.sin_addr); printf("dir IP=%s\n",cadena); // imprime 138.100.152.2 ...

Page 11: Sockets en Windows

11

Otra forma de poder asignar una dirección IP en el campo sin_addr.s_addr de la estructura sockaddr_in es utilizando la constante INADDR_ANY. Esta constante le indica al sistema que asigne la dirección IP que ese equipo tenga. Utilizar esa constante permite poder portar directamente el código de una máquina a otra sin tener que volver a compilar porque la dirección IP haya cambiado. A veces en vez de disponer de la dirección IP, lo que tenemos es el nombre de dominio del equipo. Para poder convertir ese nombre de dominio en el formato necesario para el campo sin_addr.s_addr de la estructura sockaddr_in, disponemos de la estructura hostent y de la función gethostbyname(), que vamos a explicar a continuación: struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; }; #define h_addr h_addr_list[0] • h_name: Es el nombre oficial del equipo. • h_aliases: Es un array con los nombres alternativos del equipo. • h_addrtype: Tipo de la dirección (en el caso de Internet, es AF_INET). • h_length: Longitud de la dirección (en bytes). • h_addr_list: Un array (terminado en cero) de direcciones IP del equipo. Es muy

importante resaltar que las direcciones IP siguen el formato “network byte order”, por lo que no hay que utilizar la función htonl().

• h_addr: Como ya sabemos, la mayoría de los hosts sólo tienen una dirección IP. Para facilitar su uso, se define La primera dirección de h_addr_list.

Esta estructura está definida en <winsock.h>. En la mayoría de los casos, de todos los campos sólo se suele utilizar h_addr_list[0] (en realidad, h_addr) para convertir a una dirección IP un determinado nombre de domininio de un equipo. Para ello se utiliza la función gethostbyname(), cuyo prototipo en C es:

Page 12: Sockets en Windows

12

#include <winsock2.h> struct hostent *gethostbyname(const char *nombre); El parámetro nombre indentifica el nombre de dominio del equipo cuya estructura hostent queremos que nos devuelva (en realidad nos devuelve un puntero a esa estructura). Si devuelve NULL, es porque ha habido un error. Obviamente para que esta función no de error, el nombre de dominio que pasamos debe estar dado de alta en la estructura de DNS (Servidor de Nombres de Dominio), y el sistema operativo de la aplicación tener acceso a uno de estos DNS. Un ejemplo de esta utilización sería: #include <winsock2.h> #include <stdio.h> ...

struct sockaddr_in direccion; struct hostent *datosHost; ... datosHost=gethostbyname("fenix.eui.upm.es"); if (datosHost==NULL){ printf("ERROR no existe ese nombre de dominio\n"); exit(1); } direccion.sin_addr=*((struct in_addr *)datosHost->h_addr); ...

Números de puerto bien-conocidos Como ya se ha visto, se utiliza el campo sin_port de la estructura sockaddr_in para indicar al socket el puerto al que queremos asociarlo. A veces vamos a querer utilizar el puerto de un servicio estándar, es decir, ya definido. Para poder obtener ese puerto bien-conocido (“well-known”) de un determinado servicio estándar disponemos de la estructura servent y de la función getservbyname(), que vamos a explicar a continuación:

Page 13: Sockets en Windows

13

struct servent { char *s_name; char **s_aliases; short s_port; char *s_proto; }; • s_name: Es el nombre del servicio estándar. • s_aliases: Es un array con los posibles nombres alternativos del servicio • s_port: Indica el puerto del servicio. Es muy importante resaltar que este número sigue

el formato “network byte order”, por lo que no hay que utilizar la función htons(). • s_proto: Es el nombre del protocolo que implementa el servicio. Esta estructura está definida en <winsock.h>. En la mayoría de los casos, de todos los campos sólo se suele utilizar s_port. Para ello se utiliza la función getservbyname(), cuyo prototipo en C es: #include <winsock2.h> struct servent *getservbyname(const char *servicio, const char *protocolo);

Page 14: Sockets en Windows

14

Un ejemplo de esta utilización sería: #include <winsock2.h> #include <stdio.h> ...

struct sockaddr_in direccion; struct servent *datosServicio; short puerto; ... datosServicio=getservbyname("http","tcp"); if (datosServicio==NULL){ printf("ERROR no existe ese servicio estandar\n"); exit(1); } direccion.sin_port=datosServicio->s_port; ... puerto=ntohs(direccion.sin_port); printf("puerto del servicio=%d\n",puerto); ...

Nótese en el ejemplo que la variable puerto, como todas las variables de un programa excepto las del tipo sockaddr_in (y sockaddr), debe tener el formato “host byte order”. Por eso si se quiere que el equipo almacene bien el número debe utilizarse la función ntohs(). Como pequeño ejercicio pruebe que pasaría si se elimina la función ntohs() del ejemplo anterior. 2.4 Función bind Como ya se ha comentado previamente, un socket necesita asociarse a una dirección para poder enviar o recibir datos por la red. Esta asociación puede hacerse de forma explícita o implícita (ver Figura 2). Normalmente son los servidores los que de forma explícita (mediante la función bind) eligen la dirección a la que unirse. En la mayoría de los casos, como los host sólo disponen de una dirección IP, lo que se debe elegir es un número de puerto que no esté ocupado por otra aplicación. En el caso de los clientes, normalmente dejan que sean otras funciones utilizadas en la comunicación (connect, sendto, recvfrom, …) las que elijan el puerto al que unirse (usualmente escogen el primer puerto que no esté ya ocupado). No obstante un cliente también puede de forma explícita unirse a una determinada dirección, aunque no es lo habitual.

Page 15: Sockets en Windows

15

El prototipo en C de la función es: #include <winsock2.h> int bind(SOCKET s, const struct sockaddr * dir, int long_dir); El primer parámetro s es el socket devuelto por la función socket(), el parámetro dir es un puntero a la estructura sockaddr (ver sección 2.3), donde deberá ponerse la dirección (es decir, el par <direccion IP>,<número de puerto>) a la que se quiere unir el socket. Recuérdese de la sección 2.3 que es más fácil de usar la estructura sockaddr_in. Para evitar “warnings” del compilador, si vamos a utilizarla hay que hacer un casting al tipo sockaddr (ver el ejemplo siguiente). El parámetro long_dir indica el tamaño de la estructura apuntada por dir. Esta función devuelve 0 si todo ha ido bien, y SOCKET_ERROR si no se ha podido unir a la dirección apuntada por dir. Un ejemplo de utilización de la función bind() es: #include <winsock2.h> #include <stdio.h> ... SOCKET s; struct sockaddr_in dirMiEquipo; int resul; ... s = socket(PF_INET, SOCK_DGRAM, 0); if (s == INVALID_SOCKET) exit(1); //error al crear el socket

memset(&dirMiEquipo, 0, sizeof(struct sockaddr_in));// pone a // cero toda la estructura dirMiEquipo.sin_family = AF_INET; dirMiEquipo.sin_addr.s_addr = INADDR_ANY; // IP que tenga el equipo dirMiEquipo.sin_port = htons(2222); //elijo un puerto libre

resul=bind(s, (struct sockaddr *) &dirMiEquipo, sizeof(dirMiEquipo));

if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n",WSAGetLastError()); exit(2); } ...

Page 16: Sockets en Windows

16

Recuérdese de la sección 2.3 que tenemos toda una serie de estructuras y funciones para poder manejar la dirección de un socket: inet_addr(), gethostbyname(), … También es muy importante conocer que el orden en el que almacenan los datos tanto en la estructura sockaddr (y sockaddr_in) como en el resto de variables del equipo. Por tanto, hay que utilizar correctamente las funciones htonl(), htons(), ntohl(), y ntohs(). Capítulo 3. Operaciones para comunicaciones con UDP

En este capítulo se van a presentar todas las funciones que van a necesitarse para poder comunicar un cliente y un servidor utilizando sockets de datagramas, es decir, utilizando el protocolo UDP.

3.1 Función sendto

Esta función permite a un socket enviar información a través de la red a otro socket que se encuentra en una determinada dirección (es decir, al socket de una aplicación con un determinado par formado por <dir IP> y <número puerto>). El prototipo en C de la función es:

#include <winsock2.h> int sendto(SOCKET s, const char *msj, int long_msj, int flags, const struct sockaddr *dirDestino, int long_dirDestino); Esta function envía el array de datos contenido en el parámetro msj por el socket s. El parámetro long_msj indica el tamaño del parámetro anterior. El parámetro flags permite enviar datos con distintas opciones (“fuera de banda”, “adelantados”, etc). Un envío normal de datos se consigue poniendo en este campo flags un 0. El parámetro dirDestino es un puntero a la estructura sockaddr, donde deberá ponerse la dirección del socket de la aplicación donde se quieren enviar los datos. Podemos utilizar también la estructura sockaddr_in, pero haciendo casting con sockaddr para evitar “warnings” del compilador. El parámetro long_dirDestino indica el tamaño de la estructura apuntada por dirDestino. Esta función devuelve el número de bytes enviados por la red si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo al enviar. Al final de esta sección se presenta un ejemplo donde se utilizará esta función sendto().

Page 17: Sockets en Windows

17

3.2 Función recvfrom

Esta función permite a un socket recibir información a través de la red, indicándonos desde qué dirección nos envían dicha información. El prototipo en C de la función es:

#include <winsock2.h> int recvfrom(SOCKET s, const char *msj, int long_msj, int flags, struct sockaddr *dirDestino, int *long_dirDestino); Esta function recibe para el socket s una serie de datos que almacena en el array del parámetro msj. El parámetro long_msj indica el tamaño del parámetro anterior. El parámetro flags permite, al igual que en el caso de sendto, recibir datos con distintas opciones (“fuera de banda”, “adelantados”, etc). Una recepción normal de datos se consigue poniendo en el campo flags un 0. Nótese, a diferencia de lo que pasa en sendto(), que a priori no podemos saber quién será quien nos va a enviar los datos. Por lo tanto, esto dos últimos parámetros los rellenará el sistema una vez que se reciban los datos, nunca la aplicación que invoca a esta función. Por ello el parámetro dirDestino es un puntero a la estructura sockaddr, donde deberá recibirse la dirección del socket de la aplicación que nos ha enviado los datos. Al igual que con sendto(), podemos utilizar también la estructura sockaddr_in, pero haciendo casting con sockaddr para evitar “warnings” del compilador. El parámetro long_dirDestino es (a diferencia de la función sendto()) un puntero que nos indica el tamaño de la estructura apuntada por dirDestino. Esta función devuelve el número de bytes recibidos si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo al enviar. Al final de esta sección se presenta un ejemplo donde se utilizará esta función recvfrom(). 3.3 Función closesocket Esta función en los sockets de datagramas sólo tiene un efecto local a la aplicación que lo invoca. La función closesocket() se invoca cuando la aplicación ya no quiere hacer uso del socket que creó previamente. Hay que resaltar que esta función siempre debe ser invocada antes de la función WSACleanup(). El prototipo en C de la función closesocket() es: #include <winsock2.h> int closesocket(SOCKET s);

Page 18: Sockets en Windows

18

Esta función closesocket() devuelve 0 si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo al cerrar el socket. 3.4 Esquema cliente/servidor con UDP

En la siguiente figura 3 presentamos un esquema con las funciones a utilizar para una comunicación con el protocolo UDP. Se ha supuesto, por sencillez, que se hace únicamente un envío y una recepción de datos entre el emisor y el receptor. Obsérvese que la función recvfrom() es bloqueante, por lo que hasta que no reciba los datos (enviados mediante la función sendto()) la aplicación no pasará a ejecutar ninguna otra instrucción.

recvfrom( )

sendto( )

DATOS (RESPUESTA)

socket( )

bind( )

sendto( )

DATOS (PETICION) recvfrom( ) BLOQUEO

BLOQUEO

socket( )

Cliente

WSAStartup( )

WSACleanup( )

WSAStartup( )

WSACleanup( )

closesocket()) closesocket()

Servidor

Figura 3: Una posible comunicación con sockets de datagrama

Page 19: Sockets en Windows

19

3.5 Un ejemplo con UDP

Para clarificar los conceptos presentados hasta ahora, se va a presentar un ejemplo sencillo de comunicación con UDP siguiendo el esquema del apartado anterior. En él tanto el cliente como el servidor enviarán un mensaje de saludo. En “Windows Visual Studio” podemos crear un proyecto con el cliente y el servidor. Con otros compiladores, con tener un fichero con la extensión “.c” será suficiente. El cliente #include <winsock2.h> #include <stdio.h> #include <string.h>

void main(){ SOCKET s; struct sockaddr_in dir_serv; int resul, puerto_serv, error, long_dir_serv; WSADATA wsa_datos; char cadena_dir_ip_serv[20]; // cadena con la ip del servidor char msj_env[80]; // datos a enviar char msj_rec[80]; // datos a recibir

printf("--- CLIENTE ---\n"); printf("Direccion IP del servidor="); scanf("%s",&cadena_dir_ip_serv); //lee la dir IP del servidor printf("Puerto del servidor="); scanf("%d",&puerto_serv); //lee el puerto del servidor

error = WSAStartup(MAKEWORD( 2, 2 ), &wsa_datos); if ( error != 0 ) exit(1); // error al iniciar la DLL if ( LOBYTE( wsa_datos.wVersion ) != 2 || BYTE( wsa_datos.wVersion ) != 2 ) { HI WSACleanup( ); exit(1); }

s = socket(PF_INET, SOCK_DGRAM, 0); if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n",WSAGetLastError()); exit(2); }

strcpy(msj_env,"Me saludas?, soy el cliente");

memset(&dir_serv, 0, sizeof(struct sockaddr_in)); dir_serv.sin_family = AF_INET; dir_serv.sin_addr.s_addr = inet_addr(cadena_dir_ip_serv); dir_serv.sin_port = htons(puerto_serv);

Page 20: Sockets en Windows

20

resul=sendto(s, msj_env, sizeof(msj_env),0, (struct sockaddr *) &dir_serv, sizeof(dir_serv)); if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n",WSAGetLastError()); exit(3); } long_dir_serv=sizeof(dir_serv); resul=recvfrom(s, msj_rec, sizeof(msj_rec),0, (struct sockaddr *) &dir_serv, &long_dir_serv); if (resul == SOCKET_ERROR){ printf("ERROR AL recibir: %d\n",WSAGetLastError()); exit(4); } printf("MENSAJE recibido: %s\n",msj_rec); closesocket(s); WSACleanup( );

} // fin del main

Figura 4: Código del cliente UDP Con todo el código de la figura anterior se puede crear un fichero al que llamar, por ejemplo, clienteUDP.cpp en “Windows Visual Studio”. Obsérvese que lo único que hace el cliente es enviar un mensaje a la dirección IP y puerto del servidor que se le pasen por la consola. La dirección del equipo servidor hay que pasarla como “notación decimal con puntos”. Por ejemplo, una dirección válida sería: 192.168.200.128 Si no se tiene red en el equipo, se puede pasar como dirección del servidor la 127.0.0.1 (que es la dirección local del propio equipo, o también llamada “localhost”) Obsérvese que el socket del cliente no se une de manera explícita (es decir con la función bind()) a ninguna dirección. Es el sistema, al ejecutar sendto(), el que le asignará al cliente una dirección IP (la de la máquina) y un número de puerto (el primero que encuentre libre).

Page 21: Sockets en Windows

21

El servidor #include <winsock2.h> #include <stdio.h> #include <string.h>

void main(){ SOCKET s; struct sockaddr_in dirMiEquipo, dir_cli; int resul, error, long_dir_cli; WSADATA wsa_datos; char msj_env[80]; // datos a enviar char msj_rec[80]; // datos a recibir

error = WSAStartup(MAKEWORD( 2, 2 ), &wsa_datos); if ( error != 0 ) exit(1); // error al iniciar la DLL if ( LOBYTE( wsa_datos.wVersion ) != 2 || BYTE( wsa_datos.wVersion ) != 2 ) { HI WSACleanup( ); exit(1); }

printf("--- SERVIDOR ---\n");

s = socket(PF_INET, SOCK_DGRAM, 0); if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n",WSAGetLastError()); exit(2); }

memset(&dirMiEquipo, 0, sizeof(struct sockaddr_in)); dirMiEquipo.sin_family = AF_INET; dirMiEquipo.sin_addr.s_addr = INADDR_ANY; dirMiEquipo.sin_port = htons(8888); // puerto del servidor

resul=bind(s, (struct sockaddr *) &dirMiEquipo, sizeof(dirMiEquipo)); if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n",WSAGetLastError()); exit(3); }

Page 22: Sockets en Windows

22

long_dir_cli=sizeof(dir_cli); resul=recvfrom(s, msj_rec, sizeof(msj_rec),0, (struct sockaddr *) &dir_cli, &long_dir_cli); if (resul == SOCKET_ERROR){ printf("ERROR AL recibir: %d\n",WSAGetLastError()); exit(4); } printf("MENSAJE recibido: %s\n",msj_rec); strcpy(msj_env,"Hola, soy el servidor"); resul=sendto(s, msj_env, sizeof(msj_env),0, (struct sockaddr *) &dir_cli, sizeof(dir_cli)); if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n",WSAGetLastError()); exit(5); }

closesocket(s); WSACleanup( );

} // fin del main

Figura 5: Código del servidor UDP

Con todo el código de la figura anterior se puede crear un fichero al que llamar, por ejemplo, servidorUDP.cpp en “Windows Visual Studio”. Lo que hace el servidor es unir su socket s a la dirección formada por: la IP de la máquina (INADDR_ANY) y al puerto 8888 (no hay que olvidarse de utilizar la función htons()). Una vez que el servidor recibe el mensaje del cliente, lo escribe en la consola y le responde.

Page 23: Sockets en Windows

23

Capítulo 4. Operaciones para comunicaciones multicast

En el capítulo anterior se analizó la comunicación UDP cuando cada envío de datos (hecho mediante sendto()) sólo tenía un destinatario posible. Esto era así porque como se ha visto la dirección a la que se vinculaban los sockets era única. A este tipo de comunicación se la denomina unicast. En ciertas aplicaciones (como chats, foros, videoconferencias, etc) es necesario (por razones de eficiencia) que un único envío de datos llegue a múltiples destinatarios. Esta forma de comunicarnos se denomina multicast. Obviamente, para que este mecanismo funcione necesitaremos que varios sockets se puedan vincular (explícitamente con la función bind()) a una misma dirección (denominada dirección multicast). Para poder distinguirlas, a las direcciones utilizadas en la comunicación unicast también se las suele denominar como direcciones unicast. Estas direcciones multicast, como con las unicast, la tenemos que ver divididas en el par <dirección IP> y <número de puerto>. Recuérdese, del tema donde se presentaban los conceptos de la arquitectuta TCP/IP, que las direcciones IP multicast eran de clase D (estaban en el rango desde 224.0.0.0 hasta 239.255.255.255). En el caso de los puertos no hay nada especial, siguen siendo números con el mismo significado que en la comunicación unicast. Por lo tanto, en la comunicación multicast podemos tener a múltiples sockets unidos a la misma dirección: <224.10.10.10><6666>. En la API Winsock sólo se pueden utilizar las direcciones de multicast con el protocolo UDP, es decir, con sockets de datagramas. Para ello, además de utilizar las direcciones multicast y las funciones sendto() y recvfrom(), debemos utilizar otras funciones que preparen a las aplicaciones para el envío multicast. Aunque parece obvio, no está de más decir de manera explícita que siempre se puede utilizar una comunicación unicast y hacer sendto() de un mismo mensaje a un grupo de n direcciones unicast. Obviamente, esto supone n envíos del mensaje a cada dirección unicast. Esto es mucho más ineficiente que utilizando una comunicación (y direcciones) multicast, ya que en este último caso sólo se enviará.un único mensaje. 4.1 Función setsockopt

Esta función permite cambiar la configuración del “driver” que implementa un determinado socket. Tales cambios pueden ser: la modificación del buffer donde se almacenan los datos, el protocolo que implementa al socket, la MTU, etc. Vamos a orientar esta explicación a multicast. El prototipo en C de la función es: #include <winsock2.h> #include <ws2tcpip.h> int setsockopt(SOCKET s, int nivel, int opcion, const char *valores, int long_opcion);

Page 24: Sockets en Windows

24

El primer parámetro s indica el socket sobre el que se van a cambiar algunas opciones. En el parámetro nivel señalamos el protocolo al que afectarán dichas modificaciones. El identificador de la opción se incluye en el parámetro opcion, y en el parámetro valores ponemos los datos que queramos modificar de opcion. Por último, long_opcion contiene el tamaño de valores. Esta función setsockopt() devuelve 0 si todo ha ido bien, y SOCKET_ERROR si se ha producido un error. Un ejemplo de utilización de esta función orientado al uso multicast es: #include <winsock2.h> #include <ws2tcpip.h> ... SOCKET s; struct sockaddr_in dirMiEquipo; int resul; struct ip_mreq req_multi; int ttl; ... s = socket(PF_INET, SOCK_DGRAM, 0); if (s == INVALID_SOCKET) exit(1); //error al crear el socket

memset(&dirMiEquipo, 0, sizeof(struct sockaddr_in)); dirMiEquipo.sin_family = AF_INET; dirMiEquipo.sin_addr.s_addr = INADDR_ANY; //IP unicast dirMiEquipo.sin_port = htons(6666); // puerto libre

resul=bind(s, (struct sockaddr*) &dirMiEquipo, sizeof(dirMiEquipo)); if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n",WSAGetLastError()); exit(3); } //asociamos la dir. IP unicast con la multicast req_multi.imr_interface.s_addr =INADDR_ANY; //IP unicast req_multi.imr_multiaddr.s_addr=inet_addr("224.10.20.30"); resul=setsockopt(s, IPPROTO_IP,IP_ADD_MEMBERSHIP, (const char *) & req_multi, sizeof(req_multi)); if (resul == SOCKET_ERROR){ printf("ERROR EN OPCIONES MULTICAST: %d\n",WSAGetLastError()); exit(4); } // ahora se puede recibir datos por <224.10.20.30><6666> ...

Page 25: Sockets en Windows

25

... //preparamos un posible envio multicast ttl=1; //saltos que puede dar el datagrama en multicast resul=setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, (const char *) &ttl, sizeof(ttl)); if (resul == SOCKET_ERROR){ printf("ERROR EN OPCIONES MULTICAST: %d\n",WSAGetLastError()); exit(3); } // ahora se puede enviar datos multicast por <224.10.20.30><6666> ... En el ejemplo vemos que hemos elegido la dirección IP multicast 224.10.20.30 y el puerto 6666 para unir al socket. Seleccionamos como opción para el envío multicast el protocolo IP (IPPROTO_IP), y decimos (IP_ADD_MEMBERSHIP) que la aplicación que ejecuta este código se una a la dirección multicast <224.10.20.30><6666>. Esto último lo que provoca es que el protocolo de multicast (de forma transparente para el programador) envíe datos indicando que le incluyan como uno de los miembros de esa dirección multicast. A partir de ese momento tenemos el equipo preparado para recibir datos (con recvfrom()) por la dirección multicast. Para poder hacerlo vemos que utilizamos la variable req_multi del tipo struct ip_mreq con las siguiente operaciones del ejemplo: req_multi.imr_interface.s_addr =INADDR_ANY; //IP unicast req_multi.imr_multiaddr.s_addr=inet_addr("224.10.20.30"); Con ellas vamos a asociar en el interfaz la dirección unicast del equipo con la multicast. Para poder configurar el socket para enviar datos (con sendto()) a una dirección multicast, seleccionamos como opción para el envío multicast el protocolo IP (IPPROTO_IP), y decimos (IP_MULTICAST_TTL) que la aplicación va a poder enviar por ese socket a la dirección multicast <224.10.20.30><6666>. La variable ttl lo que hace es limitar el rango de equipos que componen los posibles miembros a los que llega un envío multicast. Como sabemos por el tema de la arquitectura TCP/IP, la red Internet está formada por muchas redes IP conectadas entre sí por routers. El valor ttl=1 limita a todos los equipos dentro de la misma red los posibles miembros del multicast. Este ttl=1 es el valor por defecto. Obviamente se puede poner un valor mayor que 1, pero para que tenga efecto debe contar con el permiso de los distintos routers (normalmente este permiso está inhibido para evitar la inundación de Internet por datos no deseados). Para aclararlo más, seguidamente se va a presentar un ejemplo de multicast.

Page 26: Sockets en Windows

26

4.2 Función closesocket

Con muticast esta función, además de realizar las operaciones locales que mencionamos en la sección 3.3, genera el envío de datos a través de la red para informar que el grupo multicast ya no cuenta con ese miembro. Tanto la sintaxis como la utilización de esta función es igual que la ya descrita en la sección 3.3.

Page 27: Sockets en Windows

27

4.2 Esquema cliente/servidor con multicast

En la figura 6 presentamos un posible esquema con las funciones a utilizar para una comunicación con el protocolo UDP en multicast. Se ha supuesto, para hacerlo sencillo, que el cliente hace un envío, y el servidor estará permanentemente esperando recibir datos. Obsérvese que la función recvfrom() es bloqueante, por lo que hasta que no reciba los datos (enviados mediante la función sendto()) la aplicación no pasará a ejecutar ninguna otra instrucción.

WSAStartup( )

socket( )

setsockopt( )

sendto( )

closesocket( )

WSAcleanup( )

WSAStartup( )

socket( )

bind( )

recvfrom( )

closesocket( )

WSAcleanup( )

setsockopt( )

BLOQUEO

DATOS

Servidor Cliente

Figura 6: Una posible comunicación multicast con sockets de datagrama

Page 28: Sockets en Windows

28

4.3 Un ejemplo con multicast

Se va a presentar seguidamente el ejemplo de comunicación con UDP multicast descrito en el esquema del apartado anterior. En este ejemplo el cliente manda un mensaje de saludo, y el servidor lo muestra en la pantalla. Para que se pueda ver el concepto de multicast lo interesante es ejecutar n clientes que manden los mensajes al servidor. Para ello será suficiente con ejecutar n veces el cliente en n ventanas windows, y ejecutar en una ventana de windows el servidor. En “Windows Visual Studio” podemos crear un proyecto con el cliente y el servidor. Con otros compiladores, con tener un fichero con la extensión “.c” será suficiente. El cliente #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <string.h>

void main(){ SOCKET s; struct sockaddr_in dir_serv; int resul, error; int ttl; char msj_env[80]; // datos a enviar WSADATA wsa_datos; error = WSAStartup(MAKEWORD( 2, 2 ), &wsa_datos); if ( error != 0 ) exit(1); // error al iniciar la DLL if ( LOBYTE( wsa_datos.wVersion ) != 2 || IBYTE( wsa_datos.wVersion ) != 2 ) { H WSACleanup( ); exit(1); }

printf("--- CLIENTE MULTICAST ---\n");

s = socket(PF_INET, SOCK_DGRAM, 0); if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n",WSAGetLastError()); exit(2); } ttl=1; //saltos que puede dar el datagrama en multicast resul=setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, (const char *) &ttl, sizeof(ttl)); if (resul == SOCKET_ERROR){ printf("ERROR EN OPCIONES MULTICAST: %d\n",WSAGetLastError()); exit(4); }

Page 29: Sockets en Windows

29

memset(&dir_serv, 0, sizeof(struct sockaddr_in)); dir_serv.sin_family = AF_INET; dir_serv.sin_addr.s_addr = inet_addr("224.10.20.30"); dir_serv.sin_port = htons(6666);

strcpy(msj_env,"Envio multicast desde el cliente"); resul=sendto(s, msj_env, sizeof(msj_env),0, (struct sockaddr *) &dir_serv, sizeof(dir_serv)); if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR EN MULTICAST: %d\n",WSAGetLastError()); exit(5); } closesocket(s); WSACleanup( ); } // fin del main

Figura 7: Código del cliente UDP con multicast Con todo el código de la figura anterior se puede crear un fichero al que se puede llamar, por ejemplo, cliente_multicast.cpp en “Windows Visual Studio”. Obsérvese que lo único que hace el cliente es enviar un mensaje a la dirección multicast: <224.10.20.30> <6666>. Para ello seleccionamos las opciones IPPROTO_IP y la IP_MULTICAST_TTL. El valor ttl=1 es para que el envío multicast no se propague más alla del router que forman todos los equipos de la misma red IP (que es lo permitido por defecto).

Page 30: Sockets en Windows

30

El servidor #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <string.h> void main(){ SOCKET s; struct sockaddr_in dirMiEquipo, dir_cli; int resul, error, long_dir_cli; char msj_rec[80]; // datos a recibir WSADATA wsa_datos; struct ip_mreq req_multi; error = WSAStartup(MAKEWORD( 2, 2 ), &wsa_datos); if ( error != 0 ) exit(1); // error al iniciar la DLL if ( LOBYTE( wsa_datos.wVersion ) != 2 || IBYTE( wsa_datos.wVersion ) != 2 ) { H WSACleanup( ); exit(1); }

printf("--- SERVIDOR MULTICAST ---\n");

s = socket(PF_INET, SOCK_DGRAM, 0); if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n",WSAGetLastError()); exit(2); } memset(&dirMiEquipo, 0, sizeof(struct sockaddr_in)); dirMiEquipo.sin_family = AF_INET; dirMiEquipo.sin_addr.s_addr = INADDR_ANY; //IP multicast dirMiEquipo.sin_port = htons(6666); // puerto libre

resul=bind(s, (struct sockaddr *) &dirMiEquipo, sizeof(dirMiEquipo)); if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n",WSAGetLastError()); exit(3); }

Page 31: Sockets en Windows

31

req_multi.imr_interface.s_addr =INADDR_ANY; req_multi.imr_multiaddr.s_addr=inet_addr("224.10.20.30"); resul=setsockopt(s, IPPROTO_IP,IP_ADD_MEMBERSHIP, (const char *) & req_multi, sizeof(req_multi)); if (resul == SOCKET_ERROR){ printf("ERROR EN OPCIONES MULTICAST: %d\n",WSAGetLastError()); exit(4); }

while(1) { long_dir_cli=sizeof(dir_cli); resul=recvfrom(s, msj_rec, sizeof(msj_rec),0, (struct sockaddr *) &dir_cli, &long_dir_cli); if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR EN MULTICAST: %d\n",WSAGetLastError()); exit(5); } printf("MENSAJE recibido: %s\n",msj_rec); } closesocket(s); WSACleanup( ); } // fin del main

Figura 8: Código del servidor UDP con multicast

Con todo el código de la figura anterior se puede crear un fichero al que llamar, por ejemplo, servidor_multicast.cpp en “Windows Visual Studio”. Obsérvese que lo único que hace el servidor es unirse primero a una dirección unicast (ver el código de la página anterior a ésta). Posteriormente, gracias a: req_multi.imr_interface.s_addr =INADDR_ANY; req_multi.imr_multiaddr.s_addr=inet_addr("224.10.20.30"); Asocia la dirección unicast con la dirección multicast 224.10.20.30.

Page 32: Sockets en Windows

32

Una vez hecha la asociación, el programa lo que hace es esperar de forma indefinida a que le lleguen mensajes Obviamente, para finalizar este servidor hay que teclear en algún momento las teclas <ctrl.>C. Como podrá observarse, las funciones closesocket() y WSACleanup() no se van a ejecutar, por lo que no hace falta que se incluyan. Si lo hacemos es por seguir la “metodología” de siempre. Capítulo 5. Operaciones para comunicaciones con TCP

En este capítulo se van a presentar todas las funciones que van a necesitarse para poder comunicar un cliente y un servidor utilizando sockets de flujo, es decir, utilizando el protocolo TCP. El protocolo TCP es un protocolo orientado a la conexión. Una conexión se establece entre sólo un cliente y un servidor, por tanto, sólo se pueden utilizar direcciones unicast. Por supuesto que siempre se pueden establecer n conexiones TCP entre un cliente y un servidor, donde el servidor puede ser el mismo. No es multicast porque serán n conexiones diferentes, no una única conexión. En muchos casos las funciones presentadas en este capítulo podrán ser invocadas tanto por el servidor como por el cliente. En algunos casos no es así, y será indicado de forma explícita al explicar su funcionamiento. 5.1 Función connect

Esta es una función en principio pensada para ser utilizada sólo por los clientes, no por los servidores. Por ser un protocolo orientado a la conexión el que implementa connect(), antes de poder mandar información con las funciones de enviar y recibir debe establecerse la conexión. Es connect() la llamada empleada por el cliente para solicitar el establecimiento de una conexión TCP con un servidor. Esta solicitud de establecimiento se plasmará en el envío de un segmento TCP solicitando la conexión (ver tema de la arquitectura TCP/IP). Recuérdese que para que un socket fuera útil se necesitaba que estuviera asociado con una determinada dirección. Como ya hemos visto, esto se realiza de forma explícita con la función bind(). En caso de implementar un cliente que no utilice esa función, connect() tiene también el efecto de unir implícitamente el socket a una determinada dirección. Lo que hace connect es unirlo al par formado por la dirección IP de la máquina y el primer puerto que encuentre libre. Como TCP es orientado a la conexión, connect() debe esperar a que el servidor responda con un segmento TCP de confirmación. Una vez recibido, connect() también vincula el socket con la dirección del otro extremo de la comunicación. Por tanto, una vez ejecutada la función connect(), el socket tiene por defecto asociado la dirección origen y destino desde donde enviar o recibir los datos. Una observación importante es que la función connect() fue diseñada para ser utilizada con sockets de flujo (es decir, con TCP). No obstante, debido a este efecto que tiene de unir el socket de forma implícita a un par de direcciones (una la del cliente y otra la del servidor), muchos programadores utilizan esta función con sockets de datagrama (es decir, con UDP). En este caso, hará la misma asociación implícita de un par de direcciones, pero obviamente no generará una solicitud de conexión, ya que el protocolo UDP es no orientado a la conexión (ver tema de la arquitectura TCP/IP).

Page 33: Sockets en Windows

33

Se ha dicho que connect() es una función en principio pensada para ser utilizada sólo por los clientes, no por los servidores. Esto es siempre así con los sockets de flujo porque el sistema genera un segmento TCP distinto para el cliente que sólicita una conexión que para el servidor que tiene que aceptarla, y por tanto Winsock utiliza funciones distintas (como se verá para el servidor la función es accept()). En el caso de los sockets de datagrama ya no es así, al no generar el protocolo UDP ningún intercambio de unidades para establecer la conexión. Por tanto, lo único que utiliza el programador es el efecto local que hace que el socket se vincule tanto a su dirección como a la dirección destino. Por tanto, con sockets de datagrama la función connect() puede ser invocada tanto por el cliente como por el servidor. El prototipo en C de la función es:

#include <winsock2.h> int connect(SOCKET s, const struct sockaddr *dirDestino, int long_dirDestino); Esta función asocia al socket s con la dirección destino apuntada por dirDestino. En caso de sockets de flujo (SOCK_STREAM), genera un establecimiento de conexión con dirDestino. El parámetro dirDestino es un puntero a la estructura sockaddr, donde deberá ponerse la dirección del socket de la aplicación donde se quieren enviar los datos. Podemos utilizar también la estructura sockaddr_in, pero haciendo casting con sockaddr para evitar “warnings” del compilador. El parámetro long_dirDestino indica el tamaño de la estructura apuntada por dirDestino. Esta función devuelve 0 si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo. En la sección 5.7 se presenta un ejemplo donde se utilizará connect().

Page 34: Sockets en Windows

34

5.2 Función listen

Esta función prepara a un socket para recibir solicitudes de conexión (que se realizarán mediante connect()). Por tanto, esta función debe ser invocada únicamente por los servidores (nunca por un cliente). Cuando el servidor esté ya conectado con un cliente (con la función accept() que se verá más adelante) y esté ejecutando otras instrucciones, puede ser que otros clientes realicen también solicitudes de conexión. Para que no se pierdan y el sistema las guarde hasta que el servidor pueda tratarlas, la función listen() también proporciona la posibilidad de crear una cola. El prototipo en C de la función es:

#include <winsock2.h> int listen(SOCKET s, int long_peticiones);

El primer parámetro indica que el socket s debe ponerse en modo “pasivo”, es decir, a la espera de recibir peticiones de conexión. El segundo parámetro long_peticiones indica el número máximo de peticiones que debe encolar a la espera que el servidor pueda tratarlas. Esta función devuelve 0 si todo ha ido bien, y SOCKET_ERROR si se produce un fallo. Cuando expliquemos accept() también se presentará un ejemplo de uso de esta función listen(). 5.3 Función accept

Una vez preparado el socket para recibir solicitudes de conexión (después de ejecutar listen()), esta función acepta la conexión con el cliente (bien porque está encolada, o bien porque espera hasta que llega una solicitud hecha con connect() por un cliente). Esta llamada provoca por parte de TCP el envío de un segmento de confirmación de la conexión. Esta función accept() sólo puede ser invocada con sockets de flujo y por los servidores (nunca por un cliente). La función accept(), una vez establecida la conexión TCP, devuelve un nuevo socket que identificará la conexión entre el cliente y el servidor, es decir, ese nuevo socket estará asociado al par de direcciones formado por la dirección del cliente y la del servidor. El prototipo en C de la función es:

Page 35: Sockets en Windows

35

#include <winsock2.h> SOCKET accept(SOCKET s, struct sockaddr *dirCliente, int *long_dirCliente); El primer parámetro s indica el socket que está en modo “pasivo” a la espera de que los clientes le hagan connect() a su dirección. Una vez recibida una petición, el sistema nos devuelve un nuevo socket que será el resultado de la conexión entre un cliente y el servidor. Por tanto, el nuevo socket creado estará vinculado tanto a la dirección del cliente aceptado como a una dirección del servidor. Al finalizar correctamente la ejecución del accept(), el sistema indica en el parámetro dirCliente el puntero a la dirección del cliente al que se ha conectado. El tercer parámetro es un puntero al tamaño de dirCliente. Es importante resaltar que el programador debe poner antes de invocar a accept() este valor apuntando al tamaño esperado de dirCliente (que es la estructura sockaddr o sockaddr_in). En el caso de que la conexión no se haya podido realizar, la función devuelve INVALID_SOCKET. Un ejemplo de utilización de esta función accept() es: #include <winsock2.h> ... SOCKET s_serv; SOCKET s_con; struct sockaddr_in dirMiEquipo, dir_cli; int resul, long_dir_cli; ... //s_serv recibe las peticiones de conexion de los clientes s_serv = socket(PF_INET, SOCK_STREAM, 0); if (s_serv == INVALID_SOCKET)exit(1); memset(&dirMiEquipo, 0, sizeof(struct sockaddr_in)); dirMiEquipo.sin_family = AF_INET; dirMiEquipo.sin_addr.s_addr = INADDR_ANY; dirMiEquipo.sin_port = htons(8989);

resul=bind(s_serv, (struct sockaddr *) &dirMiEquipo, sizeof(dirMiEquipo)); if (resul == SOCKET_ERROR) exit(2);

Page 36: Sockets en Windows

36

// prepara s_serv para aceptar conexiones listen(s_serv,5); while(1) { //acepta una conexion a la dirección de s_serv long_dir_cli=sizeof(dir_cli); s_con=accept(s_serv, (struct sockaddr *) &dir_cli, &long_dir_cli); if (s_con == INVALID_SOCKET){ printf("ERROR AL ACEPTAR CONEXION: %d\n",WSAGetLastError()); exit(3); } // s_con es el socket creado para la conexión // que se acaba de establecer ... send(s_con, ... ); // el envío se hace con s_con ... recv(s_con, ... ); // se recibe por s_con ... closesocket(s_con); //al finalizar, se cierra s_con } ... En el ejemplo se puede ver que s_serv es el socket para que los clientes soliciten la conexión, mientras que s_con es el socket para trabajar con una conexión en concreto. El ejemplo reproduce un esquema habitual en el cual los servidores están permanentemente aceptando conexiones de clientes. A partir de la sección 5.8 se presentan ejemplos completos de servidores con TCP.

Page 37: Sockets en Windows

37

5.4 Función send

Esta función permite enviar datos a través de un socket. Es similar a sendto(). La única diferencia es que si se ha utilizado previamente la función connect() (o la función accept()), el socket ya sabe a qué dirección queremos enviar los datos, y por lo tanto no necesitamos ningún parámetro que lo indique. Al igual que pasaba con connect(), esta función fue diseñada originalmente para utilizarse con sockets de flujo. No obstante, puede utilizarse también con sockets de datagrama si previamente se ha utlizado la función connect(). El prototipo en C de la función es:

#include <winsock2.h> int send(SOCKET s, const char *msj, int long_msj, int flags); Esta function envía el array de datos contenido en el parámetro msj por el socket s. El parámetro long_msj indica el tamaño del parámetro anterior. El parámetro flags permite enviar datos con distintas opciones (“fuera de banda”, “adelantados”, etc). Un envío normal de datos se consigue poniendo en este campo flags un 0. Esta función devuelve el número de bytes enviados por la red si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo al enviar. En la secciones 5.7 y 5.8 se presentan ejemplos donde se utilizará send(). 5.5 Función recv

Esta función permite recibir datos a través de un socket. Es similar a recvfrom(). La única diferencia es que si se ha utilizado previamente la función connect() (o la función accept()), el socket ya sabe la dirección desde la cual queremos recibir los datos, y por lo tanto no necesitamos ningún parámetro que lo indique. Al igual que pasa con connect() y send(), esta función fue diseñada originalmente para utilizarse con sockets de flujo. No obstante, puede utilizarse también con sockets de datagrama si previamente se ha utlizado la función connect(). El prototipo en C de la función es: #include <winsock2.h> int recv(SOCKET s, const char *msj, int long_msj, int flags);

Page 38: Sockets en Windows

38

Esta function recibe para el socket s una serie de datos que almacena en el array del parámetro msj. El parámetro long_msj indica el tamaño del parámetro anterior. El parámetro flags permite, al igual que en el caso de sendto, recibir datos con distintas opciones (“fuera de banda”, “adelantados”, etc). Una recepción normal de datos se consigue poniendo en el campo flags un 0. Esta función recv() devuelve el número de bytes recibidos si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo. Es muy importante destacar que la función recv() también puede devolver 0 como número de bytes recibidos por el socket de flujo s. En este caso lo que quiere decir es que la aplicación remota ha cerrado la conexión. Este valor 0 se suele utilizar al implementar muchas aplicaciones para indicar que la aplicación remota ya ha enviado todo lo que tenía y que no hay por qué esperar a recibir más datos de ella. Es también muy importante resaltar que en los sockets de flujo un envío de n datos con un send() no tiene por qué corresponderse con una única recepción de n datos. Esto es debido a que, a diferencia de UDP, el protocolo TCP puede generar segmentos de datos de un tamaño distinto de los datos volcados por una función send(). Esto es así para poder optimizar el tamaño de la ventana de TCP (ver el tema de la arquitectura TCP). Por tanto, esto se traduce para el programador en que un envío de n datos con un send() se puede traducir en recibir n veces 1 byte, o en recibir 2 veces n/2 bytes (o cualquier otra combinación). En la secciones 5.7 y 5.8 se presentan ejemplos donde se utilizará recv(). 5.6 Funciones closesocket y shutdown

Con sockets de flujo la función closesocket(), además de realizar las operaciones locales que mencionamos en la sección 3.3, provoca la liberación de la conexión. Para ello se intercambiarán (de forma transparente para el programador) los segmentos TCP que solicitan y confirman el cierre de la conexión en ambos sentidos (ver el tema de la arquitectura TCP/IP). Un aspecto a tener en cuenta es que cuando Winsock devuelve el control de closesocket() la liberación a nivel TCP no está del todo finalizada (la otra parte puede estar todavía mandando su segmento TCP de liberación) Por tanto, si un cliente inmediatamente intenta volver a conectar puede tener problemas (no obstante este tiempo es muy pequeño, está en 1 ó 2 segundos). Tanto la sintaxis como la utilización de esta función es igual que la ya descrita en la sección 3.3. Como se ha visto closesocket() cierra la conexión. A veces se quiere tener un mayor control y poder cerrar sólo un extremo de la conexión, para ello tenemos la función shutdown(). El prototipo en C de la función es:

Page 39: Sockets en Windows

39

#include <winsock2.h> int shutdown(SOCKET s, int tipo_cierre); El primer parámetro indica que el cierre de la conexión se realiza sobre el socket de flujo s. El significado del parámetro tipo_cierre depende de los valores: • 0. La aplicación remota ya no puede enviar más datos a la aplicación local (es decir, a la

que invoca a esta función). • 1. La aplicación local no puede enviar más datos. • 2. Ni la aplicación local ni la remota pueden enviar más datos (es equivalente

closesocket()). Esta función devuelve 0 si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo. 5.7 Cliente con TCP.

Los clientes en TCP suelen ser mucho más sencillos que los servidores. Por tanto vamos a dividir la explicación en diferentes secciones. Para intentar reducir código y aclarar los conceptos, vamos a presentar un ejemplo simple de cliente TCP que nos sirva para ser empleado con todos los tipos de servidores que explicaremos posteriormente. El funcionamiento siempre será el mismo: el cliente envía un único mensaje, y el servidor cuando reciba todo el mensaje le responde al servidor enviando otro mensaje de respuesta. 5.7.1 Un ejemplo de cliente con TCP.

El cliente enviará un mensaje (de 24 bytes o caracteres), esperando que el servidor le conteste con cierta información (de la que no conoce su tamaño, aunque sí sabe que es menor que 80 caracteres). En “Windows Visual Studio” podemos crear un proyecto con el cliente y el servidor. Con otros compiladores, con tener un fichero con la extensión “.c” será suficiente.

Page 40: Sockets en Windows

40

#include <winsock2.h> #include <stdio.h> #include <string.h>

void main(){ SOCKET s; struct sockaddr_in dir_serv; int resul, puerto_serv, error; WSADATA wsa_datos; char cadena_dir_ip_serv[20]; // cadena con la ip del servidor char msj_env[80]; // datos a enviar char msj_rec[80]; // datos a recibir char msj[80]; // variable auxiliar para escribir lo recibido

printf("--- CLIENTE TCP ---\n"); printf("Direccion IP del servidor TCP="); scanf("%s",&cadena_dir_ip_serv); printf("Puerto del servidor TCP="); scanf("%d",&puerto_serv);

error = WSAStartup(MAKEWORD( 2, 2 ), &wsa_datos); if ( error != 0 ) exit(1); // error al iniciar la DLL if ( LOBYTE( wsa_datos.wVersion ) != 2 || HIBYTE( wsa_datos.wVersion ) != 2 ) { WSACleanup( ); exit(1); } s = socket(PF_INET, SOCK_STREAM, 0); if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n",WSAGetLastError()); exit(2); }

memset(&dir_serv, 0, sizeof(struct sockaddr_in)); dir_serv.sin_family = AF_INET; dir_serv.sin_addr.s_addr = inet_addr(cadena_dir_ip_serv); dir_serv.sin_port = htons(puerto_serv);

resul=connect(s, (struct sockaddr *) &dir_serv, sizeof(dir_serv)); if (resul == SOCKET_ERROR){ printf("ERROR AL CONECTAR: %d\n",WSAGetLastError()); exit(3); }

strcpy(msj_env,"Dame toda la informacion"); // mensaje de 24 bytes

Page 41: Sockets en Windows

41

resul=send(s, msj_env, sizeof(msj_env),0); if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n",WSAGetLastError()); exit(4); } printf("MENSAJE recibido: "); do{ resul=recv(s, msj_rec, sizeof(msj_rec),0); if (resul == SOCKET_ERROR){ printf("ERROR AL recibir: %d\n",WSAGetLastError()); exit(4); } strncpy(msj, msj_rec,resul); printf("%s",msj); // escribe el mensaje recibido strcpy(msj,""); //limpia msj }while (resul!=0); // espera a que el servidor libere la conex. printf("\n FIN de la conexion \n"); closesocket(s); WSACleanup( );

} // fin del main

Figura 9: Código del cliente TCP

Con todo el código de la figura se puede crear un fichero al que llamar, por ejemplo, cliente_TCP.cpp en “Windows Visual Studio”. Aunque nos estamos adelantando a la presentación del servidor, el código del cliente es muy fácil de comprender. Lo único que puede sorprender es el bucle do-while para leer lo que el servidor nos envíe. Hay que recordar lo explicado para la función recv() en TCP: aunque el servidor utilice un solo send(), la información puede ir en varios segmentos de TCP, de forma que eso se puede traducir siempre en tener que hacer varios recv(). El lector un poco experimentado puede advertir que en muchos ejemplos que se pueden encontrar en la literatura no se hace como aquí, si no que si una aplicación hace un único send(), la otra hace un único recv(). Esto es así porque la mayoría de las implementaciones de sockets intentan respetar que lo indicado en el send() vaya en un único segmento TCP. Pero lo importante es saber que nunca se pueden tener garantías de que eso vaya a ser así.

Page 42: Sockets en Windows

42

5.8 Servidor iterativo con TCP

Un posible diseño del servidor es aquel en el cual las peticiones de conexión se atienden unas detrás de otras. Es decir, hasta que no se acaban de ejecutar todas las instrucciones involucradas en una conexión, el servidor no acepta otra nueva conexión. Esto es lo que se llama un servidor iterativo. Seguidamente vamos a presentar como sería esa comunicación. 5.8.1 Esquema cliente/servidor con servidor iterativo con TCP

WSAStartup( )

socket( )

connect( )

send( )

closesocket( )

WSAcleanup( )

Cliente WSAStartup( )

socket( )

bind( )

recv( )

closesocket( )

WSAcleanup( )

Servidor

listen( )

accept( )

DATOS (petición)

recv( ) send( )

DATOS (respuesta)

Page 43: Sockets en Windows

43

5.8.2 Un ejemplo con servidor iterativo con TCP #include <winsock2.h> #include <stdio.h> #include <string.h> void procesa_conexion(SOCKET s); // atiende la conexión con el cliente void main(){ SOCKET s_serv; SOCKET s_con; struct sockaddr_in dirMiEquipo, dir_cli; int resul, long_dir_cli, error;

WSADATA wsa_datos;

printf("--- SERVIDOR TCP ---\n"); error = WSAStartup(MAKEWORD( 2, 2 ), &wsa_datos); if ( error != 0 ) exit(1); // error al iniciar la DLL if ( LOBYTE( wsa_datos.wVersion ) != 2 || BYTE( wsa_datos.wVersion ) != 2 ) { HI WSACleanup( ); exit(1); } //s_serv recibe las peticiones de conexion de los clientes s_serv = socket(PF_INET, SOCK_STREAM, 0); if (s_serv == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n",WSAGetLastError()); exit(2); } memset(&dirMiEquipo, 0, sizeof(struct sockaddr_in)); dirMiEquipo.sin_family = AF_INET; dirMiEquipo.sin_addr.s_addr = INADDR_ANY; dirMiEquipo.sin_port = htons(8989);

resul=bind(s_serv, (struct sockaddr *) &dirMiEquipo, sizeof(dirMiEquipo)); if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n",WSAGetLastError()); exit(3); }

Page 44: Sockets en Windows

44

// prepara s_serv para aceptar conexiones listen(s_serv,5); while(1) { //acepta una conexion a la dirección de s_serv long_dir_cli=sizeof(dir_cli); s_con=accept(s_serv, (struct sockaddr *) &dir_cli,&long_dir_cli); if (s_con == INVALID_SOCKET){ printf("ERROR AL ACEPTAR CONEXION: %d\n",WSAGetLastError()); exit(4); } printf("--- CONEXION ACEPTADA ---\n"); procesa_conexion(s_con); //realiza la conexión aceptada } closesocket(s_serv); // cierra s_serv WSACleanup( );

} // fin del main

Page 45: Sockets en Windows

45

// funcion para tratar la connexion con un cliente void procesa_conexion(SOCKET s_con){ int cont, resul; char msj_env[80]; // datos a enviar char msj_rec[80]; // datos a recibir char msj[80]; // variable auxiliar para escribir lo recibido

printf("MENSAJE recibido: "); cont=0; do{ resul=recv(s_con, msj_rec, sizeof(msj_rec),0); if (resul == SOCKET_ERROR){ printf("ERROR AL RECIBIR: %d\n",WSAGetLastError()); exit(5); } cont=cont+resul; strncpy(msj, msj_rec,resul); printf("%s",msj); // escribe el mensaje }while (cont<24); // espera envio completo printf("\n procesando la peticion, espere ... \n"); // simulacion de procesamiento en el servidor Sleep(10000); // se detiene durante 10 segundos strcpy(msj_env,"Respuesta del servidor"); resul=send(s_con, msj_env, sizeof(msj_env),0); if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n",WSAGetLastError()); exit(4); } closesocket(s_con); //al finalizar, se cierra s_con printf("\n FIN de la conexion \n"); }

Figura 10: Código del servidor TCP iterativo Con todo el código de la figura se puede crear un fichero al que llamar, por ejemplo, servidor_TCP_iterativo.cpp en “Windows Visual Studio”. En el ejemplo se puede apreciar que existe la función procesa_conexion() que es la que se encarga, una vez aceptada por el servidor, de atender al cliente. En ella se puede observar que el servidor recibe la petición del cliente (de 24 bytes), y se responde con un send(). Si la

Page 46: Sockets en Windows

46

función sólo hiciera eso el tiempo de ejecución de la conexión sería muy pequeño. Como se verá en la siguiente sección, el mayor o menor tiempo de ejecución de las conexiones también influirá en el diseño de los servidores. En vez de complicar la tarea a realizar por el servidor en cada conexión, lo que se hace es utlilizar la función Sleep(), que detiene la ejecución del servidor el tiempo que se le indique (en nuestro ejemplo es 10000 milisegundos, es decir, 10 segundos). De esta forma, variando únicamente el parámetro de Sleep() se consigue adaptar el tiempo de respuesta del servidor ante una conexión. Como se puede ver, el servidor del ejemplo estará permanentemente aceptando conexiones porque está dentro de un bucle infinito. En este caso nos podemos preguntar para que sirven las funciones closesocket() y WSACleanup( ), ya que no se van a ejecutar nunca. Efectivamente podrían no ponerse. Si lo hacemos es por seguir la metodología de siempre, aunque como ya se ha indicado, no serían necesarias. Por último, volver a señalar que la existencia del bucle do-while, pese a que el cliente sólo hizo un send(), es porque pueden llegar varios recv().

5.9 Servidor concurrente con hilos con TCP

Ciertas tareas (como el acceso masivo a bases de datos de gran tamaño) requieren que la conexión entre el cliente y el servidor requiera mucho tiempo para realizarse por completo. En estos casos la atención iterativa de las peticiones de conexión no suele resultar eficiente. Para mejorar el rendimiento, lo que se propone es que de forma concurrente (es decir simultáneamente) se puedan atender a más de una petición. Para realizar esta concurrencia se suelen utilizar hilos para que cada uno de ellos, de forma independiente, trate una conexión con cada cliente que lo pida. Aunque no es el tema que queremos estudiar, lo primero que se va a hacer es introducir de forma muy sencilla cómo poder crear hilos en windows para poder tener concurrencia. 5.9.1 Función _beginthread

Esta función es una llamada al sistema operativo de Windows y permite crear hilos. Una vez invocada, _beginthread() pasa el control al sistema operativo para crear un nuevo hilo (al que se le suele llamar “hilo hijo”) y hace que tanto el hilo que hizo la llamada a esta función (al que se le suele llamar “hilo padre”) y el hilo hijo continúen ejecutándose concurrentemente y de forma independiente. El hilo padre continúa ejecutándose en la siguiente instrucción después de _beginthread(), mientras que el hilo hijo continúa ejecutándose en la función que se le pasa con uno de los parámetros a _beginthread(). El prototipo en C de la función es: #include <process.h> unsigned long _beginthread( (void (*)(void *)) funcion_hijo, unsigned long long_pila, void * argumento_funcion);

Page 47: Sockets en Windows

47

El primer parámetro funcion_hijo indica el nombre de la función que el hilo hijo debe ejecutar al ser creado por el sistema operativo. Esta función debe ser declarada y definida como cualquier otra función de C. El segundo tamaño long_pila indica al sistema el tamaño que debe reservar en memoria para la creación del hilo hijo. Cuando no se sabe a priori, lo mejor es poner un 0 (que hace que el sistema lo cree del mismo tamaño que el hilo padre). El tercer parámetro argumento_funcion permite pasar una variable desde el hilo padre a la función funcion_hijo cuando el sistema operativo crea al hilo hijo. La función _beginthread() devuelve un identificador del hilo hijo si todo ha ido bien, y un -1 en caso de error en la creación del hilo hijo. 5.9.2 Esquema cliente/servidor con servidor concurrente con TCP Ahora vamos a presentar el esquema en dos partes: por un lado presentamos la estructura que va a tener el servidor en cuanto a la creación de hilos que atiendan las peticiones de los clientes concurrentemente, y por otro lado, el esquema de un cliente con uno de los hilos que le va a atender en la conexión.

s_con=accept(s_serv)

_beginthread(. . . , . . . , s_con)

socket(s_serv, …)

Hilo padre

Hilo hijo

… recv(s_con, …) … send(s_con, …)

. . . Hilo hijo

… recv(s_con, …) … send(s_con, …) … …

bind(s_serv, …) listen(serv, …)

cada invocación crea un hilo hijo

Figura 11: Esquema de creación de hilos en el servidor

Page 48: Sockets en Windows

48

WSAStartup( )

socket( )

connect( )

send( )

closesocket( )

WSAcleanup( )

Cliente WSAStartup( )

socket(s_serv )

bind(s_serv )

recv(s_con )

closesocket(s_con )

WSAcleanup( )

Servidor

listen(s_serv )

DATOS (petición)

recv( )

s_con=accept(s_serv )

send(s_con) )

DATOS (respuesta)

closesocket(s_serv )

Hilo padre

Hilo padre

Hilo hijo

Figura 12: Esquema de comunicación entre un cliente y un hilo del servidor

Page 49: Sockets en Windows

49

5.9.3 Un ejemplo con servidor concurrente con TCP #include <winsock2.h> #include <stdio.h> #include <string.h> #include <process.h> void procesa_conexion(SOCKET s); // atiende la conexión con el cliente void main(){ SOCKET s_serv; SOCKET s_con; struct sockaddr_in dirMiEquipo, dir_cli; int resul, long_dir_cli, error;

WSADATA wsa_datos;

printf("--- SERVIDOR TCP ---\n"); error = WSAStartup(MAKEWORD( 2, 2 ), &wsa_datos); if ( error != 0 ) exit(1); // error al iniciar la DLL if ( LOBYTE( wsa_datos.wVersion ) != 2 || BYTE( wsa_datos.wVersion ) != 2 ) { HI WSACleanup( ); exit(1); } //s_serv recibe las peticiones de conexion de los clientes s_serv = socket(PF_INET, SOCK_STREAM, 0); if (s_serv == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n",WSAGetLastError()); exit(2); } memset(&dirMiEquipo, 0, sizeof(struct sockaddr_in)); dirMiEquipo.sin_family = AF_INET; dirMiEquipo.sin_addr.s_addr = INADDR_ANY; dirMiEquipo.sin_port = htons(8989);

resul=bind(s_serv, (struct sockaddr *) &dirMiEquipo, sizeof(dirMiEquipo)); if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n",WSAGetLastError()); exit(3); }

Page 50: Sockets en Windows

50

// prepara s_serv para aceptar conexiones listen(s_serv,5); while(1) { //acepta una conexion a la dirección de s_serv long_dir_cli=sizeof(dir_cli); s_con=accept(s_serv, (struct sockaddr *) &dir_cli,&long_dir_cli); if (s_con == INVALID_SOCKET){ printf("ERROR AL ACEPTAR CONEXION: %d\n",WSAGetLastError()); exit(4); } printf("--- CONEXION ACEPTADA ---\n"); // crea un hilo para atender la conexion aceptada resul=_beginthread( (void (*)(void *)) procesa_conexion,0, (void *)s_con); if(resul<0) { printf("ERROR AL CREAR UN HILO: %d\n",WSAGetLastError()); exit(5); } } closesocket(s_serv); // cierra s_serv WSACleanup( );

} // fin del main

Page 51: Sockets en Windows

51

// funcion para tratar la connexion con un cliente void procesa_conexion(SOCKET s_con){ int cont, resul; char msj_env[80]; // datos a enviar char msj_rec[80]; // datos a recibir char msj[80]; // variable auxiliar para escribir lo recibido

printf("MENSAJE recibido: "); cont=0; do{ resul=recv(s_con, msj_rec, sizeof(msj_rec),0); if (resul == SOCKET_ERROR){ printf("ERROR AL RECIBIR: %d\n",WSAGetLastError()); exit(5); } cont=cont+resul; strncpy(msj, msj_rec,resul); printf("%s",msj); // escribe el mensaje }while (cont<24); // espera envio completo printf("\n procesando la peticion, espere ... \n"); // simulacion de procesamiento en el servidor Sleep(30000); // se detiene durante 30 segundos strcpy(msj_env,"Respuesta del servidor"); resul=send(s_con, msj_env, sizeof(msj_env),0); if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n",WSAGetLastError()); exit(4); } closesocket(s_con); //al finalizar, se cierra s_con printf("\n FIN de la conexion \n"); }

Figura 13: Código del servidor TCP concurrente con hilos

Page 52: Sockets en Windows

52

Con todo el código de la figura se puede crear un fichero al que llamar, por ejemplo, servidor_TCP_concurrente.cpp en “Windows Visual Studio”. En el ejemplo de la figura anterior lo único que se ha añadido con respecto al servidor iterativo es la función _beginthread() para crear hilos que traten cada conexión. Para simular que el tiempo de respuesta del servidor es más elevado que en el caso del servidor iterativo, se ha cambiado el valor del parámetro de la función Sleep() a 30 segundos. Para ver los efectos de trabajar con un servidor concurrente frente a hacerlo con otro iterativo, deben ejecutarse a la vez más de un cliente. Entonces podremos comprobar que si 3 clientes de forma simultánea establecieran una conexión, con el servidor iterativo el tiempo de finalización de procesar las 3 peticiones sería de 90 segundos (30+30+30 segundos), frente al concurrente que sólo sería de 30 segundos.