Guía Beej de Programación en redes

Anterior


Siguiente


8. Preguntas más comunes (QnA)

Q: ¿De dónde saco esos archivos de cabecera?
Q: ¿Qué hago cuando bind() responde "Address already in use" [La dirección ya se está usando]?
Q: ¿Cómo puedo obtener una lista de los sockets abiertos en el sistema?
Q: ¿Cómo se ve la tabla de encaminamiento?
Q: ¿Cómo puedo ejecutar el servidor y el cliente si solamente tengo un ordenador? ¿No necesito una red para escribir programas de redes?
Q: ¿Cómo puedo saber si el sistema remoto ha cerrado la conexión?
Q: ¿Cómo se implementa la utilidad "ping"? ¿Qué es ICMP? ¿Dónde puedo averiguar más cosas sobre los sockets puros [raw sockets]?
Q: ¿Cómo compilo sobre Windows ?
Q: ¿Cómo compilo sobre Solaris/SunOS? ¡Todavía tengo errores al enlazar!
Q: ¿Porque termina select() al recibir una señal?
Q: ¿Cómo puedo implementar un temporizador [timeout] en una llamada a recv() ?
Q: ¿Cómo puedo comprimir o encriptar los datos antes the enviarlos al socket?
Q: ¿Qué es eso de " PF_INET" tiene algo que ver con AF_INET ?
Q: ¿Cómo puedo escribir un servidor que acepte comandos de shell de un cliente y los ejecute?
Q: ¿Estoy enviando un montón de datos, pero cuando hago recv(), sólo recibo 536 bytes ó 1460 bytes a la vez. Sin embargo, si ejecuto en mi máquina local recibo todos los datos al mismo tiempo. ¿Qué pasa?
Q: Yo uso Windows y no tengo la llamada al sistema fork() ni ningún tipo de estructura struct sigaction. ¿Qué hago?

Q: ¿De dónde saco esos archivos de cabecera?

A: Si no están ya en tu sistema, probablemente no los necesitas. Consulta el manual de tu plataforma. Si estás compilando en Windows sólo necesitas incluir #include <winsock.h>.

Q: ¿Qué hago cundo bind() responde "Address already in use" [La dirección ya se está usando]?

A: Tienes que usar setsockopt() con la opción SO_REUSEADDR sobre el socket en el que estás escuchando ( listen() ). Consulta la sección sobre bind() y la secció sobre select() para ver un ejemplo.

Q: ¿Cómo puedo obtener una lista de los sockets abiertos en el sistema?

A: Usa netstat . Revisa la página man para conocer a fondo los detalles, pero deberías tener un resultado aceptable con sólo teclear:

$ netstat

El único truco consiste en averiguar qué socket está asociado con qué programa. :-)

Q: ¿Cómo se ve la tabla de encaminamiento?

A: Usa el comando route (En la mayoría de Linuxes lo encontrarás en /sbin) o el comando netstat -r.

Q: ¿Cómo puedo ejecutar el servidor y el cliente si solamente tengo un ordenador? ¿No necesito una red para escribir programas de redes?

A: Afortunadamente para ti, no. Virtualmente todas las máquinas implementan un "dispositivo" de red de cierre de circuito [loopback] que reside en el núcleo y simula ser una tarjeta de red. (Este es el interfaz que se lista como "lo" en la tabla de encaminamiento.)

Supón que has iniciado sesión en una máquina que se llama "goat". Ejecuta el cliente en una ventana y el servidor en otra. O inicia el servidor en segundo plano [background] (usando "&") y ejecuta el cliente en la misma ventana. Como consecuencia del dispositivo de cierre de circuito puedes sencillamente ejecutar cliente goat o cliente localhost (puesto que  "localhost", muy probablemente, está definido en tú fichero /etc/hosts ) y tu cliente estará hablando de inmediato con el servidor.

En resumen, ¡no es necesario cambiar nada para que el código funcione en una sola máquina sin conexión a la red! ¡Magnífico!
In short, no changes are necessary to any of the code to make it run on a single non-networked machine! Huzzah!

Q: ¿Cómo puedo saber si el sistema remoto ha cerrado la conexión?

A: Lo sabes porque recv() devuelve 0.

Q: ¿Cómo se implementa la utilidad "ping"? ¿Qué es ICMP? ¿Dónde puedo averiguar más cosas sobre los sockets puros [raw sockets]?

A: Todas tus dudas referidas a   sockets puros tienen respuesta en los libros de programación UNIX en redes de W. Richard Stevens. Consulta la sección de libros de esta guía.

Q: ¿Cómo compilo sobre Windows ?

A: Empieza borrando Windows e instalando en su lugar Linux o BSD. };-) . No, en realidad sólo has de seguir las recomendaciones de la sección sobre  compilación en Windows de la introducción.

Q: ¿Cómo compilo sobre Solaris/SunOS ? ¡Todavía tengo errores al enlazar!

A: Los errores de enlace sucenden porque las máquinas Sun no enlazan automáticamente las bibliotecas de sockets. Consulta la sección sobre compilación en Solaris/SunOS de la introducción, donde hallarás un ejemplo de cómo hacerlo.

Q: ¿Por qué termina select() al recibir una señal?

A: Las señales suelen causar que las llamadas al sistema que están bloqueadas devuelvan el valor -1 con errno establecifo a EINTR. Cuando preparas un manejador de señales con sigaction() , puedes establecer el indicador SA_RESTART, que se supone que reiniciará la llamada al sistema que fue interrumpida.

Naturalmente, esto no siempre funciona.

Mi solución favorita a esto, requiere el uso de una sentencia goto. Sabes que esto irrita a tus profesores sobremanera, así que ¡al ataque!



select_restart:
    if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) {
        if (errno == EINTR) {
            // Alguna señal nos ha interrumpido, así que regresemos a select()
            goto select_restart;
        }
        // Los errores reales de select() se manejan aquí:
        perror("select");
    } 

Por supuesto que, en este caso, no necesitas una sentencia goto; puedes usar otras estructuras de control. Pero creo que goto es una solución más limpia.

Q: ¿Cómo puedo implementar un temporizador [timeout] en una llamada a recv()?

A: ¡Usa select() ! Te permite indicar un parámetro de control de tiempo sobre los descriptores de socket en los que esperas leer. También puedes implementar toda esa funcionalidad en una única función como esta:



#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
int recvtimeout(int s, char *buf, int len, int timeout)
{
    fd_set fds;
    int n;
    struct timeval tv;
    // Construir el conjunto de descriptores de fichero
    FD_ZERO(&fds);
    FD_SET(s, &fds);
    // Construir la estructura timeval del temporizador
    tv.tv_sec = timeout;
    tv.tv_usec = 0;
    // Esperar hasta que se reciban datos o venza el temporizador
    n = select(s+1, &fds, NULL, NULL, &tv);
    if (n == 0) return -2; // ¡El temporizador ha vencido!
    if (n == -1) return -1; // error
    // Los datos deben estar ahí, así que llamo normalmente a recv()
    return recv(s, buf, len, 0);
}
// Ejemplo de llamada a recvtimeout():
    .
    .
    n = recvtimeout(s, buf, sizeof(buf), 10); // 10 segundos de espera
    if (n == -1) {
        // ocurrió un error
        perror("recvtimeout");
    }
    else if (n == -2) {
        // El temporizador venció
    } else {
        // Hay datos en el buffer
    }
    .
    . 

Nota que recvtimeout() devuelve -2 en caso de que venza el temporizador. ¿Por qué no devolver 0? Bueno, si recuerdas un valor de retorno de 0 para una llamada a recv() significa que la máquina remota ha cerrado la conexión. Así que ese valor de retorno ya tiene un significado. El valor -1 significa "error", así que escogí -2 como mi indicador de temporizador vencido.

Q: ¿Cómo puedo comprimir o encriptar los datos antes de enviarlos al socket?

A: Una forma sencilla de encriptar es usar SSL (secure sockets layer), pero eso va más allá del alcance de esta guía.

Pero suponiendo que quieres implementar tu propio sistema de compresión o encriptado, sólo es cuestión de pensar que tus datos siguen una secuencia de etapas entre ambos extremos. Cada paso altera los datos en cierta manera.

  1. El servidor lee datos de un fichero (o de donde sea)

  2. El servidor encripta los datos (tú añades esta parte)

  3. El servidor envía ( send() ) los datos encriptados

Y ahora el camino inverso:

  1. El cliente recibe ( recv() ) los datos encriptados

  2. El cleinte desencripta los datos (tú añades esta parte)

  3. El cliente escribe los datos en un archivo (o donde sea)

Podrías comprimir los datos en el punto en que arriba se encritan/desencriptan los datos. ¡O podrías hacer las dos cosas!. Sencillamente recuerda comprimir antes de encriptar. :)

En la medida en que el cliente deshaga correctamente lo que el servidor haga, los datos llegarán correctamente al final, sin importar cuántos pasos intermedios se añadan.

