Boletín 8

Anuncio
Laboratorio de Sistemas Operativos
Juan Antonio Álvarez, Toñi Reina, David Ruiz,
Antonio Tallón, Pablo Neira, José Ángel Bernal y Sergio Segura
Boletı́n ♯8: Mecanismo de IPC de Berkeley. Sockets
Curso 2006/07
Índice
1. Introducción
2
2. Comunicación orientada a conexión (con sockets en modo stream)
3
3. Llamadas del sistema relacionadas con sockets
4
3.1. Creación de un socket. socket () . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
3.2. Asociación de un socket con una dirección. bind () . . . . . . . . . . . . . . . . . . . . . . . . .
5
3.2.1. Direcciones en el dominio UNIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
3.2.2. Direcciones en el dominio Internet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
3.3. Creación de una cola en el socket. listen () . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
3.4. Aceptación de la petición de conexión de un cliente. accept () . . . . . . . . . . . . . . . . . . .
8
3.5. Solicitud de conexión en un cliente. connect () . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
3.6. Liberación de recursos. close () . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
3.7. Envı́o/recepción de datos mediante sockets en modo stream. send (), recv () . . . . . . . . . . .
9
4. Otras funciones de utilidad
10
4.1. Funciones de ordenación de bytes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
4.2. Funciones de conversión de nombres y direcciones . . . . . . . . . . . . . . . . . . . . . . . . .
11
4.2.1. Búsqueda de máquinas. gethostbyname () . . . . . . . . . . . . . . . . . . . . . . . . .
11
4.2.2. Conversión de formato de direcciones IP. . . . . . . . . . . . . . . . . . . . . . . . . . .
12
4.2.3. Obtención del nombre del host. gethostname () . . . . . . . . . . . . . . . . . . . . . .
13
5. Ejercicios
13
6. Ejercicios de Examen
15
1
1. Introducción
Mediante el uso de sockets podemos crear una conexión punto a punto entre dos procesos. Esta conexión se trata
de forma similar a un fichero en ambos procesos. El mecanismo por el que se crea la conexión es un mecanismo
cliente-servidor, lo que implica la existencia de un proceso que acepta conexiones (el servidor) y otro que solicita
conexiones (cliente). Una vez establecida la conexión, esta se comporta como un canal de datos bidireccional.
Según el tipo de conexión establecida, podemos distinguir en UNIX distintos tipos de sockets, entre los que
destacan:
1. Sockets en modo stream. Son aquellos que establecen una conexión punto a punto en la que se garantiza que:
No se pierde información; es decir, todo lo que se envı́a se recibe.
El orden de recepción de los datos es el mismo que el orden en el que se enviaron.
No llegan paquetes duplicados.
2. Sockets en modo datagrama. En este tipo de socket no se establece una conexión punto a punto, sino que
se envı́an los bloques de información de un proceso a otro sin la existencia de ningún tipo de canal. Cada
bloque de información enviado se conoce como datagrama. Esta forma de transmisión no garantiza el orden
de recepción de los datagramas, ni garantiza tampoco que se reciba toda la información que se envı́a. Se
dice, por tanto, que este tipo de sockets son un medio de transmisión no fiable.
3. Sockets raw, que acceden directamente al nivel de red, que en el caso de internet serı́a el protocolo IP. Este
tipo de socket se deja a usuarios experimentados, y no se verán en este boletı́n.
A los sockets en modo stream se les suele llamar ”sockets conectados” y a los sockets en modo datagrama se
les suele llamar ”sockets no conectados”. Tanto en un caso como en otro, el sistema operativo creará un punto de
conexión a nivel de transporte (véase el modelo OSI de redes), que en el caso de modo stream será el protocolo
TCP, protocolo fiable, y en el caso de los datagrama, será el protocolo UDP (no fiable). En la figura 1 se muestra
un esquema de los tipos de sockets
STREAM
SOCKET
DATAGRAM
SOCKET
RAW
SOCKET
TCP
UDP
IP
Nivel de Red
Nivel Físico
Figura 1: Tipos de sockets y niveles sobre los que se sustentan.
Si nos centramos en el dominio en el que se realiza la comunicación, los sockets se pueden clasificar en dos
tipos:
1. Sockets en dominio UNIX. En este caso, los procesos cliente y servidor han de residir en la misma máquina.
2
2. Sockets en el dominio de Internet. Aquı́ se permite la comunicación de procesos de forma independiente a la
máquina en la que se ejecutan, pudiendo estar el cliente en una máquina y el servidor en otra en otro lugar
del mundo. Aquı́ el sistema operativo es el encargado de gestionar la comunicación, y para ello se apoya en
algún protocolo de red, como TCP/IP.
2. Comunicación orientada a conexión (con sockets en modo stream)
Una conexión tı́pica es una relación no simétrica, esto implica que para comenzar una conexión de red un programa tiene que saber qué papel juega (cliente o servidor). El servidor será el proceso que ofrezca las conexiones,
mientras que el cliente solicitará una conexión. Hasta que no queda establecida la conexión, no contamos con un
canal simétrico y bidireccional (lo que implica que no hay diferencia entre cliente y servidor).
Los pasos a realizar en cualquier conexión serán, por tanto:
1. Establecimiento de conexión. Este proceso será distinto para el cliente y para el servidor.
2. Recepción/envı́o de datos. Aquı́ ya no habrá diferencias entre cliente y servidor.
3. Destrucción del canal. Cliente y servidor han de destruir el canal, terminando la comunicación y liberando
los recursos del sistema utilizados.
Como cliente y servidor se diferencian a la hora de establecer la conexión, vamos a ver, de forma más detallada,
cuáles son estas diferencias.
Establecimiento de conexión en el servidor
1. Solicitar al sistema operativo un socket. Para ello se utiliza la llamada al sistema socket. A este primer socket
se le denomina socket de escucha. A él le llegarán las solicitudes de conexión de los clientes, pero nunca se
utilizará para el envı́o o la recepción de datos.
2. Asignar un nombre (en el caso de dominio UNIX) o un número de puerto (dominio Internet) al socket de
escucha. Para rellenar esta información se utiliza la llamada al sistema bind.
3. Determinar la longitud de la cola de espera y dejar listo el socket de escucha para .escuchar”solicitudes de
conexión por parte de los clientes. Esto se realiza con la llamada al sistema listen.
4. Aceptar conexiones de los procesos clientes. Para aceptar una conexión se utiliza la llamada al sistema
accept. Esta llamada es bloqueante, lo que implica que el proceso queda suspendido hasta que recibe una
petición de conexión por parte de un cliente. Si no hubo ningún problema, accept devuelve un descriptor de
socket. Este socket se conoce como socket conectado, y es el que se utilizará para mandar y recibir datos del
cliente, dejando el socket de escucha libre para aceptar nuevas peticiones de otros clientes.
Establecimiento de conexión en el cliente
1. Solicitar al sistema operativo un socket. Para ello se utiliza la llamada al sistema socket.
2. Conectar el socket con un socket del servidor. Esto se consigue mediante la llamda al sistema connect, que
utiliza el nombre (dominio UNIX) o el número de puerto (dominio Internet) para identificar al socket al que
se desea conectar.
En la figura 2 se muestra gráficamente de forma más detallada los pasos a seguir para establecer la conexión
tanto en el cliente, como en el servidor.
Como se puede observar, una vez establecida la conexión, se entra en un bucle de lectura-escritura hasta que el
cliente decida terminar el diálogo con el servidor.
En el siguiente apartado se verán estas llamadas al sistema con más detalle.
3
(protocolo orientado a conexión)
Servidor
socket ( )
bind ( )
listen ( )
Cliente
socket ( )
accept ( )
Proceso bloqueado hasta
que se conecta con el
cliente
Establecimiento Conexión
read ( )
Petición
connect ( )
write ( )
Procesar Petición
read ( )
write ( )
Respuesta
Figura 2: Llamadas al sistema de sockets para un protocolo orientado a conexión.
3. Llamadas del sistema relacionadas con sockets
3.1.
Creación de un socket. socket ()
La llamada al sistema socket es el primer paso a realizar para crear una conexión, tanto en el cliente como en
el servidor. Su declaración es la siguiente:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
en la cual
domain Indica la familia de direcciones que se quiere emplear. Las distintas familias se definen mediante constantes que se incluyen en el archivo de cabecera <sys/socket.h>. Las dos familias más comunes son:
AF UNIX Es una familia de protocolos internos de UNIX. Se utiliza para comunicar procesos que se encuentran en una misma máquina. No requiere que haya hardware de red, puesto que no realiza accesos
a ninguna red.
AF INET Es la familia de protocolos de Internet. Implica la creación de sockets que se van a comunicar
mediate protocolos como TCP o UDP.
4
type Indica la semántica de la comunicación para el socket. Puede tomar uno de los siguientes valores predefinidos:
SOCK STREAM El socket se crea para una comunicación orientada a conexión.
SOCK DGRAM El socket se crea para una comunicación no orientada a conexión o datagrama.
protocol Especifica el protocolo particular que se va a utilizar con el socket. Normalmente, cada tipo de socket
tiene asociado un sólo protocolo, pero si hubiera más de uno, se especificarı́a mediante este argumento.
Cuando protocol vale 0, la elección del protocolo se deja en manos del sistema.
Si la llamada a socket se ejecuta correctamente, devolverá un descriptor de fichero. En caso contrario, devolverá un -1 y en la variable global errno colocará el código del error que se ha producido.
3.2.
Asociación de un socket con una dirección. bind ()
La llamada al sistema bind se utiliza en el servidor para registrar su dirección, de forma que le dice al sistema
.esta es mi dirección y cualquier mensaje que recibas en esta dirección me lo tienes que dar a mi”. En el caso del
dominio UNIX esta dirección será un nombre, y en el dominio Internet será und dirección de red. Su formato es el
siguiente:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int s, const struct sockaddr *name, int namelen);
donde
s Es el descriptor del socket de escucha al que se quiere asociar a la dirección.
name Es un puntero a un tipo genérico que almacena los datos de la dirección. El tipo real va a depender del
dominio del socket
namelen Es el tamaño en bytes de la estructura que se pasa con el puntero name.
Si la llamada a bind funciona correctamente, se devuelve un 0. Si hay algún error, se devuelve un -1 y establece
el código de error en la variable global errno .
3.2.1.
Direcciones en el dominio UNIX
En el caso de la familia de direcciones AF UNIX la dirección se indica utilizando la estructura struct sockaddr un que está definida en el archivo de cabecera <sys/un.h>. Esta estructura tiene el siguiente formato:
struct
sockaddr_un {
sa_family_t
char
sun_family;
sun_path[108];
/* AF_UNIX */
/* path name (gag) */
};
y en ella,
sun family Indicará la familia de direcciones. Y en este caso, tendrá siempre el valor AF UNIX.
sun path Aquı́ hay que indicar la ruta de un archivo del sistema de ficheros, ya que cuando se une un socket
AF UNIX a una ruta, por ejemplo, / tmp/misocket”, se crea un fichero. Si el fichero ya existiese, la llamada
a bind devolverı́a un error.
5
Ejemplo 1 El siguiente trozo de código muestra la creación de un socket de escucha del dominio UNIX, y le
asigna como dirección una ruta de un archivo mediante la llamada bind
#include <sys/types.h>
#include <sys/socket.h>
#include<sys/un.h>
#include <string.h>
...
struct sockaddr_un su;
int s;
s = socket ( AF_UNIX, SOCK_DGRAM, 0);
su.sun_family = AF_UNIX;
strcpy( su.sun_path, "/tmp/misockunix");
bind ( s, (struct sockaddr *) &su, sizeof(su));
3.2.2.
Direcciones en el dominio Internet
Para la familia de direcciones de Internet (AF INET), la dirección se recoge en una estructura de tipo struct
sockaddr in, la cual, está declarada en el fichero de cabecera <netinet/in.h>. Su definición es la siguiente:
struct sockaddr_in {
sa_family_t
in_port_t
struct in_addr
char
};
sin_family;
sin_port;
sin_addr;
sin_zero[8];
struct in_addr {
in_addr_t s_addr; /* Direccion IPv4 de 32 bits */
};
/* en Network Byte Order */
donde,
sin family Indicará la familia de direcciones. Y en este caso, tendrá siempre el valor AF INET.
sin port Indica el puerto. Un puerto no es más que un número no negativo de 16 bits, es decir, que puede
tomar valores entre el 0 y el 65535, y que es utilizado por el protocolo TCP para identificar un servicio de
una máquina. Los puertos menores de 1024 están reservados para servicios estándares. El resto, es decir,
desde el 1024 al 65535, están libres para ser asignados a cualquier servicio de usuario. Algunos ejemplos
de estos servicios estándares son el ftp (que por defecto trabaja con los puertos 20 y 21), el http (que por
defecto trabaja con el puerto 80). Si se le da al campo puerto el valor 0, será el núcleo el que se encargue de
seleccionar uno de los puertos libres que tenga disponible. Aunque conozcamos los números de los puertos
estándares asociados con determinados servicios, a la hora de programar es conveniente utilizar la llamada
al sistema getservbyname, que obtiene el número de puerto asignado a un servicio estándar. Hay que tener
en cuenta que el puerto hay que expresarlo en network byte order, para lo que habrá que utilizar la función
htons. Aunque esto se explicará con más detalle en el apartado Funciones de ordenación de bytes.
sin addr En este campo irá la dirección de la máquina sobre la que se ejecuta el servidor. Para rellenar este
campo se utilizan funciones auxiliares como gethostname y gethostbyname. Para ver estas funciones con
más detalle ir al apartado Funciones de conversión de nombres y direcciones.
sin zero Este campo se diseñó para posibles ampliaciones futuras, a efectos de programación, no se tendrá en
cuenta, es decir, que no habrá que preocuparse por inicializarlo ni darle ningún valor.
6
Ejemplo 2 El siguiente trozo de código muestra la creación de un socket de escucha del dominio Internet, y le
asigna una dirección IP y un puerto mediante bind. Para entender el funcionamiento de las llamadas gethostname y gethostbyname mı́rese el apartado Funciones de conversión de nombres y direcciones
#include
#include
#include
#include
#include
...
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<netdb.h>
<string.h>
struct sockaddr_in sin; int s;
struct hostent *hp;
char hostname[LON_HOSTNAME];
/* Obtenemos el nombre de la máquina donde se ejecuta el
código */
gethostname(hostname, sizeof(hostname));
/* Obtenemos su dirección IP, a partir del nombre */
hp = gethostbyname (hostname);
if (hp == NULL)
printf ("Error");
else{
s = socket ( AF_INET, SOCK_STREAM, 0);
sin.sin_family = AF_INET;
sin.sin_port = htons(15495); /* Asignamos puerto en
Network byte order */
/* Obtenemos la dirección IP en Network byte order */
memmove (&sin.sin_addr, hp->h_addr, hp->h_length);
/* Asociamos dirección y puerto al socket de escucha */
bind ( s, (struct sockaddr *) &sin, sizeof(sin));
...
}
3.3.
Creación de una cola en el socket. listen ()
La llamada al sistema listen se ejecuta en el servidor tras las llamadas a socket y a bind, y realiza dos acciones:
1. Cuando se crea un socket, por defecto, el sistema lo crea como un socket activo, es decir, un socket cliente
que más tarde realizará una petición de conexión mediante la llamada connect. La función listen se encarga
de convertir un socket no conectado en un socket pasivo, indicando que el núcleo deberı́a aceptar peticiones
de conexión y dirigirlas a este socket. Es decir, le indica al sistema que el socket se tendrá que poner a
.escuchar”.
2. Habilita una cola asociada con el descriptor del socket. Esta cola se va a encargar de recojer las peticiones
de conexión procedentes de los procesos cliente.
La llamada a listen solamente tiene sentido en los sockets de tipo SOCK STREAM.
El formato de la llamada a listen es el siguiente:
#include <sys/types.h>
#include <sys/socket.h>
7
int listen(int s, int backlog);
donde
s Es el descriptor del socket de escucha que se va a poner a .escuchar”, previamente se le ha tenido que asociar la
dirección y el puerto mediante la llamada bind.
backlog Indica la longitud de la cola asociada al descriptor del socket, o lo que es lo mismo, el número máximo
de peticiones de conexión pendientes de aceptación. Si esta cola está llena y se recibe una petición de
conexión, ésta es ignorada.
La llamada a listen devuelve un 0 si se ejecuta correctamente, y un -1 en caso de error, estableciendo en errno
el código del error
3.4.
Aceptación de la petición de conexión de un cliente. accept ()
accept se llama para aceptar conexiones de los clientes. Esta función coge la primera petición de conexión de
la cola y crea otro socket con las mismas propiedades que el socket de escucha, el cual se conoce como socket conectado. Si no hubiera ninguna petición en la cola, y el socket no tiene activo el modo no bloqueante (O NDELAY)
, esta llamada bloquea al proceso que la llama hasta que llegue una petición.
El prototipo de la función accept es el siguiente:
#include <sys/types.h>
#include <sys/socket.h>
int
accept(int
s, struct
sockaddr *addr, int *addrlen);
donde
s Es el descriptor del socket de escucha que está siendo escuchado y por donde llegan las peticiones de los clientes.
addr Este es un parámetro de salida, y se utiliza para devolver la dirección de cliente, una vez que cliente y
servidor están conectados.
addrlen Argumento de entrada-salida. Antes de llamar a la función accept hay que inicializarlo al tamaño de
la estructura a la que apunta addr. En el retorno de accept se tendrá el número de bytes rellenados por la
función con los datos del cliente.
En este punto es importante hacer la distinción entre socket de escucha y socket conectado. A lo largo de la
vida de un proceso servidor, solamente se creará un socket de escucha, sin embargo, se creará un socket conectado
por cada conexión de cliente que se acepte. Cuando el servidor termina de servir a un cliente, debe cerrar el socket
conectado.
La función accept devuelve el descriptor del socket conectado, en caso de que no se haya producido ningún
tipo de error, y -1 en caso contrario. En caso de error, el código del mismo se podrá obtener consultando la variable
errno.
Ejemplo 3 El siguiente trozo de código muestra la utilización de la llamada accept. Tenga en cuenta que se es
un socket de escucha que ha debido ser asignado a una dirección y un puerto y .escuchado”
#include <sys/types.h>
#include <sys/socket.h>
8
#include <netinet/in.h>
...
int se;
int sc;
int longaddr;
struct sockaddr_in si;
longaddr = sizeof(si);
sc = accept (se, (struct sockaddr *)&si, &longaddr);
if (sc == -1) {
/* Error aceptando conexión */
}
3.5.
Solicitud de conexión en un cliente. connect ()
La llamada al sistema connect la utiliza un cliente para establecer una conexión con un servidor. Su prototipo
es el siguiente:
#include <sys/types.h>
#include <sys/socket.h>
int
connect(int
s, const struct sockaddr *name, int namelen);
donde
s Es el descriptor del socket que previamente se ha tenido que crear con la llamada a socket.
name Este es un puntero a una estructura que debe contener la dirección del servidor al que se quiere conectar el
cliente.
namelen Es el tamaño en bytes de la estructura que contiene la dirección del servidor.
Si el socket es de tipo SOCK DGRAM, connect especifica la dirección del socket remoto al que se le van a
enviar los mensajes, pero no se conecta con él. La llamada devuelve el control inmediatamente. Además a través
del socket solamente se podrán recibir mensajes procedentes de la dirección especificada en name.
Si el socket es de tipo SOCK STREAM, connect intenta conectar con el ordenador remoto para realizar una
conexión cliente-servidor. La llamada se bloquea hasta que la conexión se completa. Si la conexión no se puede
realizar inmediatamente, pero el socket tiene activo el modo de acceso O NDELAY, la llamada a connect devuelve
el control indicando que se ha producido un error de conexión.
Si la conexión se realiza con éxito, la llamada devuelve un 0; en caso de error, devuelve un -1 y en errno
estará el código del error producido.
3.6.
Liberación de recursos. close ()
Como ya se ha visto en los apartados anteriores, los sockets son tratados por el sistema igual que los ficheros.
Por lo tanto, para liberar los recursos utilizados por un socket cuando no se necesiten más, se utiliza la llamada
close vista en el boletı́n de ficheros.
3.7.
Envı́o/recepción de datos mediante sockets en modo stream. send (), recv ()
Una vez que cliente y servidor están conectados, se tiene un canal de comunicación bidireccional y simétrico.
Podemos mandar o recibir información por los sockets de tres formas distintas:
9
1. Mediante llamadas al sistema especı́ficas de sockets, es decir, utilizando send y recv.
2. Utilizando las llamadas read y write que se vieron en el boletı́n de ficheros, ya que el sistema trata a un
socket como a un descriptor de fichero.
3. Asociándole un buffer al descriptor del socket, con la función fdopen, vista en el boletı́n de ficheros, y
trabajar ası́ a alto nivel (con un puntero a FILE) y utilizar las funciones de stdio: fprintf, fgets, fgetc, fputc,
....
En este apartado nos centraremos en describir las llamadas especı́ficas de sockets, porque el resto se supone
conocido. El prototipo de las llamadas send y recv es el siguiente:
#include <sys/socket.h>
ssize_t recv(int s, void *buf, size_t len, int flags);
ssize_t send(int s, const void *buf, size_t len, int flags);
las dos funciones son similares a las estándares read y write, pero tienen un parámetro extra. Los tres primeros
argumentos son los mismos que los tres primeros de read y write. Es decir,
s Es el descriptor del socket conectado del que se va a leer o por el que se va a escribir.
buf Es un puntero al inicio del bloque a enviar, en el caso de send, o al inicio del bloque donde se colocará la
información leı́da, en el caso de recv.
len Es el tamaño en bytes a enviar (send) o bytes disponibles para recibir (recv).
flags Es o bien 0, o bien, se forma mediante la suma lógica (OR) de una serie de constantes definidas en el
sistema. Para ver estas constantes, consulte el manual.
send devuelve el número de bytes enviados, o -1 en caso de error. recv devuelve el número de bytes leı́dos, o
-1 si ocurrió algún error. Si el número de bytes a leer fuese mayor que el tamaño de buf, entonces habrı́a que hacer
más invocaciones a recv para leer toda la información que haya disponible.
(recv) y send son llamadas bloqueantes. Esto quiere decir que, para el caso de recv, si se hace una llamada
al sistema y no hay datos disponibles para leer, el proceso que hizo la llamada quedará suspendido hasta que haya
datos disponibles. En el caso de send, el proceso que realiza la llamada puede quedar bloqueado en el caso en el
que el sistema tenga sus buffers lleno y no pueda almacenar temporalmente los datos a enviar.
4. Otras funciones de utilidad
4.1.
Funciones de ordenación de bytes
A la hora de enviar datos por la red, nos podemos encontrar con problemas cuando se interpreta la información
recibida. Consideremos, por ejemplo, un entero de 16 bits, que consta de dos bytes. Para almacenarlo, y por lo
tanto, para enviarlo, tenemos dos posibilidades:
1. Almacenar el byte menos significativo en las direcciones más bajas y el más significativo en las direcciones
más altas, lo que en la bibliografı́a se conoce como little-endian.
2. Almacenar el byte más significativo en la direcciones más bajas y el menos significativo en las más altas, o
big-endian.
Desafortunadamente, no hay un estándar para el orden de los bytes, y nos podemos encontrar con sistemas que
usan un formato y sistemas que usan el otro. La forma que tiene un sistema de ordenar los bytes se conoce como
host byte order.
Para evitar el tener que conocer la ordenación de los bytes de la máquina destino, se utiliza lo que se conoce
como network byte order o un criterio ocmún para enviar los datos por la red.
10
Para convertir datos numéricos de network byte order a host byte order, y viceversa, se emplean las siguientes
funciones:
#include <netinet/in.h>
unsigned short htons (unsigned short);
unsigned long htonl (unsigned long);
unsigned short ntohs (unsigned short);
unsigned long ntohl(unsigned long);
htons y htonl convierten de host byte order a network byte order. La primera convierte un entero corto, y la
segunda un entero largo.
ntohs y ntohl convierten de network byte order a host byte order. La primera convierte un entero corto, y la
segunda un entero largo.
4.2.
Funciones de conversión de nombres y direcciones
En cualquier sistema cliente-servidor es necesario conectar el cliente con el servidor, por lo tanto, debe existir
un sistema de identificación y localización de servicios. En el caso de la famila de direcciones de Internet, el protocolo que se utiliza es el IP, y las direcciones, se conocen como direcciones IP. Una dirección IP viene dada por 32
bits, y para que el ser humano la entienda bien, se suede dar en formato ASCII agrupada por cadenas de bytes separados por puntos. Por ejemplo, 138.4.22.25, la cual corresponde al número 10001010000001000001011000011001.
Pero con la dirección IP no basta para la localización, ya que una máquina puede ofertar múltiples servicios como ftp, telnet, http, echo, etc. De la misma forma que para localizar a una persona no basta con saber el portal, sino
también el piso y la puerta, es necesario dar más información para localizar un servicio concreto. Esta información
se da con un número que se conoce como puerto.
Sin embargo, los usuarios normales no están acostumbrados a trabajar con números, sino que es mucho más
fácil recordar el nombre de una máquina (murillo.eii.us.es) que su dirección IP. Para no obligar a que el usuario
conozca la dirección IP de una máquina y poder trabajar con los nombres de los hosts se emplean las siguientes
funciones que vamos a ver en esta sección.
Pero esto no es todo, ya que trabajar con números no es demasiado cómodo para el ser humano. Ası́ que,
las máquinas tienen asignados nombres, de tal forma, que un nombre de máquina tiene una equivalencia con
una dirección IP. Surgieron ası́ bases de datos para convertir nombres de máquina a direcciones IP y viceversa. En
UNIX esta información se encuentra en ficheros situados en el directorio /etc. Las correspondencias entre nombres
y direcciones IP se encuentran en el fichero /etc/hosts.
4.2.1.
Búsqueda de máquinas. gethostbyname ()
La función más básica que busca un nombre de host es la función gethostbyname, la cual devuelve una
estructura que contendrá todas las direcciones IP para esa máquina. Esta información la obtendrá o bien de un
servidor de nombres, o bien del archivo /etc/hosts El formato de esta función es el siguiente:
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
donde name es el nombre del host del que se desea conocer la dirección IP.
La estructura que devuelve gethostbyname es la siguiente:
11
struct
hostent
char
char
int
int
char
#define h_addr
compatiblity */
{
*h_name;
**h_aliases;
h_addrtype;
h_length;
**h_addr_list;
h_addr_list[0]
};
/*
/*
/*
/*
/*
/*
official name of host */
alias list */
host address type */
length of address */
list of addresses from name server */
address, for backward
En esta estructura:
h name Contiene una cadena con el nombre oficial de la máquina.
h aliases Es un array de cadenas de caracteres, cada una de las cuales es un alias de la máquina.
h addrtype Indica el tipo de dirección, en nuestro caso, AF INET.
h length Indica la longitud en bytes de la dirección, que para el caso AF INET es de 4 bytes (la dirección IP).
h addr list Es un array de cadenas en el que pada posición corresponde a una dirección IP asociada a la
máquina.
h addr Este campo se mantiene por compatibilidad con versiones anteriores. Contiene la dirección IP principal,
y no es más que la primera posición del array h addr list.
En la figura 3 se describe gráficamente el formato de la estructura hostent.
struct hostent
h_name
nombre oficial\0
alias #1\0
h_aliases
alias #2\0
h_addrtype:
AF_INET
NULL
h_length: 4
struct in_addr()
direccionIP#1\0
h_addr_list
struct in_addr()
direccionIP#2\0
struct in_addr()
direccionIP#3\0
NULL
h_length = 4
Figura 3: Llamadas al sistema de sockets para un protocolo orienteado a conexión.
4.2.2.
Conversión de formato de direcciones IP.
Para convertir una dirección IP del formato de caracteres separados por puntos as su representación como un
entero largo, disponemos de la función inet addr. La conversión inversa se realiza mediante la función inet ntoa.
Sus prototipos son los siguientes:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
12
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(const struct in_addr in);
4.2.3.
Obtención del nombre del host. gethostname ()
Para obtener el nombre de la máquina donde que ejecuta la llamada a esta función se utiliza gethostname
cuyo prototipo es el siguiente:
#include <unistd.h>
int gethostname(char *name, int namelen);
5. Ejercicios
1. Realice un programa cliente de intercambio de mensajes entre máquinas utilizando el protocolo TCP/IP y
utilizando sockets de tipo stream. El proceso cliente, deberá pedirle al usuario un mensaje por la entrada
estándar. Este mensaje deberá ser enviado a un proceso servidor, que estará situado en una máquina distinta.
El proceso servidor, cuando reciba el mensaje, deberá imprimirlo por la salida estándar. La invocación del
cliente se hará de la siguiente forma:
cliente <nombre_maquina> <puerto>
donde <nombre_maquina> será la dirección de la máquina en la que se ha lanzado el proceso servidor
que atenderá sus conexiones, y <puerto> será el número de puerto TCP por el que el servidor atiende las
peticiones de los clientes. Para probar el cliente utilice el servidor que le será proporcionado por el profesor
de prácticas. Una prueba de ejecución puede ser la siguiente:
Lance el proceso servidor en el servidor de prácticas murillo de la siguiente forma:
$ servidor 1234
Lance el proceso cliente en su máquina local:
$ cliente murillo 1234
Introduca un mensaje: Hola
Introduzca un mensaje en el terminal asociado al cliente y compruebe que el mensaje aparece en el
terminal asociado al servidor.
2. Implemente un proceso servidor concurrente de intercambio de mensajes que se comunique con el cliente
que ha implementado en el ejercicio anterior. El proceso servidor deberá estar pendiente de atender las
solicitudes de conexiones por parte de los clientes. Una vez establecida la conexión, deberá imprimir por su
salida estándar los mensajes que envı́en los clientes. La invocación al servidor se realizará de la siguiente
forma:
servidor <puerto>
donde <puerto> será el número de puerto TCP por el que el servidor atenderá las peticiones. Puede
realizar la misma prueba que en el ejercicio anterior, pero en lugar de utilizar el servidor proporcionado por
el profesor de prácticas, utilice su propio servidor.
N OTA :
Para hacer la implementación del ejercicio vaya por pasos. Pruebe primero a realizar un servidor que atienda una conexión. Después, modifique el código para convertirlo en iterativo y que pueda atender más de
una conexión, pero no de forma simultánea. Y, por último, haga que su servidor atienda varias conexiones
concurrentemente.
13
3. Realice un programa cliente y un programa servidor concurrente de intercambio de archivos entre máquinas
utilizando el protocolo TCP/IP y utilizando sockets de tipo stream. El servidor estará pendiente de atender
las peticiones de conexión por parte del cliente. Una vez aceptada una conexión, el cliente le enviará al
servidor un mensaje con el siguiente formato:
GET <archivo>
donde <archivo> debe ser el nombre de un archivo que exista en el sistema de archivos del servidor.
Una vez que el servidor reciba el mensaje, comprobará si existe el archivo solicitado por el cliente. Si el
archivo existe, le mandará un mensaje con el siguiente formato:
OK <bytes><retorno><fin_de_linea>
donde <bytes> es el tamaño en bytes del archivo que se va a enviar a continuación, <retorno> es el
carácter \r y <fin_de_linea> el \n. Y justo a continuación le enviará, carácter a carácter, el archivo
solicitado.
Si el archivo no existiera o hubiera algún problema para acceder a él. El servidor mandará un mensaje con
el formato:
NOK <descripcion_del_error>
donde <descripcion_del_error> es un pequeño mensaje de texto que describe por qué no se ha
podido enviar el archivo.
Las invocaciones al cliente y al servidor tendrán los siguientes formatos:
servidor <puerto>
cliente <nombre_maquina> <puerto> <archivo>
donde <nombre_maquina> será la dirección de la máquina en la que se ha lanzado el proceso servidor
que atenderá sus conexiones, <puerto> será el número de puerto TCP por el que el servidor atiende las
peticiones de los clientes, y <archivo> será el archivo que ha solicitado el cliente al servidor.
14
6. Ejercicios de Examen
1. (Ejercicio de Examen 1a CONV II 2003-04)
Se desea implementar un servidor de archivos concurrente (fileserv) que trabaje en modo conectado
(SOCK_STREAM) y que pueda funcionar a través de internet (AF_INET). Para implementar el servidor, se
han decidido crear las siguientes funciones:
int Serv_Crea_Socket (void);
Reserva una posición en la tabla de descriptores de archivos para almacenar información sobre un
socket. Devuelve el descriptor del socket creado, si no hay ningún tipo de problema, y -1 en caso de
error.
int Serv_Reserva_Puerto(int sock, int puerto, struct hostent *hp);
Une el socket dado por sock con el número de puerto TCP/IP y la dirección dada en la estructura
struct hostent. Devuelve -1 si hay algún error, y 0 en caso contrario.
int Serv_Asigna_ColaEsc(int sock);
Asigna el tamaño de la cola de espera a la constante TAM_COLA, y convierte el socket en pasivo. Si
existe algún problema al realizar la operación, devuelve -1. En caso contrario devuelve 0.
int Serv_Acepta_Conexion(int sock);
Toma como argumento un socket pasivo, y devuelve un socket conectado. Si hay algún error, devuelve
-1. En caso contrario, devuelve el descriptor del socket conectado.
int Serv_Procesa_Conexion(int sock, char directorio[]); Toma como argumento un socket conectado y la ruta absoluta del directorio que contendrá los archivos que sirve el
servidor de archivos.
El servidor se deberá invocar de la siguiente forma:
fileserv
<puerto> <directorio>
donde <puerto> es el número de puerto TCP/IP por el que el servidor atenderá las peticiones recibidas
por parte de los clientes, y <directorio> será la ruta absoluta de un directorio, que deberá existir en el
sistema, y en el que se situarán los archivos y/o directorios que servirá nuestro programa.
Apartado a.- Escriba el código de la función Serv_Reserva_Puerto.
Apartado b.- Escriba el código del programa principal para crear el servidor de archivos concurrente. El
servidor deberá capturar la combinación de teclas Ctrl+C (señal SIGINT) para terminar correctamente el programa, lo que implica que no se deben dejar procesos huérfanos, ni sockets abiertos.
Apartado c.- Escriba el código de una función C llamada enviar_archivo que pueda ser llamada por
la función serv_procesa_conexion, y que se encargue de enviar al cliente a través del socket la
siguiente información:
Si el archivo solicitado no existe, al cliente se le enviará el siguiente mensaje: \ERR-01: Archivo
¨ Nótese que se utiliza como terminador de mensaje el conjunto de caracteres
inexistente \r\n”.
$\backslash$r $\backslash$n.
Si el archivo solicitado existe, pero no se tiene permiso de lectura sobre el mismo, se debe enviar
¨
el mensaje: \ERR-02:Sin permiso \r\n””.
Por último, si el archivo existe y se tiene permiso de lectura, se deberá enviar una cabecera con el
tamaño en bytes del archivo (Por ejemplo, v, y a continuación se deben enviar, carácter a carácter,
los 456 bytes de que consta el archivo. El prototipo de la función que debe implementar es el
siguiente:
void enviar_archivo (int sc, char directorio[], char archivo[])
donde:
• sc es el descriptor del socket conectado al cliente.
• directorio contiene la ruta absoluta del directorio donde están los archivos que sirve el
servidor, y
• archivo es el nombre del archivo solicitado por el cliente.
NOTA: El archivo se debe tratar a bajo nivel.
15
2. (Ejercicio de Examen 2a CONV II 2003-04)
Se desea implementar un servidor de archivos concurrente fileserv y un cliente filecli para este servidor que
trabajen en modo conectado (SOCK_STREAM) y que puedan funcionar a través de internet (AF_INET).
Las invocaciones a estos programas se tendrán el siguiente formato:
fileserv
filecli
<puerto> <directorio>
<nombre-maquina> <puerto> <peticion>
donde:
<puerto> es el número de puerto TCP/IP por el que el servidor atenderá las peticiones recibidas por
parte de los clientes, en el caso del servidor, y el puerto por el que realizará las peticiones en caso del
cliente.
<directorio> será la ruta absoluta de un directorio, que deberá existir en el sistema, y en el que se
situarán los archivos y/o directorios que servirá el servidor.
<nombre-maquina> será el nombre de la máquina en la que se esté ejecutando el servidor al que
se desea conectar el cliente.
<peticion> indicará la operación que le solicita el cliente al servidor. Esta <peticion> podrá tener tres formatos distintos:
ls En este caso el cliente le solicita al servidor un listado de los archivos disponibles para descargar.
Como resultado de la operación en el terminal del cliente debe aparecer un listado de los mismos.
get <nombre archivo> El cliente le solicita al servidor el archivo <nombre_archivo>. Como resultado de la operación en el directorio desde donde se lanzó el cliente debe aparecer el
archivo solicitado, si la operación se ha realizado correctamente, o un mensaje de error, en caso
contrario.
put <nombre archivo> El cliente le envı́a al servidor el archivo <nombre_archivo>. Como
resultado de la operación en el directorio <directorio> del servidor debe aparecer el archivo
enviado, si la operación se ha realizado correctamente. Si no, aparecerá en el terminal del cliente
un mensaje de error.
Se pide:
Apartado a.- Escriba el código de la función Cliente_Solicita_Conexion que toma como parámetro de entrada el puerto TCP/IP por el que el cliente realizará la conexión, y un puntero a una estructura
hostent, que contendrá información acerca del servidor al que el cliente se quiere conectar. Devolverá un entero que será el descriptor del socket creado y ya conectado ó -1 en caso de error.
Apartado b.- Escriba el código de la función del servidor Servidor_Envia_Listado, que tomará como argumentos de entrada un descriptor de socket conectado sd, y el directorio que contiene los
archivos que el cliente puede descargar del directorio. Esta función deberá enviarle al cliente un listado
de aquellos archivos regulares que tengan permiso de lectura para aquellos usuarios que no sean ni
el propietario ni pertenezcan al grupo del propietario. El listado tendrá el formato que se presenta a
continuación, donde, aparece el nombre del archivo, seguido de su tamaño en bytes:
practica.c
100 bytes
f1.c
233 bytes
<fin>
2 archivos disponibles
3. (Ejercicio de Examen 3a CONV II 2003-04)
Se desea implementar un servidor de archivos concurrente fileserv y un cliente filecli para ese
servidor que trabajen en modo conectado (SOCK_STREAM) y que puedan funcionar a través de internet
(AF_INET).
Las invocaciones a estos programas se tendrán el siguiente formato:
fileserv
<puerto> <directorio>
filecli <nombre-maquina><puerto> <peticion>
donde:
16
<puerto> es el número de puerto TCP/IP por el que el servidor atenderá las peticiones recibidas por
parte de los clientes, en el caso del servidor, y el puerto por el que realizará las peticiones el cliente.
<directorio> será la ruta absoluta de un directorio, que deberá existir en el sistema, y en el que se
situarán los archivos y/o directorios que servirá el servidor.
<nombre-maquina> será el nombre de la máquina en la que se esté ejecutando el servidor al que
se desea conectar el cliente.
<peticion> indicará la operación que le solicita el cliente al servidor. Esta <peticion> podrá tener tres formatos distintos:
ls En este caso el cliente le solicita al servidor un listado de los archivos disponibles para descargar.
Como resultado de la operación en el terminal del cliente debe aparecer un listado de los mismos.
get <nombre archivo> El cliente le solicita al servidor el archivo <nombre_archivo>. Como resultado de la operación en el directorio desde donde se lanzó el cliente debe aparecer el
archivo solicitado, si la operación se ha realizado correctamente, o un mensaje de error, en caso
contrario.
put <nombre archivo> El cliente le envı́a al servidor el archivo <nombre_archivo>. Como
resultado de la operación en el directorio <directorio> del servidor debe aparecer el archivo
enviado, si la operación se ha realizado correctamente. Si no, aparecerá en el terminal del cliente
un mensaje de error.
Se pide:
Apartado a.- Escriba el código de la función Servidor_Acepta_Conexion, cuyo prototipo es el siguiente: void Servidor_Acepta_Conexion(int se, int *sc). Dicha función tomará como argumentos de entrada un descriptor de socket de escucha se en modo pasivo y un argumento de
salida (que se pasa por referencia) sc que es el descriptor de socket conectado. La función bloqueará al
proceso servidor hasta recibir una conexión por parte de un cliente, en cuyo caso, se aceptará la conexión y se devolverá el socket conectado.
Apartado b.- Realice la función int Cliente_Recibe_Archivo, cuyo prototipo es el siguiente:
int Cliente_Recibe_Archivo(int sc, char archivo[],int permisos). Dicha
función recibe como argumentos un entero sc que representa el socket conectado al servidor de archivos, archivo que es el nombre del archivo que vamos a recibir y que habrá que crear y dónde
introduciremos el contenido leı́do del socket conectado y por último permisos que indica los permisos con el que crearemos el archivo. En caso de que el cliente reciba correctamente el archivo y guarde
su contenido, devolveremos 0 en caso contrario, devolveremos 1. Para saber si el archivo se ha localizado en el servidor y puede ser enviado correctamente, lo primero que se enviará desde el servidor
será la cadena Correcto: y a continuación el contenido del archivo. En caso de que lo que inicialmente
enviado no sea dicha cadena, se habrá producido un error.
4. (Ejercicio de Examen 3a CONV II 2003-04)
Se desea implementar un servidor de archivos concurrente (fileserv) que trabaje en modo conectado
(SOCK_STREAM) y que pueda funcionar a través de internet (AF_INET). Para implementar el servidor, se
han decidido crear las siguientes funciones:
int Serv_Crea_Socket_Escucha (int puerto, struct hostent *hp); Toma como entrada un número de puerto TCP/IP y la dirección dada en la estructura struct hostent, y
devuelve, el descriptor del socket en modo pasivo, preparado para la escucha, ó, -1 en caso de error.
int Serv_Acepta_Conexion(int sock);
Toma como argumento un socket pasivo, y devuelve un socket conectado. Si hay algún error, devuelve
-1. En caso contrario, devuelve el descriptor del socket conectado.
int Serv_Procesa_Conexion(int sock, char directorio[]);
Toma como argumento un socket conectado y la ruta absoluta del directorio que contendrá los archivos
que sirve el servidor de archivos El servidor se deberá invocar de la siguiente forma:
fileserv <puerto> <directorio>
donde <puerto> es el número de puerto TCP/IP por el que el servidor atenderá las peticiones recibidas por parte de los clientes, y <directorio> será la ruta absoluta de un directorio, que deberá existir en el sistema, y en el que se situarán los archivos y/o directorios que servirá nuestro programa.
17
Apartado a.- Escriba el código de la función Serv_Crea_Socket_Escucha.
Apartado b.- Una de las posibles peticiones que le pueden llegar al servidor es la operación PUT <archivo>,
mediante la cual, un proceso cliente puede enviarle un archivo al servidor. Implemente la función:
int Servidor_Guardar_Archivo (int socket, char archi[], int tam) que toma el descriptor del socket conectado al cliente (socket), un nombre de archivo (arch) y el tamaño
del mismo (tam), y deberá crear una copia en el directorio dado por la constante HOME, que se supone
definida, del archivo que el cliente envı́a por el socket.
18
Descargar