Sei sulla pagina 1di 52

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

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 cliente servidor ESPERA LA PETICIÓN


SERVICIO AL DE
SERVIDOR. UN CLIENTE,
LA/S PETICIÓN/ES PROCESA DICHA
SE HACEN PETICIÓN, Y, EN LA
UTILIZANDO LAS MAYORÍA DE LOS
OPERACIONES DE CASOS, CONTESTA AL
UN API PROCESO PROCESO CLIENTE
CLIENTE SERVIDOR

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 Servidor
Nivel
… Aplicación

SOCKET s_cli; SOCKET s_serv;
… …
send(s_cli, …) recv(s_serv, …)

s cli s serv

<199.33.22.12, 3333> <130.40.50.10, 80> Nivel


TCP o UDP Transporte

Nivel
IP Red

interfaz de red

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

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
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.

6
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);
}
...

7
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:

8
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:

9
• 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
...
10
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:

11
#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:

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

13
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.

14
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);
}
...

15
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().

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

17
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.

Servidor
Cliente
WSAStartup( )
WSAStartup( )

socket( )
socket( )

bind( )

DATOS (PETICION)
recvfrom( )
sendto( BLOQUEO
)

recvfrom( ) DATOS (RESPUESTA)


sendto( )
BLOQUEO

closesocket()
closesocket())

WSACleanup( ) WSACleanup( )

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

18
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 ||
HIBYTE( wsa_datos.wVersion ) != 2 ) {
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);

19
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).

20
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 ||
HIBYTE( wsa_datos.wVersion ) != 2 ) {
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);
}

21
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.

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

23
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>
...

24
...

//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.

25
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.

26
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.

Cliente Servidor

WSAStartup( ) WSAStartup( )

socket( ) socket( )

setsockopt( ) bind( )

sendto( ) setsockopt( )
DATOS

closesocket( ) recvfrom( )
BLOQUEO

WSAcleanup( ) closesocket( )

WSAcleanup( )

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

27
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 ||
HIBYTE( wsa_datos.wVersion ) != 2 ) {
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);
}

28
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).

29
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 ||
HIBYTE( wsa_datos.wVersion ) != 2 ) {
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);
}

30
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.

31
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).

32
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().

33
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:

34
#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);

35
// 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.

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

37
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:

38
#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.

39
#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

40
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í.

41
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

Servidor
Cliente
WSAStartup( )

WSAStartup( )
socket( )

socket( )
bind( )

connect( )
listen( )
send( )
accept( )

DATOS (petición)
recv( )

DATOS (respuesta)
send( )
recv( )

closesocket( )
closesocket( )

WSAcleanup( )
WSAcleanup( )
42
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 ||
HIBYTE( wsa_datos.wVersion ) != 2 ) {
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);
}

43
// 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

44
// 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
45
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);

46
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.

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

Hilo padre s_con=accept(s_serv)

cada invocación
_beginthread(. . . , . . . , s_con) crea un hilo hijo

Hilo hijo
Hilo hijo
...

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

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

47
Servidor
Cliente
WSAStartup( )

WSAStartup( )
Hilo padre
socket(s_serv )

socket( )
bind(s_serv )

connect( )
listen(s_serv )
send( )
s_con=accept(s_serv )

DATOS (petición)
recv(s_con )

DATOS (respuesta)
send(s_con)
recv( ) )

Hilo hijo
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
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 ||
HIBYTE( wsa_datos.wVersion ) != 2 ) {
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);
}

49
// 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

50
// 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

51
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.

52

Potrebbero piacerti anche