A programar enchufes! - Segunda parte

Ahora sabemos que es un socket, conocemos las funciones basicas para conectarnos y esperar conecciones. Veamos como enviar y recibir datos.

La diferencia entre TCP y UDP radica en que TCP siempre esta "conectado" al destino, por lo que al enviar datos sabemos a donde debemos enviarlos. En UDP esto no es generalmente asi, por lo que para enviar se usa otra funcion que requiere que le indiquemos a donde enviar. A decir verdad, los sockets UDP conectados tambien existen: en estos podremos usar las mismas funciones que usa un socket TCP. Veamos cuales son.

Sockets conectados

Para enviar utilizamos la funcion send(), cuyo propotipo es el siguiente:

int send(int sockfd, const void *msg, int len, int flags)

Imagino que a esta altura ya saben que el primer parametro (int sockfd) es el descriptor de nuestro socket ya conectado, o sea, el que obtuvimos con socket() o con accept(). El segundo (msg) es un puntero a los datos que queremos enviar. Y (len) es la cantidad de esos datos que queremos enviar. El ultimo parametro (flags) con solo ponerlo en cero alcanza. Las otras opciones no son comentables aqui, pero si les interesan, vean la pagina del manual de send(), en la seccion 2. (man 2 send).

Veamos algunas lineas para ejemplificar send():

char *msg = "Aqui... Linux USERS!";

int len, bytes_enviados;

len = strlen(msg);

bytes_enviados = send(sockfd, msg, len, 0);

bytes_enviados ahora contendra la cantidad de bytes que send() ha enviado ¡y este numero puede ser MENOR al que nosotros le dijimos que envie! Esto generalmente sucede cuando queremos enviar mas de 1 kilobyte de datos. Cosas de TCP/IP... Si esto pasa, tendremos que enviar de vuelta el resto de los datos. En la proxima entrega de esta nota veremos tecnicas para manejar esta y otras situaciones. Si devuelve -1 significa que hubo un error, y la variable errno nos dira de que error se trata.

Para recibir disponemos de la funcion recv(), definida asi:

int recv(int sockfd, void *buf, int len, unsigned int flags)

Como pueden ver, es bastante parecida a send(). Por supuesto, sockfd sigue siendo lo mismo. El segundo parametro es un puntero a una variable de tipo char que generalmente se llama "buffer" en la jerga de programacion, del cual no conozco equivalente que se use en nuestro idioma. El tercero (len) es el tamanio de este buffer, y flags debe ser cero, como en send().

recv() devuelve la cantidad de bytes recibidos, o sino nos da -1 en caso de error. De vuelta, errno nos dira que problema hubo.

Pero atencion muchachos, porque recv() puede devolver CERO! Esto nos indica que la otra computadora a cerrado la coneccion. A decir verdad, este es uno de los metodos mas comunes para saber si el vinculo sigue establecido.

Ya con haber entendido las funciones de la primera parte, y ahora send() y recv(), pueden empezar a jugar un poco. Como idea, prueben de modificar el programa que les deje en el sistema USERS te da MAS para que funcione como analizador de puertos, o sea, que nos diga que puertos estan abiertos o cerrados de una determinado host. Es facil. Lo mas simple seria reemplazar la constante de puerto por una variable, y meterla dentro de un ciclo for. Haganme saber sus progresos, si quieren.

Sockets no conectados

Les dije que para los sockets UDP no conectados se utilizan variantes de send() y recv() casi equivalentes, con la unica diferencia que se agrega un par de parametros relacionados con el destino y origen de los datos. Estas funciones se llaman sendto() y recvfrom(). ("enviar hacia" y "recibir desde" respectivamente). Veamos sendto():

int sendto (int sockfd, const void *msg, 
int len, unsigned int flags,
const struct sockaddr *to, int tolen)

Los primeros 4 parametros son equivalentes a los de send(), por lo que no se los voy a explicar de vuelta y voy a ir a lo importante. El quinto parametro es un puntero a una estructura de tipo sockaddr, seguramente la recordaran de bind() y connect(). Y el sexto, es el tamaño de dicha estructura: sizeof(struct sockaddr). Esta estructura contiene el puerto y direccion IP de destino.

En verdad, la estructura que nosotros usamos en general es sockaddr_in, pero esta es compatible con sockaddr, por lo que utilizamos un recurso para "disfrazarla"; esto se llama type casting. Se pone entre parentesis el tipo que va a hacer de "disfraz", mientras que detras y fuera del parentesis se pone la variable original.

sendto() devuelve la cantidad de bytes enviados igual que send(), y si devuelve -1, ya saben: debemos examinar la variable errno para saber que error es.

recvfrom() tiene el siguiente prototipo:

int recvfrom (int sockfd, void *buf, 
int len, unsigned int flags,
struct sockaddr *from, int *fromlen)

Como ven, es igual a recv() mas dos parametros adicionales. (from) es un puntero a una estructura sockaddr que sera llenada con el puerto e IP de origen de los datos. (fromlen) es un puntero a una variable int que inicialmente contendra el valor devuelto por sizeof(struct sockaddr), al finalizar recvfrom(), este puntero contendra el tamanio de la direccion ahora contenida en (from).

Para conectar un socket UDP, tienen que utilizar connect(). Y si lo hacen, entonces van a poder usar send() y recv(), en vez de sendto() y recvfrom(). Esto NO significa que el socket UDP pase a ser TCP por conectarlo, sino que la interfaz de sockets va a agregar automaticamente el destino u origen de los datos. Si necesitan seguridad de transmision, utilicen TCP, y no UDP o UDP conectado.

Cerrando un socket

Bueno, yo ya les habia comentado que para cerrar un socket se utiliza close(), al igual que con archivos. Al cerrarlo se previenen lecturas y escrituras. Si alguien intentara leer o escribir en este socket ahora cerrado, va a recibir el error correspondiente.

Pero tenemos una segunda manera de cerrar un socket, una forma que nos provee de un poco mas de control, y mas de uno de ustedes verá inmediatamente la utilidad de esta forma. La funcion se llama shutdown() y su prototipo es el siguiente:

int shutdown (int sockfd, int how)

Ya saben: sockfd es el socket que queremos cerrar. Lo interesante viene en el segundo parametro (how). Este puede tomar 3 valores diferentes, dependiendo de lo que querramos hacer.

  • 0:Cierra la capacidad de recepcion
  • 1:Cierra la capacidad de envio
  • 2:Ni recepcion, ni envio.

shutdown() devuelve 0 si la operacion se realizo con exito, o -1 en caso de error. Por supuesto, errno nos dira que error es.

Les menti. En verdad shutdown() no cierra un socket, solamente modifica su capacidad de recibir o enviar. Para liberar los recursos que utiliza un socket, hay que usar close(). No queda otra.

Ok, en verdad no les dije toda la verdad. Cuando hacemos close, estamos cerrando nuestra interfaz al socket, no el socket en si. El cerrarlo es trabajo del nucleo (el kernel Linux), y esto puede llevar un maximo de 4 minutos. Durante este tiempo, el socket estara en estado TIME_WAIT. Prueben de conectarse a algun lado o de hacer algo en internet y ejecuten el comando "netstat". La ultima columna les dice el estado de cada socket.

Los errores

Habran visto que todas las funciones cuando quieren indicarnos un error, devuelven -1 y setean la variable errno al numero de error correspondiente. Ok, buenisimo. Pero nosotros como hacemos para saber que error es? Facil. Tenemos una funcion llamada perror(), que viene de "print error" (imprimir error), que lo que hace es mostrar en pantalla una frase descriptiva del error. Como parametro, acepta uno solo, que es un mensaje que nosotros queremos que se muestre justo delante del error. Este mensaje es generalmente el modulo o funcion donde se produjo el error, o simplemente el nombre del programa. Por ejemplo, supongamos que despues de hacer connect() obtengamos un error. Podriamos hacer algo asi:

error = connect(blabla);

if (error == -1) {

perror("coneccion");

exit(2);

}

Este pseudo-codigo guarda en la variable error el resultado de connect(), luego lo compara con -1, y en caso afirmativo muestra la palabra "coneccion", seguida de dos puntos ":" que perror() siempre pone, y luego el error "traducido" de numero a una frase. Luego, finaliza la ejecucion del programa devolviendo al sistema el codigo de salida 2. Este codigo de salida podria ser interpretado por el programa o script que haya llamado a nuestro programa originalmente. Esta es una practica muy comun, incluso de sistemas operativos tipo DOS.

En las proximas entregas les voy a hablar sobre DNS, programas servidores de multiples clientes, como solucionar el problema de que send() no haya enviado todos los datos que le dijimos. Y tambien veremos algo de diseño de un protocolos. Tambien vamos a hablar un poco sobre multiplexado sincronico, bloqueo y manejo de señales.