Programacion de Sockets - Parte 1

A programar enchufes!

Si queremos hacer un programa para tal o cual cosa que necesita acceso a Internet o a nuestra red local, los sockets son la respuesta.

Para que dos programas se comuniquen entre si sobre una red, se necesitan al menos dos sockets: uno en nuestra maquina, y otro en el sistema remoto. Podemos suponer asi que entonces uno esta actuando de servidor, a la espera de conecciones por parte de clientes. Y el otro socket es justamente el que se conectara al socket servidor. Este esquema cliente/servidor es el clasico, y de el se desprenden muchos metodos, como por ejemplo el de servidor centralizado y clientes distribuidos, que para comunicarse entre si el mensaje debe pasar primero por el servidor, y de alli reenviarse al cliente destino. Pero hoy vamos a concentrarnos en aprender basicamente a crear sockets de ambos tipos (cliente y servidor).

ATENCION: No es nuestro proposito enseniar a programar en lenguaje C, pero ante cualquier duda consulten en los foros de TecTimes o envien eMail a linux@tectimes.com

Conceptos clave

Un socket es simplemente un tipo de archivo Unix especial, como un pipe o un fifo, con una funcionalidad diferente. Al ser un archivo, tiene un numero asociado cuando lo abrimos o creamos, llamado "file descriptor", y tambien podemos leerlo, escribirlo y cerrarlo como cualquier archivo comun abierto con fopen(). Pero los sockets se crean, leen y escriben con otras funciones, y no se utilizan read() o write() para leer o escribir simplemente porque no nos proveen de la funcionalidad adicional que necesitamos para el caso de un socket.

Y como creamos un socket? Para tal efecto debemos utilizar la función socket(). No olviden leer las paginas del manual de todas las funciones que yo nombre en este articulo. Para leer la de socket(), utilicen "man 2 socket", y no "man socket". Debemos especificar la seccion del manual, caso contrario veremos la funcion socket() de TCL y no la llamada al sistema, que es la que nos interesa. Es buena practica intentar primero "man 2 funcion" y si no, "man 3 funcion". En las secciones 2 y 3 vamos a encontrar documentacion sobre funciones del nucleo y funciones de libreria, respectivamente.

Primero, debemos incluir sys/types.h junto con sys/socket.h y netinet/in.h, ya que los prototipos de las funciones que vamos a utilizar se encuentran alli definidos, y el de socket() es asi:

int socket(int domain, int type, int protocol);

Pueden ver 3 argumentos. El primero (domain) es un entero que debe ser seteado como AF_INET, porque nuestro socket va a ser utilizado sobre una red. Hay otros dos valores, uno para Unix local y otro para redes X.25, pero no nos interesan para el proposito de esta guia.

El segundo argumento (type), puede ser SOCK_STREAM o SOCK_DGRAM. El primero es para utilizar el protocolo TCP, y el segundo para UDP. En la nota sobre IPTables de este mismo numero pueden encontrar la diferencia entre ambos protocolos.

Finalmente, el tercer argumento (protocol) nos conviene setearlo en cero, para que socket() elija el protocolo correcto en base a type. Si esto les trae dudas, revisen la pagina del manual de la funcion getprotobyname(). Socket devuelve el descriptor de archivo ('file descriptor'), que debemos guardar en una variable creada a tal efecto. Este numero sera utilizado en todas las demas funciones.

Los puertos

Ustedes ya saben, probablemente, que cuando navegamos generalmente nos conectamos al puerto 80, y que cuando bajamos mails, lo hacemos desde el 110, y cuando enviamos, por el 25. Si nosotros creamos un programa que va a actuar de servidor necesitamos utilizar la funcion bind() para definir a que puerto van a tener que conectarse los clientes. Como norma general, se utilizan los valores mayores a 1024 y hasta el 65535. Para utilizar puertos menores que 1025, necesitamos privilegios de administrador. Esta es una vieja norma de seguridad (MUY vieja), que se mantiene solo por propositos de retrocompatibilidad.

La funcion bind() esta definida asi:

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

El primer argumento (sockfd) toma por valor el del descriptor que nos dio socket(). El segundo es un puntero a una estructura llamada sockaddr que contiene informacion sobre nuestra IP y puerto. Y el tercero, con poner sizeof (struct sockaddr) alcanza. Veamos un pequenio ejemplo para comprender mejor:

#include 

#include 

#include 

#include 

main()

{

int enchufe;

struct sockaddr_in nos;

enchufe = socket(AF_INET, SOCK_STREAM, 0);

nos.sin_family = AF_INET;

nos.sin_port = htons(3490);

nos.sin_addr.s_addr = INADDR_ANY;

memset(&(nos.sin_zero), 0, 8);

bind(enchufe, (struct sockaddr *)&nos, sizeof(struct sockaddr));

La funcion htons() que vemos alli debe ser utilizada para cambiar nuestros numeros (o sea, del "host") al Orden de Bytes de Red ("Network Byte Order"). La 'h' significa 'host', 'to' significa 'hacia' y 'ns' significa 'network short' (un 's'hort de red, tambien tenemos 'l'ong de red). En base a esto podemos entender que hace la funcion ntohl(): 'n'etwork 'to' 'host long'.

No voy a explicar todo esto del orden de bytes, tan solo recuerden donde deben usarlo.

Si queremos que bind seleccione automaticamente un puerto libre, igualen nos.sin_port a cero, sin htons(), ya que el orden de bytes no importa en el numero cero.

INADDR_ANY es en verdad el valor cero, e indica que nuestra IP debe ser llenada automaticamente, en vez de proveerla nosotros. Pueden usar htonl(INADDR_ANY) si prefieren.

Conectándonos

Bien, a la hora de hacer un cliente debemos indicar a que IP y puerto queremos conectarnos, para luego efectuar dicha coneccion. Veamos el prototipo de la funcion connect():

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

El primer argumento es lo que ustedes se imaginan: el descriptor que nos dio socket() y que guardamos en una variable. El segundo y tercero ya lo vimos en bind(), solo que esta vez en vez de usarlo en bind() para esperar conecciones DESDE cierta IP y puerto, lo usaremos en connect() para conectarnos a cierta IP y puerto. Miren esto:

#include 

#include 

#include 

#include 

main()

{

int enchufe;

struct sockaddr_in destino;

enchufe = socket(AF_INET, SOCK_STREAM, 0);

destino.sin_family = AF_INET;

destino.sin_port = htons(23);

destino.sin_addr.s_addr = inet_addr("10.12.110.57");

memset(&(destino.sin_zero), 0, 8);

connect(enchufe, (struct sockaddr *)&destino, sizeof(struct sockaddr))

En el ejemplo nos conectamos a 10.12.110.57, puerto 23. La funcion inet_addr() convierte un IP como cadena en un entero sin signo (unsigned long). La funcion inversa, de unsigned long a cadena es inet_ntoa (por 'n'etwork 'to' 'a'scii).

ATENCION: Todas las funciones devuelven -1 ante error, y la variable errno nos indicara que error es. Lean las paginas del manual para ver todos los errores posibles, ya que no entrarian aqui.

Hola, te escucho

Cuando estamos haciendo un server, luego de socket() y bind(), debemos esperar conecciones, con listen() y cuando una llegue, aceptarla con accept(). Veamos el prototipo de listen():

int listen(int sockfd, int backlog);

Adivinen que debemos poner en el primer argumento? Si! El descriptor que nos dio socket()! Pero el segundo, backlog, indica que cantidad maxima de clientes pueden estar en la cola, esperando ser aceptados. Un numero comun es 5, o 10. Vuestra eleccion, lectores, pero no exagereis.

Ahora, agarrense, que se viene lo interesante. Estamos finalmente esperando conecciones en nuestro socket, en un determinado puerto, llega un cliente, espera ser aceptado, y cuando lo aceptamos... accept() nos da un NUEVO descriptor de socket! Ahora tenemos DOS. El original, que esta esperando nuevas conecciones, y el nuevo, en el que ya podemos enviar y recibir con send() y recv(). Veamos el prototipo:

int accept(int sockfd, void *addr, int *addrlen);

El primer argumento ya no es necesario explicarlo. El segundo es un puntero a una estructura sockaddr_in donde se guardara la IP y puerto DESDE LA CUAL se conecta el cliente. El tercero debe ser sizeof(struct sockaddr_in). Veamos un ejemplo:

#include 

#include 

#include 

#include 

main()

{

int escucha, cliente;

struct sockaddr_in nos;

struct sockaddr_in ellos;

int addr_len;

escucha = socket(AF_INET, SOCK_STREAM, 0);

nos.sin_family = AF_INET;

nos.sin_port = htons(3490);

nos.sin_addr.s_addr = INADDR_ANY;

memset(&(nos.sin_zero), 0, 8);

bind(escucha, (struct sockaddr *)&nos, sizeof(struct sockaddr));

listen(escucha, 10);

addr_len = sizeof(struct sockaddr_in);

cliente = accept(escucha, (struct sockaddr *)&ellos, &addr_len);

Si no queremos mas conecciones, debemos usar close(escucha).

En la proxima: enviar y recibir con send() y recv() para TCP, y sendto() y recvfrom() para UDP, junto con otras practicas y funciones necesarias. Hasta Linux USERS v1.4, amigos.