Home UC3M
Home IT
Home /Docencia /Ing. Informática /Redes y Servicios de Comunicaciones /Práctica

Práctica Redes y Servicios de Comunicaciones: Implementación de TCP sobre UDP

Fecha: 5 de Noviembre de 2003.
Conceptos: entramado, estados TCP, establecimiento y cierre de conexiones TCP, ventana deslizante, control de congestión en TCP, control de flujo en TCP
Programación: sockets UDP, colas circulares, procesos, manejadores de señales,  temporizadores, comunicación entre procesos.
Plazo de entrega: 5 de Febrero de 2004 (instrucciones)
Enunciado comprimido:
Profesores: Carlos García Rubio Andrés Marín López y Norberto Fernández García Guillermo Díez-Andino Sancho

  Introducción

El objeto de esta práctica es ilustrar los conceptos vistos en las clases de teoría sobre el nivel de control de transporte en Internet: TCP.
La práctica propone la utilización de sockets UDP para la construcción de una biblioteca que proporcione a las aplicaciones que la utilicen una funcionalidad similar a la ofrecida por TCP, aunque con restricciones. Para la realización de la práctica se requieren conocimientos básicos sobre sockets UDP, y dependiendo del lenguaje de programación elegido, también serán necesarios conocimientos adicionales sobre gestión y comunicación entre procesos, hebras, manejadores de señales y temporizadores.

La práctica proporciona algunos interfaces para implementar en la biblioteca, también se proporcionan algunas funciones auxiliares y código fuente de ejemplo. También se proporcionan ficheros de pruebas para poder auto-evaluar la práctica de forma bastante aproximada.

  Entramado

Para poder conseguir una implementación de TCP sobre UDP el primer paso es poder tener un formato de trama en el que incluir la información de:

  • tipo de segmento: inicio de conexión (SYN), fin de conexión (FIN), nadie escucha (RST), asentimiento (ACK), error (ERR)
  • tamaño de ventana (win)
  • número de asentimiento
  • número de secuencia
  • tamaño del segmento
La implementación elegida utilizará un entero de 16 bits para codificar el tipo de segmento, otro para la ventana, otro para el tamaño de trama, y dos enteros de 32 bits para los asentimientos y números de secuencia. La codificación será big-endian (atención programadores de C/C++!) .

La definición en C de esta cabecera se proporciona con el resto del material, y corresponde a la estructura:
        typedef struct ttcp_udp_header{
unsigned short size; // Número de bytes de datos del segmento

unsigned short type;
unsigned long ack;
unsigned long seq;
unsigned short win;
} *tcp_udp_header;
 Propiedades del socket TCP_UDP

La abstracción de sockets TCP_UDP que maneja la biblioteca a desarrollar debe de tener una serie de propiedades que nos permitirán implementar la funcionalidad de TCP. En los ficheros entregados se propone una estructura denominada stcp_socket que contiene las propiedades recomendadas que debe de tener una conexión:

  • estado de la conexión
  • descriptor del socket UDP
  • indicación de si el socket es activo o pasivo
  • uúmeros de secuencia del siguiente byte a enviar/recibir
  • último números de asentimiento enviado/recibido
  • tamaño de ventana anunciado por el otro extremo (win
  • tamaño de la ventana de congestión (cwnd)
  • umbral de la ventana de congestión (ssthresh)
  • buffer de envío
  • buffer de recepción
  • temporizador de segmentos pendientes de confirmación
La implementación de estas propiedades se hará a través de estructuras u objetos, dependiendo del lenguaje de programación utilizado.

 Máquina de estados

Debemos de implementar una máquina de estados que refleje como debe de actuar la conexión en cada momento dependiendo de las entradas. No es necesario implementar el total de estados de TCP, pues nos limitaremos a establecimientos y liberaciones de conexiones no simultáneas (three way handshake). En cuanto al interfaz programático, debe de realizarse igual que en TCP:

  • Las conexiones de tipo activo, se conectarán cuando se invoque su función tcp_connect(), devolviendo el resultado de la operación 0 (éxito), ó -1 (error).
  • Las conexiones de tipo pasivo sobre las que se haya invocado tcp_accept(), recibirán una referencia a la nueva conexión (en caso de éxito), ó -1 en caso de error.
  • Al invocar tcp_close() las conexiones devolverán 0 (éxito), ó -1 (error).
En la figura podemos ver el interfaz completo. Vemos como las aplicaciones llamarán a nuestras variantes de las funciones del API de socket, y que estas llamadas se traducirán finalmente en llamadas a sockets de tipo SOCK_DGRAM y que en realidad llamadas del tipo connect se convertirán a su vez en varios sendto y recvfrom para enviar y recibir los datagramas UDP con las banderas de SYN y ACK correspondientes. El encargado de llevar la cuenta de esto es nuestra máquina de estados.

Es de destacar que todas las funciones definidas en el interfaz devuelven un entero. En caso de ser negativo señala una condición de error. En la tabla se muestran las distintas llamadas y el significado del valor devuelto:

tcp_socket
"Descriptor" del socket creado (índice a una estructura tipo stcp_socket)
Los MAX_SOCKETS están ocupados
tcp_bind
Éxito
Ya hay un socket escuchando en dicho puerto
tcp_accept
"Descriptor" a un nuevo socket conectado (socket hijo)
La apertura de conexión ha fallado
(se recibió un RST o se alcanzó MAX_RETRIES)
tcp_connect
Éxito
La apertura de conexión ha fallado
(se recibió un RST o se alcanzó MAX_RETRIES)
tcp_read
Número de bytes leidos
Fallo en la lectura
(se recibió un RST)
tcp_write
Número de bytes enviados (no es necesario esperar a
que se hayan recibido los ack correspondientes)
Fallo en la escritura
(se recibió un RST o se alcanzó MAX_RETRIES)
tcp_close
Éxito
se recibió un RST o se alcanzó MAX_RETRIES

El comportamiento de la biblioteca al recibir segmentos fuera de secuencia es el siguiente:
El comportamiento de tcp_write es el siguiente:
  • se recibe solicitud de escritura de size bytes
  • se divide la solicitud de escritura en trozos de tamaño MSS (si size%MSS != 0 habrá un trozo menor que MSS)
  • se van copiando a la cola de transmisión para su envío (sino caben los size bytes se copian los que caben)
  • en el momento en que todos los datos copiados a la cola de transmisión han sido enviados se devuelve el número de bytes enviados
Es de gran ayuda mantener actualizado el valor de las variables SEQ y ACK para facilitar la implementación de tcp_read y tcp_write.

 Generación de números de secuencia

Cada conexión tiene un contador de números de secuencia que utiliza para numerar cada uno de los bytes que envía. Distintas conexiones tendrán distintos números de secuencia. En el caso de los servidores, hay que tener en cuenta que el número de secuencia junto con la dirección del otro extremo, se utilizan para distinguir los segmentos recibidos de una u otra conexión. Estos números de secuencia también nos sirven para saber a que datos se refiere un asentimiento recibido.

 slow start

De la aplicación iremos recibiendo buffers con datos para enviar al otro extremo. A la hora de ir haciendo los sendto, deberemos de ir teniendo en cuenta el tamaño de la ventana de congestión, su umbral, y el de la ventana de recepción que anuncia el otro extremo. Debemos de implementar el mecanismo de slow start, a fin de garantizar que no vamos a congestionar la red y que en caso de congestión disminuiremos nuestra tasa de envío y cwnd.



 Retransmisiones y control de congestión

Cuando recibamos un datagrama con la indicación de error (ERR) debemos pedir retransmisión, enviando un ack duplicado.

Los temporizadores tendrán un valor fijo de 1 segundo. El numero de retransmisiones que deberemos de hacer será de seis intentos.

En caso de que venza el temporizador de envío y no hayamos recibido el ack de un datagrama, deberemos realizar la retransmisión del datagrama en cuestión. Además estos eventos también pueden influir en la percepción de congestión, afectando al parámetro ssthresh.

Hay que considerar también los temporizadores de los segmentos de establecimiento (SYN) y liberación (FIN) de conexión, y el temporizador final de 2MSL (que a efectos prácticos valdrá 2 segundos).



 Memoria compartida

Es conveniente utilizar algún mecanismo de comunicación entre procesos (IPC) para compartir datos entre los procesos que utilizan la biblioteca. Por ejemplo, si una aplicación llama a tcp_accept() sobre un socket pasivo, obtiene un nuevo socket conectado (hijo) y posteriormente hace un fork(), ambos procesos deben tener acceso al socket pasivo y al hijo. Además ambos comparten el mismo socket UDP, que es único para todos los sockets que se obtengan de sucesivas llamadas a tcp_accept sobre un mismo socket pasivo.

Lo que proponemos es utilizar memoria compartida de forma que todos los objetos stcp_socket que gestiona la biblioteca se encuentren en una zona de común acceso a todos los procesos, y que los descriptores de los stcp_socket sean un offset dentro de la memoria compartida a la zona correspondiente al socket en cuestión. En el material entregado se encuentran las funciones para crear y liberar la memoria compartida.

 Colas de Almacenamiento

En la estructura de socket que se propone se introducen diversas variables para controlar las colas circulares necesarias para implementar el algoritmo de ventana deslizante que utiliza TCP. En concreto se definen:
  • dos arrays (rx_buf y tx_buf) para almacenar los datos (recepción/transmisión)
  • seis índices (tres por array de datos) para indicar el frente, el final y el número de elementos de cada cola
  • dos arrays auxiliares (rx y tx) que contienen información adicional de cada segmento de datos almacenado en la cola, en concreto el número de secuencia del segmento, los índices al primer y último bytes del segmento en el array de datos, y un indicador de si la estructura esta libre u ocupada.
El esquema propuesto se detalla en la siguiente figura:
colas
En la figura se muestra que podemos tener una gestión de las colas más sencilla si podemos sacrificar un poco de memoria. Por ejemplo, se propone que la estructura de almacenamiento (rx_buf/tx_buf) se divida en trozos de tamaño MSS y se copien los datos de cada segmento en cada trozo, de forma que el inicio del segmento y el fin del segmento se apuntan desde el array auxiliar (rx/tx) donde también se indica si un segmento esta libre o no. La razón de ser del índice al inicio de segmento es que la aplicación al consumir datos puede solicitar una lectura de un tamaño inferior al del bloque en cuestión, y este índice nos facilita este tipo de situaciones. El motivo del indicador de segmento libre es el siguiente: en la cola de recepción, indica que la aplicación ya ha consumido este segmento; en la cola de transmisión indica que se ha recibido el asentimiento correspondiente. En ambos casos, el espacio en la cola de datos correspondiente está libre y se puede utilizar para almacenar un nuevo segmento que se va a enviar o un segmento que se ha recibido.

En el material entregado se  propone una implementación de varias funciones para:
  •  insertar un segmento en una cola (encolar_segmento)
  • extraer un segmento de la cola de envío (desencolar_segmento_tx) y devolver el número de secuencia del segmento
  • extraer un segmento de la cola de recepción (desencolar_segmento_rx) y copiar los datos en un buffer
Su utilización es voluntaria y se puede optar por una implementacion mas tradicional de colas circulares.

 Temporizadores

En el nivel TCP existen diversos temporizadores y es necesario gestionarlos de manera coordinada y mínimamente eficiente. En general cada segmento que se envía tiene asociado un temporizador (RTO) que en caso de vencimiento marca la retransmisión del segmento. Puesto que podemos enviar varios segmentos (dependiendo de cwnd y nagle) antes de recibir sus asentimientos correspondientes, es necesario un mecanismo para gestionar todos.  En la figura se muestra una propuesta de utilización de una cola para almacenar los temporizadores. timers
En la figura se muestran los instantes de envío de cuatro segmentos (T0, T1, T1-2, T1-3). Cada segmento tiene asociado su propio tiempo de retransmisión (RTO_0, RTO_1, RTO_2, RTO_3) aunque en general serán muy parecidos a no ser que ocurran pérdidas o congestión en la red. Lo que se propone es almacenar en la cola de temporizadores la diferencia de los tiempos previstos de retransmisión entre cada segmento y el anterior. Esto permite que en el sistema solo haya un temporizador activo, para el evento que va a ocurrir primero. Cuando activamos dicho temporizador almacenamos el instante en una variable (en la figura corresponde a T0). Cuando salta el temporizador, se retransmite el segmento y se vuelve a activar el temporizador con el valor del siguiente elemento en la cola. En caso de que sea necesario quitar el temporizador activo (como en la figura que ha llegado el ACK_1 que asiente el segmento 0), tenemos que:
  •  marcar el frente de la cola como libre, 
  • avanzar el frente de la cola al siguiente elemento, 
  • cambiar su valor teniendo en cuenta el valor del elemento anterior (pues cada uno es relativo al anterior) y el instante de tiempo actual, y
  • activar de nuevo el temporizador con el valor del elemento en el nuevo frente
En resumen, si salta el temporizador solo necesitaremos avanzar el frente y activarlo de nuevo, y en caso de que vayamos a quitarlo, solo tenemos que manipular un elemento de la cola.

 Programadores de C

Para la gestión de temporizadores se recomienda el uso de las funciones setitimer y getitimer con el ITIMER_REAL. La señal SIGALRM se entregará al proceso en cuyo socket hay que retransmitir un segmento, y si se ha registrado previamente un manejador para esta señal (con sigaction) se ejecutará la función indicada.

También resulta aconsejable registrar manejadores para cuando haya datos disponibles en el socket UDP. En caso de que los datos puedan ser para varios sockets distintos, se puede optar por dos alternativas.

  • Utilizar lecturas (recvfrom) con bandera MSG_PEEK de forma que si el segmento no es para el socket de este proceso se puede volver a consumir en sucesivas lecturas por otro proceso.
  • Si tenemos actualizados los valores SEQ y ACK de los distintos sockets, podemos consultar en aquellos que tengan nuestro mismo puerto y encolarlo en la cola de recepción correcta.
En la clase teórica de sockets se incluirá información sobre la utilización de manejadores de señales.
 Material y pruebas

Para homogeneizar las prácticas se entrega un fichero con las definiciones de funciones a implementar por la práctica, asi como el entramado: tcp_udp.h. En caso que se opte por la utilización de manejadores de señales se da un ejemplo de parte de un servidor.



 ENTREGA

  • Entrega: Código fuente y Makefile de la práctica. 
  • Plazo: 5 de Febrero de 2004.
  • Modo de entrega: en el directorio $HOME/rysc/febrero de vuestra cuenta.


 REFERENCIAS

Especificaciones:



Última actualización: Thu, 03 Oct 2002 15:09:52 GMT

Localización |Personal |Docencia |Investigación |Novedades |Intranet
inicio | mapa del web | contacta