Member of The Internet Defense League Últimos cambios
Últimos Cambios
Blog personal: El hilo del laberinto Geocaching

Desarrollo de una Herramienta Software para el Acceso a Redes TCP/IP a través de la Red Telefónica Conmutada

Última Actualización: 30 de Junio de 1.996 - Domingo

Capítulo 8:
El Módulo TCP

El módulo TCP (Transport Control Protocol) [RFC793] es el equivalente al nivel OSI de transporte orientado a conexión. Sus características principales son:

  • Conexiones fiables, garantizando la integridad de la información, su secuenciamiento correcto y la no pérdida o duplicación

  • Adaptación automática al tiempo de tránsito de la red y fiabilidad frente a congestión

  • Capacidad de envío de datos urgentes

  • Gestión de búfferes en transmisión y recepción

  • Posibilidad de esperar conexiones remotas en ciertos canales especificados, denominados "puertos"

  • Conexiones 8 bits orientadas al byte, sin distinción alguna de campos, y bidireccionales

En cuanto a los servicios que utilizan este módulo, destacan:

  • Distribución de las tablas de enrutado
    Para que la red en su conjunto ofrezca una imagen homogenea y coherente es necesario que las diferentes pasarelas dispongan de información actualizada sobre su topología. Esto se consigue mediante el intercambio de información de encaminamiento entre los diferentes sistemas, vía los servicios UDP y TCP.

  • Protocolo TELNET [RFC854]
    Este servicio posibilita el acceso en modo terminal a una máquina remota, de forma transparente.

  • Protocolo de transferencia de ficheros (FTP) [RFC959]
    Este protocolo permite el intercambio de ficheros residentes en máquinas distintas, de forma simple, rápida y fiable.

  • Protocolo de transferencia de correo electrónico (SMTP) [RFC821]
    Uno de los servicios más utilizados y útiles de las redes informáticas.

  • Protocolo de transporte hipertexto (HTTP) [HTTP1.1] [RFC1866]
    Esta familia de protocolos son los principales responsables de crecimiento exponencial de Internet, al facilitar considerablemente la búsqueda, el acceso y la gestión de la información desperdigada por la red.

El protocolo TCP se define en [RFC793], texto notable por el gran número de erratas y ambigedades que contiene. En [RFC813], [RFC879], [RFC876], [RFC944] y [RFC1122] se concretan algunos detalles oscuros y se eliminan diversos errores.


Interfaz

La interfaz de este módulo está constituida por procesos y por subrutinas ejecutadas en el contexto del llamante. Su utilización es muy simple.


PROC_TCP_BC

Este proceso es el encargado de inicializar este módulo. Debe ser invocado con un mensaje "MSG_INIT" o "MSG_QUIT". La inicialización de este módulo debe ser posterior a la del módulo IP.


PROC_TCP_SUP
Este es el proceso que recibe los mensajes de las capas superiores y los gestiona adecuadamente. Los mensajes definidos son:

  • MSG_TCP_OPEN

    Este mensaje informa a la capa TCP que se acepta una conexión solicitada por una máquina remota. Los valores de los campos son:

    campo1: Ignorado
    campo2: Identificador de la conexión
    campo3: Ignorado

    El enlace podrá ser utilizado a partir de ese momento. Este mensaje es generado por una capa superior cuando ésta decide aceptar una conexión remota propuesta por el módulo TCP.

  • MSG_TCP_CLOSE

    Este mensaje informa a la capa TCP que no tenemos más información que transmitir a través de una conexión dada. TCP se encarga de que cualquier dato pendiente en los búfferes internos sea correctamente entregado. La conexión sigue abierta para recibir, y no se cerrará hasta que el otro extremo lo decida o enviemos el mensaje definido a continuación. Los valores de los campos son:

    campo1: Ignorado
    campo2: Identificador de la conexión
    campo3: Ignorado

  • MSG_TCP_ABORT

    Este mensaje cierra una conexión en ambas direcciones. Cualquier dato en tránsito o en los búfferes internos se perderá. Los parámetros de este mensaje son:

    campo1: Ignorado
    campo2: Identificador de la conexión
    campo3: Ignorado

  • MSG_MBUF

    A través de este mensaje una capa superior informa al módulo TCP que hay nueva información para transmitir. La capa TCP se hará cargo de la entrega de los datos. Los parámetros del mensaje son:

    campo1: Cadena de MBUFs conteniendo la información que se desea transmitir
    campo2: Identificador de la conexión
    campo3: Puede contener los valores CERO, PUSH o MODO_URGENTE

    Un valor de cero indica que los datos especificados no están sujetos a ningún tratamiento especial. Si su longitud es pequeña la capa TCP puede realizar un almacenamiento temporal en espera de nuevos datos en vez de enviar un segmento de tamaño reducido.

    Si campo3 tiene el valor "PUSH" significa que los datos deben enviarse cuanto antes al otro extremo, y que éste debe entregarlos lo antes posible al proceso responsable. En cuanto a "MODO_URGENTE", supone un "PUSH" implícito (aunque es recomendable incluir la bandera "PUSH" para que sea señalada adecuadamente en el otro extremo) y su objetivo primordial consiste en la resincronización de la transmisión y el intercambio de datos "fuera de banda".

  • MSG_TCP_TIMEOUT

    Este mensaje se genera cuando vence el número de reintentos en alguna conexión. Ello puede ser debido a que la red se ha roto, a que la máquina destino se ha caido o bien a que el tiempo de tránsito de los datagramas en la red es demasiado elevado.

    campo1: Indeterminado
    campo2: Identificador de la conexión
    campo3: Indeterminado

    La conexión se cierra de forma automática.


