Make your own free website on Tripod.com

EL MODELO DE REFERENCIA TCP/IP

La Agencia de Proyectos de Investigación Avanzada del Departamento de Defensa de los Estados Unidos de Norteamérica definieron un conjunto de reglas que establecieron cómo conectar computadoras entre sí para lograr el intercambio de información, soportando incluso desastres mayores en la subred. Fue así como se definió el conjunto de protocolos de TCP/IP ( TCP/IP Internet Suite of Protocols). Para los años 80 una gran cantidad de instituciones estaban interesados en conectarse a esta red que se expandió por todo EEUU. La Suite de TCP/IP consta de 4 capas principales que se han convertido en un estándard a nivel mundial.

Las capas del modelo TCP/IP

Las capas de la suite de TCP/IP son menos que las del modelo de referencia OSI, sin embargo son tan robustas que actualmente une a más de 3 millones de nodos en todo el mundo.

 

La capa inferior, que podemos nombrar como física respecto al modelo OSI, contiene varios estándares del Instituto de Ingenieros Electrónicos y Eléctricos (IEEE en inglés) como son el 802.3 llamado Ethernet que establece las reglas para enviar datos por cable coaxial delgado (10Base2), cable coaxial grueso (10Base5), par trenzado (10Base-T), fibra óptica (10Base-F) y su propio método de acceso, el 802.4 llamado Token Bus que puede usar estos mismos medios pero con un método de acceso diferente, el X.25 y otros estándares denominados genéricamente como 802.X.

La siguiente capa cumple, junto con la anteriormente descrita, los niveles del modelo de referencia 1,2 y 3 que es el de red. En esta capa se definió el protocolo IP también conocido como "capa de internet". La responsabilidad de este protocolo es entregar paquetes en los destinos indicados, realizando las operaciones de enrutado apropiadas y la resolución de congestionamientos o caídas de rutas.

La capa de transporte es la siguiente y está implantada por dos protocolos: el Transmission Control Protocol y el User datagram Protocol. El primero es un protocolo confiable (reliable) y orientado a conexiones, lo cual significa que nos ofrece un medio libre de errores para enviar paquetes. El segundo es un protocolo no orientado a conexiones (connectionless) y no es confiable (unreliable). El TCP se prefiere para la transmisión de datos a nivel red de área amplia y el otro para redes de área local.

La última capa definida en la suite de TCP/IP es la de aplicación y en ella se encuentran decenas de aplicaciones ampliamente conocidas actualmente. Las más populares son el protocolo de transferencia de archivos (FTP), el emulador de terminales remotas (Telnet), el servicio de resolución de nombres (Domain Name Service DNS), el WWW, el servicio de correo electrónico (Simple Mail Transfer Protocol SMTP), el servicio de tiempo en la red (Network Time Protocol NTP), el protocolo de transferencia de noticias (Network News Transfer Protocol NNTP) y muchos más.

Comparación con el modelo OSI

El model TCP/IP no tiene bieen divididas las capas de ligado de datos, presentación y sesión y la experiencia ha demostrado que en la mayoría de los casos son de poca utilidad [Tan96].

Los estándares 802.X junto con el protocolo IP realizan todas las funciones propuestas en el modelo OSI hasta la capa de red. Los protocolos TCP y UDP cumplen con la capa de transporte. Finalmente, las aplicaiones ya mencionadas son ejemplos prácticos y reales de la funcionalidad de la capa de aplicación.

Tipos de Comunicaciones

El modelo OSI propone tener comunicaciones orientadas y no orientadas a conexión en la capa de red, mientras que TCP/IP sólo ofrece no orientadas a conexión, mientras que OSI propone en el nivel de transporte comunicaciones orinetadas a conexión mientras que TCP/IP ofrece orientadas y no orientadas a conexión en dicha capa. [Tan96].

Críticas al modelo OSI

El modelo OSI tiene siete niveles que fueron propuestos debido a que IBM tenía su protocolo de siete capas SNA (Systems Network Architecture) y el comité no quiso ir contra la corriente peleando contra la preponderancia de IBM en esos días [Tan96]. Por otro lado, mientras se planeaba y discutía el modelo OSI, ya se estaba trabajando y creando redes usando TCP/IP, de manera que al estar disponible el trabajo del modelo OSI la mayoría de las compañías ya no quiso hacer el esfuerzo de migrar sus productos. En general, las críticas más importantes al modelo OSI y sus implantaciones se pueden resumir en los siguientes puntos [Tan96]:

