Curso de programación en C para GNU/Linux (VIII)

En la pasada entrega vimos cómo comunicar datos entre procesos mediante tuberías y vimos cómo también se pueden utilizar los mecanismos IPC estándar. En esta última entrega (oooohhh ;-D) veremos cómo comunicarlos a través de sockets TCP y UDP.

 

Comunicación por red

Muchas de las utilidades que usamos todos los días, como el correo electrónico o los navegadores web, utilizan protocolos de red para comunicarse. En este apartado veremos cómo utilizar esos protocolos, comprenderemos todo lo que rodea a una comunicación a través de los interfaces de red y aprenderemos a programar clientes y servidores sencillos. Breve repaso a las redes TCP/IP

Antes de afrontar la configuración de red de nuestros equipos, vamos a desempolvar nuestras nociones sobre redes TCP/IP. Siempre que se habla de redes de ordenadores, se habla de protocolos. Un protocolo no es más que un acuerdo entre dos entidades para entenderse. Es decir, si yo le digo a un amigo que le dejo una llamada perdida cuando llegue a su portal, para que baje a abrirme, habremos establecido un protocolo de comunicación, por ejemplo.

Los ordenadores funcionan de una forma más o menos parecida. Cuando queremos establecer una conexión entre ordenadores mediante una red, hay muchos factores en juego: primero está el medio físico en el que se producirá la conexión (cable, ondas electromagnéticas, etc.), por otro lado está la tecnología que se utilizará (tarjetas de red Ethernet, modems, etc.), por otro los paquetes de datos que enviaremos, las direcciones o destinos dentro de una red… Dado que hay tantos elementos que intervienen en una comunicación, se establecen protocolos o normas para entidades del mismo nivel, es decir, se divide todo lo que interviene en la comunicación en diferentes capas. En el nivel más bajo de todas las capas estaría el nivel físico: el medio que se va a utilizar, los voltajes utilizados, etc. Sobre esa capa se construye la siguiente, en donde transformamos las señales eléctricas en datos propiamente dichos. Esta sería la capa de enlace de datos. Posteriormente, se van estableciendo una serie de capas intermedias, cada una con mayor refinamiento de la información que maneja, hasta llegar a la capa de aplicación, que es con la que interactúan la mayoría de programas que utilizamos para conectarnos a redes: navegadores, clientes de correo, etc.

Así, por ejemplo, si queremos mandar un correo electrónico a un amigo, utilizaremos la aplicación indicada para mandar un mensaje, en este caso nuestro cliente de correo preferido (mutt, Kmail, mozilla…). El cliente de correo enviará el mensaje al servidor, para que éste lo encamine hasta el buzón de nuestro amigo, pero en ese envío sucederán una serie de pasos que pueden parecernos transparentes en un principio:

  • Primeramente se establece una conexión con el servidor, para ello la aplicación (el cliente de correo) enviará a las capas más bajas de protocolos de red una petición al servidor de correo.
  • Esa capa, aceptará la petición, y realizará otro encargo a una capa inferior, solicitando enviar un paquete de datos por la red.
  • La capa inferior, a su vez, pedirá a la capa física enviar una serie de señales eléctricas por el medio, para hacer su cometido.

Tal y como está enfocada la “pila de protocolos de red”, cada “trabajo” de red complejo se divide en partes cada vez más sencillas hasta llegar a la capa física, que se encargará de la transmisión eléctrica. Es como si el jefe de una empresa de videojuegos mandara a su subdirector que hiciera un juego de acción. El subdirector iría a donde sus subordinados y les pediría un guión, unos gráficos, un motor de animaciones, etc. Los encargados de los gráficos irían a donde sus subordinados y les pedirían, la portada, los decorados, los personajes, etc. Éstos, a su vez, se repartirían en grupos y cada uno haría un trabajo más concreto, y así sucesivamente. Es decir, una idea compleja, se divide en trabajos concretos y sencillos para hacerse, estructurándose en capas.

En el caso específico que nos interesa, la pila de protocolos que utilizamos se denomina TCP/IP, porque dos de sus protocolos principales se llaman TCP (capa de transporte) e IP (capa de red).

cursoc09.gif

Comunicación mediante capas de protocolos de red.

Cuando utilizamos estos protocolos, cada uno de los posibles destinos de una red necesita un nombre diferente o dirección IP. Al igual que sucede con los teléfonos, para diferenciar todos los ordenadores y dispositivos conectados a una red, se les asigna a cada uno un número diferente, y basta con “marcar” ese número para acceder a él. Actualmente esos números van desde el 0 al 4294967296, pero en lugar de utilizar simplemente el número, se emplea una notación más sencilla, separando el número en 4 dígitos del 0 al 255, por ejemplo: 128.244.34.12 ó 192.168.0.1.

En un futuro no muy lejano, las direcciones IP cambiarán su formato, ya que el espacio de direcciones que ofrece la versión actual de IP (IPv4) se está agotando, por lo que habrá que ampliar su rango (al igual que ocurre en ciudades o provincias con mucha demanda de números de teléfono, que amplían la longitud de sus números de teléfono en una o varias cifras).

