ÍNDICE

 

1.INTRODUCCIÓN.

2. ¿QUÉ ES UN SOCKET?.

2.1. Dominio de un socket.

2.2. Tipos de sockets.

2.3. Primitivas de un socket.

2.3.1. La primitiva Socket.

2.3.2. La primitiva Close.

2.3.3. La primitiva Bind.

2.3.4. La primitiva Connect.

2.3.5. La primitiva Write.

2.3.6. La primitiva Read.

2.3.7. La primitiva Listen.

2.3.8. La primitiva Accept.

2.3.9. Sumario de primitivas.

3. EL MODELO CLIENTE/SERVIDOR.

3.1. El servidor.

3.2. El cliente.

3.3. Comunicación cliente/servidor orientada a la conexión.

4. RUTINAS DE OPERACIÓN ADICIONALES.

4.1. Rutinas de ordenación de bytes.

4.2. Operaciones de bytes.

4.3. Rutinas de conversión de direcciones.

5. BIBLIOGRAFÍA.

 

1. INTRODUCCIÓN.

En este trabajo nos adentramos en el apasionante mundo de las comunicaciones, y más concretamente, en la comunicación a través de los sockets. Estos aparecieron a principios de los 80 con el sistema UNIX de Berkeley, para proporcionar un medio de comunicación a los procesos, con el fin de proporcionar un medio de comunicación entre ellos. Los sockets, si hacemos un símil, tienen la misma función que la que pudiera tener la comunicación por correo o por teléfono (de un buzón se extraen mensajes completos, mientras que el teléfono permite el envío de flujos de información que no tienen una estructura claramente definida; esta dualidad se encuentra en los sockets). El socket, por tanto, ofrece dos puntos de contacto entre distintas aplicaciones, a través de los cuales estas se comunican.

Otra característica importante de los puntos de comunicación concierne al conjunto de otros puntos que permite acceder. La noción de dominio de un socket permite definir el conjunto de sockets con los cuales se podrá establecer una comunicación por medio de él.

 

2. ¿QUÉ ES UN SOCKET?.

En la introducción comentamos de una forma genérica qué es un socket, pero aquí lo haremos de una manera precisa y clara, mostrando todos sus detalles. Un socket es un punto de comunicación por el cual un proceso puede emitir o recibir información. En el interior de un proceso, un socket se identifica por un descriptor de la misma naturaleza que los que identifican los archivos, al igual que todos los procesos del sistema UNIX de Berkeley.

La comunicación mediante sockets es una interfaz (o servicio) con la capa de transporte (nivel 4) de la jerarquía OSI. La filosofía de la división por capas de un sistema es encapsular, dentro de cada una de ellas, detalles que conciernen sólo a cada capa, y presentársela al usuario de tal forma que este pueda trabajar con ella sin necesidad de conocer sus detalles de implementación. La interfaz de acceso a la capa de transporte del sistema UNIX de Berkeley no está totalmente aislada de las capas inferiores, por lo que a la hora de trabajar con sockets, es necesario conocer algunos detalles sobre esas capas. En concreto, a la hora de establecer una conexión mediante sockets, es necesario conocer la familia o dominio de la conexión, y el tipo de conexión.

La creación de un socket se realizará por la primitiva socket, la cual veremos con más detenimiento más adelante, cuyo valor de vuelta es un descriptor sobre el cual es posible realizar operaciones de escritura y lectura. Un socket permite la comunicación en los dos sentidos (conexión full-dúplex).

 

Figura 1.- Tabla descriptora de ficheros de un proceso.

Una diferencia esencial con los archivos es que el nombrado de los sockets es una operación distinta de su creación (apertura y creación van a la par, pero después es posible fijar una dirección de su dominio al objeto creado por medio de la primitiva bind).

 

2.1. Dominio de un socket.

Una familia, o dominio de la conexión, agrupa todos aquellos sockets que comparten características comunes. Especifica el formato de las direcciones que se podrán dar al socket y los diferentes protocolos soportados por las comunicaciones vía los sockets de este dominio.