El conjunto total de la pila de protocolos resultó sere demasiada compleja para entender e implantar.
Las capas contienen demasiadas actividades redundantes, por ejemplo, el control de errores se integra en casi todas las capas siendo que tener un único control en la capa de aplicación o presentación sería suficiente.
La enormidad de código que fue necesario para implantar el modelo OSI y su consecuente lentitud hizo que la palabra OSI se asociara a "calidad pobre", lo cual contrstó con TCP/IP que se implantó exitosamente en el sistema operativo UNIX y era gratis.
OSI tuvo poca aceptación en EEUU porque la mayoría de la gente pensó que era un estándard implantado por la comunidad europea, y todos sabemos que la tecnología o deporte que no es inventado en EEUU es discriminada rápidamente.

Críticas al modelo TCP/IP

El modelo TCP/IP primero fue llevado a la práctica y luego fue descrita su funcionalidad, por lo cual se acepta que no puede usarse para describir otros modelos. Las críticas en general se resumen a continuación:

El modelo no distingue bien entre servicios, interfaces y protocolos, lo cual afecta el diseño de nuevas tecnologías en base a TCP/IP.
Las capas que le faltan con respecto al modelo OSI ni siquiera se mencionan y eso es lógico porque TCP/IP fue un predecesor de OSI.
No se puede hablar propiamente de un modelo TCP/IP, pero se tiene que discutir acerca de él forzados por su uso en todo el mundo.
Algunos de los protocolos de TCP/IP fueron creados por estudiantes y para solucionar problemas viejos y las necesidades modernas requieren de otros protocolos.

Conluyendo, el modelo OSI es muy bueno como marco teórico para describir la funcionalidad de los dispositivos y protocolos que hacen funcionar una red, pero se acepta que las capas de sesión y presentación no son muy útiles [Tan96], por lo cual generalmente se usa un modelo reducido con las capas física, ligado de datos, red, transporte y apicación.

Programación en red usando sockets bajo UNIX.

BSD Sockets: Un Tutorial Introductorio
por Jim Frost y Enrique Sanchez (traducción y ejemplos)
November 22, 1989, May 1996



Los aprendices de UNIX muchas veces encontramos conceptos y facilidades que son difíciles de aprender en primera instancia. Uno de esos conceptos es la facilidad de SOCKETS. En este tutorial se explica qué son, cómo se usan y se muestran ejemplos de código fuente para su uso correcto.

Una Analogía ¿ Qué es un socket?

El socket es el método de BSD para llevar a cabo la comunicación entre procesos (Interprocess Communication o IPC). Esto quiere decir que un socket se usa para permitir que un proceso pueda platicar o intercambiar información con otro, de una manera muy parecida a cómo se usa una línea telefónica entre dos personas.

La analogía del teléfono es buena, y será usada en repetidas ocasiones para describir el comportamiento de un socket.

Instalación de un Nuevo Teléfono ¿ Cómo escuchar en la red por conexiones de socket ?

Para que una persona reciba llamadas telefónicas, primero necesita tener una línea instalada. Para que un socket pueda aceptar peticiones primero debe crearse el socket. Para crear el socket se deben seguir algunos pasos. El primero es preparar el lugar para poner el teléfono, en este caso, debemos hacer el socket con el comando socket() .

Como los sockets pueden crearse de varios tipos, debemos especificar de qué tipo lo queremos al crearlo. Una opción es indicar el formato de direccionamiento del socket. Así como el correo electrónico usa un esquema diferente para entregar correspondencia a como un teléfono funciona para lograr la comunicación, asi pueden diferir los sockets. Los dos esquemas más comunes de direccionamiento son AF_UNIX y AF_INET . El direccionamiento AF_UNIX usa senderos ( pathnames ) para identificar a los sockets y son muy útiles para la comunicación entre procesos en una misma computadora. El otro esquema, AF_INET usa direcciones de Internet (Internet Protocol Addresses o Direcciones IP) que consisten en cuatro números decimales escritos de la forma #1.#2.#3.#4, por ejemplo, (140.148.101.26). Además, cada computadora con una dirección IP puede manejar varios tipos de conexiones al mismo tiempo, cada una de ellas en un puerto distinto.

Otra opción que debemos proveer cuando creamos un socket es el tipo de socket, que indique cómo preferimos que los datos viajen por la red. Los dos tipos más comunes son SOCK_STREAM y SOCK_DGRAM . La opción SOCK_STREAM indica que los datos viajan por la red como una secuencia de letras una tras otra, mientras que SOCK_DGRAM indica que los mismos lo harán en grupos (llamados datagramas). Nosotros preferiremos SOCK_STREAM.

Después de crear un socket, debemos darle una dirección para que escuche llamadas, así como nosotros damos nuestro número telefónico a nuestros conocidos para que nos llamen. La función bind() es usada para hacer esta tarea.

Los sockets de tipo SOCK_STREAM tienen la capacidad de encolar llamadas pendientes cuando estamos ocupados en atender una llamada en particular. La función listen() es usada para establecer el máximo número de llamadas que somos capaces de encolar. Aunque no es obligatorio invocar o usar a listen(), es muy recomendable hacerlo para procesar llamadas pendientes.

Las siguientes funciones muestran cómo usar socket(), bind() y listen() para establecer un socket que acepta llamadas:

/* código para establecer un socket;
original de bzs@bu-cs.bu.edu
*/

int establece(num_puerto)
u_short num_puerto;
{ char minombre[MAXHOSTNAME+1];
int s;
struct sockaddr_in sa;
struct hostent *hp;

bzero(&sa,sizeof(struct sockaddr_in)); /* limpia nuestra direccion*/
gethostname(minombre,MAXHOSTNAME); /*Obtengo mi nombre */

hp= gethostbyname(minombre); /* Determina nuestra información */
if (hp == NULL) /* Aborto si no existo!! */
return(-1);

sa.sin_family= hp->h_addrtype; /* Esta es nuestra direccion*/
sa.sin_port= htons(num_puerto); /* Este es nuestro puerto */

/* crea socket */
if ((s=socket(AF_INET,SOCK_STREAM,0))<0)
return(-1);

if (bind(s,&sa,sizeof sa,0) < 0) {
close(s);
return(-1); /* Asigna la dirección al socket */
}

listen(s, 3); /* Encolo hasta 3 llamadas */

return(s);
}

Después de que creamos el socket, debemos esperar que alguien nos llame. La función accept() es usada para hacer esto. Esta función esperara a que alguien llame y cuando esto sucede, accept() hace una copia del teléfono original y nos la entrega. El teléfono original se queda esperando otra llamada.

La siguiente función puede ser usada para aceptar una llamada en un socket:

int descuelga_tel(s)
int s; /* s es un socket creado con establece() */
{ struct sockaddr_in isa; /* dirección del socket */
int i; /* tamano de la dirección */
int t; /* socket de la conexion */

i = sizeof(isa); /* obtiene el tamano */
getsockname(s, & isa,&i); /* obtiene el nombre del socket */

if ((t = accept(s, & isa,&i)) < 0) /* acepta la llamada y duplica */
return(-1);

return(t);
}

Así como en una central telefónica se reciben llamadas que son delegadas a una persona mientras la recepcionista se queda esperando nuevas llamadas, en UNIX usamos la función fork() para crear un nuevo proceso que atienda la llamada actual. El siguiente código muestra cómo usar la función establece() y descuelga_tel() para controlar varias conexiones simultáneas:

#include <errno.h> /* Encabezados necesarios*/
#include <signal.h> /* vea "man bind" */
#include <stdio.h> /* vea "man listen */
#include <sys/types.h> /* vea "man accept */
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netdb.h>

#define PORTNUM 50000 /* Puerto donde vamos a escuchar */

void bomberazo(), haz_algo();

main()
{ int s, t;

if ((s= establece(PORTNUM)) < 0) { /* instala el teléfono */
perror("establece");
exit(1);
}

signal(SIGCHLD, bomberazo); /* Para eliminar "zombis" */

for (;;) { /* Ciclo infinito para llamadas*/
if ((t= descuelga_tel(s)) < 0) { /* alguien llama */
if (errno == EINTR) /* error EINTR en accept() */
continue; /* darle chance de intentar */
perror("accept"); /* otra vez */
exit(1);
} /* Delegar esta llamada */
switch(fork()) {
case -1 : /* No se pudo delegar llamada */
perror("fork");
close(s);
close(t);
exit(1);
case 0 : /* Ya pude delegar, atiendo la */
haz_algo(t); /* llamada con haz_algo(). */
exit(0);
default : /* Soy la telefónista, espero */
close(t); /* por otra llamada */
continue;
}
}
}

/* Como atiendo llamadas delegadas, una vez que fueron atendidas
* finiquito a quien la atendió (que es un proceso hijo)
*/

void bomberazo()
{ union wait wstatus;

while(wait3(&wstatus,WNOHANG,NULL) >= 0);
}

/* Esta función haz_algo() es la que va a leer la información que
* produzca la llamada. Puede ser, por ejemplo, guardar en un
* archivo los datos, desplegarlos en pantalla o cualquier otra
* cosa.
*/

void haz_algo(s)
int s;
{
/* código tuyo para manipular la información recibida
:
:
*/
}

Haciendo la Llamada ¿Cómo llamar en el socket?


Ahora ya sabemos cómo crear un socket en el cual despachar llamadas. Así que, ¿Cómo hacemos llamadas sobre ese socket ? Los pasos para realizar una llamada es similar a como las escuchamos. Primero necesitamos el teléfono para hacer la llamada, es decir, necesitamos crear el socket. Para crearlo, usamos la función socket().

Después de conseguir el teléfono (socket), tratamos de marcar el número asignandole la dirección al socket e invocando a la función connect() . La siguiente función intenta hacer una llamada a un socket particular.

int marca_número(hostname, num_puerto)
char *hostname;
{ struct sockaddr_in sa;
struct hostent *hp;
int a, s;

/* verificamos nombre */
if ((hp= gethostbyname(hostname)) == NULL) {
errno= ECONNREFUSED; /* y dirección */
return(-1); /* error, salimos */
}

bzero(&sa,sizeof(sa)); /* limpiamos estructura*/
/* pon dirección */
bcopy(hp->h_addr,(char*)&sa.sin_addr,hp->h_length);
sa.sin_family= hp->h_addrtype;
sa.sin_port= htons((u_short)num_puerto);

/* crea socket */
if ((s= socket(hp->h_addrtype,SOCK_STREAM,0)) < 0)
return(-1);
if (connect(s,&sa,sizeof sa) < 0) { /* conexión */
close(s);
return(-1);
}
return(s);
}

Esta función regresa un teléfono (socket) ya conectado a través del cual los datos ya pueden fluir.

Conversación ¿Cómo platicar entre sockets?

Ahora que ya tenemos una conexion hecha con sockets queremos enviar datos entre ellos. Las funciones read() y write nos servirán para realizar la lectura y escritura de los mismos. La diferencia de usar las funciones read() y write() con archivos abiertos a usarlas con sockets es el número de bytes que la función regresa. Con un archivo, read() lee los bytes que se especifiquen en sus argumentos, y write() escribe tambien lo indicado en los argumentos. En un socket, esas funciones leerán o escribirán los bytes que esteén disponibles en ese momento, de manera que para leer o escribir el número deseado, es necesario realizar un ciclo de lecturao escritura hasta llegar al número deseado de bytes. Una función muy simple que realiza el ciclo mencionado se muestra enseguida:

int lee_datos(s,buf,n)
int s; /* s = socket ya conectado */
char *buf; /* buf = apuntador a un arreglo de letras */
int n; /* n = número de bytes que deseamos leer */
{ int cnt, /* cnt = bytes leidos en un instante dado */
br; /* br = bytes leidos en un paso */

cnt= 0;
br= 0;
while (cnt < n) { /* ciclo de lectura */
if ((br= read(s,buf,n-cnt)) > 0) {
cnt += br; /* incrementa bytes leidos */
buf += br; /* mueve el apuntador del buffer */
}
if (br < 0) /* si no se puede leer mando -1 */
return(-1);
}
return(cnt);
}

Una función similar debería usarse para la escritura de datos, pero se deja al lector ese ejercicio para que se divierta, y haga algo!

Colgando el teléfono ¿Cómo deshacerse de un socket después de usarlo) ?

Igual que colgamos el teléfono cuando terminamos una llamada, necesitamos cerrar la conexion que se hizo entre sockets. Para esta labor se usa la función close() , la cual debe invocarse en cada lado del socket. Si un extremo del socket es cerrado y en el otro se intenta leer o escribir, el sistema enviará un mensaje de error.

Hablando un Mismo Lenguaje (El orden de los bytes es importante)

Ahora que podemos hacer que dos computadoras platiquen entre ellas debemos tener cuidado en cómo se transmiten la informacion. Sabemos que hay diferentes formas de codificar los datos, por ejemplo usando un código ASCII o un código EBCDIC. Otro aspecto es que una misma cadena de 7 u 8 bits puede ser interpretada de dos formas diferentes: que el bit 0 sea el mas significativo y el bit 7/8 sea el menos significativo, o viceversa. Es el famoso problema de maquinas "BIG-ENDian" y "LITTLE-ENDian". Así que si comunicamos una máquina BIG-END con una LITTLE-END, es posible que un conjunto de bytes signifiquen cosas diferentes para una y otra.

Para atacar el problema del orden de bytes se usan las funciones ntohs() (netork to host short integer), htons (host to network short integer), htoni() (host to network integer),
htonl() (host to network long integer) y ntohl() (network to host long integer). Por ejemplo, para poder enviar un número entero a través de un socket en la red, primero le aplicamos la función host to network integer htoni() como se muestra enseguida.

i= htoni(i);
escribe_datos(s, &i, sizeof(i));

Y después de leer un entero de la red, debemos convertirlo con ntohi():

lee_datos(s, &i, sizeof(i));
i= ntohi(i);

Es recomendable seguir el hábito de usar estas funciones para evitar problemas del orden de bytes.

El Futuro esta en tus manos ¿Qué hacer ahora?


Usando lo que ya se ha discutido aquí debe ser suficiente para construir nuestros propios programas de comunicación con sockets. Como con todas las cosas nuevas, es buena idea revisar el código de programas ya hechos para dilucidar su funcionamiento o la aplicación novedosa de esta tecnología.

Existen muchos programas de dominio publico que usan el concepto del socket y hay muchos libros que los exploran con mucha mas profundidad de lo que aqui se hace. Al final de este documento se anexan el código de un servidor y de un cliente que usan sockets. Realizan una función muy sencilla. El servidor escucha llamadas en un puerto dado y cualquier información que reciba la vuelve a escribir al socket hacia el cliente, es decir, responde la llamada repitiendo lo que le dicen. Por otro lado, el cliente lee un renglón de texto del teclado y se lo envía al servidor, y escribe en la pantalla todo aquello que el servidor le responda.

Si tienes preguntas adicionales acerca de sockets o de este tutorial, sientete en la confianza de enviarlas a:

madd@bu-it.bu.edu
enrique@cca.pue.udlap.mx


Jim Frost Enrique Sanchez Lara
Saber Software UDLA Consultores
(617) 876-7636 (5222) 292750
madd@saber.com enrique@udlac.com.mx


/***********************************************************************
* Archivo: tutor10.c
* Creador: Enrique SL
* Proposito: Demostrar el uso de la programación en red, SERVIDOR
* Compilar con: cc -o tutor10 tutor10.c -lsocket para una SUN
**********************************************************************/
#include "inet.h" /* definiciones y rutinas propias */

main(argc,argv)
int argc;
char **argv;
{

/* declaración de descriptores de red */
/**************************************/
int sockfd, newsockfd, clilen, childpid;
struct sockaddr_in cli_addr, serv_addr;
pname = argv[0];

/* creación del socket, que es como un "pipe" pero en red */
/**********************************************************/
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) {
perror("socket opening");
exit(1);
}

/* Inicialización de dirección y puerto de red del socket */
/**********************************************************/

bzero((char*) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_TCP_PORT);

/* Le digo al sistema que reserve el socket para mi aplicacion*/
/**************************************************************/
if (bind(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr))<0){
perror("binding");
exit(1);
}

/* Una vez reservado el puerto, espero por siempre por peticiones*/
/*****************************************************************/
listen(sockfd, 5);

for(;;) {

clilen = sizeof(cli_addr);
newsockfd = accept(sockfd,(struct sockaddr *)&cli_addr,&clilen);
if ( newsockfd < 0 ) {
perror("accepting");
exit(1);
}

/* Ejemplo de la creación de subprocesos */
/*****************************************/

if ((childpid = fork()) < 0 ) {
perror("forking");
exit(1);
}
else if (childpid == 0 ) {
close(sockfd);
atiende_llamada(newsockfd); /* este es el hijo */
exit(0);
}

close(newsockfd); /* aqui no llega el hijo, es el 'papa' */
}
}

/********** FIN DE TUTOR10.c ******************************************/


/***********************************************************************
* Archivo: tutor11.c
* Creador: Enrique SL
* Proposito: Demostrar la programación en red, CLIENTE
* Compilar: cc -o tutor11 tutor11.c -lsocket en una SUN
**********************************************************************/
#include "inet.h"
main(argc,argv)
int argc;
char **argv;
{
int sockfd;
struct sockaddr_in cli_addr, serv_addr;
pname = argv[0];

/* Note la diferencia con el servidor, aca se pone la direccion
* del servidor. El servidor acepta peticiones de quien sea
**************************************************************/
bzero((char*) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(SERV_HOST_ADDR);
serv_addr.sin_port = htons(SERV_TCP_PORT);
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) {
perror("socket opening");
exit(1);
}

if ( connect(sockfd,(struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0 ) {

perror("connecting");
exit(1);
}

/* La rutina platicame lo que hace es leer del teclado o stdin
* y enviarlo al servidor, el servidor retorna lo que se le envia
* y se imprime en pantalla.
*****************************************************************/

platicame(stdin, sockfd);
close(sockfd);
exit(0);


}