Siempre que queramos acceder a una red TCP/IP, deberemos tener una dirección IP que nos identifique. Está prohibido viajar sin matrícula por estas carreteras. En nuestras redes privadas, nuestras intranets o pequeñas LANs, la manera de establecer esas direcciones IP la marcamos nosotros mismos (o el administrador de red, en su caso). Es decir, dentro de nuestras organizaciones, somos nosotros los que ponemos los nombres. Esto es lo mismo que lo que sucede en una organización grande, con muchos teléfonos internos y una centralita. El número de extensión de cada teléfono, lo inventamos nosotros mismos, no la compañía telefónica. Cuando queremos salir a una red pública como pueda ser Internet, no podemos inventarnos nuestra dirección IP, deberemos seguir unas normas externas para poder circular por allí. Siguiendo el símil telefónico, si queremos un teléfono accesible por todo el mundo, deberemos solicitar un número válido a la empresa telefónica.

Hasta aquí todo claro: los ordenadores tienen unos números similares a los números de teléfono para identificarse, y cuando queremos comunicarnos con un destino en concreto, sólo tenemos que “marcar” su número, pero… ¿cuándo pedimos una página web a www.linux.org cómo sabe nuestra máquina qué número “marcar”? Buena pregunta, tiene que haber un “listín telefónico” IP, que nos diga que IP corresponde con una dirección específica. Estas “páginas amarillas” de las redes IP se denominan DNS (Domain Name System). Justo antes de hacer la conexión a www.linux.org, nuestro navegador le pregunta la dirección IP al DNS, y luego conecta vía dirección IP con el servidor web www.linux.org.

Una conexión de un ordenador a otro precisa, además de una dirección IP de destino, un número de puerto. Si llamamos por teléfono a un número normalmente preguntamos por alguien en concreto. Llamas a casa de tus padres y preguntas por tu hermano pequeño. Con las comunicaciones telemáticas sucede algo parecido: llamas a una determinada IP y preguntas por un servicio en concreto, por ejemplo el Servicio Web, que tiene reservado el puerto 80. Los servicios reservan puertos y se quedan a la escucha de peticiones para esos puertos. Existen un montón de puertos que típicamente se usan para servicios habituales: el 80 para web, el 20 y 21 para FTP, el 23 para telnet, etc. Son los puertos “bien conocidos” (“well known ports”) y suelen ser puertos reservados, por debajo de 1024. Para aplicaciones extrañas o de ámbito personal se suelen utilizar puertos “altos”, por encima de 1024. El número de puerto lo define un entero de 16 bits, es decir, hay 65535 puertos disponibles. Un servidor o servicio no es más que un programa a la escucha de peticiones en un puerto. Así pues, cuando queramos conectarnos a un ordenador, tendremos que especificar el par “DirecciónIP:Puerto”.

Con esta breve introducción hemos repasado nociones básicas de lo que son protocolos de comunicaciones, la pila de protocolos TCP/IP, el direccionamiento IP, y la resolución de nombres o DNS.

Sockets

Un socket es, como su propio nombre indica, un conector o enchufe. Con él podremos conectarnos a ordenadores remotos o permitir que éstos se conecten al nuestro a través de la red. En realidad un socket no es más que un descriptor de fichero un tanto especial. Recordemos que en UNIX todo es un fichero, así que para enviar y recibir datos por la red, sólo tendremos que escribir y leer en un fichero un poco especial.

Ya hemos visto que para crear un nuevo fichero se usan las llamadas open() o creat(), sin embargo, este nuevo tipo de ficheros se crea de una forma un poco distinta, con la función socket():

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

Una vez creado un socket, se nos devuelve un descriptor de fichero, al igual que ocurría con open() o creat(), y a partir de ahí ya podríamos tratarlo, si quisiéramos, como un fichero normal. Se pueden hacer read() y write() sobre un socket, ya que es un fichero, pero no es lo habitual. Existen funciones específicamente diseñadas para el manejo de sockets, como send() o recv(), que ya iremos viendo más adelante.

Así pues, un socket es un fichero un poco especial, que nos va a servir para realizar una comunicación entre dos procesos. Los sockets que trataremos nosotros son los de la API (Interfaz de Programación de Aplicaciones) de sockets Berkeley, diseñados en la universidad del mismo nombre, y nos centraremos exclusivamente en la programación de clientes y servidores TCP/IP. Dentro de este tipo de sockets, veremos dos tipos:

  • Sockets de flujo (TCP).
  • Sockets de datagramas (UDP).

Los primeros utilizan el protocolo de transporte TCP, definiendo una comunicación bidireccional, confiable y orientada a la conexión: todo lo que se envíe por un extremo de la comunicación, llegará al otro extremo en el mismo orden y sin errores (existe corrección de errores y retransmisión).

Los sockets de datagramas, en cambio, utilizan el protocolo UDP que no está orientado a la conexión, y es no confiable: si envías un datagrama, puede que llegue en orden o puede que llegue fuera de secuencia. No precisan mantener una conexión abierta, si el destino no recibe el paquete, no ocurre nada especial.

Alguien podría pensar que este tipo de sockets no tiene ninguna utilidad, ya que nadie nos asegura que nuestro tráfico llegue a buen puerto, es decir, podría haber pérdidas de información. Bien, imaginemos el siguiente escenario: el partido del siglo (todos los años hay dos o más, por eso es el partido del siglo), una única televisión en todo el edificio, pero una potente red interna que permite retransmitir el partido digitalizado en cada uno de los ordenadores. Cientos de empleados poniendo cara de contables, pero siguiendo cada lance del encuentro… ¿Qué pasaría si se usaran sockets de flujo? Pues que la calidad de la imagen sería perfecta, con una nitidez asombrosa, pero es posible que para mantener intacta la calidad original haya que retransmitir fotogramas semidefectuosos, reordenar los fotogramas, etc. Existen muchísimas posibilidades de que no se pueda mantener una visibilidad en tiempo real con esa calidad de imagen. ¿Qué pasaría si usáramos sockets de datagramas? Probablemente algunos fotogramas tendrían algún defecto o se perderían, pero todo fluiría en tiempo real, a gran velocidad. Perder un fotograma no es grave (recordemos que cada segundo se suelen emitir 24 fotogramas), pero esperar a un fotograma incorrecto de hace dos minutos que tiene que ser retransmitido, puede ser desesperante (quizá tu veas la secuencia del gol con más nitidez, pero en el ordenador de enfrente hace minutos que lo han festejado). Por esto mismo, es muy normal que las retransmisiones de eventos deportivos o musicales en tiempo real usen sockets de datagramas, donde no se asegura una calidad perfecta, pero la imagen llegará sin grandes saltos y sin demoras por retransmisiones de datos imperfectos o en desorden.

Tipos de datos

Es muy importante conocer las estructuras de datos necesarias a la hora de programar aplicaciones en red. Quizá al principio pueda parecer un tanto caótica la definición de estas estructuras, es fácil pensar en implementaciones más eficientes y más sencillas de comprender, pero debemos darnos cuenta de que la mayoría de estas estructuras son modificaciones de otras existentes, más generales y, sobre todo, que se han convertido en un estándar en la programación en C para UNIX. Por lo tanto, no nos queda más remedio que tratar con ellas.

La siguiente estructura es una struct derivada del tipo sockaddr, pero específica para Internet:

struct sockaddr_in {
	short int sin_family; // = AF_INET
	unsigned short int sin_port;
	struct in_addr sin_addr;
	unisgned char sin_zero[8];
}

A simple vista parece monstruosamente fea. Necesitábamos una estructura que almacenase una dirección IP y un puerto, ¿y alguien diseñó eso? ¿En qué estaba pensando? No desesperemos, ya hemos dicho que esto viene de más atrás. Comentemos poco a poco la estructura:

  • sin_family: es un entero corto que indica la “familia de direcciones”, en nuestro caso siempre tendrá el valor “AF_INET”.
  • sin_port: entero corto sin signo que indica el número de puerto.
  • sin_addr: estructura de tipo in_addr que indica la dirección IP.
  • sin_zero: array de 8 bytes rellenados a cero. Simplemente tiene sentido para que el tamaño de esta estructura coincida con el de sockaddr.

La estructura in_addr utilizada en sin_addr tiene la siguiente definición:

struct in_addr {
	unisgned long s_addr;
}

Es decir, un entero largo sin signo.

Además de utilizar las estructuras necesarias, también deberemos emplear los formatos necesarios. En comunicaciones telemáticas entran en juego ordenadores de muy diversas naturalezas, con representaciones diferentes de los datos en memoria. La familia de microprocesadores x86, por ejemplo, guarda los valores numéricos en memoria utilizando la representación “Little-Endian”, es decir, para guardar “12345678”, se almacena así: “78563412”, es decir, el byte de menos peso (“78”) al principio, luego el siguiente (“56”), el siguiente (“34”) y por último, el byte de más peso (“12”). La representación “Big-Endian”, empleada por los microprocesadores Motorola, por ejemplo, guardaría “12345678” así: “12345678”. Si dos ordenadores de estas características compartieran información sin ser unificada, el resultado sería ininteligible para ambas partes. Por ello disponemos de un conjunto de funciones que traducen de el formato local (“host”) al formato de la red (“network”) y viceversa:

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

Quizá parezca complicado, pero sus nombres son muy representativos: “h” significa “host”·y “n” significa “network”. Las funciones que acaban en “l” son para enteros largos (“long int”, como los usados en las direcciones IP) y las que acaban en “s” son para enteros cortos (“short int”, como los usados al especificar un número de puerto). Por lo tanto para indicar un número de puerto, por ejemplo, podríamos hacerlo así:

sin_port = htons( 80 );

Es decir, convertimos “80” del formato de nuestro host, al formato de red (“h” to “n”), y como es un “short” usamos htons().

Ya para terminar con los formatos veremos dos funciones más. Normalmente la gente no utiliza enteros largos para representar sus direcciones IP, sino que usan la notación decimal, por ejemplo: 130.206.100.59. Pero como ya hemos visto, in_addr necesita un entero largo para representar la dirección IP. Para poder pasar de una notación a otra tenemos dos funciones a nuestra disposición:

int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

Los nombres de las funciones también ayudan: inet_aton() traduce de un array (“a”) de chars, es decir, un string, a una dirección de red (“n”, de “network”), mientras que inet_ntoa() traduce de una dirección de red (“n”) a un array de chars (“a”). Veamos un ejemplo de su uso:

struct in_addr mi_addr;

inet_aton( “130.206.100.59”, &(mi_addr) );

printf( “Direccion IP: %s\n”, inet_ntoa( mi_addr ) );

Para terminar este apartado, vamos a ver cómo rellenar una estructura sockaddr_in desde el principio. Imaginemos que queremos preparar la estructura “mi_estructura” para conectarnos al puerto 80 del host “130.206.100.59”. Deberíamos hacer los siguiente:

struct sockaddr_in mi_estructura;

mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( 80 );
inet_aton( “130.206.100.59”, &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), ‘’, 8 );

Como ya sabemos, “sin_family” siempre va a ser “AF_INET”. Para definir “sin_port”, utilizamos htons() con el objeto de poner el puerto en formato de red. La dirección IP la definimos desde el formato decimal a la estructura “sin_addr” con la función inet_aton(), como sabemos. Y por último necesitamos 8 bytes a 0 (“”) en “sin_zero”, cosa que conseguimos utilizando la función memset(). Realmente podría copiarse este fragmento de código y utilizarlo siempre así sin variación:

#define PUERTO 80
#define DIRECCION “130.206.100.59”

struct sockaddr_in mi_estructura;

mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( PUERTO );
inet_aton( DIRECCION, &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), ‘’, 8 );

Funciones necesarias

Una vez conocidos los tipos de datos que emplearemos a la hora de programar nuestras aplicaciones de red, es hora de ver las llamadas que nos proporcionarán la creación de conexiones, envío y recepción de datos, etc.

Lo primero que debemos obtener a la hora de programar una aplicación de red es un socket. Ya hemos explicado que un socket es un conector o enchufe para poder realizar intercomunicaciones entre procesos, y sirve tanto para crear un programa que pone un puerto a la escucha, como para conectarse a un determinado puerto. Un socket es un fichero, como todo en UNIX, por lo que la llamada a la función socket() creará el socket y nos devolverá un descriptor de fichero:

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

De los tres parámetros que recibe, sólo nos interesa fijar uno de ellos: “type”. Deberemos decidir si queremos que sea un socket de flujo (“SOCK_STREAM”)o un socket de datagramas (“SOCK_DGRAM”). El resto de parámetros se pueden fijar a “AF_INET” para el dominio de direcciones, y a “0”, para el protocolo (de esta manera se selecciona el protocolo automáticamente):

mi_socket = socket( AF_INET, SOCK_DGRAM, 0 );

Ya sabemos crear sockets, utilicémoslos para conectarnos a un servidor remoto. Para conectarnos a otro ordenador deberemos utilizar la función connect(), que recibe tres parámetros:

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

El primero de ellos es el socket (“sockfd”) que acabamos de crear, el segundo es un puntero a una estructura de tipo sockaddr (“serv_addr”) recientemente explicada (recordemos que sockaddr_in era una estructura sockaddr especialmente diseñada para protocolos de Internet, así que nos sirve aquí), y por último es preciso indicar el tamaño de dicha estructura (“addrlen”).

Veamos un fragmento de código que ponga todo esto en juego:

#define PUERTO 80
#define DIRECCION “130.206.100.59”

int mi_socket, tam;
struct sockaddr_in mi_estructura;

mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( PUERTO );
inet_aton( DIRECCION, &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), ‘’, 8 );

mi_socket = socket( AF_INET, SOCK_STREAM, 0 );

tam = sizeof( struct sockaddr );

connect( mi_socket, (struct sockaddr *)&mi_estructura, tam );

Como vemos, lo único que hemos hecho es juntar las pocas cosas vistas hasta ahora. Con ello ya conseguimos establecer una conexión remota con el servidor especificado en las constantes DIRECCION y PUERTO. Sería conveniente comprobar todos los errores posibles que podría provocar este código, como que connect() no logre conectar con el host remoto, que la creación del socket falle, etc.

Para poder enviar y recibir datos existen varias funciones. Ya avanzamos anteriormente que un socket es un descriptor de fichero, así que en teoría sería posible escribir con write() y leer con read(), pero hay funciones mucho más cómodas para hacer esto. Dependiendo si el socket que utilicemos es de tipo socket de flujo o socket de datagramas emplearemos unas funciones u otras:

  • Para sockets de flujo: send() y recv().
  • Para sockets de datagramas: sendto() y recvfrom().

Los prototipos de estas funciones son los siguientes:

int send(int s, const void *msg, size_t len, int flags);

int  sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

int recv(int s, void *buf, size_t len, int flags);

int  recvfrom(int  s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

El parámetro “s” es el socket que emplearemos. Tanto “msg” como “buf” son los buffers que utilizaremos para almacenar el mensaje que queremos enviar o recibir. Posteriormente tenemos que indicar el tamaño de ese buffer, con “len”. En el campo “flags” se pueden indicar varias opciones juntándolas mediante el operador OR, pero funcionará perfectamente si ponemos un 0 (significa no elegir ninguna de esas opciones múltiples). Las funciones para sockets de datagramas incluyen además el puntero a la estructura sockaddr y un puntero a su tamaño, tal y como ocurría con connect(). Esto es así porque una conexión mediante este tipo de socket no requiere hacer un connect() previo, por lo que es necesario indicar la dirección IP y el número de puerto para enviar o recibir los datos. Veamos unos fragmentos de código de ejemplo:

char buf_envio[] = “Hola mundo telematico!\r\n”;
char buf_recepcion[255];
int tam, numbytes;

// aquí creamos el socket mi_socket y
// la estructura mi_estructura, como hemos hecho antes

tam = sizeof( struct sockaddr );

connect( mi_socket, (struct sockaddr *)&mi_estructura, tam );

numbytes = send( mi_socket, buf_envio, strlen( buf_envio ), 0 );
printf( “%d bytes enviados\n”, numbytes );

numbytes = recv( mi_socket, buf_recepcion, 255-1, 0 );
printf( “%d bytes recibidos\n”, numbytes );

Creamos dos buffers, uno para contener el mensaje que queremos enviar, y otro para guardar el mensaje que hemos recibido (de 255 bytes). En la variable “numbytes” guardamos el número de bytes que se han enviado o recibido por el socket. A pesar de que en la llamada a recv() pidamos recibir 254 bytes (el tamaño del buffer menos un byte, para indicar con un “” el fin del string), es posible que recibamos menos, por lo que es muy recomendable guardar el número de bytes en dicha variable. El siguiente código es similar pero para para sockets de datagramas:

char buf_envio[] = “Hola mundo telematico!\r\n”;
char buf_recepcion[255];
int tam, numbytes;

// aquí creamos el socket mi_socket y
// la estructura mi_estructura, como hemos hecho antes

tam = sizeof( struct sockaddr );

// no es preciso hacer connect()

numbytes = sendto( mi_socket, buf_envio, strlen( buf_envio ), 0,
           (struct sockaddr *)&mi_estructura, tam );
printf( “%d bytes enviados\n”, numbytes );

numbytes = recvfrom( mi_socket, buf_recepcion, 255-1, 0,
           (struct sockaddr *)&mi_estructura, &tam );
printf( “%d bytes recibidos\n”, numbytes );

Las diferencias con el código anterior son bastante fáciles de ver: no hay necesidad de hacer connect(), porque la dirección y puerto los incluimos en la llamada a sendto() y recvfrom(). El puntero a la estructura “mi_estructura” tiene que ser de tipo “sockaddr”, así que hacemos un cast, y el tamaño tiene que indicarse en recvfrom() como un puntero al entero que contiene el tamaño, así que referenciamos la variable “tam”.

Con todo lo visto hasta ahora ya sabemos hacer clientes de red, que se conecten a hosts remotos tanto mediante protocolos orientados a la conexión, como telnet o http, como mediante protocolos no orientados a la conexión ,como tftp o dhcp. El proceso para crear aplicaciones que escuchen un puerto y acepten conexiones requiere comprender el uso de unas cuantas funciones más.

Para crear un servidor de sockets de flujo es necesario realizar una serie de pasos:

  1. Crear un socket para aceptar las conexiones, mediante socket().
  2. Asociar ese socket a un puerto local, mediante bind().
  3. Poner dicho puerto a la escucha, mediante listen().
  4. Aceptar las conexiones de los clientes, mediante accept().
  5. Procesar dichas conexiones.

La llamada a bind() asocia el socket que acabamos de crear con un puerto local. Cuando un paquete de red llegue a nuestro ordenador, el núcleo del Sistema Operativo verá a qué puerto se dirige y utilizará el socket asociado para encaminar los datos. La llamada a bind() tiene unos parámetros muy poco sorprendentes:

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

Es decir, el socket, la estructura que indica la dirección IP y el puerto, y el tamaño de dicha estructura. Un ejemplo del uso de bind():

#define PUERTO 80
#define DIRECCION “130.206.100.59”

int mi_socket, tam;
struct sockaddr_in mi_estructura;

mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( PUERTO );
inet_aton( DIRECCION, &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), ‘’, 8 );

mi_socket = socket( AF_INET, SOCK_STREAM, 0 );

tam = sizeof( struct sockaddr );

bind( mi_socket, (struct sockaddr *)&mi_estructura, tam );

Si quisiéramos poner a la escucha nuestra propia dirección, sin tener que saber cuál es en concreto, podríamos haber empleado “INADDR_ANY”, y si el puerto que ponemos a la escucha nos da igual, podríamos haber puesto el número de puerto “0”, para que el propio kernel decida cuál darnos:

mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = 0;
mi_estructura.sin_addr.s_addr = INADDR_ANY;
memset( &(mi_estructura.sin_zero), ‘’, 8 );

Esta es la única llamada que se requiere para crear un servidor de sockets de datagramas, ya que no están orientados a la conexión. Para crear servidores de sockets de flujo, una vez hayamos asociado el socket al puerto con bind(), deberemos ponerlo a la escucha de peticiones mediante la función listen(). Esta función recibe sólo dos parámetros:

int listen(int s, int backlog);

El primero de ellos es el socket, y el segundo es el número máximo de conexiones a la espera que puede contener la cola de peticiones. Después de poner el puerto a la escucha, aceptamos conexiones pendientes mediante la llamada a la función accept(). Esta función saca de la cola de peticiones una conexión y crea un nuevo socket para tratarla. Recordemos qué sucede cuando llamamos al número de información de Telefónica: marcamos el número (connect()) y como hay alguien atento a coger el teléfono (listen()), se acepta nuestra llamada que pasa a la espera (en función del backlog de listen() puede que nos informen de que todas las líneas están ocupadas). Después de tener que escuchar una horrible sintonía, se nos asigna un telefonista (accept()) que atenderá nuestra llamada. Cuando se nos ha asignado ese telefonista, en realidad estamos hablando ya por otra línea, porque cualquier otra persona puede llamar al número de información de Telefónica y la llamada no daría la señal de comunicando. Nuestro servidor puede aceptar varias peticiones (tantas como indiquemos en el backlog de listen()) y luego cuando aceptemos cada una de ellas, se creará una línea dedicada para esa petición (un nuevo socket por el que hablar). Veamos un ejemplo con todo esto:

#define PUERTO 80

int mi_socket, nuevo, tam;
struct sockaddr_in mi_estructura;

mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( PUERTO );
mi_estructura.sin_addr.s_addr = INADDR_ANY;
memset( &(mi_estructura.sin_zero), ‘’, 8 );

mi_socket = socket( AF_INET, SOCK_STREAM, 0 );

tam = sizeof( struct sockaddr );

bind( mi_socket, (struct sockaddr *)&mi_estructura, tam );

listen( mi_socket, 5 );

while( 1 ) // bucle infinito para tratar conexiones
{
  nuevo = accept( mi_socket, (struct sockaddr *)&mi_estructura,
           &tam );
  if( fork() == 0 )
  { // hijo
    close( mi_socket ); // El proceso hijo no lo necesita
    send( nuevo, "200 Bienvenido\n", 15, 0);
    close( nuevo );
    exit( 0 );
  } else { // padre
    close( nuevo );  // El proceso padre no lo necesita
  }
}

Lo más extraño de este ejemplo puede ser el bucle “while”, todo lo demás es exactamente igual que en anteriores ejemplos. Veamos ese bucle: lo primero de todo es aceptar una conexión de las que estén pendientes en el backlog de conexiones. La llamada a accept() nos devolverá el nuevo socket creado para atender dicha petición. Creamos un proceso hijo que se encargue de gestionar esa petición mediante fork(). Dentro del hijo cerramos el socket inicial, ya que no lo necesitamos, y enviamos “200 Bienvenido\n” por el socket nuevo. Cuando hayamos terminado de atender al cliente, cerramos el socket con close() y salimos. En el proceso padre cerramos el socket “nuevo”, ya que no lo utilizaremos desde este proceso. Este bucle se ejecuta indefinidamente, ya que nuestro servidor deberá atender las peticiones de conexión indefinidamente.

Ya hemos visto cómo cerrar un socket, utilizando la llamada estándar close(), como con cualquier fichero. Esta función cierra el descriptor de fichero del socket, liberando el socket y denegando cualquier envío o recepción a través del mismo. Si quisiéramos tener más control sobre el cierre del socket podríamos usar la función shutdown():

int shutdown(int s, int how);

En el parámetro “how” indicamos cómo queremos cerrar el socket:

  • 0: No se permite recibir más datos.
  • 1: No se permite enviar más datos.
  • 2: No se permite enviar ni recibir más datos (lo mismo que close()).

Esta función no cierra realmente el descriptor de fichero del socket, sino que modifica sus condiciones de uso, es decir, no libera el recurso. Para liberar el socket después de usarlo, deberemos usar siempre close().

Ejemplos con sockets de tipo stream

Servidor

He aquí el código de ejemplo de un servidor sencillo TCP:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

int main( int argc, char *argv[] )
{
  int mi_socket, nuevo, tam;
  struct sockaddr_in mi_estructura;

  mi_estructura.sin_family = AF_INET;
  mi_estructura.sin_port = 0;
  mi_estructura.sin_addr.s_addr = INADDR_ANY;
  memset( &(mi_estructura.sin_zero), '', 8 );

  mi_socket = socket( AF_INET, SOCK_STREAM, 0 );

  tam = sizeof( struct sockaddr );

  bind( mi_socket, (struct sockaddr *)&mi_estructura, tam );

  listen( mi_socket, 5 );

  while( 1 ) // bucle infinito para tratar conexiones
  {
    nuevo = accept( mi_socket,
                   (struct sockaddr *)&mi_estructura, &tam);
    if( fork() == 0 )
    { // hijo
      close( mi_socket ); // El proceso hijo no lo necesita
      send( nuevo, "200 Bienvenido\n", 15, 0 );
      close( nuevo );
      exit( 0 );
    } else { // padre
      close( nuevo );  // El proceso padre no lo necesita
    }
  }

  return 0;
}

Un ejemplo de su ejecución:

txipi@neon:~$ gcc servertcp.c -o servertcp
txipi@neon:~$ ./servertcp &
1 419
txipi@neon:~$ netstat -pta
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address  Foreign Address         State
PID/Program name

tcp        0      0 *:www          *:*                    LISTEN
-
tcp        0      0 *:46101        *:*                    LISTEN
419/servertcp

txipi@neon:~$ telnet localhost 46101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
200 Bienvenido
Connection closed by foreign host.

Al haber indicado como número de puerto el 0, ha elegido un número de puerto aleatorio de entre los posibles (de 1024 a 65535, ya que sólo root puede hacer bind() sobre los puertos “bajos”, del 1 al 1024).

Cliente

He aquí el código de ejemplo de un simple cliente TCP:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define SIZE 255

int main(int argc, char *argv[])
{
  int mi_socket, tam, numbytes;
  char buffer[SIZE];
  struct sockaddr_in mi_estructura;

  if( argc != 3 )
  {
    printf( "error: modo de empleo: clienttcp ip puerto\n" );
    exit( -1 );
  }

  mi_estructura.sin_family = AF_INET;
  mi_estructura.sin_port = htons( atoi( argv[2] ) );
  inet_aton( argv[1], &(mi_estructura.sin_addr) );
  memset( &(mi_estructura.sin_zero), '', 8 );

  mi_socket = socket( AF_INET, SOCK_STREAM, 0);
  tam = sizeof( struct sockaddr );
  connect( mi_socket, (struct sockaddr *)&mi_estructura, tam );

  numbytes = recv( mi_socket, buffer, SIZE-1, 0 );
  buffer[numbytes] = '';
  printf( "%d bytes recibidos\n", numbytes );
  printf( "recibido: %s\n", buffer );

  close( mi_socket );
  return 0;
}

Veámoslo en funcionamiento:

txipi@neon:~/programacion$ gcc clienttcp.c -o clienttcp
txipi@neon:~/programacion$ ./clienttcp 127.0.0.1 46105
15 bytes recibidos
recibido: 200 Bienvenido

Ejemplos con sockets de tipo datagrama

Servidor

He aquí el código de ejemplo de un servidor sencillo UDP:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define PUERTO 5000
#define SIZE 255

int main(int argc, char *argv[])
{
  int mi_socket, tam, numbytes;
  char buffer[SIZE];
  struct sockaddr_in mi_estructura;

  mi_estructura.sin_family = AF_INET;
  mi_estructura.sin_port = htons( PUERTO );
  mi_estructura.sin_addr.s_addr = INADDR_ANY;
  memset( &(mi_estructura.sin_zero), '', 8);

  mi_socket = socket( AF_INET, SOCK_DGRAM, 0);
  tam = sizeof( struct sockaddr );
  bind( mi_socket, (struct sockaddr *)&mi_estructura, tam );

  while( 1 ) // bucle infinito para tratar conexiones
  {
    numbytes = recvfrom( mi_socket, buffer, SIZE-1, 0, (struct sockaddr *)&mi_estructura, &tam );
    buffer[numbytes] = '';
    printf( "serverudp: %d bytes recibidos\n", numbytes );
    printf( "serverudp: recibido: %s\n", buffer );
  }

  close( mi_socket );

  return 0;
}

El puerto a la escucha se define con la constante PUERTO, en este caso tiene el valor 5000. Con “netstat –upa” podemos ver que realmente está a la escucha:

txipi@neon:~$ netstat -upa
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address    Foreign Address       State       PID/Program name
udp        0      0 *:talk           *:*                   -
udp        0      0 *:ntalk          *:*                   -
udp        0      0 *:5000           *:*                    728/serverudp
Cliente

He aquí el código de ejemplo de un simple cliente UDP:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define SIZE 255

int main(int argc, char *argv[])
{
  int mi_socket, tam, numbytes;
  char buffer[SIZE];
  struct sockaddr_in mi_estructura;

  if( argc != 3 )
  {
    printf( "error: modo de empleo: clienttcp ip puerto\n" );
    exit( -1 );
  }

  mi_estructura.sin_family = AF_INET;
  mi_estructura.sin_port = htons( atoi( argv[2] ) );
  inet_aton( argv[1], &(mi_estructura.sin_addr) );
  memset( &(mi_estructura.sin_zero), '', 8 );

  mi_socket = socket( AF_INET, SOCK_DGRAM, 0);

  tam = sizeof( struct sockaddr );

  strcpy( buffer, "Hola mundo telematico!\n" );
  numbytes = sendto( mi_socket, buffer, strlen(buffer), 0, (struct sockaddr *)&mi_estructura, tam );
  printf( "clientudp: %d bytes enviados\n", numbytes );
  printf( "clientudp: enviado: %s\n", buffer );

  strcpy( buffer, "Este es otro paquete.\n" );
  numbytes = sendto( mi_socket, buffer, strlen(buffer), 0, (struct sockaddr *)&mi_estructura, tam );
  printf( "clientudp: %d bytes enviados\n", numbytes );
  printf( "clientudp: enviado: %s\n", buffer );

  close( mi_socket );

  return 0;
}

Veámoslo en funcionamiento:

txipi@neon:~$ gcc clientudp.c -o clientudp
txipi@neon:~$ ./clientudp 127.0.0.1 5000
clientudp: 23 bytes enviados
clientudp: enviado: Hola mundo telematico!

clientudp: 22 bytes enviados
clientudp: enviado: Este es otro paquete.

serverudp: 23 bytes recibidos
serverudp: recibido: Hola mundo telematico!

serverudp: 22 bytes recibidos
serverudp: recibido: Este es otro paquete.

13 pensamientos en “Curso de programación en C para GNU/Linux (VIII)

  1. Rafa Carmona

    Impresionante!!! Muy bueno….
    Por pedir que no quede , seria interesante, hacer una recopilacion y montarse un PDF , PARA ENMARCARLO !!!

    Me lo he estado cogiendo , y lo he metido en un odt, pero….tengo problemas con las imagenes….
    ( pa leerlo más tranquilamente en casa )

    Responder
  2. adrihin

    Txipi yo lo estoi juntando en el .doc porque no tengo tiempo de leerlo en internet y si kieres cuando se acaben las lecciones me lo dices y te envio el .doc para que lo pases a pdf y lo cuelgues..

    Responder
  3. señor x

    bueno probe los codigos de los sockets no orientados a conexión y les hacen falta declarar las cabeceras stdio.h, string.h y stdlib.h espero lo documenten mejor para la proxima ok

    Responder
  4. señor x

    bueno ya lo hice funcionar solo le faltan esas librerias y me pareece que por ahi le faltan unos corchetes cuando hacen referencia a un elemento del argv, y bueno si funciona pero me gustaria saber como hacer para poder enviar un archivo o mejor aun un video mediante el socket udp si alguien sabe porfavor mandeme su codigo a sr_nataz@hotmail.com gracias o si no pues proporcionenme alguna idea para hacerlo ok

    Responder
  5. Adolfo Leon Hernandez Abadia

    Mucho gusto.

    En la actualidad estoy desarrollando un proyecto que require leer los datos de un PLC y deseo emplear data_gramas, los PLC tiene un covertidor de protocolo Modbus a Ethernet. como aplicar los socket.

    Responder
  6. Jose Luis García

    sencillamente fantástico…..hacía tiempo que no leía algo con conceptos tan complicados tan bien explicados.

    seguro que me servirá para hacer el Pinger que toy intentando hacer en C….¿Alguna ayuda?

    Responder
  7. Adolfo Leon Hernandez A

    Mucho gusto,

    Tengo un servidor Linux y ya logre leer las variables de 18 PLC con modpoll de http://www.modbusdriver.com "Free Modbus Tool", ahora necesito escribir a los PLC. como los puedo hacer, con herremienta que tengo no veo ejemplos como escribir.

    Me pueden yudar.

    Responder
  8. Luis Enrique

    Amigos una gran ayuda por favor…ªª

    Que tal amigos un fuerte saludo a Todos soy nuevo por aca, miren sucede que mi profesor me dejó hacer un proyecto en Visual Basic utilizando Socket’s pero la finalidad es hacer un buzón de correo electrónico en el que pueda leer un mensaje, la verdad no tengo ni la mas minima idea de como hacerlo solo se que es utilizando 1 Servidor de Correo y 1 Cliente de Correo (Cliente/Servidor).
    Este programa si alguien ya lo tiene echo se los agradecería mucho
    El cliente deberá “Leer Correo” y “Enviar Correo”.
    El Servidor es quien Maneja la Base de Datos
    Utilizando el Puerto que uno guste.
    Muchas gracias por su ayuda muchachos y espero que alguien me pueda apoyar
    Y de nuevo …!!Un saludo a Todos !!!

    Responder

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *