Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.com

Sockets UDP en C para Linux

Aquí suponemos que ya están claros los conceptos de lo que es una arquitectura cliente/servidor, un socket, un servicio (fichero /etc/services ), etc. Si no es así, puedes leer el primer ejemplo antes de seguir leyendo en esta página.

Los sockets UDP son sockets no orientados a conexión. Esto quiere decir que un programa puede abrir un socket y ponerse a escribir mensajes en él o leer, sin necesidad de esperar a que alguien se conecte en el otro extremo del socket.

El protocolo UDP, al no ser orientado a conexión, no garantiza que el mensaje llegue a su destino. Parece claro que si mi programa envía un mensaje y no hay nadie escuchando, ese mensaje se pierde. De todas formas, aunque haya alguien escuchando, el protocolo tampoco garantiza que el mensaje llegue. Lo único que garantiza es, que si llega, llega sin errores.

 ¿Para qué sirve entonces?. Este tipo de sockets se suele usar para información no vital, por ejemplo, envío de gráficos a una pantalla. Si se pierde algún gráfico por el camino, veremos que la pantalla pierde un refresco, pero no es importante. El que envía los gráficos puede estar dedicado a cosas más importantes y enviar los gráficos sin preocuparse (y sin quedarse bloqueado) si el otro los recibe o no.

Otra ventaja es que con este tipo de sockets es que mi programa puede recibir mensajes de varios sitios a la vez. Si yo estoy escuchando por un socket no orientado a conexión, cualquier otro programa en otro ordenador puede enviarme un mensaje. Mi programa servidor no necesita preocuparse de establecer y mantener conexiones con varios clientes a la vez .

El concepto de cliente/servidor sigue teniendo aquí el mismo sentido. El servidor abre un socket UDP en un servicio conocido por los clientes y se queda a la escucha del mismo. El cliente abre un socket UDP en cualquier servicio/puerto que esté libre y envía un mensaje al servidor solicitando algo. La diferencia principal con los TCP (orientados a conexión), es que en estos últimos ambos sockets (de cliente y de servidor) están conectados y lo que escribimos en un lado, sale por el otro. En un UDP los sockets no están conectados, así que a la hora de enviar un mensaje, hay que indicar quién es el destinatario.

En los sockets orientados a conexión se envían mensajes con write() o send() y se reciben con read() o recv(). En un socket no orientado a conexión hay que indicar el destinatario, así que se usan las funciones sendto() y recvfrom().

Veamos todo esto con código y un ejemplo concreto.

EL SERVIDOR

Los pasos que debe seguir un programa servidor son los siguientes:

Veamos todo esto con un poco más de detalle.

Abrir el socket

Se abre el socket de la forma habitual, con la función socket(). Esto símplemente nos devuelve un descriptor de socket, que todavía no funciona ni es útil. La forma de llamarla sería la siguiente:

int Descriptor;
Descriptor = socket (AF_INET, SOCK_DGRAM, 0);

El primer parámetro indica que es socket es de red, (podría ser AF_UNIX para un socket entre procesos dentro del mismo ordenador).

El segundo indica que es UDP (SOCK_STREAM indicaría un socket TCP orientado a conexión).

El tercero es el protocolo que queremos utilizar. Hay varios disponibles, pero poniendo un 0 dejamos al sistema que elija este detalle.

Asociar el socket con un puerto

Aunque ya se explicó en el ejemplo simple de socket, damos aquí un pequeño repaso al tema del puerto/servicio y el fichero /etc/services.

En unix para el establecimiento de conexiones con sockets hay 65536 puertos disponibles, del 0 al 65535. Del 0 al 1023 están reservados para el sistema. El resto están a nuestra disposición para lo que queramos. Cuando abrimos un socket servidor, debemos decir al sistema operativo que queremos atender a uno de estos puertos y eso se hace con la función bind().

Para decir que puerto queremos atender, hay dos opciones:

En nuestro ejemplo hemos dado de alta en el fichero /etc/services un servicio al que hemos llamado cpp_java, que es udp y con un número de puerto. Es decir, hemos añadido en dicho fichero una línea como esta:

cpp_java   25558/udp   # servicio para programa de pruebas.

Vamos ahora con los detalles del código.

Para decir al sistema operativo que deseamos atender a un determinado servicio, de forma que cuando llegue un mensaje por ese servicio nos avise, debemos llamar a la función bind(). La forma de llamarla es la siguiente:

struct sockaddr_in Direccion;

/* Aquí hay que rellenar la estructura Direccion */
Direccion = ...

bind ( Descriptor, (struct sockaddr *)&Direccion, sizeof (Direccion));

El primer parámetro es el descriptor de socket obtenido con la función socket().

El segundo parámetro es un puntero a una estructura sockaddr que debemos rellenar adecuadamente. La Direccion la hemos declarado como struct sockaddr_in porque esta es una estructura adecuada para sockets de red (AF_INET) y es compatible con la estructura sockaddr (podemos hacer cast de una a otra). Para los sockets que comunican procesos en la misma máquina AF_UNIX, tenemos la estructura sockaddr_un, que también es compatible con sockaddr).

El tercer parámetro es el tamaño de la estructura sockaddr_in.

¿ Cómo rellenamos la estructura sockaddr_in ?. Hay tres campos que debemos rellenar:

Direccion.sin_family = AF_INET;
Direccion.sin_port = ... ;  /* Este campo se explica cómo rellenarlo un poco más adelante */
Direccion.sin_addr.s_addr = INADDR_ANY;

El campo sin_family se rellena con el tipo de socket que estamos tratando, AF_INET en nuestro caso

El campo s_addr es la dirección IP del cliente al que queremos atender. Poniendo INADDR_ANY atenderemos a cualquier cliente.

El campo sin_port es el número de puerto/servicio. Para hacerlo bien, debemos leer del fichero /etc/services el número del servicio cpp_java. Para ello tenemos la función getservbyname(). La llamada a esta función es de la siguiente manera:

struct servent *Puerto = NULL;
Puerto = getservbyname (Servicio, "udp");

Direccion.sin_port = Puerto->s_port;

Se declara un puntero a la estructura servent. La función getservbyname() recibe el nombre del servicio y si es "tcp" o "udp". Busca en el fichero /etc/services y nos devuelve una estructura servent con lo que ha encontrado (NULL en caso de no encontrar nada o de error). Una vez rellena la estructura servent, tenemos en el campo s_port el número del puerto. Basta asignarlo a Direccion.sin_port.

Con esto el socket del servidor está dispuesto para recibir y enviar mensajes.

Recibir mensajes en el servidor

La función para leer un mensaje por un socket upd es recvfrom(). Esta función admite seis parámetros (función compleja donde las haya). Vamos a verlos:

Nuestro código para nuestro ejemplo quedaría, para recibir un mensaje,

/* Contendrá los datos del que nos envía el mensaje */
struct sockaddr_in Cliente;

/* Tamaño de la estructura anterior */    
int longitudCliente = sizeof(Cliente);  

/* Nuestro mensaje es simplemente un entero, 4 bytes. */
int buffer;  

recvfrom (Descriptor, (char *)&buffer, sizeof(buffer), 0, (struct sockaddr *)&Cliente, &longtudCliente);

La función se quedará bloqueada hasta que llegue un mensaje. Nos devolverá el número de bytes leidos o -1 si ha habido algún error.

Responder con un mensaje al cliente

La función para envío de mensajes es sendto(). Esta función admite seis parámetros, que son los mismos que la función recvfrom(). Su signifcado cambia un poco, así que vamos a verlos:

Nuestro código, después de haber recibido el mensaje del cliente, quedaría más o menos

/* Rellenamos el mensaje de salida con los datos que queramos */
buffer = ...; 

sendto (Descriptor, (char *)&buffer, sizeof(buffer), 0, (struct sockaddr *)&Cliente, longitudCliente);

La llamada envía el mensaje y devuelve el número de bytes escritos o -1 en caso de error.

EL CLIENTE

Los pasos que debe seguir un cliente son los siguientes:

Abrir el socket

Esto es fácil, al menos la explicación por mi parte: Exactamente igual que el servidor.

Asociar el socket a un puerto

Igual que en el caso del servidor, se hace con la función bind(). Hay pequeñas diferencias en la forma de rellenar el segundo parámetro (la estructura sockaddr), así que las contamos aquí.

Direccion.sin_family = AF_INET;

/* Dejamos que el sistema elija el puerto, uno libre cualquiera */
Direccion.sin_port = 0;  

Direccion.sin_addr.s_addr = INADDR_ANY;

Vemos que la diferencia es el campo sin_port. Se pone un cero para dejar que el sistema operativo elija el puerto libre que quiera. Esto se puede hacer así porque el cliente no necesita tener un puerto conocido por el servidor. Normalmente el cliente es el que comienza la comunicación pidiéndole algo al servidor. Cuando el mensaje de petición llega al servidor, también llega el puerto y máquina en la que está el cliente, con lo que el servidor podrá responderle.

Enviar un mensaje al servidor

Para enviar un mensaje al servidor la función es sendto(). Los parámetros y forma de funcionamiento es igual que en el servidor, pero con la diferencia de que esta vez sí que tenemos que rellenar la estructura sockaddr del quinto parámetro. La forma de hacerlo es la siguiente:

Direccion.sin_family = AF_INET;

/* Aquí debemos dar el puerto del servidor, el cpp_java del fichero /etc/services */
Direccion.sin_port = ...;  

/* Aquí debemos dar la dirección IP del servidor, en formato de red. Lo vemos más abajo */
Direccion.sin_addr.s_addr = ...; 

La forma de rellenar el campo sin_port ya nos la sabemos. El servicio cpp_java debe estar en el fichero /etc/services de la máquina donde corre el cliente, con el mismo número de puerto que se puso en la del servidor. Se obtiene el puerto con la función getservbyname(), igual que se hizo en el bind() del servidor.

La dirección s_addr del servidor se debe obtener con la función gethostbyname(). En el fichero /etc/hosts de la máquina hay una lista de nombres de máquinas conocidas y direcciones IP de las mismas (si esto te suena a chino, te remito nuevamente al ejemplo simple). Para que gethostbyname() funcione correctamente, se le pasa de parámetro el nombre de una máquina que esté dada de alta en el fichero /etc/hosts. En nuestro ejemplo, puesto que servidor y cliente van a correr en la misma máquina, usaremos como nombre de máquina "localhost", que está dado de alta por defecto en cualquier instalación de linux. La llamada se haría así:

struct hostent *Maquina;

Maquina = gethostbyname ("localhost");
Direccion.sin_addr.s_addr = ((struct in_addr *)(Maquina->h_addr))->s_addr;

Bueno, la asignación parece un poco compleja. Veamos de donde sale.

La estructura Maquina tiene un campo h_addr, de ahí la parte Maquina->h_addr

Este campo no es del tipo deseado, así que se convierte con un cast a struct in_addr, de ahí lo de (struct in_addr *)(Host->h_addr)

Bueno, pues todo esto es a su vez una estructura, de la que nos interesa el campo s_addr, así que metemos todo entre paréntesis, cogemos el campo y nos queda lo que tenemos puesto en el código ((struct in_addr *)(Host->h_addr))->s_addr

Una vez rellena esta compleja estructura, ya podemos enviar el mensaje al servidor, igual que hicimos en él, con el mensaje sendto().

Recibir un mensaje del servidor

Es exactamente igual que en el servidor para recibir mensajes, con la funcion recvfrom(). La estructura sockaddr no hace falta rellenarla, ya que nos la rellenará la función con los datos del servidor.

EL EJEMPLO

Con todo esto, vamos al ejemplo. Vamos a hacer un servidor que espera recibir un entero de los clientes, incrementa dicho entero y se lo devuelve incrementado. Los fuentes de este ejemplo son Servidor.c y Cliente.c.

En nuestro ejemplo usamos la mini-librería que construimos para el uso de sockets. Si quieres probar el ejemplo, debes:

  1. Bajarte la librería ChSocket.tar.gz en un directorio, descomprimirla y compilarla desde una shell de linux con
    $ make 
  2. Bajarte los fuentes  Makefile, Servidor.c y Cliente.c en otro directorio distinto del de la librería y quitarles la extensión .txt
  3. Cambiar la línea del Makefile que pone
    DIRLIBSOCKET=../LIBRERIA
    y poner tú el directorio donde te hayas bajado y compilado la librería.
    DIRLIBSOCKET=<<MiDirectorioDeLibreria>>
  4. Compilar el ejemplo desde una shell de linux con
    $ make

En una shell de unix puesta en el directorio donde has compilado estos fuentes, puedes ejecutar el servidor con ./Servidor. Desde otras shells puedes arrancar clientes con ./Cliente. El cliente enviará un número aleatorio entre 0 y 19 al servidor y este se lo devolverá incrementado en 1.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: