A Programar Enchufes! - El gran final

En esta ultima parte les contaremos, como prometimos, sobre conceptos de programacion de sockets mas avanzados. Hicieron la tarea, no?

Hola amigos! Antes que nada yo, Buanzo, les quiero desear un 2002 lleno de libertad, felicidad y prosperidad, como asi tambien el resto del equipo que hace Linux USERS. Mi atrasado regalo de navidad es este pequeño articulo, que sabemos que les interesa a muchos de ustedes. Llegamos a la edicion 1.5, gracias a ustedes. Y ese es el mejor regalo. Si, soy cursi, lo se. Ahora, hablemos de DNS.

Y tu direccion es...?

Todos probablemente saben que para obtener la direccion IP de un cierto dominio u host, el sistema operativo le pregunta a un servidor de nombres de dominio. Pero nosotros queremos saber como hacer esto desde C. Para este proposito, utilizamos la funcion gethostbyname(), cuyo prototipo se encuentra en netdb.h, y es asi:

struct hostent *gethostbyname(const char *name);

El unico argumento, name, es el dominio sobre el cual queremos obtener informacion. Esta funcion nos devuelve un puntero a una estructura hostent, definida asi:

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]

Los campos tienen el siguiente significado:

  • h_name: Nombre oficial del host
  • h_aliases: una lista de nombres alternativos
  • h_addrtype: Tipo de direccion (AF_INET generalmente)
  • h_length: Direccion en bytes de la direccion
  • h_addr_list: una lista de direcciones del host, en formato Network Byte
  • Order (debemos usar ntohl(), recuerdan?)
  • h_addr: la primera direccion de h_addr_list

La funcion nos devolvera NULL en caso de producirse un error. Pero atencion, errno NO es seteada, sino h_error. Esto lo manejamos con la funcion herror(), que funciona igual que perror(). (Ver Linux Users 1.4).

Ejemplo:

[...]

if ((h=gethostbyname("www.buanzo.com.ar")) == NULL) {

herror("gethostbyname");

exit(1);

}

printf("Host: %s\n", h->h_name);

printf(" IP: %s\n", inet_ntoa(*((struct in_addr*)h->h_addr)));

Arriba pueden ver un excelente ejemplo de typecasting, en inet_ntoa().

send() no me quiere!

Recuerdan que les conte que send() podia enviar menos datos de los que queremos enviar? Por ejemplo, tenemos un buffer de 512 bytes lleno, y send() devuelve 300. O sea, nos quedan 512-300=212 bytes para enviar. Estos SIGUEN en el buffer, por lo que haciendo otro send() desde esa posicion, y repitiendo hasta que send() devuelva la cantidad enviada lo solucionamos. Les ahorro el trabajo de hacerla: en USERS te da MAS les dejo una funcion llamada enviar(), que no entra aca por cuestion de espacio. Agreguenla a sus programas, y usenla exactamente igual que send(), ya que sus prototipos son iguales.

La funcion enviar() devuelve el mismo numero de bytes que queriamos enviar, de a no ser que haya habido un error. Por supuesto, errno sera seteada de manera acorde. A decir verdad, enviar() solo les resuelve una parte del problema: la de enviar todos los datos. Pero de el otro lado, el receptor NO SABE que estos datos han sido fragmentados y enviados por pedazos. Si, es un despelote. Para este problema, vamos a acudir a algo llamado encapsulamiento.

Encapsulamiento

El encapsulamiento es solamente agregarle una 'cabecera' (header) a nuestros datos. Esta cabecera es una estructura armada por nosotros dependiendo de nuestro proyecto particular. Por ejemplo, en un programa de chat, tenemos que enviar quien dijo algo y que es lo que dijo. El problema es que esas dos cosas pueden tener largos diferentes. Podriamos enviar siempre 1024 bytes, por ejemplo, pero gastariamos ancho de banda para que "noelia" solo pueda decir "hola". Por ejemplo, supongamos que el usuario tiene un ancho fijo de 8 bytes (rellenamos con ASCII 0) y el mensaje un maximo de 128 bytes, ancho variable. Usariamos una estructura asi:

  1. 1 byte: tamanio total del paquete (8+N bytes de mensaje)
  2. 8 bytes: nombre de usuario
  3. N bytes: mensaje en si

Los numeros enteros como el campo 1 (tamanio total) debe ser guardado en Network Byte Order, pero en este caso es solo un byte, asi que no importa.

Entonces, si el usuario fuera "noelia" y el mensaje "hola", el paquete seria algo asi (en hexadecimal arriba, ASCII abajo):

0C 6E 6F 65 6C 69 61 00 00 68 6F 6C 61
(largo) n o e l i a h o l a

