Home UC3M
Home IT
Home / Docencia / Ing. de Telecomunicación / Redes de Ordenadores / Prácticas de sockets III  
anterior


MATERIAL DE APOYO

 

Páginas del manual on-line: socket(2), send(2), recv(2), read(2), write(2), setsockopt(2), fcntl(2), select(2), tcp(7), ip(7), sigaction(2).

Manual de sockets

Guía Beej de Programación en Redes (manual on-line)

Manual de tcpdump

Capítulos 6, 7 y 8 de "Linux Socket Programming" de Sean Walton, Sams Publishing Co. 2001

 

 PRÁCTICAS DE SOCKETS

 

Las prácticas de sockets están organizadas en tres partes:
  1. Servidores secuenciales (servidor y cliente de eco, manipulación de opciones de sockets, análisis con tcpdump, servidor de ficheros).
  2. Servidores concurrentes (procesos, hilos, manejadores de señales).
  3. Entrada/salida bloqueante y no bloqueante(poll, select)

Entrada/salida bloqueante y no bloqueante

Las operaciones de entrada/salida son por lo general bloqueantes y esto también ocurre cuando se utilizan con sockets. Esto implica que cuando se realiza alguna de estas operaciones sobre un socket, el proceso pasa al estado dormido, esperando que se satisfaga alguna condición que permita que se complete la operación. Así:

  • Si realizamos operaciones de entrada (read, recv, recvfrom,...) sobre un socket TCP y no hay datos disponibles en el buffer de recepción del socket, el proceso pasa a estado dormido hasta que lleguen datos.
  • Si realizamos operaciones de salida (write, send, sendto,...)
  • sobre un socket TCP, cuando nosotros realizamos esta llamada, el kernel copia los datos del buffer de la aplicación en el buffer de envío del socket, si no queda espacio en este buffer, el proceso se bloquea hasta que haya suficiente espacio.

En muchas ocasiones es recomendable emplear algún mecanismo que nos permita realizar estas operaciones de forma no bloqueante y así poder realizar otras tareas en vez de esperar a que los datos estén disponibles. En esta práctica vamos a ver algunos de los mecanismos que existen para poder realizar operaciones de entrada/salida no bloqueantes sobre sockets, en concreto, los mecanismos de polling y los asíncronos.

La función fcntl() y los mecanismos de polling

La función fcntl() es una función de control que nos permite realizar diferentes operaciones sobre descriptores (de ficheros, de sockets,...) en general. El prototipo de la función es el siguiente:

  #include <fcntl.h>

  int fcntl(int fd, int cmd, /* int arg*/);

Cada descriptor tiene asociado un conjunto de flags que nos permiten saber o conocer ciertas características del descriptor. Para obtener el valor de estos flags, se realiza una llamada a fcntl() con el parámetro cmd al valor F_GETFL. De un modo similar cuando queremos modificar el valor de los flags, se utiliza el valor F_SETFL.

Se recomienda ver detalladamente el uso de esta función leyendo la página del manual fcntl(2).

Para indicar que las operaciones de entrada y salida sobre un socket no sean bloqueantes, es necesario activar el flag O_NONBLOCK en el descriptor del socket. El código necesario para ello es el siguiente:

  // sd es el descriptor del socket

  if ( fcntl(sd, F_SETFL, O_NONBLOCK) < 0 ) 
    perror("fcntl: no se puede fijar operaciones no bloqueantes");

De esta forma ya sabemos cómo activar que las operaciones de lectura y escritura no sean bloqueantes, pero ¿cómo sabemos cuando están los datos disponibles?:

Cuando el socket no es bloqueante al realizar una operación de lectura o escritura, si ésta no se puede completar, la llamada devuelve un error (-1) y le asignará a la variable errno el valor EWOULDBLOCK (de todas formas, recordad, que es necesario comprobar el número de bytes que devuelven estas llamadas, porque no siempre coincide con el número de bytes que queriamos leer o escribir). Así, para saber cuando existen datos disponibles se suele utilizar un mecanismos de "encuesta", denominado (polling), en que se consulta continuamente cuando existen datos disponibles y si no los hay, se realizan otras tareas.

  1. Utilizando el código de la práctica 2 (psockets2.tgz) modificad el cliente para que se puedan realizar operaciones de entrada/salida no bloqueantes y emplead un mecanismo de polling para saber si existen datos disponibles cuando se realizan operaciones de lectura.

    Para ver este comportamiento, haced que se imprima en pantalla un contador, que se incrementa cada vez que el programa tiene que esperar por lo datos que devuelve el servidor (operación de lectura). Compilad el código y ejecutadlo utilizando el servidor de eco de la práctica anterior.

    Nota: para ver mejor la diferencia con el caso de sockets no bloqueantes, eliminad del cliente anterior la llamada a fcntl y comprobad que no se incrementa el contador antes de recibir los datos.

Mecanismos asíncronos utilizando señales

La señal SIGIO se genera cuando cambia el estado de un socket, por ejemplo:

  • Existen nuevos datos disponibles en el buffer de recepción o se ha liberado espacio en el buffer de envío y por lo tanto, podemos realizar nuevas operaciones de escritura.
  • Existen nuevos clientes que se quieren conectar.

