Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Sockets en Windows
Sockets en Windows
ndice
1 Introduccin............................................................................................................... 1.1 Arquitectura Cliente/Servidor.............................................................................. 1.2 Concepto y tipos de sockets .............................................................................. 1.3 La API de Windows............................................................................................ Operaciones bsicas con sockets ............................................................................ 2.1 Inicializacin de Ias DLLs ................................................................................ 2.2 Funcin socket ................................................................................................ 2.3 Utilidades para las funciones ........................................................................... 2.4 Funcin bind .................................................................................................... Operaciones para comunicaciones con UDP ........................................................... 3.1 Funcin sendto ................................................................................................. 3.2 Funcin recvfrom .......................................................................................... 3.3 Funcin closesocket ......................................................................................... 3.4 Esquema cliente/servidor con UDP.................................................................... 3.5 Un ejemplo con UDP ......................................................................................... Operaciones para comunicaciones multicast .......................................................... 4.1 Funcin setsockopt .......................................................................................... 4.2 Funcin closesocket ........................................................................................ 4.3 Esquema cliente/servidor con multicast ............................................................ 4.4 Un ejemplo con multicast .................................................................................. Operaciones para comunicaciones con TCP ........................................................... 5.1 Funcin connect ............................................................................................... 5.2 Funcin listen..................................................................................................... 5.3 Funcin accept ................................................................................................. 5.4 Funcin send .................................................................................................... 5.5 Funcin recv ................................................................................................. 5.6 Funciones closesocket y shutdown .................................................................. 5.7 Cliente con TCP................................................................................................. 5.7.1 Ejemplo de un cliente con TCP .............................................................. 5.8 Servidor iterativo con TCP ........................................................................... 5.8.1 Esquema cliente/servidor con servidor iterativo con TCP ..................... 5.8.2 Un ejemplo con servidor iterativo con TCP .......................................... 5.9 Servidor concurrente con TCP ...................................................................... 5.9.1 Funcin _beginthread ........................................................................... 5.9.2 Esquema cliente/servidor con servidor concurrente con TCP................ 5.9.3 Un ejemplo con servidor concurrente con TCP..................................... 2 2 2 5 5 5 7 8 14 16 16 17 17 18 19 23 23 26 27 28 32 32 34 34 37 37 38 39 39 42 42 43 46 46 47 49
Captulo 1. Introduccin
En este captulo 1 se desea presentar una serie de conceptos necesarios para poder utilizar las funciones proporcionadas por la librera de sockets para Windows (winsock).
SOLICITA UN SERVICIO AL SERVIDOR. LA/S PETICIN/ES SE HACEN UTILIZANDO LAS OPERACIONES DE UN API
cliente
servidor
PROCESO CLIENTE
PROCESO SERVIDOR
ESPERA LA PETICIN DE UN CLIENTE, PROCESA DICHA PETICIN, Y, EN LA MAYORA DE LOS CASOS, CONTESTA AL CLIENTE
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 travs de ellos por donde se enviarn o se recibirn los datos. Desde el punto de vista de la red, un socket debe ser implementado de forma que se le identifique de forma unvoca con respecto a todas las posibles aplicaciones que puedan existir en la red. Para realizar esa identificacin depender de cul sea la red que vamos a utilizar. Hoy en da la red que se emplea en la inmensa mayora de los casos es la red Internet, tambin llamada arquitectura TCP/IP. En todo este tema vamos a centrar nuestro estudio en la comunicacin 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 <direccin IP del equipo> y por el <nmero de puerto>. La <direccin IP del equipo> identifica la ubicacin del ordenador donde se encuentra la aplicacin con el socket. El <nmero de puerto> identifica uno de los distintos procesos que pueden tener lugar en la mquina <direccin IP del equipo>. En la siguiente figura 2 podemos ver que la aplicacin cliente (y servidora) utiliza en el cdigo la variable s_cli (s_serv) para poder acceder a la red Internet. El cliente enva los datos por el socket s_cli con la funcin de la librera del API de sockets send() (ms adelante se estudiar con detalle). En el caso del servidor, los datos se reciben por el socket s_serv con la funcin de la librera del API de sockets recv() (tambin ms adelante se estudiar esta funcin con detalle). Obsrvese tambin en la siguiente figura que, desde el punto de vista del nivel de transporte, el enchufe s_cli se implementa mediante la concatenacin de la direccin IP 199.33.22.12 y el nmero de puerto 3333. En el caso de s_serv es mediante la concatenacin de la direccin IP 130.40.50.10, y del nmero de puerto 80.
Cliente
SOCKET s_cli; send(s_cli, ) s cli <199.33.22.12, 3333>
Servidor
SOCKET s_serv; recv(s_serv, ) s serv <130.40.50.10, 80> Nivel Transporte Nivel Aplicacin
Nivel Red
Se puede decir que hay dos clases de aplicaciones clientes: aquellos que invocan servicios estndar TCP/IP y aquellos que invocan servicios a definir. Los servicios estndar son aquellos servicios ya definidos por TCP/IP, y que por lo tanto tienen ya asignado un nmero de puerto (llamado puerto bien-conocido o well-known). Por ejemplo, 80 es el nmero de puerto para el servidor web (http). Los puertos bien-conocidos estn en el rango de 1 a 1024. Consideramos al resto como servicios a definir, y su rango ser superior a 1024. En la mayora de sistemas operativos hay que tener permisos especiales para poder ejecutar los servidores que implementan los servicios estndar (puertos por debajo del 1024). Por ejemplo en UNIX, slo los puede ejecutar el super-usuario (o tambin llamado usuario root)
Tipos de sockets
Cuando los programadores disean las aplicaciones cliente-servidor, deben elegir entre dos tipos de interaccin: orientada a conexin y no orientada a conexin. Los dos tipos de interaccin 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 interaccin es no orientada a conexin. Si utilizan TCP, la interaccin es orientada a conexin. Vase el tema anterior para un conocimiento ms exhaustivo de ambos protocolos. TCP proporciona toda la fiabilidad necesaria para la comunicacin a travs de la Internet. Para ello, verifica que los datos llegan y automticamente retransmite los segmentos que no llegan. Computa un checksum sobre los datos para garantizar que no se corrompen durante la transmisin. Usa nmeros de secuencia para asegurar que los datos llegan en orden, y automticamente elimina segmentos duplicados. Proporciona control de flujo para asegurar que el emisor no transmite datos ms rpidos que el receptor puede consumir. Finalmente, TCP informa tanto al cliente como al servidor si la red es inoperante por algn motivo. Los clientes y servidores que utilizan UDP no tienen garanta acerca de una entrega fiable. Cuando un cliente enva una peticin, la peticin 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 conexin hace ms fcil 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.
El primer parmetro determina el nmero de versin de Winsock ms alto que nuestro programa puede manejar (en nuestro caso usamos la 2.2). Se puede poner la versin utilizando la macro MAKEWORD. El segundo parmetro es un puntero a una estructura de tipo WSADATA, que recibir informacin sobre la implementacin de Winsock que tengamos en nuestro ordenador: su nmero de versin, una descripcin 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 utilizacin de la funcin WSAStartup() es:
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 aade "ws2_32.lib" ). Para una explicacin ms detallada, ver el tema de herramientas grficas.
Por ltimo, al finalizar la utilizacin de todas las funciones de la API Winsock hay que ejecutar la funcin 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 automtica al finalizar la ejecucin de cualquier programa. Esto es as porque, como veremos ms adelante, muchos servidores no pueden invocan a esta funcin al tener que ejecutarse en un bucle permanente. 6
El parmetro familia_protos especifica la familia de protocolos que usaremos. Los diseadores de la API de Berkeley pensaron que podran coexistir muchas arquitecturas de comunicaciones que soportaran las operaciones proporcionadas por el interfaz. Hoy en da en la prctica totalidad de los casos se utiliza la arquitectura TCP/IP (o tambin 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 parmetro 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 automticamente por Winsock. Cualquier valor distinto de cero da la posibilidad de utilizar otros protocolos que no sean el estndar. Si todo va bien, obtendremos un valor de tipo SOCKET, que representa el nuevo punto de conexin obtenido y que tendremos que usar en las siguientes funciones para llegar a establecer una comunicacin completa. En caso de error, la funcin devolver la constante INVALID_SOCKET. Un ejemplo de utilizacin de la funcin 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); } ...
Ntese en el ejemplo que en caso de error al crear el socket se emplea la funcin WSAGetLastError(). Esta funcin se puede utilizar siempre que se produzca un error al invocar cualquier funcin del API Winsock.
Debido a esta generalidad no es muy usada. Pensada para Internet existe la siguiente estructura de datos:
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 nmero con el puerto elegido. En el campo sin_addr debe ponerse una direccin 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; S_un_w; struct { u_short s_w1, s_w2;} u_long S_addr; }S_un; }; #define s_addr S_un.S_addr
Normalmente slo la definicin s_addr del campo S_un.S_addr va a ser utilizado, como veremos ms adelante. No hay que hacer nada ms. 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 funcin memset(). Tanto la estructura sockaddr como sockaddr_in se encuentran declaradas en <winsock2.h>. Posteriormente se van a presentar mltiples ejemplos de uso de esta estructura sockaddr_in.
Conversiones
Para poder trabajar con los datos de la estructura sockaddr_in (bsicamente una direccin IP y un nmero 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 informacin. Estas funciones son: Para el almacenamiento de un nmero de puerto (que tiene 16 bits) pasndolo del host byte order al network byte order: htons(). Para el almacenamiento de una direccin IP (que tiene 32 bits) pasndola del host byte order al network byte order: htonl(). Recurdese 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 aplicacin (que obviamente debe ser almacenada en el formato host byte order). Para ello contamos con las siguientes funciones: 9
Para el almacenamiento de un nmero de puerto (que tiene 16 bits) pasndolo del network byte order al host byte order: ntohs(). Para el almacenamiento de una direccin IP (que tiene 32 bits) pasndola del network byte order al host byte order: ntohl().
#include <winsock2.h> ... u_short puerto1, puerto2; //unsigned short es igual que u_short struct sockaddr_in direccion1, direccin2; ... 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 funcin inet_addr() convierte una direccin IP en un entero largo sin signo (u_long). Es importante resaltar que esta funcin devuelve el valor en el formato network byte order, por lo que no hay que utilizar la funcin htonl(). La funcin inet_ntoa() convierte un entero largo sin signo a una cadena de caracteres. Un ejemplo de utilizacin 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 direccin 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 direccin IP que ese equipo tenga. Utilizar esa constante permite poder portar directamente el cdigo de una mquina a otra sin tener que volver a compilar porque la direccin IP haya cambiado. A veces en vez de disponer de la direccin 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 funcin gethostbyname(), que vamos a explicar a continuacin:
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 direccin (en el caso de Internet, es AF_INET). h_length: Longitud de la direccin (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 funcin htonl(). h_addr: Como ya sabemos, la mayora de los hosts slo tienen una direccin IP. Para facilitar su uso, se define La primera direccin de h_addr_list.
Esta estructura est definida en <winsock.h>. En la mayora de los casos, de todos los campos slo se suele utilizar h_addr_list[0] (en realidad, h_addr) para convertir a una direccin IP un determinado nombre de domininio de un equipo. Para ello se utiliza la funcin gethostbyname(), cuyo prototipo en C es: 11
El parmetro 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 funcin 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 aplicacin tener acceso a uno de estos DNS. Un ejemplo de esta utilizacin sera:
#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); ...
struct servent { char *s_name; char **s_aliases; short s_port; char *s_proto; };
s_name: Es el nombre del servicio estndar. 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 nmero sigue el formato network byte order, por lo que no hay que utilizar la funcin htons(). s_proto: Es el nombre del protocolo que implementa el servicio.
Esta estructura est definida en <winsock.h>. En la mayora de los casos, de todos los campos slo se suele utilizar s_port. Para ello se utiliza la funcin getservbyname(), cuyo prototipo en C es:
#include <winsock2.h> struct servent *getservbyname(const char *servicio, const char *protocolo);
13
#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); ...
Ntese 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 nmero debe utilizarse la funcin ntohs(). Como pequeo ejercicio pruebe que pasara si se elimina la funcin ntohs() del ejemplo anterior.
14
El prototipo en C de la funcin es: #include <winsock2.h> int bind(SOCKET s, const struct sockaddr * dir, int long_dir);
El primer parmetro s es el socket devuelto por la funcin socket(), el parmetro dir es un puntero a la estructura sockaddr (ver seccin 2.3), donde deber ponerse la direccin (es decir, el par <direccion IP>,<nmero de puerto>) a la que se quiere unir el socket. Recurdese de la seccin 2.3 que es ms fcil 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 parmetro long_dir indica el tamao de la estructura apuntada por dir. Esta funcin devuelve 0 si todo ha ido bien, y SOCKET_ERROR si no se ha podido unir a la direccin apuntada por dir. Un ejemplo de utilizacin de la funcin 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
Recurdese de la seccin 2.3 que tenemos toda una serie de estructuras y funciones para poder manejar la direccin de un socket: inet_addr(), gethostbyname(), Tambin 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>
int sendto(SOCKET s, const char *msj, int long_msj, int flags, const struct sockaddr *dirDestino, int long_dirDestino);
Esta function enva el array de datos contenido en el parmetro msj por el socket s. El parmetro long_msj indica el tamao del parmetro anterior. El parmetro flags permite enviar datos con distintas opciones (fuera de banda, adelantados, etc). Un envo normal de datos se consigue poniendo en este campo flags un 0. El parmetro dirDestino es un puntero a la estructura sockaddr, donde deber ponerse la direccin del socket de la aplicacin donde se quieren enviar los datos. Podemos utilizar tambin la estructura sockaddr_in, pero haciendo casting con sockaddr para evitar warnings del compilador. El parmetro long_dirDestino indica el tamao de la estructura apuntada por dirDestino. Esta funcin devuelve el nmero 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 seccin se presenta un ejemplo donde se utilizar esta funcin sendto(). 16
#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 parmetro msj. El parmetro long_msj indica el tamao del parmetro anterior. El parmetro flags permite, al igual que en el caso de sendto, recibir datos con distintas opciones (fuera de banda, adelantados, etc). Una recepcin normal de datos se consigue poniendo en el campo flags un 0. Ntese, a diferencia de lo que pasa en sendto(), que a priori no podemos saber quin ser quien nos va a enviar los datos. Por lo tanto, esto dos ltimos parmetros los rellenar el sistema una vez que se reciban los datos, nunca la aplicacin que invoca a esta funcin. Por ello el parmetro dirDestino es un puntero a la estructura sockaddr, donde deber recibirse la direccin del socket de la aplicacin que nos ha enviado los datos. Al igual que con sendto(), podemos utilizar tambin la estructura sockaddr_in, pero haciendo casting con sockaddr para evitar warnings del compilador. El parmetro long_dirDestino es (a diferencia de la funcin sendto()) un puntero que nos indica el tamao de la estructura apuntada por dirDestino. Esta funcin devuelve el nmero de bytes recibidos si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo al enviar. Al final de esta seccin se presenta un ejemplo donde se utilizar esta funcin recvfrom().
#include <winsock2.h>
17
Esta funcin 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( )
BLOQUEO
sendto( ) recvfrom( )
BLOQUEO
DATOS (RESPUESTA)
closesocket()) WSACleanup( )
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 ), if ( error != 0 ) exit(1); // error if ( LOBYTE( wsa_datos.wVersion ) != HIBYTE( wsa_datos.wVersion ) != WSACleanup( ); exit(1); } &wsa_datos); al iniciar la DLL 2 || 2 ) {
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
Con todo el cdigo de la figura anterior se puede crear un fichero al que llamar, por ejemplo, clienteUDP.cpp en Windows Visual Studio. Obsrvese que lo nico que hace el cliente es enviar un mensaje a la direccin IP y puerto del servidor que se le pasen por la consola. La direccin del equipo servidor hay que pasarla como notacin decimal con puntos. Por ejemplo, una direccin vlida sera: 192.168.200.128 Si no se tiene red en el equipo, se puede pasar como direccin del servidor la 127.0.0.1 (que es la direccin local del propio equipo, o tambin llamada localhost) Obsrvese que el socket del cliente no se une de manera explcita (es decir con la funcin bind()) a ninguna direccin. Es el sistema, al ejecutar sendto(), el que le asignar al cliente una direccin IP (la de la mquina) y un nmero 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
Con todo el cdigo 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 direccin formada por: la IP de la mquina (INADDR_ANY) y al puerto 8888 (no hay que olvidarse de utilizar la funcin htons()). Una vez que el servidor recibe el mensaje del cliente, lo escribe en la consola y le responde.
22
int setsockopt(SOCKET s, int nivel, int opcion, const char *valores, int long_opcion); 23
El primer parmetro s indica el socket sobre el que se van a cambiar algunas opciones. En el parmetro nivel sealamos el protocolo al que afectarn dichas modificaciones. El identificador de la opcin se incluye en el parmetro opcion, y en el parmetro valores ponemos los datos que queramos modificar de opcion. Por ltimo, long_opcion contiene el tamao de valores. Esta funcin setsockopt() devuelve 0 si todo ha ido bien, y SOCKET_ERROR si se ha producido un error. Un ejemplo de utilizacin de esta funcin 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 direccin IP multicast 224.10.20.30 y el puerto 6666 para unir al socket. Seleccionamos como opcin para el envo multicast el protocolo IP (IPPROTO_IP), y decimos (IP_ADD_MEMBERSHIP) que la aplicacin que ejecuta este cdigo se una a la direccin multicast <224.10.20.30><6666>. Esto ltimo lo que provoca es que el protocolo de multicast (de forma transparente para el programador) enve datos indicando que le incluyan como uno de los miembros de esa direccin multicast. A partir de ese momento tenemos el equipo preparado para recibir datos (con recvfrom()) por la direccin 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 direccin unicast del equipo con la multicast. Para poder configurar el socket para enviar datos (con sendto()) a una direccin multicast, seleccionamos como opcin para el envo multicast el protocolo IP (IPPROTO_IP), y decimos (IP_MULTICAST_TTL) que la aplicacin va a poder enviar por ese socket a la direccin 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 envo 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 inundacin de Internet por datos no deseados). Para aclararlo ms, seguidamente se va a presentar un ejemplo de multicast. 25
26
Cliente
WSAStartup( )
Servidor
WSAStartup( )
socket( ) setsockopt( )
socket( ) bind( )
sendto( )
DATOS
setsockopt( )
closesocket( )
recvfrom( )
BLOQUEO
WSAcleanup( )
closesocket( )
WSAcleanup( )
El cliente
#include #include #include #include <winsock2.h> <ws2tcpip.h> <stdio.h> <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
Con todo el cdigo de la figura anterior se puede crear un fichero al que se puede llamar, por ejemplo, cliente_multicast.cpp en Windows Visual Studio. Obsrvese que lo nico que hace el cliente es enviar un mensaje a la direccin 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 envo multicast no se propague ms alla del router que forman todos los equipos de la misma red IP (que es lo permitido por defecto).
29
El servidor
#include #include #include #include <winsock2.h> <ws2tcpip.h> <stdio.h> <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
Con todo el cdigo de la figura anterior se puede crear un fichero al que llamar, por ejemplo, servidor_multicast.cpp en Windows Visual Studio. Obsrvese que lo nico que hace el servidor es unirse primero a una direccin unicast (ver el cdigo de la pgina 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 direccin unicast con la direccin multicast 224.10.20.30. 31
Una vez hecha la asociacin, el programa lo que hace es esperar de forma indefinida a que le lleguen mensajes Obviamente, para finalizar este servidor hay que teclear en algn 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 metodologa de siempre.
Se ha dicho que connect() es una funcin en principio pensada para ser utilizada slo 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 slicita una conexin que para el servidor que tiene que aceptarla, y por tanto Winsock utiliza funciones distintas (como se ver para el servidor la funcin es accept()). En el caso de los sockets de datagrama ya no es as, al no generar el protocolo UDP ningn intercambio de unidades para establecer la conexin. Por tanto, lo nico que utiliza el programador es el efecto local que hace que el socket se vincule tanto a su direccin como a la direccin destino. Por tanto, con sockets de datagrama la funcin connect() puede ser invocada tanto por el cliente como por el servidor. El prototipo en C de la funcin es:
#include <winsock2.h>
Esta funcin asocia al socket s con la direccin destino apuntada por dirDestino. En caso de sockets de flujo (SOCK_STREAM), genera un establecimiento de conexin con dirDestino. El parmetro dirDestino es un puntero a la estructura sockaddr, donde deber ponerse la direccin del socket de la aplicacin donde se quieren enviar los datos. Podemos utilizar tambin la estructura sockaddr_in, pero haciendo casting con sockaddr para evitar warnings del compilador. El parmetro long_dirDestino indica el tamao de la estructura apuntada por dirDestino. Esta funcin devuelve 0 si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo. En la seccin 5.7 se presenta un ejemplo donde se utilizar connect().
33
#include <winsock2.h>
El primer parmetro indica que el socket s debe ponerse en modo pasivo, es decir, a la espera de recibir peticiones de conexin. El segundo parmetro long_peticiones indica el nmero mximo de peticiones que debe encolar a la espera que el servidor pueda tratarlas. Esta funcin devuelve 0 si todo ha ido bien, y SOCKET_ERROR si se produce un fallo. Cuando expliquemos accept() tambin se presentar un ejemplo de uso de esta funcin listen().
34
#include <winsock2.h>
El primer parmetro s indica el socket que est en modo pasivo a la espera de que los clientes le hagan connect() a su direccin. Una vez recibida una peticin, el sistema nos devuelve un nuevo socket que ser el resultado de la conexin entre un cliente y el servidor. Por tanto, el nuevo socket creado estar vinculado tanto a la direccin del cliente aceptado como a una direccin del servidor. Al finalizar correctamente la ejecucin del accept(), el sistema indica en el parmetro dirCliente el puntero a la direccin del cliente al que se ha conectado. El tercer parmetro es un puntero al tamao de dirCliente. Es importante resaltar que el programador debe poner antes de invocar a accept() este valor apuntando al tamao esperado de dirCliente (que es la estructura sockaddr o sockaddr_in). En el caso de que la conexin no se haya podido realizar, la funcin devuelve INVALID_SOCKET. Un ejemplo de utilizacin de esta funcin 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 direccin 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 conexin // que se acaba de establecer ... send(s_con, ... ); // el envo 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 conexin, mientras que s_con es el socket para trabajar con una conexin en concreto. El ejemplo reproduce un esquema habitual en el cual los servidores estn permanentemente aceptando conexiones de clientes. A partir de la seccin 5.8 se presentan ejemplos completos de servidores con TCP.
36
#include <winsock2.h>
Esta function enva el array de datos contenido en el parmetro msj por el socket s. El parmetro long_msj indica el tamao del parmetro anterior. El parmetro flags permite enviar datos con distintas opciones (fuera de banda, adelantados, etc). Un envo normal de datos se consigue poniendo en este campo flags un 0. Esta funcin devuelve el nmero 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 parmetro msj. El parmetro long_msj indica el tamao del parmetro anterior. El parmetro flags permite, al igual que en el caso de sendto, recibir datos con distintas opciones (fuera de banda, adelantados, etc). Una recepcin normal de datos se consigue poniendo en el campo flags un 0. Esta funcin recv() devuelve el nmero de bytes recibidos si todo ha ido bien, y SOCKET_ERROR si se ha producido un fallo. Es muy importante destacar que la funcin recv() tambin puede devolver 0 como nmero de bytes recibidos por el socket de flujo s. En este caso lo que quiere decir es que la aplicacin remota ha cerrado la conexin. Este valor 0 se suele utilizar al implementar muchas aplicaciones para indicar que la aplicacin remota ya ha enviado todo lo que tena y que no hay por qu esperar a recibir ms datos de ella. Es tambin muy importante resaltar que en los sockets de flujo un envo de n datos con un send() no tiene por qu corresponderse con una nica recepcin de n datos. Esto es debido a que, a diferencia de UDP, el protocolo TCP puede generar segmentos de datos de un tamao distinto de los datos volcados por una funcin send(). Esto es as para poder optimizar el tamao de la ventana de TCP (ver el tema de la arquitectura TCP). Por tanto, esto se traduce para el programador en que un envo 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 combinacin). En la secciones 5.7 y 5.8 se presentan ejemplos donde se utilizar recv().
38
#include <winsock2.h>
El primer parmetro indica que el cierre de la conexin se realiza sobre el socket de flujo s. El significado del parmetro tipo_cierre depende de los valores: 0. La aplicacin remota ya no puede enviar ms datos a la aplicacin local (es decir, a la que invoca a esta funcin). 1. La aplicacin local no puede enviar ms datos. 2. Ni la aplicacin local ni la remota pueden enviar ms datos (es equivalente closesocket()). Esta funcin 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{ 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
Con todo el cdigo 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 presentacin del servidor, el cdigo del cliente es muy fcil de comprender. Lo nico que puede sorprender es el bucle do-while para leer lo que el servidor nos enve. Hay que recordar lo explicado para la funcin recv() en TCP: aunque el servidor utilice un solo send(), la informacin 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 aplicacin hace un nico send(), la otra hace un nico recv(). Esto es as porque la mayora 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 garantas de que eso vaya a ser as. 41
Servidor Cliente
WSAStartup( ) WSAStartup( ) socket( ) socket( ) bind( ) connect( ) listen( ) send( ) accept( )
DATOS (peticin)
recv( )
DATOS (respuesta)
send( )
43
// prepara s_serv para aceptar conexiones listen(s_serv,5); while(1) { //acepta una conexion a la direccin 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 conexin 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"); }
Con todo el cdigo 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 funcin 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 peticin del cliente (de 24 bytes), y se responde con un send(). Si la 45
funcin slo hiciera eso el tiempo de ejecucin de la conexin sera muy pequeo. Como se ver en la siguiente seccin, el mayor o menor tiempo de ejecucin de las conexiones tambin influir en el diseo de los servidores. En vez de complicar la tarea a realizar por el servidor en cada conexin, lo que se hace es utlilizar la funcin Sleep(), que detiene la ejecucin del servidor el tiempo que se le indique (en nuestro ejemplo es 10000 milisegundos, es decir, 10 segundos). De esta forma, variando nicamente el parmetro de Sleep() se consigue adaptar el tiempo de respuesta del servidor ante una conexin. 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 podran no ponerse. Si lo hacemos es por seguir la metodologa de siempre, aunque como ya se ha indicado, no seran necesarias. Por ltimo, volver a sealar que la existencia del bucle do-while, pese a que el cliente slo hizo un send(), es porque pueden llegar varios recv().
unsigned long _beginthread( (void (*)(void *)) funcion_hijo, unsigned long long_pila, void * argumento_funcion); 46
El primer parmetro funcion_hijo indica el nombre de la funcin que el hilo hijo debe ejecutar al ser creado por el sistema operativo. Esta funcin debe ser declarada y definida como cualquier otra funcin de C. El segundo tamao long_pila indica al sistema el tamao que debe reservar en memoria para la creacin del hilo hijo. Cuando no se sabe a priori, lo mejor es poner un 0 (que hace que el sistema lo cree del mismo tamao que el hilo padre). El tercer parmetro argumento_funcion permite pasar una variable desde el hilo padre a la funcin funcion_hijo cuando el sistema operativo crea al hilo hijo. La funcin _beginthread() devuelve un identificador del hilo hijo si todo ha ido bien, y un -1 en caso de error en la creacin del hilo hijo.
Hilo padre
Hilo hijo
recv(s_con, ) send(s_con, )
...
Hilo hijo
recv(s_con, ) send(s_con, )
47
Servidor Cliente
WSAStartup( ) WSAStartup( )
Hilo padre
socket(s_serv ) bind(s_serv )
socket( )
recv(s_con )
DATOS (respuesta)
recv( )
Hilo hijo
send(s_con) ) closesocket(s_con )
WSAcleanup( )
48
void procesa_conexion(SOCKET s); // atiende la conexin 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 direccin 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"); }
51
Con todo el cdigo 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 aadido con respecto al servidor iterativo es la funcin _beginthread() para crear hilos que traten cada conexin. Para simular que el tiempo de respuesta del servidor es ms elevado que en el caso del servidor iterativo, se ha cambiado el valor del parmetro de la funcin 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 ms de un cliente. Entonces podremos comprobar que si 3 clientes de forma simultnea establecieran una conexin, con el servidor iterativo el tiempo de finalizacin de procesar las 3 peticiones sera de 90 segundos (30+30+30 segundos), frente al concurrente que slo sera de 30 segundos.
52