Programación con sockets para Windows

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

2

3

4

5

1

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)

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

cliente

servidor

PROCESO CLIENTE

PROCESO 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

2

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.

Cliente
… SOCKET s_cli; … send(s_cli, …) s cli <199.33.22.12, 3333>

Servidor
… SOCKET s_serv; … recv(s_serv, …) s serv <130.40.50.10, 80> Nivel Transporte Nivel Aplicación

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

Nivel Red

3

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.

4

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); 5

.h> . //error en version DLL } . 6 . Por último. y se añade "ws2_32. Esto es así porque.wVersion ) != 2 ) { WSACleanup( ). Un ejemplo de utilización de la función WSAStartup() es: #include <winsock2. Se puede poner la versión utilizando la macro MAKEWORD. int error. .. Con Visual Studio esto debe hacerse desde el propio proyecto (en menú Proyecto -> Propiedades -> Vinculador -> Entrada.lib" ).wVersion ) != 2 || HIBYTE( wsa_datos. &wsa_datos). una descripción y el estado actual de la misma. exit(2).dll)... WSADATA wsa_datos. Para una explicación más detallada. como veremos más adelante. muchos servidores no pueden invocan a esta función al tener que ejecutarse en un bucle permanente. ya podremos usar el resto de las funciones de sockets. que recibirá información sobre la implementación de Winsock que tengamos en nuestro ordenador: su número de versión. // error al iniciar la DLL if ( LOBYTE( wsa_datos. error = WSAStartup(MAKEWORD( 2. Para compilar hay que decirle al compilador que enlace la biblioteca Winsock (ws2_32.2). si se nos olvida utilizarla. El segundo parámetro es un puntero a una estructura de tipo WSADATA. Si la llamada tiene éxito. if ( error != 0 ) exit(1). etc..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. No obstante. 2 ). el sistema descarga la correspondiente DLL de forma automática al finalizar la ejecución de cualquier programa. 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.. ver el tema de herramientas gráficas.

Hoy en día en la práctica totalidad de los casos se utiliza la arquitectura TCP/IP (o también llamada internet). el parámetro proto establece el protocolo que se usará en este socket dentro de la familia de protocolos familia_protos. 7 .. obtendremos un valor de tipo SOCKET. la función devolverá la constante INVALID_SOCKET. Por último. Obviamente.. El parámetro familia_protos especifica la familia de protocolos que usaremos. s = socket(PF_INET. int tipo.. exit(1).h> #include <stdio. lo primero que tiene que hacer es crear un socket al cual pueda dirigirse.. El prototipo en C de la función es: #include <winsock2. y SOCK_DGRAM para crear el socket de datagramas. la constante empleada será PF_INET. 0). Un ejemplo de utilización de la función socket() es: #include <winsock2. En caso de error. si usa TCP o UDP. tipo indica si el tipo de socket que vamos a crear es de flujo o de datagramas.h> . if (s = = INVALID_SOCKET) { printf("ERROR AL CREAR EL SOCKET: %d\n".h> SOCKET socket(int familia_protos. Con el valor cero el protocolo es asignado automáticamente por Winsock. En el caso de internet que es el que nos concierne en este tema. para que una aplicación pueda realizar operaciones de E/S en red para comunicarse con otra aplicación remota.2 Función socket Una vez inicializada la DLL.2. 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. Se emplea el valor SOCK_STREAM para crear un socket de flujo. es decir. 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. int proto). } . SOCKET s. esto es necesario tanto en el cliente como en servidor. Cualquier valor distinto de cero da la posibilidad de utilizar otros protocolos que no sean el estándar. . Si todo va bien.WSAGetLastError()).. Por su parte. SOCK_DGRAM..

sin_addr. { sin_family. struct sockaddr { u_short sa_family. Pensada para Internet existe la siguiente estructura de datos: struct sockaddr_in short u_short struct in_addr char }.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. En el caso de la arquitectura TCP/IP es el valor de la constante AF_INET el que hay que utilizar. y un número de puerto no ocupado ya por otro socket. 2. 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. La estructura in_addr tiene la siguiente estructura: 8 .Nótese en el ejemplo que en caso de error al crear el socket se emplea la función WSAGetLastError(). Debido a esta generalidad no es muy usada. 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). char sa_data[14]. El campo sin_family indica la familia o formato de direcciones. }. La primera es una estructura genérica pensada para poder trabajar con múltiples arquitecturas de protocolos. sin_port. Para hacer esto el lenguaje C proporciona una serie de estructuras de datos. Esta función se puede utilizar siempre que se produzca un error al invocar cualquier función del API Winsock. sin_zero[8].

Para ello contamos con las siguientes funciones: 9 . Posteriormente se van a presentar múltiples ejemplos de uso de esta estructura sockaddr_in. Los campos sin_addr.S_addr va a ser utilizado.} u_long S_addr. No obstante.S_addr Normalmente sólo la definición s_addr del campo S_un. como veremos más adelante. Para asegurarnos de ello.struct in_addr{ union { struct {u_char s_b1. Recuérdese que con estas funciones se garantiza el orden que deben tener los datos en los campos sin_addr. 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. }.s_addr y sin_port de la estructura sockaddr_in deben tener almacenados sus valores en el formato ”network byte order". struct { u_short s_w1. Tanto la estructura sockaddr como sockaddr_in se encuentran declaradas en <winsock2. S_un_w.} S_un_b. }S_un.s_addr y sin_port de la estructura sockaddr_in. 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().h>. tenemos datos en los campos sin_addr. en el campo sin_zero de la estructura sockaddr_in debe encontrarse con todos sus campos a cero. El problema es que los ordenadores almacenan los datos en el formato “host byte order”.s_b4.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”). s_b2. No hay que hacer nada más. • Para el almacenamiento de una dirección IP (que tiene 32 bits) pasándola del “host byte order” al “network byte order”: htonl(). Para evitar esta posible disparidad. En algunas ocasiones nos ocurrirá lo contrario. s_w2. #define s_addr S_un. s_b3. existen funciones que aseguren el buen almacenamiento de la información. y ambos formatos no siempre coinciden. normalmente se utiliza la función memset().

s_addr = inet_addr("138.h> #include <stdio.. cadena=inet_ntoa(direccion.2 ....• • Para el almacenamiento de un número de puerto (que tiene 16 bits) pasándolo del “network byte order” al “host byte order”: ntohs(). puerto2=ntohs(direccion2. . 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). ... // valor almacenado en “host byte order” direccion1. //unsigned short es igual que u_short struct sockaddr_in direccion1. direccion.152. 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.sin_port). char * cadena...sin_port=htons(puerto). Es importante resaltar que esta función devuelve el valor en el formato “network byte order”.cadena). por lo que no hay que utilizar la función htonl().2"). puerto2..h> .sin_addr. // imprime 138..h> .100. puerto1=80. dirección2. //valor almacenado en //“network byte order” . Un ejemplo de utilización de direcciones IP: #include <winsock2. Para el almacenamiento de una dirección IP (que tiene 32 bits) pasándola del “network byte order” al “host byte order”: ntohl().. • La función inet_ntoa() convierte un entero largo sin signo a una cadena de caracteres. printf("dir IP=%s\n". #include <winsock2. //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”. En el siguiente ejemplo presentamos un posible caso. 10 . struct sockaddr_in direccion.100.152. u_short puerto1..sin_addr).

Esta constante le indica al sistema que asigne la dirección IP que ese equipo tenga. h_addr: Como ya sabemos. h_addr) para convertir a una dirección IP un determinado nombre de domininio de un equipo. que vamos a explicar a continuación: struct hostent { char *h_name. h_aliases: Es un array con los nombres alternativos del equipo. char **h_aliases. h_addr_list: Un array (terminado en cero) de direcciones IP del equipo. es AF_INET). de todos los campos sólo se suele utilizar h_addr_list[0] (en realidad. cuyo prototipo en C es: 11 . se define La primera dirección de h_addr_list. Para ello se utiliza la función gethostbyname(). char **h_addr_list. int h_addrtype. Esta estructura está definida en <winsock. A veces en vez de disponer de la dirección IP.h>.Otra forma de poder asignar una dirección IP en el campo sin_addr. #define h_addr h_addr_list[0] • • • • • • h_name: Es el nombre oficial del equipo. Para poder convertir ese nombre de dominio en el formato necesario para el campo sin_addr. Es muy importante resaltar que las direcciones IP siguen el formato “network byte order”. disponemos de la estructura hostent y de la función gethostbyname(). 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. }. int h_length.s_addr de la estructura sockaddr_in es utilizando la constante INADDR_ANY. por lo que no hay que utilizar la función htonl(). lo que tenemos es el nombre de dominio del equipo. Para facilitar su uso. En la mayoría de los casos. h_addrtype: Tipo de la dirección (en el caso de Internet. la mayoría de los hosts sólo tienen una dirección IP. h_length: Longitud de la dirección (en bytes).s_addr de la estructura sockaddr_in.

. . struct sockaddr_in direccion.#include <winsock2. A veces vamos a querer utilizar el puerto de un servicio estándar.es"). Obviamente para que esta función no de error... y el sistema operativo de la aplicación tener acceso a uno de estos DNS. 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. if (datosHost==NULL){ printf("ERROR no existe ese nombre de dominio\n").h> . . } direccion. el nombre de dominio que pasamos debe estar dado de alta en la estructura de DNS (Servidor de Nombres de Dominio). Un ejemplo de esta utilización sería: #include <winsock2.h> struct hostent *gethostbyname(const char *nombre). struct hostent *datosHost. datosHost=gethostbyname("fenix.. 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). que vamos a explicar a continuación: 12 .eui.upm.h> #include <stdio.sin_addr=*((struct in_addr *)datosHost->h_addr).. es decir.. Si devuelve NULL. exit(1). 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(). es porque ha habido un error. ya definido.

const char *protocolo). En la mayoría de los casos.h> struct servent *getservbyname(const char *servicio. char *s_proto. por lo que no hay que utilizar la función htons().struct servent { char *s_name. 13 . cuyo prototipo en C es: #include <winsock2. Es muy importante resaltar que este número sigue el formato “network byte order”. }. • • • • 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. short s_port. Esta estructura está definida en <winsock. char **s_aliases. s_proto: Es el nombre del protocolo que implementa el servicio. de todos los campos sólo se suele utilizar s_port.h>. Para ello se utiliza la función getservbyname().

Nótese en el ejemplo que la variable puerto.. exit(1). Como pequeño ejercicio pruebe que pasaría si se elimina la función ntohs() del ejemplo anterior. sendto. struct sockaddr_in direccion. printf("puerto del servicio=%d\n". como los host sólo disponen de una dirección IP.sin_port=datosServicio->s_port. } direccion.puerto). Normalmente son los servidores los que de forma explícita (mediante la función bind) eligen la dirección a la que unirse.. datosServicio=getservbyname("http".. struct servent *datosServicio.Un ejemplo de esta utilización sería: #include <winsock2. Esta asociación puede hacerse de forma explícita o implícita (ver Figura 2). 14 . 2. . No obstante un cliente también puede de forma explícita unirse a una determinada dirección. En la mayoría de los casos.h> . . normalmente dejan que sean otras funciones utilizadas en la comunicación (connect. if (datosServicio==NULL){ printf("ERROR no existe ese servicio estandar\n"). lo que se debe elegir es un número de puerto que no esté ocupado por otra aplicación. como todas las variables de un programa excepto las del tipo sockaddr_in (y sockaddr)..h> #include <stdio. un socket necesita asociarse a una dirección para poder enviar o recibir datos por la red.. puerto=ntohs(direccion.sin_port). . short puerto. En el caso de los clientes... recvfrom.4 Función bind Como ya se ha comentado previamente."tcp").. aunque no es lo habitual. …) las que elijan el puerto al que unirse (usualmente escogen el primer puerto que no esté ya ocupado). 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().

3). El primer parámetro s es el socket devuelto por la función socket().. if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n". si vamos a utilizarla hay que hacer un casting al tipo sockaddr (ver el ejemplo siguiente).<número de puerto>) a la que se quiere unir el socket.sin_family = AF_INET. s = socket(PF_INET..h> . // IP que tenga el equipo dirMiEquipo. 0. el par <direccion IP>. 0). (struct sockaddr *) &dirMiEquipo. //elijo un puerto libre resul=bind(s. el parámetro dir es un puntero a la estructura sockaddr (ver sección 2. sizeof(struct sockaddr_in)). SOCKET s. struct sockaddr_in dirMiEquipo.El prototipo en C de la función es: #include <winsock2. El parámetro long_dir indica el tamaño de la estructura apuntada por dir. donde deberá ponerse la dirección (es decir. 15 . int long_dir).h> int bind(SOCKET s.s_addr = INADDR_ANY. const struct sockaddr * dir. Para evitar “warnings” del compilador. exit(2). 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..sin_addr. int resul.// pone a // cero toda la estructura dirMiEquipo.. if (s == INVALID_SOCKET) exit(1). } . dirMiEquipo. Esta función devuelve 0 si todo ha ido bien. //error al crear el socket memset(&dirMiEquipo. .h> #include <stdio. SOCK_DGRAM.WSAGetLastError()).3 que es más fácil de usar la estructura sockaddr_in. Recuérdese de la sección 2.. sizeof(dirMiEquipo)).sin_port = htons(2222).

es decir. int long_msj.h> int sendto(SOCKET s. utilizando el protocolo UDP. El parámetro dirDestino es un puntero a la estructura sockaddr. int flags. hay que utilizar correctamente las funciones htonl().Recuérdese de la sección 2. etc). Un envío normal de datos se consigue poniendo en este campo flags un 0. El parámetro long_dirDestino indica el tamaño de la estructura apuntada por dirDestino. al socket de una aplicación con un determinado par formado por <dir IP> y <número puerto>). pero haciendo casting con sockaddr para evitar “warnings” del compilador. Podemos utilizar también la estructura sockaddr_in. const struct sockaddr *dirDestino. El parámetro flags permite enviar datos con distintas opciones (“fuera de banda”. Esta function envía el array de datos contenido en el parámetro msj por el socket s. ntohl(). htons(). y SOCKET_ERROR si se ha producido un fallo al enviar.3 que tenemos toda una serie de estructuras y funciones para poder manejar la dirección de un socket: inet_addr().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. y ntohs(). “adelantados”. Esta función devuelve el número de bytes enviados por la red si todo ha ido bien. const char *msj. 3. … 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. Al final de esta sección se presenta un ejemplo donde se utilizará esta función sendto(). El parámetro long_msj indica el tamaño del parámetro anterior. 16 . donde deberá ponerse la dirección del socket de la aplicación donde se quieren enviar los datos. int long_dirDestino). 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. gethostbyname(). Capítulo 3. El prototipo en C de la función es: #include <winsock2.

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

por sencillez.Esta función closesocket() devuelve 0 si todo ha ido bien. 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. 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. Obsérvese que la función recvfrom() es bloqueante. Servidor Cliente WSAStartup( ) WSAStartup( ) socket( ) socket( ) bind( ) DATOS (PETICION) recvfrom( ) BLOQUEO sendto( ) recvfrom( ) BLOQUEO DATOS (RESPUESTA) sendto( ) closesocket() WSACleanup( ) closesocket()) WSACleanup( ) Figura 3: Una posible comunicación con sockets de datagrama 18 . que se hace únicamente un envío y una recepción de datos entre el emisor y el receptor. Se ha supuesto. y SOCKET_ERROR si se ha producido un fallo al cerrar el socket.

5 Un ejemplo con UDP Para clarificar los conceptos presentados hasta ahora. 0)."Me saludas?.&puerto_serv). // cadena con la ip del servidor char msj_env[80]. //lee el puerto del servidor error = WSAStartup(MAKEWORD( 2. En “Windows Visual Studio” podemos crear un proyecto con el cliente y el servidor. Con otros compiladores.3. if ( error != 0 ) exit(1). con tener un fichero con la extensión “.WSAGetLastError()). dir_serv. int resul. error. memset(&dir_serv. printf("Direccion IP del servidor=").CLIENTE ---\n"). SOCK_DGRAM. 0. } &wsa_datos). scanf("%d". struct sockaddr_in dir_serv.h> #include <string. exit(2). long_dir_serv.sin_family = AF_INET. puerto_serv. se va a presentar un ejemplo sencillo de comunicación con UDP siguiendo el esquema del apartado anterior. // error if ( LOBYTE( wsa_datos. //lee la dir IP del servidor printf("Puerto del servidor="). 2 ). exit(1).c” será suficiente.sin_addr.h> #include <stdio. 19 .h> void main(){ SOCKET s. char cadena_dir_ip_serv[20]. En él tanto el cliente como el servidor enviarán un mensaje de saludo.s_addr = inet_addr(cadena_dir_ip_serv). // datos a recibir printf("--. dir_serv. El cliente #include <winsock2. al iniciar la DLL 2 || 2 ) { s = socket(PF_INET.&cadena_dir_ip_serv). dir_serv. soy el cliente"). WSADATA wsa_datos.wVersion ) != WSACleanup( ). scanf("%s". if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n".wVersion ) != HIBYTE( wsa_datos. // datos a enviar char msj_rec[80]. sizeof(struct sockaddr_in)). } strcpy(msj_env.sin_port = htons(puerto_serv).

WSAGetLastError()). 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).128 Si no se tiene red en el equipo. msj_rec.1 (que es la dirección local del propio equipo. if (resul == SOCKET_ERROR){ printf("ERROR AL recibir: %d\n". Es el sistema. exit(3).resul=sendto(s. sizeof(msj_env).0.cpp en “Windows Visual Studio”. sizeof(msj_rec). } printf("MENSAJE recibido: %s\n".0. 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.0. por ejemplo.200. una dirección válida sería: 192. (struct sockaddr *) &dir_serv.msj_rec). La dirección del equipo servidor hay que pasarla como “notación decimal con puntos”. if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n". clienteUDP. sizeof(dir_serv)). (struct sockaddr *) &dir_serv. Por ejemplo.WSAGetLastError()). exit(4). &long_dir_serv). } // 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. closesocket(s).0. al ejecutar sendto(). se puede pasar como dirección del servidor la 127.168. WSACleanup( ). msj_env. 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. } long_dir_serv=sizeof(dir_serv). 20 . resul=recvfrom(s.

} printf("--. // datos a enviar char msj_rec[80]. 0.El servidor #include <winsock2. WSADATA wsa_datos. dirMiEquipo.wVersion ) != 2 ) { WSACleanup( ). long_dir_cli. exit(2).sin_family = AF_INET. 0). SOCK_DGRAM. // datos a recibir error = WSAStartup(MAKEWORD( 2.h> void main(){ SOCKET s. dirMiEquipo. sizeof(dirMiEquipo)). if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n". dirMiEquipo. // puerto del servidor resul=bind(s. // error al iniciar la DLL if ( LOBYTE( wsa_datos. } 21 . 2 ).h> #include <stdio. if ( error != 0 ) exit(1). exit(3). dir_cli. sizeof(struct sockaddr_in)). exit(1).WSAGetLastError()). struct sockaddr_in dirMiEquipo.s_addr = INADDR_ANY.sin_port = htons(8888).SERVIDOR ---\n"). int resul. s = socket(PF_INET. char msj_env[80]. (struct sockaddr *) &dirMiEquipo.WSAGetLastError()). } memset(&dirMiEquipo. if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n". error.wVersion ) != 2 || HIBYTE( wsa_datos.sin_addr.h> #include <string. &wsa_datos).

0. sizeof(dir_cli)). strcpy(msj_env. } closesocket(s). resul=recvfrom(s. lo escribe en la consola y le responde. msj_env. por ejemplo. exit(5).cpp en “Windows Visual Studio”. servidorUDP. soy el servidor"). msj_rec. } printf("MENSAJE recibido: %s\n". sizeof(msj_env). exit(4). if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n". Una vez que el servidor recibe el mensaje del cliente. WSACleanup( ). resul=sendto(s.WSAGetLastError()).long_dir_cli=sizeof(dir_cli). if (resul == SOCKET_ERROR){ printf("ERROR AL recibir: %d\n".msj_rec). (struct sockaddr *) &dir_cli. sizeof(msj_rec). 22 . } // 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. (struct sockaddr *) &dir_cli.0."Hola. &long_dir_cli). 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()).WSAGetLastError()).

foros. Tales cambios pueden ser: la modificación del buffer donde se almacenan los datos. En la API Winsock sólo se pueden utilizar las direcciones de multicast con el protocolo UDP. Estas direcciones multicast. 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. 23 . Recuérdese. el protocolo que implementa al socket.1 Función setsockopt Esta función permite cambiar la configuración del “driver” que implementa un determinado socket.0 hasta 239. en la comunicación multicast podemos tener a múltiples sockets unidos a la misma dirección: <224. int nivel.Capítulo 4. 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). es decir. Por lo tanto. Para ello. la MTU. Esto era así porque como se ha visto la dirección a la que se vinculaban los sockets era única. la tenemos que ver divididas en el par <dirección IP> y <número de puerto>. a las direcciones utilizadas en la comunicación unicast también se las suele denominar como direcciones unicast. A este tipo de comunicación se la denomina unicast. int opcion. En el caso de los puertos no hay nada especial. como con las unicast.255). Esto es mucho más ineficiente que utilizando una comunicación (y direcciones) multicast. esto supone n envíos del mensaje a cada dirección unicast. con sockets de datagramas. El prototipo en C de la función es: #include <winsock2. etc. 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. En ciertas aplicaciones (como chats. siguen siendo números con el mismo significado que en la comunicación unicast. int long_opcion). Obviamente.10.h> int setsockopt(SOCKET s. videoconferencias. ya que en este último caso sólo se enviará.10><6666>. const char *valores. etc) es necesario (por razones de eficiencia) que un único envío de datos llegue a múltiples destinatarios. Para poder distinguirlas. debemos utilizar otras funciones que preparen a las aplicaciones para el envío multicast.un único mensaje.0. Obviamente.255.10.h> #include <ws2tcpip. del tema donde se presentaban los conceptos de la arquitectuta TCP/IP. Esta forma de comunicarnos se denomina multicast. además de utilizar las direcciones multicast y las funciones sendto() y recvfrom(). Vamos a orientar esta explicación a multicast.0.255. que las direcciones IP multicast eran de clase D (estaban en el rango desde 224. Aunque parece obvio.

. struct sockaddr_in dirMiEquipo.imr_interface.sin_family = AF_INET. //error al crear el socket memset(&dirMiEquipo. exit(3).El primer parámetro s indica el socket sobre el que se van a cambiar algunas opciones. dirMiEquipo.sin_port = htons(6666). sizeof(struct sockaddr_in)).s_addr =INADDR_ANY. y SOCKET_ERROR si se ha producido un error. s = socket(PF_INET. dirMiEquipo. sizeof(dirMiEquipo)).10.h> #include <ws2tcpip. IPPROTO_IP.20.. int resul.30><6666> . if (s == INVALID_SOCKET) exit(1). // puerto libre resul=bind(s. 0). 24 . } // ahora se puede recibir datos por <224. //IP unicast req_multi.WSAGetLastError())..30").20. if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n". sizeof(req_multi)). En el parámetro nivel señalamos el protocolo al que afectarán dichas modificaciones. struct ip_mreq req_multi. long_opcion contiene el tamaño de valores. Por último. } //asociamos la dir. y en el parámetro valores ponemos los datos que queramos modificar de opcion.IP_ADD_MEMBERSHIP. SOCK_DGRAM. 0. IP unicast con la multicast req_multi.s_addr=inet_addr("224.s_addr = INADDR_ANY..10. El identificador de la opción se incluye en el parámetro opcion..sin_addr.. exit(4). //IP unicast dirMiEquipo. int ttl.WSAGetLastError()). SOCKET s. (struct sockaddr*) &dirMiEquipo. if (resul == SOCKET_ERROR){ printf("ERROR EN OPCIONES MULTICAST: %d\n".imr_multiaddr. (const char *) & req_multi. resul=setsockopt(s. . Un ejemplo de utilización de esta función orientado al uso multicast es: #include <winsock2. Esta función setsockopt() devuelve 0 si todo ha ido bien.h> .

10. exit(3). (const char *) &ttl. En el ejemplo vemos que hemos elegido la dirección IP multicast 224.10. IP_MULTICAST_TTL..20. y decimos (IP_ADD_MEMBERSHIP) que la aplicación que ejecuta este código se una a la dirección multicast <224.20.imr_multiaddr. seguidamente se va a presentar un ejemplo de multicast. Para aclararlo más. y decimos (IP_MULTICAST_TTL) que la aplicación va a poder enviar por ese socket a la dirección multicast <224.20.imr_interface. } // ahora se puede enviar datos multicast por <224.20..20. sizeof(ttl)).30><6666>. 25 .s_addr =INADDR_ANY.. //saltos que puede dar el datagrama en multicast resul=setsockopt(s.10. IPPROTO_IP. Obviamente se puede poner un valor mayor que 1. //preparamos un posible envio multicast ttl=1. Seleccionamos como opción para el envío multicast el protocolo IP (IPPROTO_IP). 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)..30"). El valor ttl=1 limita a todos los equipos dentro de la misma red los posibles miembros del multicast. la red Internet está formada por muchas redes IP conectadas entre sí por routers. Para poder hacerlo vemos que utilizamos la variable req_multi del tipo struct ip_mreq con las siguiente operaciones del ejemplo: req_multi.s_addr=inet_addr("224.. Este ttl=1 es el valor por defecto.10. seleccionamos como opción para el envío multicast el protocolo IP (IPPROTO_IP). Como sabemos por el tema de la arquitectura TCP/IP. Con ellas vamos a asociar en el interfaz la dirección unicast del equipo con la multicast. //IP unicast req_multi. 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.30><6666>.30 y el puerto 6666 para unir al socket. if (resul == SOCKET_ERROR){ printf("ERROR EN OPCIONES MULTICAST: %d\n".30><6666> . Para poder configurar el socket para enviar datos (con sendto()) a una dirección multicast.WSAGetLastError()). 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.10. A partir de ese momento tenemos el equipo preparado para recibir datos (con recvfrom()) por la dirección multicast.