Así que todo lo que necesitas para usar mi código es encontrar el lugar situado entre la lectura y el envío ( send() ) de los datos, y poner allí el código que se encarga de realizar el encriptado.

Q: ¿Qué es eso de " PF_INET" ? ¿Tiene algo que ver con AF_INET ?

A: Por supuesto que sí. Revisa la sección socket() para los detalles.

Q: ¿Cómo puedo escribir un servidor que acepte comandos de shell de un cliente y los ejecute?

A: Para simplificar las cosas, digamos que el cliente conecta ( connect() ), envía ( send() ), y cierra ( close() ) la conexión (es decir no hay llamadas al sistema posteriores sin que el cliente vuelva a conectar.)

El proceso que sigue el cliente es este:

  1. connect() con el servidor

  2. send("/sbin/ls > /tmp/client.out")

  3. close() la conexión

Mientras tanto el servidor procesa los datos de esta manera:

  1. accept() la conexión del cliente

  2. recv(str) el comando a ejecutar

  3. close() la conexión

  4. system(str) ejecuta el comando

¡Cuidado! Dejar que el servidor ejecute lo que el cliente dice es como dar acceso remoto al shell y la gente podría hacer cosas no deseadas con tu cuenta cuando se conectan al servidor. Por ejemplo, en el ejemplo anterior, ¿qué pasa si el cliente envía "rm -rf * "? Borra todo lo que tengas en tu cuenta, ¡eso es lo que pasa!

Así que aprendes la lección e impides que el cliente use cualquier comando, a excepción de un par de utilidades que sabes que son seguras, como la utilidad  foobar :



    if (!strcmp(str, "foobar")) {
        sprintf(sysstr, "%s > /tmp/server.out", str);
        system(sysstr);
    } 

Desgraciadamente, todavía no estás seguro: ¿qué pasa si el cliente envía "foobar; rm -rf ~ "? Lo mejor que se puede hacer es escribir una pequeña rutina que ponga un carácter de escape("\") delante de cualquier carácter no alfanumérico (incluyendo espacios, si es necesario) en los argumentos para el comando.

Como puedes ver, la seguridad es un problema bastante importante cuando el servidor comienza a ejecutar cosas que el cliente envía.

Q: ¿Estoy enviando un montón de datos, pero cuando hago recv(), sólo recibo 536 bytes ó 1460 bytes a la vez. Sin embargo, si ejecuto en mi máquina local recibo todos los datos al mismo tiempo. ¿Qué pasa?

A: Te estás tropezando con la MTU (Maximum Transfer Unit)--El tamaño máximo de paquete que el medio físico puede manejar. En la máquina local estás usando el dispositivo de cierre de circuito [loopback] que puede manejar sin problemas 8K o más. Pero en una Ethernet, que sólo puede manejar 1500  bytes con una cabecera, tienes que plegarte a ese límite. Sobre un módem, con una MTU de 576 (con cabecera), has de plegarte a un límite aún menor.

En primer lugar tienes que asegurarte de que todos los datos se están enviando. (Revisa la implementación de la función  sendall() para ver los detalles). Una vez que estás seguro de eso, tienes que llamar a recv() en un bucle hasta que todos los datos se hayan leído.

Revisa la sección Consecuencias de la encapsulación de datos para los detalles acerca de como recibir paquetes completos de datos usando múltiples llamadas a recv().

Q: Yo uso Windows y no tengo la llamada al sistema fork() ni nigún tipo de estructura struct sigaction . ¿Qué hago?

A: De estar en algún sitio estarán en las bibliotecas POSIX que quizás hayan venido con tu compilador. Como no tengo Windows, en realidad no puedo darte la respuesta, pero creo recordar que Microsoft tiene una capa de compatibilidad POSIX , y ahí es donde fork() tendría que estar (y a lo mejor también sigaction.)

Busca "fork" o "POSIX" en la ayuda de VC++, por si te da alguna pista.

Si no hay forma de que funcione, olvídate de fork()/sigaction y usa en su lugar la función equivalente de Win32: CreateProcess(). No sé cómo se usa CreateProcess() --Tiene tropecientos argumentos, pero seguramente está explicada en la ayuda de VC++
If that doesn't work at all, ditch the fork()/ sigaction stuff and replace it with the Win32 equivalent: CreateProcess() . I don't know how to use CreateProcess() --it takes a bazillion arguments, but it should be covered in the docs that came with VC++.


Anterior

Inicio

Siguiente

Referencias adicionales


Declinación de responsabilidad y ayuda