Programacion de sockets en lenguaje C.

Tutorial

Programación de sockets en lenguaje C.

Indice.

  1. Notas del autor
  2. ¿Qué es un socket?
  3. Dominios de comunicación.
  4. Tipos de socket en AF_INET.
  5. Byte order.
  6. Creación de un socket.
  7. Función bind().
  8. Estructuras
  9. Otras funciones utilizadas.
  10. Asignación de valores a una variable tipo sockaddr_in.
  11. Pasos para establecer la conexión.
  12. Descripción de las funciones
  13. Ejempo cliente-servidor simple.
  14. Ejemplo cliente -servidor concurrente.
  15. Bloqueo.
  16. Función select()
  17. Ejemplos.
  18. Bibliografía.
  19. Copyright.


1 - Notas del autor.

Aún se están corrigiendo errores, incrementando el contenido
y la cantidad de ejemplos. Ultima modificación 20/4/2000.

Hice todos los esfuersos para que este tutorial no contenga errores, pero
legalmente no me responsabilizo por cualquier daño ocasionado por
este tutorial o por cualquier parte del mismo.

Este tutorial aún cotinúa corrigiéndose y aumentando
su contenido. Le aconsejo visitar su sitio web oficial para verificar si
fue modificado.

El sitio oficial es :http://www.starlinux.net

1.1 - Agradecimientos ( por orden de aparición ).

Nov. 1999
Daniel Sacco.
Proporcionó los gráficos de las sesiones Nros. 13 y
14.

2/1/2000
Raul Cejas.
Se agregaron las secciones Nros. 15 y 16 para responder sus preguntas.


2 - ¿Qué es un socket?

  • Es una interfaz de entrada-salida de datos que permite la intercomunicación
    entre procesos.
  • Los procesos pueden estar ejecutándose en el mismo o en distintos
    sistemas, unidos mediante una red.

2.1 - La analogía con los teléfonos.

Los sockets permiten la comunicación entre procesos, como los teléfonos
permiten la comunicación entre las personas.


3 - Dominios de comunicación.

Los sockets se crean dentro de un dominio de comunicación, igual
que un archivo se crea dentro de un filesystem.

El dominio de comunicación nos dice donde se encuentran los procesos
que se van a intercomunicar.

Si los procesos están en el mismo sistema, el dominio de comunicación
será AF_UNIX, si los procesos están en distintos sistemas
y estos se hallan unidos mediante una red TCP/IP, el dominio de comunicación
será AF_INET.

Cabe aclarar que existen otros dominios de comunicación.

Los sockets no se han diseñado solamente para TCP/IP. La idea original
fue que se usase la misma interfaz también para distintas familias
de protocolos.

En esta introducción solo trataremos el dominio AF_INET.

Algunos dominios:

  • AF_INET ( unidos mediante una red TCP/IP).
  • AF_UNIX (en el mismo sistema).
  • Otros dominios.

4 - Tipos de sockets en el dominio AF_INET.

  • Sockets Stream.
  • Sockets Datagram.
  • Sockets Raw.






Figura 4-1.

Sockets Stream son los más utilizados, hacen uso del protocolo
TCP ( figura 4-1), el cual nos provee un flujo de datos bidireccional, secuenciado,
sin duplicación de paquetes y libre de errores.

La especificación del protocolo TCP se puede leer en la RFC-793 .

Sockets Datagram hacen uso del protocolo UDP, el cual nos provee
un flujo de datos bidireccional, pero los paquetes pueden llegar fuera de
secuencia, pueden no llegar o contener errores.

Por lo tanto el proceso que recibe los datos debe realizar resecuencimiento,
eliminar duplicados y asegurar la confiabilidad.

Se llaman también sockets sin conexión, porque no hay que
mantener una conexión activa, como en el caso de sockets stream.

Son utilizados para transferencia de información paquete por paquete.
Ejemplo: dns, tftp, bootp, etc.

Entoces podríiamos preguntar, Cómo hacen estos programas para
funcionar si pueden perder datos ?

Ellos implementan un protocolo encima de UDP que realiza control de errores.


La especificacion del protocolo UDP se puede leer en la RFC-768.

Sockets raw no son para el usuario más común, son
provistos principalmente para aquellos interesados en desarrollar nuevos
protocolos de comunicación o para hacer uso de facilidades ocultas
de un protocolo existente.


5 - Byte order.

Network byte order y Host byte order son dos formas en las que el sistema
puede almacenar los datos en memoria.

Está relacionado con el orden en que se almacenan los bytes en la
memoria RAM.

Si al almacenar un short int (2 bytes) o un long int (4 bytes) en RAM,
en la posición más alta se almacena el byte menos significativo,
entonces está en network byte order, caso contrario es host byte order
( figura 5-1).

Network byte order.

short int.

byte mas significativo
byte menos significativo

Direccion n n+1


long int.

byte mas significativo


byte menos significativo

Direccion n n+1 n+2 n+3

Host byte order.

short int.

byte mas significativo
byte menos significativo

Direccion n+1 n


long int.

byte mas significativo


byte menos significativo

Direccion n+3 n+2 n+1 n

Figura 5-1.

Esto depende del microprocesador que se esté utilizando, podríamos
estar programando en un sistema host byte order o network byte order, pero
cuando enviamos los datos por la red deben ir en un orden especificado, sino
enviaríamos todos los datos al revés. Lo mismo sucede cuando
recibimos datos de la red, debemos ordenarlos al orden que utiliza nuestro
sistema. Debemos cumplir las siguientes reglas :

  • Todos los bytes que se transmiten hacia la red, sean números
    IP o datos, deben estar en network byte order.
  • Todos los datos que se reciben de la red, deben convertirse a host
    byte order.




Figura 5-2.

Para realizar estas conversiones utilizamos las funciones que se describen
a continuación.

5.1 - Funciones de conversión.

htons() - host to network short - convierte un short int de host byte
order a network byte order.

htonl() - host to network long - convierte un long int de host byte order
a network byte order.

ntohs() - network to host short - convierte un short int de network byte
order a host byte order.

ntohl() - network to host long - convierte un long int de network byte
order a host byte order.

Puede ser que el sistema donde se esté programando almacene los
datos en network byte order y no haga falta realizar ninguna conversión,
pero si tratamos de compilar el mismo código fuente en otra plataforma
host byte order no funcionará. Como conclusión, para que nuestro
código fuente sea portable se debe utilizar siempre las funciones
de conversión.

Para más información : man 3 byteorder

5.2 - Ejemplo:

#include

...

port = htons ( 3490 ); // Convertimos a network byte
order el número de port que utilizamos.


...


6 - Creación de un socket.



Analogía:

Para que una persona pueda recibir llamadas debe tener instalado un teléfono,
para poder realizar una conexión se debe crear un socket.

Los sockets se crean llamando a la función socket(), esta función
retorna un descritpor de socket, que es tipo int.

Si hubo algún error, socket() retorna -1 y la variable global errno
se establece con un valor que indica el error que se produjo ( ver man 3
perror).

sockfd = socket ( int dominio, int tipo, int protocolo );

sockfd
Es el descriptor de socket devuelto. Luego se utilizará
para conectarse, recibir conexiones, enviar y recibir datos, etc.

dominio
Dominio donde se realiza la conexión.

Para este tutor siempre será AF_INET.

tipo
Podrá ser SOCK_STREAM o SOCK_DGRAM o SOCK_RAW.

protocolo
0 (cero, selecciona el protocolo más apropiado).

Veamos un ejemplo:

#include

#include

....

int sockfd;

sockfd =socket ( AF_INET, SOCK_STREAM, 0 );


...

Si se especifica al protocolo como cero, el sistema selecciona el protocolo
apropiado de uno de los protocolos disponibles, dependiendo del tipo de
socket requerido.

Para más información: man 2 socket

6.1 - Selección del protocolo:



Se puede especificar el número de protocolo o también seleccionar
el protocolo por su nombre utilizando la función getprotobyname(),
que retorna una estructura tipo protent con los siguientes datos:

char *p_name; /* Nombre oficial del protocolo */

char **p_aliases; /* lista de alias. */

int p_proto; /* número de protocolo */

Esta función lee el archivo /etc/protocols para completar la estructura.

pp=getprotobyname(\"tcp\");
s=socket(AF_INET, SOCK_STREAM, pp->p_proto);



Para más información : man 3 getprotoent

6.2 - Errores en la creación:

Los errores más comunes son :

  • El protocolo especificado no está soportado dentro del dominio.
  • Permiso negado para crear el socket del tipo y/o protocolo especificado.
  • Etc.


7 - Función Bind()

Analogía:

Para poder recibir llamadas, se debe tener un número telefónico,
para poder recibir conexiones, se le debe asignar un nombre al socket.

El socket se crea sin nombre, debemos asignarle uno para poder recibir
conexiones.

bind () se utiliza para darle un nombre al socket, osea una dirección
IP y número de puerto del host local por donde escuchará, al
especificar una IP del host local le estamos diciendo por cual interfaz física
escuchará (el sistema puede tener varias interfaces ethernet, ppp,
etc).

Es necesario llamar a bind() cuando se está programando un servidor.


Cuando se está programando un cliente generalmente no se utiliza
esta función, el kernel le asignará al socket la dirección
IP y número de puerto disponible al llamar a ciertas funciones, por
ejemplo cuando llamamos a connect() para conectarnos con un sistema remoto.


En el servidor es necesario llamar a bind() debido a que el número
de puerto debe ser conocido para que los clientes puedan conectarse. Por
ejemplo si estamos programando un servidor de telnet debemos llamar a bind()
para asignarle al socket el puerto 23.

En la aplicación cliente se puede llamar a bind() y asignarle un
número de puerto, pero no es necesario porque nadie va a tratar ni
podra conectarse con él.

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

sockfd :
Es el descriptor de socket devuelto por la función socket().

my_addr :
Es un puntero a una estuctura sockaddr que contiene la IP
del host local y el número de puerto que se va a asignar al socket.


(Mas abajo se detalla).

addrlen :
Debe ser establecido al tamaño de la estuctura sockaddr. sizeof(struct
sockaddr).

Ejemplo :

...
struct sockaddr_in sin;
...
bind ( sockfd, (struct sockaddr *) &sin, sizeof (sin) );



NOTA: Nos queda por ver como está formada la estructura sockaddr_in
y como establecemos su valor.


8 - Estructuras.

Almacenan el nombre del socket. Se utilizan con la función bind().

struct sockaddr

{

unsigned short sa_family; // AF_*

char sa_data[14]; // Direccion de
protocolo.


};

struct sockaddr_in

{

short int sin_family; // AF_INET

unsigned short sin_port; // Numero de puerto.


struct in_addr sin_addr; // Dirección IP.


unsigned char sin_zero[8]; // Relleno.

};

struct in_addr

{

unsigned long s_addr; // 4 bytes.

};

La primer estructura, sockaddr, almacena la dirección de protocolo
para muchos tipos de protocolos.

sa_family puede ser AF_INET, AF_UNIX u otros dominios, para nuestro
tutorial solo será AF_INET.

sa_data contiene la dirección IP y número de puerto
asignado al socket.

Se creó la esctuctura sockaddr_in para el caso de internet, para
poder referenciar los elementos de forma más fácil.

Los punteros a la estructura sockaddr_in deben ser precedidos con un cast
tipo *struct sockaddr antes de pasarlos como parámetros a funciones.

Notas sobre sockaddr_in:

  • sin_family sera AF_INET
  • sin_port (número de puerto) y sin_addr (dirección IP)
    deberán estar en network byte order, osea habrá que usar htons().
  • sin_family no debe convertirse a network byte order porque es solo
    usado por el kernel y no es enviado por la red.
  • sin_zero se utiliza para rellenar la estructura a la longuitud de
    sockaddr, debe estar inicializada a cero con la función bzero(). Ver
    la página del manual


9 - Otras funciones utilizadas.

  • inet_addr()

Convierte una dirección IP en notación números y puntos,
en un unsigned long, retorna la dirección en network byte order. Retorna
-1 si hubo error.

Ejemplo:

struct sockaddr_in ina;
....
ina.sin_addr.s_addr=inet_addr(\"192.168.1.1\");
  • inet_ntoa()

Realiza la conversión inversa, convierte una direccion IP en unsigned
long en network byte order, a un string en números y puntos.

Ejemplo:

printf(\"%s\", inet_ntoa(ina.sin_addr));



inet_ntoa() retorna un puntero a un array de caracteres, que se encuentra
almacenado estáticamente dentro de inet_ntoa(). Cada vez que se llama
a inet_ntoa() se sobreescribe la última dirección IP.

Ejemplo:

char *addr1, *addr2;
addr1=inet_ntoa(ina1.sin_addr)       /* Supongamos que vale 200.41.32.127 */
addr2=inet_ntoa(ina2.sin_addr)      /* Supongamos que vale 132.241.5.10 */
printf(\"direccion 1: \\n\", addr1);
printf(\"direccion 2: \\n\", addr2);

Esto imprimirá:

 direccion 1: 132.241.5.10
 direccion 2: 132.241.5.10



Se ve que la primer dirección IP es sobreescrita en la segunda llamada
a inet_ntoa() y se pierde su valor.

Para que esto no suceda, luego de la primera llamada a inet_ntoa() se debe
usar strcpy() para guardar la primera dirección IP y luego llamar
por segunda vez a inet_ntoa(), de esta manera no se pierde la primera dirección
IP.

Ver : man 3 inet


10 - Asignación de valores a una variable tipo sockaddr_in



Debemos asignarle valores a una variable tipo sockaddr_in antes de llamar
a las función bind().

Veamos un ejemplo:

...

..

struct sockaddr_in my_addr;

......

my_addr.sin_family = AF_INET;

my_addr.sin port = htons ( 3490 );
// Numero de puerto por donde escuchara el servidor.



my_addr.sin_addr.s_addr = inet_addr (\"132.241.5.10\");
// IP de la interface por donde escuchara el servidor.


bzero ( &(my_addr.sin_zero), 8);
// Relleno con ceros.

Notas :

  • Si asignamos el valor cero a sin_port, el sistema nos dará
    automáticamente un puerto disponible.
my_addr.sin_port=0;
  • Podemos automatizar la asignación de la IP, si ponemos el valor
    INADDR_ANY a s_addr, el sistema le asignará la dirección IP
    local. Recordar que el programa puede ejecutarse en distintas PC\'s con distintas
    IP\'s
my_addr.sin_addr.s_addr = htonl (INADDR_ANY);
  • Las variables my_addr.sin_port y my_addr.sin_addr.s_addr
    deben estar en network byte order, son valores que viajan por la red, pero
    my_addr.sin_family no porque solo es utilizado por el kernel
    para saber que tipo de dirección contiene la estructura.


11 - Pasos para establecer la conexión.

11.1 - Caso sockets stream.

En la figura 11.1 se visualiza los pasos para realizar una conexión
mediante sockets stream.

Primero haremos una descripción funcional, para luego poder realizar
un estudio más detallado de cada función utilizada.




Figura 11.1

  • Ambos, cliente y servidor, deben crean un socket mediante la función
    socket(), para poder comunicarse.
  • El servidor llama a bind() para darle un nombre al socket, para
    luego poder recibir conexiones, es decir establece por cual número
    de puerto escuchará.  Por ejemplo si este sería un servidor
    de telnet, establecería el puerto 23. Para el cliente no es necesario
    establecer el número de puerto, porque no recibirá intentos
    de conexión, sólo intentará conectarse con el servidor
    .
  • El servidor hablilita su socket para poder recibir conexiones,
    llamando a la función listen(). El cliente no necesita realizar este
    paso porque no va a recibir conexiones, solo intentará conectarse
    con el servidor.
  • El servidor ejecuta la función accept() y queda en estado
    de espera, la función accept() no retorna hasta que intenten conectarse.El
    cliente usa la función connect() para realizar el intento de conexión,
    en ese momento la función accept() del servidor retorna con un parámetro
    que es un nuevo descriptor de socket, el cual se utiliza para realizar la
    transferencia de datos por la red con el cliente.
  • Una vez establecida la conexión se utilizan las funciones
    send() y recv() con el descriptor de socket del paso anterior para realizar
    la transferencia de datos.
  • Para finalizar la conexión se utilizan las funciones close()
    o shutdown().

11.2 - Caso sockets datagram.

La figura 11.2 es para el caso de utilizar sockets datagram.




Figura 11.2

  • Ambos, cliente y servidor, crean un socket mediante la función
    socket().
  • El servidor debe establecer por qué número de puerto
    recibirá los datos, en este caso no existe la conexión, los
    datos se envían como si fueran mensajes.
  • Para realizar transferencia de datos, utilizan las funciones sendto()
    y recvfrom().



Se visualiza que no es necesario que el servidor llame a la función
listen(), ni tampoco el paso connect() / accept().

Ahora realizaremos un estudio más detallado de las funciones.


12 - Descripción de la funciones.

Se recomienda ir viendo simultáneamente las páginas del manual
de las siguientes funciones, que contienen información mucho más
detallada.

socket() y bind() fueron descriptas más arriba.

  • listen()

Se llama desde el servidor, habilita el socket para que pueda recibir conexiones.
Solo se aplica a sockets tipo SOCK_STREAM.

int listen ( int sockfd, int backlog)

sockfd :
Es el descriptor de socket devuelto por la función socket()
que será utilizado para recibir conexiones.

backlog :
Es el número máximo de conexiones en la cola
de entrada de conexiones. Las conexiones entrantes quedan en estado de espera
en esta cola hasta que se aceptan ( accept () ).

  • accept()

Se utiliza en el servidor, con un socket habilitado para recibir conexiones
( listen() ). Esta función retorna un nuevo descriptor de socket
al recibir la conexión del cliente en el puerto configurado.

La llamada a accept() no retornará hasta que se produce una conexión
o es interrumpida por una señal.

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

sockfd :
Es el descriptor de socket habilitado para recibir conexiones.

addr :
Puntero a una estructura sockadd_in. Aquí se almacenará
informacion de la conexión entrante. Se utiliza para determinar que
host está llamando y desde qué número de puerto.

addrlen :
Debe ser establecido al tamaño de la estuctura sockaddr.


sizeof(struct sockaddr).

accept() no escribirá más de addrlen bytes en addr . Si
escribe

menos bytes, modifica el valor de addrlen a la cantidad de bytes

escritos.

Ejemplo:

...
int sockfd, new_sockfd;
struct sockaddr_in  my_addr;
struct sockaddr_in  remote_addr;
int addrlen;
...
// Creo el socket.
sockfd = socket (AF_INET, SOCK_STREAM, 0 );
...
// Se le asigna un número de puerto al socket por donde el servidor escuchará.
// Antes de llamar a bind()  se debe asignar valores a my_addr.
bind (sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr) );
// Se habilita el socket para poder recibir conexiones.
listen ( sockfd, 5);
addrlen = sizeof (struct sockaddr );
...
// Se llama a accept() y el servidor queda en espera de conexiones.
new_sockfd = accept ( sockfd, &remote_addr, &addrlen);
...
  • connect()

Inicia la conexión con el servidor remoto, lo utiliza el cliente
para conectarse.

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

sockfd :
Es el descriptor de socket devuelto por la función socket().

serv_addr :
Es una estructura sockaddr que contine la dirección
IP y número de puerto destino.

addrlen :
Debe ser inicializado al tamaño de struct sockaddr

( sizeof (struct sockaddr) ).

  • send() y recv()

Después de establecer la conexión, se puede comenzar con
la transferencia de datos. Estas dos funciones son para realizar transferencia
de datos sobre sockets stream.

send() y recv() son identicas a write() y read(), exepto que se agrega un
parametro flags.

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

sockfd :
Descriptor socket por donde se enviarán los datos.

msg :
Puntero a los datos a ser enviados.

len :
Longitud de los datos en bytes.

flags :
Leer: man 2 send

send() retorna la cantidad de datos enviados, la cual podrá ser
menor que la cantidad de datos que se escribieron en el buffer para enviar
. send() enviará la máxima cantidad de datos que pueda manejar
y retorna la cantidad de datos enviados, es responsabilidad del programador
comparar la cantidad de datos enviandos con len y si no se enviaron todos
los datos, enviarlos en la próxima llamada a send().

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

sockfd :
Descriptor socket por donde se recibirán los datos.

buf :
Puntero a un buffer donde se almacenarán los datos
recibidos.

len :
Longitud del buffer buf.

flags :
Ver : man 2 recv

Si no hay datos a recibir en el socket , la llamada a recv() no retorna
(bloquea) hasta que llegan datos, se puede establecer al socket como no
bloqueante (ver: man 2 fcntl ) de manera que cuando no hay datos para recibir
la función retorna -1 y establece la variable errno=EWOULDBLOCK. Más
adelante se hablará de esto.

recv() retorna el número de bytes recibidos.

  • sendto() y recvfrom()

Funciones para realizar transferencia de datos sobre sockets datagram.

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

sockfd :
Descriptor socket por donde se enviarán los datos.

msg :
Puntero a los datos a ser enviados.

len :
Longitud de los datos en bytes.

flags :
Leer : man 2 sendto

to :
Puntero a una estructura sockaddr que contiene la dirección
IP y número de puerto destino.

tolen :
Debe ser inicializado al tamaño de struct sockaddr

( sizeof (struct sockaddr) ).

sendto() retorna el número de bytes enviados, el cual puede ser
menor que len, igual que en send().

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

sockfd :
Descriptor socket por donde se recibirán los datos.

buf :
Puntero a un buffer donde se almacenarán los datos recibidos.

len :
Longitud del buffer buf.

flags :
Ver la página del manual de recv().

from :
Puntero a una estructura sockaddr que contiene la dirección
IP y número de puerto del host origen de los datos.

fromlen :
Debe ser inicializado al tamaño de struct sockaddr

( sizeof (struct sockaddr) ).

Si no hay datos a recibir en el socket , la llamada a recvfrom() no retorna
(bloquea) hasta que llegan datos, se puede establecer al socket como no bloqueante
(ver: man 2 fcntl ) de manera que cuando no hay datos para recibir la función
retorna -1 y establece la variable errno=EWOULDBLOCK. Más adelante
se hablará de esto.

recvfrom() retorna el numero de bytes recibidos, igual que recv().

  • close () y shutdown ().

Finalizan la conexión del descriptor de socket.

close ( sockfd)

Despues de utilizar close, el socket queda desabilitado para realizar lecturas
o escrituras.

shutdown (sockfd, int how)

Permite desabilitar la comunicación en una determinada dirección
o en ambas direcciones.

how : Especifica en qué dirección se desabilita la comunicación.
Puede tomar los siguientes

valores :

0 : Se desabilita la recepción.

1 : se desabilita el envío.

2 : se desabilitan la recepción y el envío, igual que en
close ().

  • getpeername(), gethostname(), gethostbyname().
int getpeername ( int sockfd, struct sockaddr *addr, int *addrlen )

Nos dice quién está conectado en el otro extremo de un socket
stream.


sockfd :
Descriptor del socket stream conectado.

addr :
Puntero a una estructura sockaddr que almacenará la dirección
del otro extremo de la conexión.

addrlen :
Puntero a un int que contiene la longuitud de sockaddr

( sizeof (sockaddr) ).

int gethostname ( char *hostname, size_t size )

Retorna el nombre del sistema donde esta ejecutandose el programa.

El nombre puede ser utilizado por gethostbyname() para determinar la direccion
IP del host local.


hostname :
Puntero a un array de caracteres que contendra el nombre

del host.

size :
Longitud en bytes del array hostname.

gethostname () retorna cero cuando se ejecuta con éxito.

struct hostent *gethostbyname (const char *name)

Se utiliza para convertir un nombre de un host a su dirección IP,
osea utiliza el servidor de nombres.

Ejemplo: Supongamos que estamos programando un cliente telnet.

$ telnet algun.sitio.com

La aplicación cliente primero debe transladar el nombre del sitio
(algun.sitio.com) a conectarse a su dirección IP, para luego poder
realizar todos los pasos de conexión anteriormente descriptos.

Retorna un puntero a una estructura hostent, que está formada como
sigue:

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]



Descripción de los campos de la estructura hostent:


h_name :
Nombre oficial del host.

h_aliases :
Array de nombres alternativos.

h_addrtype :
Tipo de dirección que se retorno ( AF_INET ).

h_length :
Longitud de la dirección en bytes.

h_addr_list :
Array de direcciones de red para el host.

h_addr :
La primer dirección en h_addr_list.

En el caso de producirse algún error devuelve NULL y establece
la variable h_errno con el número de error, en vez de la variable
errno ( ver man herror).

Nota : Todas estas funciones excepto gethostbyname(),
retornan -1 cuando se produce un error y establecen la variable errno.



13 - Ejemplo cliente-servidor TCP simple


Luego del accept() el servidor queda en estado de espera hasta que el
cliente intente conectarse.

El cliente trata de conectarse con connect() y accept() retorna con un
nuevo descriptor de socket, el cual es utilizado por el server para realizar
la transferencia de datos con el cliente.

Mientras está realizando transferencia de datos con un cliente, los
intentos de conexión de otros clientes son almacenados en una cola
de conexiones, que luego serán atendidos. Los clientes se atienden
de a uno por vez en este tipo de aplicación cliente-servidor.


14 - Ejemplo cliente-servidor TCP concurrente.

Igual que en el caso anterior, luego del accept() el servidor queda en
estado de espera. Cuando el cliente intenta conectarse, accept() retorna
con un nuevo descritpor de socket, pero antes de comenzar la transferencia
de datos, el servidor realiza un fork(), apareciendo en memoria un proceso
hijo igual que el servidor, que solo se dedica a realizar la transferencia
de datos con el cliente, mientras que el proceso padre retorna al estado
en espera.

De esta forma, el servidor puede atender a varios clientes en forma simultánea,
por cada cliente que está atendiendo en forma simultánea, aparecerá
un proceso hijo ejecutándose.


15 - Bloqueo.

Supongamos un ejemplo: creamos un socket y nos conectamos a un servidor,
sabemos que el socket nos provee una comunicación bidireccional,
podemos enviar y recibir datos simultáneamente.

Llamamos a la función recv() para recibir datos, pero el servidor
en ese momento no tiene nada para enviarnos, entonces la función recv()
no retorna. Justo en ese momento queremos enviar datos hacia el servidor,
pero no podemos porque la función recv() no retornó y nos bloqueó
el programa.

Debemos encontrar alguna forma para que la función recv() retorne
aunque el servidor no envie nada. Esto se realiza estableciendo al socket
como no bloqueante.

Cuando creamos un socket, se establece como bloqueante, al llamar a ciertas
funciones como accept(), recv() , recvfrom(), etc, se bloquea el programa.

Para establecer al socket como no bloqueante utilizamos la función
fcntl() de la siguiente manera :

int fcntl ( int sockfd, int cmd, long arg);

sockfd
Descriptor de socket sobre el cual se va a realizar alguna operación.

cmd


Determina el comando que se va a aplicar, para nuestro caso usaremos el comando
F_SETFL, el cual establece los flag del descriptor al valor especificado en
arg.

arg
Argumentos que necesita el comando, para establecer el socket como
no bloqueante sera O_NONBLOCK.

Si se produce un error, retorna -1 y se establece errno especificando
el error.

Leer la página del manual de fcntl().

Ejemplo:

#include 
#include 
...
sockfd=socket(AF_INET, SOCK_STREAM, 0);
fcntl (sockfd, F_SETFL, O_NONBLOCK);
...

Una vez establecido el socket como no bloqueante, se puede llamar a la
funciones bloqueantes como recv() para recibir datos, si no hay datos disponibles
recv() devuelve -1 y establece errno=EWOULDBLOCK.

Se puede ir consultando (polling) el socket para saber si hay datos disponibles,
pero esta no es una solución muy buena, se consume tiempo de CPU consultando
al socket si tiene datos, se verá en la próxima sección
una solución más elegante.


16 - Función select()

Nos permite monitorear un conjunto de descriptores de sockets y nos avisa
cuales tienen datos para leer, cuáles están listos para escribir,
y cuáles producieron excepciones.

#include

#include

#include

int select ( int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

Monitorea 3 conjuntos distintos de sockets, aquellos agregados al conjunto
readfds los monitorea para ver si hay caracteres disponibles para leer,
al conjunto writefds los monitorea para ver si están listos para ser
escritos y al conjunto exceptfds los monitorea para ver si se producen excepciones.
Los conjuntos de descriptores son tipo fd_set.

Se proveen 4 macros para manejar los conjuntos de descriptores :

FD_ZERO ( fd_set *set )
limpia un conjunto de descriptores.

FD_SET ( int fd, fd_set *set )
agrega fd a un conjunto.

FD_CLR ( int fd, fd_set *set )
borra fd de un conjunto.

FD_ISSET ( int fd, fd_set *set )
Verifica si fd está dentro de un conjunto.

Se utiliza luego del retorno de select() para verificar cual descriptor
cambio su estado.

Aquellos conjuntos que no tienen descriptores, se especifican con NULL.

n
Es el número de descriptor más alto de cualquiera de
los 3 conjuntos, más uno.

timeout
Select puede retornar por dos causas, se produce algún cambio
en un descriptor o paso más del tiempo especificado en timeout sin
producirse cambios. Si se establece timeout a cero se retorna inmediatamente,
si establecemos timeout a NULL se monitorea hasta que se produce algun cambio
en los conjuntos, osea puede bloquear. Cuando retorna, timeout indica el
tiempo remanente.

Cuando retorna select(), modifica los conjuntos de descriptores reflejando
cuál de los descriptores está listo para leer, cuáles
para escribir y cuáles causaron excepciones.

Si se produce un error, retorna -1 y se establece errno con el número
de error.

Veamos la estructura timeval :

struct timeval
{
int tv_sec;             /*  segundos  */
int tv_usec;            /*  micro segundos  */
};

Ejemplo :

Monitoreamos la entrada estandard ( descriptor 0 )

#include 
#include 
#include 
#include 
#define STDIN  0         /* descriptor para la entrada estandard */
main()
{
struct timeval timeout;
fd_set readfds;
timeout.tv_sec = 2;
timeout.tv_usec = 500000 ;
FD_ZERO ( &readfds );
FD_SET ( STDIN, &readfds );
select ( STDIN+1, &readfds, NULL, NULL, &timeout );
if ( FD_ISSET ( STDIN, &readfds ) )
printf (\" Se oprimio una tecla\\n\");
else
printf(\" se vencio el tiempo\\n\");
}

17 - Ejemplos

17.1 - Servidor Stream.

Veremos un servidor stream simple, lo único que hace es enviar la
frase \"Hello World\" hacia el cliente.

Para probarlo, ejecutar esta programa en una terminal y ejecutar el telnet
desde otra terminal de la siguiente manera:

telnet localhost 3490

Código fuente del servidor:

#include

#include

#include

#include

#include

#include

#include

#include

#define MYPORT 3490 /*Numero de puerto donde se conectaran
los clientes*/


#define BACKLOG 10 /* Tamaño de la cola de
conexiones recibidas */


main()

{

int sockfd;, /* El servidor escuchara
por sockfd */


int newfd; /* las transferencias
de datos se realizar mediante newfd */


struct sockaddr_in my_addr; /*
contendra la direccion IP y el numero de puerto local */


struct sockaddr_in their_addr; /*
Contendra la direccion IP y numero de puerto del cliente */


int sin_size;
/* Contendra el tamanio de la escructura sockaddr_in */



if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
/*Crea un socket y verifica si hubo algun error*/

{

perror(\"socket\");

exit(1);

}
/* Asignamos valores a la estructura my_addr para luego
poder llamar a la funcion bind() */


my_addr.sin_family = AF_INET; /*no debe convertirse a
network byte order, es solo utilizado por el kernel*/


my_addr.sin_port = htons(MYPORT); /*debe convertirse
a network byte order porque es enviado por la red*/


my_addr.sin_addr.s_addr = INADDR_ANY; /* automaticamente
usa la IP local */


bzero(&(my_addr.sin_zero), 8); /* rellena
con ceros el resto de la estructura */


/* Le asignamos un nombre al socket */

if ( bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct
sockaddr)) == -1)

{

perror(\"bind\");

exit(1);

}
/* Habilitamos el socket para recibir conexiones, con
una cola de 5 conexiones en espera como maximo */


if (listen(sockfd, BACKLOG) == -1)
{

perror(\"listen\");

exit(1);

}
while(1) /* loop que llama
a accept() */


{
sin_size = sizeof(struct sockaddr_in);


/*Se espera por conexiones ,*/

if ((newfd = accept(sockfd, (struct sockaddr *)&their_addr,
&sin_size)) == -1)


{
perror(\"accept\");

continue; /* Si se produce un error
se finaliza el programa */

}

printf(\"server: conexion desde: %s\\n\", inet_ntoa(their_addr.sin_addr));


/* Llamamos a fork() para crear un proceso hijo que
atendera a la conexion recien establecida


El proceso hijo sera igual que el padre, hereda los
descriptores de sockets, lo unico que los diferencia


es el valor devuelto por fork(), al padre le devuelve
el PID del hijo , y al hijo le devuelve un


valor cero . Por eso, para saber si estamos en el
proceso padre o hijo, comparamos el valor


devuelto por fork(). Si fork devuelve cero, entonces
estamos en el proceso hijo. Ver la pagina


del manual de fork. */

if (!fork())

{ /* Aca comienza
el proceso hijo, enviamos los datos mediante newfd */

if (send(newfd, \"Hello, world!\\n\", 14, 0)
== -1)


perror(\"send\");

close(newfd);

exit(0);
}

/* El proceso padre no necesita este descriptor, solo
lo utiliza el proceso hijo, en la proxima llamada a


accept(), retornara con un nuevo descriptor de socket
*/


close(newfd);

/* Se suspende la ejecucion del proceso padre hasta
que finalice el proceso hijo */


while(waitpid(-1,NULL,WNOHANG) > 0);


/* Una vez finalizado el proceso hijo, se vuelve a
llamar a accept() */

}

}

17.2- Un cliente stream.

Este cliente se conecta con el servidor anterior y visualiza el string
\"Hello World\"

Codigo fuente :

#include

#include

#include

#include

#include

#include

#include

#include

#define PORT 3490 /* El puerto
donde se conectara */


#define MAXDATASIZE 100 /* maxima cant. De bytes
que se pueden recibir en una llamada a recv */


int main(int argc, char *argv[])

{

int sockfd, numbytes; /* Contendra el
numero de bytes recibidos despues de llamar a recv() */


char buf[MAXDATASIZE]; /* Buffer donde se
reciben los datos */


struct hostent *he; /*
Se utiliza para convertir el nombre del host a su direccion IP */



struct sockaddr_in their_addr; /* direccion del
server donde se conectara */


/* Tratamiento de la linea de comandos. */


if (argc != 2)

{

fprintf(stderr,\"usage: client hostname\\n\");


exit(1);
}

/* Convertimos el nombre del host a su direccion IP */


if ((he=gethostbyname(argv[1])) == NULL)

{
herror(\"gethostbyname\");

exit(1);
}

/* Creamos el socket */

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)


{

perror(\"socket\");

exit(1);
}

/* Establecemos their_addr con la direccion del server
*/


their_addr.sin_family = AF_INET;

their_addr.sin_port = htons(PORT);

their_addr.sin_addr = *((struct in_addr *)he->h_addr);


bzero(&(their_addr.sin_zero), 8);

/* Intentamos conectarnos con el servidor */


if (connect(sockfd, (struct sockaddr *)&their_addr,
sizeof(struct sockaddr)) == -1)


{
perror(\"connect\");

exit(1);
}

/* Recibimos los datos del servidor */

if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1)


{
perror(\"recv\");

exit(1);
}

/* Visualizamos lo recibido */

buf[numbytes] = \'\\0\';

printf(\"Recivido: %s\\n\",buf);

/* Devolvemos recursos al sistema */

close(sockfd);

return 0;

}

17.3- Un servidor datagram.

Este servidor espera recibir paquetes en el puerto 4950, nos informa quién
nos envió el paquete, qué longuitud tiene y nos muestra su
contenido.

Código fuente:

#include

#include

#include

#include

#include

#include

#include

#include

#define MYPORT 4950 /* puerto donde
los cliente envian los paquetes */


#define MAXBUFLEN 100 /* Max. cantidad de bytes que
podra recibir en una llamada a recvfrom() */


main()

{

int sockfd;

struct sockaddr_in my_addr;
/* direccion IP y numero de puerto local */


struct sockaddr_in their_addr;
/* direccion IP y numero de puerto del cliente */


/* addr_len contendra el tamanio de la estructura sockadd_in
y numbytes el numero de bytes recibidos */


int addr_len, numbytes;

char buf[MAXBUFLEN];
/* Buffer de recepcion */


/* se crea el socket */

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)


{

perror(\"socket\");

exit(1);
}

/* Se establece la estructura my_addr para luego llamar
a bind() */


my_addr.sin_family = AF_INET;
/* host byte order */


my_addr.sin_port = htons(MYPORT);
/* network byte order */


my_addr.sin_addr.s_addr = INADDR_ANY; /* se asigna
automaticamente la direccion IP local */


bzero(&(my_addr.sin_zero), 8);
/* rellena con ceros el resto de la estructura */



/* Se le da un nombre al socket */

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct
sockaddr)) == -1)


{
perror(\"bind\");

exit(1);
}

/* Se reciben los datos */

addr_len = sizeof(struct sockaddr);

if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, (struct
sockaddr *)&their_addr, &addr_len)) == -1)


{
perror(\"recvfrom\");

exit(1);
}

/* Se visualiza lo recibido */

printf(\"paquete proveniente de : %s\\n\",inet_ntoa(their_addr.sin_addr));


printf(\"longitud del paquete en bytes : %d\\n\",numbytes);


buf[numbytes] = \'\\0\';

printf(\"el paquete contiene : %s\\n\",buf);


/* devolvemos recursos al sistema */

close(sockfd);

}

17.4 - Código fuente del cliente:

Este cliente enviará un mensaje al servidor anterior, especificándose
en la linea de comando el nombre del host donde está ejecutándose
el servidor y el mensaje a enviar.

Codigo fuente :

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define MYPORT 4950 /* el puerto donde se enviaran
los datos */


int main(int argc, char *argv[])

{

int sockfd;

struct sockaddr_in their_addr; /* Almacenara
la direccion IP y numero de puerto del servidor */


struct hostent *he;

int numbytes;

if (argc != 3)

{

fprintf(stderr,\"usage: cliente hostname mensaje\\n\");


exit(1);
}

/* convertimos el hostname a su direccion IP */


if ((he=gethostbyname(argv[1])) == NULL)

{
herror(\"gethostbyname\");

exit(1);
}

/* Creamos el socket */

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)


{
perror(\"socket\");

exit(1);
}

their_addr.sin_family = AF_INET; /* host
byte order */


their_addr.sin_port = htons(MYPORT); /* network
byte order */


their_addr.sin_addr = *((struct in_addr *)he->h_addr);


bzero(&(their_addr.sin_zero), 8);

/* enviamos el mensaje, esta linea contiene una barra
invertida al final, indicando que sigue abajo*/


if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]),
0, (struct sockaddr *)&their_addr, \\


sizeof(struct sockaddr))) == -1)

{
perror(\"sendto\");

exit(1);
}

printf(\"enviados %d bytes hacia %s\\n\",numbytes,inet_ntoa(their_addr.sin_addr));


close(sockfd);

return 0;

}


18 - Bibliografía.

18.1 - Páginas del Manual :

Sección.
Página.
Descripción.




3
bstring
byte and byte string operations.

3
byteorder
convert values between host and network byte order.

3
gethostbyname
get network host entry.

3
getnetent
get network entry.

3
getprotoent
get protocol entry.

3
getservent
get service entry.

3
gettimeofday
get date and time.

3
herror
system error messages.

3
inet
internet address manipulation routines.

3
insque
insert/remove element from queue.

3
killpg
send signal to a process group.

3
perror
system error messages.

3
resolver
resolver routines.

3
string
string operations.

3
wait3
wait for process to terminate or stop.




2
accept
accept a connection on a socket.

2
bind
bind a name to a socket.

2
connect
initiate a conection on a socket.

2
fcntl
Manipulate file descriptor.

2
fork
create a child process.

2
getdtablesize
get descriptor table size.

2
gethostname
get/set name of current host.

2
getpeername
get name of connected peer.

2
getsockname
get socket name.

2
getsockopt
get and set options on a socket.

2
listen
listen for connection on a socket.

2
recv
receive a message from a socket.

2
select
syncronous I/O multiplexing.

2
send
send a message to a socket.

2
shutdown
shut down part of a full duplex connection.

2
socket
create an endpoint for communication.




18.2 - Libros.

  • Internetworking with TCP/IP, volumes I- III - Douglas Comer and David
    Stevens - Prentice Hall.

19 - Copyright.

Copyright- 1999, 2000, 2001, 2002, 2003 por Ariel Pereira. Este tutorial puede ser reproducido
solo para fines educativos, si no se altera su contenido, se muestra en
su totalidad y esta nota de copyright permanece intacta.