2 Función closesocket Con muticast esta función.3. además de realizar las operaciones locales que mencionamos en la sección 3.3.4. 26 . 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.

Se ha supuesto. Obsérvese que la función recvfrom() es bloqueante. Cliente WSAStartup( ) Servidor WSAStartup( ) socket( ) setsockopt( ) socket( ) bind( ) sendto( ) DATOS setsockopt( ) closesocket( ) recvfrom( ) BLOQUEO WSAcleanup( ) closesocket( ) WSAcleanup( ) Figura 6: Una posible comunicación multicast con sockets de datagrama 27 .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. que el cliente hace un envío. para hacerlo sencillo. 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.4. y el servidor estará permanentemente esperando recibir datos.

wVersion ) != 2 ) { WSACleanup( ). En este ejemplo el cliente manda un mensaje de saludo. y el servidor lo muestra en la pantalla. En “Windows Visual Studio” podemos crear un proyecto con el cliente y el servidor. int ttl.4. // datos a enviar WSADATA wsa_datos. (const char *) &ttl. //saltos que puede dar el datagrama en multicast resul=setsockopt(s. exit(4). int resul. IPPROTO_IP.h> <string. Para ello será suficiente con ejecutar n veces el cliente en n ventanas windows. struct sockaddr_in dir_serv.h> <stdio. IP_MULTICAST_TTL. error.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.h> void main(){ SOCKET s. El cliente #include #include #include #include <winsock2.h> <ws2tcpip. } printf("--.WSAGetLastError()). // error al iniciar la DLL if ( LOBYTE( wsa_datos. Con otros compiladores. } ttl=1. } 28 . Para que se pueda ver el concepto de multicast lo interesante es ejecutar n clientes que manden los mensajes al servidor. exit(1).WSAGetLastError()). y ejecutar en una ventana de windows el servidor.c” será suficiente. s = socket(PF_INET. &wsa_datos). 0).CLIENTE MULTICAST ---\n"). if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n". con tener un fichero con la extensión “. sizeof(ttl)).wVersion ) != 2 || HIBYTE( wsa_datos. SOCK_DGRAM. error = WSAStartup(MAKEWORD( 2. 2 ). exit(2). char msj_env[80]. if ( error != 0 ) exit(1). if (resul == SOCKET_ERROR){ printf("ERROR EN OPCIONES MULTICAST: %d\n".

sizeof(msj_env). exit(5). 0.sin_port = htons(6666). cliente_multicast. dir_serv. msj_env.s_addr = inet_addr("224. dir_serv. 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). strcpy(msj_env.cpp en “Windows Visual Studio”. WSACleanup( ). Obsérvese que lo único que hace el cliente es enviar un mensaje a la dirección multicast: <224. if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR EN MULTICAST: %d\n". resul=sendto(s.0.memset(&dir_serv.20. } // 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.20. sizeof(struct sockaddr_in)).30"). por ejemplo. 29 .10. dir_serv.sin_family = AF_INET. Para ello seleccionamos las opciones IPPROTO_IP y la IP_MULTICAST_TTL."Envio multicast desde el cliente").10. sizeof(dir_serv)).sin_addr. } closesocket(s).WSAGetLastError()). (struct sockaddr *) &dir_serv.30> <6666>.

if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n". if ( error != 0 ) exit(1).s_addr = INADDR_ANY. sizeof(dirMiEquipo)). //IP multicast dirMiEquipo. exit(1). exit(3).wVersion ) != 2 ) { WSACleanup( ).wVersion ) != 2 || HIBYTE( wsa_datos.h> <ws2tcpip. s = socket(PF_INET.sin_family = AF_INET.WSAGetLastError()).h> void main(){ SOCKET s. } memset(&dirMiEquipo. char msj_rec[80]. 2 ).h> <stdio. // datos a recibir WSADATA wsa_datos.sin_port = htons(6666). // puerto libre resul=bind(s.sin_addr. } 30 . dirMiEquipo. 0).WSAGetLastError()). } printf("--. &wsa_datos). int resul. dirMiEquipo. exit(2). error. long_dir_cli. if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n". dir_cli. error = WSAStartup(MAKEWORD( 2. struct ip_mreq req_multi. SOCK_DGRAM.h> <string. struct sockaddr_in dirMiEquipo.El servidor #include #include #include #include <winsock2. (struct sockaddr *) &dirMiEquipo. 0. // error al iniciar la DLL if ( LOBYTE( wsa_datos.SERVIDOR MULTICAST ---\n"). sizeof(struct sockaddr_in)).

por ejemplo.req_multi.IP_ADD_MEMBERSHIP.s_addr=inet_addr("224.20.imr_multiaddr. msj_rec. if (resul == SOCKET_ERROR){ printf("ERROR EN OPCIONES MULTICAST: %d\n".30"). req_multi.20. exit(4). &long_dir_cli). } closesocket(s). IPPROTO_IP. gracias a: req_multi.10. if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR EN MULTICAST: %d\n".cpp en “Windows Visual Studio”.s_addr=inet_addr("224.msj_rec).20.10.30. } printf("MENSAJE recibido: %s\n".s_addr =INADDR_ANY. } while(1) { long_dir_cli=sizeof(dir_cli).s_addr =INADDR_ANY. resul=recvfrom(s. req_multi. sizeof(msj_rec).imr_interface.WSAGetLastError()). servidor_multicast. WSACleanup( ).WSAGetLastError()).imr_multiaddr. 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). sizeof(req_multi)).0.30"). } // 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. (const char *) & req_multi.10. Posteriormente.imr_interface. exit(5). (struct sockaddr *) &dir_cli. 31 . resul=setsockopt(s. Asocia la dirección unicast con la dirección multicast 224.

No es multicast porque serán n conexiones diferentes. 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. por tanto.1 Función connect Esta es una función en principio pensada para ser utilizada sólo por los clientes. antes de poder mandar información con las funciones de enviar y recibir debe establecerse la conexión. Por ser un protocolo orientado a la conexión el que implementa connect(). connect() debe esperar a que el servidor responda con un segmento TCP de confirmación. pero obviamente no generará una solicitud de conexión. connect() también vincula el socket con la dirección del otro extremo de la comunicación. Por supuesto que siempre se pueden establecer n conexiones TCP entre un cliente y un servidor. el socket tiene por defecto asociado la dirección origen y destino desde donde enviar o recibir los datos. las funciones closesocket() y WSACleanup() no se van a ejecutar. con TCP). hará la misma asociación implícita de un par de direcciones. Si lo hacemos es por seguir la “metodología” de siempre. Una conexión se establece entre sólo un cliente y un servidor. 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). ya que el protocolo UDP es no orientado a la conexión (ver tema de la arquitectura TCP/IP). No obstante. utilizando el protocolo TCP. con UDP). sólo se pueden utilizar direcciones unicast. esto se realiza de forma explícita con la función bind(). una vez ejecutada la función connect(). 32 . no por los servidores. y será indicado de forma explícita al explicar su funcionamiento. Es connect() la llamada empleada por el cliente para solicitar el establecimiento de una conexión TCP con un servidor. En algunos casos no es así. En este caso. donde el servidor puede ser el mismo. Capítulo 5. Recuérdese que para que un socket fuera útil se necesitaba que estuviera asociado con una determinada dirección. el programa lo que hace es esperar de forma indefinida a que le lleguen mensajes Obviamente.>C. Como podrá observarse. no una única conexió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. para finalizar este servidor hay que teclear en algún momento las teclas <ctrl. connect() tiene también el efecto de unir implícitamente el socket a una determinada dirección.Una vez hecha la asociación. 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). El protocolo TCP es un protocolo orientado a la conexión. Una vez recibido. muchos programadores utilizan esta función con sockets de datagrama (es decir. En muchos casos las funciones presentadas en este capítulo podrán ser invocadas tanto por el servidor como por el cliente. Una observación importante es que la función connect() fue diseñada para ser utilizada con sockets de flujo (es decir. Por tanto. Como ya hemos visto. En caso de implementar un cliente que no utilice esa función. por lo que no hace falta que se incluyan. 5. es decir. Como TCP es orientado a la conexión.

El parámetro long_dirDestino indica el tamaño de la estructura apuntada por dirDestino. y por tanto Winsock utiliza funciones distintas (como se verá para el servidor la función es accept()). y SOCKET_ERROR si se ha producido un fallo. const struct sockaddr *dirDestino. 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. no por los servidores. El prototipo en C de la función es: #include <winsock2. En caso de sockets de flujo (SOCK_STREAM). 33 . Podemos utilizar también la estructura sockaddr_in.h> int connect(SOCKET s. Por tanto. El parámetro dirDestino es un puntero a la estructura sockaddr. En la sección 5. 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. donde deberá ponerse la dirección del socket de la aplicación donde se quieren enviar los datos.7 se presenta un ejemplo donde se utilizará connect(). En el caso de los sockets de datagrama ya no es así. pero haciendo casting con sockaddr para evitar “warnings” del compilador. Esta función asocia al socket s con la dirección destino apuntada por dirDestino. Por tanto. int long_dirDestino). Esta función devuelve 0 si todo ha ido bien. al no generar el protocolo UDP ningún intercambio de unidades para establecer la conexión. con sockets de datagrama la función connect() puede ser invocada tanto por el cliente como por el servidor.Se ha dicho que connect() es una función en principio pensada para ser utilizada sólo por los clientes. genera un establecimiento de conexión con dirDestino.

5. Cuando expliquemos accept() también se presentará un ejemplo de uso de esta función listen(). El prototipo en C de la función es: #include <winsock2. El primer parámetro indica que el socket s debe ponerse en modo “pasivo”. esta función debe ser invocada únicamente por los servidores (nunca por un cliente).2 Función listen Esta función prepara a un socket para recibir solicitudes de conexión (que se realizarán mediante connect()). Esta función devuelve 0 si todo ha ido bien. 5. puede ser que otros clientes realicen también solicitudes de conexión. es decir. Para que no se pierdan y el sistema las guarde hasta que el servidor pueda tratarlas. devuelve un nuevo socket que identificará la conexión entre el cliente y el servidor. y SOCKET_ERROR si se produce un fallo. Esta función accept() sólo puede ser invocada con sockets de flujo y por los servidores (nunca por un cliente). 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 llamada provoca por parte de TCP el envío de un segmento de confirmación de la conexión. int long_peticiones). es decir. una vez establecida la conexión TCP. El prototipo en C de la función es: 34 . esta función acepta la conexión con el cliente (bien porque está encolada.h> int listen(SOCKET s. La función accept(). 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. la función listen() también proporciona la posibilidad de crear una cola. ese nuevo socket estará asociado al par de direcciones formado por la dirección del cliente y la del servidor.3 Función accept Una vez preparado el socket para recibir solicitudes de conexión (después de ejecutar listen()). Por tanto. a la espera de recibir peticiones de conexión. o bien porque espera hasta que llega una solicitud hecha con connect() por un cliente).

SOCKET s_serv. dirMiEquipo. Por tanto.. SOCKET s_con. if (s_serv == INVALID_SOCKET)exit(1). . Una vez recibida una petición.. int resul. el sistema nos devuelve un nuevo socket que será el resultado de la conexión entre un cliente y el servidor. SOCK_STREAM. struct sockaddr_in dirMiEquipo.. 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. En el caso de que la conexión no se haya podido realizar. resul=bind(s_serv. Un ejemplo de utilización de esta función accept() es: #include <winsock2. 35 .sin_family = AF_INET. el nuevo socket creado estará vinculado tanto a la dirección del cliente aceptado como a una dirección del servidor. dirMiEquipo.. if (resul == SOCKET_ERROR) exit(2). memset(&dirMiEquipo.sin_port = htons(8989).h> SOCKET accept(SOCKET s. la función devuelve INVALID_SOCKET. El tercer parámetro es un puntero al tamaño de dirCliente. Al finalizar correctamente la ejecución del accept(). 0. long_dir_cli.h> . sizeof(struct sockaddr_in)). struct sockaddr *dirCliente.#include <winsock2. int *long_dirCliente). el sistema indica en el parámetro dirCliente el puntero a la dirección del cliente al que se ha conectado. //s_serv recibe las peticiones de conexion de los clientes s_serv = socket(PF_INET. sizeof(dirMiEquipo)). 0). (struct sockaddr *) &dirMiEquipo. dir_cli. dirMiEquipo. 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).sin_addr.s_addr = INADDR_ANY.

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

37 . esta función fue diseñada originalmente para utilizarse con sockets de flujo. La única diferencia es que si se ha utilizado previamente la función connect() (o la función accept()). El prototipo en C de la función es: #include <winsock2. puede utilizarse también con sockets de datagrama si previamente se ha utlizado la función connect(). No obstante. int long_msj. el socket ya sabe la dirección desde la cual queremos recibir los datos. En la secciones 5. 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.7 y 5. “adelantados”. y por lo tanto no necesitamos ningún parámetro que lo indique.4 Función send Esta función permite enviar datos a través de un socket. esta función fue diseñada originalmente para utilizarse con sockets de flujo. Es similar a recvfrom().h> int send(SOCKET s. int flags). Al igual que pasa con connect() y send(). int flags).h> int recv(SOCKET s. Esta función devuelve el número de bytes enviados por la red si todo ha ido bien. const char *msj. 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”. y por lo tanto no necesitamos ningún parámetro que lo indique. 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. 5. etc). Esta function envía el array de datos contenido en el parámetro msj por el socket s. Un envío normal de datos se consigue poniendo en este campo flags un 0. y SOCKET_ERROR si se ha producido un fallo al enviar. const char *msj. Al igual que pasaba con connect().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. int long_msj. No obstante.

Esto es debido a que. al igual que en el caso de sendto. El parámetro flags permite. Por tanto. provoca la liberación de la conexión. Como se ha visto closesocket() cierra la conexión. 5.7 y 5. recibir datos con distintas opciones (“fuera de banda”. En la secciones 5. 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). 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.3. Tanto la sintaxis como la utilización de esta función es igual que la ya descrita en la sección 3. Esto es así para poder optimizar el tamaño de la ventana de TCP (ver el tema de la arquitectura TCP). etc). El prototipo en C de la función es: 38 . y SOCKET_ERROR si se ha producido un fallo. Una recepción normal de datos se consigue poniendo en el campo flags un 0. A veces se quiere tener un mayor control y poder cerrar sólo un extremo de la conexión. o en recibir 2 veces n/2 bytes (o cualquier otra combinación). 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. 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. está en 1 ó 2 segundos). si un cliente inmediatamente intenta volver a conectar puede tener problemas (no obstante este tiempo es muy pequeño.6 Funciones closesocket y shutdown Con sockets de flujo la función closesocket().3. el protocolo TCP puede generar segmentos de datos de un tamaño distinto de los datos volcados por una función send(). para ello tenemos la función shutdown(). El parámetro long_msj indica el tamaño del parámetro anterior. 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.8 se presentan ejemplos donde se utilizará recv(). En este caso lo que quiere decir es que la aplicación remota ha cerrado la conexión.Esta function recibe para el socket s una serie de datos que almacena en el array del parámetro msj. además de realizar las operaciones locales que mencionamos en la sección 3. a diferencia de UDP. “adelantados”. Esta función recv() devuelve el número de bytes recibidos si todo ha ido bien. 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.

5. El primer parámetro indica que el cierre de la conexión se realiza sobre el socket de flujo s. Con otros compiladores. y el servidor cuando reciba todo el mensaje le responde al servidor enviando otro mensaje de respuesta. • 1. El funcionamiento siempre será el mismo: el cliente envía un único mensaje. con tener un fichero con la extensión “. • 2. 5. Los clientes en TCP suelen ser mucho más sencillos que los servidores. esperando que el servidor le conteste con cierta información (de la que no conoce su tamaño. vamos a presentar un ejemplo simple de cliente TCP que nos sirva para ser empleado con todos los tipos de servidores que explicaremos posteriormente.1 Un ejemplo de cliente con TCP. aunque sí sabe que es menor que 80 caracteres). Para intentar reducir código y aclarar los conceptos.c” será suficiente. La aplicación remota ya no puede enviar más datos a la aplicación local (es decir.7. y SOCKET_ERROR si se ha producido un fallo. La aplicación local no puede enviar más datos.7 Cliente con TCP. 39 . 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. El significado del parámetro tipo_cierre depende de los valores: • 0. int tipo_cierre).#include <winsock2. Por tanto vamos a dividir la explicación en diferentes secciones. En “Windows Visual Studio” podemos crear un proyecto con el cliente y el servidor. El cliente enviará un mensaje (de 24 bytes o caracteres). a la que invoca a esta función).h> int shutdown(SOCKET s.

if (s == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n". } s = socket(PF_INET. printf("Direccion IP del servidor TCP="). // mensaje de 24 bytes 40 . dir_serv. // variable auxiliar para escribir lo recibido printf("--. dir_serv.wVersion ) != 2 ) { WSACleanup( ). &wsa_datos).sin_addr.h> #include <stdio. exit(2). error = WSAStartup(MAKEWORD( 2.&cadena_dir_ip_serv). // error al iniciar la DLL if ( LOBYTE( wsa_datos. SOCK_STREAM.sin_port = htons(puerto_serv). // datos a enviar char msj_rec[80]. exit(3). WSADATA wsa_datos.sin_family = AF_INET. dir_serv. exit(1). puerto_serv. printf("Puerto del servidor TCP="). // cadena con la ip del servidor char msj_env[80]. scanf("%s".h> #include <string. if (resul == SOCKET_ERROR){ printf("ERROR AL CONECTAR: %d\n". if ( error != 0 ) exit(1). error.s_addr = inet_addr(cadena_dir_ip_serv).wVersion ) != 2 || HIBYTE( wsa_datos.WSAGetLastError()). struct sockaddr_in dir_serv. // datos a recibir char msj[80]. resul=connect(s. int resul.&puerto_serv).CLIENTE TCP ---\n").h> void main(){ SOCKET s. 2 ). (struct sockaddr *) &dir_serv.#include <winsock2. 0. } memset(&dir_serv. sizeof(dir_serv)). } strcpy(msj_env."Dame toda la informacion").WSAGetLastError()). scanf("%d". char cadena_dir_ip_serv[20]. 0). sizeof(struct sockaddr_in)).

//limpia msj }while (resul!=0).msj). printf("\n FIN de la conexion \n"). msj_rec. WSACleanup( ).0). // escribe el mensaje recibido strcpy(msj. por ejemplo. if (resul == SOCKET_ERROR){ printf("ERROR AL recibir: %d\n".""). sizeof(msj_rec). cliente_TCP.0). } strncpy(msj. msj_env. do{ resul=recv(s.cpp en “Windows Visual Studio”. exit(4). // espera a que el servidor libere la conex. 41 . Hay que recordar lo explicado para la función recv() en TCP: aunque el servidor utilice un solo send(). la otra hace un único recv().WSAGetLastError()). de forma que eso se puede traducir siempre en tener que hacer varios recv().resul=send(s. Aunque nos estamos adelantando a la presentación del servidor. Pero lo importante es saber que nunca se pueden tener garantías de que eso vaya a ser así. 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. closesocket(s).WSAGetLastError()). la información puede ir en varios segmentos de TCP. Lo único que puede sorprender es el bucle do-while para leer lo que el servidor nos envíe.resul). si no que si una aplicación hace un único send(). if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n". el código del cliente es muy fácil de comprender. } // 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. sizeof(msj_env). } printf("MENSAJE recibido: "). msj_rec. El lector un poco experimentado puede advertir que en muchos ejemplos que se pueden encontrar en la literatura no se hace como aquí. exit(4). printf("%s".

5. Es decir.5. el servidor no acepta otra nueva conexión.1 Esquema cliente/servidor con servidor iterativo con TCP Servidor Cliente WSAStartup( ) WSAStartup( ) socket( ) socket( ) bind( ) connect( ) listen( ) send( ) accept( ) DATOS (petición) recv( ) DATOS (respuesta) send( ) recv( ) closesocket( ) closesocket( ) WSAcleanup( ) WSAcleanup( ) 42 . Esto es lo que se llama un servidor iterativo. Seguidamente vamos a presentar como sería esa comunicación.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. hasta que no se acaban de ejecutar todas las instrucciones involucradas en una conexión.8.

(struct sockaddr *) &dirMiEquipo. } //s_serv recibe las peticiones de conexion de los clientes s_serv = socket(PF_INET. error = WSAStartup(MAKEWORD( 2. } memset(&dirMiEquipo. sizeof(dirMiEquipo)). dirMiEquipo. dir_cli. printf("--. if ( error != 0 ) exit(1). &wsa_datos). WSADATA wsa_datos. dirMiEquipo.WSAGetLastError()). int resul. if (s_serv == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n".h> #include <stdio. struct sockaddr_in dirMiEquipo. 2 ). // error al iniciar la DLL if ( LOBYTE( wsa_datos.5.sin_family = AF_INET.sin_port = htons(8989). exit(2). dirMiEquipo. if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n".wVersion ) != 2 ) { WSACleanup( ). 0. exit(1). SOCK_STREAM. resul=bind(s_serv. SOCKET s_con.SERVIDOR TCP ---\n").WSAGetLastError()). // atiende la conexión con el cliente void main(){ SOCKET s_serv.2 Un ejemplo con servidor iterativo con TCP #include <winsock2. long_dir_cli.h> void procesa_conexion(SOCKET s). error. 0).s_addr = INADDR_ANY. sizeof(struct sockaddr_in)).8. exit(3).wVersion ) != 2 || HIBYTE( wsa_datos.h> #include <string. } 43 .sin_addr.

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

En ella se puede observar que el servidor recibe la petición del cliente (de 24 bytes). } Figura 10: Código del servidor TCP iterativo Con todo el código de la figura se puede crear un fichero al que llamar.. resul=send(s_con. strncpy(msj. msj_env. // espera envio completo printf("\n procesando la peticion. // datos a recibir char msj[80]. } cont=cont+resul. y se responde con un send().WSAGetLastError()). // variable auxiliar para escribir lo recibido printf("MENSAJE recibido: ").0). // escribe el mensaje }while (cont<24). // datos a enviar char msj_rec[80]. // se detiene durante 10 segundos strcpy(msj_env. Si la 45 ."Respuesta del servidor"). se cierra s_con printf("\n FIN de la conexion \n"). una vez aceptada por el servidor. resul. \n"). //al finalizar. do{ resul=recv(s_con. servidor_TCP_iterativo. if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n". } closesocket(s_con).WSAGetLastError()).// funcion para tratar la connexion con un cliente void procesa_conexion(SOCKET s_con){ int cont. printf("%s". msj_rec. // simulacion de procesamiento en el servidor Sleep(10000). msj_rec.msj). cont=0.0)..cpp en “Windows Visual Studio”. por ejemplo. char msj_env[80]. exit(4). sizeof(msj_rec). de atender al cliente. sizeof(msj_env).resul). exit(5). if (resul == SOCKET_ERROR){ printf("ERROR AL RECIBIR: %d\n". En el ejemplo se puede apreciar que existe la función procesa_conexion() que es la que se encarga. espere .

En este caso nos podemos preguntar para que sirven las funciones closesocket() y WSACleanup( ). unsigned long long_pila. lo primero que se va a hacer es introducir de forma muy sencilla cómo poder crear hilos en windows para poder tener concurrencia.función sólo hiciera eso el tiempo de ejecución de la conexión sería muy pequeño. Una vez invocada. Como se puede ver. el servidor del ejemplo estará permanentemente aceptando conexiones porque está dentro de un bucle infinito. volver a señalar que la existencia del bucle do-while. ya que no se van a ejecutar nunca. no serían necesarias. que detiene la ejecución del servidor el tiempo que se le indique (en nuestro ejemplo es 10000 milisegundos. El hilo padre continúa ejecutándose en la siguiente instrucción después de _beginthread(). De esta forma. El prototipo en C de la función es: #include <process. pese a que el cliente sólo hizo un send(). 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(). En vez de complicar la tarea a realizar por el servidor en cada conexión. 5. es porque pueden llegar varios recv(). el mayor o menor tiempo de ejecución de las conexiones también influirá en el diseño de los servidores. es decir. 10 segundos). 46 . de forma independiente.h> unsigned long _beginthread( (void (*)(void *)) funcion_hijo.9. 5. En estos casos la atención iterativa de las peticiones de conexión no suele resultar eficiente. _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. void * argumento_funcion). lo que se hace es utlilizar la función Sleep(). Para realizar esta concurrencia se suelen utilizar hilos para que cada uno de ellos. Para mejorar el rendimiento. Efectivamente podrían no ponerse. Si lo hacemos es por seguir la metodología de siempre.1 Función _beginthread Esta función es una llamada al sistema operativo de Windows y permite crear hilos. lo que se propone es que de forma concurrente (es decir simultáneamente) se puedan atender a más de una petición. Aunque no es el tema que queremos estudiar.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. Como se verá en la siguiente sección. aunque como ya se ha indicado. variando únicamente el parámetro de Sleep() se consigue adaptar el tiempo de respuesta del servidor ante una conexión. Por último. trate una conexión con cada cliente que lo pida.

Cuando no se sabe a priori. …) … send(s_con. 5.9. …) … Figura 11: Esquema de creación de hilos en el servidor 47 .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. La función _beginthread() devuelve un identificador del hilo hijo si todo ha ido bien. . . . el esquema de un cliente con uno de los hilos que le va a atender en la conexión. 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. Esta función debe ser declarada y definida como cualquier otra función de C. lo mejor es poner un 0 (que hace que el sistema lo cree del mismo tamaño que el hilo padre). y un -1 en caso de error en la creación del hilo hijo.. Hilo hijo … recv(s_con. . . …) … . . …) Hilo padre s_con=accept(s_serv) _beginthread(. El segundo tamaño long_pila indica al sistema el tamaño que debe reservar en memoria para la creación del hilo hijo.. …) bind(s_serv. …) … send(s_con. y por otro lado. socket(s_serv. . s_con) cada invocación crea un hilo hijo Hilo hijo … recv(s_con.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. …) listen(serv.

Servidor Cliente WSAStartup( ) WSAStartup( ) Hilo padre socket(s_serv ) bind(s_serv ) socket( ) connect( ) listen(s_serv ) send( ) s_con=accept(s_serv ) DATOS (petición) recv(s_con ) DATOS (respuesta) recv( ) Hilo hijo send(s_con) ) closesocket(s_con ) closesocket( ) closesocket(s_serv ) WSAcleanup( ) Hilo padre WSAcleanup( ) Figura 12: Esquema de comunicación entre un cliente y un hilo del servidor 48 .

} 49 .WSAGetLastError()). sizeof(struct sockaddr_in)). int resul. } //s_serv recibe las peticiones de conexion de los clientes s_serv = socket(PF_INET. } memset(&dirMiEquipo.h> void procesa_conexion(SOCKET s). dirMiEquipo.5.9. exit(3). WSADATA wsa_datos. dirMiEquipo.sin_addr.h> <process. // atiende la conexión con el cliente void main(){ SOCKET s_serv. error. // error al iniciar la DLL if ( LOBYTE( wsa_datos. dirMiEquipo.SERVIDOR TCP ---\n").wVersion ) != 2 || HIBYTE( wsa_datos. exit(2). long_dir_cli. printf("--. if ( error != 0 ) exit(1).sin_family = AF_INET. exit(1). dir_cli. SOCKET s_con.s_addr = INADDR_ANY. error = WSAStartup(MAKEWORD( 2. (struct sockaddr *) &dirMiEquipo. sizeof(dirMiEquipo)). 2 ).3 Un ejemplo con servidor concurrente con TCP #include #include #include #include <winsock2. 0.h> <string. 0). if (resul == SOCKET_ERROR){ printf("ERROR AL UNIR EL SOCKET: %d\n". struct sockaddr_in dirMiEquipo.WSAGetLastError()). &wsa_datos). if (s_serv == INVALID_SOCKET){ printf("ERROR AL CREAR EL SOCKET: %d\n". SOCK_STREAM.sin_port = htons(8989). resul=bind(s_serv.h> <stdio.wVersion ) != 2 ) { WSACleanup( ).

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

strncpy(msj. } cont=cont+resul. // simulacion de procesamiento en el servidor Sleep(30000). if (resul == SOCKET_ERROR){ printf("ERROR AL ENVIAR: %d\n". resul=send(s_con."Respuesta del servidor"). // escribe el mensaje }while (cont<24)..WSAGetLastError()). sizeof(msj_env). //al finalizar. if (resul == SOCKET_ERROR){ printf("ERROR AL RECIBIR: %d\n". resul. } Figura 13: Código del servidor TCP concurrente con hilos 51 . \n"). // datos a enviar char msj_rec[80].// funcion para tratar la connexion con un cliente void procesa_conexion(SOCKET s_con){ int cont.resul). // variable auxiliar para escribir lo recibido printf("MENSAJE recibido: "). } closesocket(s_con). exit(5).WSAGetLastError()). msj_rec. // datos a recibir char msj[80].0). sizeof(msj_rec). msj_rec.. se cierra s_con printf("\n FIN de la conexion \n"). exit(4). printf("%s". // espera envio completo printf("\n procesando la peticion. do{ resul=recv(s_con. msj_env. espere . cont=0. // se detiene durante 30 segundos strcpy(msj_env. char msj_env[80].msj).0).

Con todo el código de la figura se puede crear un fichero al que llamar. se ha cambiado el valor del parámetro de la función Sleep() a 30 segundos. 52 . frente al concurrente que sólo sería de 30 segundos.cpp en “Windows Visual Studio”. deben ejecutarse a la vez más de un cliente. 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 ver los efectos de trabajar con un servidor concurrente frente a hacerlo con otro iterativo. por ejemplo. servidor_TCP_concurrente. 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). Para simular que el tiempo de respuesta del servidor es más elevado que en el caso del servidor iterativo.