PROC_TCP_INF

Este proceso recibe los segmentos TCP y los gestiona adecuadamente. Está diseñado para intercomunicarse con los procesos en los módulos IP e ICMP. Los mensajes que espera recibir son:

  • MSG_ICMP_SOURCE_QUENCH

    Este mensaje indica a la capa TCP que alguna de sus conexiones transcurre a través de una red muy cargada (congestión) y que debería reducir su tasa de transferencia para aliviarlo. Los parámetros de este mensaje fueron definidos en el módulo ICMP.

  • MSG_ICMP_DEST_UNREACHABLE

    Este mensaje informa a la capa TCP que alguna de sus conexiones (o intentos de conexión) no puede alcanzar a la máquina destino. Los parámetros de este mensaje fueron definidos en el módulo ICMP.

  • MSG_MBUF

    Este mensaje contiene un segmento TCP recibido por la capa IP. Su formato corresponde al definido en el capítulo dedicado al módulo IP.

En cuanto a los mensajes que transmite, tenemos:

  • MSG_TCP_OPEN

    Este mensaje informa a una capa superior que se ha recibido una petición de conexión a uno de sus puertos declarados como "LISTEN". Para que la conexión se establezca la capa superior debe responder con otro "MSG_TCP_OPEN" dirigido a "PROC_TCP_SUP", tal y como se ha visto previamente.

    Este mensaje también se genera cuando somos nosotros los que iniciamos la conexión y la máquina remota lo acepta (ver más adelante).

    El formato de este mensaje es:

    campo1: Cabecera TCP interfaz
    campo2: Identificador de la conexión
    campo3: Indeterminado

    La cabecera TCP interfaz se define como:

    
        typedef struct {
          uint16  puerto_remoto;
          uint16  puerto_local;
          uint32  ip_remoto;
          uint32  dummy[4];
        } tcp_header;
    

    "puerto_remoto" y "puerto_local" indican los puertos a través de los cuales se ha establecido la conexión. "ip_remoto" contiene la dirección IP de la otra máquina. "dummy" contiene valores indeterminados.

  • MSG_TCP_CLOSE

    Este mensaje es generado cuando la máquina remota no desea transmitir más información. Con ello se informa al nivel superior de que no hay más datos pendientes. No obstante nosotros podemos seguir transmitiendo.

    Este mensaje también es generado cuando se aborta una conexión, ya sea en la negociación inicial o bien durante la fase de transferencia. En ese caso cualquier dato que queramos transmitir será ignorado.

    El formato de este mensaje es:

    campo1: Ignorado
    campo2: Identificador de la conexión
    campo3: Ignorado

  • MSG_MBUF

    Con este mensaje enviamos a las capas superiores los datos que se van recibiendo. El formato es idéntico al especificado para el proceso "PROC_TCP_SUP". No obstante resulta conveniente realizar algunas matizaciones:

    Dado que tanto los procesos de transmisión como los de recepción TCP incorporan mecanismos de buffering no puede esperarse que cada segmento enviado a este módulo mediante "MSG_MBUF" genere uno y sólo un mensaje "MSG_MBUF" en el receptor. En un momento dado el transmisor puede decidir retrasar el envío de un segmento debido a su escasa longitud, congestión de la red, o al cierre de la ventana de transmisión. Por otra parte, un bloque de información puede suponer el envío de más de un segmento debido al MSS (Maximum Segment Size) negociado al principio de la conexión o la MTU de las redes intermedias. Además el receptor puede decidir concatenar varios segmentos en un sólo "MSG_MBUF" si la red cambia su secuenciamiento, etc.

    La opción "PUSH" tiene como objetivo la entrega cuanto antes de los datos pendientes al proceso destino. No obstante tampoco sirve como delimitador de campos dentro de lo que es la propia secuencia de bytes. El proceso receptor puede no ver el "PUSH" en la misma posición que el transmisor, ni recibirse el mismo número de PUSHs que se transmitieron. De hecho en [RFC1122] se especifica que la bandera "PUSH" no necesita ser transferida al proceso receptor.

    En cuanto a "MODO_URGENTE", tampoco sirve como delimitador claro. Su tarea consiste en conmutar de modo al proceso receptor. Su funcionamiento es inmediato: cuando el transmisor recibe un mensaje "MSG_MBUF" urgente, todos los datos previos todavía no entregados serán marcados como urgentes en el receptor. La utilidad habitual de todo esto consiste en la recuperación de sincronismo entre el transmisor y el receptor. El proceso receptor puede, por ejemplo, ignorar todos los datos marcados como urgentes. Se utiliza una técnica parecida en el protocolo TELNET [RFC854].

    Lo único que se garantiza en "MODO_URGENTE" es que los datos que siguen a un mensaje urgente y que no están marcados con ese modo no son recibidos como urgentes en el proceso receptor, siempre y cuando no haya ningún segmento urgente posterior. Esa es la única forma de marcar campos que tiene este protocolo.

En cuanto a rutinas, tenemos:


uint16 tcp_puerto_libre(void);

Esta rutina devuelve un puerto TCP actualmente no utilizado. Por compatibilidad con protocolos superiores, el valor del puerto es igual o superior a 1024.


estado tcp_listen(uint16 puerto,proc_id proc_retorno,puint32 id);

Esta rutina activa un puerto y espera un intento de conexión remoto. "puerto" es el valor del puerto en el que nos ponemos a escuchar. "proc_retorno" contiene el identificador del proceso al cual hay que informar de cualquier evento. Por último "id" es un puntero al lugar donde hay que dejar el identificador de esta conexión. La rutina retorna "OK" si todo ha ido bien y "ERR_OVERFLOW" si hay demasiadas conexiones TCP. "id" contendrá el identificador que debemos utilizar para comunicarnos con dicha conexión.

Si el puerto especificado ya está en modo "LISTEN" devuelve "ERR_EN_USO". Si necesitamos aceptar varias conexiones a un mismo puerto tendremos que lanzar un nuevo "LISTEN" cuando se recibe una petición remota de conexión y se ocupa el anterior.

Cualquier intento de conexión genera un mensaje "MSG_TCP_OPEN" que puede ser aceptado con otro "MSG_TCP_OPEN" o denegado con un "MSG_TCP_CLOSE". Si se deniega, el puerto regresará al modo "LISTEN" y esperará un nuevo intento de conexión.


uint32 tcp_connect(uint16 puerto,uint32 ip,proc_id proc_retorno);

Esta rutina inicia una conexión al puerto "puerto" de la máquina "ip". "proc_retorno" es el proceso al cual hay que informar de cualquier eventualidad. La rutina retorna el identificador asociado a esa conexión. Si es cero indica que hay demasiadas conexiones abiertas. El puerto local utilizado en la conexión se elige de forma arbitraria.

Si la conexión es aceptada se produce un "MSG_TCP_OPEN", mientras que si se deniega se enviará un "MSG_TCP_CLOSE". Si se excede el número de reintentos se enviará "MSG_TCP_TIMEOUT".


tcp_estado tcp_status(uint32 id);

Esta rutina devuelve el estado de una conexión, a partir de su identificador. Los valores posibles son:

  • CLOSED

    La conexión no existe.

  • LISTEN

    Esperamos una petición remota de conexión.

  • LISTEN_2

    Hemos recibido un intento de conexión que todavía no hemos aceptado o denegado. Este estado ha sido añadido en nuestra implementación, no estando recogido en el documento original [RFC793]. Su objetivo es el evitar que dos conexiones diferentes pero casi simultaneas al mismo puerto "LISTEN" causen problemas. La segunda conexión será ignorada y cuando el segmento original sea reenviado ya habremos decidido si crear o no un nuevo puerto "LISTEN".

  • SYN_SENT

    Intentamos una conexión.

  • SYN_RECEIVED

    Hemos recibido un intento de conexión, lo hemos aceptado y ahora estamos esperando a que el otro extremo complete su inicialización.

  • ESTABLISHED

    Conexión establecida.

  • FIN_WAIT_1

    La conexión está abierta pero nosotros hemos decidido que no tenemos nada más que decir y estamos esperando a que el otro extremo tome nota de ese cambio de situación.

  • FIN_WAIT_2

    La conexión sigue abierta, pero nosotros no vamos a transmitir nada más y el otro extremo TCP ha informado de ello a sus niveles superiores.

  • CLOSE_WAIT

    La conexión sigue abierta, pero el otro extremo nos ha indicado que no piensa transmitir nada más.

  • CLOSING

    Ambos extremos hemos decidido que no hay nada más que decir y estamos procediendo a cerrar la conexión

  • LAST_ACK

    Estado equivalente a "CLOSING". Ver la sección de implementación para más detalles.

  • TIME_WAIT

    La conexión ya ha sido cerrada y no puede utilizarse, pero todavía no eliminamos las tablas internas asociadas por si permanecen todavía segmentos en tránsito en la red. Tras un tiempo prudencial en este estado se elimina el contexto que quedaba y se pasa al estado "CLOSED".


estado tcp_flujo(uint32 id,uint16 tamanho);

Esta rutina es la que implanta el mecanismo de control de flujo TCP. Cada vez que una capa superior desea enviar información a través de una conexión TCP "id", debe pedir permiso. La rutina devuelve "OK" si el flujo está abierto y "FLUJO_LLENO" si la conexión no se encuentra en el estado "ESTABLISHED" o "CLOSE_WAIT".

Es posible, aunque no recomendable, enviar información a través de una conexión aunque su control de flujo esté cerrado. Con ello se pretende simplificar la implementación de algunos protocolos superiores. No debería abusarse de esta característica si no se está completamente seguro de sus implicaciones.

Si el control de flujo está cerrado hay que volver a intentarlo tras un tiempo prudencial (por ejemplo, una décima de segundo).


void tcp_cerrar_flujo_rx(uint32 id);

A través de esta llamada se informa a la máquina remota que la conexión "id" no admite más datos. La capa TCP enviará un segmento vacío con el fin de actualizar la información de ventana del otro extremo. Aún cuando se reciban más segmentos no se enviarán hacia la capa superior. No obstante se entregará cualquier segmento que ya hubiese sido enviado al dispatcher.

Esta acción sólo debe solicitarse en casos de necesidad. En [RFC1122] se especifica claramente que debe evitarse su uso en lo posible.


void tcp_abrir_flujo_rx(uint32 id);

Esta rutina complementa la anterior e informa al otro extremo que estamos dispuestos a aceptar nuevos datos. La capa TCP enviará un segmento para que la máquina remota pueda actualizar su información de ventana.


void tcp_trace(uint32 id,ttcp_trace POINTER trace);

Esta rutina facilita diversa información de depuración y análisis sobre una conexión dada. "id" contiene el identificador de la conexión en la que estamos interesados. "trace" es un puntero a una estructura definida como:


  typedef struct {
    tcp_estado estado;
    uint32     bytes_tx;
    uint32     bytes_rx;
    uint32     srtt;
    uint16     ventana_tx;
    uint16     bytes_cola_tx;
    uint16     mss;
  } ttcp_trace;

"estado" contiene el estado de la conexión, tal y como se vió con anterioridad. "bytes_tx" y "bytes_rx" indican el número de bytes transmitidos y recibidos, respectivamente. "srtt" es el tiempo ida y vuelta estimado de la conexión, en milisegundos. "ventana_tx" informa del tamaño de la ventana publicada por el otro extremo. "bytes_cola_tx" es el número de bytes que pendientes de confirmación. Por último "mss" es el tamaño máximo de segmento que admite la máquina remota.


Implementación