Cada protocolo, a la hora de referirse a un nodo de la red, implementa un mecanismo de direccionamiento. La dirección distingue de forma inequívoca a cada nodo u ordenador, y es utilizada para encaminar los datos desde el nodo origen hasta el nodo destino. Hay muchas llamadas al sistema que necesitan un puntero a una estructura de dirección de socket para trabajar. La siguiente estructura genérica, se utiliza para describir las diferente primitivas.

struct sockaddr {

u_short sa_family; /* familia de sockets; se emplean constantes de la forma AF_xxx */

char sa_data[14]; /* 14 bytes que contienen la dirección; su significado depende de la familia de sockets que se emplee */

};

Pero en el caso de que estemos tratando con una aplicación particular, esta estructura se deberá reemplazar por la estructura correspondiente del dominio de comunicaciones utilizado, ya que por desgracia, no todas las familias de direcciones se ajustan a la estructura genérica descrita.

No obstante, esta estructura genérica encaja en la estructura definida para la familia AF_INET, perteneciente al dominio Internet, lo que hace que el software de TCP/IP trabaje correctamente aunque el programador use la estructura genérica anterior, puesto que ambas tienen el mismo número de bytes. A continuación, pasamos a describir esta estructura del dominio Internet.

struct sockaddr_in {

short sin_family; /* la familia de la dirección AF_INET*/

u_short sin_port; /* 16 bits con el número del puerto */

u_long sin_addr; /* 32 bits con la dirección Internet (identificación de la red y del host)*/

char sin _zero[8]; /* 8 bytes no usados */

Se puede observar que en la dirección Internet el campo sin_family es equivalente al campo sa_family de la dirección genérica y que los campos sin_port, sin_addr y sin_zero cumplen la misma función que el campo sa_addr.

También podemos ver el dominio UNIX (AF_UNIX), donde los sockets son locales al sistema en el cual han sido definidos. Permiten la comunicación interna de procesos, y su designación se realiza por medio de una referencia UNIX.

struct sockaddr_un {

short sun_family; /* dominio UNIX: AF_UNIX */

char sun_data[108]; /* path */

};

Estas direcciones se corresponden en realidad con paths de ficheros, y su longitud (110 bytes) es superior a los 16 bytes que de forma estándar tienen las direcciones del resto de familias. Esto es posible debido a que esta familia se usa para comunicar procesos ejecutados bajo control de la misma máquina, no teniendo así que acceder a la red. Otros dominios son:

AF_NS /* protocolos XEROX NS */

AF_CCITT /* protocolos CCITT, protocolos X.25, etc. */

AF_SNA /* IBM SNA */

AF_DECnet /* DECnet */

 

2.2. Tipos de sockets

Cada tipo de socket va a definir una serie de propiedades en función de las comunicaciones en las cuales está implicado:

a) La fiabilidad de la transmisión. Ningún dato transmitido se pierde.

b) La conservación del orden de los datos. Los datos llegan en el orden en el que han sido emitidos.

c) La no duplicación de datos. Sólo llega a destino un ejemplar de cada dato emitido.

d) La comunicación en modo conectado. Se establece una conexión entre dos puntos antes del principio de la comunicación (es decir, se establece un circuito virtual). A partir de entonces, una emisión desde un extremo está implícitamente destinada al otro extremo conectado.

e) La conservación de los límites de los mensajes. Los límites de los mensajes emitidos se pueden encontrar en el destino.

f) El envío de mensajes (urgentes). Corresponde a la posibilidad de enviar datos fuera del flujo normal, y por consecuencia accesibles inmediatamente (datos fuera de flujo).

Cabe reseñar que un cauce de comunicación normal tiene las cuatro primeras propiedades, pero no las dos últimas.

En cuanto a los tipos de sockets disponibles, se pueden considerar:

* SOCK_STREAM: Los sockets de este tipo permiten comunicaciones fiables en modo conectado (propiedades a, b, c y d) y eventualmente autorizan, según el protocolo aplicado los mensajes fuera de flujo (propiedad f). El protocolo subyacente en el dominio Internet es TCP. Se establece un circuito virtual realizando una búsqueda de enlaces libres que unan los dos ordenadores a conectar (parecido a lo que hace la red telefónica conmutada para establecer una conexión entre dos teléfonos). Una vez establecida la conexión, se puede proceder al envío secuencial de los datos, ya que la conexión es permanente. Son streams de bytes full-dúplex (similar a pipes). Un socket stream debe estar en estado conectado antes de que se envíe o reciba en él.