Para que se genere la señal de SIGIO tenemos que realizar las siguientes llamadas a fcntl sobre el socket correspondiente (sd en el ejemplo):

  if ( fcntl(sd, F_SETFL, O_ASYNC | O_NONBLOCK) < 0 ) {
    perror("fcntl error");
  }

  if ( fcntl(sd, F_SETOWN, getpid()) < 0 ) {
    perror("fcntl error");
  }

Los mecanismos asíncronos utilizan esta señal para saber cuando están listos los datos en un socket y de esta forma poder realizar otras tareas mientras no se reciben datos.

Como ya hemos visto en la práctica anterior, cuando se genera una señal podemos hacer que se ejecute un manejador en el que codificamos las acciones que se deben llevar a cabo cuando la señal se produce. En el caso de la señal SIGIO realizaremos operaciones del tipo: leer datos que se encuentran disponibles en el buffer de recepción del socket, enviar los datos que tiene pendientes la aplicación, y/o aceptar nuevos clientes que quieren establecer conexiones.

  1. El código siguiente demand-accept.c es un ejemplo sencillo de un servidor que utiliza un manejador de SIGIO para detectar cuando se conectan nuevos clientes. Así cuando se genera la señal, en el manejador, se acepta la conexión con el nuevo cliente, se le envía un mensaje y se cierra la conexión. Compílelo y pruebelo con un cliente telnet (ejecutad en una shell, una vez arrancado el servidor, telnet localhost 9999).

Control de varios descriptores usando la llamada select

Normalmente a un programa servidor se conectan varios clientes simultáneamente y por ello, nuestros programas deben estar preparados para esta circunstancia. Para ello tenemos dos posibles opciones:

  • Crear un nuevo proceso o hilo por cada cliente que llegue, que es lo que hemos visto en la práctica anterior de sockets.
  • Utilizar la llamada select(), que vamos a ver ahora en detalle.

La llamada select() nos permite comprobar el estado de varios sockets al mismo tiempo. Con ella podemos saber qué sockets de los que maneja nuestro programa están listos para leer datos, para escribir datos, cuáles reciben conexiones, cuáles generan excepciones,...

El prototipo de la función select() es el siguiente:

  #include <sys/time.h> 
  #include <sys/types.h> 
  #include <unistd.h> 

  int select(int numfds, fd_set *readfds, fd_set *writefds,
             fd_set *exceptfds, struct timeval *timeout);
Los parámetros de la función son los siguientes:
  • numfds: es el valor del descriptor de socket más alto que queremos tratar más uno. Cada vez que abrimos un fichero, socket o similar, se nos da un descriptor de fichero que es un número entero. Estos descriptores suelen tener valores consecutivos.


  • readfds: es un puntero a los descriptores de los que nos interesa saber si hay algún dato disponible para leer o que queremos que nos avisen cuando los haya. También se nos avisará cuando haya un nuevo cliente o cuando un cliente cierre la conexión.


  • writefds: es un puntero a los descriptores de los que nos interesa saber si podemos escribir en ellos sin peligro. Si en el otro lado han cerrado la conexión e intentamos escribir, se nos enviará una señal SIGPIPE.


  • exceptfds: es un puntero a los descriptores de los que nos interesa saber si ha ocurrido alguna excepción.


  • timeout: es el tiempo que queremos esperar como máximo. Si pasamos NULL, nos quedaremos bloqueados en la llamada a select() hasta que suceda algo en alguno de los descriptores. Se puede poner un tiempo cero si únicamente queremos saber si hay algo en algún descriptor, sin quedarnos bloqueados.


select() nos devuelve: -1 en caso de error (ver errno), 0 si venció el temporizador, y en caso de éxito nos devuelve un número mayor que cero (el número de descriptores en los conjuntos de descriptores).

fd_set es el tipo de los conjuntos de descriptores, y las variables de este tipo se manipulan con unas macros. Si suponemos que hemos definido un conjunto
fd_set set;
  • FD_ZERO(&set) inicializa (y borra) el conjunto de descriptores.
  • FD_SET(fd, &set) añade un nuevo descriptor al conjunto
  • FD_CLR(fd,&set) quita un descriptor del conjunto.
  • FD_ISSET(fd,&set) devuelve mayor que cero si el descriptor fd se encuentra en el conjunto. Es la función que utilizamos para saber si después de una llamada a select() hay datos listos en el descriptor fd.
  1. En smart-select.c puede observar un servidor que utiliza select para atender a los clientes. El servidor prelanza cinco procesos (MAXPROCESSES) de eco, de forma que todos ellos aceptan conexiones de clientes de forma simultánea en el mismo socket. Cada servidor tiene sus propios clientes de eco, y la forma de diferenciar si es una nueva petición o si llegan datos de alguno de los clientes se hace a través de select() y de la macro FD_ISSET().
Compile el ejemplo y pruebelo con varios clientes.

Localización | Personal | Docencia | Investigación | Novedades | Intranet
inicio | mapa del web | contacta