Para recibir un paquete la cosa se complica. Debemos utilizar recv() sucesivamente hasta armar un paquete completo. Esto es facil de hacerlo, ya que tenemos la cantidad de bytes de datos. (8+4=12=0x0C). Entonces, armemos un buffer del doble de tamanio maximo de un paquete (128+1+8=137*2=274). Alli vamos a ir poniendo los 'pedazos' de paquete que nos van llegando con recv(), hasta obtener la cantidad de bytes de datos especificada por el largo. Tambien puede pasar que leamos el comienzo del siguiente paquete. En este caso, debemos utilizar el primer paquete, mover el nuevo pedazo de paquete al principio del buffer, y volver a recibir hasta armar un paquete completo.

Bloqueo: espera de datos

Los que hayan compilado y utilizado los programas de esta seccion en USERS te da MAS se habran dado cuenta que, por ejemplo, recv() se queda esperando la llegada de datos. Se dice que recv() bloquea. Bloquear es un sinonimo de esperar, pero hay veces que nosotros no queremos un programa que se quede esperando, sino hacer alguna otra cosa mientras tanto, como aceptar comandos por parte del usuario. Esta metodologia es la que implemente en el sistema de mensajeria instantanea segura que estoy desarrollando, llamado OneCQ SIMS. Los interesados van a encontrar el codigo fuente (pronto disponible en www.buanzo.com.ar) del cliente del sistema. Alli implemento todas las tecnicas comentadas en este articulo. Continuando con el bloqueo, si nosotros no queremos que una funcion bloquee, solamente debemos setear un bit del socket, utilizando la funcion fcntl(), de esta forma:

[...]

sockfd = socket(AF_INET, SOCK_STREAM, 0);

fcntl(sockfd, F_SETFL, O_NONBLOCK);

Luego, podriamos intentar leer continuamente en un bucle. recv() devolveria -1, y errno seria EWOULDBLOCK. Obviamente, este tipo de lectura no es aconsejable, ya que consumiriamos muchos ciclos de procesador. Tenemos otras dos opciones: utilizar select() o utilizar la senial SIGIO (Input/Output Signal). Sobre select() no voy a hablar. Pero hablare del metodo mas interesante, versatil y productivo: SIGIO.

Dame una Señal!

Utilizar este metodo es muy simple y va a transformarse en el preferido de todos ustedes, si es que se deciden a hacer programacion de sockets. Por normal general, primero setearemos el bit O_NONBLOCK del socket para que no bloquee y luego, utilizando signal(), haremos que al recibir la senial SIGIO el flujo de ejecucion de nuestro programa se dirija a una funcion nuestra que se encargara de leer y administrar los datos que se reciben. En el caso de OneCQ, el socket bloquea hasta estar conectado, esperando (sin bloquear) la llegada de la senial SIGIO, utilizando la funcion pause(). Luego de estar conectado, el socket pasa a no bloquear, para permitir al usuario tipear comandos y al mismo tiempo recibir datos del socket. La funcion que sera disparada por la senial SIGIO debe ser definida asi:

void FUNCION (int senial);

void FUNCION (int senial) {

/* definir variables */

signal (SIGIO, SIG_IGN);

signal (SIGIO, FUNCION);

/* trabajar y retornar */

}

Luego, en nuestro main(), incluyamos:

signal (SIGIO,SIG_IGN);

signal (SIGIO,FUNCION);

Por supuesto, de nuestro proyecto dependera cuando y donde configuraremos el socket para que no bloquee. Esto lo hacemos como vimos mas arriba, con fcntl();

La verdad, el espacio no alcanzo para cubrir el diseño de un servidor de multiples clientes. Pero me alcanza para decirles que con modificar MUY poquito el servidor de ejemplo que les di, alcanza. Solo tienen que hacer que acepte conecciones, utilizar fork(). El proceso hijo administrara su coneccion, mientras que el server solo hara dos cosas: esperar nuevas conecciones (generando nuevos hijos) y atender la muerte de hijos con lo siguiente:

signal (SIGCHLD, SIG_IGN);

signal (SIGCHLD, OTRA_FUNCION);

OTRA_FUNCION es asi:

void OTRA_FUNCION (int senial) {

signal (señal, SIG_IGN);

wait (NULL);

signal (señal, OTRA_FUNCION);

}

Mas Información

Si les interesa el tema, tiene muchos libros y guias en Internet. Estos libros estan en ingles, quiza alguno este traducido:

  • Unix Network Programming (Vol I y II) de Stevens, ed. Prentice Hall: ISBN 013490012X (vol.I) y 0130810819 (vol.II). Excelente.
  • Advanced Programming in the UNIX Environment de Stevens, ed. Prentice Hall: ISBN 0201563177. Stevens es un maestro.

En la Web:

Cualquier duda no duden en enviar sus consultas a linux@tectimes.com o revisar www.buanzo.com.ar. Un fuerte abrazo a todos!