* SOCK_DGRAM: Corresponde a los sockets destinados a la comunicación en modo no conectado para el envío de datagramas de tamaño limitado. Las comunicaciones correspondientes tienen la propiedad e. En el dominio Internet, el protocolo subyacente es el UDP. Los datagramas no trabajan con conexiones permanentes. La transmisión por los datagramas es a nivel de paquetes, donde cada paquete puede seguir una ruta distinta, no garantizándose una recepción secuencial de la información.

* SOCK_RAW: Permite el acceso a los protocolos de más bajo nivel (por ejemplo, el protocolo IP en el dominio Internet). Su uso está reservado al superusuario.

* SOCK_SEQPACKET: Corresponde a las comunicaciones que poseen las propiedades a, b, c, d y e. Estas comunicaciones se encuentran en el dominio XNS.

Los dos tipos de sockets más utilizados son los dos primeros.

 

2.3. Primitivas de un socket.

Todos los sockets proporcionan una serie de primitivas, las cuales están orientadas a ser utilizadas por sus usuarios, de forma que se pueda establecer una comunicación determinada, y marcar unas pautas dentro de la misma.

Cada socket puede ser usado de múltiples formas y por diferentes usuarios, lo que invita a que para su mayor comprensión, sea prácticamente indispensable estudiar el conjunto de primitivas que ofrece. A continuación mostramos las primitivas más importantes que ofrecen, estudiando más profundamente aquellas que tienes más relevancia dentro de este conjunto, y mostrando después un cuadro con la totalidad de primitivas y una breve descripción de cada una.

 

2.3.1. La primitiva Socket.

Esta primitiva permite la creación de un socket, es decir, la creación e inicialización de entradas en las diferentes tablas del sistema de gestión de archivos, que son: tabla de descriptores de procesos, tabla de archivos y estructuras de datos, conteniendo las características del socket. Entre estas características se encuentran:

- el tipo, el dominio y el protocolo;

- el estado del socket (conectado o no, enlazado, en estado de recibir o de emitir);

- la dirección del socket conectado (si hay alguno): al socket se le asocia un buffer de emisión y otro de recepción;

- punteros a los datos (en emisión y en recepción);

- un grupo de procesos para la gestión de mecanismos asíncronos.

La forma general de la primitiva que permite crear un socket y obtener un descriptor para utilizarlo es:

int socket (dominio, tipo, protocolo)

int dominio; /* AF_INET, AF_UNIX, ... */

int tipo; /* SOCK_DGRAM, SOCK_STREAM, ... */

int protocolo; /* 0: protocolo por defecto */

En caso de fallo de la primitiva se devuelve el valor -1 y en errno estará codificado el error producido. Si no hay fallo se devuelve un descriptor referenciando al socket, que se utilizará en llamadas posteriores a funciones de la interfaz.

El primer parámetro especifica la familia de sockets que se desea emplear. El segundo parámetro especifica el tipo de socket. El tercer parámetro especifica el protocolo que se va a usar en el socket. Normalmente, cada tipo de socket tiene asociado sólo un protocolo, pero si hubiera más de uno, se especificaría mediante este argumento. Generalmente su valor es 0, en cuyo caso la elección del protocolo se deja en manos del sistema.

 

2.3.2. La primitiva Close.

Un socket es suprimido cuando ya no hay ningún proceso que posea un descriptor para accederlo. Esta supresión, la cual es el resultado de al menos una llamada a la primitiva close, implica la liberación de entradas en las diferentes tablas y buffers reservados por el sistema relacionados con el socket.

 

2.3.3. La primitiva Bind.

Cuando se crea un socket con la llamada socket, se le asigna una familia de direcciones, pero no una dirección particular. Después de esto, un socket no es accesible más que por los procesos que conocen su descriptor. Sin un mecanismo suplementario de designación, sólo los procesos que hayan heredado tal descriptor en su creación (por la primitiva fork) podrían utilizar un socket. La primitiva bind permite con una sola operación dar un nombre a un socket, es decir, asignar un nombre a un socket sin nombre.

int bind (sock, p_direccion, lg)

int sock; /* descriptor de socket */

struct sockaddr *p_direccion; /* puntero de memoria a la dirección*/

int lg; /* longitud de la dirección */

Bind hace que el socket, cuyo descriptor es sock, se una a la dirección de socket específica en la estructura apuntada por p_direccion; lg indica el tamaño de la dirección.

Para una utilización en un dominio particular, el puntero p_direccion apunta a una zona cuya estructura es la de una dirección en ese dominio (sockaddr_un para el dominio AF_UNIX y sockaddr_in para el dominio AF_INET).

El socket nombrado después de realizarse con éxito la primitiva (valor de retorno 0), puede ser identificado por cualquier proceso por medio de la dirección que se le ha dado sin necesidad de poseer un descriptor. Un cierto número de primitivas específicas permiten explotar estas direcciones.

Las causas de error (valor devuelto -1) de petición de conexión son múltiples: descriptor incorrecto, dirección incorrecta, inaccesible o ya utilizada, o un socket ya conectado a una dirección.

* Dominio UNIX.

En el dominio UNIX, los sockets no se destinan más que a una comunicación local. Por tanto, les corresponden direcciones locales que son referencias UNIX idénticas a las de los archivos. Un socket del dominio UNIX, aparecerá después del nombrado en los resultados producidos por la orden ls con el tipo s. La supresión de una referencia de este tipo, es por medio de la orden rm o de la primitiva unlink.

* Dominio Internet.

a) Preparación de la dirección.

La conexión a una dirección Internet de un socket de este dominio necesita la preparación de un objeto que tenga la estructura sockaddr_in. Esto supone en particular:

- el conocimiento de la dirección de la máquina local (obtenida por medio de la primitiva gethostname y la primitiva gethostbyname) o la elección del valor INADDR_ANY;

- la elección de un número de puerto.

b) Recuperación de una dirección.

Un proceso puede disponer de un descriptor de un socket conectado o enlazado a una dirección pero sin saber cuál es (si la conexión ha sido realizada por otro proceso y el proceso ha heredado el descriptor o si la conexión ha sido realizada sin especificar el número de puerto). La primitiva:

int getsockname (sock, p_adr, p_lg)

int sock; /* descriptor del socket */

struct sockaddr *p_adr; /* puntero a la zona de dirección */

int *p_lg; /* puntero a la longitud de la dirección */

permite recuperar la dirección relacionada con el socket del descriptor sock. Cuando se llama a esta primitiva, el tercer parámetro se utiliza como dato y como resultado:

- en la llamada, *p_lg tiene como valor el tamaño de la zona reservada a la dirección p_adr para recuperar la dirección del socket;

- en el retorno de la llamada, tiene como valor el tamaño efectivo de la dirección. El valor de retorno de la primitiva es 0 o -1 según si la llamada ha tenido éxito o no.

 

2.3.4. La primitiva Connect.

Después de la creación de un socket, un proceso hace la llamada connect para establecer una conexión activa con otro proceso remoto, es decir, abre un circuito virtual entre dos sockets, el cual permite intercambios bidireccionales

int connect (sock, p_adr, lgadr)

int sock; /* descriptor del socket local */

struct sockaddr *p_adr; /* dirección del socket remoto */

int lgadr; /* longitud dirección remota */

El socket correspondiente al descriptor sock será conectado o enlazado a una dirección local. Connect intenta contactar con el ordenador remoto con el objeto de realizar una conexión entre el socket remoto y el socket local especificado en sock.

La conexión puede establecerse (y la primitiva connect devuelve el valor 0) si se cumplen las siguientes condiciones:

 

a) los parámetros son "localmente" correctos ;

b) la dirección *p_adr se asocia a un socket del tipo SOCK_STREAM en el mismo dominio que el socket local de descriptor sock y un proceso (servidor) tiene solicitado escuchar sobre este socket (por una llamada a listen);

c) la dirección *p_adr no está utilizada por otra conexión;

d) el archivo de conexiones pendientes del socket distante o remoto no está lleno.

En caso de éxito (valor de retorno 0 de la primitiva connect), el socket local sock está conectado con un nuevo socket y la conexión está pendiente hasta que el proceso llamado tenga conocimiento de ella a través de la primitiva accept. Sin embargo, el proceso que llama puede comenzar a escribir o a leer del socket.

En el caso de que no se cumpla alguna de las tres primeras condiciones, el valor devuelto por la primitiva es -1, indicando error.

El comportamiento de la primitiva es particular si no se cumple la condición d:

- si el socket es de modo bloqueante, el proceso se bloquea. La petición de conexión se repite durante un cierto tiempo; si al cabo de este lapso de tiempo la conexión no se ha podido establecer, el proceso es despertado.

- si el socket es de modo no bloqueante, la vuelta es inmediata. Sin embargo, la petición de conexión no se abandona en seguida (se repite durante el mismo lapso de tiempo). Finalmente, en el caso de que se realice una nueva solicitud de conexión del mismo socket (por una llamada a connect) antes de que se haya abandonado la petición anterior, la vuelta de la primitiva es igualmente inmediata.

 

2.3.5. La primitiva Write.

A través de esta primitiva, dos procesos pueden intercambiar información a través de una conexión TCP. En el caso de un cliente y un servidor, el cliente realizaría con esta primitiva peticiones, y el servidor les daría réplica.

Esta primitiva requiere tres argumentos: el descriptor de un socket al cual deben ser enviados los datos, la dirección de los datos a ser enviados y la longitud de los datos.

int write (sock, msg, lg)

int sock; /* descriptor del socket local */

char *msg; /* dirección de memoria del mensaje a enviar */

int lg; /* longitud del mensaje */

Normalmente, para facilitar la comunicación, la primitiva write no manda la información directamente sobre el socket, sino que lo hace sobre un buffer perteneciente al kernel del sistema operativo, lo que permite que la aplicación siga su ejecución mientras se transmiten los datos a través de la red. En el caso de que el buffer estuviese lleno, se bloquearía la aplicación hasta que los datos del mismo pudiesen ser mandados por la red.

 

2.3.6. La primitiva Read.

De la misma forma que con la primitiva write los procesos mandaban información a través de la red, con la primitiva read, los procesos esperan información procedente de otros procesos que desean comunicar con ellos. El uso de una primitiva write por parte del proceso llamante, implica que para que el proceso receptor pueda recibir lo que ha escrito el primero, necesita ejecutar la primitiva read, de forma que el proceso receptor pueda leer los datos de una conexión TCP.

Al igual que la primitiva write, esta primitiva requiere también tres argumentos: el descriptor del socket a usar, la dirección del mensaje a enviar y la longitud de dicho mensaje.

int write (sock, msg, lg)

int sock; /* descriptor del socket local */

char *msg; /* dirección de memoria del mensaje a enviar */

int lg; /* longitud del mensaje */

Cuando llegan datos, read los copia del socket y los ubica en el buffer del área de usuario. En el caso de que no haya llegado ningún dato, read bloquea la aplicación hasta que llegue algún dato al socket. Si llega más información de la que cabe en el buffer, read solo extrae lo que tenga cabida en el buffer, y en el caso de que hayan llegado menos datos que espacio en el buffer, read los copia todos y devuelve el número de bytes leídos.

 

2.3.7. La primitiva Listen.

Cuando un socket es creado, no se encuentra en estado pasivo (por ejemplo, preparado para ser usado por un servidor) ni activo (por ejemplo, para ser usado por un cliente), hasta que la aplicación tome una acción determinada. Los servidores en modo conectado realizan la llamada listen para colocar un socket en modo pasivo (pasándolo como argumento), y prepararlo para futuras conexiones entrantes que tendrán lugar, es decir, no hará nada hasta que llegue una conexión. Para llamar con éxito a esta primitiva (con valor de retorno 0), el proceso debe disponer de un descriptor de socket del tipo SOCK_STREAM.

La mayoría de los servidores consisten en un bucle infinito que acepta cada conexión entrante que le llega, la maneja, y después sigue en el bucle a la espera de otras conexiones. Si en el momento en el que el servidor atiende una petición (conexión) no llega ninguna otra conexión, no existe problema, pero este si existe en el momento en que el servidor está atendiendo una determinada conexión, y en ese instante llega otra. Para asegurar de que esta nueva conexión no se pierda, a la primitiva listen se le pasa un argumento que le indica al sistema operativo que encole la petición de conexión para un socket recibida. Así pues, los dos argumentos que se les pasa a la llamada listen son el socket que debe ser ubicado en modo pasivo, y el tamaño de la cola que va a ser usada por dicho socket.

int listen (sock, nb)

int sock; /* descriptor de socket */

int nb; /* número máximo de peticiones de conexión pendiente */

 

2.3.8. La primitiva Accept.

Esta primitiva permite extraer una conexión pendiente de la cola asociada a un socket para la cual se ha realizado una llamada a listen. De esta manera, el sistema toma conocimiento de un enlace realizado.

int accept (sock, p_adr, p_lgadr)

int sock; /* descriptor del socket */

struct sockaddr *p_adr; /* dirección del socket conectado */

int *p_lgadr; /* puntero al tamaño de la zona reservada a p_adr */

Una vez extraída la petición de conexión, accept crea un nuevo socket con las mismas propiedades que sock y reserva (que se devuelve como resultado de la función) un nuevo descriptor de fichero para él. El socket de servicio creado se enlaza a un nuevo puerto (tomado del conjunto de puertos no reservados).

A la vuelta, también se recupera memoria en la zona apuntada por p_adr, la dirección del socket del cliente con el cual se ha restablecido la conexión. El valor de *p_lgadr se modifica: si en la llamada era el tamaño de la zona reservada para guardar la dirección, en el retorno da el tamaño efectivo de la dirección.

En el caso en el que no exista ninguna conexión pendiente, el proceso se bloquea hasta que exista una (o hasta que llegue una señal) a menos que el socket esté en modo no bloqueante.

 

2.3.9. Sumario de primitivas.

Hasta ahora, hemos visto las primitivas más importantes usadas con sockets. Pero hay muchas más, por lo que pasamos a hacer un pequeño esquema en el que se muestran todas las primitivas existentes usadas con TCP:

Primitiva Función

socket

Crea un descriptor para que sea usado en la comunicación.

connect

Conexión con un cliente remoto.

write

Manda datos a través de una conexión.

read

Lee los datos entrantes de una conexión.

close

Termina la conexión y elimina el descriptor.

bind

Vincula una dirección local IP y un puerto de protocolo a un socket.

listen

Pone el socket en modo pasivo y establece el número de conexiones TCP entrantes que el sistema puede encolar.

accept

Acepta la siguiente conexión entrante.

recv

Recibe el siguiente datagrama entrante.

recvmsg

Recibe el siguiente datagrama entrante (variación de recv).

recvfrom

Recibe el siguiente datagrama entrante y graba la dirección fuente.

send

Envía un datagrama.

select

Informa de sockets listos para escribir o leer de ellos.

sendmsg

Envía un datagrama (variación de send).

sendto

Envía un datagrama, usualmente a un dirección previamente grabada.

shutdown

Termina un conexión TCP en una o ambas direcciones.

getpeername

Después de la llegada de una conexión, obtiene la dirección completa de la máquina remota desde un socket.

getsockopt

Obtiene las opciones actuales de un socket.

setsockopt

Cambia la opción de un socket.

 

3 EL MODELO CLIENTE/SERVIDOR.

El modelo cliente/servidor, es el modelo de ejecución que siguen todas las aplicaciones de red. Un servidor es un proceso que se está ejecutando en un nodo de la red, y su función es gestionar el acceso a un determinado recurso. Un cliente es un proceso que se ejecuta en el mismo nodo, o en uno diferente, y que realiza peticiones al servidor. Las peticiones están originadas por la necesidad de acceder al recurso que gestiona el servidor.

La comunicación entre cliente y servidor, puede ser o bien orientada a la conexión, o bien sin conexión. En el caso de que la comunicación sea orientada a la conexión, esta se lleva a cabo mediante el establecimiento de circuitos virtuales entre el cliente y el servidor. En este caso, el intercambio de información se realiza con una alta fiabilidad fluyendo la información a través del circuito virtual de una forma secuencial, es decir, tiene un flujo continuo. Esto no ocurre en el caso de que la comunicación sea sin conexión, puesto que aquí el intercambio de información se efectúa mediante el envió de datagramas. La fiabilidad es menor, y al contrario que en el caso anterior, los datagramas no siguen un flujo continuo.

La comunicación sin conexión presenta un aspecto simétrico en la medida de que el iniciador del diálogo puede ser cualquiera de los dos que intervienen. Esto no ocurre en el caso de la comunicación orientada a la conexión, ya que uno de los dos procesos (en posición de cliente) pregunta al otro (en posición de servidor) si acepta esta comunicación.

Dado que la comunicación que normalmente se utiliza es orientada a la conexión, describiremos a continuación cómo sería el comportamiento tanto del cliente como del servidor con este tipo de servicio, mostrando la secuencia de llamadas de cliente y servidor para un servicio sin conexión al final.

3.1. El servidor

El servidor está continuamente esperando peticiones de servicio. Cuando se produce una petición, el servidor despierta y atiende al cliente. Cuando el servicio concluye, el servidor vuelve al estado de espera. De acuerdo con la forma de prestar el servicio, podemos considerar dos tipos de servidores:

- Servidores interactivos: El servidor no sólo recoge la petición de servicio, sino que él mismo se encarga de atenderla. Esta forma de trabajo presenta un inconveniente; si el servidor es lento en atender a los clientes y hay una demanda de servicio muy elevada, se van a originar unos tiempos de espera muy grandes.

- Servidores concurrentes. El servidor recoge cada una de las peticiones de servicio y crea otros procesos para que se encarguen de atenderlas. Este tipo de servidores sólo es aplicable en sistemas multiproceso, como UNIX. La ventaja que tiene este tipo de servicio es que el servidor puede recoger peticiones a muy alta velocidad, porque está descargado de la tarea de atención al cliente. En las aplicaciones donde los tiempos de servicio son variables, es recomendable implementar este tipo de servidores.

Su papel es pasivo en el establecimiento de la comunicación, ya que después de haber avisado al sistema al que pertenece de que está preparado para responder a las peticiones de servicio, el servidor se pone a la espera de peticiones de conexión que provengan de clientes. Para esto dispone de un socket de escucha, enlazado al puerto TCP correspondiente al servicio, sobre el que espera las peticiones de conexión. Cuando llega al sistema una petición de este tipo, se despierta al proceso servidor y se crea un nuevo socket, que se llama socket de servicio, el cual se conecta al cliente. Entonces el servidor podrá, por una parte delegar el trabajo necesario para la realización del servicio a un nuevo proceso (creado por fork) que utilizará entonces la conexión, y por otra parte volverá al socket de escucha.

Después de la aceptación de la comunicación, el servidor tiene dos posibilidades:

- o hacerse cargo de ella, lo que significa eventualmente que otras conexiones pendientes no serán efectivamente aceptadas hasta que el servicio demandado haya sido satisfecho

- o bien subtratar la gestión de la conexión y la realización del servicio mediante un proceso hijo.

La secuencia de primitivas para la utilización de sockets que el servidor tiene que usar, y su orden, se muestra en el diagrama expresado a continuación.

Figura 2.- Secuencia de llamadas hecha por un servidor.

En el caso de que la comunicación sea sin conexión, la secuencia de llamadas varía sensiblemente, puesto que no es necesario encolar las peticiones que le llegan al servidor, ni esperar a que la conexión se efectúe. El servidor, una vez conectado al puerto correspondiente, se bloquea hasta que recibe alguna petición por parte de un cliente, contestando a este, y retomando la escucha a la espera de nuevas peticiones. La secuencia de llamadas:

Figura 3.- Secuencia de llamadas hecha por servidor en una comunicación sin conexión.

 

3.2 El cliente

El cliente es la entidad activa en el establecimiento de una conexión, puesto que es el que toma la iniciativa de la demanda de conexión a un servidor. Esta demanda se realiza por medio de la primitiva connect, solicitando el establecimiento de una conexión que será conocida por los dos extremos. Además, el cliente está informado del éxito o del fracaso del establecimiento de la conexión.

Para que un proceso cliente inicie una conexión con un servidor a través de un socket, es necesario realizar una llamada a connect. Así, se crea un circuito virtual entre los dos procesos cuyos extremos son los sockets. La secuencia de primitivas para la utilización de sockets que el servidor tiene que usar, se muestra a continuación.

Figura 4.- Secuencia de llamadas hecha por un cliente.

Pero en el caso de una comunicación sin conexión, el cliente no utiliza la llamada connect para establecer un circuito virtual entre él y el servidor, sino que lo único que hace es conectarse a un puerto por el cual envía peticiones de servicio a un servidor. La secuencia de llamadas:

 

Figura 5.- Secuencia de llamadas hecha por un cliente en una comunicación sin conexión.

 

3.3 Comunicación cliente/servidor orientada a la conexión.

Una vez establecida la conexión entre un servidor y un cliente a través de dos sockets, los dos procesos pueden intercambiar flujos de información. El corte en diferentes mensajes no está preservado en el socket destino. Esto significa que el resultado de una operación de lectura puede provenir de la información resultado de varias operaciones de escritura. En el caso de los sockets del dominio Internet, una petición de escritura de una cadena de caracteres larga, puede provocar la partición de esta cadena, siendo accesibles los diferentes fragmentos por el socket destino. La única garantía que proporciona el protocolo TCP es que los fragmentos son accesibles en el orden correcto. Esto implica que la sincronización de una recepción y una emisión en una conexión de un mismo número de elementos no está asegurada por este mecanismo.

Figura 6.- Interactuación entre clientes y servidor concurrente.

 

4. RUTINAS DE OPERACIÓN ADICIONALES.

4.1. Rutinas de ordenación de bytes.

Hay funciones para manejar las diferencias existentes, en la ordenación de bytes, entre diferentes arquitecturas de ordenadores y diferentes protocolos de red. Estas son:

u_long htonl(u_long hostlong); /* convierte host a network, long integer */

u_short htons(u_short hostshort); /* convierte host a network, short integer */

u_long ntohl(u_long netlong); /* convierte red a host, long integer */

u_short ntohs(u_short netshort); /* convierte red a host, short integer */

Estas funciones se diseñaron para los protocolos Internet. Afortunadamente, los protocolos XNS utilizan la misma ordenación de bytes que Internet. En los sistemas que utilizan la misma ordenación de byte que los protocolos Internet (sistemas basados en los Motorola 68000, por ejemplo) estas funciones son macros nulas.

Estas cuatro funciones operan sobre valores integer sin signo, aunque también lo hacen sobre los integer con signo (short integer = 16 bits, long integer = 32 bits).

 

4.2. Operaciones de bytes.

En las estructuras de dirección de sockets hay campos multibyte que necesitan ser manipulados. Algunos de estos campos, sin embargo, no son campos integer de C, así que se tienen que usar otras técnicas para operar con ellos de forma portable.

Se definen algunas rutinas que operan sobre strings de bytes definidos por el usuario, es decir, no son cadenas C estándar (terminadas en null). Las cadenas de bytes definidas por el usuario pueden tener bytes null dentro que no signifiquen final de cadena. En vez de eso, hay que especificar la longitud de cada cadena como un argumento en la función:

- bcopy(char *src, char *dest, int nbytes).- Copia el número de bytes especificado desde el fuente al destino (notar que el orden de los dos argumentos puntero es diferente del orden usado por la función estándar strcpy de I/O).

- bzero(char *dest, int nbytes).- Escribe el número especificado de bytes null al destino especificado.

- int bcmp(char *ptr1, char *ptr2, int nbytes).- Compara dos cadenas de bytes, y devuelve cero si son iguales, o no cero en caso contrario (difiere en los valores devueltos de la función strcpy).

Estas funciones se utilizan para operar con las estructuras de dirección de los sockets.

 

4.3. Rutinas de conversión de direcciones.

Una dirección Internet se escribe normalmente en formato decimal con punto, como por ejemplo 147.29.255.8. Las siguientes funciones hacen conversiones entre el formato decimal con punto y una estructura in_addr:

unsigned long inet_addr (char *ptr);

convierte una cadena de caracteres en notación decimal con punto a una dirección Internet de 32 bits, y la siguiente función:

char *inet_ntoa (struct in_addr inaddr);

hace la conversión contraria. UNIX ofrece un mecanismo de comunicación general entre dos procesos cualesquiera que pertenecen a un mismo sistema o a dos sistemas diferentes.

 

5. BIBLIOGRAFÍA.

* Computer Networks, 2ª edición. Autor: Tanembaum.

* Internetworking with TCP/IP, volumen 1, capítulo 5. Autor: D. E. Conner.