Subido por Danilo Coletto

Tanenbaum - Socket TCP

Anuncio
432
LA CAPA DE TRANSPORTE
CAP. 6
navegador web que solicita varios objetos al mismo servidor. Con los sockets, lo más natural es que los
programas de aplicación usen un flujo por cada objeto. Esta estructura significa que el control de congestión se aplica por separado para cada flujo, no entre todo el grupo, lo cual es subóptimo. Esto libra a la
aplicación de la carga de tener que administrar el conjunto. Se han desarrollado protocolos e interfaces
más recientes que soportan grupos de flujos relacionados de una forma más efectiva y simple para la
aplicación. Dos ejemplos son SCTP (Protocolo de Control de Transmisión de Flujo, del inglés Stream
Control Transmission Protocol ), que se define en el RFC 4960, y SST (Transporte Estructurado de Flujo,
del inglés Structured Stream Transport) (Ford, 2007). Estos protocolos deben modificar un poco la API
para obtener los beneficios de los grupos de flujos relacionados; además soportan características tales
como una mezcla de tráfico orientado a conexión y sin conexión, e incluso múltiples rutas de red. El
tiempo dirá si tienen éxito o fracasan.
6.1.4 Un ejemplo de programación de sockets: un servidor de archivos de Internet
Veamos ahora el código de cliente y servidor de la figura 6-6 como ejemplo del uso real de las llamadas de
sockets. Ahí se muestra un servidor de archivos de Internet muy primitivo, junto con un cliente de ejemplo
que lo utiliza. El código tiene muchas limitaciones (que analizaremos más adelante), pero en principio el
código del servidor se puede compilar y ejecutar en cualquier máquina UNIX conectada a Internet y
el código del cliente también se puede compilar y ejecutar en cualquier otra máquina UNIX en Internet,
en cualquier parte del mundo. El código del cliente se puede ejecutar con los parámetros apropiados para
obtener cualquier archivo al que el servidor tenga acceso en su máquina. El archivo se escribe a la salida
estándar que, por supuesto, se puede redirigir a un archivo o conducto.
Veamos primero el código del servidor. Comienza con algunos encabezados estándar, los últimos tres
contienen las principales definiciones y estructuras de datos relacionadas con Internet. A continuación se
encuentra una definición de SERVER_PORT como 12345. Este número se eligió de manera arbitraria.
Cualquier número que se encuentre entre 1024 y 65535 funcionará siempre y cuando otro proceso no lo
esté utilizando; los puertos debajo de 1023 están reservados para los usuarios privilegiados.
Las siguientes líneas del servidor definen dos constantes necesarias. La primera determina el tamaño
de bloque en bytes que se utiliza para la transferencia de archivos. La segunda determina cuántas conexiones pendientes se pueden almacenar antes de empezar a descartar las conexiones adicionales que vayan
llegando.
/* Esta página contiene un programa cliente que puede solicitar un archivo desde el programa servidor de la siguiente
página. El servidor responde
* enviando el archivo completo.
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define SERVER_PORT 12345
#define BUF_SIZE 4096
/* arbitrario, pero el cliente y el servidor deben coincidir */
/* tamaño de bloque para transferencia */
int main(int argc, char **argv)
{
int c, s, bytes;
char buf[BUF_SIZE];
struct hostent *h;
struct sockaddr_in channel;
/* búfer para el archivo entrante */
/* información sobre el servidor */
/* contiene la dirección IP */
(continúa)
EL SERVICIO DE TRANSPORTE
SEC. 6.1
if (argc != 3) fatal(“Usar: cliente nombre-servidor nombre-archivo”);
h = gethostbyname(argv[1]);
/* busca la dirección IP del host */
if (!h) fatal(“falla en gethostbyname “);
433
(continuación)
s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s < 0) fatal(“socket”);
memset(&channel, 0, sizeof(channel));
channel.sin_family= AF_INET;
memcpy(&channel.sin_addr.s_addr, h->h_addr, h->h_length);
channel.sin_port= htons(SERVER_PORT);
c = connect(s, (struct sockaddr *) &channel, sizeof(channel));
if (c < 0) fatal(“falla en conexion”);
/* Se ha establecido la conexión. Se envía el nombre del archivo incluyendo el byte 0 al final. */
write(s, argv[2], strlen(argv[2])+1);
/* Obtiene el archivo y lo escribe en la salida estándar. */
while (1) {
bytes = read(s, buf, BUF_SIZE);
if (bytes <= 0) exit(0);
write(1, buf, bytes);
}
/* lee del socket */
/* verifica el final del archivo */
/* escribe en la salida estándar */
}
fatal(char *string)
{
printf(“%s\n”, string);
exit(1);
}
Figura 6-6. Código del cliente que utiliza sockets. El código del servidor está en la siguiente página.
#include <sys/types.h>
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
/* Éste es el código del servidor */
#define SERVER_PORT 12345
#define BUF_SIZE 4096
#define QUEUE_SIZE 10
/* arbitrario, pero el cliente y el servidor deben coincidir */
/* tamaño de bloque para la transferencia */
int main(int argc, char *argv[])
{
int s, b, l, fd, sa, bytes, on = 1;
char buf[BUF_SIZE];
struct sockaddr_in channel;
/* búfer para el archivo saliente */
/* contiene la dirección IP */
/* Construye la estructura de la dirección para enlazar el socket. */
memset(&channel, 0, sizeof(channel));
/* canal cero */
channel.sin_family = AF_INET;
channel.sin_addr.s_addr = htonl(INADDR_ANY);
channel.sin_port = htons(SERVER_PORT);
/* Apertura pasiva. Espera una conexión. */
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/* crea el socket */
if (s < 0) fatal(“falla en socket “);
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on));
(continúa)
LA CAPA DE TRANSPORTE
434
b = bind(s, (struct sockaddr *) &channel, sizeof(channel));
if (b < 0) fatal(“falla en bind “);
l = listen(s, QUEUE_SIZE);
if (l < 0) fatal(“falla en listen”);
CAP. 6
(continuación)
/* especifica el tamaño de la cola */
/* El socket ahora está configurado y enlazado. Espera una conexión y la procesa. */
while (1) {
sa = accept(s, 0, 0);
/* se bloquea para la solicitud de conexión */
if (sa < 0) fatal(“falla en accept”);
read(sa, buf, BUF_SIZE);
/* lee el nombre del archivo desde el socket */
/* Obtiene y regresa el archivo.*/
fd = open(buf, O_RDONLY);
if (fd < 0) fatal(“falla en open”);
/* abre el archivo para regresarlo */
while (1) {
bytes = read(fd, buf, BUF_SIZE); /* lee del archivo */
if (bytes <= 0) break;
write(sa, buf, bytes);
}
close(fd);
close(sa);
/* verifica el final del archivo */
/* escribe bytes en el socket */
/* cierra el archivo */
/* cierra la conexión */
}
}
El código del servidor empieza después de las declaraciones de las variables locales. Primero
inicializa una estructura de datos que contendrá la dirección IP del servidor. Esta estructura de datos
pronto se anexará al socket del servidor. La llamada a memset establece en 0s toda la estructura de
datos. Las siguientes tres asignaciones llenarán tres de sus campos. El último de éstos contiene el
puerto del servidor. Las funciones htonl y htons están relacionadas con la conversión de valores a un
formato estándar a fin de que el código se ejecute correctamente, tanto en las máquinas little-endian
(por ejemplo, Intel x86) como en las máquinas big-endian (por ejemplo, la SPARC). Su semántica
exacta no es importante aquí.
A continuación el servidor crea un socket y verifica si hay errores (lo cual se indica mediante s < 0).
En una versión de producción del código, el mensaje de error puede ser un poco más explicativo. La
llamada a setsockopt es necesaria para permitir la reutilización del puerto, a fin de que el servidor se pueda
ejecutar de manera indefinida, sorteando una solicitud tras otra. A continuación, la dirección IP se enlaza
con el socket y se realiza una verificación para ver si la llamada a bind tuvo éxito. El último paso en la
inicialización es la llamada a listen para anunciar que el servidor está dispuesto a aceptar llamadas entrantes e indicar al sistema que almacene la cantidad de estas llamadas, que se especifica en QUEUE_SIZE,
en caso de que lleguen más solicitudes mientras el servidor aún esté procesando la actual. Si la cola está
llena y llegan solicitudes adicionales, se descartan en silencio.
En este punto el servidor entra a su ciclo principal, del cual nunca sale. La única forma de detenerlo
es desde afuera. La llamada a accept bloquea el servidor hasta que algún cliente trata de establecer una
conexión con él. Si la llamada a accept tiene éxito, se devuelve un descriptor de archivo que se puede
utilizar para leer y escribir, de la misma forma en la que se pueden usar los descriptores de archivo para
leer y escribir hacia/desde las canalizaciones. Sin embargo, a diferencia de las canalizaciones, que son
unidireccionales, los sockets son bidireccionales, por lo que sa (el socket aceptado) se puede utilizar para
leer de la conexión y también para escribir en ella. Un descriptor de archivo de canalización es para leer
o escribir, pero no para ambas cosas.
SEC. 6.1
EL SERVICIO DE TRANSPORTE
435
Una vez que se establece la conexión, el servidor lee en ella el nombre del archivo. Si el nombre aún
no está disponible, el servidor se bloquea y lo espera. Una vez que obtiene el nombre, el servidor abre
el archivo y luego entra en un ciclo que alterna entre leer bloques del archivo y escribirlos en el socket
hasta que el archivo se haya copiado por completo. A continuación, el servidor cierra el archivo y la
conexión, y espera hasta que aparezca la siguiente conexión. Este ciclo se repite de manera indefinida.
Ahora analicemos el código del cliente. Para entender su funcionamiento es necesario comprender
cómo se invoca. Suponiendo que se llama cliente, una llamada típica es:
cliente flits.cs.vu.nl /usr/tom/nombredearchivo >f
Esta llamada sólo funciona si el servidor ya se está ejecutando en flits.cs.vu.nl, si existe el archivo /usr/
tom/nombredearchivo y si el servidor tiene acceso de lectura a él. Si la llamada es exitosa, el archivo se
transfiere a través de Internet y se escribe en f, después de lo cual finaliza el programa cliente. Puesto que
el servidor continúa después de una transferencia, el cliente puede iniciarse una y otra vez para obtener
otros archivos.
El código del cliente inicia con algunas inclusiones y declaraciones. La ejecución verifica primero
si se invocó el código con el número correcto de argumentos (argc = 3 significa el nombre del programa
más dos argumentos). Observe que argv [1] contiene el nombre del servidor (por ejemplo, flits.cs.vu.nl ) y
que gethostbyname lo convierte en una dirección IP. Esta función utiliza DNS para buscar el nombre. En
el capítulo 7 estudiaremos el sistema DNS.
A continuación se crea e inicializa un socket. Después de esto, el cliente intenta establecer una conexión TCP con el servidor mediante connect. Si el servidor está activo y ejecutándose en la máquina especificada y enlazado a SERVER_PORT, y si está inactivo o tiene espacio en su cola listen, la conexión
se establecerá (en algún momento dado). El cliente envía el nombre del archivo mediante esta conexión, para lo cual escribe en el socket. El número de bytes enviados es un byte mayor que el propio nombre,
puesto que también hay que enviar el byte 0 de terminación del nombre para indicar al servidor en dónde
termina dicho nombre.
Ahora el cliente entra en un ciclo, lee el archivo bloque por bloque desde el socket y lo copia a la
salida estándar. Cuando termina, simplemente abandona la conexión.
El procedimiento fatal imprime un mensaje de error y termina. El servidor necesita el mismo procedimiento, sólo que lo omitimos debido a la falta de espacio en la página. Puesto que el cliente y el servidor
se compilan por separado y, por lo general, se ejecutan en computadoras diferentes, no pueden compartir
el código de fatal.
Estos dos programas (así como el material adicional relacionado con este libro) se pueden obtener
del sitio web del libro
http://www.pearsonhighered.com/tanenbaum
Sólo como aclaración, este servidor no es lo último en tecnología de servidores. Su proceso de verificación de errores no es muy bueno y su proceso de reporte de errores es regular. Puesto que maneja todas
las solicitudes estrictamente en forma secuencial (ya que cuenta con un solo hilo), su desempeño es pobre.
Es evidente que nunca ha escuchado sobre la seguridad; además, utilizar sólo llamadas de sistema UNIX
no es la manera de obtener independencia de la plataforma. También da por sentados algunos detalles que
son técnicamente ilegales, como suponer que el nombre del archivo cabe en el búfer y se transmite instantáneamente. Pese a estas deficiencias, es un servidor de archivos de Internet funcional. En los ejercicios,
invitamos al lector a mejorarlo. Para mayor información sobre la programación con sockets, consulte a
Donahoo y Calvert (2008, 2009).
Descargar