Este módulo sigue escrupulosamente todas las guías definidas en [RFC793], implantándolo completamente. Se trata de un protocolo complejo y ambiguo. El texto original contiene dos errores graves:

  • El gráfico de la página 23 (diagrama de estados) no se corresponde al texto en sí del documento, que elimina el estado "LAST_ACK" y transfiere el arco de salida de "CLOSE_WAIT" a "CLOSING". El efecto que tiene este cambio es el permanecer en "TIME_WAIT" aún cuando el otro extremo nos haya confirmado la finalización. Ello resulta útil ante la posibilidad de que la red desordene o duplique datagramas. Nosotros hemos optado por implantar esa característica, por lo que el estado "LAST_ACK" no se alcanza nunca.

  • Según [RFC944] el documento TCP [RFC793] contiene un error respecto a la gestión de datos urgentes. Hemos programado este módulo teniendo en cuenta esta eventualidad.

En [RFC1122] se indican más errores en la especificación original, y se concretan muchos detalles oscuros.

En la implementación actual los mensajes de control de flujo, "MSG_ICMP_SOURCE_QUENCH", son ignorados. La razón de ello es que este Proyecto ha sido diseñado para trabajar en redes de baja velocidad (modems) y con acceso final a redes rápidas, por lo que la congestión que provocamos es nula. Se mantiene en estudio el utilizar un esquema sencillo de control de flujo mediante, por ejemplo, la paralización de las transmisiones TCP durante un tiempo determinado (por ejemplo, cinco o diez segundos). Los posibles mensajes "MSG_ICMP_DEST_UNREACHABLE" también son ignorados, confiando en el mecanismo de reintentos para o bien solucionar el problema o bien cerrar la conexión.

A la hora de calcular el tiempo de tránsito de la red se cronometra el tiempo transcurrido entre el envío de un segmento y su confirmación, sujeto a una exponencial 2^-x. Suponemos que el retardo de la red es aproximadamente igual en ambas direcciones. El mecanismo de retransmisiones se dispara cuando transcurre un período superior al 25% del RTT estimado sin que llegue alguna confirmación. Cada vez que se realiza una retransmisión crece el RTT, y la conexión se aborta cuando éste supera un valor crítico (actualmente unos 75 segundos).

Todo este mecanismo está siendo estudiado con cuidado, intentando acomodarlo lo máximo posible a las características de este Proyecto. En [RFC1122] se mencionan dos esquemas alternativos que dejan obsoleto a éste, pero desgraciadamente no han sido publicados como RFCs. Uno de nuestros mayores problemas es el hecho de que la longitud de los datagramas influye considerablemente en el RTT, ya que nuestro enlace es de muy baja velocidad (comparativamente). El otro problema es que si nosotros no estamos transmitiendo nada no podemos recalcular el RTT. De todos modos la implementación efectuada ha dado unos resultados bastante aceptables y, además, todo esto no influye en la recepción de datos, acción mucho más habitual en nuestro contexto.

No es necesario que los "MSG_MBUF" que se transmitan midan lo mismo que el MSS de la conexión. La capa TCP se encarga de realizar las correcciones necesarias. Lo que sí es imprescindible es que nunca se supere el valor "MEM_BLOQUE" declarado en el módulo de gestión de memoria. En la negociación del MSS se tiene en cuenta el MTU del canal asociado a nuestra dirección IP, con el fin de evitar la fragmentación de los segmentos.

El estado "TIME_WAIT" se emplea para conservar ciertas estructuras internas asociadas a una conexión que ya ha sido cerrada, en previsión de que la red pueda duplicar y/o desordenar datagramas. Según [RFC793] este estado debe mantenerse durante un tiempo mínimo que garantice que cualquier datagrama en tránsito agote su tiempo de vida. En la implementación actual se ha fijado una temporización de dos minutos. No obstante las estructuras asociadas a una conexión son grandes y, por consiguiente, ocupan una considerable cantidad de memoria. Por ello, si se intenta abrir una conexión y no hay suficiente sitio en las tablas internas para ello, se busca la conexión en estado "TIME_WAIT" más antigua y se elimina para acomodar los nuevos datos. Si no hay ninguna conexión en "TIME_WAIT" no se podrá crear el nuevo enlace.

