Skip to content

Instantly share code, notes, and snippets.

@iago64
Last active May 20, 2019 22:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iago64/9f6acf5959846dc2a0f548ae3af7d34d to your computer and use it in GitHub Desktop.
Save iago64/9f6acf5959846dc2a0f548ae3af7d34d to your computer and use it in GitHub Desktop.
Ejemplos de Select

UNA FORMA SIMPLE DE USAR SELECT

Este artículo se concentra en explicar el uso de la llamada "select()" de manera práctica. Con ejemplos simples de su uso, con código fácil de entender, y con ejemplos que buscan guiar al lector a entender su uso, ventajas, desventajas e incluso ayudarle a modificar/depurar código existente.

Los primeros dos ejemplos son muy simples e intentan mostrar el uso de select() con sólo un descriptor. El objetivo es que el lector obtenga una inducción acerca de la llamada y algunos de sus parámetros.

Primero, se debe tener un descriptor de archivo abierto. Como esta es la sección de Networking del foro, asumo que tenemos un socket de tipo SOCK_STREAM que YA está conectado.

PRIMER EJEMPLO: El primer ejemplo espera indefinidamente hasta que hay datos listos para ser leídos del socket.

    int socket_fd, result;
    fd_set readset;
    ...
    /* Socket has been created and connected to the other party */
    ...
    
    /* Call select() */
    do {
       FD_ZERO(&readset);
       FD_SET(socket_fd, &readset);
       result = select(socket_fd + 1, &readset, NULL, NULL, NULL);
    } while (result == -1 && errno == EINTR);
    
    if (result > 0) {
       if (FD_ISSET(socket_fd, &readset)) {
          /* The socket_fd has data available to be read */
          result = recv(socket_fd, some_buffer, some_length, 0);
          if (result == 0) {
             /* This means the other side closed the socket */
             close(socket_fd);
          }
          else {
             /* I leave this part to your own implementation */
          }
       }
    }
    else if (result < 0) {
       /* An error ocurred, just print it to stdout */
       printf("Error on select(): %s\", strerror(errno));
    }

La explicación: Primero, lleno una estructura fd_set con la información del(los) sockets que quiero monitorear. En este caso, primero "limpio" los bits de la estructura, luego enciendo el bit correspondiente al socket. Esta operación se hace con las macros FD_ZERO y FD_SET.

Luego uso la función select() para monitorear la disponibilidad de datos en el socket.

El primer parámetro de select() es el valor máximo de los descriptores en la estructura (o en cualquiera de ellas, ya que select() recibe hasta 3 estructuras fd_set) MAS UNO. Por ejemplo, si se tienen 20 descriptores en la estructura y el valor más grande de los descriptores es 123, entonces el valor que se pasa como primer parámetro será 124.

Las siguientes líneas simplemente chequean el valor retornado por select() y hacen lo necesario para cada caso.

select() puede retornar cualquiera de los siguientes valores: -1, 0, > 0 -1: Significa que se encontró un error que se debe controlar al nivel de la aplicación. En el código simplemente se imprime la descripción del error. 0: Quiere decir que se terminó el tiempo especificado a select() (TIMEOUT)

0: Es el número de sockets que están disponibles para lectura, escritura, o que tienen excepciones.

P & R: Para que es el ciclo "do { ... } while()" que escribió alrededor de select()? Lo utilizo para que se reinicie select() cuando se presente un error de tipo EINTR. La página "man" de select() dice que los valores de las estructuras y del timeout quedan indefinidas después de un error, por lo que no se debe confiar en su contenido. Es por eso que la inicialización de readset se hace dentro del ciclo.

Por qué usa un ciclo "do ... while()" y no uno "while(...){}" ? Es mi estilo de programación...

Por qué no usa otra estructura fd_set y copia de la original a la temporal como he visto en muchos otros programas? No sean impacientes. Más adelante se verá esa forma de hacerlo...

Pero es lo mismo que poner una línea con "recv()" dentro de un ciclo... Por qué hacer tanto código para lo miso? Por motivo de demostración. Más adelante haré más compleja la cosa!

Por qué no chequeo un resultado de 0 de select()? Porque no pasé un valor de time out, o sí?

SEGUNDO EJEMPLO: En este ejemplo, trato de hacer algo más útil que el ejemplo anterior, complicando un poco el código: Una función recv() con time out. Cómo se supone que debe ser útil, el código quedará encapsulado en una función.

    /*
       Params:
          fd       -  (int) socket file descriptor
          buffer - (char*) buffer to hold data
          len     - (int) maximum number of bytes to recv()
          flags   - (int) flags (as the fourth param to recv() )
          to       - (int) timeout in milliseconds
       Results:
          int      - The same as recv, but -2 == TIMEOUT
       Notes:
          You can only use it on file descriptors that are sockets!
          'to' must be different to 0
          'buffer' must not be NULL and must point to enough memory to hold at least 'len' bytes
          I WILL mix the C and C++ commenting styles...
    */
    int recv_to(int fd, char *buffer, int len, int flags, int to) {
    
       fd_set readset;
       int result, iof = -1;
       struct timeval tv;
    
       // Initialize the set
       FD_ZERO(&readset);
       FD_SET(fd, &readset);
       
       // Initialize time out struct
       tv.tv_sec = 0;
       tv.tv_usec = to * 1000;
       // select()
       result = select(fd+1, &tempset, NULL, NULL, &tv);
    
       // Check status
       if (result < 0)
          return -1;
       else if (result > 0 && FD_ISSET(fd, &tempset)) {
          // Set non-blocking mode
          if ((iof = fcntl(fd, F_GETFL, 0)) != -1)
             fcntl(fd, F_SETFL, iof | O_NONBLOCK);
          // receive
          result = recv(fd, buffer, len, flags);
          // set as before
          if (iof != -1)
             fcntl(fd, F_SETFL, iof);
          return result;
       }
       return -2;
    }

Esta función se supone como un reemplazo a recv(), sólo recibe un parámetro extra. Esto significa que la función:

  • puede ser interrumpida por una señal
  • puede retornar -1, 0 o >0
  • puede copiar al buffer menos datos de los que se le pidió

Además, la función puede retornar -2, que significa que la operación hizo time out. Pensé en retornar -1 y asignar ETIMEDOUT a errno, pero este último es un error de red, mientras que el time out de select() es una condición que se puede esperar no un error.

La función cra un fd_set que luego inicializa y llena con el descriptor del socket. Luego asigna el valor de time out a la estructura timeval. Y después usa select() para chequear la disponibilidad de datos.

La función puede fallar quí por dos razones: -1=algún error, incluyendo EINTR; y 0 time out, en este caso retorno -2 (recuerden que recv()==0 significa que se cerró el socket)

Si hay datos por recibir entonces llamo a recv(). Antes de hacerlo cambio a modo no bloqueante el socket, y después de recv() lo vuelvo a dejar como estaba. Esto quiere decir que la función puede retornar errno=EAGAIN/EWOULDBLOCK.

Gracias a Rob Seace por la ayuda sobre esto

P & R:

Por qué cambia el modo del socket a non-blocking antes de llamara a recv()? Bueno, como Rob me hizo caer en cuenta, la llamada a recv() puede bloquearse incluso cuando el socket haya sido anunciado por select() con datos. En programas de múltiples hilos (multithreaded), es usual que más de un thread ejecute recv() sobre el mismo descriptor. Sólo me lavo las manos...

Por qué no pone select() dentro de un ciclo do ... while() como hizo en el ejemplo anterior? Lo pensé (e incluso originalmente lo escribí así), pero la idea es tener el comportamiento similar al de recv(). Lo único diferente es que puede retornar -1 con errno == EWOULDBLOCK incluso cuando el socket entregado no esté en modo non-blocking.

No le veo utilidad a este código. Es casi lo mismo que hay en las páginas "man" - no me sirve de nada Intenté hacerlo fácil de entender. El código que sigue de aquí en adelante será cada vez más complejo, y seguramente servirá un poco más.

UNA FORMA NO TAN SIMPLE - PERO MÁS ÚTIL - DE USAR SELECT

En esta parte, intentaré mostrar como se usa select() para chequear disponibilidad de datos para lectura en más de un descriptor de archivo al mismo tiempo. NOTA: Sirve para cualquier tipo de descriptor de archivo.

TERCER EJEMPLO:

Asumo que tengo un arreglo/vector de enteros (int) (el vector contiene los descriptores de archivos), y que voy a chequear disponibilidad de lectura en todos ellos al mismo tiempo.

    int myfds[N], maxfd, j;
    fd_set readset;
    ...
    // Initialize the set
    FD_ZERO(&readset);
    maxfd = 0;
    for (j=0; j<N; j++) {
       FD_SET(myfds[j], &readset);
       maxfd = (maxfd>myfds[j])?maxfd:myfds[j];
    }
    
    // Now, check for readability
    result = select(maxfd+1, &readset, NULL, NULL, NULL);
    if (result == -1) {
       // Some error...
    }
    else {
       for (j=0; j<N; j++) {
          if (FD_ISSET(myfds[j], &readset)) {
             // myfds[j] is readable
          }
       }
    }

No estuvo tan mal, cierto? Bueno, de hecho este es el núcleo de casi todos los procesos que usan select() con el mismo objetivo.

No hay mucho que decir sobre el código, excepto que puede haber más de un descriptor en el fd_set de salida, y por ello es que uso un ciclo después de chequear el resultado de select().

P & R: Para qué es eso de (maxfd>myfds[j])? Es sólo para guardar el mayor valor de todos los descriptores. El primer parámetro de select() necesita este valor.

No me ayuda mucho. Necesito hacer un servidor de "echo" para mi clase de "Introducción a redes y comunicaciones", y el código no es muy útil... Si, lo sé. Pero si realmente quieren aprender (a excepción de mí, Albert Einstein y otro tipo que no recuerdo), deben empezar por cosas sencillas y moverse a tareas más difíciles a medida que vayan aprendiendo...

Y ahora, para los que no quieren esperar más...

CUARTO EJEMPLO:

En este ejemplo final (espero), mostraré parte del código de un servidor de eco (echo server). El servidor espera nuevas conexiones (en algún puerto TCP) y se mantiene referencia a cada nueva conexión en una estructura fd_set. Cuando hay datos disponibles en uno o más de los sockets, recibe los datos y hace eco de ellos a través del socket donde los recibió...

NO usuaré sockets en modo non-blocking. Sólo chequearé los errores básicos e intentaré mantener el código muy simple.

Este es el código:

    fd_set readset, tempset;
    int maxfd, flags;
    int srvsock, peersoc, j, result, result1, sent, len;
    timeval tv;
    char buffer[MAX_BUFFER_SIZE+1];
    sockaddr_in addr;
    
    /* Here should go the code to create the server socket bind it to a port and call listen
        srvsock = socket(...);
        bind(srvsock ...);
        listen(srvsock ...);
    */
    
    FD_ZERO(&readset);
    FD_SET(srvsock, &readset);
    maxfd = srvsock;
    
    do {
       memcpy(&tempset, &readset, sizeof(tempset));
       tv.tv_sec = 30;
       tv.tv_usec = 0;
       result = select(maxfd + 1, &tempset, NULL, NULL, &tv);
    
       if (result == 0) {
          printf("select() timed out!\n");
       }
       else if (result < 0 && errno != EINTR) {
          printf("Error in select(): %s\n", strerror(errno));
       }
       else if (result > 0) {
    
          if (FD_ISSET(srvsock, &tempset)) {
             len = sizeof(addr);
             peersock = accept(srvsock, &addr, &len);
             if (peersock < 0) {
                printf("Error in accept(): %s\n", strerror(errno));
             }
             else {
                FD_SET(peersock, &readset);
                maxfd = (maxfd < peersock)?peersock:maxfd;
             }
             FD_CLR(srvsock, &tempsock);
          }
    
          for (j=0; j<maxfd+1; j++) {
             if (FD_ISSET(j, &tempset)) {
    
                do {
                   result = recv(j, buffer, MAX_BUFFER_SIZE, 0);
                } while (result == -1 && errno == EINTR);
    
                if (result > 0) {
                   buffer[result] = 0;
                   printf("Echoing: %s\n", buffer);
                   sent = 0;
    
                   do {
                      result1 = send(j, buffer+sent, result-sent, MSG_NOSIGNAL);
                      if (result1 > 0)
                         sent += result1;
                      else if (result1 < 0 && errno != EINTR);
                         break;
                    } while (result > sent);
    
                }
                else if (result == 0) {
                   close(j);
                   FD_CLR(j, &readset);
                }
                else {
                   printf("Error in recv(): %s\n", strerror(errno));
                }
             }      // end if (FD_ISSET(j, &tempset))
          }      // end for (j=0;...)
       }      // end else if (result > 0)
    } while (1);	

Explicación: Este fué mas complejo, no les parece? El código inicializa un fd_set con el descriptor srvsock (que es el socket utilizado para recibir nuevas conexiones), y asigna el valor del descriptor a 'maxfd'.

El ciclo llama a select() con una copia del fd_set y con un valor de timeout de 30 segundos. Una vez select() retorna que hay algún socket disponible para lectura, se verifica si es 'srvsock' el que está "encendido" en el fd_set. Si es así, significa que hay una nueva conexión disponible, así que simplemente se llama a accept() para aceptarla. Esta parte puede ser problemática ya que la conexión puede estar o no disponible después de haber retornado de select() por lo que accept() podría bloquear la ejecución del programa hast aque haya una nueva conexión. El uso de sockets en modo non-blocking sería útil en este caso.

Una vez el nuevo socket es retornado, se adiciona a la variable 'readset' utilizando FD_SET(), y 'maxfd' es modificado dependiendo del nuevo valor. Luego, el descriptor srvsock es eliminado de 'tempset' para evitar problemas en el ciclo que sigue después.

Después de dicha verificación, se entra en un ciclo 'for' desde 0 hasta maxfd (inclusive) y se chequea cada valor de la iteración en 'tempset'. Si un valor está en 'tempset' significa que dicho socket tiene datos para leer, y se llama a recv() con ese socket. Por ello es importante antes del ciclo eliminar a srvsock de 'tempset' ya que no queremos llamar a recv() en ese socket!

Cuando recv() retorna datos (copia los datos al buffer proveído), estos son envíados al mismo socket de forma que se hace un eco de lo recibido.

Si recv() retorna 0 significa que el socket fué cerrado, en tal caso, se elimina el socket de 'readset' y la operación continúa.

En la siguiente iteración del ciclo externo (el ciclo infinito), el contenido de 'readset' (incluyendo las nuevas conexiones, y eliminando los sockets que se han cerrado) se copia a 'tempset', llevando así registro de las conexiones activas en 'readset'.

Espero que les haya gustado... Aún es inmaduro el código, y necesita (mucho) trabajo para que sea a prueba de balas.

P & R:

NO veo ningún arreglo con los sockets. Cómo hace para llevar el registro de las conexiones? Bueno, esa parte es un poco díficil de encontrar para los principiantes, yo también pensé lo mismo hace algún tiempo, hasta que caí en cuenta que:

  1. Los descriptores de archivos (y de sockets obviamente) son simplemente enteros (ints)
  2. La estructura fd_set es algún tipo de campo de bit que puede ser accedido a través de las operaciones: FD_SET(), FD_CLR(), FD_ISSET(), FD_ZERO(), etc...

Así que lo único necesario es utilizar la funcionalidad de campo de bits para llevar el registro de conexiones activas. Es más fácil de llevar el registro de esta manera porque además es necesario usar un fd_set al utilizar select(), por lo que es simplemente necesario copiar de una a la otra variable.

Cuando debo usar select(), procesos dedicados por conexión o threads dedicados por conexión? Bueno, no es fácil de contestar... Creo que discutir ese tema en este mensaje no es bueno (puede volverse muy larga la discusión), sin embargo, puedo explicar cuando select() es una buena opción:

  1. Tiene muchas, muchas conexiones que no necesitan mucho tiempo de procesamiento desde que se reciben los datos hasta que se da la respuesta, por ello se puede mantener un buen tiempo de respuesta para todas las conexiones.
  2. El servidor actúa como un punto central, distribuyendo mensajes o información procesada entre las conexiones que tiene. Por ejemplo, un servidor de chat sería más fácil de programar usando select() que con threads, y muchísimo más fácil que con procesos dedicados (tendrías que comunicar procesos entre sí...)
  3. El tiempo de procesamiento para dar una respuesta a un cliente es largo para cada mensaje recibido (contrario a lo expuesto en el primer punto), pero se puede delegar la tarea a otro proceso, usando sockets o pipes o cualquier tipo de IPC que permita select() (por ejemplo con System V IPC no es posible), de tal forma que el procesamiento de cada mensaje no tomaría mucho tiempo. (De hecho el procesamiento si tomaría mucho tiempo en total, pero desde el punto de vista del proceso que recibe y envía respuestas no ya que lo delega y continúa procesando otros)

Después de leer esto, concluyo que se podría siempre usar select(), a menos que tenga restricciones especiales que hagan su uso "incómodo", o que hagan otro tipo de solución más fácil de implementar, depurar y/o entender.

Offline

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment