Sockets en Windows
Sockets en Windows
Sockets en 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).
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
Nivel
IP Red
interfaz de 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.
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í.
#include <winsock2.h>
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;
...
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>
#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.
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];
};
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;
};
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.
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().
#include <winsock2.h>
...
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.
#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;
};
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>
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.
#include <winsock2.h>
#include <stdio.h>
...
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);
...
12
struct servent {
char *s_name;
char **s_aliases;
short s_port;
char *s_proto;
};
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>
13
Un ejemplo de esta utilización sería:
#include <winsock2.h>
#include <stdio.h>
...
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.
14
El prototipo en C de la función es:
#include <winsock2.h>
#include <winsock2.h>
#include <stdio.h>
...
SOCKET s;
struct sockaddr_in dirMiEquipo;
int resul;
...
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().
#include <winsock2.h>
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>
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().
#include <winsock2.h>
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.
Servidor
Cliente
WSAStartup( )
WSAStartup( )
socket( )
socket( )
bind( )
DATOS (PETICION)
recvfrom( )
sendto( BLOQUEO
)
closesocket()
closesocket())
WSACleanup( ) WSACleanup( )
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);
}
closesocket(s);
WSACleanup( );
} // fin del main
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
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);
}
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.
#include <winsock2.h>
#include <ws2tcpip.h>
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;
...
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
...
...
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:
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( )
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;
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");
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;
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);
}
closesocket(s);
WSACleanup( );
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");
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.
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>
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>
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().
34
#include <winsock2.h>
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
...
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>
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().
#include <winsock2.h>
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().
38
#include <winsock2.h>
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.
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{
closesocket(s);
WSACleanup( );
} // fin del main
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.
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 main(){
SOCKET s_serv;
SOCKET s_con;
struct sockaddr_in dirMiEquipo, dir_cli;
int resul, long_dir_cli, error;
WSADATA wsa_datos;
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");
}
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
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().
#include <process.h>
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.
socket(s_serv, …)
bind(s_serv, …)
listen(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, …) …
…
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( )
48
5.9.3 Un ejemplo con servidor concurrente con TCP
#include <winsock2.h>
#include <stdio.h>
#include <string.h>
#include <process.h>
void main(){
SOCKET s_serv;
SOCKET s_con;
struct sockaddr_in dirMiEquipo, dir_cli;
int resul, long_dir_cli, error;
WSADATA wsa_datos;
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");
}
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
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