Aún cuando el protocolo permite enlaces unidireccionales, cuando uno de los extremos cierra la conexión pero el otro no, la filosofía general de las aplicaciones que hacen uso de esta capa consiste en cerrar la conexión en cuanto el otro extremo lo hace.

Se han considerado algunas extensiones:

  • En [RFC1072] se definen tres extensiones para redes con un producto Ancho de Banda * Retardo elevado: confirmaciones selectivas, cálculo preciso del tiempo ida y vuelta, y ventanas de recepción de más de 16 bits. Aunque el ancho de banda de los enlaces telefónicos es bastante bajo, el tiempo de retardo suele ser muy elevado (valores de hasta treinta segundos no son raros).

  • En [RFC1146] se definen dos algoritmos adicionales para calcular la suma de control TCP, basados en los trabajos de Fletcher. Se trata de una extensión con fines puramente experimentales.

  • [RFC1323] es una propuesta de estándar basada en [RFC1072], pero que no incluye la opción de confirmaciones selectivas. Se utiliza una variante para la secuencia de numeración, con un tamaño efectivo de 64 bits (basado en firmas temporales), ya que con las redes de alta velocidad 32 bits se recorren en pocos segundos.

En el documento [RFC1263] se hace un análisis detallado del impacto, a nivel de compatibilidad y ampliaciones futuras, de las extensiones propuestas. No se ha incluido ninguna de ellas por tratarse de modificaciones experimentales poco difundidas.


Bibliografía


[HTTP1.1]   "HyperText Transfer Protocol"
            http://www.w3.org

[RFC793]    RFC793: "Transport Control Protocol"
            Jon Postel
            Septiembre 1.981

[RFC813]    RFC813: "WINDOW AND ACKNOWLEDGEMENT STRATEGY IN TCP"
            David D. Clark
            Julio 1.982

[RFC821]    RFC821: "SIMPLE MAIL TRANSFER PROTOCOL"
            Jonathan B. Postel
            Agosto 1.982

[RFC854]    RFC854: "Telnet Protocol specification"
            Jon Postel
            Joyce Reynolds
            Mayo 1.983

[RFC862]    RFC862: "Echo Protocol"
            Jon Postel
            Mayo 1.983

[RFC863]    RFC863: "Discard Protocol"
            Jon Postel
            Mayo 1.983

[RFC879]    RFC879: "The TCP Maximum Segment Size and Related
            Topics"
            Jon Postel
            Noviembre 1.983

[RFC896]    RFC896: "Congestion Control in IP/TCP Internetworks"
            John Nagle
            Enero 1.984

[RFC944]    RFC944: "Official ARPA-Internet protocols"
            Joyce Reynolds
            Jon Postel
            Abril 1.985

[RFC959]    RFC959: "FILE TRANSFER PROTOCOL (FTP)"
            Joyce Reynolds
            Jon Postel
            Octubre 1.985

[RFC1072]   RFC1072: "TCP Extensions for Long-Delay Paths"
            Van Jacobson
            R. Braden
            Octubre 1.988

[RFC1122]   RFC1122: "Requirements for Internet Hosts --
            Communication Layers"
            Robert Braden
            Octubre 1.989

[RFC1146]   RFC1146: "TCP Alternate Checksum Options"
            Johnny Zweig
            Craig Partridge
            Marzo 1.990

[RFC1191]   RFC1191: "Path MTU Discovery"
            Jeffrey Mogul
            Steve Deering
            Noviembre 1.990

[RFC1263]   RFC1263: "TCP EXTENSIONS CONSIDERED HARMFUL"
            Larry L. Peterson
            Sean O'Malley
            Octubre 1.991

[RFC1323]   RFC1323: "TCP Extensions for High Performance"
            Van Jacobson
            Bob Braden
            Dave Borman
            Mayo 1.992

[RFC1435]   RFC1435: "IESG Advice from Experience with Path MTU
            Discovery"
            Stev Knowles
            Marzo 1.993

[RFC1693]   RFC1693: "An Extension to TCP : Partial Order Service"
            Phill Conrad
            Paul D. Amer
            Tom Connolly
            Noviembre 1.994

[RFC1866]   RFC1866: "Hypertext Markup Language - 2.0"
            T. Berners-Lee
            D. Connolly
            Noviembre 1.995



Python Zope ©1996 jcea@jcea.es

Más información sobre los OpenBadges

Donación BitCoin: 19niBN42ac2pqDQFx6GJZxry2JQSFvwAfS