índice. 1. introducción. ………………………………………………………… 2 2. una aproximación peer-to-peer al problema. ………………………. 4 3

ÍNDICE.
1.
Introducción. ………………………………………………………… 2
2.
Una aproximación Peer-to-Peer al problema. ………………………. 4
3.
El protocolo DFP (Distributed FTP Protocol). ……………….…….. 6
4.
Estructura del sistema. …………….………………………………… 11
5.
Detalles de implementación. ….…………….………………………… 15
5.1 La Tabla de Directorios Virtuales (TDV) ………………………… 16
5.2 Tipos de mensaje y su estructura ………………………………… 18
5.3 Instrucciones FTP admitidas …………….…………….………… 22
5.4 Comunicación entre procesos y dinámica ……………….…….… 25
5.5 Códigos de respuesta …………………………………………….. 29
5.6 Diseño detallado ………………………………………………… 31
5.6.1 El módulo de variables globales ……………………….. 32
5.6.2 El gestor de la conexión ………………………………... 34
5.6.3 El gestor de mensajes al usuario ……………………….. 49
5.6.4 El intérprete de comandos ……………………………… 55
5.6.5 El sistema de directorios virtuales ……………………… 69
5.6.6 El gestor de la coherencia de los datos …………………. 125
5.6.7 El programa de configuración ………………………….. 161
5.6.8 Formato y uso de los ficheros de trabajo …………..…… 171
5.6.9 Ejemplos de funcionamiento …………………………… 174
6.
Breve manual del administrador ………..……………………………… 176
7.
Resumen y conclusiones ………………………………………………. 179
8.
Opciones abiertas …………………………………..………………….. 181
9.
Bibliografía y referencias ……………………………………………… 182
Versión 1.1
1. INTRODUCCIÓN.
El protocolo de transferencia de ficheros (o FTP, por sus siglas, en
inglés) quedó especificado en la RFC 9591, en el año 1985. Su objetivo
es la reglamentación de las transmisiones de archivos entre máquinas
remotas, y es, hoy por hoy, una de las aplicaciones más extendidas de
Internet, junto con la WWW y el correo electrónico.
La implementación clásica de un servicio FTP se basa en una
arquitectura cliente / servidor. Es éste el encargado de alojar el
sistema de directorios y archivos que serán accesibles por parte de
aquél.
Con frecuencia, para que un cliente pueda iniciar una sesión FTP con
un servidor, es necesario que se identifique. Por tanto, el usuario
habrá de constar en alguna base de datos, con un nombre y una
contraseña que le garanticen una forma de acceso única y segura. Sin
embargo, no es raro encontrar lo que se conoce como “servidores de FTP
anónimos”, de libre acceso y empleo, en los que el usuario no tiene
que identificarse, y basta con que utilice una dirección de correo
electrónico, para autenticarse. Eso sí, el método se fundamenta en la
buena fe de quien accede al servidor, ya que no existen, de momento,
métodos sencillos y eficientes de comprobar la validez de una
dirección de e-mail.
Normalmente, la diferencia fundamental, de cara al usuario, entre un
sistema de FTP anónimo, y otro que requiere autenticación, estriba en
el hecho de que, los primeros, son de sólo lectura, y en los segundos,
el servidor se comporta como, digámoslo así, una extensión del disco
duro del usuario (aunque compartida por otros, en igualdad de
condiciones), ya que, si dispone de todos los permisos necesarios,
será capaz no sólo de descargar archivos desde la máquina remota a la
local, sino de realizar la operación inversa, e incluso crear
directorios, o renombrar archivos. Un servicio de FTP anónimo suele
emplearse como un simple almacén de archivos. Los clientes no podrán
modificar su estructura de directorios (esta tarea quedará, por tanto,
a cargo del administrador del servidor), y deberán limitarse a
descargar ficheros. Desde un punto de vista más cercano a la
especificación, una sesión FTP consta de cinco elementos:
*
Interfaz de usuario: existen numerosos clientes de FTP, muchos de
ellos gratuitos y que, con una frecuencia cada vez mayor, emplean
entornos gráficos (GUI). De cualquier modo, la interfaz de usuario
en una sesión FTP es la parte encargada de interpretar las
instrucciones que éste suministra, y serán enviadas al servidor, y
de implementar la parte del protocolo correspondiente al cliente.
*
Intérprete del protocolo del cliente: incluido dentro de la
interfaz de usuario, este bloque tiene como objetivo enviar
instrucciones al intérprete del protocolo del servidor, y
gestionar la parte de las transferencias concerniente a la máquina
local.
*
Intérprete del protocolo del servidor: responde a las
instrucciones enviadas por el intérprete del protocolo del
cliente, y gestiona la parte de las transferencias concerniente al
servidor.
*
Proceso de transferencia de datos del cliente: su objetivo es la
comunicación con el proceso análogo del servidor, y la gestión del
sistema de ficheros de la máquina local.
*
Proceso de transferencia de datos del servidor: realiza la función
equivalente al proceso de transferencia de datos del cliente, y
además, se encarga de la gestión del sistema de ficheros de la
máquina remota.
Este proyecto se centra exclusivamente en la parte remota, es decir,
el servidor. De hecho, uno de sus objetivos es la transparencia total
de cara al cliente. A ojos de éste, el acceso se realizará a un
sistema FTP ordinario. De este modo, se obvia cualquier interfaz. La
única manera de interactuar con el sistema, para un usuario, es a
través de una interfaz estándar de cliente FTP.
---------------------------------------------------------------------
La idea del protocolo de FTP distribuido (o DFP) consiste en repartir
un sistema de transferencia de ficheros entre varios servidores o
hosts, coordinados de manera que, de cara a un cliente cualquiera,
aparezcan como si se tratara de uno solo. Al adoptar este enfoque,
inmediatamente, se adquieren varias ventajas:
*
El cliente tendrá más ficheros a su disposición, ya que se
combinará la capacidad de almacenamiento de cada host, para formar
una especie de “supernodo”.
El sistema se plantea de un modo escalable y flexible, esto es,
pueden añadirse nuevos nodos en cualquier momento, y con cada uno
de ellos, se estarán también añadiendo más capacidad de
almacenamiento y más archivos.
*
Se reducen las posibilidades de que un servidor saturado, o con
cualquier tipo de problemas técnicos, deje su sistema de
directorios fuera del alcance de nuevos clientes, o sufra una
drástica disminución de su ancho de banda. De esta manera, si un
cliente trata de descargar un archivo de un nodo problemático, el
protocolo redirigirá la petición hacia otro menos saturado, o
plenamente funcional, y que también contenga el mismo fichero.
*
Mejora la eficiencia: una petición de un archivo almacenado en
varios hosts separados, se gestionará de manera que éste sea
finalmente enviado al cliente, desde la máquina con mejor tiempo
de respuesta.
El principal problema que plantea esta estrategia, es el de garantizar
la coherencia de los datos entre los hosts, esto es, el protocolo debe
asegurar que todos los servidores que formen parte del sistema de FTP
distribuido, compartan las mismas tablas de directorios. Si uno de
ellos experimentara cualquier clase de problemas, o fuera necesario
desconectarlo temporalmente (por ejemplo, para someterlo a
mantenimiento), se correría el riesgo de que su tabla de directorios
dejara de estar actualizada. Las demás máquinas del sistema
continuarían cambiando y alterándose, sin que la que está offline
tuviera constancia de tales modificaciones. Es fácil deducir, pues,
que la gestión de la coherencia de los datos (una expresión, esta, que
se usará con bastante asiduidad a partir de este punto) es la piedra
angular del protocolo DFP.
Para el desarrollo de este proyecto, se ha optado por una arquitectura
“peer-to-peer” (de igual a igual), un campo de investigación muy
activo actualmente. La descentralización en el control de una red de
nodos conectados entre sí es una solución que se está demostrando muy
eficiente y llena de posibilidades. En un sistema “peer-to-peer” cada
máquina integrante del conjunto es un “individuo” por sí misma. La
unión de varios hosts no diluye o reprime las características y las
ventajas de uno solo, considerado de forma aislada, sino que las
potencia.
2. UNA APROXIMACIÓN “PEER-TO-PEER” AL PROBLEMA.
Una arquitectura “peer-to-peer”2 (que podría traducirse como “de igual
a igual”) es completamente descentralizada. No existe ningún tipo de
nodo central coordinador, sino que cada host que integra el sistema
está dotado de exactamente la misma funcionalidad que el resto. De
este modo, se garantiza la escalabilidad. Añadir una nueva máquina a
la red de FTP distribuido es un proceso prácticamente automático. De
hecho, el protocolo está diseñado para que la inserción de ese recién
llegado sea casi transparente al usuario (en este caso, el
administrador del servidor FTP), que sólo tendrá que configurar un
reducido número de parámetros (y esto, únicamente en algunos casos).
La red está, por lo tanto, formada por máquinas que se comportan y se
comunican entre ellas “de igual a igual”. Todas alojan una tabla de
directorios virtual (TDV, para abreviar), es decir, almacenada en
memoria3, con los contenidos de los de las máquinas remotas, de manera
que un cliente pueda navegar libremente por tales estructuras,
conectándose a un solo nodo.
La idea es que los hosts se comuniquen entre sí para mantener en todo
momento actualizadas sus TDV. Si se realiza alguna operación de
mantenimiento sobre alguna de las máquinas, de modo que cambie su
estructura de directorios local, o simplemente, se añadan, eliminen o
renombren archivos, cuando vuelva a unirse a la red compartirá estos
cambios con todos los demás nodos. Así, el cliente tendrá siempre la
“ilusión” de que ve una sola gran tabla de directorios.
La siguiente figura ilustra este concepto:

Figura 1 – Enfoque “peer-to-peer” para el sistema de FTP distribuido
En todo momento, se pretende mantener la facilidad de ampliación del
sistema, y la igualdad entre los nodos que lo integran, lo que hace
más sencillo el mantenimiento, algo que se ve reforzado por el hecho
de que la red se apoya sobre un protocolo bien conocido, como es el
FTP.
En una aproximación “peer-to-peer”, cada nodo es, en todos los
niveles, equivalente a cualquier otro. Esto implica que todas las
máquinas del sistema pueden asumir uno de los dos papeles que define
el protocolo DFP: propagador y receptor. En el primer caso, el host es
el desencadenante de una actualización de las TDV de todos los demás.
Esto es, cuando se realiza algún tipo de operación de mantenimiento
sobre él, es necesario que la comunique al resto de los servidores que
integran el sistema. Por tanto, debe enviar un mensaje que logre que
cada host modifique pertinentemente su tabla (ver la sección 3 - El
protocolo DFP –página 6-, o la sección 5 - Detalles de la
implementación –página 15-).
En el segundo caso, es decir, cuando un servidor asume el papel de
receptor de un mensaje, su cometido es el de responder al mismo
convenientemente.
En cualquier caso, conviene insistir en la naturaleza descentralizada
de la aplicación: no hay máquinas ligadas a un rol concreto y fijo.
Todas pueden actuar como propagadoras o receptoras, en cualquier
momento. Todas son absolutamente equivales, como copias exactas. Su
papel, simplemente depende de las circunstancias.
3. EL PROTOCOLO DFP (Distributed FTP Protocol).
La tarea más importante del protocolo DFP es la de garantizar la
coherencia de los datos de los hosts que integran el sistema, esto es,
que todos compartan tablas de directorios virtuales equivalentes. Si
uno de los nodos almacenara datos más recientes, debería compartirlos
con el resto.
Llegados a este punto, es necesario definir el concepto de “Estado
Coherente”. Un servidor está en él cuando comparte los datos de su TDV
con todos los demás.
Si alguna de las máquinas tiene datos obsoletos, se considerará en
“Estado No Coherente”. Esta situación debe solventarse lo antes
posible (nótese que, sin embargo, si un host tiene datos más recientes
que los de las demás máquinas de la red, no se le considera en Estado
No Coherente. Por decirlo así, en ese caso, serían todas las demás las
que se encontrarían en tal situación. De este modo, en puridad, puede
decirse que el Estado Coherente de un nodo concreto sucede cuando
mantiene los datos más recientes, y que el Estado Coherente de todo el
sistema, ocurre cuando todas las máquinas que lo componen comparten
los mismos datos recientes).
Los nodos emplean mensajes para su comunicación. Éstos se dividen en
dos categorías:
*
Modificación de la TDV.
*
Aviso.
Los primeros contienen los datos de la tabla que ha sido alterada, y
que deben compartirse con todos los servidores de la red. Los segundos
se utilizan para informar a uno o más servidores de situaciones
relevantes para el funcionamiento del sistema, a saber:
*
Nodo conectado: lo envía un host cuando se incorpora al sistema
(ya sea por primera vez, o tras un periodo de desconexión).
Mensaje recibido: confirmación de la recepción de un mensaje de
tipo “nodo conectado”.
*
Borrado de un nodo: lo transmite un servidor cuando se va a dar de
baja definitivamente.
*
Petición de la TDV: solicitud, que un nodo concreto envía a otro
para que le transmita su TDV.
Por “modificación de la TDV” se entiende cualquier alteración que ésta
pueda sufrir. En estos mensajes se engloban, por tanto, las altas,
modificación y borrado de filas. Además, no se transmiten sólo
aquellas regiones de la tabla que hayan sido modificadas, sino el
bloque completo, correspondiente al nodo en el que se ha efectuado la
actualización, con la intención de simplificar la codificación4.
Recordemos que el sistema se implementa utilizando un enfoque “peer-to-peer”
y que lo único que dicta la manera en la que cada host debe
comportarse, es el contexto. Todos pueden ser emisores (propagadores)
o receptores, en función de las circunstancias.
Llamemos ahora Np al nodo propagador, y Nr al nodo receptor. El
proceso de alta de un nuevo nodo, desde el punto de vista de un
servidor que está actuando como Np (es decir, su TDV ha sido
actualizada, y debe propagar el cambio a las demás máquinas del
sistema) es el siguiente:
Se lanza la aplicación que pone en marcha al Np, y éste envía un
mensaje de tipo “nodo conectado”.
Su segundo paso consiste en transmitir la tabla de directorios
virtuales local, por medio de multicast, a todos los integrantes de la
red. Podría parecer redundante, ciertamente, pero en cualquier caso
hay que tener en cuenta que, aunque un host no vea modificaciones en
su tabla de directorios, es perfectamente posible que durante el
tiempo que permaneció desconectado, la distribución de la red de DFP
haya cambiado significativamente, con el añadido de varios hosts
nuevos y la eliminación de otros antiguos. Si nuestro nodo se limitara
a enviar su TDV sólo cuando tuviera constancia de que ha sufrido
alguna modificación, llevaría a un estado no coherente a cualquier
máquina que se hubiera incorporado a la red mientras estuvo
desconectado.
A continuación, el nodo se pone a la escucha de las confirmaciones con
las que las demás máquinas de la red responden al mensaje “nodo
conectado”.
He de insistir en ello: incluso aunque se tratara de un antiguo
integrante de la red DFP, podría darse el caso de que, durante el
tiempo que permaneció desconectado, se añadieran y/o eliminaran tantos
otros nodos, que la nueva configuración estuviera formada por muchas
máquinas que antes desconocía totalmente. El recién llegado aprovecha
estas confirmaciones para anotar en su tabla interna de nodos las
direcciones de todas las máquinas remitentes. Además, contesta a cada
petición, a su vez, solicitando el envío de la tabla de directorios
virtuales de cada nodo de cuya existencia acaba de tener constancia.
Desde la óptica de un nodo que está actuando como receptor, el alta de
un nuevo nodo se lleva a cabo en tres pasos:
Primero, llega un aviso de “nodo conectado”. Es necesario generar una
confirmación de la recepción del mismo.
En segundo lugar, se recibe una TDV. Puede pertenecer a una máquina
desconocida hasta entonces, en cuyo caso se añade a la tabla de nodos,
o bien puede que se trate de un antiguo miembro del sistema DFP que
vuelve a incorporarse al mismo tras una desconexión (por motivos que
no tienen relevancia en este punto), situación esta que no requerirá
acción de ningún tipo por parte del receptor. La recepción de la tabla
remota no suscita, por tanto, respuesta alguna. El nodo se limita a
procesarla y a continuar a la escucha de nuevos mensajes.
Al término de este proceso, el recién llegado contestará, pidiendo al
remitente de dicha confirmación, el envío de su tabla local.
De este modo, se establece algo parecido a un breve diálogo, que puede
ilustrarse con el cronograma de la figura siguiente:

Figura 2.1 – Cronograma del proceso de alta de un nodo
Como ya se ha mencionado, uno de los puntos más importantes del
protocolo DFP es garantizar la coherencia de los nodos que integran el
sistema distribuido. El mecanismo a seguir, desde el punto de vista
del Np, es esencialmente “optimista”. Digamos que la filosofía del
protocolo a este respecto podría resumirse con una frase: “todo nodo
se encuentra en un estado coherente, mientras no se demuestre lo
contrario”. Y aún cuando se demuestra lo contrario, sucede sólo a
nivel local. Pero entremos en detalles:
El hecho de que un nodo no se encuentre en estado coherente con
respecto a otro, es una situación local. Esto es: un host puede ser no
coherente para otro, pero coherente para todos los demás. Así, se
solventan los problemas que podría causar el hecho de que se rompiera
el enlace físico entre dos máquinas plenamente operativas. Entre ellas
no serían coherentes, esto es, no podrían verse mutuamente, pero de
cara al resto de la red, ambas estarían plenamente funcionales. La
situación se representa en la siguiente figura.

Figura 2.2 – Enlace roto entre dos máquinas operativas
Cuando deba enviarse un mensaje de actualización recurriendo a
multicast, no se tienen en cuenta los estados de coherencia de los
nodos receptores. El mensaje se transmitirá a todos los integrantes
del sistema. Si hay alguna máquina desconectada, simplemente no
recibirá la actualización.
A continuación, se incluyen una serie de diagramas de flujo que
pretenden ilustrar el proceso de incorporación de un nodo. El primero
de ellos, en la figura 3.1, refleja el mecanismo que sigue cualquier
host, en cuanto se pone en funcionamiento.

Figura 3.1 – Diagrama de flujo del proceso de alta de un nodo que se
ha caído
Cuando el propagador se pone a la escucha de mensajes, comenzará a
recibir las confirmaciones a la transmisión de su “nodo conectado”.
Responderá a cada una de ellas con una solicitud de envío de la TDV
del remitente.
Este comportamiento se ilustra en la figura siguiente.

Figura 3.2 – Diagrama de flujo del proceso de alta de un nodo que se
desconectó por mantenimiento,
desde el punto de vista del nodo emisor.
Mientras tanto, en el receptor, tiene lugar un proceso diferente. Si
recibe un mensaje de tipo “nodo conectado”, responderá transmitiendo
una confirmación de la recepción del mismo. Si, por el contrario, el
mensaje recibido es, precisamente, una petición de la transmisión de
la TDV, se contesta a la misma en consecuencia. Nótese que la
recepción de la tabla del recién llegado no provoca ninguna respuesta.
La idea se ilustra mediante el diagrama de flujo que puede verse a
continuación:

Figura 3.3 – Diagrama de flujo de la llegada de mensajes, desde el
punto de vista del receptor
El proceso para dar de baja a un nodo es trivial: el host que va a
eliminarse envía un mensaje por multicast a todos los demás. Éstos
comprueban la corrección del mismo y, si todo está en orden, lo
eliminan de sus tablas.
El sistema tiende a regularse por sí solo, sin la ayuda de ningún nodo
central, ni ninguna estructura comparable, de modo que no es necesario
añadir reglas superfluas.
4. ESTRUCTURA DEL SISTEMA
Un primer análisis de los requisitos de la aplicación, reveló un
problema: las propias limitaciones de las herramientas de programación
que se emplearían impedían que toda la funcionalidad del protocolo
estuviera confinada a una sola tarea. La razón es sencilla: cuando se
implementa un servidor FTP, la forma más usual y razonable de llevar a
cabo esta tarea, es mediante el empleo de sockets5. El servidor abre
uno, y permanece a la escucha, aguardando las peticiones de conexión
por parte de los clientes. Por lo tanto, esta parte de la aplicación,
quedará suspendida mientras no se produzcan intentos de acceso al
servidor, por parte de ningún cliente.
Por otro lado, la filosofía esencial de la comunicación entre nodos,
es muy similar. Aunque se emplea un enfoque multicast, también son
necesarios los sockets, para la transmisión de las tramas UDP6 entre
los hosts del sistema, y así, cuando uno de los nodos abra un socket,
quedará a la escucha y no podrá llevar a cabo ninguna otra operación.
En conclusión: si todo el código de la aplicación se concentrara en
una sola tarea (independientemente de que se distribuya entre varios
módulos –como, de hecho, ocurre-, con objeto de mejorar la legibilidad
y facilitar el mantenimiento), cuando un nodo abriera el socket
correspondiente a la comunicación FTP con los clientes, no podría
atender a las peticiones de otros nodos DFP. Y viceversa.
La solución consiste, por tanto, en dividir la aplicación en dos
tareas que se ejecutan por separado, pero de un modo no totalmente
independiente (ver la sección 5 - Detalles de la implementación,
página 15). Una de esas tareas consiste en un servidor FTP básico,
plenamente funcional, y la otra se encarga de la comunicación entre
nodos y la gestión de la coherencia de los mismos. Esto es, la
implementación del protocolo DFP. El gestor de la coherencia se
compone de un solo módulo. El servidor FTP, sin embargo, consiste en
cuatro, tal y como se muestra en la figura 5.

Figura 5 – Estructura de módulos del servidor FTP
La idea es, evidentemente, que los módulos sean independientes en la
medida de lo posible, para facilitar la legibilidad del código, y el
mantenimiento del mismo.
Nótese que todos están conectados con el gestor de mensajes al
usuario. Éste se encarga de la selección del tipo de respuesta que
debe enviarse al cliente. La transmisión en sí, corre a cargo del
gestor de la conexión.
NOTA: Hay un quinto módulo, llamado “variables_globales.pm”, que
alberga los valores de ciertos parámetros esenciales configurados por
el administrador; no se ha incluido en la ilustración anterior, porque
no puede considerarse como un bloque más, sino más bien como un
archivo sencillo de variables globales, cuyo empleo tiene como
finalidad aumentar la legibilidad del código fuente.
Respecto a las características, funcionamiento de cada módulo, y
relaciones entre ellos:
Gestor de mensajes al usuario:
Entradas:
Todos los módulos invocan al gestor de mensajes al usuario, en un
momento u otro, para que éste construya una cadena que se enviará al
cliente, y que debe respetar el formato de los mensajes de texto entre
el cliente y el servidor, en una sesión FTP.
El módulo gestor de la conexión pide la generación de mensajes
relacionados con la sesión FTP y la comunicación con el cliente, como
la bienvenida que éste imprime en pantalla cuando accede al servicio.
El intérprete de comandos utiliza llamadas al gestor de mensajes al
usuario para responder a las instrucciones recibidas desde el cliente.
Y el sistema de directorios virtuales emplea el módulo para construir
mensajes de error relacionados con la navegación de la TDV (intento de
descarga de un archivo inexistente, o de cambiar a un directorio
incorrecto, por citar dos ejemplos).
Salidas:
El módulo gestor de mensajes determina cuál de ellos debe enviarse,
pero la transmisión en sí, es asunto del Gestor de la conexión.
Intérprete de comandos:
Entradas:
Tanto las instrucciones introducidas por el usuario, como las propias
de la parte del cliente, en la sesión FTP, son recibidas a través del
gestor de la conexión. Éste, las envía inmediatamente al intérprete de
comandos, para su procesado.
Si la instrucción es una de las admitidas por el sistema (ver la
sección 5.3 - Lista de instrucciones FTP admitidas, página 22), se
invoca a la función pertinente. En caso contrario, se muestra un
mensaje de error (ya sea de tipo “502 Command not implemented”, si se
trata de una instrucción correcta según la definición del protocolo
FTP, pero que no ha sido implementada, o “501 Syntax error” si la
instrucción es incorrecta).
Salidas:
Las instrucciones recibidas del cliente pueden ejercer su efecto, una
vez interpretadas, sobre el gestor de la conexión (p. ej: si es
necesario abrir un socket para una descarga, o finalizar la sesión FTP
con el cliente), sobre el gestor de mensajes al usuario (p. ej: si se
recibe una instrucción sintácticamente incorrecta), y sobre el sistema
de directorios virtuales (para navegar a través de la TDV).
Gestor de la conexión:
Entradas:
Este módulo es el encargado de abrir los sockets de datos con los
clientes (y, en el caso concreto de las descargas remotas, con otro de
los servidores de la red DFP). Las instrucciones que éstos transmitan
serán enviadas al intérprete de comandos.
Además, es el único punto de contacto entre las dos partes de la
aplicación: el servidor FTP y el gestor de la coherencia.
Salidas:
Las instrucciones recibidas se remiten al intérprete de comandos.
Cualquier mensaje que sea necesario transmitir al cliente, se genera a
través del gestor de mensajes al usuario.
Sistema de directorios virtuales:
Entradas:
El módulo se encarga de implementar una “versión virtual”, en memoria,
del sistema de directorios en disco (que, recordemos, es compartido
por todos los nodos). Esto implica que debe desarrollarse otra
“versión virtual” de algunos de los mandatos FTP más comunes, para
navegar a través de él.
Se sigue una filosofía parecida a la que propone el patrón de diseño7
Proxy Remoto: esto es, se “hace creer” al cliente que todos los
ficheros son locales. Cuando éste pide uno que se encuentre en otra
máquina, se establece la conexión con el nodo que presente mejor
tiempo de respuesta. Si ese falla, se intenta con el segundo, etc. Si
todos los nodos fallaran, se mandaría un mensaje de error a través del
gestor de mensajes al usuario, y del gestor de la conexión.
Salidas:
El sistema de directorios virtuales podría considerarse casi como un
“sumidero” dentro del sistema. Es el núcleo dentro del servidor FTP, y
prácticamente todos los demás módulos, de una forma u otra, trabajan
para él. Su única salida es a través de los mensajes que se generan en
el gestor de mensajes al usuario.
5. DETALLES DE IMPLEMENTACIÓN.
La aplicación se ha desarrollado en Perl, utilizando la versión 5.6.0.
Entre las razones de la elección de este lenguaje se cuentan su
capacidad expresiva (especialmente en lo referente a la búsqueda de
patrones mediante las expresiones regulares), su robustez y la
facilidad para encontrar multitud de módulos y librerías gratuitos en
Internet (el archivo en http://www.cpan.org resultó particularmente
útil). De hecho, se han utilizado tres de estas librerías de dominio
público: IO::Socket::Multicast, para las transmisiones multicast8
entre los nodos que conforman el sistema, XML::DOM, para garantizar la
corrección (o “well-formedness”) de los mensajes en XML9 que emplean
los hosts en su comunicación, y Digest::MD5 para la autenticación de
estos mensajes, empleando el algoritmo MD510 (ver la sección 5.2 -
Tipos de mensaje y su estructura; página 18).
La elección de XML para los mensajes entre los nodos se debe
principalmente a su sencilla sintaxis, su flexibilidad, y la
existencia de numerosos parsers11 gratuitos (como el propio módulo
XML::DOM, precisamente) que facilitan la validación del código.
Como se ha citado anteriormente, el sistema consta de dos procesos que
funcionan de un modo prácticamente independiente. Aunque, como es
obvio, en ciertos momentos, tendrán que comunicarse entre ellos. Para
cumplir este objetivo, se emplean un sistema de archivos, y una serie
de interrupciones software (ver la sección 5.4 - Comunicación entre
procesos y dinámica; página 25).
5.1 LA TABLA DE DIRECTORIOS VIRTUALES (TDV).
Si hubiera que resumir el concepto detrás del protocolo DFP en dos
expresiones, éstas serían “coherencia” y “Tabla de Directorios
Virtuales”.
Mejor aún; en una sola: “coherencia de las Tablas de Directorios
Virtuales”.
Como se detalla en la sección 2 - Una aproximación Peer-to-Peer al
problema; página 4, el protocolo pretende que la totalidad de los
hosts que integran el sistema, aparezcan a ojos del cliente como si
conformaran una única máquina; un “supernodo” que reúne los contenidos
de todas las demás, y permite una navegación sencilla y transparente
al usuario, empleando los mismos mandatos FTP que se utilizarían en
una sesión ordinaria cliente / servidor. Por lo tanto, cada nodo del
sistema tiene que “conocer” el contenido de la estructura de
directorios de todos los demás.
Cada TDV consta de dos bloques: en uno, se almacena la estructura de
directorios de la máquina local, y en el otro, las de los nodos
remotos. Cada uno de éstos se hace colgar de un directorio llamado
“DIRn”, donde n es un número de orden (que comienza en 1). Esto motiva
que una de las reglas que deben observar los administradores del
sistema, consiste en que ninguno de los nombres de directorios de la
estructura que la aplicación emplea para generar la TDV, debe contener
esa cadena.
En resumen: cada TDV contiene un bloque local, y otro en el que se
almacenan los bloques remotos de los demás nodos del sistema. De este
modo, el cliente puede navegar a través de las estructuras de
directorios de todas las máquinas que integran la red de FTP
distribuido, conectándose sólo a una de ellas.
Dado que los accesos al sistema son anónimos, sólo se permite que el
usuario realice operaciones de lectura (que no modifiquen la
estructura de la TDV). Por tanto, no se contemplan los renombrados de
directorios o archivos, el borrado de éstos, el almacenamiento (upload),
y procesos similares. Esto simplifica sobremanera el protocolo (de
otra forma, sería necesario que, cada vez que se aplicara el menor
cambio sobre una TDV dada –por ejemplo, cuando el cliente creara un
nuevo directorio-, la actualización se transmitiera a todos los demás
nodos del sistema, lo que haría que el tráfico en la red aumentara
drásticamente).
No obstante, el planteamiento elegido presenta aún una complicación de
cierta seriedad: las descargas. Cuando un cliente solicita un archivo
que existe en el host al que está conectado, éste se lo suministra sin
más, siguiendo los mismos pasos que daría cualquier servidor FTP
ordinario. Pero, ¿qué sucede si el usuario quiere descargar un fichero
que se encuentra en un nodo remoto? A sus ojos, dado que la TDV
incluye todas las estructuras de directorios de la red, el fichero se
encontraría en la misma máquina a la que está conectado. Sin embargo,
no es así. La solución a este problema se detalla en la sección 5.4 -
Comunicación entre procesos y dinámica, página 25.
Dado que uno de los objetivos del protocolo DFP es la transparencia al
usuario, las TDV se organizan de modo que almacenan, para cada fichero
o directorio, todos los datos que mostraría en pantalla un servidor
FTP corriente cuando el cliente se lo solicitara. Cada entrada (fila)
de la tabla contiene un directorio o un fichero, y cada fila consta de
8 columnas, a saber:
- Nombre: el nombre del directorio o fichero, incluyendo el path
completo. El administrador debe configurar lo que se conoce como el “path
base”, esto es, el camino en el disco duro local, a partir del cual se
generará la TDV. En otras palabras: la sección del disco que contendrá
la estructura de directorios accesible a los clientes del servicio de
FTP.
Así, por ejemplo, si el administrador decide que el path base sea:
/home/usuario/proyecto/
… y el camino completo hacia un archivo determinado en el disco duro
local es:
/home/usuario/proyecto/codigo/fichero.txt
… la entrada que le corresponderá, en la TDV, será:
/codigo/fichero.txt
En cierto modo, se podría considerar que el path base es la raíz de la
TDV.
- Tipo: toma sólo dos valores. 1, significa que la entrada es un
fichero. 0, que es un directorio.
- Tamaño: medido en bytes.
- Fecha: de la última modificación del fichero o directorio.
- Propietario: almacena la identificación del propietario del archivo.
Si se trata de la máquina local, el campo contendrá la cadena “localhost”.
Si se trata de un fichero que se encuentra, físicamente, en un nodo
remoto, en el campo se encontrará la dirección IP del mismo.
Para ahorrar espacio, sólo la primera fila de cada bloque contiene la
identificación del propietario. Todos los demás, almacenan un 0. Algo
como esto:
Nodo
Propietario
Fila 1 - Máquina local
“localhost”
Fila 2 – Máquina local
0
Fila 3 – Máquina local
0


Fila n – Máquina local
0
Fila 1 – Tabla del nodo A
IP del nodo A
Fila 2 – Tabla del nodo A
0
Fila 3 – Tabla del nodo A
0


Fila n – Tabla del nodo A
0
Fila 1 – Tabla del nodo B
IP del nodo B


- Permisos: almacena los permisos del fichero o directorio.
- Enlaces: número de enlaces simbólicos (referencias, desde otros
puntos del disco) al fichero12.
- Usuario: identificación numérica del usuario propietario del
archivo.
- Grupo: identificación numérica del grupo de usuarios del archivo.
5.2. TIPOS DE MENSAJE Y SU ESTRUCTURA.
La comunicación entre los nodos que integran el sistema se lleva a
cabo por medio de mensajes en XML.
Hay dos tipos:
*
Actualizaciones de la TDV de un nodo.
*
Avisos.
Los primeros contienen todo el bloque de la tabla que ha sido
modificada, fila a fila. Los segundos, se utilizan como mecanismo de
control o coordinación de los nodos.
Cualquier código en XML posee una cabecera que incluye lo que se
conoce como DTD13, abreviatura de Document Type Definition (o
“definición del tipo de documento”). Con frecuencia, esta definición
no se explicita, sino que se refiere a ella por medio de un enlace.
Los fuentes XML se pueden asimilar a una estructura en árbol. Los
elementos (que son los pares formados por una etiqueta de apertura y
otra de cierre, esto es, algo como ) se
asocian a nodos no terminales, y las etiquetas aisladas, a hojas, como
se ilustra en la siguiente figura:

Figura 6 – Organización en árbol de un fuente XML
La disposición de las etiquetas, desplazando ligeramente hacia la
derecha las que están contenidas (anidadas) dentro de un elemento
superior, es meramente estética, aunque resulta obvio que ayuda mucho
a la legibilidad del código fuente.
Entendiendo esta correspondencia entre el código y su árbol asociado,
es sencillo construir una DTD “personalizada”. Las claves del lenguaje
XML, su portabilidad y su flexibilidad, radican precisamente en esa
DTD. No hay etiquetas predefinidas por ningún convenio o
arbitrariedad: es el propio programador quien decide cómo se llamarán
los elementos que compondrán su fuente, y a qué estructura se
atendrán. Lo único importante es que el cuerpo del archivo XML respete
la estructura que propone la DTD.
Éste puede estar contenido en un archivo almacenado en la propia
máquina, o en un host remoto, con lo que sería necesario incluir la
referencia pertinente, en la cabecera del fichero XML, o bien, podría
incluirse al comienzo del mismo.
En este proyecto, se ha optado por la segunda alternativa.
La sintaxis de las DTD es casi trivial. Todas las definiciones de
elementos están contenidas entre etiquetas “!DOCTYPE”.
Cada elemento define, a su vez, a todos los que serían sus
descendientes en el árbol asociado. Las hojas, por último, se
describen con la palabra reservada #PCDATA.
Por ejemplo, en el caso del código XML que se muestra en la figura 6,
la DTD sería el siguiente:



]>
Las estructuras de los dos tipos de mensaje que considera la
aplicación, son:
Para los mensajes de aviso:





]>

MD5(cuerpo+clave)

Identificación del mensaje
Tipo de mensaje


La etiqueta “id” contiene la identificación del nodo que remite el
mensaje en cuestión. Se obtiene aplicando el algoritmo de
autenticación MD5 sobre el cuerpo del mensaje y una clave secreta,
común a todos los nodos que integran el sistema.
La identificación del mensaje es un número, y el tipo de mensaje, una
cadena descriptiva. De este modo, para las tres clases posibles de
mensajes de aviso, tendremos:
*
Mensaje recibido:
Se envía (por unicast14) como confirmación de la recepción de un
mensaje de tipo “nodo conectado”. La utilidad principal se
encuentra en el caso de la incorporación de un nuevo nodo. Éste no
tiene constancia del número de hosts que integran la red de FTP
distribuido, ni de sus direcciones IP15.
Al enviar un mensaje de tipo “nodo conectado” por multicast, todos
los demás responderán con un “mensaje recibido”. El recién llegado
identificará así a las máquinas que lo transmitan, y las añadirá a
su tabla interna de nodos.
ResponseId = 1.
ResponseType = Message received.
*
Nodo conectado:
Lo envía una máquina por multicast cuando se conecta al sistema,
ya sea por primera vez (digamos que “ingresa en el club”), o
después de haber sido desconectada, bien por cuestiones de
mantenimiento, o bien por algún problema técnico.
ResponseId = 2.
ResponseType = Node online.
*
Borrado de un nodo:
Lo envía un nodo cuando va a darse de baja del sistema. Todos los
demás hosts, cuando lo reciban (y tras comprobar su autenticidad),
lo eliminarán de sus tablas internas. Esta es una baja definitiva,
no temporal, como puede ser la causada por una desconexión.
ResponseId = 3.
ResponseType = Delete node.
*
Petición de envío de una TDV:
Cuando una máquina se incorpora al sistema, envía un mensaje de
este tipo a las demás, una por una; es decir, no en multicast,
simultáneamente y a todas a la vez; de lo contrario, se produciría
una auténtica avalancha de datos que el nodo receptor a duras
penas podría procesar. Téngase en cuenta que no es infrecuente que
las tablas de directorios midan varios megabytes, y que existan un
número considerable de ellas.
ResponseId = 5
ResponseType = Get VDT.
Para la actualización de la TDV:
La principal diferencia entre este tipo de mensaje y los avisos, es
que las actualizaciones tienen un tamaño indefinido, y se generan a
partir del contenido de la TDV local. La estructura, a efectos
prácticos, es análoga a la del resto de los mensajes que utiliza el
protocolo, solo que incluye todos los campos necesarios para la
actualización de la TDV en los nodos receptores:



user, group)>









]>

MD5 (Cuerpo + clave secreta)
4
VDT Update


Nombre 1
Tipo 1
Tamaño 1
Fecha 1
Propietario 1
Permisos 1
Número enlaces 1
Identificación usuario 1
Identificación grupo 1


Nombre 2
Tipo 2

Identificación grupo 2



Nombre n
Tipo n

Identificación grupo n



Cada fila de la TDV se almacena en un elemento del mensaje XML.
5.3 INSTRUCCIONES FTP ADMITIDAS.
El objetivo principal de este proyecto es el desarrollo de un
protocolo. Lo realmente importante es, pues, la definición clara y
concisa de una serie de conceptos relativos a la comunicación entre
los nodos que integran una red determinada. Por ello, la parte de la
aplicación que hace las veces de servidor FTP, se debe considerar más
como un medio que como un fin. Y esto explica el hecho de que la
funcionalidad de ese servidor se reduzca a la esencial y estrictamente
necesaria para que se puedan llevar a cabo sesiones ordinarias de FTP
con tantos clientes como sea preciso.
Algunos de los mandatos estándar del protocolo FTP han sido
eliminados, del mismo modo que muchos, no recogidos en las RFC
pertinentes y que se emplean con asiduidad, hasta casi haberse
convertido en normas de facto. La relación de instrucciones FTP que
admite el servidor desarrollado para el proyecto, es la siguiente:
INSTRUCCIÓN
SINTAXIS
DESCRIPCIÓN
ls
ls
Muestra el contenido del directorio remoto.
El servidor también acepta “list” y “nlst” indistintamente.
cwd
cwd
Cambia de directorio (change working directory). El servidor también
acepta “cd”.
cdup
cdup
Cambia al directorio padre en la jerarquía. Si se ejecuta la
instrucción desde la raíz de la estructura, se muestra un mensaje de
aviso.
También se puede utilizar “cd ..”
get
get |
get
Descarga un archivo ubicado en el servidor. Se puede especificar un
nombre diferente para el fichero local en el que se almacenará la
descarga, escribiéndolo a continuación del remoto. Algunos sistemas
utilizan “retr”.
help
help |
help
Muestra la ayuda general, o si se acompaña de uno de los mandatos
admitidos, imprime en pantalla información sobre el mismo.
pass
pass
Se utiliza, tras enviar la instrucción USER, para mandar la contraseña
de acceso, y completar así el proceso de login. En nuestro caso, se
tratará siempre de una dirección de email sintácticamente correcta.
pasv
pasv
Hace que el servidor entre en modo pasivo (muy extendido en la
actualidad). Consiste en que es el servidor quien espera a que el
cliente intente establecer la conexión, en lugar de tratar él mismo de
acceder a uno de los puertos de éste.
El servidor responde enviando la dirección del puerto del cliente al
que está escuchando, con el siguiente formato:
(a1,a2,a3,a4,p1,p2), donde a1.a2.a3.a4 es la dirección IPv4, y
p1*256+p2 es el número del puerto.
port
port a1,a2,a3,a4,p1,p2
Especifica la dirección IP y el puerto del cliente a los que debe
conectarse el servidor para la próxima transferencia.
La IP es explícita: a1.a2.a3.a4, mientras que el número del puerto se
obtiene haciendo p1*256+p2
pwd
pwd
Muestra el nombre del directorio actual de la máquina remota (print
working directory).
quit
quit
Se desconecta de la máquina remota, y termina la sesión FTP.
type
type
::= A | I
Determina el tipo de los datos de la transferencia. A, es ASCII (para
enviar texto), e I es binario de 8 bits. El estándar contempla más
tipos de datos, pero sólo estos dos han sido implementados en el
proyecto.
user
user
Comienza el proceso de login. Esta instrucción se utiliza para enviar
el nombre de usuario. En nuestro caso, será siempre “anonymous”.
NOTA: Hay que tener especial cuidado a la hora de distinguir entre las
instrucciones que admite el cliente, y las que admite el servidor.
Cuando se teclean mandatos en un cliente sin interfaz gráfica de
usuario, primero deben pasar a través de su propio intérprete, de modo
que es posible que un mandato que el servidor reconozca perfectamente,
quede “atrapado” en un cliente que no lo admite.
En esos casos, se debe anteponer la palabra reservada “quote” (que
podría traducirse como: “citar textualmente”) a la instrucción, para
que sea enviada, tal cual, al servidor, sin tener que pasar antes por
el tamiz del intérprete de comandos del cliente.
De este modo, si el usuario teclea “help”, lo más probable es que se
muestre la ayuda del cliente. Para ver la del servidor, tendría que
introducir la expresión “quote help”. Esto sucede con muchos otros
mandatos. Por ejemplo, “pass” es interpretado por algunos clientes
como “entrar en modo pasivo”. El servidor desarrollado para este
proyecto, no obstante, siguiendo las normas descritas en las RFC,
interpreta que dicha instrucción es la que precede al envío de la
contraseña (password) por parte del cliente. De este modo, en dichos
clientes, si el usuario quisiera enviar manualmente su clave de
acceso, debería teclear “quote pass”.
Algunos matices a la lista:
Las instrucciones user y pass forman parte del proceso de login.
Teóricamente, no tiene sentido enviarlas por separado y en cualquier
momento.
Cuando se inicie una sesión FTP, el cliente solicitará el nombre de
usuario, y sólo admitirá que el cliente envíe “anonymous”, ya que el
proyecto contempla exclusivamente accesos en “modo lectura” a la
estructura de directorios.
Seguidamente, y de forma automática, se solicitará una dirección de
correo electrónico que haga las veces de contraseña. Dado que es muy
complicado garantizar que la dirección es válida, y virtualmente
imposible, hoy por hoy, asegurar que pertenece realmente al usuario
que está estableciendo la conexión, lo único que se exige es que sea
sintácticamente correcta, esto es, que responda a la forma genérica:
[email protected]ón.dominio. Nada impide que el usuario
introduzca algo como “[email protected]”.
Nótese, sin embargo, que es posible acceder al servicio incluso si no
se envía “anonymous” como nombre de usuario. En ese caso concreto, es
posible retomar el proceso de login en cualquier instante, simplemente
tecleando “user”.
Y un último apunte: el servidor FTP no distingue entre mandatos
tecleados en mayúsculas o en minúsculas.
Los mandatos no implementados son: “acct”, “smnt”, “rein”, “stru”, “mode”,
“stou”, “appe”, “allo”, “rest”, “rnrf”, “rnto”, “dele”, “nlst”, “rmd”,
“syst”, “mkd” y “site”.
5.4 COMUNICACIÓN ENTRE PROCESOS Y DINÁMICA.
Como se ha mencionado anteriormente, la aplicación consta de dos
procesos que se ejecutan por separado: un servidor FTP, y un “gestor
de la coherencia”, encargado de implementar el protocolo y asegurar la
consistencia de los datos contenidos en la TDV del nodo pertinente. Se
pretende que ambas tareas sean independientes en la medida de lo
posible, pero hay ciertas circunstancias en las que se verán obligadas
a comunicarse.
Existen multitud de maneras de programar tareas concurrentes, y Perl
ofrece varias de esas posibilidades, incluyendo la utilización de
técnicas bien conocidas de sincronización entre procesos paralelos (threads),
como las regiones críticas y los semáforos, y otras estrategias que
involucran memoria y recursos compartidos. Sin embargo, gran parte de
estas aproximaciones a la programación de procesos concurrentes es
poco aconsejable por diversas razones, entre las que se cuentan la
dificultad de garantizar la exclusión en el acceso a los recursos
compartidos, y el hecho de que algunas de las soluciones que las
últimas versiones de Perl ponen al alcance del usuario, aún se
encuentran en estados casi experimentales. De hecho, el propio
diseñador del lenguaje, Larry Wall, recomienda encarecidamente no
emplear semejantes estrategias en la versión 5.6.0, salvo que sea
estrictamente necesario16.
Es más que probable que el método de comunicación entre procesos que
se ejecutan al mismo tiempo más extendido de todos sea además el más
simple: la utilización de ficheros. Se dice que su empleo, junto con
el de una herramienta fiable (cuando está bien gestionada) como la
instrucción “fork”, es más frecuente que el de todos los demás
mecanismos juntos. No en vano, estos enfoques son limpios y sencillos
de programar.
La comunicación entre las dos tareas que integran este proyecto pone
el acento en esa estrategia, precisamente, y se utiliza un sencillo
sistema de ficheros para intercambiar información entre los procesos
(complementado con el uso de interrupciones software –o “señales”- en
aquellos casos en los que es necesaria una comunicación inmediata y en
tiempo real entre las dos partes de la aplicación). Véase la siguiente
ilustración:

Figura 7- Comunicación entre procesos
Todos los ficheros que se emplean en la comunicación, se escriben en
formato texto, de manera que sean legibles mediante cualquier editor
estándar. De este modo, se facilita el mantenimiento del sistema, en
el caso de que sea necesario comprobar el contenido de alguno de estos
archivos si se produce algún problema, o simplemente, se opta por
llevar a cabo algún tipo de control.
En la sección 5.6.8 - Formato de los ficheros de trabajo (página 171)
puede consultarse un resumen del formato de los archivos utilizados.
- mypid.txt
Generado por el servidor FTP, y leído por el gestor de la coherencia.
Contiene el PID (Process Identification Number, o número identificador
del proceso) del servidor FTP. Es necesario para que el gestor de la
coherencia pueda enviarle interrupciones software (SWI).
- localVDT.txt
Generado por el servidor FTP, y leído por el gestor de la coherencia.
Almacena el bloque local de la TDV del nodo (es decir, el generado
exclusivamente a partir de la estructura de directorios del disco duro
local). Lo emplea el gestor de la coherencia para construir los
mensajes XML que se enviarán a los demás nodos, cuando se haya
producido alguna actualización.
El formato es el siguiente:
Nombre de la entrada 1 de la TDV
Propietario17
Tamaño
Fecha
Permisos
Número de enlaces
Identificación del usuario
Identificación del grupo
Nombre de la entrada 2 de la TDV
Propietario

Nótese que cada 8 líneas del fichero (correspondientes a las 8
columnas de una fila completa de la TDV), se incluye una en blanco.
- ResponseTimes.txt
Generado por el gestor de la coherencia, y leído por el servidor FTP.
Contiene la relación de los nodos que integran el sistema, junto con
sus tiempos de respuesta (o “-1” en el caso de que éste supere un
umbral –configurable por el administrador- y se considere que está
desconectado). Cada cierto tiempo, el gestor de la coherencia vuelve a
calcular los tiempos de respuesta de los nodos, y a almacenarlos en
este fichero.
El servidor FTP lo utiliza cuando algún cliente solicita la descarga
de un fichero remoto. En ese caso, se determina en qué nodos se
encuentra el fichero (es perfectamente posible que esté en más de
uno), y se ordenan en función del tiempo de respuesta. Primero, se
tratará de “redireccionar” la petición de descarga hacia el nodo con
el mejor tiempo de respuesta. Si surgiera algún problema, se
intentaría con el segundo en la relación, y así sucesivamente, hasta
agotar todos los hosts en la lista, o hasta que alguno de ellos
responda y transmita el fichero remoto.
El formato del fichero es:
IP del nodo 1
Tiempo de respuesta del nodo 1
IP del nodo 2
Tiempo de respuesta del nodo 2

Hay que tener en mente, no obstante, que este archivo es un recurso
que comparten los dos procesos, ya que el gestor de la coherencia
escribe en él cada cierto tiempo, y el servidor FTP lee de él cada vez
que un cliente solicita la descarga de un archivo remoto. Se plantea,
por tanto, la necesidad de garantizar la exclusividad en los accesos
al fichero, para evitar que colisionen una lectura y una escritura que
se produzcan simultáneamente.
La solución escogida consiste en modelar, de un modo muy sencillo, una
región crítica en la que sólo uno de los dos procesos puede estar en
cualquier momento. Cuando uno trata de acceder al fichero “ResponseTimes.txt”,
ya sea para leer de él, o para escribir en él, comprueba primero que
no exista un archivo (vacío) con el nombre “RC”. Éste sólo figurará en
el disco duro cuando uno de los dos procesos esté trabajando sobre “ResponseTimes.txt”.
De este modo, si la otra tarea detecta la presencia de “RC”, aguardará
hasta que el fichero sea eliminado, antes de trabajar sobre el recurso
compartido. Creará entonces, a su vez, y para garantizar la
exclusividad en el uso del mismo, la región crítica.
- updatevdt.txt
Generado por el gestor de la coherencia, y leído por el servidor FTP.
Cuando aquél recibe un mensaje de actualización de la TDV de un nodo
remoto, extrae la información contenida en los elementos XML, y la
almacena en un archivo con el mismo formato que ”localVDT.txt”, salvo
la línea en blanco cada 8 filas (que no existe en este caso).
El gestor de la coherencia avisa entonces al servidor FTP de que tiene
un mensaje pendiente de ser procesado (debe actualizar la TDV local
con los datos recibidos), para lo que recurre a una interrupción
software18 (concretamente, una señal USR2), referida en la figura 7
con la etiqueta “SWI”.
Con todo esto, es posible esbozar un ejemplo sencillo, y paso a paso,
del funcionamiento del sistema (en un nivel cercano a la
implementación) cuando el nodo arranca por primera vez:
1. Se inicia el sistema. Se lanzan los dos procesos (la única
restricción es que primero se ejecute el servidor FTP).
2. A partir de la estructura de directorios cuyo “path” (configurable,
no lo olvidemos) figura en el fichero “variables_globales.pm”, el
módulo de directorios virtuales genera la TDV local, en memoria.
Cuando termina el proceso, la escribe en el fichero “localVDT.txt”.
3. El servidor FTP escribe su identificador de proceso (PID) en el
archivo “mypid.txt”, y permanece a la escucha de posibles clientes.
4. Mientras tanto, el gestor de la coherencia determina que el nodo
vuelve a estar conectado al sistema, e informa de ello a los demás
hosts, enviándoles a todos un mensaje XML de tipo “Node online”.
5. A continuación, se piden las tablas locales a todos los miembros
del sistema DFP, uno por uno. Conforme vayan llegando, nuestro nodo
derivará procesos hijos para trabajar sobre ellas en paralelo.
El resultado del procesamiento de cada una de esas tablas remotas,
será un archivo llamado “updatevdtN.txt” (donde N es el número
identificador del proceso hijo que generó el fichero, y varía entre 0
y 999).
Sólo resta esperar a que todos los hijos terminen de procesar las
tablas, para utilizar el contenido del archivo “mypid.txt” y enviar
así una interrupción software al servidor FTP, avisándole que tiene
una actualización preparada.
Supongamos ahora que el nodo lleva ya un tiempo funcionando, y recibe
un mensaje de tipo “node online” desde otro host de la red.
1. Al recibir este mensaje, el gestor de la coherencia deriva un hijo
que genera el archivo “mensajeN.xml” (de nuevo, N es el número de
identificación del proceso hijo, entre 0 y 999; no aparece en la
figura 7 dado que no se emplea como medio de comunicación entre los
dos procesos que integran el sistema), y le aplica el parser para
comprobar su corrección. Si todo va bien, sólo resta determinar el
tipo del mensaje, que resulta ser “1” (“node online”).
2. A continuación, construye un mensaje de respuesta, de tipo “Message
received”, confirmando al recién llegado la recepción de su aviso.
3. Cuando el nuevo nodo obtenga la confirmación, pide la TDV de la
máquina que la ha enviado.
4. El gestor de la coherencia utiliza el archivo “localVDT.txt” para
generar un mensaje de tipo “Update VDT”. Cuando esté listo, lo
transmite al nodo que le remitió el mensaje “Get VDT”.
5.5 CÓDIGOS DE RESPUESTA.
El protocolo FTP define una serie de códigos numéricos para la
comunicación entre el cliente y el servidor. Generalmente, van
acompañados por una cadena de texto legible cuya única finalidad es la
descripción del mensaje, para facilitar al usuario la comprensión del
proceso que está llevándose a cabo, por ejemplo:
150 Opening data connection.
Las RFC definen un significado genérico para cada código de respuesta.
La descripción queda en manos del programador. En el caso de este
proyecto, se han empleado los siguientes códigos:
Código
Significado genérico
Utilización o descripción en el proyecto
150
Se va a abrir una conexión de datos.
Mensaje: “Opening data connection”.
200
La instrucción enviada por el cliente se ha ejecutado con éxito.
Dependiendo de a qué instrucción se está respondiendo, puede mostrarse
uno de estos tres mensajes:
”Command successful”, “PORT command succesful”, “Type set to 8-bit
binary” o “Type set to ASCII”. Las dos últimas se refieren al tipo de
datos utilizado en las transferencias.
202
El cliente ha enviado una instrucción no implementada, o que no tiene
sentido en este servidor.
Como el servidor FTP desarrollado en este proyecto es anónimo, esta
respuesta se utiliza cuando el cliente envía un nombre de usuario
distinto de “anonymous”.
214
Mensaje de ayuda acerca del servidor o de alguna instrucción no
estándar. Sólo es de utilidad para el usuario humano.
Ayuda general sobre las instrucciones implementadas (LS, CWD, CDUP y
GET).
220
Indica que el servidor está listo para atender a un nuevo cliente.
Texto de bienvenida.
221
Se ha cerrado la conexión de datos y termina la sesión FTP.
Mensaje: “Quitting. Have a nice day ”
226
Termina con éxito una transferencia, y se cierra la conexión.
Mensaje: “Closing data connection”.
230
El usuario se ha conectado correctamente al servidor FTP, y puede
proceder.
Mensaje: “Login OK. Starting FTP session”.
250
Se solicitó algún tipo de operación sobre el sistema de ficheros, y se
ha completado con éxito.
En función de en qué directorio remoto se encuentra el usuario y de a
cuál pretenda cambiar, se mostrará uno de estos tres mensajes:
”Directory change successful”, “Changed to root” o “Already at root”
257
Se ha creado un “pathname” (camino hacia el directorio de trabajo).
Mensaje: “Current directory is ”, donde es
el camino hacia el directorio remoto en el que está trabajando al
cliente. Este mensaje se envía como respuesta a la instrucción PWD
(Print Working Directory).
331
Se ha recibido el nombre de usuario, y se solicita la clave de acceso.
Mensaje: “Username OK. Please, send your email address as a password”.
425
No se puede abrir la conexión de datos.
Mensaje: “Can’t open data connection”.
426
Se ha cerrado la conexión de datos, y se ha abortado la transferencia.
Mensaje: “Data connection closed. Transfer aborted”.
501
Hay un error de sintaxis en una instrucción enviada desde el cliente,
o en alguno de sus parámetros.
Como ya se ha mencionado, nuestro sistema FTP es anónimo, de manera
que se pide como clave una dirección de correo electrónico. Lo único
que se puede exigir, es que esté bien formada sintácticamente. Si no
es así, se muestra el mensaje “Badly formed email address”.
Otro caso en el que se envía el código 501 al cliente es,
precisamente, aquel en el que se recibe una instrucción
sintácticamente incorrecta. Entonces, se transmite también la
descripción: “Syntax error. Type ‘help’ for further information”.
502
Instrucción no implementada.
Mensaje: “Command not implemented”.
530
El usuario ha intentado efectuar una operación remota sin estar
conectado al servidor.
Mensaje: “Wrong directory”. A pesar de la definición estándar de este
código, también se utiliza para avisar al cliente de que ha intentado
cambiar a un directorio inexistente.
550
No se pudo efectuar la operación enviada por el cliente. El fichero
solicitado no está disponible.
Mensaje: “File not found” o bien, si no pudo abrirse un socket: “Can’t
open a listening socket”.
5.6 DISEÑO DETALLADO
Se ha dedicado un gran esfuerzo para comentar el código fuente de la
aplicación con la suficiente profusión como para que sea legible y
que, en la medida de lo razonable, se explique sin dificultad por sí
mismo. No obstante, en aras de la facilidad de comprensión y de
mantenimiento del proyecto, no está de sobra detallar aún más cada
módulo y cada rutina incluyendo, en algunos casos, ejemplos de
funcionamiento.
Recordemos que la aplicación consta de dos tareas que funcionan de un
modo casi independiente, y en paralelo. Una de ellas trabaja, a todos
los efectos, como un servidor FTP ordinario, aunque dispone de cierta
funcionalidad añadida específicamente para que satisfaga algunos de
los requisitos de este proyecto. Este proceso se divide en cuatro
módulos: el gestor de la conexión, el intérprete de comandos, el
sistema de directorios virtuales, y el gestor de mensajes al usuario.
La otra tarea, el gestor de la coherencia de los datos, consta de un
solo bloque de código, y contiene las funciones necesarias para la
implementación del protocolo de FTP distribuido (DFP).
En lo que respecta al servidor FTP, los cuatro módulos se integran en
un pequeño programa Perl almacenado en el fichero “principal.pl”:
#!/usr/bin/perl
use strict;
use warnings;
use mensajes_usuario;
use directorios_virtuales;
use interprete_comandos;
use gestor_conexion;
La primera línea: “#!/usr/bin/perl”, encabeza la práctica totalidad de
los programas escritos en Perl, y se utiliza, en sistemas Unix, para
señalar la ubicación del intérprete de este lenguaje.
Este archivo no contiene código efectivo, sino más bien llamadas a
otros módulos, sin olvidar los dos pragmas19: “use strict” y “use
warning”.
En Perl, las variables se interpretan en función de lo que se conoce
como “contexto”. Existen tres de ellos: el escalar (que se denota
anteponiendo al nombre de la variable, el carácter “$”), el contexto
de lista (se emplea el carácter “@”), y el contexto de hash (el
carácter utilizado es “%”).
De esta manera, algo como “@Cadena”, se interpreta como una lista.
Pero si se quiere obtener el elemento i-ésimo, y éste es un escalar
(cualquier tipo de dato que no sea ni un agregado –lista, cadena,
matriz…-, ni una estructura hash), habría que preceder el nombre de la
variable con el carácter “$”, así: $Cadena[$i].
Pues bien: un nombre definido por el usuario, en un fuente Perl, que
no esté precedido por uno de los tres caracteres que determinan el
contexto, se denomina “bareword” (podría traducirse como “palabra
desnuda”), y su uso no es especialmente aconsejable, por su
ambigüedad. ¿Cómo podría determinarse si una palabra desnuda es el
nombre de una función, o simplemente una cadena de texto?
Para evitar este problema potencial, se utiliza, precisamente, el
pragma “use strict”.
Respecto a “use warnings”, su utilidad se centra en permitir que el
intérprete de Perl muestre mensajes de aviso (warnings) al programador
(por ejemplo, si se encuentran variables que se utilizan una sola vez,
o que redefinen una declaración anterior, o bien, si el intérprete
descubre alguna conversión de tipos forzada, etc… en general, se trata
de situaciones que, si bien son sintácticamente correctas, pueden
producir problemas en tiempo de ejecución).
5.6.1 El módulo de variables globales
Este no es un proyecto pensado para interactuar con el usuario. En
realidad, gran parte de su desempeño se realiza de forma automática.
En la mayoría de los casos, el administrador no tiene más que
configurar algunos parámetros importantes la primera vez que el
sistema se ejecuta y, a partir de entonces, el funcionamiento del
mismo será independiente de toda intervención humana. La idea es que,
en cualquier situación prevista por el protocolo, los nodos que
integran la red puedan regularse por sí mismos. En principio, y salvo
dicha configuración previa, y las inevitables tareas de mantenimiento
(léase, la actualización de la estructura de directorios de un nodo,
añadiendo o eliminando directorios y ficheros), la aplicación
desarrollada puede funcionar indefinidamente, sin la asistencia de
ningún técnico.
Si algún nodo se de alta o de baja en otro punto de la red (para lo
que, obviamente, sí que es necesaria la mediación humana), el sistema
es lo suficientemente independiente como para reaccionar a la nueva
situación de forma adecuada.
Por este motivo, no ha sido necesario el desarrollo de una interfaz de
usuario. Sí que es esencial, no obstante, la utilización de una
sencilla aplicación20 que permita la asignación de valores a serie de
variables globales que contendrán los parámetros de configuración
mencionados anteriormente, imprescindibles para el correcto
funcionamiento de la aplicación.
Una vez que el administrador ha determinado los parámetros esenciales
del sistema, cuando éste arranca son importados desde el módulo de
variables globales que, a su vez, exporta al resto de los bloques de
código que conforman las dos tareas que constituyen la aplicación: el
servidor FTP y el gestor de la coherencia de los datos.
La idea detrás de este módulo es muy sencilla: se limita a leer los
datos almacenados en el fichero de configuración “DFP.cfg”, para
mantenerlos constantes y visibles desde cualquier punto del código
fuente.
Es destacable la utilización del módulo estándar de Perl llamado
“Exporter”, cuyo objetivo es la implementación de un mecanismo de
herencia entre distintas partes del código fuente de una aplicación21.
Las dos primeras variables globales que reciben un valor son las que
determinan el modo de operación inicial del servidor FTP (activo, por
defecto), y el tipo de transferencia (ASCII). Nótese que dichas
variables no son accesibles para el administrador, y simplemente se
incluyen aquí para que reciban un valor inicial.
package variables_globales;
require Exporter;
our @ISA=("Exporter");
our $_modo_pasivo=0;
our $_tipo_transferencia='A';
Se leen los valores de las variables esenciales, almacenadas en el
fichero de configuración “DFP.cfg”.
open CFG," or die "Error - Need configuration file 'DFP.cfg' - $!\n";
our $path=;
our $Host=;
our $Puerto=;
our $PingTimeout=;
our $Password=;
our $DeleteNode=;
close CFG;
chomp($path);
chomp($Host);
chomp($Puerto);
chomp($PingTimeout);
chop($Password);
El módulo concluye con la definición de las variables que se
consideran “exportables”.
our @EXPORT=qw($_modo_pasivo $_tipo_transferencia $path $Host
$Puerto $PingTimeout $Password $DeleteNode);
return TRUE;
5.6.2 El gestor de la conexión.
Este módulo se encarga de abrir el socket de comunicaciones con el que
se conectarán los clientes del servicio FTP. Para evitar que el
servidor sólo pueda atender a uno de ellos, se emplea una instrucción
“fork”, que deriva una copia (hijo) del proceso principal (padre). A
partir del punto en el que se separan, se ejecutarán en paralelo y por
separado. Hay que tener en cuenta que, como cada proceso tiene su
conjunto de variables y recursos propios, cualquier modificación que
un hijo efectúe sobre los mismos, no se reflejará sobre los del padre.
La única transmisión de información es en el sentido del padre al hijo
en el momento de la división (herencia).
El código de la rutina principal del módulo se muestra a continuación:
La primera línea define el fichero como un módulo (package).
package gestor_conexion;
… y esta otra, importa las variables definidas por el administrador
mediante el programa de configuración, y almacenadas en el fichero “variables_globales.pm”,
configuradas por el administrador. Se trata del nombre de la máquina
en la que se ha instalado la aplicación, el puerto22 en el que el
servidor FTP escuchará las solicitudes de conexión de los clientes, el
path que sirve de raíz a la TDV, la contraseña del grupo de nodos DFP
y el tipo de transferencia en las descargas (ASCII o binario de 8
bits).
use variables_globales qw($Host $Puerto $path $Password
$_tipo_transferencia);
Las siguientes líneas son el equivalente de la orden #include en el
lenguaje C (con un matiz, quizás: en el caso de “POSIX”, sólo se
importa un método: “ceil”, mediante la operación “qw”).
use IO::Socket::INET;
use POSIX qw(ceil);
use File::Basename;
use Net::FTP;
A continuación, se asignan los valores iniciales a dos variables:
my $mensaje="";
my $fin=0;
En este punto, se definen los llamados “manejadores de interrupción”.
Las señales (signals) no son si no interrupciones software que el
programador puede activar a voluntad.
En este caso, se contemplan dos: la señal CHLD, y la señal USR2. La
primera de ellas se activa automáticamente cuando termina uno de los
hijos derivados del proceso principal con la instrucción “fork”. Es
necesario ejecutar “wait” para evitar que ese hijo siga ocupando
memoria.
USR2, en este caso, es una señal externa. Se trata de una interrupción
que está a disposición del usuario. En esta aplicación, se utiliza
para llamar a una rutina que actualiza la TDV. Es el gestor de la
coherencia quien activa esta señal, cuando ha recibido un mensaje de
tipo “Update VDT” remitido por otro nodo. De esta manera, avisa al
servidor FTP para que se detenga momentáneamente, y actualice su tabla
de directorios virtuales. En realidad, el proceso lleva bastante poco
tiempo, y se realiza de una forma transparente a los clientes.
$SIG{CHLD}=sub { wait };
$SIG{USR2}=sub { directorios_virtuales::busca_actualizaciones() };
Para que el gestor de la coherencia pueda avisar al servidor FTP,
mediante una interrupción software, cuando haya una actualización de
la TDV lista para ser procesada, es necesario que conozca su PID, es
decir el identificador numérico del proceso. En Linux, cada tarea
tiene un PID asignado, lo cual es especialmente útil cuando se trata
de comunicarlas, o cuando es necesario terminar con una que no
responde, por ejemplo.
Pues bien, cuando el servidor FTP arranca, uno de los primeros pasos
que da consiste en detectar su propio PID (que cambia cada vez que se
lanza la tarea), y almacenarlo en el archivo “mypid.txt”, que
posteriormente leerá el gestor de la coherencia. El PID puede leerse
de la variable global “$$”.
open MIPID,">mypid.txt" or die "Cannot open 'mypid.txt' - $!\n";
print MIPID "$$";
close MIPID;
A partir de este punto, el gestor de la conexión entra en su bucle
principal, del que no saldrá nunca (sólo puede terminarse si el propio
administrador aborta la tarea).
while(1)
{
print "Opening socket...\n";
$server=IO::Socket::INET->new(LocalAddr => $Host,
LocalPort => $Puerto,
Type => SOCK_STREAM,
Reuse => 1,
Listen => 10)
or die "Can’t open a listening socket: $!\n";
print "Waiting for a client to connect...\n";
Aquí, se ha abierto el socket, y se aguarda a que algún cliente
solicite la conexión. El servidor escucha sobre un puerto determinado
por el administrador mediante el programa de configuración.
Lo siguiente es utilizar la instrucción “fork” para derivar un hijo
del servidor en cuanto llega la petición de conexión de un cliente. Si
no se recurriera a esta técnica, el servidor no podría gestionar
varias conexiones simultáneamente.
Es importante, sin embargo, tener en mente que los hijos derivados con
“fork”, en algunos sistemas operativos, quedan inertes en memoria
cuando terminan (transformados en “zombies” –curiosamente, esa es su
denominación técnica), así que es siempre conveniente liberar
manualmente los recursos que ocupan.
REQUEST:
while($cliente=$server->accept())
{
El flujo de ejecución permanece esperando en la instrucción anterior,
hasta que algún cliente solicite una conexión. En ese momento, se
avanza hasta la instrucción “fork”, que intenta derivar un hijo. Si el
proceso tiene éxito, a partir de ese momento habrá dos tareas
exactamente iguales ejecutándose simultáneamente, con sus propias
variables y recursos (es importante recordar esto: esos recursos son
independientes; una vez que la derivación ha tenido lugar, ni se
comparten, ni las modificaciones de los de una de las tareas, afectan
a los de las otras, independientemente de su relación jerárquica).
La llamada a la rutina “registra”, definida en el módulo de mensajes
al usuario, y que se lleva a cabo mediante el código “mensajes_usuario::registra”,
almacena la operación en un fichero de log, junto con la dirección IP
del cliente, procesada previamente mediante la rutina
“procesa_IP_cliente”, para hacerla imprimible23.
my $IPv4_cliente=procesa_IP_cliente($cliente);
if($kidpid=fork)
{
close $cliente;
next REQUEST;
}
defined($kidpid) or die "Cannot fork: $!\n";
mensajes_usuario::muestra_mensaje(BIENVENIDA,$cliente);
print "Client connected... initializing...\n";
mensajes_usuario::registra("Accepted connection from $IPv4_cliente");
Un apunte más acerca de la orden “fork”: dado que al ejecutarla, el
flujo de ejecución se divide en dos copias exactas, la tarea padre
también seguirá adelante en el código. Esto quiere decir que, si no se
controla adecuadamente, ambos procesos ejecutarán las mismas
instrucciones, lo que puede dar lugar a resultados impredecibles e
incorrectos.
Para evitarlo, en el caso que nos ocupa, se utiliza la instrucción “next”
asociada a una etiqueta que se coloca al principio del bucle en el que
se ubica la orden “fork”. Pues bien: esta instrucción devuelve dos
valores, uno al padre y otro al hijo. Aquél recibirá el número
identificador del proceso hijo (que en el código se almacena en la
variable $kidpid), y éste, el valor 0. De este modo, sólo hay que
comprobar el contenido de $kidpid para determinar a cuál de las dos
tareas pertenece el código que se está ejecutando. Si es distinto de
cero se trata del proceso padre, de modo que se fuerza la siguiente
iteración del bucle, para evitar que siga ejecutando el código, e
intente recibir mensajes del cliente, y enviárselos al intérprete de
comandos.
Después de enviar el mensaje de bienvenida, el proceso entra en un
bucle del que sólo podrá salir cuando el cliente le transmita una
instrucción “quit”, para terminar su sesión FTP. El objeto de dicho
bucle es capturar cada mensaje transmitido por el cliente, y
enviárselo al módulo intérprete de comandos, que se encargará de
responder y actuar en consecuencia.
A continuación, se eliminan los caracteres de retorno de carro y nueva
línea, que el cliente transmite al final de todos los mensajes, y la
cadena resultante se almacena en el fichero de log del servidor.
La ejecución permanece en este bucle hasta que el cliente envía una
instrucción “quit” para terminar su sesión FTP. En ese momento, se
llama a la subrutina “cierra_conexion” (descrita más adelante), que
almacena el valor “1” en la variable “$fin”. De este modo, el flujo de
ejecución puede alcanzar el código más allá del bucle, en el que se
cierra el socket del cliente y se termina con el hijo del proceso
principal.
while(!$fin)
{
$mensaje=<$cliente>;
mensajes_usuario::registra("$mensaje command issued from
$IP_cliente");
$mensaje=~s/\r|\n//;
mensajes_usuario::registra("$mensaje issued from $IP_cliente");
}
En este momento, se ha recibido una instrucción “quit” remitida por el
cliente, de modo que se cierra el socket, y se termina el proceso hijo
mediante la orden “exit”. Esto dispara la interrupción software CHLD,
como se describió antes. Su manejador se limita a liberar los recursos
que ocupa el proceso terminado, ejecutando la instrucción “wait”.
close $cliente;
exit;
}
$fin=0;
$mensaje="";
}
A partir de este punto, en el código se encuentran las subrutinas del
módulo gestor de la conexión. Ninguna de ellas se invoca directamente
desde el cuerpo principal (descrito hasta ahora), sino que se ejecutan
mediante llamadas que efectúan otros módulos de la aplicación
Subrutina “envia_respuesta”.
Entradas: el handle24 del socket del cliente, y una cadena de texto.
Salidas: ninguna.
Envía una cadena de texto, recibida como parámetro, al cliente
señalado por el “handle”.
sub envia_respuesta
{
my ($respuesta,$socket_cliente)[email protected]_;
print $socket_cliente $respuesta;
}
Subrutina “abre_conexion_datos”.
Entradas: la dirección IP del cliente y, en modo activo, el puerto con
el que se desea conectar. En modo pasivo, este parámetro contiene el
valor –1.
Salidas: ninguna.
Abre un socket para la transmisión de datos al cliente (por ejemplo,
descargas de ficheros, envío de la lista de entradas de un directorio,
etc.)
sub abre_conexion_datos
{
my ($IP_cliente,$puerto_cliente)[email protected]_;
mensajes_usuario::registra("Opening data connection to
$IP_cliente:$puerto_cliente");
$conexion_datos=new IO::Socket::INET->new(PeerAddr => $IP_cliente,
PeerPort => $puerto_cliente,
Type => SOCK_STREAM,
Proto => "tcp",
Reuse => 1)
or return undef;
return $conexion_datos;
}
Subrutina: “modo_pasivo”.
Entradas: un “handle” al socket del cliente.
Salida: si todo va bien, devuelve el valor “1”, y el socket abierto en
modo pasivo. Y si no, se devuelven los valores “0” y “–1”.
En modo pasivo, el cliente es quien inicia las conexiones con el
servidor. Así se soluciona el problema que se plantea cuando se emplea
un firewall25 que filtra los datos que el servidor envía.
sub modo_pasivo
{
my ($socket_cliente)[email protected]_;
La siguiente línea de código define el rango de puertos disponibles
para el modo pasivo. Según la IANA (Internet Assigned Numbers
Authority)26, los puertos de comunicaciones se dividen en tres rangos:
- Los puertos “bien conocidos”, desde el 0 al 1023, que sólo pueden
ser empleados por los procesos que lanza el propio sistema, o por los
que ejecutan usuarios con los privilegios suficientes.
- Los puertos “registrados”, desde el 1024 al 49511, pueden ser
utilizados por procesos o usuarios normales.
- Y los puertos “dinámicos” o “privados”, desde el 49512 al 65535,
que, digamos, quedan libres para poder ser empleados en otras
aplicaciones.
Pues bien: para el modo pasivo, se emplea el rango de puertos
dinámicos. De éste, se escoge uno, al azar:
my $rango_puertos="49152-65535";
my @rangos=split /\s*,\s*/, $rango_puertos;
my $total_width=0;
foreach(@rangos)
{
my ($min,$max)=split /\s*-\s*/, $_;
$_=[$min,$max,$max-$min+1];
$total_width+=$->[2];
}
my $cuenta=100;
my $sock;
until(defined $sock || --$count==0)
{
my $n=int(rand $total_width);
my $port;
foreach(@ranges)
{
if($n<$_->[2])
{
$port=$_->[0]+$n;
last;
}
$n-=$_->[2];
}
Ahora, se abre el socket con el puerto seleccionado aleatoriamente del
rango de los puertos dinámicos:
$sock=IO::Socket::INET->new(Listen => 1,
LocalAddr => $Host,
LocalPort => $port,
Reuse => 1,
Proto => "tcp",
Type => SOCK_STREAM);
}
Si hubiera algún problema y el socket no pudiera abrirse, se enviaría
un mensaje al cliente (concretamente, el correspondiente al código 55027),
a través del gestor de mensajes al usuario, y se devolverían los
valores “0” y “–1”.
unless($sock)
{
mensajes_usario::muestra_mensaje(CODIGO550SOCKET,
$socket_cliente);
return (0,-1);
}
Si todo ha ido bien, obtiene el socket, y lo divide en sus componentes
“alto” y “bajo”, necesarios para calcular los parámetros de la
instrucción PASV. Recuérdese que éstos se muestran con el formato:
(a1, a2, a3, a4, p1, p2), de modo que la dirección IP en la que se
abre el socket pasivo, se construiría con a1.a2.a3.a4, y el puerto se
obtendría de resolver la expresión: (p1*256)+p2.
my $sockport=$sock->sockport;
my $p1=int($sockport/256);
my $p2=$sockport%256;
La IP del host se consigue mediante la instrucción “gethostbyname”.
Como se obtiene empaquetada (comprimida), es necesario dividirla en
sus cuatro componentes, empleando la orden “unpack”.
my $ip_addr=gethostbyname($Host);
my ($a,$b,$c,$d)=unpack('C4',$ip_addr);
Ya sólo queda utilizar el módulo de mensajes al usuario para enviar la
cadena que contiene la dirección y el puerto del modo pasivo.
mensajes_usuario::muestra_mensaje_cp("227 Entering passive mode
($a,$b,$c,$d,$p1,$p2)",$socket_cliente);
mensajes_usuario::registra("Entering passive mode
($a,$b,$c,$d,$p1,$p2)");
return (1,$sock);
}
Subrutina: “cierra_conexion”.
Entradas: ninguna.
Salidas: ninguna.
Pone la variable global (del módulo) “$fin” a 1, para que se pueda
salir del bucle principal, y alcanzar la instrucción “exit”, que
termina con el hijo actual. De este modo, termina el clon que se había
derivado cuando el cliente actual solicitó la conexión, y se liberan
sus recursos.
sub cierra_conexion
{
print "Closing connection...\n";
$fin=1;
}
Subrutina “miniclienteFTP”
Entradas: la dirección IP del servidor que aloja el fichero objeto de
una descarga remota, el nombre de éste y su tamaño, el socket de datos
abierto hacia el cliente, y el tiempo máximo durante el que se va a
esperar a que el servidor origen responda, antes de determinar que es
inaccesible, y que se debe intentar la descarga remota con otra
máquina diferente (si es posible).
Salidas: 1 si la descarga se lleva a cabo con éxito, y 0 en caso
contrario.
Para simular las descargas remotas en tiempo real, esto es, que
conforme los datos van llegando al proxy, éste los reenvía sin más
dilación hacia el cliente, se utiliza un mecanismo no especialmente
elegante, pero sin duda eficaz: recurriendo a la librería estándar de
Perl “Net::FTP”, se implementa un sencillo cliente FTP que se conecta
al servidor origen, pero no como un cliente más. Deja constancia de su
condición de proxy enviando como nombre de usuario la contraseña del
grupo DFP, y como clave, el nombre completo del fichero objeto de la
descarga remoto. Esta operación pondrá sobre aviso al servidor que,
desde ese momento, atenderá a la solicitud como corresponde.
Cuando reciba la petición de descarga, primero generará un fichero
temporal que contiene un fragmento de 64 Kbytes, del fichero original.
La rutina “miniclienteFTP” no tiene más que entrar en un bucle en el
que solicita la descarga de sucesivos ficheros temporales, todos de 64
Kbytes de tamaño que, en el instante en que se reciben, son
automáticamente retransmitidos al cliente.
sub miniclienteFTP
{
my ($IP_server,$fichero,$tamanho,$socket_datos)[email protected]_;
my $exito=1;
my $path_base=dirname($fichero);
Para hallar el número de iteraciones necesarias, esto es, el número de
fragmentos de 64 Kbytes en los que se puede dividir el archivo
original, simplemente se calcula el cociente entre el tamaño de éste,
y la constante 65536 (redondeando por exceso).
my $iteraciones=POSIX::ceil($tamanho/65536);
Comienza el proceso de descarga remota: el proxy se conecta al
servidor origen, utilizando la contraseña del grupo DFP como login, y
el nombre completo del fichero, como password. La llamada al método “login”
recibe un parámetro obligatorio, la dirección IP del servidor, y tres
opcionales: 1026 (el puerto al que se conectará el proxy), 0 (para
desactivar el modo debug) y la variable “$timeout”, para determinar el
tiempo máximo de espera.
Todos los métodos de la librería Net::FTP devuelven 1 si su aplicación
ha tenido éxito, y 0 en caso contrario. Esta característica se utiliza
para determinar el valor que debe retornar la rutina “miniclienteFTP”.
my $ftp=Net::FTP->new($IP_server,Port => 1026,Debug => 0,
Timeout => $timeout);
if(!$ftp)
{
mensajes_usuario::registra("Can't connect to remote DFP node");
$exito=0;
}
else
{
mensajes_usuario::registra("Remote login successful");
El siguiente paso es cambiar al directorio que contiene el fichero
remoto.
if($ftp->cwd($path_base))
{
mensajes_usuario::registra("Successfully changed to $path_base");
Comienza el bucle interno, en el que se pide cada fragmento de 64
Kbytes, y se retransmite al cliente, utilizando la rutina “descarga_archivo_local”
(evidentemente, en cuanto se recibe el trozo, se convierte en un
fichero local más).
for(my $i=0;$i<$iteraciones;$i++)
{
my $destino=$path . "tmpdwnld" . $i;
if($ftp->get("tmpdwnld$i",$destino))
{
directorios_virtuales::descarga_archivo_local("tmpdwnld$i",
$socket_datos,
$_tipo_transferencia,
1);
unlink $destino;
}
else
{
mensajes_usuario::registra("Can't retrieve remote file
$destino");
$exito=0;
}
last if !$exito;
}
}
Si el flujo de ejecución alcanza una de las dos siguientes ramas de
sendas instrucciones condicionales, se deberá a que no ha podido
encontrar el directorio remoto (en el primer caso), o a que no ha
podido conectar con el servidor origen (en el segundo).
else
{
mensajes_usuario::registra("Can't change to $path_base");
$exito=0;
}
}
else
{
mensajes_usuario::registra("Remote login failed");
$exito=0;
}
Finalmente, se cierra la sesión FTP, y se registra el resultado de la
operación en el fichero de log del servidor.
mensajes_usuario::registra("Closing connection");
$ftp->quit;
}
if($exito)
{
mensajes_usuario::registra("Successfully retrieved $fichero");
}
return $exito;
}
Subrutina: “procesa_IP_cliente”
Entradas: la dirección IPv6 del cliente.
Salidas: la misma dirección, en formato IPv4 imprimible.
Para registrar la dirección de cada cliente que accede al servidor FTP
en el fichero de log, es conveniente procesarla previamente. Esta
rutina se encarga de convertirla al formato IPv4, y hacerla
imprimible, para lo que recurre al método estándar “getpeername”, que
desencripta una dirección remota.
Esta rutina es prácticamente idéntica a “procesa_IP”, del gestor de la
coherencia.
sub procesa_IP_cliente
{
my $IP_remota=shift;
$IP_remota=getpeername($IP_remota);
my $IP_imprimible=sprintf "%vd",$IP_remota;
my @bytesIP=split /\./, $IP_imprimible;
my $IPv4="$bytesIP[4].$bytesIP[5].$bytesIP[6].$bytesIP[7]";
return $IPv4;
}
5.6.3 El gestor de mensajes al usuario.
Este módulo almacena en una estructura hash28 todas las respuestas que
pueden enviarse al cliente (excepto las que deben instanciarse con un
parámetro –ver la descripción de la rutina “muestra_mensaje_cp”, en
este mismo módulo). Mediante este enfoque, modificar los mensajes, ya
sea editando sus descripciones o añadiendo nuevos y borrando antiguos,
es tan sencillo como escribir la clave asociada al mensaje, y a
continuación, la cadena que se transmitirá al cliente llegado el caso.
Sólo hay que tener en cuenta dos normas: la descripción debe
escribirse entre comillas, y las entradas de la estructura hash deben
separarse por comas.
El cuerpo principal del módulo alberga estos mensajes, mientras que
las funciones necesarias para enviarlas al cliente se dejan a cargo de
llamadas a operaciones del gestor de la conexión, que se efectúan
desde las subrutinas pertinentes.
package mensajes_usuario;
Esta es la estructura hash que aloja los mensajes posibles: “%grupo_mensajes”.
(Obsérvese cómo el nombre de la variable está precedido por el
carácter “%” para determinar el contexto). Para dar una serie de
valores iniciales a una variable de esta índole, basta con seguir esta
sintaxis:
my %nombre_hash =
(‘clave_1’,valor_asociado_1,’clave_2’,valor_asociado2, … ,
‘clave_N’,valor_asociado_N);
En este caso, las claves son palabras o expresiones que describen, a
ser posible, de un modo conciso, las variables a las que están
asociadas, y éstas no son sino los mensajes que se enviarán al
cliente, en forma de cadenas de texto ordinario. Eso sí: todas deben
terminar en “\r” cuando se pretende que el cliente imprima en su
pantalla un retorno de carro, y “\r\n”29, cuando es la última línea
del mensaje.
Nótese también que cuando un mensaje está formado por varias líneas,
al código identificador le sigue un guión en todas ellas, salvo en la
última.
Los códigos que acepta el servidor FTP que se ha desarrollado, y sus
descripciones, aparecen en la sección 5.5 - Códigos de respuesta,
página 29.
my %grupo_mensajes=(
'AYUDA_GENERAL',
"214-Commands supported:\r
214-ls - Perform a directory listing.\r
214-cwd - Change working directory.\r
214-cdup - Change to parent directory.\r
214-help - Display help.\r
214-retr - Download a remote file.\r
214 Type 'help ' for further information.\r\n",
'AYUDA_LS',
"214-ls - Perform a directory listing.\r
214-SYNTAX: ls\r
214-SHORT: None.\r
214 Some clients also use DIR, LST or NLST\r\n",
'AYUDA_CWD',
"214-cwd - Change working directory.\r
214-SYNTAX: cwd \r
214-SHORT: None.\r
214 Some clients use CD instead\r\n",
'AYUDA_CDUP',
"214-cdup - Changes to parent directory.\r
214-SYNTAX: cdup\r
214-SHORT: None.\r
214 Some clients use CD \.\. instead\r\n",
'AYUDA_RETR',
"214-retr - Download a remote file and store it locally.\r
214-SYNTAX: retr \r
214-SHORT: None\r
214 Some clients use GET instead\r\n",
'BIENVENIDA',
"220-Initializing FTP session.\r
220-This is a read-only server.\r
220 Welcome.\r\n",
'CODIGO150',
"150 Opening data connection.\r\n",
'CODIGO200',
"200 Command succesful.\r\n",
'CODIGO200TYPEI',
"200 Type set to 8-bit binary\r\n",
'CODIGO200TYPEA',
"200 Type set to ASCII\r\n",
'CODIGO200PORT',
"200 PORT command succesful.\r\n",
'CODIGO202',
"202 This is an anonymous FTP server. Please send 'USER
anonymous'\r\n",
'CODIGO221',
"221 Quitting. Have a nice day :)\r\n",
'CODIGO226',
"226 Closing data connection.\r\n",
'CODIGO230',
"230 Login OK. Starting FTP session.\r\n",
'CODIGO250',
"250 Directory change successful.\r\n",
'CODIGO250RAIZ',
"250 Changed to root.\r\n",
'CODIGO250ENRAIZ',
"250 Already at root directory.\r\n",
'CODIGO331',
"331 Username OK. Please, send your email address as a
password.\r\n",
'CODIGO331PROXY',
"331 Proxy detected. Send remote path\r\n",
'CODIGO425',
"425 Couldn't open data connection.\r\n",
'CODIGO426',
"426 Data connection closed. Transfer aborted.\r\n",
'CODIGO501',
"501 Badly formed email address.\r\n",
'CODIGO501NOPROXY',
"501 Bad proxy password. Are you cheating?\r\n",
'CODIGO501SYNTERROR',
"501 Syntax error. Type 'help' for further information\r\n",
'CODIGO502',
"502 Command not implemented.\r\n",
'CODIGO530',
"530 Wrong directory.\r\n",
'CODIGO550',
"550 File not found.\r\n",
'CODIGO550SOCKET',
"550 Can't open a listening socket.\r\n",
);
Subrutina: “muestra_mensaje”.
Entradas: el código de identificación de la respuesta, y un “handle”
al socket del cliente.
Salidas: ninguna.
La rutina se limita a invocar una función del gestor de la conexión,
que se encarga de transmitir al cliente el mensaje determinado por el
parámetro “$tipo_mensaje”. Éste se utiliza como clave de acceso en la
estructura hash “%grupo_mensajes”, para obtener el código y la cadena
de texto que se enviarán.
De este modo, si, por ejemplo, se pretende transmitir al cliente la
respuesta al envío de una instrucción sintácticamente incorrecta, no
hay más que escribir:
mensajes_usuario::muestra_mensaje(CODIGO501SYNTERROR,$socket_del_cliente);
El intérprete de Perl buscará en la estructura hash “%grupo_mensajes”
aquella entrada asociada a la clave CODIGO501SYNTERROR. Corresponde,
precisamente, a la cadena “501 Syntax error. Type ‘help’ for further
information.\r\n”. Pues bien: este es el texto que se transmite al
cliente.
Obsérvese que la instrucción queda registrada en el fichero de log del
sistema, junto con la dirección IP del cliente, convenientemente
procesada para hacerla imprimible. La distinción entre el mensaje de
bienvenida y el resto se debe a motivos meramente estéticos: cuando se
transmite dicho texto al cliente, no tiene mucha utilidad almacenar en
el fichero de registro todas las líneas de que consta, así que el
módulo se limita a grabar un testimonial “Welcome sent to…”.
El código de la rutina es el que sigue:
sub muestra_mensaje
{
my ($tipo_mensaje,$socket_cliente)[email protected]_;
gestor_conexion::envia_respuesta($grupo_mensajes{$tipo_mensaje},
$socket_cliente);
my $IP_cliente=gestor_conexion::procesa_IP_cliente($socket_cliente);
if($tipo_mensaje ne BIENVENIDA)
{
$mensaje=~s/(\r\n)//;
registra("$mensaje sent to $IP_cliente");
}
else
{
registra("220 Welcome sent to $IP_cliente");
}
registra("$grupo_mensajes{$tipo_mensaje} sent to IP_cliente");
}
Subrutina “muestra_mensaje_cp”.
Entradas: el mensaje a transmitir y un handle al socket del cliente.
Salidas: ninguna.
Esta rutina es análoga a la anterior, sólo que permite transmitir
cadenas que se parametrizan en el módulo de origen (de ahí el sufijo “cp”
en el nombre, que significa: “con parámetros”).
En ocasiones, es conveniente que la descripción sea algo más concisa
que las que pueden encontrarse en la estructura hash “%grupo_mensajes”.
Por ejemplo, cuando el cliente solicita al servidor que le envíe el
nombre del directorio en el que está trabajando, mediante la
instrucción PWD, la respuesta debe contener una cadena de texto que se
instanciará en función de dicho nombre. Una respuesta típica a la
ejecución de la instrucción PWD, suele tener esta apariencia: “257
Current directory is home/Desktop/”. Por tanto, es necesario que la
cadena de texto que se va a enviar al cliente, acepte un parámetro.
Esa es la finalidad de esta rutina.
En realidad, la parametrización se lleva a cabo en el módulo que la
invoca. Su cometido es, exclusivamente, el de pasar la cadena
recibida, tal cual, al módulo gestor de la conexión. Una llamada a
esta rutina puede tener este aspecto:
mensajes_usuario::muestra_mensaje_cp("257 Current directory is
\"$path_cliente\"",$socket_cliente);
Nótese cómo la cadena de texto se instancia con la variable
$path_cliente, que contiene el camino hasta el directorio de trabajo
(las barras invertidas: “\” se utilizan para que se impriman las
comillas dobles que encierran el nombre del path, y así evitar que
intenten evaluarse.
De nuevo, se procesa la dirección IP del cliente que figurará en el
archivo de log, para hacerla imprimible.
El código fuente se muestra a continuación:
sub muestra_mensaje_cp
{
my ($mensaje,$socket_cliente)[email protected]_;
gestor_conexion::envia_respuesta("$mensaje\r\n",$socket_cliente);
my $IP_cliente=gestor_conexion::procesa_IP_cliente($socket_cliente);
registra("$mensaje sent $IP_cliente");
}
Subrutina: “registra”
Entradas: la cadena de texto que pretende almacenarse en el fichero de
log.
Salidas: ninguna.
Esta rutina almacena la cadena de texto recibida como parámetro en el
fichero de registro del servidor FTP (hay otro, de finalidad idéntica,
en dedicado a anotar las incidencias y operaciones que tienen lugar en
el gestor de la coherencia).
El nombre de este fichero se construye con el prefijo “SRV” (de
“server”; el gestor de la coherencia utiliza el acrónimo “CM”, de
“coherence manager”) y la fecha en la que se creó, de modo que se
abrirá uno distinto cada día.
Así, el 15 de Marzo de 2002, esta rutina crearía un fichero llamado “SRV15.3.2002.log”.
(El equivalente del gestor de la coherencia recibiría el nombre: “CM15.3.2002.log”).
El archivo de abre en modo de actualización para permitir que cada
escritura que se efectúe sobre él, se haga a continuación de la
anterior (de lo contrario, las cadenas de texto se sobreescribirían).
Obsérvese cómo se cierra siempre después de cada actualización;
recordemos que en Perl, los cambios que se apliquen sobre un archivo,
sólo se hacen definitivos cuando se cierra.
Para generar el nombre del fichero, se utiliza el método estándar
“localtime”, que devuelve una lista que contiene, en sus seis primeras
posiciones (que son, precisamente, las que se utilizan en esta
rutina), los segundos, minutos, hora, día, mes y año en el que se
invocó.
sub registra
{
my $texto=shift;
my @fecha=localtime;
A continuación, se emplean las posiciones 4, 5 y 6 de la lista
devuelta por “localtime” (contando desde 0), para construir el nombre
del archivo. Es interesante resaltar que el mes se devuelve en formato
numérico, de 0 a 11 (esto es, a Enero le corresponde el 0, y a
Diciembre el 11); por eso, se suma 1 al resultado. También conviene
destacar que el valor que ocupa la sexta posición no es exactamente el
año, sino el número de éstos transcurridos desde 1900. De este modo,
el 2002 se representa con 102 (de ahí que la rutina sume “1900” al
resultado).
Para terminar, la cadena de texto se almacena en el fichero junto a la
hora en la que se produjo la incidencia, contenida en las posiciones
1, 2 y 3 de la lista devuelta por “localtime”.
my $nombre_fichero="SRV$fecha[3]." . ($fecha[4]+1) . "." .
($fecha[5]+1900) . ".log";
open LOG,">>$nombre_fichero";
print LOG "$fecha[2]:$fecha[1]:$fecha[0] - $texto\n";
close LOG;
}
5.6.4. El intérprete de comandos.
El cometido de este módulo es recibir los mensajes transmitidos desde
el cliente, determinar su corrección y construir la respuesta
apropiada. Muchas de las instrucciones recibidas afectarán a la TDV,
en cuyo caso, se remitirán al módulo de directorios virtuales.
package interprete_comandos;
El módulo importa las variables globales “$_modo_pasivo”, “$_tipo_transferencia”
y “$Password”. La primera determina si el servidor está funcionando o
no en modo pasivo; la segunda se utiliza para decidir si se ha
seleccionado un tipo de transferencia ASCII (el usual para el envío de
ficheros de texto), o binario de 8-bits (que suele ser el habitual
cuando se pretende transferir archivos binarios, esto es, ejecutables,
imágenes, y en general, cualquier fichero no legible mediante un
editor estándar de texto). La tercera se emplea en la conexión con un
proxy que solicita una descarga remota.
use variables_globales qw($_modo_pasivo $_tipo_transferencia
$Password);
my $instruccion;
my $codigo;
my $parametros;
my $socket_conexion_datos;
my $IP_cliente;
my $puerto_cliente;
my $socket_datos;
my $es_proxy;
my $indice;
my $archivo_remoto;
La línea siguiente define una tabla global (dentro del ámbito del
presente módulo) que almacena las instrucciones no implementadas en
este servidor FTP. De este modo, si la orden recibida desde el cliente
no es reconocida por el intérprete de comandos, pero figura en esta
tabla, no se le avisará que ha cometido un error de sintaxis, sino que
ha utilizado un mandato correcto, pero no reconocido.
my @ordenes_no_implementadas=
(
"ACCT","SMNT","REIN","STRU","MODE","PORT","STOU","APPE","ALLO",
"REST","RNRF","RNTO","DELE","RMD",”NLST”,"SYST","MKD","SITE"
);
A diferencia de otros módulos, el intérprete de comandos no cuenta con
un cuerpo principal y una serie de rutinas de apoyo, sino que todo su
código está formado por subprogramas. Uno de ellos es especialmente
importante, ya que es el que se encarga de analizar los mensajes
recibidos por el gestor de la conexión. El procedimiento recibe dos
parámetros: el mensaje transmitido por el cliente, y el socket abierto
con él. Éste es imprescindible a la hora de devolverle una respuesta.
sub interpreta
{
my ($orden,$socket_cliente)[email protected]_;
Comprobar si la variable “$orden” está definida es una medida de
seguridad para evitar que, si sucede algún tipo de problemas con la
sesión FTP, el servidor reciba mensajes incorrectos que puedan
producir errores en el intérprete. Además, se comprueba la sintaxis de
la instrucción transmitida, haciendo uso de una de las herramientas
más potentes de Perl: las expresiones regulares30.
Obsérvese la expresión utilizada dentro de la segunda instrucción “if”
que aparece a continuación. Se interpreta del siguiente modo: la orden
remitida por el cliente debe comenzar por una o más letras, de la “a”
a la “z”, ya sean mayúsculas o minúsculas. Se admite que estén
separadas por un espacio, de un grupo de cualquier tipo de caracteres.
Más en detalle:
La expresión [a-zA-Z], se refiere a un rango. En este caso,
alfabético, aunque es frecuente utilizar el numérico [0-9]. El hecho
de que se acepten mayúsculas o minúsculas, se representa incluyendo
los dos intervalos alfabéticos, uno junto al otro: a-zA-Z.
El símbolo “+” es lo que se conoce como “clausura positiva”. Indica
que lo que está encerrado entre los corchetes puede repetirse una o
más veces. En resumen: esta expresión informa al intérprete de Perl
que “$orden” tiene que comenzar con una serie de 1 ó más caracteres
alfabéticos, ya sea en mayúsculas o en minúsculas, para poder acceder
a la rama positiva de la instrucción “if”.
En el código se observa que esta primera parte de la expresión regular
está encerrada entre paréntesis. Pues bien: esto no sólo se emplea
para agrupar símbolos, sino para que sea posible almacenar en
variables los fragmentos que el parser reconoce. Pero no adelantemos
acontecimientos; aún restan varios símbolos por comentar:
La expresión “\s” indica al intérprete que se esperan espacios en
blanco, y el símbolo de cierre de interrogación que le sigue,
significa “0 ó 1”. Es decir, que después de la serie de 1 ó más
caracteres, en mayúsculas o minúsculas, con la que comienza la orden
enviada por el cliente, puede haber un espacio en blanco, aunque
también se admite que no aparezca ninguno.
Y para terminar, el punto “.” es un comodín que se refiere a cualquier
carácter reconocible por el intérprete. El asterisco “*” simboliza lo
que se conoce como “clausura”, y significa “repetición de cero o más
veces de lo anterior”. Obsérvese la diferencia entre este concepto y,
el más concreto, de la “clausura positiva”.
Así que, para terminar, la expresión asegura que la orden del cliente
concluye con una serie indefinida de caracteres. Así se contempla la
posibilidad de que la instrucción incluya un parámetro.
La siguiente ilustración pretende clarificar la idea detrás de esta
expresión regular:

Así, una instrucción como “PORT”, o “USER”, se reconocerían mediante
la primera parte de la expresión regular: ([a-zA-Z]+). Detrás de la
secuencia inicial de letras, no aparece ningún espacio en blanco, ni
ninguna serie de caracteres de cualquier tipo.
Algo como “Cwd /home/proyecto” también sería admitido. “Cwd” pasa el
filtro de la primera expresión, mientras que “/home/proyecto” pasa el
segundo. Ambas expresiones están separadas por un espacio en blanco.
Esto parece que puede dar pie a ciertas ambigüedades. Por ejemplo, si
el usuario tecleara (incorrectamente), algo como “cwd/home/proyecto”
(sin el espacio en blanco), ¿el parser lo interpretaría como
perteneciente a la primera expresión, con lo que no concordaría con el
patrón, ya que las barras separadoras de directorios no son letras del
alfabeto entre la “a” y la “z”, o lo tomaría como una expresión
correcta, ya que comienza con una secuencia de caracteres alfabéticos,
concluye con otra, de cualquier tipo (lo que admite las barras
separadoras), que, simplemente, están separadas por cero espacios en
blanco?
Ante una situación así, hay que tener en cuenta que el motor de Perl,
de interpretación de expresiones regulares, sigue una filosofía
conocida como “leftmost” (algo así como “lo que está más a la
izquierda”), es decir, que intenta siempre encontrar una coincidencia
con el patrón planteado, que esté lo más a la izquierda posible de la
expresión. En el caso que nos ocupa, el intérprete intentaría hacer
coincidir “cwd/home/proyecto” con el primer bloque de la expresión
regular, esto es, con “([a-zA-Z]+)”. No coincidiría, así que la
ejecución continuaría después de la instrucción “if”.
if(defined($orden))
{
if($orden =~ m/([a-zA-Z]+)\s?(.*)/)
{
$instruccion=(uc $1);
$parametro=$2;
chop($parametro);
}
}
else
{
gestor_conexion::cierra_conexion($socket_cliente);
return;
}
A partir de aquí, se intenta identificar la instrucción enviada por el
usuario.
Primero, se comprueba si se trata de la orden “USER”, que precede al
envío del nombre de usuario.
Si es así, se analiza el parámetro, que debe contener la palabra “anonymous”.
En caso contrario, se informa al cliente que el servidor es anónimo
(esto, no obstante, no le impedirá continuar con su sesión FTP).
Nótese cómo se invoca a la rutina “muestra_mensaje” en el módulo de
mensajes al usuario: se le pasa, como primer parámetro, el código del
mensaje, y como segundo, el socket abierto con el cliente. En este
caso, el CODIGO202, envía al usuario el mensaje “This is an anonymous
FTP server. Please, send ‘USER anonymous’”, y el CODIGO331, “Username
Ok. Please, send your email address as a password”.
Hay que hacer un matiz, no obstante: puede que quien envía la
instrucción “USER” no sea un cliente ordinario, sino un proxy que está
tratando de redirigir la solicitud de descaega de un cliente hacia
este servidor. En ese caso, la password, en lugar de “anonymous”, será
la contraseña del grupo DFP.
if($instruccion eq "USER")
{
if($parametro ne "anonymous")
{
if($parametro ne $Password)
{
mensajes_usuario::muestra_mensaje(CODIGO202,$socket_cliente);
}
else
{
mensajes_usuario::muestra_mensaje(CODIGO331PROXY,
$socket_cliente);
$es_proxy=1;
}
}
else
{
mensajes_usuario::muestra_mensaje(CODIGO331,$socket_cliente);
}
}
Se hace uso de la instrucción “elsif” para encadenar las estructuras
condicionales. Es bastante similar a la estructura “case”, aunque su
sentido semántico es el de una serie de “if” anidados, con la ventaja
de que todas las condiciones están al mismo nivel. Esto facilita
sensiblemente la legibilidad del código31. Como se puede suponer, no
es tan sencillo depurar y mantener algo como:
if(Condición1)
{

}
else
{
if(Condición2)
{

}
else
{
if(Condición3)
{

}
else
{

}
}
}
… que algo como…
if(Condición1)
{

}
elsif(Condición2)
{

}
elsif(Condición3)
{

}
else
{

}
La siguiente condición evalúa si la orden transmitida es “PASS”. Sólo
se admitirá como contraseña válida, una dirección de correo
electrónico sintácticamente correcta. De todos modos, el usuario será
capaz de proceder aún cuando la clave suministrada no se atenga a esta
restricción.
Antes de nada, se comprueba que no estamos ante el intento de acceso
de un proxy. En un caso así, la contraseña debe ser el nombre completo
de uno de los archivos almacenados en la TDV de este servidor (lo que
se comprueba mediante la rutina “encuentra_fichero_remoto”, que
devuelve un 1 si el archivo figura en la TDV, y 0 en caso contrario).
De lo contrario, se consideraría que se está produciendo un acceso no
legítimo, y se rompería la conexión automáticamente.
En caso de que se trate del acceso de un cliente ordinario, ha de
comprobarse si la dirección está bien formada, para lo cual, se aplica
una expresión regular que podría interpretarse así: “si el parámetro
de PASS no comienza con una letra de la ‘a’ a la ‘z’, ya sea en
mayúsculas o minúsculas, seguida por uno o más caracteres alfabéticos,
un símbolo ‘@’, uno o más caracteres alfabéticos y un punto, y
concluye con otra hilera de cero o más letras, muestra un mensaje de
error”.
elsif($instruccion eq "PASS")
{
if($es_proxy)
{
if(directorios_virtuales::encuentra_fichero_remoto($parametro))
{
$archivo_remoto=$parametro;
mensajes_usuario::muestra_mensaje(CODIGO230,$socket_cliente);
}
else
{
mensajes_usuario::muestra_mensaje(CODIGO501NOPROXY,
socket_cliente);
gestor_conexion::cierra_conexion($socket_cliente);
}
}
else
{
if($parametro !~ /[a-zA-Z](\w*)@(\w+)\.(\w+)/)
{
mensajes_usuario::muestra_mensaje(CODIGO501,$socket_cliente);
}
else
{
mensajes_usuario::muestra_mensaje(CODIGO230,$socket_cliente);
}
}
Si la orden no es “USER” ni “PASS”, se comprueba si se trata de “PWD”,
es decir, “Print Working Directory” (escribe el directorio de
trabajo). Como cualquier otra operación que se aplique sobre la TDV,
esta se remite al módulo de directorios virtuales.
elsif($instruccion eq "PWD")
{
directorios_virtuales::navega(3,$socket_cliente);
}
Aquí, figura el código necesario para entrar en modo pasivo, en el
caso de que la instrucción remitida desde el cliente, así lo ordene.
Lo que la lógica quizás sugiere es que este módulo debería detectar el
envío de una segunda instrucción “PASV”, y debería entonces finalizar
el modo pasivo del servidor. No obstante, en este estado, el servidor
no recibe semejante instrucción. La única manera de regresar al modo
activo es detectar el envío de una orden “PORT” tras un “LS”, es
decir, que el cliente está pidiendo al servidor que envíe los datos de
la lista de directorios a una dirección y un puerto que le suministra
precisamente con la instrucción “PORT”, algo que sólo tiene sentido en
modo activo.
elsif($instruccion eq "PASV")
{
if(!$_modo_pasivo)
{
($_modo_pasivo,$socket_pasivo)=gestor_conexion::modo_pasivo
($socket_cliente);
}
else
{
close($socket_pasivo);
($_modo_pasivo,$socket_pasivo)=gestor_conexion::modo_pasivo
($socket_cliente);
}
}
La instrucción “TYPE”, analizada a continuación, fija el tipo de datos
de una transmisión desde el servidor al cliente. Como ya se ha
descrito anteriormente, esta aplicación considera dos clases: binario
de 8 bits, y ASCII. En el primer caso, la variable global
$_tipo_transferencia, contendrá la letra “I”, y en el segundo, la “A”.
elsif($instruccion eq "TYPE")
{
if($parametro eq "I")
{
$_tipo_transferencia="I";
mensajes_usuario::muestra_mensaje(CODIGO200TYPEI,
$socket_cliente);
}
else
{
$_tipo_transferencia="A";
mensajes_usuario::muestra_mensaje(CODIGO200TYPEA,
$socket_cliente);
}
}
Como ya se ha dicho, si se recibe una instrucción “PORT” significa que
el servidor está en modo activo, o que, si estaba en modo pasivo, debe
salir de él.
elsif($instruccion eq "PORT")
{
if($_modo_pasivo)
{
$_modo_pasivo=0;
close($socket_pasivo);
}
Recuérdese que el parámetro de la instrucción “PORT” tiene la forma
“a1,a2,a3,a4,p1,p2”, donde “a1.a2.a3.a4” forman la dirección IP del
cliente, y el puerto al que el servidor debe conectarse se obtiene
haciendo: (p1*256)+p2.
Pues bien, el código que sigue a continuación, se encarga precisamente
de llevar estas operaciones a cabo. Primero, divide el parámetro en
cuatro partes, correspondiente a los bytes de la dirección IP. Para
ello, se emplea la instrucción “split”, que busca un patrón dentro de
una cadena, y la divide en las subcadenas separadas por dicho patrón.
En el caso concreto que se muestra a continuación, el patrón es la
coma, y la cadena contiene el parámetro de la instrucción “PORT”. Es
decir: “split” divide el parámetro de “PORT” en los trozos que, en la
variable original, estaban separados por comas. Los fragmentos
resultantes de esta división, se almacenan en las posiciones de un
vector (@partes_port, concretamente), de la siguiente manera:
@partes_port[0]  a1
@partes_port[1]  a2
@partes_port[2]  a3
@partes_port[3]  a4
@partes_port[4]  p1
@partes_port[5]  p2
Así se aplica la función:
my @partes_port=split /,/, $parametro;
Ya sólo resta construir la IP, simplemente concatenando los fragmentos
del parámetro de PORT correspondientes a la misma, intercalando un
punto entre cada pareja, y calcular el puerto del cliente al que debe
conectarse el servidor, realizando la operación descrita
anteriormente.
$IP_cliente="$partes_port[0].$partes_port[1].$partes_port[2].
$partes_port[3]";
$puerto_cliente=256*($partes_port[4])+$partes_port[5];
mensajes_usuario::muestra_mensaje(CODIGO200PORT,$socket_cliente);
}
Si en el siguiente fragmento de código se detecta que la orden
transmitida por el cliente es “LS” o “LIST” (son equivalentes), se
muestra el contenido del directorio de trabajo.
En realidad, el proceso implica enviar al cliente los datos de las
entradas de dicho directorio (nombre, fecha de la última modificación,
permisos, tamaño, etc.), de modo que es necesario que se abra un
socket de datos. Si el cliente así lo pide, el servidor entrará en
modo pasivo.
elsif(($instruccion eq "LS") || ($instruccion eq "LIST"))
{
mensajes_usuario::muestra_mensaje(CODIGO150,$socket_cliente);
if($_modo_pasivo)
{
$socket_datos=gestor_conexion::abre_conexion_datos_pasv
($socket_pasivo);
}
else
{
$socket_datos=gestor_conexion::abre_conexion_datos
($IP_cliente,$puerto_cliente);
}
Si todo ha ido bien, y se ha podido abrir sin problemas el socket de
datos, se invoca a la rutina “navega_ls”, del módulo de directorios
virtuales, que se encarga de recorrer el directorio pertinente de la
TDV, y enviar su contenido al cliente, formateado para que pueda
mostrarse en pantalla de un modo fácilmente legible.
if(defined($socket_datos))
{
directorios_virtuales::navega(0,$socket_datos);
close($socket_datos);
}
else
{
Si surge algún problema, la variable que debería contener el handle
del socket de datos no estará definida, así que el flujo de ejecución
entrará en esta rama de la instrucción “if”, en la que se ordena al
módulo de mensajes al usuario, construir uno que informe del error al
cliente.
mensajes_usuario::muestra_mensaje(CODIGO425,$socket_cliente);
}
En cualquier caso, se haya podido abrir correctamente el socket de
datos o no, el proceso termina con el envío al cliente del mensaje
“226 Closing data connection”.
mensajes_usuario::muestra_mensaje(CODIGO226,$socket_cliente);
}
Si la instrucción es “CWD” o “CD” (ambas se admiten), se llama a la
rutina del módulo de directorios virtuales, “navega_cwd”, que se
encarga simular el cambio de directorio, en el caso de que el
parámetro sea correcto, y se refiera a un subdirectorio accesible
desde el actual.
La operación se lleva a cabo en memoria, y no sobre disco, y requiere
posicionar correctamente el puntero que señala al comienzo del
directorio (virtual) de trabajo, en la TDV.
Si el parámetro de la instrucción es “..”, la orden equivale a “CDUP”
(cambiar al directorio padre, en la jerarquía), de modo que se llama a
la rutina pertinente.
elsif (($instruccion eq "CWD") || ($instruccion eq "CD"))
{
directorios_virtuales::navega_cwd($socket_cliente,$parametro);
}
La variable “$instruccion” también puede contener la cadena “CDUP”, en
cuyo caso, se invoca a la rutina “navega_cdup”. Ésta hace que el
puntero de la TDV ascienda al directorio padre (si lo hubiera). Nótese
que “CDUP” es equivalente a “CD ..”, pero se diferencian en que la
primera no recibe parámetros, y la segunda es un caso particular de la
instrucción “CD ”, tratada en el fragmento de código
comentado antes.
elsif ($instruccion eq "CDUP")
{
directorios_virtuales::navega_ls($socket_cliente);
}
Hasta ahora, la gestión de las instrucciones era equivalente a la de
un servidor FTP ordinario. Sin embargo, en el siguiente fragmento de
código, se sugiere una complicación relativamente importante (aunque
no se pone de manifiesto en este módulo, sino en el de directorios
virtuales –véase la rutina “descarga_archivo”, en la página 94).
Se trata de la descarga de ficheros. Hay que tener en cuenta que el
cliente puede acceder al contenido de las estructuras de directorios
de cualquiera de los nodos que integran la red. Esto significa que, si
solicita la descarga de un fichero, en ocasiones éste estará ubicado
en el mismo servidor al que se ha conectado, pero en muchas otras, se
encontrará en el disco duro de un host remoto, de modo que la petición
de descarga debe redirigirse hacia el nodo que contenga físicamente el
archivo; no obstante, conviene tener en mente que ha de ser el
servidor FTP local quien se lo envíe realmente, a través del socket de
datos.
A todos los efectos, el servidor local se comportará como un Proxy32,
conectándose con el servidor remoto como si fuera un cliente más, y
retransmitiendo la información del archivo remoto, conforme va
recibiéndola, hacia el cliente, de un modo totalmente transparente al
mismo, y en tiempo real.
El concepto es parecido al de un repetidor de radio o televisión. La
estación emisora sería el nodo que contiene físicamente el archivo
solicitado por el cliente, y la antena repetidora, el servidor local,
que capta la señal (es decir, la información), y la retransmite al
receptor (o seáse, al cliente). La figura 9 ilustra esta idea.

Figura 9 – Descarga de un archivo ubicado en un tercer nodo
Para descargar un archivo del servidor, el cliente envía la
instrucción “GET” (aunque algunos sistemas utilizan “RETR”).
elsif (($instruccion eq "RETR") || ($instruccion eq "GET"))
{
Antes de continuar, se comprueba que el parámetro de “GET” sea el
nombre de un fichero disponible en el directorio de trabajo actual.
Para ello, se llama a la función “encuentra_fichero”, implementada en
el módulo de directorios virtuales, y que devuelve 1 si se encuentra
el archivo, y 0 en caso contrario. Esto sólo tiene sentido si la
solicitud de descarga viene de un cliente ordinario, y no de un proxy.
En ese caso, se invoca a la rutina “genera_fragmento”, que lee un
bloque de 64 Kbytes del fichero origen.
if(!directorios_virtuales::encuentra_fichero($parametro) &&
!$es_proxy)
{
mensajes_usuario::muestra_mensaje(CODIGO550,$socket_cliente);
}
else
{
if($es_proxy)
{
indice=directorios_virtuales::genera_fragmento($archivo_remoto);
}
my $modo;
if($_tipo_transferencia eq 'A')
{
$modo="ASCII";
}
else
{
$modo="8-BIT BINARY";
}
En este punto, se ha localizado el fichero y se ha determinado el tipo
de transmisión (binario o ASCII), de modo que se procede a abrir el
socket de datos con el cliente (en modo pasivo, si procede).
mensajes_usuario::muestra_mensaje_cp
("150 Opening $modo data connection to send
$parametro",$socket_cliente);
if($_modo_pasivo)
{
$socket_datos=gestor_conexion::abre_conexion_datos_pasv
($socket_pasivo);
}
else
{
$socket_datos=gestor_conexion::abre_conexion_datos
($IP_cliente,$puerto_cliente);
}
if(defined($socket_datos))
{
Aquí se llama a la función “descarga_archivo”, del módulo de
directorios virtuales. Ésta detecta cuándo el fichero solicitado por
el cliente se encuentra en la máquina local, o en un nodo remoto, y
actúa en consecuencia, enviando el fichero según el procedimiento
usual para un servidor FTP ordinario, en el primer caso, o
conectándose al nodo remoto en cuestión, y actuando como un Proxy, en
el segundo.
directorios_virtuales::descarga_archivo
($parametro,$socket_datos,$_tipo_transferencia);
close($socket_datos);
}
else
{
mensajes_usuario::muestra_mensaje(CODIGO425,$socket_cliente);
}
mensajes_usuario::muestra_mensaje(CODIGO226,$socket_cliente);
}
else
{
mensajes_usuario::muestra_mensaje(CODIGO550,$socket_cliente);
}
}
La instrucción “HELP” puede admitir, como parámetro, la instrucción
del servidor sobre la que el usuario desea obtener información
detallada. Si no recibe argumentos, se mostrará la ayuda general.
elsif(($instruccion eq "HELP") || ($instruccion eq "REMOTEHELP"))
{
Para simplificar, se pasa el parámetro a mayúsculas. Recordemos que
los sistemas Unix son sensibles a las mayúsculas y minúsculas, esto
es, la cadena “Ls” es diferente de “LS”.
$parametro=uc($parametro);
if($parametro eq "LS")
{
mensajes_usuario::muestra_mensaje(AYUDA_LS,$socket_cliente);
}
elsif($parametro eq "CWD")
{
mensajes_usuario::muestra_mensaje(AYUDA_CWD,$socket_cliente);
}
elsif($parametro eq "CDUP")
{
mensajes_usuario::muestra_mensaje(AYUDA_CDUP,$socket_cliente);
}
elsif($parametro eq "RETR")
{
mensajes_usuario::muestra_mensaje(AYUDA_RETR,$socket_cliente);
}
else
{
mensajes_usuario::muestra_mensaje(AYUDA_GENERAL,$socket_cliente);
}
}
La rutina “interpreta” termina intentado determinar si la instrucción
remitida desde el cliente es “QUIT”. En caso contrario, significará
que la orden no coincide con ninguna de las admitidas por este
servidor, de modo que sólo restará decidir si se trata de un mandato
estándar, correctamente formado, pero no implementado en el servidor,
o de un error de sintaxis.
Si se trata de la instrucción “QUIT”, se envía un mensaje de despedida
al cliente, y se cierra su socket. Esto pone fin al hijo derivado del
servidor FTP en el gestor de la conexión.
Si la conexión se ha establecido con un proxy, antes de concluir, hay
que borrar el último archivo temporal empleado en la descarga remota.
elsif($instruccion eq "QUIT")
{
if($es_proxy)
{
directorios_virtuales::borra_ultimo_fragmento($indice);
}
mensajes_usuario::muestra_mensaje(CODIGO221,$socket_cliente);
gestor_conexion::cierra_conexion($socket_cliente);
}
Si la instrucción no coincide con ninguna de las admitidas por el
servidor, se llama a la rutina “no_implementada”, que devuelve 1 si la
orden es correcta, dentro de los estándares, pero no ha sido
implementada, y 0 si ni siquiera figura en la lista de no admitidas
(lo que significa que se trata de un error de sintaxis).
elsif(no_implementada($instruccion))
{
mensajes_usuario::muestra_mensaje(CODIGO502,$socket_cliente);
}
else
{
mensajes_usuario::muestra_mensaje(ERROR1,$socket_cliente);
}
}
Subrutina: “no_implementada”.
Entradas: la instrucción remitida por el cliente.
Salidas: 1 si la instrucción está en la lista de las no implementadas,
y 0 en caso contrario.
Quizás el nombre de esta rutina pueda inducir a error:
“no_implementada” devuelve un 1 cuando la instrucción que recibe como
parámetro, pertenece a la lista global “@ordenes_no_implementadas”,
definida al principio de este módulo. Que devuelva un 0 no significa
que la instrucción SÍ esté implementada, sino que NO pertenece a la
mencionada lista. Dado que la función se invoca desde un punto de
“interpreta” en el que se ha descartado cualquier concordancia entre
la instrucción enviada por el cliente y todas las implementadas por el
servidor, su utilidad es la de distinguir entre una orden estándar,
pero no admitida en esta aplicación (“no_implementada” devolvería 1),
y un error de sintaxis (devolvería 0).
sub no_implementada
{
my ($instruccion)[email protected]_;
my $encontrada=0;
En los bucles “for”, en Perl, es posible declarar la variable que
servirá de índice de las iteraciones en la misma inicialización. De
este modo, se define un contexto (scope)33 que abarca solamente al
propio bucle, y evita tener que declarar la variable que hace las
veces de índice, fuera del mismo.
Además, en Perl, todos los arrays tienen una variable asociada, que
alberga en todo momento la posición de su último elemento (que vendría
a ser equivalente a la longitud, menos un elemento, dado que, al igual
que en lenguaje C, los vectores y matrices comienzan a indexarse a
partir de la posición 0). Su nombre coincide con el del array en
cuestión, salvo que se precede por “$#”.
Así, en el siguiente bucle se emplea, como tope de la iteración, la
posición del último elemento de la lista de órdenes no implementadas:
for(my $i=0;$i<$#ordenes_no_implementadas;$i++)
{
if($instruccion =~ /$ordenes_no_implementadas[$i]/)
{
$encontrada=1;
}
last if $encontrada==1;
}
return $encontrada;
}
5.6.5. El sistema de directorios virtuales.
La única forma de conseguir que un cliente, conectado a uno de los
hosts que conforman la red de FTP distribuido, pueda acceder al
contenido del disco duro de cualquiera de ellos, sin recurrir a un
complejo sistema de comunicaciones, es manteniendo en la memoria de
cada uno de ellos la estructura de directorios de todos los demás. Es
decir: la Tabla de Directorios Virtuales.
Ahora bien: almacenar esta información en memoria no es tan sencillo
como crear una tabla, y guardar en ella los nombres de los ficheros y
directorios remotos, junto con sus atributos (fecha, tamaño,
permisos…). Para conseguir que el sistema sea transparente al usuario,
es decir, que ningún cliente FTP ordinario pueda distinguir entre el
sistema distribuido, y uno corriente, es necesario que los usuarios
tengan la posibilidad de navegar libremente a través de esta
estructura virtual de directorios, empleando para ellos las mismas
instrucciones que utilizarían si la estructura se encontrara
físicamente en disco, y obteniendo exactamente los mismos resultados.
Precisamente este es el cometido del módulo de directorios virtuales:
no sólo construir la TDV a partir de los archivos y directorios en el
disco local, sino “engañar” a los clientes FTP, haciéndoles creer que
están accediendo a una estructura de directorios almacenada íntegra y
físicamente en el servidor.
Junto con el gestor de la coherencia, este módulo es uno de los puntos
neurálgicos de la aplicación, ya que marcan claramente la diferencia
entre un servidor FTP corriente (pocos de los fragmentos del código
comentado hasta ahora sugerirían que no estamos ante uno), y uno
perteneciente a uno de los nodos de la red de FTP distribuido.
Este es el cuerpo principal del módulo:
package directorios_virtuales;
use File::Basename;
use File::stat;
use Time::localtime;
use POSIX qw(strftime);
use FileHandle;
use variables_globales qw($path);
Aquí se encuentran las variables del programa principal, y algunas de
las globales, como “$puntero_tabla”, que actúa como índice para
navegar a través de la TDV, y la propia tabla de directorios
virtuales, almacenada en “@TabDirVir”.
También se declara la tabla global “@TablaNodos”, que contiene una
relación de los nodos remotos, con sus direcciones IP y el número que
identifica a los subdirectorios virtuales en los que se almacenan sus
contenidos. Y es que las TDVs remotas no “cuelgan” directamente del
directorio raíz local, sino dentro de subdirectorios en memoria.
Dentro de cada uno de esos subdirectorios, “DIR1” hasta “DIRn”, se
encuentra el contenido de las tablas de sendos nodos remotos.
my $base;
my $sufijo;
my @directorio;
our @TabDirVir;
my $df;
our $puntero_tabla;
our @TablaNodos;
Respecto a la tabla de nodos, se crea la primera vez que el servidor
FTP recibe una TDV remota, y debe unirla a la local. En ese instante,
obtendrá la información pertinente acerca del host que le ha remitido
el contenido de su estructura virtual de directorios, y la añadirá a
la “@TablaNodos”.
De esta manera, si el flujo de ejecución pasa por este punto y no
detecta la presencia del fichero “tabnodsrv.txt” –que contiene una
copia de seguridad de la tabla de nodos, para impedir que cualquier
problema técnico cause la pérdida de los datos-, significará que este
host aún no tiene constancia de la existencia de ningún otro nodo.
La táctica es muy sencilla: se intenta abrir el fichero
“tabnodsrv.txt” en modo lectura. Si la variable “$abre” no está
definida después de invocar al método “open”, es porque el archivo en
cuestión no existe todavía. No obstante, si la variable “$abre” está
definida, el fichero “tabnodsrv.txt” si existirá, y podrá cargarse en
memoria.
Para ello se emplea un bucle en el que se lee línea a línea (empleando
el método “getline”, asociado al handle del fichero, “TABNOD”).
$abre=open TABNOD," if(defined($abre))
{
for(my $i=0; $_=TABNOD->getline; $i++)
{
chomp($_);
$TablaNodos[$i][0]=$_;
$_=TABNOD->getline;
chomp($_);
$TablaNodos[$i][1]=$_;
}
close TABNOD;
}
“$path” es una variable importada desde el módulo “variables_globales”
y contiene el camino a partir del cual, se genera la TDV. Para leer su
contenido y almacenarlo en la lista “@directorio”, se emplea el método
“readdir” que, en ciertos sentidos, funciona de un modo bastante
similar a las rutinas para leer ficheros. De hecho, también se
gestiona mediante un descriptor (llamado, en este caso, “DIRECTORIO”).
Sin embargo, para que el método funcione correctamente, el path no
debe terminar con una barra de separación de directorios, de manera
que se elimina mediante “chop”.
Nótese, por cierto, que muchos de los métodos predefinidos en Perl,
actúan sobre sus parámetros por referencia, a diferencia del lenguaje
C. Según las reglas de éste, una llamada a una función equivalente a
“chop”, es decir, que tome una cadena, elimine su último elemento, y
devuelva el mismo, tendría una apariencia similar a esta (perdón por
la burda castellanización del verbo chop):
elemento_chopeado=chop(cadena_original,cadena_chopeada);
Semejante sintaxis puede inducir a error a muchos programadores
formados en lenguaje C. En Perl, el parámetro de “chop” es de entrada
y salida . El método lo lee, y devuelve el resultado en la misma
variable, y en el mismo punto, de modo que no es necesario declarar
una variable auxiliar para almacenar la salida de la operación.
$path_sinbarra=$path;
chop($path_sinbarra);
opendir(DIRECTORIO,$path_sinbarra);
@directorio=readdir(DIRECTORIO);
print "Generating Directory Table .";
Ahora, se llama a la rutina “genera_tabla_directorios”. Ésta se
encarga de construir la TDV en memoria, a partir de los datos de la
estructura de directorios en el disco duro.
@TabDirVir=genera_tabla_directorios($path,@directorio);
$puntero_tabla=0;
print "\nOk.\n";
mensajes_usuario::registra("VDT successfully generated");
La TDV obtenida se almacena en el fichero “localVDT.txt”. Éste será el
que el gestor de la coherencia emplee para generar los mensajes de
actualización en XML, que enviará a los demás nodos cuando sea
preciso.
Cada vez que arranca la aplicación, y se llama a este módulo, uno de
los primeros pasos que se dan consiste en generar la TDV de nuevo.
Así, si la aplicación se detiene y el nodo se desconecta para realizar
tareas de mantenimiento y modificar la estructura de directorios
(añadiendo y eliminando ficheros, por ejemplo), cuando el host vuelva
a ponerse en funcionamiento, volverá a construir la tabla virtual, y
la escribirá en el mencionado archivo. Enviarla a los demás nodos es
misión del gestor de la coherencia.
En el siguiente fragmento de código se emplea otra de las sintaxis
posibles para los bucles “for”, y que consiste en declarar la variable
que servirá de índice de las iteraciones, justo después de la palabra
clave “for”, y a continuación, definir su intervalo de variación, de
esta forma:
for ( .. )
Así:
open FICHERO, ">localVDT.txt";
for $i (0 .. $#TabDirVir)
{
for $col(0 .. 8)
{
print FICHERO "$TabDirVir[$i][$col]\n";
}
print FICHERO "\n";
}
close(FICHERO);
closedir(DIRECTORIO);
Subrutina: “genera_tabla_directorios”.
Entradas: El path hasta el directorio de trabajo y una lista que
contiene todas las entradas del mismo.
Salidas: La tabla de directorios virtuales generada a partir del
directorio de trabajo actual.
Esta subrutina es recursiva34. Genera las entradas de la TDV para cada
entrada del directorio de trabajo actual, y vuelve a invocarse cada
vez que se encuentre un subdirectorio.
El flujo de ejecución avanza, por tanto, siguiendo la estructura de
directorios, y uniendo las TDVs locales, generadas para cada uno de
ellos, hasta formar la tabla de la estructura general.
La siguiente figura ilustra este concepto:

Figura 10 – Esquema de la generación de la TDV
En la ilustración, cuando la línea de flechas gris avanza hacia la
derecha y hacia abajo, se debe a llamadas recursivas a la subrutina
“genera_tabla_directorios”, y cuando avanza hacia la izquierda y hacia
arriba, se trata de los retornos de tales llamadas. Cada retorno lleva
consigo una TDV local, que se une a la devuelta por la llamada
anterior.
sub genera_tabla_directorios
{
my ($path_local,@directorio)[email protected]_;
my @TDV; # Tabla de directorios virtuales
my $df;
my $i;
my $cambiar;
my @TabTemp;
La siguiente línea no tiene ninguna utilidad práctica; simplemente,
imprime en pantalla un punto por cada directorio recorrido, es decir,
por cada llamada recursiva a la subrutina. Así, el administrador sabe
que se está procesando la estructura de directorios, ya que podrá ver
una línea de puntos avanzando en la pantalla.
print ".";
$i=0;
$j=0;
Respecto a las tres siguientes líneas, conviene recordar que el camino
(path) completo de cada entrada de la tabla de directorios virtuales,
no tiene por qué coincidir con el que esos mismos ficheros y
directorios tienen en el disco duro. De hecho, en la mayoría de los
casos, son diferentes.
La primera de las siguientes líneas de código, almacena en la variable
auxiliar “$path_tmp” el contenido del camino local, es decir, del path
hasta el subdirectorio que se está procesando según el recorrido que
se ilustra en la figura 10.
La segunda, elimina la parte correspondiente al path que podríamos
llamar “raíz”, es decir, a partir del cual comienza a montarse el
sistema de directorios del servidor FTP y que, por tanto, no debe
aparecer en la tabla. Esto se consigue haciendo uso de la expresión de
sustitución (el operador es “=~s”). Responde a esta sintaxis:
=~s///
… y su funcionamiento es bien sencillo: busca el
dentro de y lo cambia por . El
resultado, se almacena en la misma . Si lo que se pretende
es eliminar un cierto patrón dentro de la expresión, no hay más que
dejar en blanco el .
Por fin, la tercera línea añade el separador de directorios (la barra
“/”; a todos los efectos, es como si el nuevo camino comenzara en el
directorio raíz) al principio del path obtenido, mediante el operador
punto.
my $path_tmp=$path_local;
$path_tmp=~s/$path//;
$path_tmp="/" . $path_tmp;
El siguiente bucle procesa todos los elementos de la lista de entradas
del directorio actual:
while($i<=$#directorio)
{
La instrucción “stat”, aplicada a un nombre de fichero o directorio,
devuelve los atributos que éste tenga asociados (tamaño, fecha de la
última modificación, permisos, etc…). Nótese que, sin embargo, ese
nombre de fichero o directorio debe tener el camino completo, y no el
de la TDV, ya que ésta se genera a partir de las entradas del disco
duro.
De este modo, si el camino completo es algo como:
/home/users/angeldv/proyecto/source.pl
… y el sistema de directorios que se almacena en la TDV comienza a
montarse desde:
/home/users/angeldv
En la tabla, el camino se almacenará como:
/proyecto/source.pl
Pero para obtener los atributos del archivo, es necesario usar como
parámetro el camino completo:
/home/users/angeldv/Desktop/source.pl
… que es como consta en el disco duro.
$df=stat($path_local.$directorio[$i]);
Algunas de las entradas de la lista de directorios pueden ser
problemáticas; especialmente, las que no son sino referencias a
archivos ubicados en otro lugar del disco duro local. Si se
pretendiera aplicar la función “stat” sobre ellos, no se obtendría
ningún valor (lo que en Perl se representa asignando a la variable
correspondiente, el término “undef”). Para evitar que esto provoque
errores en tiempo de ejecución, mientras se está generando la TDV,
sólo se permite que los atributos del fichero que se está procesando
pasen a la tabla en el caso de que el resultado de aplicar al mismo la
función “stat”, esté definido.
if(defined($df))
{
… pero la criba no termina aquí. Evidentemente, ninguno de los
ficheros que necesita el sistema para su funcionamiento puede figurar
en la TDV. Eso los pondría al alcande de cualquier cliente FTP.
Es de suponer que a ningún administrador se le ocurrirá la genialidad
de poner a disposición del público el mismo directorio en el que se
almacenan los archivos necesarios para el funcionamiento del servidor,
pero nunca está de más incluir medidas de seguridad adicionales.
Así que, antes de dar de alta una nueva fila en la tabla, se comprueba
que el fichero que se está procesando no es ninguno de los de los de
“consumo interno”.
my $nombre=basename($directorio[$i]);
if(($nombre ne "localVDT.txt") && ($nombre ne "mypid.txt") &&
($nombre ne "NodesTable.txt") && ($nombre ne "tabnodsrv.txt") &&
($nombre ne "ResponseTimes.txt") && ($nombre ne "VDTdata.txt")&&
($nombre !~ m/\Amessage[0-9]\.xml/) && ($nombre ne "CMpid.txt") &&
($nombre !~ m/\AResponse([0-9]+)\.txt/) &&
($nombre !~ m/\APending([0-9]+)\.txt/) &&
($nombre !~ m/\ASRV(.*)\.log/) && ($nombre !~ m/\ACM(.*)\.log/))
{
$TDV[$j][0]=$path_tmp.$directorio[$i];
$TDV[$j][2]=$df->size;
Para obtener la fecha de la última modificación, “stat” ofrece el
atributo “mtime”. Sin embargo, el contenido del mismo no es
especialmente inteligible, ya que se trata nada menos que del número
de segundos transcurridos desde lo que se conoce como la “Época” (y
que en contra de lo que tan contundente término puede sugerir, se
trata de una fecha a priori bastante anodina: el 1 de enero de 1970).
Para que se pueda mostrar en pantalla una fecha legible (día del mes y
de la semana, año, hora…), se aplica a este resultado la función “ctime”.
$TDV[$j][3]=ctime($df->mtime);
Como se describe en la sección 5.1 - La Tabla de Directorios Virtuales,
en la página 16, la cuarta columna de la TDV contiene el identificador
del propietario del bloque de datos. Para ahorrar espacio, la primera
entrada del bloque contendrá la dirección IP del host en el que éstos
se encuentran almacenados físicamente (o “localhost” si se trata de la
máquina local), y en el resto, habrá un 0.
if(!$j)
{
$TDV[$j][4]="localhost";
}
else
{
$TDV[$j][4]="0";
}
El atributo “mode” contiene tanto los permisos como el tipo de fichero
y dado que lo que se pretende almacenar son los primeros, se aplica
una máscara sobre el resultado para poner a cero el primer byte (que
es el que alberga el tipo de fichero), y mantener el resto (que son
los que contienen los permisos); es decir, se hace un “AND” lógico
entre el contenido de “mode” y 0000 1111 1111 1111, o lo que es lo
mismo, en decimal, 07777.
$TDV[$j][5]=(($df->mode) & 07777);
$TDV[$j][6]=$df->nlink;
$TDV[$j][7]=$df->uid;
$TDV[$j][8]=$df->gid;
$j++;
}
}
$i++;
}
my $limite=$j;
Ahora, el algoritmo debe identificar los subdirectorios, para que
continúe el proceso recursivo a través de ellos. Es importante
distinguir entre los directorios ordinarios, y los punteros al
directorio local, y al padre (representados mediante “.” y “..”,
respectivamente). Si el flujo de ejecución tratara de analizar el
contenido de “.”, como es un puntero al propio directorio local, el
proceso entraría en un bucle infinito.
Para evitarlo, el procesado comenzará después de las dos primeras
entradas de la TDV local (que se marcan, “manualmente” como
directorios, simplemente almacenando un “0” en la segunda columna de
la primera y la segunda fila).
if($i>1)
{
$TDV[0][1]=0;
$TDV[1][1]=0;
Este es el bucle interno encargado de recorrer la TDV en busca de
subdirectorios. Como se puede apreciar, el índice comienza en “2”, es
decir, ignorando los punteros “.” y “..”. El límite superior de las
iteraciones se fija en función del número de entradas del directorio
actual.
for(my $j=2; $j<$limite; $j++)
{
my $path_sb=$path;
chop($path_sb);
La función “basename” toma un path completo, y extrae el nombre del
fichero al final del mismo. Por medio de la siguiente comparación, se
garantiza completamente que no se procese ninguna entrada cuyo nombre
sea “.” o “..” (teóricamente, estos punteros siempre aparecen en los
dos primeros lugares de la lista de contenidos de un directorio; sin
embargo, y como medida de seguridad, aquí se efectúa una vez más la
comprobación).
Para detectar si la entrada es un directorio, se le aplica la función
“opendir”. Ésta devuelve “undef” si el parámetro recibido no se
corresponde con un nombre válido de directorio. Ergo, si estamos ante
un fichero ordinario, la variable “$cambiar” contendrá “undef”. En
caso contrario, habremos localizado un directorio correcto.
---------------------------------------------------------------------
if(basename($TDV[$j][0]) ne "." && basename($TDV[$j][0]) ne "..")
{
$cambiar=opendir(DIR_TMP,$path_sb.$TDV[$j][0]);
}
else
{
$cambiar=0;
}
Si la variable “$cambiar” está definida, se trata de un subdirectorio,
así que se puede invocar recursivamente a la rutina “genera_tabla_directorios”,
para que lo recorra.
if($cambiar)
{
$TDV[$j][1]=0;
$path_local=$path;
chop($path_local);
$path_local=$path_local.$TDV[$j][0]."/";
@directorio=readdir(DIR_TMP);
@TabTemp=genera_tabla_directorios($path_local,@directorio);
El resultado de esa llamada recursiva se almacena en la tabla @TabTemp.
Mediante el siguiente fragmento de código, se añade a la TDV local. De
esta forma, conforme las llamadas vayan retornando, las tablas
obtenidas en los distintos subdirectorios se irán uniendo unas a
otras, hasta que, cuando la ejecución regrese de la primera llamada a
la rutina, el resultado que se obtendrá será la tabla de directorios
virtuales de todo el árbol de directorios del servicio FTP.
my $k=$#TDV+1;
for $l (0 .. $#TabTemp)
{
for $col (0 .. 8)
{
$TDV[$k+$l][$col]=$TabTemp[$l][$col];
}
}
closedir(DIR_TMP);
}
else
{
$TDV[$j][1]=1;
}
}
}
return @TDV;
}
Subrutina: “navega_ls”.
Entradas: ninguna.
Salidas: ninguna.
Envía al cliente una lista con el contenido del directorio de trabajo,
formateada para que pueda mostrarse limpiamente en pantalla.
Recordemos que la tabla de directorios virtuales está alojada en
memoria, luego se trata de recorrer una matriz de datos, y no de
determinar el contenido de una parte concreta del disco duro local.
Por lo tanto, es necesaria alguna táctica que permita detectar dónde
comienza y dónde acaba un directorio, dentro de esa tabla en memoria.
La solución que sigue esta subrutina consiste en analizar el path de
las entradas de la TDV, a partir del índice “puntero_tabla” (que, no
lo olvidemos, siempre señala la posición que ocupa en la tabla el
comienzo del directorio de trabajo actual), y recorrerla mientras se
mantenga sin cambios. En el momento en que el path varía, significará
que se ha abandonado el directorio que se estaba procesando.
Entre las variables que se declaran al comienzo de la rutina, destaca
por su interés “$path_actual”, que almacena el resultado de aplicar la
función “dirname” a la entrada de la tabla correspondiente a la fila “$puntero_tabla”,
y la primera columna, esto es, la fila asociada al directorio de
trabajo actual, y la columna que alberga el nombre de la primera
entrada de dicho directorio.
”dirname” es la función complementaria de “basename” (comentada en la
rutina “genera_tabla_directorios”). Recibe una entrada del sistema de
directorios, y devuelve exclusivamente el path, esto es, descarta el
nombre del fichero al final del mismo.
En la rutina que nos ocupa, esto es especialmente útil, dado que la
estrategia a seguir consiste en determinar cuándo el path base cambia,
para detectar el instante en que el recorrido de la TDV abandona el
directorio de trabajo.
sub navega_ls
{
my ($socket_cliente)[email protected]_;
my $path_actual=dirname($TabDirVir[$puntero_tabla][0]);
my $path_temporal=$path_actual;
my $indice=$puntero_tabla;
my $nombre_base;
El límite superior del siguiente bucle es el propio tamaño de la TDV.
Siempre cabe la posibilidad de que el directorio del que el usuario ha
solicitado el contenido, sea el último de la tabla.
while($indice <= $#TabDirVir)
{
La variable “$path_temporal” contiene el camino del directorio actual.
Cuando cambie, es decir, cuando sea diferente a “$path_actual”, se
interpretará como que el recorrido de la TDV ha salido del directorio
de trabajo, y el proceso debe terminar. Si los contenidos de ambas
variables coinciden, se envía al cliente la entrada actual del
directorio de trabajo.
if($path_actual eq $path_temporal)
{
A continuación, se almacenan los atributos de interés para el usuario.
$perms=$TabDirVir[$indice][5];
$nlink=$TabDirVir[$indice][6];
$user=$TabDirVir[$indice][7];
$group=$TabDirVir[$indice][8];
El siguiente paso es determinar el tipo de entrada. Si la segunda
columna de la fila pertinente de la TDV contiene un 0, estamos ante un
directorio. Y si almacena un 1, será un fichero. Esto es importante a
la hora de mostrar el listado, ya que los directorios incluyen una “d”
al principio de su cadena de datos, a la hora de imprimirse en
pantalla, y los ficheros, comienzan con un guión (más adelante, puede
encontrarse un ejemplo).
if($TabDirVir[$indice][1])
{
$tipo="-";
}
else
{
$tipo="d";
}
La fecha almacenada en la cuarta columna de la TDV, se ajusta a un
formato en el que se incluyen el día de la semana, el nombre del mes,
y la hora, minutos y segundos de la última modificación del archivo en
cuestión. Esta exactitud, que puede ser muy útil a la hora de
garantizar que dos archivos son iguales (no tiene por qué ocurrir así,
simplemente porque sus nombres –e incluso sus tamaños- coincidan),
hace, sin embargo, que el listado del contenido de un directorio en la
pantalla del cliente, ocupe demasiado espacio horizontal, lo que
termina afectando a la legibilidad del resultado.
Para conseguir que el listado sea más agradable a la vista, las tres
líneas siguientes eliminan el día de la semana, los segundos en la
hora, y sustituyen el nombre del mes por su número de orden (de 1 a
12).
my $solo_fecha=$TabDirVir[$indice][3];
La siguiente expresión regular sustituye cualquier aparición de una
hora con el formato: HH:MM:SS, en una como HH:MM.
Se busca un patrón que coincida con el modo clásico de representación
de la hora de modificación de un archivo: tres números de uno o dos
dígitos, separados por dos puntos “:”. Al agruparse mediante
paréntesis, internamente se asocian con las variables $1, $2 y $3, si
son reconocidos por el parser. De este modo, sustituir la cadena
original por $1:$2, implica dejar fuera el tercer término ($3), es
decir, el correspondiente a los segundos.
$solo_fecha=~s/([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})/$1:$2/;
Ahora, se elimina cualquier aparición de las tres letras iniciales del
nombre de la semana.
En la siguiente expresión regular, los grupos de tres caracteres
aparecen separados por la barra vertical. Este es el operador de
disyunción. La expresión puede leerse así:
”Sustituir cualquier aparición de las letras ‘Mon’, o ‘Tue’, o ‘Wed’,
o ‘Thu’, o ‘Fri’, o ‘Sat’, o ‘Sun’, por nada” (que es lo mismo que
eliminarlas).
$solo_fecha=~s/Mon|Tue|Wed|Thu|Fri|Sat|Sun//;
Por fin, se envían al cliente los datos del fichero listado. La
función “printf” empleada a tal efecto, se comporta de un modo análogo
a su homónima en el lenguaje C. Obsérvese que recibe una serie de
parámetros cuyo formato se especifica en la hilera de símbolos
encerrados por comillas.
Cada formato se representa mediante el carácter de tanto por ciento,
seguido de una letra, (que, además, en ocasiones está precedida por un
número, que determina la longitud del campo en cuestión).
Cada parámetro adoptará el formato especificado por el código en la
posición correspondiente, tal y como muestra el siguiente ejemplo:
printf(“c1 c2 c3 … cN”,p1,p2,p3 … pN);
Donde, los ci son códigos que denotan un formato imprimible (cadenas,
números enteros...), y los pi son las expresiones que se evaluarán, y
cuyo resultado pasará por el filtro del formato i-ésimo, para ser
impresas.
Así, p1 se asociará con el formato c1; p2, con el c2; p3 con el c3,
etc…
En el caso que nos ocupa, el primer formato es “%s” (que se refiere a
una cadena), así que su parámetro asociado será, por tanto, el primero
en la lista de argumentos: “$tipo”.
Resumiendo: el contenido de la variable “$tipo” se imprimirá en primer
lugar, y con el formato de una cadena de caracteres.
Los tres siguientes puestos corresponden a los permisos. En los
sistemas Unix y relacionados, todos los ficheros y directorios tienen
tres grupos de permisos asociados: propietario del archivo, grupo de
usuarios, y resto de usuarios. Cada uno de estos grupos puede, a su
vez, tener hasta tres permisos diferentes: de lectura (“r”), de
escritura (“w”) y de ejecución (“x”)35.
Por ejemplo, si un fichero determinado puede ser leído, escrito y
ejecutado por su propietario, leído y escrito, solamente, por el grupo
de usuarios, y exclusivamente leído por el resto de los usuarios, la
hilera de permisos tendría esta apariencia: rwxrw-r--, donde los
guiones simbolizan permisos no concedidos.
Pues bien, para mostrar esta información de un modo inteligible, se
recurre a la aplicación de una instrucción condicional compacta, y una
serie de máscaras.
La primera equivale a condensar una orden “if” en una sola línea, con
la siguiente sintaxis:
( ? : )
… que se lee así: si el resultado de evaluar es cierto (o
distinto de cero), ejecútese la . En caso contrario,
ejecútese la . Pues bien: los permisos se almacenan en 3
bytes; cada uno de ellos corresponderá a uno de los 3 bloques ya
mencionados (propietario, grupo y resto de usuarios). Para evaluar el
contenido de un permiso, exclusivamente, se recurre al uso de máscaras
que mantienen el valor del permiso que se está analizando, y pone a
cero el resto. Así, para comprobar el valor del primer permiso (que
sería el de lectura, para el propietario del archivo), se aplica un “AND”
lógico de la variable que contiene todos los permisos, y el número “0400”.
Ahora: nótese que “0400”, en binario, se escribe:
0000 0000 0000 0100 0000 0000 0000 0000.
Los primeros ocho ceros se aportan, únicamente, legibilidad. Podemos
descartarlos, pues. Tendremos, entonces, el número binario:
0000 0100 0000 0000 0000 0000
… es decir, 3 bytes (24 bits). Recordemos ahora que la operación “AND”
lógica es tal que su resultado es siempre 0, a menos que los dos
operandos valgan 1. Esto es, al aplicar la máscara sobre el contenido
de $perms, mediante “AND”, se pondrán a 0 todos los bits, salvo el
situado en la posición del 4, en el número decimal 400. Ese, se
mantendrá. Si tenía originalmente el valor 0, al hacer 0 AND 1, el
resultado será 0. Y si, por el contrario, se trataba de un 1, la
aplicación de la operación 1 AND 1, dará como resultado 1.
Pues bien: precisamente, el bit correspondiente al permiso de lectura
para el propietario del archivo, ocupa la posición del 4 en el número
400. Por tanto, si se enmascara el resto de los bits, poniéndolos a 0,
y se mantiene el valor de éste en concreto, se puede determinar si el
permiso está concedido (contenía un 1), o no (un 0).
Con estos datos, la lectura de la instrucción condicional compacta es
mucho más sencilla, y viene a ser algo como:
(<¿está concedido el permiso?> ? : “-“>)
El razonamiento se aplica a los demás bits de la variable “$perms”,
para obtener el resto de los permisos del archivo.
$socket_cliente->printf
("%s%s%s%s%s%s%s%s%s%s%4d %8s %8s %8d %s %s\r\n",
$tipo,
($perms & 0400 ? 'r' : '-'),
($perms & 0200 ? 'w' : '-'),
($perms & 0100 ? 'x' : '-'),
($perms & 040 ? 'r' : '-'),
($perms & 020 ? 'w' : '-'),
($perms & 010 ? 'x' : '-'),
($perms & 04 ? 'r' : '-'),
($perms & 02 ? 'w' : '-'),
($perms & 01 ? 'x' : '-'),
Los siguientes atributos son el número de enlaces (almacenados en la
variable “$nlink”, que se imprimen de acuerdo al formato “%4d”, es
decir, como un número de 4 dígitos de longitud), el identificador de
usuario (formato “%8s”, que es una cadena de 8 caracteres de
longitud), el de grupo (otro tanto), el tamaño del archivo, tomado
directamente de la tercera columna de la TDV, y formateado para que no
supere los 8 dígitos (lo que permite mostrar el tamaño real de
archivos de hasta 99.999.999 bytes), la fecha recortada como se
describió antes, y el nombre del fichero (tomado también de la TDV;
concretamente, de la primera columna, y aplicándole la función “basename”,
para excluir el path).
$nlink,
$user,
$group,
$TabDirVir[$indice][2],
$solo_fecha,
basename($TabDirVir[$indice][0]));
}
Se sigue recorriendo el directorio de trabajo…
$indice++;
Y sólo si seguimos dentro de los límites de la TDV, se intenta obtener
el path del nuevo archivo de dicho directorio. Si no se llevara a cabo
la siguiente comprobación, cabría la posibilidad de que se estuviera
mostrando el contenido del último directorio de la tabla, de manera
que, si ya se hubieran recorrido todos los archivos de éste, al
intentar aplicar la función “dirname” sobre una posición que queda
fuera de la TDV, se produciría un error en tiempo de ejecución.
if($indice <= $#TabDirVir)
{
$path_temporal=dirname($TabDirVir[$indice][0]);
}
}
print $socket_cliente "\r\n";
}
Subrutina: “navega_cwd”
Entradas: el nombre del directorio al que se va a cambiar y el handle
del socket del cliente.
Salidas: ninguna.
Esta rutina implementa la operación de cambio de directorio (“change
working directory”). Busca en la TDV el directorio al que el usuario
pretende cambiar. Si lo encuentra, mueve el puntero de la tabla hasta
su primera entrada. En caso contrario, se enviará al cliente un
mensaje de error.
Hay que tener en cuenta que, dado que en la TDV, los nombres de los
ficheros y directorios tienen el path completo, será necesario
añadirlo al nombre del directorio al que se quiere cambiar. De lo
contrario, la rutina no lo encontraría, aún cuando estuviera escrito
correctamente y colgara del directorio de trabajo actual.
sub navega_cwd
{
my ($nombre_dir,$socket_cliente)[email protected]_;
my $path_actual=dirname($TabDirVir[$puntero_tabla][0]);
my $nuevo_path=$path_actual;
my $masde1=0;
my @componentes;
my $iteraciones;
my $puntero_tabla_aux=$puntero_tabla;
Mediante la siguiente instrucción condicional, se evita que los
caminos de los directorios comiencen con una doble barra “//” (un poco
más adelante se detalla esta idea).
if($nuevo_path eq "/")
{
$nuevo_path="";
}
El primer paso a dar consiste en comprobar si el usuario está
intentando cambiar al directorio raíz (es decir, si ha enviado la
orden: “cwd /”). Este es el caso más sencillo: consiste en poner el
puntero de la TDV a cero, lo que equivale a colocarlo de modo que
señale a la primera posición de la tabla, esto es, a la raíz de la
estructura virtual de directorios.
if($nombre_dir eq "/")
{
$puntero_tabla=0;
mensajes_usuario::muestra_mensaje(CODIGO250RAIZ,$socket_cliente);
}
else
{
Para que la rutina pueda colocar el puntero de la TDV en la entrada
correspondiente al nuevo directorio, es necesario que encuentre antes
el path completo hasta el primer elemento de dicho directorio (que no
es otro que el “puntero a sí mismo”, esto es, el punto “.”).
Por ejemplo, si el directorio actual es /trabajo/proyecto, y el
usuario intenta cambiar al directorio “perl”, habrá que colocar el
puntero de la TDV, de manera que señale a la posición que ocupa el
elemento “/trabajo/proyecto/perl/.”.
También es necesario comprobar si el path introducido por el usuario,
es relativo o absoluto. Se considera absoluto si comienza con el
separador de directorios (la barra “/”).
Veamos un ejemplo: en un sistema Unix / Linux, la instrucción “cwd
/trabajo/proyecto” se interpreta como que se debe cambiar, primero al
directorio raíz, y desde ahí, a “trabajo/proyecto”. Hay que tener en
cuenta que en la TDV, los caminos se almacenan comenzando con la barra
“/” (es decir, como paths absolutos), de modo que si se intentara
cambiar a “/lib/Net/FTPServer” desde otro punto de la tabla, si no se
tomaran las precauciones adecuadas, el path construido por esta rutina
sería “//lib/Net/FTPServer”, lo que llevaría a que el camino no se
encontrara en la TDV, aún cuando fuera correcto. Este es el motivo por
el que, al principio de esta rutina, se comprueba si el directorio
actual es el raíz y, en caso afirmativo, la variable pertinente se
deja vacía.
A continuación, se utiliza una expresión regular para determinar si el
path recibido en la instrucción remitida por el cliente comienza con
una barra de separación de directorios (esto es: debe considerarse
como un path absoluto). Para ello, se aplica el símbolo “\A”, que
fuerza a que sólo se reconozca el patrón si aparece justo al principio
de la cadena que se está analizando.
Nótese que la barra de separación de directorios en la expresión
regular, debe “escaparse”, esto es, ir precedida de una barra
invertida “\”, para evitar que el parser de Perl la interprete como la
que delimita el fin de la expresión regular (en ese caso, dado que
aparecen dos consecutivas, se produciría un error en tiempo de
compilación).
if($nombre_dir =~ /\A\//)
{
$puntero_tabla_aux=$puntero_tabla;
$puntero_tabla=0;
$path_actual=dirname($TabDirVir[$puntero_tabla][0]);
$nombre_dir=substr($nombre_dir,1);
$nuevo_path="";
}
Si el path tecleado por el usuario, está formado por más de un
directorio, se divide en sus componentes, que se almacenan en una
lista. Entonces, basta con aplicar la parte de la rutina que se
encarga de cambiar a un directorio concreto, a cada uno de esos
componentes, de modo que se vaya avanzando en el path, directorio a
directorio.
Si, por ejemplo, el camino es “trabajo/proyecto/perl”, la rutina se
encarga de dividirlo en los tres componentes: “trabajo”, “proyecto” y
“perl”. Ya sólo resta aplicar el código necesario para cambiar a “trabajo”;
volver a aplicarlo, entonces, desde ese directorio, para cambiar a “proyecto”;
y aplicarlo una tercera vez, para cambiar a “perl”.
Será, por tanto, necesario comprobar si el usuario intenta cambiar a
un directorio que está a más de uno de distancia. Es decir, si, por
ejemplo, partiendo de “trabajo”, se intenta cambiar a “trabajo/proyecto”
(1 directorio de distancia), o a “trabajo/proyecto/perl” (2
directorios de distancia).
Para ello, se recurre a otra sencilla expresión regular que comprueba
si la cadena que contiene el nombre del directorio de destino contiene
al menos un separador de directorios. En ese caso, no hay equivocación
posible: el destino está a más de 1 directorio de distancia.
La expresión puede leerse así: “buscar un patrón compuesto por 1 ó más
caracteres, seguidos por una barra de separación de directorios (‘/’),
y ésta, por 1 ó más caracteres”.
En Perl, la expresión regular para denotar “una o más repeticiones”
es, como ya se ha mencionado, el símbolo “+”, y la que concuerda con
“cualquier carácter”, es el punto.
De esta manera, algo como “trabajo/proyecto”, concuerda con el patrón.
Tenemos una serie de uno o más caracteres, seguidos por una barra de
separación de directorios, a la que, a su vez, le siguen uno o más
caracteres. Cualquier path compuesto por más de un directorio,
concordará con el patrón.
if($nombre_dir =~ m/(.+)(\/)(.+)/)
{
Recuérdese el funcionamiento del método “split”: divide una cadena en
una serie de componentes, atendiendo a un criterio de división
especificado mediante un patrón que se pasa como parámetro, delimitado
por dos barras “/”.
@componentes=split /\//,$nombre_dir;
$iteraciones=$#componentes;
}
else
{
En caso contrario, es decir, si el parámetro de la instrucción “cwd”
no contiene ningún separador de directorios, estaremos ante un cambio
de directorio simple. Sólo será necesaria, por tanto, una iteración
del bucle que aparece seguidamente.
$iteraciones=1;
}
Este es el bucle en el que se procesa el parámetro de la instrucción “cwd”.
En caso de que esté formado por varios directorios, se irá cambiando a
cada uno de ellos, secuencialmente.
for(my $i=0;$i<=$iteraciones;$i++)
{
En cada iteración, se construye el nuevo path al que hay que cambiar.
if($iteraciones == 1)
{
Ahora, será necesario distinguir si el cambio de directorio parte
desde el raíz, o no. En caso afirmativo, si se intentara construir el
nuevo path, uniendo sin más el directorio actual (“/”) con el
siguiente directorio de la lista (pongamos por caso que es “trabajo”),
incluyendo la barra de separación de directorios (“/”), se obtendría
algo como: raíz + separador + nuevo directorio = / + / + trabajo =
“//trabajo”.
Además, la barra separadora de directorios es imprescindible, ya que
si se pretende cambiar a dos directorios de distancia, por ejemplo, a
“trabajo/proyecto”, al unir los componentes partiendo de la raíz, sin
utilizar el separador, se construiría un camino como:
“/trabajoproyecto”. La solución consiste en, como se ha dicho,
distinguir entre el caso en el que se parte del directorio raíz, y en
el que se parte desde otro punto cualquiera de la TDV.
if($path_actual ne "/")
{
$path_entrada=$path_actual . "/" . $nombre_dir . "/.";
}
else # Se parte del raíz
{
$path_entrada="/" . $nombre_dir . "/.";
}
}
else
{
Si el bucle externo debe iterar más de una vez, esto es, si el
parámetro de “cwd” consta de más de un directorio, es en este punto
donde se añade el siguiente componente al path que se está utilizando,
separándolo del mismo mediante la barra “/”.
$nuevo_path=join '/',$nuevo_path,$componentes[$i];
Recordemos que el camino tiene que terminar en el primer elemento del
nuevo directorio, que no es otro que el puntero a sí mismo (“.”)
$path_entrada=join "/",$nuevo_path,".";
}
Ya sólo queda recorrer el directorio actual (que no induzca a
confusión el límite superior del siguiente bucle: el hecho de que se
haya elegido la posición del último elemento de la TDV es una cuestión
de seguridad; así se evita que, si el directorio que se está
recorriendo se encuentra justo al final de la tabla, se produzca un
error en tiempo de ejecución por intentar acceder a una posición fuera
de los límites de la misma), comparando cada entrada de la TDV con el
path construido en la última iteración del bucle externo, en busca del
primer elemento del nuevo directorio.
my $encontrado=0;
for(my $i=$puntero_tabla;$i<$#TabDirVir;$i++)
{
if($TabDirVir[$i][0] eq $path_entrada && $TabDirVir[$i][1]==0)
{
$puntero_tabla=$i;
$encontrado=1;
}
La instrucción “last” sirve para forzar una salida prematura de un
bucle. Siempre va precedida por una expresión que se evalúa cada vez
que el flujo de control llega hasta ella. Si el resultado de tal
evaluación es “cierto”36, se termina el bucle. En caso contrario, se
continúa normalmente con su ejecución.
De este modo, en el instante en que se localice el primer elemento del
nuevo directorio, la variable “$encontrado” guardará el valor “1”, y
hará que la expresión “if $encontrado” se evalúe como cierta, y la
ejecución abandone el bucle. Es una forma bastante limpia de optimizar
este tipo de estructuras de control.
last if $encontrado;
}
}
Si el bucle ha terminado y no se ha encontrado en toda la tabla el
elemento que se estaba buscando, se considera que el directorio
tecleado por el usuario es incorrecto, y se le informa de ello
mediante un mensaje de de error. En caso contrario, se le comunica que
la operación ha tenido éxito.
if($encontrado==0)
{
mensajes_usuario::muestra_mensaje(CODIGO530,$socket_cliente);
$puntero_tabla=$puntero_tabla_aux;
}
else
{
my $path_base=dirname($path_entrada);
$path_base=~s/$path//;
mensajes_usuario::muestra_mensaje_cp
("250 Successfully changed to $path_base",$socket_cliente);
}
}
}
Subrutina: “navega_cdup”.
Entradas: el handle del socket del cliente.
Salidas: ninguna.
Esta rutina sube un nivel en la jerarquía de directorios virtuales. La
idea es, en cierto modo, similar a la que sigue la rutina “navega_cwd”:
obtiene el path actual, y lo divide en sus componentes. Entonces, no
tiene más que eliminar el último de esos componentes, y sustituirlo
por un punto que, recordemos, es la primera entrada de todos los
directorios de la TDV. Cuando la encuentre, colocará el puntero en su
posición.
Si el path no puede ser dividido en una lista de componentes, la
variable que contiene su número de elementos almacenará el valor “-1”,
lo que ha de interpretarse como que el usuario partía del directorio
raíz (que no tiene padre en la jerarquía), y se le informa de ello
mediante un mensaje.
sub navega_cdup
{
my ($socket_cliente)[email protected]_;
my $path_actual=dirname($TabDirVir[$puntero_tabla][0]);
my $encontrado=0;
my $i=0;
my @elementos_path=split /\//,$path_actual;
my $nuevo_path;
if($#elementos_path == -1)
{
mensajes_usuario::muestra_mensaje
(CODIGO250ENRAIZ,$socket_cliente);
}
else
{
$elementos_path[$#elementos_path]='.';
$nuevo_path=join "\/",@elementos_path;
No queda más que buscar la entrada en la TDV.
for(my $i=0; $i<$#TabDirVir; $i++)
{
if($nuevo_path eq $TabDirVir[$i][0])
{
$encontrado=1;
$puntero_tabla=$i;
}
last if $encontrado;
}
Si, después de efectuar el cambio al directorio padre, el puntero de
la tabla señala a la posición cero, significará que el usuario partía
de un subdirectorio que colgaba directamente del raíz. En ese caso,
también se le notificará de ello.
if($puntero_tabla)
{
$nuevo_path=~s/$path//;
$nuevo_path=~s/\/\.//;
mensajes_usuario::muestra_mensaje_cp("250 Successfully changed to
$nuevo_path",$socket_cliente);
}
else
{
mensajes_usuario::muestra_mensaje_cp("250 Successfully changed to
root",$socket_cliente);
}
}
}
Subrutina “navega_pwd”
Entradas: el handle del socket del cliente.
Salidas: ninguna.
El resultado de esta rutina es la ejecución de la instrucción estándar
de FTP, “print working directory” (PWD), que envía al cliente el path
del directorio de trabajo.
Hay que tener en cuenta que, como ya se ha dicho, para el cliente, el
directorio raíz será aquel del servidor a partir del cual se almacenan
los ficheros. De este modo, si en el fichero de configuración el
directorio base del servidor es “/trabajo/proyecto”, para el cliente
será “/”. Así, el directorio del servidor “/trabajo/proyecto/source”,
será, para el cliente “/source”.
La idea es muy sencilla: si el puntero de la tabla de directorios
contiene el valor 0, significa que está señalando a la raíz de la
estructura virtual de directorios, de modo que la rutina debe enviar
al cliente simplemente una barra separadora de directorios: “/”. En
caso contrario, se busca la entrada correspondiente en la TDV, y se
devuelve el camino completo, eliminando del mismo, previamente, el
contenido de la variable $path, que almacena el directorio raíz del
servidor. En el ejemplo anterior, tendríamos: $path =
“/trabajo/proyecto”.
sub navega_pwd
{
my ($socket_cliente)[email protected]_;
my $path_cliente="\/";
if($puntero_tabla)
{
$path_cliente=$TabDirVir[$puntero_tabla][0];
$path_cliente =~ s/$path//;
$path_cliente=dirname($path_cliente);
}
else # Estamos en el directorio raíz.
{
$path_cliente="\/";
}
mensajes_usuario::muestra_mensaje_cp
("257 Current directory is \"$path_cliente\"",$socket_cliente);
}
Subrutina: “encuentra_fichero”
Entradas: una cadena con el nombre del fichero que se quiere
localizar.
Salidas: 1 si el fichero se encuentra dentro del directorio actual, y
0 si no.
La rutina recorre el directorio virtual actual, comparando cada una de
sus entradas con el parámetro recibido.
Se utiliza cuando el cliente solicita la descarga de un fichero.
sub encuentra_fichero
{
my ($parametro)[email protected]_;
my $path_actual=dirname($TabDirVir[$puntero_tabla][0]);
my $path_temporal=$path_actual;
my $encontrado=0;
my $indice=$puntero_tabla;
El usuario especifica un nombre de fichero solamente, sin path, así
que será necesario construirlo para buscarlo dentro del directorio
local.
Si, por ejemplo, el camino del directorio es “trabajo/proyecto”, y el
cliente solicita la descarga del fichero “source.pl”, como todas las
entradas de la TDV contienen el path completo, si no se añadiera al
principio del nombre de fichero, éste no se encontraría incluso aunque
estuviera dentro del directorio. Por tanto, antes de comenzar a
buscar, se unen el path actual (que es el camino de la entrada de la
TDV señalada por el $puntero_tabla), y el nombre del fichero
solicitado, separados por una barra “/”. En nuestro ejemplo, la cadena
a buscar se convertiría en: “/trabajo/proyecto/source.pl”.
if($path_actual eq "/")
{
$parametro_completo="/".$parametro;
}
else
{
$parametro_completo=$path_actual."/".$parametro;
}
while($path_actual eq $path_temporal)
{
$path_actual=dirname($TabDirVir[$indice][0]);
No basta con encontrar una entrada que coincida con el parámetro
recibido; además, hay que comprobar que no se trate de un directorio
(evidentemente, los directorios no son descargables), analizando el
valor almacenado en la segunda columna de la fila actual de la TDV.
Recordemos que un 0 significa que la entrada es un directorio, y un 1,
que es un fichero.
if(($TabDirVir[$indice][0] eq $parametro_completo) &&
($TabDirVir[$indice][1]==1))
{
$encontrado=1;
}
else
{
$indice++;
}
last if $encontrado;
}
return $encontrado;
}
Subrutina “descarga_archivo”
Entradas: una cadena con el nombre del archivo solicitado, el handle
del socket de datos, y el tipo de transferencia, determinado por el
cliente (recordemos que A, es ASCII, e I, es binario de 8 bits).
Salidas: ninguna.
Como ya se ha comentado anteriormente, el servidor FTP implementado en
este proyecto es, en principio, idéntico a cualquier otro
(simplificado, eso sí), con un matiz importante: las descargas pueden
ser locales, esto es, dirigidas a archivos del disco del propio
servidor, o remotas, es decir, de fichero ubicados en el disco de otro
servidor distinto.
Por eso, cuando un cliente solicita la descarga de un fichero, uno de
los primeros pasos a dar consiste en determinar si se encuentra en la
máquina local, o en una remota, en cuyo caso, habría que redirigir la
petición, y conseguir que el servidor FTP hiciera las veces de Proxy,
retransmitiendo al cliente los datos recibidos, en tiempo real, y
conforme van llegando.
En función de la ubicación del archivo solicitado, que determina esta
rutina, se invoca a “descarga_archivo_local”, o “descarga_archivo_remoto”.
El método consiste en extraer el path del fichero, y comprobar si
contiene, al principio, la cadena DIRn (donde “n” es un número
entero). No olvidemos que las tablas de los demás nodos integrantes
del sistema, se almacenan en la local, dentro de directorios llamados
DIR1, DIR2, … que cuelgan de la raíz de la TDV.
Es posible que el archivo solicitado se encuentre en varios nodos de
la red37, de modo que la rutina tiene que confeccionar una tabla en la
que los identifique a todos por medio de sus direcciones IP.
sub descarga_archivo
{
my ($fichero,$socket_datos,$tipo)[email protected]_;
my $path_local=dirname($TabDirVir[$puntero_tabla][0]);
my @NodosYTiempos;
my $ultimo=0;
my $encontrado;
my $IP_nodo;
if($path_local =~ /\ADIR(0-9)/)
{
Como se trata de un fichero remoto, es necesario construir una lista
en la que figuren todos los nodos en los que se puede encontrar el
mismo fichero. He de insistir: el MISMO fichero. No es suficiente con
encontrar dos entradas con el mismo nombre; además, deben coincidir
sus tamaños y la fecha completa (incluyendo hora, minutos y segundos)
de la última modificación.
Así se dificulta, en la medida de lo posible, que el usuario descargue
un archivo que no es exactamente el que quería (podría tratarse de una
versión anterior, o incluso de un fichero completamente diferente,
aunque con el mismo nombre; sólo hay que imaginar la situación que se
plantearía si en un nodo existiera un archivo comprimido llamado “proyecto.zip”,
que contuviera código fuente, o texto ASCII, y en otro nodo diferente,
un “proyecto.zip” que no fuera más que un ejecutable sin ninguna
relación con el anterior… por no pensar en situaciones bastante más
escabrosas…).
Para construir tal lista, se recurre a la rutina
“busca_fichero_en_nodos”.
my $nombre_completo=$path_local . "/" . $fichero;
my @ListaNodos=busca_fichero_en_nodos($nombre_completo);
Una vez construida la relación de nodos que almacenan el mismo
fichero, hay que determinar a cuál de ellos se le redirige la petición
de descarga.
La estrategia adoptada consiste en solicitar el archivo, primero, al
nodo con el mejor tiempo de respuesta. Si surgiera algún problema, se
intentaría con el segundo en la lista. Y así, hasta que, o bien uno de
los hosts responda, o bien se hayan consultado todos los que figuran
en la lista, sin éxito, en cuyo caso, habría que informar de ello al
usuario.
El cálculo del tiempo de respuesta de cada nodo corre a cargo de la
otra tarea de este proyecto: el gestor de la coherencia. Éste, cada
cierto tiempo, realiza un ping38 sobre cada uno de los nodos de los
que tiene constancia, y almacena los resultados en el fichero
ResponseTimes.txt.
Es necesario que el gestor de la coherencia efectúe esta operación con
cierta regularidad, ya que la disponibilidad de los nodos del sistema
no es constante, y puede ocurrir que una de las máquinas con mejor
tiempo de respuesta sufra algún problema técnico o sea desconectada
temporalmente, o bien, que uno de los hosts que estaba apagado, vuelva
a integrarse en la red de FTP distribuido, y además, con muy buen
tiempo de respuesta.
Hay un detalle importante acerca del fichero “ResponseTimes.txt”: se
trata de un recurso compartido por las dos tareas que funcionan, de
forma concurrente, en cada uno de los nodos: el servidor FTP y el
gestor de la coherencia. La primera lee de él, y la segunda escribe en
él. Por tanto, es necesario recurrir a algún mecanismo que garantice
que no se producirán las dos operaciones al mismo tiempo. Cuando uno
de los dos procesos esté trabajando con el archivo, el otro debe
aguardar.
Hay multitud de soluciones para asegurar la exclusividad en el uso de
un recurso compartido. El más sencillo (que además, es precisamente el
que emplea esta rutina) es el de las regiones críticas.
Grosso modo, se trata de una zona del programa en la que el flujo de
ejecución de una de las tareas que utilizan el recurso compartido, no
puede entrar si la otra lo está utilizando en ese momento. Aunque
existen varias formas de implementar esta estrategia, en el caso que
nos ocupa se utiliza un fichero vacío llamado “RC”.
Cuando una de las dos tareas va a trabajar con el archivo “ResponseTimes”,
crea el fichero “RC”. De este modo, si la otra intenta acceder al
recurso en ese instante, detectará la presencia de la región crítica,
y deberá aguardar hasta que sea liberada, esto es, hasta que el
proceso que estaba escribiendo o leyendo “ResponseTimes.txt”, borre el
fichero “RC”39.
Mientras ninguno de los dos procesos acceda a ResponseTimes.txt, el
fichero “RC“ no existirá. Sólo aparecerá en el disco durante una
lectura o una escritura.
En la siguiente línea de código, el proceso intenta penetrar en la
región crítica, es decir, comprueba si el fichero “RC” existe,
mediante el operador “-e”. Mientras sea así, el flujo de ejecución
permanecerá atrapado dentro de un bucle indefinido. En el instante en
que “RC” sea eliminado por el gestor de la coherencia, la ejecución de
la subrutina podrá continuar.
while(-e “RC”) { }
El recurso está libre. Ahora es el servidor FTP el que entra en la
región crítica.
open $RegionCritica,">RC";
Ya puede comenzar a leer el archivo compartido.
my $tiempo;
my $IP;
open $Tiempos," or die "Cannot read ResponseTimes.txt - $!\n";
while($IP=<$Tiempos>)
{
En este bucle, la rutina busca, dentro de “ResponseTimes.txt”, las
direcciones IP de aquellos nodos que figuran en la lista devuelta por
“busca_fichero_en_nodos”. Cada vez que encuentre una de ellas,
almacena, junto con su tiempo de respuesta, en la tabla local “@NodosYTiempo”.
$encontrado=0;
for(my $i=0;$i<=$#ListaNodos;$i++)
{
if($ListaNodos[$i] eq $IP)
{
$encontrado=1;
}
last if $encontrado;
}
$tiempo=<$Tiempos>;
Cuando la ejecución alcanza este punto, en la variable “$IP” estará
almacenada una de las direcciones que figuran en “ResponseTimes.txt”,
y en “$tiempo”, su tiempo de respuesta asociado. Si además, la
variable “$encontrado” contiene el valor “1”, significará que la
dirección IP también está en la lista de nodos que albergan el fichero
que pidió el cliente. En ese caso, el contenido de ambas variables,
esto es, “$IP” y “$tiempo”, se añaden a la tabla auxiliar “@NodosYTiempo”.
Si dicha tabla estaba vacía, la variable que almacena la posición de
su último elemento, tendrá el valor –1. Evidentemente, no se puede
utilizar como índice de la matriz así que, en su lugar, se utiliza la
variable “$ultimo” que, se inicia con el valor 0.
--------------------------------------------------------------------------
if($#NodosYTiempo==-1) { $ultimo=0; }
if($encontrado)
{
$NodosYTiempo[$ultimo][0]=$IP;
$NodosYTiempo[$ultimo][1]=$tiempo;
$ultimo++;
}
}
close $Tiempos;
---------------
El servidor FTP abandona la región crítica en este punto.
close $RegionCritica;
unlink "RC";
Una vez que se dispone de la tabla completa, se organiza en función de
los tiempos de respuesta: primero figurará el nodo con el mejor,
después, el segundo, etc.
El algoritmo empleado para conseguirlo es muy sencillo: el primer paso
consiste en determinar el tiempo de respuesta más alto de los nodos
encontrados, el cual se utilizará a modo de “valor pivote” (es decir,
de referencia), para comparar su tiempo con los de los demás hosts en
la lista.
my $tr_aux=-2;
my $maximo;
for(my $i=0;$i<=$#NodosYTiempo;$i++)
{
if($NodosYTiempo[$i][1] > $tr_aux)
{
$maximo=$NodosYTiempo[$i][0];
}
}
En una primera iteración, se almacena en la tabla auxiliar
“@ListaNodos_aux”, el primer nodo con un tiempo de respuesta inferior
a ese máximo, y se compara con todos los que figuran en “@ListaNodos”.
Cada vez que se encuentra un nodo con mejor tiempo de respuesta que el
del nodo “pivote”, ocupa su lugar (comprobando previamente que ese
tiempo no es igual a “–1”; este es un valor arbitrario que el gestor
de la coherencia utiliza para identificar a los nodos que no
responden). Antes de esto, sin embargo, se debe asegurar que el nodo
no ha sido ya procesado (de lo contrario, el algoritmo terminaría
dando como resultado una tabla ocupada varias veces por el nodo con
mejor tiempo de respuesta).
El bucle continúa iterando hasta procesar todos los elementos de “@TablaNodos”.
De esta forma, si se encuentra otro nodo con un tiempo de respuesta
aún menor que el almacenado, lo sustituirá. La idea es que, al
terminar la ejecución del bucle interno, la primera fila de la “@TablaNodos_aux”
contenga el nodo con el mejor tiempo de respuesta.
En la segunda iteración del bucle externo vuelven a compararse todos
los tiempos de respuesta de los nodos de “@TablaNodos” con el valor
pivote, y volverá a almacenarse el menor de ellos, exceptuando al que
ya se procesó en la iteración anterior. Esto quiere decir que el
resultado de esta segunda iteración del bucle externo, será la
localización del nodo con el segundo mejor tiempo de respuesta.
El proceso acabará cuando en la “@TablaNodos_aux” estén almacenados
todos los hosts que contienen el archivo solicitado, en orden
creciente de tiempo de respuesta. Las siguientes ilustraciones
muestran un ejemplo de la ejecución del algoritmo, paso a paso, para
tres servidores: supongamos que se determina que el archivo solicitado
se encuentra en tres nodos diferentes, con direcciones A, B y C, y
tiempos de respuesta de 420, 300 y 245 milisegundos respectivamente.
Antes de entrar en el bucle externo, se halla el nodo pivote,
correspondiente al máximo tiempo encontrado (es decir, 420 ms). El
nodo en cuestión es el que tiene la IP “A”, en la siguiente figura.
Cuando comienza la primera iteración de dicho bucle, el pivote se
almacena en la “@TablaNodos_aux”.

Figura 11 – Primera iteración del bucle externo del algoritmo de
ordenación
El flujo de ejecución se adentra en el bucle interno, y comienza a
recorrer la “@TablaNodos”, comparando sus tiempos con el del nodo
pivote (recurriendo para ello a la rutina “tiempo_de_respuesta”, que
dada una dirección IP, devuelve el tiempo de respuesta del host que le
corresponde). Como el tiempo de B, 0’3 segundos, es inferior al
máximo, y como aún no ha sido procesado (ya que todavía no figura en
la “@TablaNodos_aux”), ocupa su lugar.
Sin embargo, en la siguiente iteración del bucle interno, se comprueba
que hay otro nodo con un tiempo de respuesta menor que el de B. Se
trata de C, con un tiempo de 245 ms. Tampoco ha sido procesado, de
manera que sobreescribe el valor anterior de la primera fila de “@TablaNodos_aux”
(el del nodo B). El estado de las tablas, cuando termina el bucle
interno y va a comenzar la segunda iteración del externo, es el que se
muestra en la siguiente figura:

Figura 12 – Segunda iteración del bucle externo del algoritmo de
ordenación
Comienza a recorrerse la “@TablaNodos” de nuevo, desde el principio, y
se comprueba que el nodo B tiene un tiempo de respuesta menor que el
que figura en fila actual de la “@TablaNodos_aux”. Dado que aún no ha
sido procesado, sustituye a éste.
En la siguiente iteración del bucle interno, se encuentra un nodo con
menor tiempo de respuesta: C. Sin embargo, ya ha sido procesado (ya
está en la “@TablaNodos_aux”), así que se descarta, y la ejecución
continúa.
Cuando concluye esta segunda iteración del bucle externo, la tabla
está completamente ordenada. En realidad, si la “@TablaNodos” tiene N
elementos, sólo son necesarias N-1 iteraciones para ordenarlos todos
(el caso base sería el de un solo elemento; con 1-1 = 0 iteraciones,
la tabla ya estaría ordenada… o lo que es lo mismo: en ese caso, no es
necesario ni siquiera entrar en el bucle). Es por esto por lo que el
bucle externo itera hasta que el índice toma el valor de la penúltima
posición de la tabla, y no de la última.
Para concluir, hay que almacenar, “manualmente”, el nodo con el mayor
tiempo de respuesta en la última posición de la tabla, lo que se lleva
a cabo justo después del bucle externo. El resultado es:

Figura 13 – Resultado del proceso de ordenación
my @ListaNodos_aux;
my $ultimo=0;
for(my $i=0;$i<$#ListaNodos;$i++)
{
$ListaNodos_aux[$i]=$maximo;
for(my $j=0;$j<=$#ListaNodos;$j++)
{
my $tiempo1=tiempo_de_respuesta($ListaNodos_aux[$i]);
my $tiempo2=tiempo_de_respuesta($ListaNodos[$j]);
if($tiempo2<$tiempo1 && $tiempo2!=-1)
{
my $procesado=0;
for(my $k=0;$k<=$#ListaNodos_aux;$k++)
{
if($ListaNodos_aux[$k] eq $ListaNodos[$j])
{
$procesado=1; # Ya ha sido procesado.
}
last if $procesado;
}
unless($procesado)
{
$ListaNodos_aux[$ultimo]=$ListaNodos[$j];
}
}
}
$ultimo++;
}
@ListaNodos_aux[$#ListaNodos_aux]=$maximo;
Almacena el resultado en la “@ListaNodos” original.
@[email protected]_aux;
En este punto, la rutina ya cuenta con una lista que contiene todos
los nodos en los que se encuentra el archivo solicitado, ordenada por
tiempo de respuesta. El siguiente paso a dar, por lo tanto, es
intentar contactar el primero de ellos, para dirigirle la petición de
descarga. El nodo local hará las veces de retransmisor de los datos,
como se ha descrito anteriormente.
La táctica que se sigue consiste en la utilización de un bucle dentro
del cual se intenta conectar con el primer nodo de la lista. Si no
respondiera, se probaría con el segundo… y así, hasta que, o bien uno
de los nodos responde, o bien se recorre la lista completa sin éxito,
en cuyo caso habría que avisar al cliente.
my $exito=0;
my $timeout;
for(my $i=0;$i<=$#ListaNodos;$i++)
{
$IP_nodo=$ListaNodos[$i];
Se obtiene el tiempo de respuesta del nodo, buscando su dirección IP
en la tabla local “@NodosYTiempo”.
$encontrado=0;
for(my $j=0;$j<=$#NodosYTiempo;$j++)
{
if($NodosYTiempos[$j][0] eq $IP_nodo)
{
Por seguridad, se determina el tiempo de espera máximo como el doble
del que se calculó la última vez para el nodo con el que se está
intentando contactar (recordemos que las condiciones de la red varían
constantemente, así que no sería muy prudente aguardar durante
exactamente el mismo tiempo de respuesta que el gestor de la
coherencia determinó la última vez, ya que si por cualquier motivo,
hubiera aumentado mientras tanto en, digamos, 2 milisegundos, se
consideraría que el nodo ya no responde).
$timeout=$NodosYTiempo[$j][1]*2;
$encontrado=1;
}
last if $encontrado;
}
Ya disponemos de todos los datos necesarios para establecer una sesión
FTP con el servidor pertinente: su dirección IP, y el tiempo máximo
que estamos dispuestos a esperar a que responda. Para ello, se utiliza
una rutina que implementa un sencillo cliente: “miniclienteFTP”,
invocada desde “descarga_achivo_remoto”. Si la función devolviera un
0, significaría que se ha producido algún tipo de problema que ha
impedido que el servidor local contacte con el nodo elegido. Será
necesario, por lo tanto, probar con el siguiente de la “@TablaNodos”.
$exito=descarga_archivo_remoto($IP_nodo,$timeout,
$nombre_completo,$socket_datos);
El bucle termina en el instante en que la subrutina “descarga_archivo_remoto”
devuelve un 1, señal de que la conexión ha podido establecerse sin
problemas. Si el bucle acaba sin que se haya podido contactar con
ninguno de los nodos de la lista, la variable “$exito” contendrá el
valor 0. En ese caso, hay que enviar un mensaje de aviso al cliente.
last if $exito;
}
if(!$exito)
{
mensajes_usuario::muestra(CODIGO425,$socket_datos);
}
}
Todo el código de la subrutina “descarga_archivo”, descrito hasta
ahora, se utiliza sólo si el archivo solicitado por el usuario se
encuentra en un nodo remoto. Si no es así, el flujo de ejecución
entrará por la rama “else” de la instrucción condicional, y se
invocará a la rutina “descarga_archivo_local”.
-----------------------------------------------------------------
else
{
descarga_archivo_local($fichero,$socket_datos,$tipo);
}
}
-
Subrutina “descarga_archivo_local”
Entradas: una cadena con el nombre del archivo, el handle del socket
de datos, el tipo de transferencia determinado por el cliente (“A”, si
es ASCII, e “I” si es binario de 8 bits), y como último parámetro, un
1 si la descarga ha sido solicitada por un proxy (esto es, se trata de
una descarga remota), en cuyo caso hay que localizar el archivo en un
la raíz de la TDV –allí es donde se almacenan los ficheros de este
tipo de descargas-, o un 0 si se la solicitud proviene de un cliente
normal.
Salidas: ninguna.
Esta rutina se emplea cuando el cliente solicita descargar un fichero
que se encuentra ubicado físicamente en el nodo local. Si estuviera en
otra máquina, habría que invocar a “descarga_archivo_remoto”. El
funcionamiento es sencillo: simplemente, abre el fichero en disco, y
lo transfiere en bloques de 64 Kbytes.
sub descarga_archivo_local
{
my ($fichero,$socket_datos,$tipo,$remoto)[email protected]_;
my $buffer;
my $leidos=0;
my $escritos;
my $path_local=dirname($TabDirVir[$puntero_tabla][0]);
my $path_base=$path;
La variable “$path_base” contiene el camino a partir del cual se monta
el sistema de directorios virtuales. Para que esta rutina funcione
correctamente, es necesario eliminar la barra separadora que aparece
al final del mismo, lo que se consigue utilizando el método
predefinido de Perl, “chop”. Hay que distinguir, no obstante, entre
las peticiones de descarga provenientes de un cliente ordinario, y las
enviadas por un proxy. En el segundo caso, se recurre a una serie de
archivos temporales que se ubican en la raíz de la TDV, de modo que
hay que obviar el path y el nombre del archivo origen.
if($remoto)
{
$path_local="/";
}
else
{
$path_local=dirname($TabDirVir[$puntero_tabla][0]);
chomp($path_base);
}
chop($path_base);
Si el flujo de ejecución alcanza este punto, tenemos plenas garantías
de que:
a) El archivo está en la máquina local…40
b) … y se encuentra en el directorio de trabajo actual.
Por lo tanto, no hay más que abrirlo, después de construir el nombre
completo, uniendo el “$path_base” con el parámetro enviado por el
cliente.
if($path_local eq "/")
{
$nombre_completo=$path_base."/".$fichero;
}
else
{
$nombre_completo=$path_base.$path_local."/".$fichero;
}
Todo listo. Se procede a abrir el archivo.
open DESCARGA,"<$nombre_completo"
or die “Can’t read from $nombre_completo - $!\n”;
Ahora, se determina el tipo de transferencia. Si es binario de 8 bits,
el flujo de ejecución entra en la primera rama de la instrucción
condicional que aparece a continuación:
if($tipo eq 'I')
{
Mientras no se diga lo contrario, Perl considera que todos los
ficheros contienen texto. Si el cliente especificó que el tipo de
descarga debe ser binario de 8 bits, es necesario dejárselo claro al
intérprete: de ahí el uso de la siguiente función predefinida “binmode”,
y su parámetro “raw” (algo así como “crudo”). Si no se utilizara este
método, las descargas de tipo binario no funcionarían correctamente,
ya que la mayoría de los ficheros terminarían truncados cuando el
puntero de lectura se topara con ciertos caracteres.
binmode DESCARGA,":raw";
El fichero se lee a base de trozos de 64 Kbytes (es decir: 65535 bytes),
mediante la función “sysread”, que devuelve el número de bytes leídos
o, si se alcanza el fin del fichero, el valor “0”. Por eso se utiliza
como la expresión que debe evaluar el siguiente bucle. El flujo de
ejecución permanecerá iterando en su interior mientras no se alcance
el fin del fichero.
while($leidos=sysread(DESCARGA,$buffer,65535))
{
Para enviar el bloque de 64 Kb de datos, se utiliza el método
predefinido “syswrite” (como sugiere su nombre, es el complementario
de “sysread”), que recibe como parámetros el buffer que contiene los
datos a enviar, el tamaño de éste, en bytes, y el “offset”, o
desplazamiento.
Obsérvese que el tamaño del bloque de datos se expresa en función de
los bytes que ha leído el método “sysread”. La expresión,
concretamente, es: $leidos-$n. La explicación es sencilla: el archivo
se envía en bloques de 64 Kbytes pero, evidentemente, su tamaño total
no tiene por qué ser múltiplo de esta cantidad. Por lo tanto, es más
que probable que el último bloque leído (o el único, si es un archivo
pequeño) sea menor de 64 Kbytes.
Respecto al “offset” o desplazamiento, se puede comparar con un índice
que señala el punto del bloque de datos que se está escribiendo. El
desplazamiento aumenta para simular el efecto de ese puntero
“moviéndose” según recorre el bloque de datos y señala la posición en
la que debe continuar la escritura.
for($n=0; $n<$leidos;)
{
$escritos=$socket_datos->syswrite($buffer,$leidos-$n,$n);
Si se produce algún error durante la escritura, el método “syswrite”
devolverá “undef”. Esto se comprueba en la siguiente estructura
condicional. Si se confirma que la variable “$escritos” no está
definida, se advierte al usuario que ha surgido un problema,
enviándole un mensaje.
unless(defined($escritos))
{
$motivo=$!;
mensajes_usuario::muestra_mensaje_cp
("426 Error while transferring data: $motivo\r\n",$socket_cliente);
mensajes_usuario::muestra_mensaje(CODIGO426,$socket_cliente);
}
$n+=$escritos;
Se aplica el proceso análogo si se produce algún error al leer los
datos del archivo.
unless(defined($leidos))
{
$motivo=$!;
mensajes_usuario::muestra_mensaje_cp
("426 Error while transferring data: $motivo\r\n",$socket_cliente);
mensajes_usuario::muestra_mensaje(CODIGO426,$socket_cliente);
}
}
}
}
else
Si el tipo de transferencia, determinado por el cliente, es ASCII, la
transmisión de los datos es mucho más sencilla: consiste, simplemente,
en leer la información del fichero, línea a línea, y enviarla al
cliente.
{
while($_ = DESCARGA->getline)
{
s/[\r\n]+$//;
$socket_datos->print("$_\r\n");
}
}
close(DESCARGA);
}
Subrutina “busca_actualizaciones”.
Entradas: ninguna.
Salidas: ninguna.
Cuando un nuevo nodo se une a la red, o bien cuando uno de los
miembros del sistema, que ha permanecido un tiempo desconectado,
vuelve a incorporarse, todos los demás servidores deben enviarle sus
tablas locales. Esto significa que el nuevo nodo va a recibir tantos
mensajes de actualización de su TDV, como hosts haya en el sistema.
Aunque la estrategia adoptada para evitar colisiones y conflictos se
detalla en la sección 5.6.5 - El gestor de la coherencia de los datos,
página 125, no está de más mencionar uno de sus rasgos más
interesantes, como es el hecho de que se deriva un hijo de dicho
proceso por cada mensaje recibido, de modo que todos se procesan
simultáneamente y de forma independiente unos de otros.
Cuando todos los procesos hijos terminan su trabajo, el padre envía
una interrupción software al servidor FTP, avisándole que tiene una
actualización preparada. Pero, cuidado: dicha actualización puede
constar de varios archivos, no de uno solo. Concretamente, puede que
haya tantos como nodos en el sistema. Cada uno de estos ficheros
corresponderá a una de las tablas remotas.
El servidor FTP debe actualizar la TDV local leyendo el contenido de
los ficheros “updatevdtN.txt” (donde N es un número de índice que
identifica al proceso hijo que generó el archivo). Esta rutina, que en
realidad es el manejador de la interrupción software enviada desde el
gestor de la coherencia, entra en un bucle en el que itera 1000 veces.
En el bucle principal de la rutina, se comprueba qué archivos de
actualización existen, desde “updatevdt0.txt” hasta “updatevdt999.txt”.
Cada vez que se encuentra uno de ellos, se abre y se invoca a la
rutina “actualiza_tdv”, que se encarga de procesarlo y de actualizar
la tabla de directorios local.
sub busca_actualizaciones
{
for(my $i=0;$i<1000;$i++)
{
El operador unario “-e”, devuelve “1” si existe el fichero al que se
aplica.
if(-e "updatevdt$i.txt")
{
La ejecución sólo entra en esta rama de la instrucción condicional si
el fichero i-ésimo existe. Ahora, se abre y se envía su descriptor a
la rutina “actualiza_tdv”.
open $fichact," or die "Can't open update file - $!\n";
actualiza_tdv($fichact);
Una vez efectuada la actualización con los datos almacenados en el
fichero i-ésimo, se cierra y se borra del disco.
close $fichact;
unlink "updatevdt$i.txt";
}
}
}
Subrutina: “actualiza_tdv”
Entradas: el descriptor del fichero que contiene la actualización que
se está procesando.
Salidas: ninguna.
Esta rutina lee uno de los ficheros que contienen los datos de una TDV
remota, ya procesados, y los añade a la local, almacenándolos en un
directorio que cuelga de la “raíz” de la tabla.
Cada nodo tiene su propio directorio asignado y fijo (sólo varía si el
host en cuestión se da de baja; quedaría entonces un hueco en la
secuencia de números de orden de los directorios, por tanto, y podría
ser aprovechado por una máquina nueva que se incorporara al sistema en
el futuro), de modo que antes de efectuar la actualización, la rutina
comprueba la dirección IP del servidor que la envió. Si es la de uno
de los nodos que ya tienen un directorio asignado, elimina el
contenido de éste, y lo sustituye por el nuevo.
Si, por el contrario, el nodo no tenía un directorio asignado, se crea
uno para él.
sub actualiza_tdv
{
my $fichact=shift;
my $nombre;
my $tipo;
my $tamanho;
my $fecha;
my $propietario;
my $perms;
my $nlink;
my $user;
my $group;
my $numdir;
my $encontrado;
my $i;
my $dir_entrada;
my $posicion;
print "Updating VDT...\n";
Lo primero que debe hacer la rutina, es determinar si el nodo local
tiene constancia de la existencia de otros hosts en el sistema. Si la
“@TablaNodos” global está vacía, significa que el servidor aún no ha
tenido contacto con otras máquinas, de modo que la actualización que
se va a procesar se almacena en el primero de los directorios
especiales (en consecuencia, recibe un “1” como número de orden).
Si la “@TablaNodos” no está vacía, se comprueba si existe el
directorio asociado al nodo que ha enviado la actualización que se va
a procesar, o si por el contrario, debe crearse. En cualquier caso,
siempre que se modifique la “@TablaNodos”, se salva en disco.
Las siguientes líneas leen la primera fila de la tabla remota.
$nombre=<$fichact>;
$tipo=<$fichact>;
$tamanho=<$fichact>;
$fecha=<$fichact>;
$propietario=<$fichact>;
$perms=<$fichact>;
$nlink=<$fichact>;
$user=<$fichact>;
$group=<$fichact>;
… y las que se muestran a continuación, eliminan los retornos de
carro.
chomp($nombre);
chomp($tipo);
chomp($tamanho);
chomp($fecha);
chomp($propietario);
chomp($perms);
chomp($nlink);
chomp($user);
chomp($group);
Ahora se extrae la dirección IP de la máquina remota cuya tabla se
está leyendo. Hay que tener en cuenta (como se detalla en la sección
5.6.5 - El gestor de la coherencia de los datos; página 125) que en la
versión de Perl empleada para el desarrollo de este proyecto, el
método predefinido para la recepción de mensajes a través de un socket,
“recv”, devuelve la dirección del remitente, en formato IPv641. Es
conveniente tratar este tipo de direcciones con cierta cautela, dado
que pueden cambiar en función de las condiciones de la red y de la
conexión, incluso cuando se trata de dos tramas enviadas desde la
misma máquina.
Sin embargo, se puede garantizar que toda dirección IPv6 procedente de
un nodo concreto tiene una parte invariable: la propia IPv4, que se
envía “incrustada” dentro de ésta. Por este motivo, las dos líneas
siguientes dividen la IPv6 en los 16 bytes que la componen, y se queda
sólo con los cuatro que ocupan las posiciones de la cuarta a la
séptima (que, precisamente, corresponden a la IPv4).
my @bytesIP=split /\./, $propietario;
my $propietarioIPv4="$bytesIP[4].$bytesIP[5].
$bytesIP[6].$bytesIP[7]";
if(!defined($TablaNodos[0][0]))
{
La “@TablaNodos” estaba vacía, luego el host no tenía aún constancia
de la existencia de otros nodos. Este es el primero con el que
contacta, así que lo identifica mediante su IPv4, y le asigna el
primer directorio especial: “DIR1”.
$TablaNodos[0][0]=$propietarioIPv4;
$TablaNodos[0][1]=1;
Ahora, se crea el directorio especial en la TDV, recurriendo a la
rutina “crea_dir”.
crea_dir(1,$tamanho,$fecha,$propietario,$perms,$nlink,$user,$group);
$dir_entrada="/DIR1";
He de insistir: cada vez que la “@TablaNodos” sufre una modificación,
se guarda en el fichero “tabnodsrv.txt”. Eso es, precisamente, lo que
hace el siguiente fragmento de código:
open TABNOD,">tabnodsrv.txt";
print TABNOD $TablaNodos[0][0] . "\n";
print TABNOD $TablaNodos[0][1] . "\n";
close TABNOD;
}
else
{
Esta rama de la instrucción condicional se alcanza sólo si la “@TablaNodos”
ya tenía elemento. En ese caso, será necesario determinar si la
actualización que se va a procesar ha sido remitida desde uno de
ellos, o si será necesario dar de alta a uno nuevo.
$encontrado=0;
El siguiente bucle recorre la “@TablaNodos”, comparando las
direcciones almacenadas, con la del nodo que envió la actualización.
for($i=0;$i<=$#TablaNodos;$i++)
{
if($TablaNodos[$i][0] eq $propietarioIPv4)
{
$encontrado=1;
Si el nodo remitente es uno de los ya conocidos, se toma nota de la
posición que ocupa en la “@TablaNodos”, para acceder de forma rápida e
inmediata a ella, más adelante.
$posicion=$i;
}
last if $encontrado;
}
Si no se ha identificado al remitente, será necesario añadirlo a la “@TablaNodos”,
y crear un directorio especial, para él.
La idea es aprovechar cualquier hueco en la secuencia de números
asignados a los directorios especiales si alguno de los nodos
conocidos se ha dado de baja.
Por ejemplo, si el host conoce a 3 nodos más, a los que ha asignado
los directorios “DIR1”, “DIR2” y “DIR4” (porque el “DIR3” pertenecía a
una cuarta máquina, que se eliminó del sistema), y la actualización
recibida procede de un servidor desconocido hasta ahora, se le
asignará el número libre: “DIR3”.
Si no hubiera huecos, el directorio especial recibirá el último número
de la serie (es decir, si tuviéramos “DIR1”, “DIR2” y “DIR3” ya
utilizados, al nuevo host se le asignaría el “DIR4”).
if(!$encontrado)
{
$numdir=1;
Para localizar el primer directorio especial libre, se utiliza un
bucle que busca el “DIRn”, con n=$numdir. Si tal directorio existe
(ergo, no está disponible), “$numdir” se incrementa en 1, y se prueba
de nuevo. El proceso continúa hasta que se comprueba que uno de los “DIRn”
no existe (ya esté localizado en medio de la tabla de nodos o al
final).
La idea consiste en aprovechar el hecho de que los números de
directorios se asignan por orden creciente. Es decir, primero se da el
“DIR1”, luego el “DIR2”. La serie de índices está ordenada, y el 3
nunca puede situarse por encima del 4. Así pues, si el bucle lleva
hasta el “DIR4” una búsqueda del “DIR3”, significa que éste no
existía.
$encontrado=1;
while($encontrado && $i<=$#TablaNodos)
{
for($i=0;$i<=$#TablaNodos;$i++)
{
if($numdir < $TablaNodos[$i][1])
{
$encontrado=0;
}
elsif($numdir == $TablaNodos[$i][1])
{
$encontrado=1;
}
last if $encontrado;
}
if($encontrado)
{
$numdir++;
}
}
Ya tenemos un directorio especial para el nuevo nodo. Se almacena su
nombre en la variable “$dir_entrada”, y se crea en la TDV.
Si se ha recorrido la tabla de nodos completamente sin encontrar un
hueco, el nuevo directorio tendrá, como número de orden, el del más
alto de la tabla más 1.
if($encontrado && $i>$#TablaNodos) { $numdir++; }
$dir_entrada="/DIR" . $numdir;
crea_dir($numdir,$tamanho,$fecha,$propietario,$perms,$nlink,
$user,$group);
Se guardan los cambios de la “@TablaNodos” en el fichero “tabnodsrv.txt”.
open TABNOD,">tabnodsrv.txt";
for($i=0;$i<$#TablaNodos;$i++)
{
print TABNOD $TablaNodos[$i][0] . "\n";
print TABNOD $TablaNodos[$i][1] . "\n";
}
close TABNOD;
}
else
{
Si el flujo de ejecución alcanza este punto, significa que el nodo que
ha enviado la actualización que se va a procesar ya existía, así que
se vacía el directorio que tiene asignado, y se vuelve a llenar, pero
con los nuevos datos.
Lo primero es determinar qué número de directorio le corresponde al
nodo.
$dir_entrada="/DIR" . $TablaNodos[$posicion][1];
Ahora, lo localiza en la TDV, para comenzar a borrar su contenido.
for($i=0;$i<=$#TabDirVir;$i++)
{
last if ($TabDirVir[$i][0] eq $dir_entrada);
}
Ya sólo queda comprimir la tabla, sobreescribiendo la fila n con la
(n+1). En cierto modo, es como si toda la TDV se colapsara fila a fila
sobre el directorio que se quiere borrar.
Se emplea un bucle anidado, lo que implica una eficiencia de orden n2,
algo perfectamente asumible42, especialmente si tenemos en cuenta que
la tabla se encuentra íntegramente almacenada en memoria.
Nótese, no obstante, que antes del bucle anidado, se comprueba que el
directorio que se pretende borrar no ocupa la última posición de la
tabla. Podría ocurrir, perfectamente, si se tratara de un directorio
vacío.
Si no se recurriera a la instrucción condicional que se muestra
inmediatamente a continuación, en un caso como el descrito
(remotamente posible, pero posible a fin de cuentas), el flujo de
ejecución no entraría directamente en el bucle anidado, y el
directorio vacío no se eliminaría.
if($i == $#TabDirVir)
{
$#TabDirVir--;
}
else
{
for(;$i<$#TabDirVir;$i++)
{
for(my $j=0;$j<=8;$j++)
{
$TabDirVir[$i][$j]=$TabDirVir[$i+1][$j];
}
}
}
Una vez borrado el directorio, se vuelve a crear, esta vez albergando
el contenido de la nueva tabla.
crea_dir($TablaNodos[$posicion][1],$tamanho,$fecha,
$propietario,$perms,$nlink,$user,$group);
}
}
A partir de este punto, comienza a llenarse el directorio recién
creado con el contenido del fichero de actualización.
Las inserciones de nuevas filas se hacen siempre al final de la TDV,
así que conviene utilizar una variable como la que aparece en la
siguiente línea de código. Contiene en todo momento la posición del
último elemento de la tabla. No es aconsejable insertar elementos en
una posición direccionada por “$#TabDirVir”, ya que, al hacerlo, la
longitud de la tabla aumentaría en 1, así que una inserción
inmeditamente posterior se produciría en la siguiente fila y no en la
misma.
my $ultimo=$#TabDirVir+1;
Esta es la primera fila de la TDV, que se cargó desde el fichero de
actualización, al principio de esta rutina. En realidad, en aquel
punto del código sólo era necesario conocer la IP del nodo propietario
de la actualización, pero por comodidad, se leyó una fila completa que
se aprovecha a continuación:
$TabDirVir[$ultimo][0]=$dir_entrada . $nombre;
$TabDirVir[$ultimo][1]=$tipo;
$TabDirVir[$ultimo][2]=$tamanho;
$TabDirVir[$ultimo][3]=$fecha;
$TabDirVir[$ultimo][4]="0";
$TabDirVir[$ultimo][5]=$perms;
$TabDirVir[$ultimo][6]=$nlink;
$TabDirVir[$ultimo][7]=$user;
$TabDirVir[$ultimo][8]=$group;
Ya se puede terminar de cargar el fichero de actualización en la TDV.
Para ello, se recurre a un bucle, en el que se permanece mientras no
se alcance el final de fichero (momento en el que al intentar leer el
nombre del siguiente fichero de la tabla remota, se obtendrá un valor
nulo).
$ultimo++;
while(1)
{
$nombre=<$fichact>;
last if !$nombre;
$tipo=<$fichact>;
$tamanho=<$fichact>;
$fecha=<$fichact>;
$propietario=<$fichact>;
$perms=<$fichact>;
$nlink=<$fichact>;
$user=<$fichact>;
$group=<$fichact>;
… no hay que olvidar que deben eliminarse los retornos de carro.
chomp($nombre);
chomp($tipo);
chomp($tamanho);
chomp($fecha);
chomp($propietario);
chomp($perms);
chomp($nlink);
chomp($user);
chomp($group);
$TabDirVir[$ultimo][0]=$dir_entrada . $nombre;
$TabDirVir[$ultimo][1]=$tipo;
$TabDirVir[$ultimo][2]=$tamanho;
$TabDirVir[$ultimo][3]=$fecha;
$TabDirVir[$ultimo][4]=$propietario;
$TabDirVir[$ultimo][5]=$perms;
$TabDirVir[$ultimo][6]=$nlink;
$TabDirVir[$ultimo][7]=$user;
$TabDirVir[$ultimo][8]=$group;
$ultimo++;
}
print "VDT updated. Proceed...\n";
}
Subrutina: “crea_dir”
Entradas: el número del directorio especial en el que se almacenará la
TDV remota, y los atributos de la raíz de ésta.
Salidas: ninguna.
Crea un directorio al final de la TDV, en el que se almacena el
contenido de una tabla de directorios remoto. Cada nodo tiene su
propio directorio asignado, y numerado de forma unívoca. Así, el
cliente encontrará, colgando de la raíz del servidor al que se
conecte, directorios con nombres como “DIR1”, “DIR2”, etc.
sub crea_dir
{
my ($numdir,$tam,$fecha,$owner,$perms,$nlink,$user,$group)[email protected]_;
my $ultimo=$#TabDirVir+1;
El tamaño del directorio puede variar de un sistema a otro, así que,
en lugar de recurrir a uno genérico, esta rutina utiliza el de la
primera entrada de la TDV (que es el “puntero a sí mismo” del
directorio raíz), y que recibe como segundo parámetro.
$TabDirVir[$ultimo][0]="/DIR".$numdir;
$TabDirVir[$ultimo][1]=0;
$TabDirVir[$ultimo][2]=$tam;
$TabDirVir[$ultimo][3]=$fecha;
$TabDirVir[$ultimo][4]=$owner;
$TabDirVir[$ultimo][5]=$perms;
$TabDirVir[$ultimo][6]=$nlink;
$TabDirVir[$ultimo][7]=$user;
$TabDirVir[$ultimo][8]=$group;
}
Subrutina “busca_fichero_en_nodos”
Entradas: el nombre del fichero que se va a buscar.
Salidas: una lista que contiene las direcciones IP de los nodos en los
que se encuentra dicho fichero.
La rutina recorre la TDV buscando las ocurrencias del archivo que se
está buscando. No bastan las coincidencias de nombre: también deben
ser iguales la fecha de la última modificación, y el tamaño en bytes.
Para llevar esto a cabo, se emplea una variable que, en todo momento,
almacena el número del directorio especial (de la serie “DIR1”… “DIRn”),
de modo que se identifica al nodo dentro de cuya tabla de directorios
se mueve el proceso de búsqueda. Basta con analizar el path completo,
y extraer el número de índice del directorio especial, caso de que
éste aparezca. De esta manera, si el algoritmo debe buscar el fichero
“ejemplo.pl” y, cuando el proceso concluye, se determina que figura en
la TDV con los caminos:
/DIR1/home/proyecto/ejemplo.pl
y
/DIR3/codigo/fuente/ejemplo.pl
… la rutina devolverá una lista que contiene las direcciones IP del
nodo cuyas tabla se almacena en DIR1, del que tiene el DIR3 dedicado a
tal menester.
sub busca_fichero_en_nodos
{
my $fichero=shift;
my @ListaNodos;
my $IP_nodo;
my $ultimo;
my $encontrado=0;
my $tam;
my $fecha;
Insisto: para determinar que se ha encontrado el archivo en cuestión,
no basta con certificar que la entrada de la TDV que se está
procesando, contiene el mismo nombre. También deben coincidir en el
tamaño y en la fecha de la última modificación. Así que es necesario
hallar éstos.
for(my $i=$puntero_tabla;$i<=$#TabDirVir;$i++)
{
if($fichero eq $TabDirVir[$i][0])
{
$tam=$TabDirVir[$i][2];
$fecha=$TabDirVir[$i][3];
$encontrado=1;
}
last if $encontrado;
}
Comienza la búsqueda. Las dos primeras líneas del siguiente bloque de
código definen sendas variables, utilizadas para almacenar el número
del directorio especial en el que se encuentra el proceso, y el del
último que se almacenó, respectivamente.
my $nodo=0;
my $ultimo_nodo=0;
for(my $i=0;$i<=$#TabDirVir;$i++)
{
my $path_tmp=dirname($TabDirVir[$i][0]);
if($path_tmp=~/\A\/DIR([0-9])/)
{
$nodo=$1;
}
my $nombre_tmp=$TabDirVir[$i][0];
if($nombre_tmp eq $fichero)
{
my $tam_tmp=$TabDirVir[$i][2];
my $fecha_tmp=$TabDirVir[$i][3];
if(($tam_tmp == $tam) && ($fecha_tmp eq $fecha))
{
En este punto, tenemos una coincidencia. Cabe la posibilidad de que un
mismo archivo se encuentre, duplicado, en varios puntos de la tabla de
directorios de un mismo nodo. Si es así, sólo se almacena en la lista
la primera ocurrencia. Sólo si el número del directorio especial con
el que se está trabajando no coincide con el último procesado, se
almacena en la lista que la rutina devolverá al final de su ejecución.
if($ultimo_nodo != $nodo)
{
my $encontrado=0;
for(my $j=0;$j<=$#TablaNodos;$j++)
{
if($TablaNodos[$j][1]==$nodo)
{
$IP_nodo=$TablaNodos[$j][0];
$encontrado=1;
}
last if $encontrado;
}
if($#ListaNodos==-1) { $ultimo=0; }
$ListaNodos[$ultimo]=$IP_nodo;
$ultimo++;
$ultimo_nodo=$nodo;
}
}
}
}
return @ListaNodos;
}
Subrutina: “descarga_archivo_remoto”
Entradas: la dirección IP del nodo al que hay que conectarse, el
tiempo máximo de espera, el nombre completo del fichero remoto y el
socket de datos abierto con el cliente.
Salidas: 1 si la descarga remota ha tenido éxito, y 0 si no.
Localiza el fichero remoto en la TDV, toma nota de su tamaño, y con
éste y otros datos, invoca al “minicliente” FTP, definido en el gestor
de la conexión, que se encarga de dirigir la petición de descarga
hacia uno de los servidores que contiene físicamente el archivo.
sub descarga_archivo_remoto
{
my ($IP_nodo,$timeout,$fichero,$socket_datos)[email protected]_;
my $exito=0;
my $path_original=dirname($fichero);
my $tamanho=-1;
El path completo contiene el nombre del directorio especial (“DIRn”).
Evidentemente, el fichero no figurará al término de un camino
semejante, en el servidor al que el proxy pretende conectarse, así que
debe procesarse en consecuencia.
La siguiente expresión regular elimina el nombre del directorio
especial.
$path_original =~ s/\A\/DIR[0-9]//;
Seguidamente, se localiza el tamaño del fichero. Es imprescindible,
para calcular el número de fragmentos de 64 Kbytes que se enviarán
hacia el cliente.
for(my $i=0;$i<$#TabDirVir;$i++)
{
if($TabDirVir[$i][0] eq $nombre_completo)
{
$tamanho=$TabDirVir[$i][2];
}
last if $tamanho != -1;
}
Sólo resta registrar la operación en el fichero de log del servidor, y
lanzar el “minicliente” FTP.
mensajes_usuario::registra("Connecting to DFP node $IP_nodo to
download $nombre_completo");
$exito=gestor_conexion::miniclienteFTP($IP_nodo,$nombre_completo,
$tamanho,$socket_datos,
$timeout);
return $exito;
}
Subrutina “tiempo_de_respuesta”
Entradas: la dirección IP de uno de los nodos que figura en la tabla
auxiliar “@NodosYTiempo”, empleada en la rutina “descarga_archivo”
para ordenar, en función de su tiempo de respuesta, la lista de hosts
que contienen un determinado archivo.
Salidas: el tiempo de respuesta de dicho nodo.
Busca en la tabla auxiliar “@NodosYTiempo”, aquel host cuya IP
coincide con la que se recibe como parámetro, y devuelve su tiempo de
respuesta (almacenado en la segunda columna). En realidad, esta rutina
sólo tiene un objetivo: mejorar la legibilidad de “descarga_remota”,
extrayendo algunas líneas de código (que son, precisamente, las que
figuran a continuación).
sub tiempo_de_respuesta
{
my ($IP,@NodosYTiempo)[email protected]_;
my $tiempo;
my $encontrado=0;
for(my $i=0;$i<=$#NodosYTiempo;$i++)
{
if($IP eq $NodosYTiempo[$i][0])
{
$encontrado=1;
$tiempo=$NodosYTiempo[$i][1];
}
}
return $tiempo;
}
Subrutina “genera_fragmento”
Entradas: el nombre del archivo que se debe retransmitir al cliente, a
través del proxy.
Salidas: el número de índice del archivo temporal generado.
Como se describe en la rutina “miniclienteFTP” (en la página 45), para
simular el reenvío en tiempo real de los datos de un fichero remoto, a
través del proxy, y hacia el cliente, se divide dicho archivo en
fragmentos de 64 Kbytes. Cada vez que el “minicliente” FTP solicita la
descarga del siguiente fragmento, se invoca a esta rutina, que genera
el siguiente, simplemente leyendo el siguiente trozo de 64 Kbytes.
Cada fragmento se identifica por un número de índice. De este modo, la
secuencia que se sigue para la descarga remota puede resumirse así:
- El proxy pide el fichero remoto.
- El servidor original general el primer fragmento: “tmpdwnld0”, que
contiene los primeros 64 Kbytes. A continuación, lo transmite hacia el
proxy.
- El proxy recibe el primer fragmento, y lo reenvía al cliente. Pide
entonces el segundo trozo.
- El servidor original borra el fichero temporal “tmpdwnld0” y genera
el siguiente “tmpdwnld1”, que contiene la información entre 64 Kbytes
+ 1 byte, y 128 Kbytes (el segundo bloque de 64 K).
El proceso continúa hasta que el fichero original se ha leído
completamente.
sub genera_fragmento
{
my $fichero=shift;
my $indice;
my $buffer;
my $path_base=$path;
chomp($path_base);
my $nombre_completo=$path_base . $fichero;
open ORIGEN,"<$nombre_completo" or die "Can't read from
$nombre_completo - $!\n";
A continuación, busca el último fichero temporal. Simplemente, lee el
contenido del directorio raíz, y aplica una expresión regular sobre
cada una de las entradas de éste. Una vez localizado el fichero, se
extrae su número de índice, y se incrementa en una unidad para
construir el nombre del siguiente.
opendir(DIRECTORIO,$path);
my @directorio=readdir(DIRECTORIO);
my $encontrado=0;
for(my $i=0;$i<=$#directorio;$i++)
{
if($directorio[$i] =~ m/tmpdwnld([0-9]*)/)
{
$indice=$1;
$encontrado=1;
}
else
{
$indice=-1;
}
last if $encontrado;
}
Si esta es la primera vez que se intenta generar un fragmento del
archivo original, no habrá ficheros temporales, luego la variable “$indice”
contendrá el valor “–1”. En caso conrtrario, se elimina el último
fichero temporal.
$indice++;
my $destino=$path . "tmpdwnld" . $indice;
if($indice)
{
my $anterior=$indice-1;
unlink $path . "tmpdwnld" . $anterior;
}
Seguidamente, se genera el siguiente fichero temporal.
open TMP,">$destino" or die "Can't write to '$destino' - $!\n";
Y ahora, se lee el bloque pertinente de 64 Kbytes. Simplemente, se
posiciona el puntero de lectura en la posición 65536*N, donde N es el
número de índice del fichero temporal que contendrá el bloque. Para
ello, se recurre al método estándar de Perl “seek”. El primer
parámetro que éste recibe es el descriptor del fichero a leer. El
segundo, el desplazamiento, esto es, el número de bytes que se ignoran
a la hora de posicionar el puntero de lectura. Y el tercero, el punto
que sirve de referencia para el desplazamiento. En este caso es un
cero, que hace referencia al principio del fichero.
Una vez ubicado el puntero, se leen 64 Kbytes, y se almacenan en el
fichero temporal. Tras esto, sólo resta cerrar los descriptores
empleados, y devolver el número de índice del archivo generado.
fseek(ORIGEN,$indice*65536,SEEK_SET);
sysread(ORIGEN,$buffer,65536);
print TMP $buffer;
close TMP;
close ORIGINAL;
close DIRECTORIO;
return $indice;
}
Subrutina “borra_ultimo_fragmento”
Entradas: el número de índice del último archivo temporal generado
para una descarga remota.
Salidas: ninguna.
Elimina el último fichero temporal utilizado en una descarga remota.
sub borra_ultimo_fragmento
{
my $indice=shift;
unlink "tmpdwnld$indice";
}
5.6.6 El gestor de la coherencia de los datos.
Hasta ahora, y con la excepción de un par de rutinas en el módulo de
directorios virtuales, todo el código comentado implementa un servidor
FTP ordinario (si bien, algo limitado). Es realmente en el gestor de
la coherencia donde tiene lugar el desarrollo del protocolo DFP.
Esta parte del proyecto se plantea como un programa que no depende del
servidor FTP, (y no como un módulo más de éste) dado que una de sus
misiones es esperar a recibir mensajes multicast procedentes de otros
nodos; el servidor FTP también aguarda a que se produzca el acceso de
clientes, y dado que ambos procesos son excluyentes (el flujo de
ejecución se detiene tanto en uno de los puntos como en el otro), si
los dos formaran parte de una misma aplicación, el sistema no
funcionaría correctamente: o estaría esperando la conexión de nuevos
clientes, o estaría aguardando la recepción de mensajes multicast,
pero nunca podría hacer las dos cosas al mismo tiempo.
El programa se basa en el envío de mensajes XML a través de un socket
multicast, a todos los nodos que forman parte del sistema de FTP
distribuido.
Cuando el host actúa como receptor, comprueba la corrección sintáctica
de los mensajes, y responde en consecuencia.
#!/usr/bin/perl -w
use strict;
use warnings;
use IO::Socket::Multicast;
use XML::DOM;
use Digest::MD5;
use Net::Ping;
use File::stat;
use File::Copy;
El programa importa tres variables globales: “$PingTimeout”, que
determina un tiempo de espera que, si se sobrepasa, hará que un nodo
dado se considere como inaccesible, “$Password”, que contiene la
contraseña del grupo DFP, y que se utiliza durante la generación de la
firma digital de los mensajes y “$DeleteNode”, q ue contiene un 1 si
el administrador ha decidido que el presente nodo abandone la red DFP.
En ese caso, se debe enviar un mensaje de tipo “Delete Node” a todos
los integrantes de la misma.
use variables_globales qw($PingTimeout $Password $DeleteNode);
A continuación, se definen las variables globales que se utilizarán en
la aplicación.
La primera es la tabla de nodos. Tiene tres columnas, que contienen
las direcciones IPv4 de los nodos del sistema, sus tiempos de
respuesta en milisegundos, y la versión IPv6 de la dirección del nodo.
Es necesario distinguir entre la IPv4 y la IPv6, ya que la segunda
puede variar incluso entre dos mensajes procedentes de un mismo nodo,
mientras que la primera se mantiene constante. Así pues, el contenido
de la primera columna de la “@TablaNodos”, se utiliza para identificar
unívocamente a los nodos que integran el sistema (dado que este
proyecto está orientado a redes de área local, puede descartarse la
posibilidad de que, en un futuro, cuando se consuman todas las
direcciones IP, coincidan dos IPv4 de sendas máquinas del sistema
DFP), y la segunda se utiliza para enviar mensajes a nodos puntuales
(en unicast), mucho más recomendable en la versión de Perl utilizada
para el desarrollo de este proyecto.
our @TablaNodos;
La siguiente variable albergará el mensaje de 4 Kbytes de longitud,
remitido por uno de los nodos del sistema.
our $mensaje;
En “$IP_emisor” (nótese que no es global) se almacenará la dirección
IPv6 del nodo que transmitió el último mensaje recibido.
my $IP_emisor;
La interconexión de dos nodos que deben compartir sus tablas, es
trivial. Casi podríamos considerarlo como el caso base. No obstante,
cuando nos enfrentamos a la coordinación de varias máquinas que se
envían mensajes de un tamaño más que respetable (las TDV pueden
alcanzar fácilmente varios megabytes), surge un problema: cuando un
nuevo nodo se incorpora al sistema y envía un mensaje avisando al
resto de su presencia, todos le transmiten sus tablas locales al mismo
tiempo. Dado que los mensajes de gran tamaño se transfieren en trozos
de 4 Kbytes, el riesgo de que se produzcan colisiones entre los envíos
de los nodos, y se intercalen dichos fragmentos, es evidente. Los
mensajes recibidos no estarían bien formados, de acuerdo con las
reglas sintácticas del lenguaje XML, y el parser lo detectaría,
abortando por ello al proceso hijo encargado de trabajar con él.
La solución consiste en tener tantos ficheros como nodos haya en el
sistema. Cada mensaje procedente de un host distinto, se almacena en
su propio archivo, para evitar colisiones, y que se mezclen los
fragmentos de 4 Kbytes de tablas diferentes.
La idea es numerar estos ficheros (concretamente, de 0 a 999; claro
que podrían surgir problemas si la red estuviera formada por 1.000
nodos, pero es poco probable que haya semejante cantidad de
servidores; de todas maneras, si se diera el caso, sólo habría que
retocar una línea de código). Así, el primer fragmento recibido, se
almacenará en el fichero “MessgRcv0.xml”. El segundo, si pertenece a
un nodo distinto, en “MessgRcv1.xml”. Cuando se utilice el
“MessgRcv999.xml”, el siguiente volverá a emplear el “0” como número
de orden.
our $indice_fichero=0;
Para determinar cuándo ha terminado el procesado de los mensajes de
actualización, y así avisar al servidor FTP, hay que comprobar que no
quedan procesos hijos trabajando y que, además, los archivos que
contienen el resultado de dicho procesamiento, están presentes en el
disco.
Esta idea se basa en el hecho de que, cada vez que se termina de
recibir completamente un mensaje, el gestor de la coherencia deriva un
proceso hijo, para analizar la sintaxis del mismo y, si es correcta y
se trata de una actualización, generar a partir de él el fichero que
deberá leer el servidor FTP. La estrategia tiene tres ventajas
esenciales:
1.
Permite que el gestor de la coherencia siga a la escucha de nuevos
mensajes.
2.
El parser XML tiene la nefasta costumbre de abortar al proceso que
lo ha lanzado si detecta un error de sintaxis en el fichero que
está analizando. Si se invocara desde la tarea padre, la mitad de
la aplicación se desmoronaría cada vez que se recibiera un mensaje
mal formado.
3.
Mejora muy significativamente la eficiencia del programa. Si
hubiera que analizar la sintaxis de cada mensaje de actualización
por separado, secuencialmente, se emplearían enormes cantidades de
tiempo. Sin embargo, derivando un proceso hijo cada vez que se
termina de recibir un mensaje, es posible procesar tantas
actualizaciones como sea necesario, en paralelo.
La variable global que se declara a continuación, almacena el número
de procesos hijos que están trabajando, en cualquier momento. Antes de
que el padre derive uno de ellos, la variable se incrementa en uno, y
cada vez que se detecta la activación de una interrupción software de
tipo “CHLD” (que se produce cuando un proceso hijo termina, al
ejecutar la instrucción “exit”), disminuye en uno.
our $numero_hijos=0;
Las línea siguiente define la rutina llamada “manejadora” de la
interrupción software CHLD, que se activará cuando uno de los procesos
hijos termine de trabajar con un mensaje XML.
$SIG{CHLD}=sub { termina_hijo() };
A partir de este punto comienza el programa principal.
print "Initializing coherence manager...\n";
registra("Initializing coherence manager...");
A continuación, se crea un socket para la comunicación multicast,
empleando el método “new” definido en la librería “IO::Socket::Multicast”.
Recibe el parámetro “LocalPort”, que define el puerto en el que
escucharán todos los nodos. Es importante resaltar que, a diferencia
del puerto del servidor FTP, este no es configurable, ya que todas las
máquinas del sistema deben abrir el socket multicast en el mismo
puerto. Si se permitiera que una de ellas escuchara en uno distinto,
permanecería aislada de la red DFP.
our $socket_udp=IO::Socket::Multicast->new(LocalPort=>1100)
or die "Error: $!\n";
Ahora, se utiliza el método “mcast_add” para sumar el nodo local al
grupo multicast. La dirección compartida por todos los hosts de dicho
grupo, es “225.0.1.1”. No se permite su manipulación por el mismo
motivo que desaconseja que el parámetro “LocalPort” sea libremente
configurable. En conclusión, todas las máquinas que pertenezcan a la
red FTP, estarán agrupadas bajo una dirección multicast y un puerto
comunes: “225.0.1.1:1100”
$socket_udp->mcast_add('225.0.1.1');
Hay que evitar que un nodo reciba sus propios mensajes. De lo
contrario, el protocolo no funcionaría correctamente. Para
deshabilitar la “autorrecepción”, se emplea el método siguiente:
$socket_udp->mcast_loopback(0);
Antes de continuar, se comprueba si el administrador decidió dar de
baja al presente nodo, en cuyo caso, se envía a todos los demás un
mensaje de tipo “Delete Node”, y se termina el proceso.
if($DeleteNode)
{
envia_mensaje(3,0);
die "Multicasting 'Delete Node' message. Bye.\n";
}
Seguidamente, el nodo comprueba si tiene mensajes pendientes de ser
procesados. Dado que el análisis de la sintaxis XML y la generación de
las actualizaciones suelen consumir un tiempo respetable, hay que
tener en consideración la posibilidad de que un nodo sufra una caída
antes de terminar esta tarea.
La táctica se basa en comprobar si existen ficheros de recepción. Dado
que cada uno se elimina cuando el proceso hijo al que está asociado
termina de trabajar con él, si cuando arranque el gestor de la
coherencia se detecta que existe alguno, la única explicación es que
el sistema se reinició antes de que el proceso hijo tuviera tiempo de
borrarlo.
En ese caso, antes de que el nodo pueda informar al resto de su
reincorporación al sistema, debe terminar de procesar todos los
mensajes pendientes.
El siguiente bucle comprueba si existen cualquiera de los ficheros de
recepción, entre “MessgRcv0.xml” y “MessgRcv999.xml”. Hay que tener en
cuenta, sin embargo, que no basta con verificar que un archivo
concreto figura en el disco para poder procesar la actualización
pendiente, ya que en Perl, los cambios que se efectúen sobre un
fichero no se hacen definitivos hasta que se cierra su descriptor. Y
si el nodo ha sufrido algún problema que le haya impedido concluir
correctamente la recepción del mensaje, el flujo de ejecución no habrá
tenido tiempo de llegar hasta la instrucción que lo cierra, de modo
que aparecerá en disco, sí, pero con tamaño 0.
for(my $i=0;$i<=999;$i++)
{
if(-e "MessgRcv$i.xml")
{
Se ha comprobado la existencia de uno de los ficheros de recepción.
Ahora, hay que garantizar que no está vacío. Sólo si su tamaño es
distinto de cero, se permite invocar a la rutina que inicia el proceso
de actualización de la TDV. En cualquier caso, la instrucción
condicional termina borrando el fichero de recepción.
my $prueba=stat("MessgRcv$i.xml");
if($prueba->size)
{
actualiza_tdv();
}
unlink "MessgRcv$i.xml";
}
}
Cuando se alcanza este punto, el nodo está preparado para informar al
sistema que está disponible. En primer lugar, transmite por multicast
su tabla de directorios virtuales, y a continuación, también por
multicast, un mensaje de tipo “Node online”. Éste se emplea para que
los demás hosts de la red DFP respondan transmitiendo confirmaciones
al recién llegado. De esta manera, el nodo puede construir su propia
tabla de nodos con las direcciones IP de todas las máquinas del
sistema que transmiten dichas confirmaciones.
El proceso es necesario tanto si el host es un recién llegado como si
se trata de una máquina que ya ha accedido en otras ocasiones al
sistema, y que vuelve tras una desconexión temporal.
En la rutina “envia_mensaje”, el primer parámetro determina el tipo de
mensaje que debe transmitirse, y el segundo, si se trata de un envío
multicast (a todos los nodos) o unicast (solamente a uno de ellos). En
ese último caso, se utiliza un tercer parámetro que contiene la
dirección IP del host de destino. Así, la siguiente llamada a la
función “envia_mensaje”, cuyos parámetros son 2 y 0, transmite un
mensaje de tipo “Node online”, por multicast.
envia_VDT_update(0);
print "Multicasting 'Node online' message...\n";
registra("Multicasting 'Node online' message");
envia_mensaje(2,0);
A partir de este punto se entra en el bucle principal, en el que se
implementa el protocolo DFP en sí, esto es, fundamentalmente se
gestionan las respuestas a los mensajes remitidos desde otros nodos.
Un host sólo comienza una conversación “de motu proprio” cuando se
incorpora al sistema, ya sea como recién llegado o recuperándose de
una desconexión debida a tareas de mantenimiento o a un fallo técnico.
En cualquier otro caso, las máquinas que forman parte de la red DFP se
limitan a responder a avisos y actualizaciones.
La idea consiste en esperar un mensaje y, en función de su tipo y
formato (aquí es donde entra el parser XML), generar la respuesta
pertinente.
A continuación, se declaran las variables locales que se utilizarán en
el bucle. La primera de ellas, llamada “$abierto”, contiene el valor 0
si el fichero de recepción aún no ha sido abierto, y 1 en caso
contrario. Así se evita que en cada iteración se intente abrir un
archivo que ya lo estaba.
my $abierto=0;
La siguiente variable local, “$quiensoy”, identifica al proceso que la
evalúa, después de la ejecución de la instrucción “fork” para derivar
un hijo. Recordemos que este método devuelve un 0 al hijo, y al padre,
la identificación numérica de éste.
my $quiensoy=0;
El procesado de un mensaje puede requerir bastante tiempo. No sería
razonable que el gestor de la coherencia no pudiera volver a ponerse a
la escucha mientras una actualización no se hubiera analizado
completamente. Eso por eso por lo que se recurre a la derivación de
hijos, que se encargan de esta tarea. Sin embargo, es conveniente que
cada hijo trabaje sobre su propio fichero. Si todos compartieran el
mismo, se producirían conflictos de todo tipo, especialmente porque el
padre seguiría escribiendo en él cada vez que recibiera un fragmento
de una actualización, u otro tipo de mensaje, mientras los hijos
intentan leerlo. Por eso se utilizan, entre otras, variables como “$fichero_hijo”,
que almacena el nombre del fichero que contiene una copia del último
mensaje recibido, y sobre la que trabaja el último hijo.
my $fichero_hijo;
print "Ready...\n";
La etiqueta “ESCUCHA”, que aparece justo antes del bucle, se utiliza
con el mismo propósito que “NEXT”, en el gestor de la conexión, es
decir, para forzar la iteración del padre justo después de que derive
un hijo, y así evitar que siga ejecutando código destinado a éste.
ESCUCHA:
while(1)
{
Antes de ponerse a la escucha, el gestor comprueba si hay que avisar
al servidor FTP porque tiene una actualización preparada. Esto ocurre
cuando existen ficheros con actualizaciones procesadas en el disco, y
no quedan hijos funcionando (todos han terminado ya).
comprueba_si_actualizacion();
Ahora, se escucha sobre el socket multicast. Los mensajes recibidos se
almacenan en la variable “$mensaje” que hace las veces de un buffer de
4096 bytes de longitud.
El flujo de ejecución se detiene aquí, hasta que el nodo reciba una
trama UDP procedente de alguno de los otros hosts que integran la red.
En ese momento, se registra su dirección IP en la variable “$IP_emisor”,
y se sigue adelante.
$IP_emisor=$socket_udp->recv($mensaje,4096);
Cada mensaje de 4 Kbytes que se reciba, se escribe en el archivo de
recepción, “messrecv.xml”. Para los mensajes de aviso, esos 4096 bytes
son más que suficientes. Los que contienen una TDV, no obstante, casi
siempre requieren bastantes fragmentos.
Antes de intentar escribir el mensaje, se comprueba si el fichero de
recepción está abierto.
if(!$abierto)
{
open MENSAJE,">messrecv.xml"
or die "Cannot open messrecv.xml: $!\n";
$abierto=1;
}
print MENSAJE $mensaje;
Antes de continuar iterando, se debe comprobar si el último mensaje
recibido contenía la etiqueta de cierre: “”. En caso
afirmativo, se habrá completado la recepción de un mensaje, y se debe
derivar un hijo para que lo procese, mientras el padre vuelve a
ponerse a la escucha.
Se cierra entonces el archivo en el que se almacena el mensaje
recibido, se procesa la dirección IP del nodo remitente, para
adaptarla al formato IPv4 y hacerla legible, se anota la operación en
el log del gestor de la coherencia, y se continúa.
if($mensaje=~/<\/DfpMessage>/)
{
close MENSAJE;
$abierto=0;
my $IP_procesada=procesa_IP($IP_emisor);
registra("Message received from $IP_procesada");
Si el flujo de la ejecución alcanza este punto, significa que se ha
recibido un mensaje completo. Puede, por tanto, comenzar el proceso de
derivación de un hijo que trabaje con él. No obstante, son necesarias
dos operaciones previas; a saber:
- Determinar la procedencia del mensaje, analizando la dirección IP
del nodo remitente, para comprobar si es un nuevo miembro del sistema,
si se trata de uno antiguo que sigue funcionando correctamente, o si,
por el contrario, se reincorpora tras una desconexión temporal.
comprueba_nodo($IP_emisor);
Esta rutina debe invocarse antes de la derivación del hijo, para
garantizar que éste hereda la última versión de la tabla de nodos.
Sólo el proceso padre puede modificarla.
- Y generar el nombre del fichero sobre el que trabajará el proceso
hijo. Entonces, se copia el contenido del archivo de recepción en
éste.
my $fichero_hijo="MessgRcv$indice_fichero.xml";
copy("messrecv.xml",$fichero_hijo);
Ya podemos eliminar el fichero de recepión.
unlink "messrecv.xml";
Por fin, se deriva el hijo.
if($quiensoy=fork)
{
Sólo el padre puede entrar en esta zona del código, ya que sólo él
obtiene un valor distinto de cero como respuesta a la llamada a “fork”.
Antes de volver a ponerse a la escucha, el padre recalcula los tiempos
de respuesta de los nodos que conforman la red. No olvidemos que son
esenciales para las descargas remotas, que se implementan en el
servidor FTP.
calcula_tiempo_respuesta();
Incrementa la cuenta de hijos que se han derivado.
$numero_hijos++;
A continuación, se incrementa el índice que enumera a los ficheros con
los que trabajan los hijos. Se aplica una operación módulo 1.000 para
asegurar que la variable tome valores entre 0 y 999.
$indice_fichero=($indice_fichero+1)%1000;
Y ahora sí: fuerza la siguiente iteración. Así se evita que el padre
ejecute código destinado exclusivamente a los hijos.
next ESCUCHA;
}
Si no hubiera recursos suficientes para derivar un proceso hijo, la
situación sería potencialmente grave. Lo más sensato en ese caso sería
terminar el proceso inmediatamente:
defined($quiensoy)
or die "Cannot fork at coherence manager - $!\n";
Esta región del código sólo es visible para los hijos del gestor de la
coherencia. A partir de este punto comienza a procesarse el mensaje
recibido.
Lo primero es almacenar la dirección IP del emisor de dicho mensaje,
en su propia variable local. El hecho de que “$IP_emisor” sea global
desaconseja que cualquier proceso pueda modificarla a la ligera, y se
deja tal privilegio al padre (aunque, en teoría, no debería ocurrir
ningún problema, ya que la “$IP_emisor” que utiliza cualquier proceso
hijo, es una copia de la que emplea el padre, no la original; aún así,
prudencia obliga…).
my $IP_emisor_hijo=$IP_emisor;
registra("Forked child number $indice_fichero");
Seguidamente, se aplica el parser XML para identificar el tipo del
mensaje.
my $parser=new XML::DOM::Parser;
registra("Child process $indice_fichero is parsing file
$fichero_hijo");
my $result=$parser->parsefile($fichero_hijo);
Primero, se recurre a la rutina “obtener_contenido” para identificar
al emisor del mensaje. Se extrae el contenido del elemento “”, que
no es otro que la firma digital del mensaje, más la contraseña del
grupo. Si se determina la corrección de ésta, mediante una llamada a
la rutina “comprueba_MD5”, la variable “$legal” almacenará un 1. En
caso contrario, contendrá un 0, y el mensaje habrá de ser descartado.
my $contenido=obtener_contenido("id",$result);
my $legal=comprueba_MD5($contenido,$fichero_hijo);
Una vez comprobado que el emisor está accediendo legalmente a la red
DFP, se determina el tipo del mensaje recibido.
if($legal)
{
my $IP_desde=procesa_IP($IP_emisor_hijo);
$contenido=obtener_contenido("ResponseId",$result);
Ya sólo queda actuar en función del tipo obtenido.
Si se trata de un mensaje de confirmación, (es decir, de tipo “Message
received”), se utiliza para identificar al nodo que lo transmitió.
Esta clase de mensajes sólo se reciben como respuesta al envío de “Node
online” por parte de los nodos que se incorporan a la red, por primera
vez o tras haber estado temporalmente desconectados.
Si se trata del primer caso, el host aprovecha el mensaje para
almacenar la dirección IP del remitente, y así ir construyendo su
tabla de nodos, y para solicitarle su TDV local.
if($contenido eq "1")
{
print "Message received notice...\n";
print "Asking for VDT...\n";
registra("Child process $indice_fichero has received a 'Message
received'
notice from $IP_desde");
registra("Child process $indice_fichero is asking $IP_desde for its
VDT...");
pide_TDV($IP_emisor_hijo);
}
Si el mensaje recibido es de tipo “Node online”, el nodo se limita a
enviar una confirmación al nodo remitente (en unicast, por tanto).
elsif($contenido eq "2")
{
envia_mensaje(1,1,$IP_emisor_hijo);
print "'Node online' message received\n";
registra("Child process $indice_fichero has received a 'Node online'
message from $IP_desde");
}
Cuando un host solicita darse de baja de la red DFP, simplemente envía
por multicast un mensaje de tipo “Delete node”. Todos los receptores
actúan en consecuencia, eliminándolo de su tabla de nodos.
elsif($contenido eq "3")
{
print "'Delete node' message received\n";
borrar_nodo($IP_emisor_hijo);
registra("Child process $indice_fichero has received a 'Delete node'
message from $IP_desde");
}
Si el mensaje es una actualización de la tabla de directorios local,
es necesario iniciar un proceso relativamente complejo, que sólo
concluirá cuando el servidor FTP haya modificado su TDV de acuerdo a
los datos recibidos. Éstos deben procesarse (recordemos que lo que en
realidad se recibe es un archivo XML que contiene las filas de la
tabla local del nodo remitente; esos datos han de extraerse, y
prepararse para que el servidor FTP pueda utilizarlos directamente).
elsif($contenido eq "4")
{
print "VDT Update message received\n";
registra("Child process $indice_fichero has received a 'VDT Update'
message from $IP_desde");
procesa_tdv_recibida($result,$IP_emisor_hijo);
actualiza_tdv();
}
elsif($contenido eq "5")
{
print "Get VDT message received\n";
registra("Child process $indice_fichero has received a 'Get VDT'
message
from $IP_desde");
Si el flujo de ejecución alcanza esta rama de la instrucción
condicional, significa que la firma digital del mensaje recibido no es
correcta, así que éste debe descartarse. Se advierte del problema al
administrador, y se registra en el fichero de log.
{
else
{
print "Warning - Wrong CRC! Discarding message\n";
registra("Child process $indice_fichero got a message with a
wrong CRC from $IP_desde");
}
Se libera la memoria ocupada por el resultado del análisis del mensaje
XML.
$result->dispose;
El proceso hijo ha terminado su trabajo. Ya sólo le resta borrar su
fichero de recepción, y llamar al método “exit”, para activar la señal
“CHLD” y conseguir así que el padre pueda liberar sus recursos.
unlink $fichero_hijo;
exit;
}
}
Subrutina: “envia_mensaje”.
Entradas: dos parámetros que deben aparecer en cualquier caso (uno de
ellos determina el tipo de mensaje que se quiere enviar, y el otro
define el modo de envío: multicast o unicast), y uno, que contiene la
dirección IP del nodo de destino, en caso de que el envío sea unicast.
Salidas: ninguna.
Esta rutina genera un archivo XML que contiene el mensaje, y después
lo transmite, bien mediante multicast a todos los nodos del sistema, o
bien mediante unicast, a un solo nodo cuya dirección, ese caso, vendrá
especificada en el tercer parámetro.
sub envia_mensaje
{
my ($tipo_mensaje,$multuni)[email protected]_;
my $fichero;
Excepto las actualizaciones de la TDV, todos los mensajes del
protocolo DFP miden menos de 4 Kbytes. Aún así, por guardar una cierta
consistencia con la rutina que transmite las actualizaciones, en esta
se utiliza también un buffer de 4096 bytes de longitud.
my $buffer;
La rutina a la que se invoca a continuación genera un mensaje XML del
tipo especificado por el parámetro “$tipo_mensaje”, lo almacena en un
archivo, y devuelve el descriptor del mismo.
$fichero=genera_mensaje_xml($tipo_mensaje);
Lee el contenido del fichero, y lo almacena en el buffer.
read($fichero,$buffer,4096);
Ya sólo queda enviarlo a través del socket UDP. Si el parámetro
“$multuni” contiene un 0, hay que enviarlo en multicast. En ese caso,
la dirección y el puerto de destino son los del grupo multicast:
225.0.1.1:1100.
Si por el contrario, “$multuni” contiene un 1, el envío debe
realizarse a un nodo concreto, cuya dirección se recibe como tercer
parámetro de la rutina (nótese que se emplea la IPv6).
if(!$multuni)
{
$socket_udp->mcast_send($buffer,'225.0.1.1:1100');
}
else
{
$socket_udp->mcast_send($buffer,$IP_remota);
Y antes de terminar, se cierra el fichero y se borra.
close $fichero;
unlink “message$indice_fichero.xml”;
}
Subrutina: “genera_mensaje_xml”
Entradas: el número identificador del tipo de mensaje que se va a
generar.
Salidas: el descriptor del archivo que contiene el mensaje XML
generado.
El funcionamiento de esta función es bastante sencillo: simplemente
escribe un archivo en disco, con formato XML, que contiene el mensaje
que se va a enviar.
Todos estos mensajes comparten la misma estructura general (la DTD),
salvo los que transportan la TDV local. Por eso, la rutina debe
distinguir entre unos y otros.
De hecho, la única complicación (relativa, la verdad) tiene que ver
con el cálculo de la firma digital, aplicando el algoritmo MD5 al
cuerpo del mensaje.
La idea consiste en escribir el mensaje en disco, hasta que se alcanza
el punto en el que va a comenzar a generarse el cuerpo. En ese
instante, se envían los datos como parámetros del método “add” de la
librería Digest::MD5, el cual se encarga de construir la firma.
Una vez que el cuerpo ha sido procesado, podría entenderse en cierto
modo que el proceso “vuelve atrás”, para generarlo de nuevo, sólo que
esta vez se envía al fichero de salida.
sub genera_mensaje_xml
{
my $tipo_msg=shift;
my $cadena_tipo_msg;
my $fichero;
Cada hijo utiliza su propio fichero para almacenar el mensaje
generado. Hay que tener en cuenta que en un momento dado puede haber
más de un proceso hijo trabajando, cada uno con su copia de esta
rutina, así que si todos escribieran en el mismo archivo, se
producirían colisiones.
Por ello, se sigue una táctica análoga a la descrita en el programa
principal, y se utiliza la variable global “$indice_fichero” para
asignar un nombre de archivo a cada hijo, entre “message0.xml” y
“message999.xml”.
open $fichero,">message$indice_fichero.xml"
or die “Can’t write to ‘message$indice_fichero.xml’ - $!\n”;
Ahora, en función del parámetro “$tipo_msg”, se genera el contenido
del elemento XML “ResponseType”.
if($tipo_msg == 1)
{
$cadena_tipo_msg="Message received";
}
elsif($tipo_msg == 2)
{
$cadena_tipo_msg="Node online";
}
elsif($tipo_msg == 3)
{
$cadena_tipo_msg="Delete node";
}
elsif($tipo_msg == 5)
{
$cadena_tipo_msg="Get VDT";
}
La primera línea del encabezamiento es la misma para todos los
mensajes.
print $fichero "\n";
Como ya se ha dicho, la rutina debe distinguir entre los mensajes que
contienen la TDV local, y el resto. Aquéllos usan el identificador
“4”. Si se trata de un aviso, se incluye una referencia al archivo que
contiene la DTD de este tipo de mensajes: “notice.dtd”.
if($tipo_msg!=4)
{
print $fichero “”;
… de modo que si estamos generando un mensaje con un identificador
distinto, se tratará de un aviso ordinario. Ya sólo hay que escribir
en el fichero de salida el tipo del mensaje que estamos generando y el
cuerpo del mismo (es decir, los elementos “ResponseId” y “ResponseType”).
Ahora, se genera la firma digital con el cuerpo del mensaje. Primero,
se crea un objeto de tipo Digest::MD5.
my $md5=Digest::MD5->new;
Se aplica el algoritmo sobre las líneas del cuerpo del mensaje.
$md5->add(" \n");
$md5->add(" $tipo_msg\n");
$md5->add(" $cadena_tipo_msg\n");
$md5->add("
\n");
La firma se completa añadiendo la contraseña del grupo DFP, y
aplicando el algoritmo.
$md5->add("$Password");
my $resultadoMD5=$md5->hexdigest;
Ya sólo queda escribir en el fichero de salida el cuerpo del mensaje,
añadiendo el resultado del checksum43 en el elemento .
print $fichero "\n";
print $fichero "$resultadoMD5\n";
print $fichero " \n";
print $fichero " $tipo_msg\n";
print $fichero " $cadena_tipo_msg\n";
print $fichero "
\n";
print $fichero "
\n";
}
Sin embargo, si el tipo de mensaje que se está generando es “4”, se
trata de una actualización de la TDV, con una estructura sensiblemente
más compleja que la de los avisos. Además, requieren que se utilice el
contenido del fichero “localVDT.txt”, generado por el servidor FTP
cuando arranca por primera vez, y en el que se guardan las filas de la
tabla de directorios local.
elsif($tipo_msg==4)
{
print $fichero “
my $md5=Digest::MD5->new;
$md5->add("4\n");
$md5->add("VDT Update\n");
$md5->add(" \n");
open TDV," or die "Can't read from 'localVDT.txt' - $!\n";
while($_=TDV->getline)
{
$md5->add(" \n");
chomp($_);
$md5->add(" $_\n");
chomp($_=TDV->getline);
$md5->add(" $_\n");
chomp($_=TDV->getline);
$md5->add(" $_\n");
chomp($_=TDV->getline);
$md5->add(" $_\n");
chomp($_=TDV->getline);
$md5->add(" $_\n");
chomp($_=TDV->getline);
$md5->add(" $_\n");
chomp($_=TDV->getline);
$md5->add(" $_\n");
chomp($_=TDV->getline);
$md5->add(" $_\n");
chomp($_=TDV->getline);
$md5->add(" $_\n");
$_=TDV->getline;
$md5->add("
\n");
}
$md5->add("
\n");
$md5->add("$Password");
close TDV;
my $resultadoMD5=$md5->hexdigest;
Ahora, me temo que es necesario volver a abrir el fichero que contiene
la TDV local, y procesarlo desde el principio para generar el mensaje
XML44.
open TDV," or die “Can’t read from ‘localVDT.txt’ - $!\n”;
while($_=TDV->getline)
{
print $fichero " \n";
chomp($_);
print $fichero " $_\n";
chomp($_=TDV->getline);
print $fichero " $_\n";
chomp($_=TDV->getline);
print $fichero " $_\n";
chomp($_=TDV->getline);
print $fichero " $_\n";
chomp($_=TDV->getline);
print $fichero " $_\n";
chomp($_=TDV->getline);
print $fichero " $_\n";
chomp($_=TDV->getline);
print $fichero " $_\n";
chomp($_=TDV->getline);
print $fichero " $_\n";
chomp($_=TDV->getline);
print $fichero " $_\n";
$_=TDV->getline;
print $fichero "
\n";
}
print $fichero " \n";
print $fichero "\n";
close TDV;
}
Para terminar, se vuelve a abrir el fichero, esta vez en modo
escritura, y se devuelve el descriptor, listo para que la rutina que
va a utilizar el mensaje generado, lo lea sin más preámbulos.
close($fichero);
open $fichero," or die "Cannot read from 'message$indice_fichero.xml' - $!\n";
return $fichero;
}
Subrutina: “obtener_contenido”
Entradas: el identificador del elemento XML cuyo contenido se quiere
extraer (“$etiqueta”), y el descriptor del fichero en el que se
almacena el resultado de la aplicación del parser al mensaje recibido
(“$docxml”).
Salidas: el contenido del elemento identificado por el primer
parámetro.
Utilizando el método “getElementsByTagName” de la librería XML::DOM,
se obtiene una lista de nodos que coincidan con la etiqueta (“tag”)
que se pasa como primer parámetro.
Así, si por ejemplo, el argumento contiene la cadena “ResponseId”, el
método “getElementsByTagName” devolverá una lista formada por todos
los nodos de la forma: …bla bla bla….
Conviene aclarar que dichos nodos no son directamente imprimibles.
Antes, hay que aplicarles el método “toString”, que consigue
convertirlos en una cadena.
Pues bien, la idea consiste en escoger el primer nodo de esa lista (en
realidad, sólo habra uno, dado que el elemento
aparece una sola vez en la cabecera de los mensajes),
convertirlo en una cadena, y eliminar las etiquetas. En resumen, si
queremos obtener el contenido del elemento:
Get VDT
… al eliminar y , nos quedaremos con “Get
VDT”, que es precisamente lo que devolverá la rutina. El código
aparece a continuación:
sub obtener_contenido
{
my($etiqueta,$docxml)[email protected]_;
Se obtienen todos los nodos en los que aparezca la “$etiqueta”.
my $nodos=$docxml->getElementsByTagName($etiqueta);
De esos, nos quedamos con el primero.
my $nodo=$nodos->item(0);
my $contenido;
Y si ese primer nodo está definido (lo cierto es que la siguiente
comprobación no pasa de ser una medida de seguridad que, lo admito,
podría eliminarse sin miramiento alguno), se invoca al método “toString”
para convertirlo en una cadena de texto imprimible.
if(defined($nodo))
{
$contenido=$nodo->toString;
}
else
{
$contenido="undef";
}
Se eliminan las etiquetas de apertura y cierre del elemento, echando
mano de sendas expresiones regulares en las que éstas se sustituyen
“por nada”.
$contenido=~s/<$etiqueta>//;
$contenido=~s/<\/$etiqueta>//;
return $contenido;
}
Subrutina: “comprueba_nodo”
Entradas: la dirección IP del nodo que ha enviado el mensaje que se va
a procesar.
Salidas: ninguna.
El objetivo de este procedimiento es determinar si el host que ha
remitido el último mensaje, figura en la “@TablaNodos”. En caso
afirmativo, se sigue adelante, sin más.
Si el host es un recién llegado, no figurará en la tabla interna. No
hay más que darle de alta.
sub comprueba_nodo
{
my ($IPv6)[email protected]_;
my $encontrado=0;
my $i;
La dirección recibida tiene el formato IPv6. Para compararla con las
que ya están almacenadas en la “@TablaNodos”, se convierte previamente
a IPv4.
my $IPv4=procesa_IP($IPv6);
for($i=0; $i<=$#TablaNodos; $i++)
{
if($IPv4 eq $TablaNodos[$i][0])
{
$encontrado=1;
}
last if $encontrado;
}
if(!$encontrado)
{
Es un nuevo miembro, así que se le da de alta en la tabla de nodos.
my $ultimo=$#TablaNodos+1;
$TablaNodos[$ultimo][0]=$IPv4;
$TablaNodos[$ultimo][2]=$IPv6;
Sólo queda por rellenar una columna de la tabla, y es la que contiene
el tiempo de respuesta del nodo recién llegado (obtenido mediante
algunos de los métodos de la librería estándar de Perl “Net::Ping”).
Recuérdese que el valor obtenido se utilizará a cargo del servidor FTP
en las descargas remotas.
El procedimiento es el siguiente: se almacena la hora del sistema,
justo antes de hacer un “Ping” a la máquina en cuestión. En cuanto
transcurre el tiempo contenido en la variable global “$PingTimeout”,
se vuelve a guardar la hora del sistema, y se calcula la diferencia
entre ésta y la que se leyó justo antes de hacer el Ping. El resultado
se almacena en la tabla de nodos. Si el host no responde antes de que
transcurra ese tiempo, se asume que tiene problemas, lo que se
representa guardando el valor “-1” en la tabla.
El método “times” devuelve una lista que contiene el tiempo, en
segundos, consumido por el usuario, el sistema y sus procesos hijos.
Para calcular el de respuesta, basta con utilizar uno de los elementos
de esa lista; por ejemplo, el primero.
my $tiempo_antes=(times)[0];
my $p=Net::Ping->new("tcp",$PingTimeout);
$p->ping($IPv6);
El método “ping” devuelve un valor definido si el host ha respondido
antes del tiempo definido por “$PingTimeout”.
if($p)
{
Ahora, se vuelve a emplear la función “times”, y se calcula la
diferencia con el resultado de su aplicación antes de hacer el “Ping”.
my $tiempo_despues=(times)[0];
$TablaNodos[$ultimo][1]=$tiempo_despues-$tiempo_antes;
}
else
{
Si el nodo no respondió, se almacena un “-1” como su tiempo de
respuesta.
$TablaNodos[$ultimo][1]=-1;
}
$p->close();
}
Antes de terminar, se hace una copia de seguridad de la tabla de
nodos. Si sólo se almacenara en memoria, cada vez que el nodo se
apagara o se reiniciara, todos los datos se perderían, forzando a la
máquina a comportarse como un host recién llegado cuando volviera a
lanzar la aplicación.
open TABNOD,">NodesTable.txt"
or die "Can't write to NodesTable.txt - $!\n";
for($i=0;$i<=$#TablaNodos;$i++)
{
print TABNOD "$TablaNodos[$i][0]\n";
print TABNOD "$TablaNodos[$i][1]\n";
print TABNOD "$TablaNodos[$i][2]\n”;
}
close TABNOD;
}
Subrutina: “envia_VDT_update”
Entradas: un parámetro que determina si el envío se hace a todos los
nodos, o sólo a uno de ellos, y sólo en este caso, la dirección IP del
mismo.
Salidas: ninguna.
Genera un mensaje XML que contiene la TDV local, y la envía, bien por
multicast a todos los demás nodos, bien por unicast a uno en concreto.
En cualquier caso, el mensaje se transmite en fragmentos de 4 Kbytes.
sub envia_VDT_update
{
my ($multuni,$IP_remota)[email protected]_;
my $fichero;
my $buffer;
my $leidos;
$fichero=genera_mensaje_xml(4);
Si “$multuni” contiene un 0, el mensaje se envía a todos los nodos. Y
si contiene un 1, se envía a uno concreto.
if(!$multuni)
{
while(read($fichero,$buffer,4096))
{
$socket_udp->mcast_send($buffer,'225.0.1.1:1100');
}
}
else
{
while(read($fichero,$buffer,4096))
{
$socket_udp->mcast_send($buffer,$IP_remota);
}
}
print "VDT Update sent...\n";
registra("Child process $indice_fichero sent VDT Update to
$IP_remota");
}
Subrutina: “procesa_tdv_recibida”
Entradas: el descriptor del resultado de la aplicación del parser XML
al mensaje recibido, y la dirección IP del nodo que lo remite.
Salidas: ninguna.
Aplicando el método “getElementsByTagName”, se obtiene una relación de
elementos que contienen la etiqueta “row”. Así, se extraen todas las
filas de la TDV contenidas en el mensaje XML.
sub procesa_tdv_recibida
{
my ($result,$IP_remota)[email protected]_;
my $filas=$result->getElementsByTagName("row");
Habrá tantas filas en la TDV, como ocurrencias de elementos que
contengan la etiqueta “row” existan en el mensaje. Y el número de
éstas se obtiene mediante el método “getLength”.
my $numfilas=$filas->getLength;
my $elemento;
my $contenido;
open UPDATEVDT,">updatevdt$indice_fichero.txt"
or die “Cannot write to ‘updatevdt$indice_fichero.txt - $!\n”;
Comienza a procesarse el resultado del análisis del mensaje. Para cada
una de las filas, se extrae el contenido de los elementos que
almacenan el nombre, el tipo, el tamaño, la fecha de la última
modificación, etc…
for(my $i=0; $i<$numfilas; $i++)
{
my $fila=$filas->item($i);
print UPDATEVDT obtener_contenido("name",$fila) . "\n";
print UPDATEVDT obtener_contenido("type",$fila) . "\n";
print UPDATEVDT obtener_contenido("size",$fila) . "\n";
print UPDATEVDT obtener_contenido("date",$fila) . "\n";
Evidentemente cada nodo escribe que él mismo es el propietario de su
tabla local (o séase, que la quinta columna de la primera fila de la
TDV contendrá la cadena “localhost”). En este punto, la tabla ya se
considera remota; proviene de otro nodo, así que esa entrada se
sustituye por la IPv6 del remitente. En el resto de las filas se
almacena un cero.
my $IP_imprimible=sprintf "%vd",$IP_remota;
if(!$i)
{
print UPDATEVDT $IP_imprimible . "\n";
}
else
{
print UPDATEVDT "0\n";
}
print UPDATEVDT obtener_contenido("perms",$fila) . "\n";
print UPDATEVDT obtener_contenido("nlink",$fila) . "\n";
print UPDATEVDT obtener_contenido("user",$fila) . "\n";
print UPDATEVDT obtener_contenido("group",$fila) . "\n";
}
close UPDATEVDT;
}
Subrutina: “actualiza_tdv”
Entradas: ninguna.
Salidas: ninguna.
Esta rutina se encarga de enviar una interrupción software (USR2) al
servidor FTP cuando hay una actualización lista para él.
sub actualiza_tdv
{
print "Signaling the FTP Server...\n";
registra("Update ready. Signaling the FTP server");
open PIDSERVER," my $pidserver=;
close PIDSERVER;
kill('USR2',$pidserver);
}
Subrutina: “calcula_tiempo_ respuesta”.
Entradas: ninguna.
Salidas: ninguna.
Dado que las condiciones de la red cambian constantemente, es
conveniente que, de cuando en cuando, se recalculen los tiempos de
respuesta de cada uno de los nodos que la integran.
El funcionamiento de esta rutina es bien sencillo: simplemente,
efectúa un Ping a cada uno de los hosts de la “@TablaNodos”. Cuando
termina el proceso, se escriben los resultados en el fichero “ResponseTimes.txt”,
para que el servidor FTP disponga de ellos cuando necesite ordenar los
nodos por tiempo de respuesta, a la hora de efectuar una descarga
remota.
sub calcula_tiempo_respuesta
{
my $tiempo_antes;
my $tiempo_despues;
my $Tiempos;
my $RegionCritica;
for(my $i=0;$i<=$#TablaNodos;$i++)
{
$tiempo_antes=(times)[0];
my $p=Net::Ping->new("tcp",3);
$p->ping($TablaNodos[$i][0]);
if($p)
{
$tiempo_despues=(times)[0];
$TablaNodos[$i][1]=$tiempo_despues-$tiempo_antes;
}
else
{
$TablaNodos[$i][1]=-1;
}
$p->close();
}
Ahora, se escriben los resultados del proceso en el fichero “ResponseTimes.txt”.
Hay que tener en cuenta que éste es un recurso compartido: el gestor
de la coherencia lo utiliza para escribir en él cada cierto tiempo, y
el servidor FTP lee de él cada vez que se le solicita una descarga
remota, así que cabe la posibilidad de que se produzca una colisión.
Para evitarla, se desarrolla un sencillo sistema de exclusión: una
región crítica (como se describe en la rutina “descarga_archivo”, en
la página 94).
while(-e "RC") { }
open $RegionCritica,">RC" or die "Cannot create RC - $!\n";
open $Tiempos,">ResponseTimes.txt"
or die "Cannot write to ResponseTimes.txt - $!\n";
El proceso entra en la región crítica, y puede acceder al recurso
compartido. Escribe la dirección IP de cada nodo, y su tiempo de
respuesta.
for(my $i=0;$i<=$#TablaNodos;$i++)
{
print $Tiempos "$TablaNodos[$i][3]\n";
print $Tiempos "$TablaNodos[$i][1]\n";
}
close $Tiempos;
close $RegionCritica;
unlink "RC";
}
Subrutina: “borrar_nodo”.
Entradas: la dirección IP del nodo a eliminar.
Salidas: ninguna.
Cuando uno de los nodos se va a dar de baja, envía un mensaje “Delete
node”, por multicast, a todos los demás integrantes del sistema,
quienes llaman a esta rutina, que simplemente borra la fila
correspondiente de sus tablas internas.
Evidentemente, ninguna máquina toma de motu proprio la decisión de
abandonar la red DFP; es una decisión del administrador, que sólo
tiene que arrancar el programa de configuración, y elegir la opción
adecuada para que el nodo envíe este mensaje cuando arranque.
sub borrar_nodo
{
my ($IP_nodo)[email protected]_;
my $encontrado=0;
my $posicion=-1;
for(my $i=0;$i<=$#TablaNodos;$i++)
{
if($IP_nodo eq $TablaNodos[$i][0])
{
$posicion=$i;
}
last if $posicion > -1;
}
Para borrar la fila de la “@TablaNodos” correspondiente al host que va
a darse de baja, se utiliza el método predefinido de Perl “splice”.
Éste recibe tres parámetros: la tabla sobre la que se va a aplicar la
operación, la posición a partir de la cual comenzarán a eliminarse
filas, y el número de filas que deben borrarse. En el caso que nos
ocupa, la tabla es, evidentemente, la “@TablaNodos”, la posición a
partir de la cual comienzan a eliminarse filas se calcula en el bucle
anterior (y es la que ocupan los datos del nodo que se da de baja), y
el número de filas a eliminar, por supuesto, es 1.
splice(@TablaNodos,$posicion,1);
}
Subrutina: “procesa_IP”
Entradas: la dirección IPv6, comprimida, que devuelve la función “recv”.
Salidas: la dirección IPv4 que le corresponde, en formato imprimible.
La rutina divide la IPv6 recibida como parámetro, en bytes separados
por puntos. Así, sólo hay que acceder a los que ocupan las posiciones
de la cuarta a la séptima (que, precisamente, son los de la IPv4), y
construir una cadena con ellos.
sub procesa_IP
{
my $IP_remota=shift;
my $IP_imprimible=sprintf "%vd",$IP_remota;
my @bytesIP=split /\./, $IP_imprimible;
my $IPv4="$bytesIP[4].$bytesIP[5].$bytesIP[6].$bytesIP[7]";
return $IPv4;
}
Subrutina: “termina_hijo”.
Entradas: ninguna.
Salidas: ninguna.
Esta rutina no es más que un manejador para la interrupción software “CHLD”,
que envía un hijo cuando termina su tarea, y alcanza la última
instrucción de su código (“exit”). Se limita a ejecutar el método “wait”,
que el proceso padre emplea para liberar los recursos consumidos por
el hijo, y a restar 1 a la variable “$numero_hijos”.
sub termina_hijo
{
wait;
$numero_hijos--;
}
Subrutina: “comprueba_si_actualizacion”
Entradas: ninguna.
Salidas: ninguna.
Decide si hay una actualización preparada para el servidor FTP. Esto
sólo puede garantizarse con total seguridad si no hay ningún proceso
hijo trabajando sobre algún mensaje recibido, y si existen uno o más
ficheros de tipo “updatevdt” (en los que, recordemos, los hijos
almacenan el resultado del procesamiento de las TDV recibidas).
sub comprueba_si_actualizacion
{
my $encontrado=0;
if(!$numero_hijos)
{
for(my $i=0;$i<=999;$i++)
{
if(-e "updatevdt$i.txt")
{
actualiza_tdv();
$encontrado=1;
}
last if $encontrado;
}
}
}
Subrutina: “pide_TDV”
Entradas: la IPv6 del nodo al que se solicita el envío de su tabla.
Salidas: ninguna.
Todos los mensajes de control del protocolo se transmiten en multicast,
salvo “Get VDT”, que se dirige siempre a un nodo concreto. Para
facilitar la legibilidad del código, la transmisión de estas
peticiones, emplea una rutina diferente de “envia_mensaje”.
sub pide_TDV
{
my $IP_remota=shift;
my $fichero=genera_mensaje_xml(5);
my $buffer;
read($fichero,$buffer,4096);
$socket_udp->mcast_send($buffer,$IP_remota);
}
Subrutina “registra”
Entradas: la cadena de texto que se va a almacenar en el fichero de
log.
Salidas: ninguna.
Esta rutina es idéntica a su homónima, en el módulo gestor de la
conexión del servidor FTP. La única diferencia es que construye el
nombre del fichero de registro, prefijándolo con las iniciales “CM”
(de “Coherence Manager”), en lugar de “SRV” (de “Server”).
sub registra
{
my $texto=shift;
my @fecha=localtime;
my $nombre_fichero="CM$fecha[3]." . ($fecha[4]+1) . "." .
($fecha[5]+1900) . ".log";
open LOG,">>$nombre_fichero";
print LOG "$fecha[2]:$fecha[1]:$fecha[0] - $texto\n";
close LOG;
}
Subrutina “comprueba_MD5”
Entradas: la firma digital del mensaje recibido, y el nombre del
fichero que lo contiene.
Salidas: un 1 si la firma es correcta, y un 0 en caso contrario.
Para garantizar que el nodo remitente es quien dice ser, y evitar así
potenciales problemas de seguridad, esta rutina aplica el algoritmo
MD5 al mensaje recibido y, a continuación, añade la contraseña del
grupo DFP. Si el resultado coincide con la firma digital incluida en
el elemento del mensaje XML, se puede afirmar, con un grado de
certeza bastante considerable, que el nodo que transmitió el mensaje
es un miembro legítimo de la red de FTP distribuido.
En caso contrario, el mensaje se rechazará, y quedará constancia del
incidente en el fichero de registro.
sub comprueba_MD5
{
my ($CRCMD5,$fichero)[email protected]_;
my $legal=0;
open FICHERO,"<$fichero" or die "Can't read from $fichero - $!\n";
my $md5=Digest::MD5->new;
El bucle que se muestra a continuación lee el fichero que contiene el
mensaje, línea a línea, y se detiene cuando una de ellas contenga la
etiqueta XML que determina el comienzo del cuerpo del mensaje, esto
es, en el caso de los avisos, o en el de las
actualizaciones de la TDV.
while($_=FICHERO->getline)
{
last if (($_ =~ m//) || ($_ =~ m//));
}
El puntero de lectura ya está preparado para procesar el mensaje. No
obstante, como el bucle anterior terminó DESPUÉS de la lectura de la
etiqueta o , es necesario añadir al proceso la
información que ha quedado atrás.
Si estamos tratando con un mensaje de aviso, basta con agregar
. Si es una actualización de la TDV, el puntero habrá
“pasado” sobre tres líneas fundamentales para el cálculo de la firma
digital.
if($_ =~ m//)
{
$md5->add(" \n");
}
else
{
$md5->add("4\n");
$md5->add("VDT Update\n");
$md5->add(" \n");
}
Listo. Ahora, ya puede empezar el procesamiento normal del mensaje. Se
añade cada línea leída al objeto MD5 mediante un bucle, que terminará
cuando una de éstas coincida con las etiquetas que delimitan el final
del cuerpo del mensaje (
si es un aviso, y
si
se trata de una actualización).
while($_=FICHERO->getline)
{
$md5->add($_);
last if (($_ =~ m/<\/MessageBody>/) || ($_ =~ m/<\/RowsVDT>/));
}
Se ha terminado de procesar el mensaje. Sólo resta añadir la
contraseña del grupo DFP, y calcular la firma digital:
$md5->add("$Password");
my $resultado=$md5->hexdigest;
Bien. Ahora, comparemos el resultado obtenido con el que figura en el
elemento del mensaje. Si coinciden, la variable “$legal”
almacenará el valor 1, y la subrutina acaba.
if($resultado eq $CRCMD5)
{
$legal=1;
}
close FICHERO;
return $legal;
}
5.6.5 El programa de configuración.
Antes de iniciar el sistema por primera vez, es necesario que el
administrador defina los valores que contendrán algunos parámetros
esenciales, para lo que empleará un sencillo programa de configuración
llamado “configdfp”.
Su única misión es definir una especie de esquemática interfaz de
usuario para permitir al administrador almacenar los valores que
considere más oportunos, de un modo sencillo y seguro, en función de
la configuración de su máquina, y de las características de la red en
la que funcionará.
De esta manera, se garantiza que el programa no arrancará con valores
incorrectos o fuera de rangos razonables o admisibles.
#!/usr/bin/perl -w
use strict;
use warnings;
use File::Basename;
print "\n\n ** DFP SYSTEM CONFIGURATION UTILITY **\n\n";
my $correcto=0;
my $path;
my $Host;
my $Puerto;
my $PingTimeout;
my $Password;
my $DeleteNode;
La ejecución permanece dentro del siguiente bucle mientras el usuario
no confirme que los datos introducidos son correctos.
while(!$correcto)
{
Una a una, se van invocando a las subrutinas que piden los datos
pertinentes. Esta solución mejora la legibilidad del código y su
mantenimiento. Así, si en el futuro se decide modificar el conjunto de
variables configurables por el administrador añadiendo algunas,
eliminando otras, o alterando algún rango de valores aceptables, los
cambios en este programa serían razonablemente sencillos y
superficiales. En cualquier caso, no tendrían por qué ir más allá de
borrar llamadas a rutinas o añadir otras nuevas.
$path=pide_path();
$Host=pide_nombre_host();
$Puerto=pide_puerto();
$PingTimeout=pide_ping_timeout();
$Password=pide_password();
$DeleteNode=mira_si_borrar();
En este punto, todas las variables han sido configuradas. Sus valores
se muestran en pantalla, a modo de resumen, y se solicita la
confirmación del administrador, antes de proceder a almacenarlas en el
fichero “DFP.cfg”.
print "Working path = $path\n";
print "Host name = $Host\n";
print "Port number = $Puerto\n";
print "Ping timeout = $PingTimeout\n";
print “Password = $Password\n”;
my $borranodo;
if($DeleteNode == 0)
{
$borranodo="no";
}
else
{
$borranodo="yes";
}
print "Delete this node? = $borranodo"
El usuario debe pulsar “y” para aceptar los valores introducidos, o “n”
para rechazarlos y comenzar de nuevo el proceso. Mientras no se teclee
“y” o “n”, la ejecución permanece dentro del bucle que se muestra a
continuación. No es necesario que distinga entre mayúsculas y
minúsculas (siempre se pasa a mayúsculas la opción tecleada).
my $respuesta;
do
{
print "\nConfirm these values (Y/N)? ";
$respuesta=;
chomp($respuesta);
$respuesta=(uc $respuesta);
} while(($respuesta ne 'Y') && ($respuesta ne 'N'));
if($respuesta eq "Y")
{
$correcto=1;
}
}
Ya sólo queda almacenar los valores en el fichero de configuración “DFP.cfg”.
Nótese que sobreescriben a los anteriores, si los había.
print "Saving configuration file...\n";
open CFG,">DFP.cfg" or die "Can't write to 'DFP.cfg' - $!\n";
print CFG $path . "\n";
print CFG $Host . "\n";
print CFG $Puerto . "\n";
print CFG $PingTimeout . "\n";
print CFG $Password . “\n”;
print $DeleteNode;
close CFG;
print "Saved. Bye.\n";
Subrutina: “pide_path”
Entradas: ninguna.
Salidas: el path a partir del cual se montará la tabla de directorios
virtuales.
Solicita al usuario que teclee el camino a partir del cual se generará
la TDV. Debe ser un path correcto, esto es, que se refiera a
directorios que existen en el disco duro local.
sub pide_path
{
my $valido=0;
my $path;
while(!$valido)
{
print "\nPlease, enter FTP path (example: '/dir1/dir2'): ";
$path=;
chomp($path);
Se garantiza la corrección del path tecleado por el usuario si el
método predefinido de Perl “opendir” es capaz de abrirlo sin
problemas.
if(opendir(DIR,$path))
{
$valido=1;
}
else
{
print "Wrong path."
}
}
Para que el path sea correcto, debe terminar con la barra separadora
de directorios “/”. Sin embargo, no es conveniente añadirla sin más a
la variable que contiene el camino tecleado por el usuario, dado que
el método “opendir” acepta indistintamente algo como “/dir1/dir2”, y
algo como “/dir1/dir2/”. Si se insertara el separador al final del
nombre de un path que ya terminaba con éste, se formaría algo como:
“/dir1/dir2//”, a todas luces incorrecto.
Por tanto, es necesario comprobar si el último carácter del camino es
o no el separador de directorios. En caso negativo, se añade
manualmente.
Para realizar dicha comprobación, se recurre a la función “chop”, que
elimina el último carácter de una cadena de texto, y lo devuelve.
Entonces, no hay más que compararlo con el separador.
Nótese que la función se aplica sobre una copia de la variable que
contiene el path, para evitar modificarla.
my $copia_path=$path;
my $ultimo_caracter=chop($copia_path);
if($ultimo_caracter ne "/")
{
$path=$path . "/";
}
close(DIR);
return $path;
}
Subrutina: “pide_nombre_host”
Entradas: ninguna.
Salidas: el nombre de la máquina en la que habrá de funcionar la
aplicación.
Esta es la rutina en la que más libertad se permite al usuario.
Simplemente se solicita que introduzca el nombre de la máquina local.
Si teclea uno erróneo, el servidor FTP no podrá abrir el socket de
comunicación con los clientes.
sub pide_nombre_host
{
print "\nPlease, enter the host name: ";
my $Host=;
chomp($Host);
return $Host;
}
Subrutina: “pide_puerto”
Entradas: ninguna.
Salidas: el número del puerto al que se conectarán los clientes del
servicio FTP.
Aunque el servidor FTP que incluye la aplicación está pensado para
funcionar como uno estándar, esta es una versión relativamente
preliminar y dedicada exclusivamente a la gestión de conexiones
anónimas y en modo de sólo lectura, por tanto no es conveniente que,
al menos de momento, sustituya el servicio de FTP que muchas máquinas
pueden tener ya instalado. Éstos, en una gran mayoría de los casos,
utilizan el puerto 21 para establecer las conexiones con los clientes.
Para evitar conflictos, el servidor de este proyecto escuchará en el
puerto 1024, 1025 ó 1026, a elegir por el administrador.
sub pide_puerto
{
my $valido=0;
while(!$valido)
{
print "\nPlease, enter the port number [1024, 1025 or 1026]: ";
$Puerto=;
chomp($Puerto);
La comprobación es bien sencilla: si la cadena introducida por el
usuario es “1024”, “1025” ó “1026”, se considera válida.
if(($Puerto eq "1024") || ($Puerto eq "1025") ||
($Puerto eq "1026"))
{
$valido=1;
}
}
return $Puerto;
}
Subrutina: “pide_ping_timeout”
Entradas: ninguna.
Salidas: el tiempo límite para decidir si un nodo es o no accesible
mediante un Ping.
Con cierta periodicidad, el gestor de la coherencia calcula el tiempo
de respuesta de cada uno de los nodos de la red DFP y los almacena en
un archivo que, posteriormente, el servidor FTP utilizará cuando se le
solicita una descarga remota.
En esta rutina, se pide al usuario que introduzca el tiempo máximo de
espera, en segundos. Si transcurre completamente sin que el nodo
pertinente responda, se considera que es inaccesible.
sub pide_ping_timeout
{
my $valido=0;
while(!$valido)
{
print "\nPlease, enter the Ping Timeout [1-10]: ";
$PingTimeout=;
chomp($PingTimeout);
De nuevo, se aplica la “criba en tres pasos” utilizada en la rutina “pide_timeout”,
a saber: comprobar que la variable no contiene la cadena vacía ni
cualquier cosa que no sea un dígito, y que el número introducido
oscile entre los límites del rango (en este caso, 1 y 10, ambos
inclusive).
if($PingTimeout ne "")
{
if($PingTimeout !~ m/(\D)/)
{
if(($PingTimeout >=1) && ($PingTimeout <= 10))
{
$valido=1;
}
}
}
}
return $PingTimeout;
}
Subrutina: “pide_password”
Entradas: ninguna.
Salidas: la contraseña del grupo DFP.
Solicita al administrador la contraseña del grupo DFP. Es importante
tener en mente que ésta debe ser común a todos los nodos que integran
el sistema de FTP distribuido, y es esencial para que cada uno de
ellos sea reconocido por los demás. Si una máquina tiene una
contraseña diferente del resto, su firma digital no coincidiría con la
esperada por los receptores, sus mensajes serían rechazados, y no
podría integrarse en la red.
sub pide_password
{
my $Password;
print "\nPlease, enter the group password: ";
$Password=;
chomp($Password);
return $Password;
}
Subrutina “mira_si_borrar”
Entradas: ninguna.
Salidas: 1, si el administrador ha decidido que este nodo se borre, y
0 si no.
Si el administrador decide que el presente nodo se dé de baja de la
red DFP, simplemente debe teclear “DelNod” como respuesta a la
solicitud que se le hace a continuación. En caso contrario, le basta
con escribir cualquier otra cosa –o simplemente, pulsar ENTER- para
que el nodo se incorpore al sistema.
sub mira_si_borrar
{
my $borra=0;
my $cadena;
print "\nType in 'DelNod' to delete this node or ENTER to ignore: ";
$cadena=;
if($cadena eq "DelNod")
{
$borra=1;
}
}
5.6.8 Formato y uso de los ficheros de trabajo.
Durante la ejecución de la aplicación, pueden estar funcionando un
número considerable de procesos paralelos e independientes. Cada uno
utiliza su propio espacio de variables, aislado del resto. En el mejor
de los casos, se consigue la herencia de éstas, desde un proceso padre
hacia los procesos hijos que se derivan de él. En otros, la
intercomunicación directa (ya que no se emplean sockets ni zonas de
memoria compartidas) sería imposible si no se recurriera al empleo de
ficheros, una táctica que, si bien puede parecer poco elegante, es
sencilla y robusta.
En ciertos casos, que no son necesariamente extremos, pueden existir
en un momento dado, decenas de ficheros temporales en el disco duro
sobre los que sendas aplicaciones trabajan. Es necesario por lo tanto
que uno de los cometidos de este documento sea el de identificarlos
claramente, nombrarlos, concretar su utilidad y describir su formato.
A continuación, se enumeran los archivos temporales utilizados por el
proyecto, a modo de fichas en las que figuran sus características más
relevantes: el nombre del archivo, el proceso lector, el proceso
escritor, la intención con la que se emplea, y el formato. En lo que
respecta a éste, se utiliza una sencilla notación en la que los campos
que contiene el archivo se expresan entre símbolos de desigualdad (< y
>). Si terminan con un retorno de carro, se representa con “\n”.
Para más información acerca de los ficheros que se utilizan en la
comunicación entre las dos tareas principales que integran la
aplicación (el gestor de la coherencia, y el servidor FTP), consúltese
la sección 5.4 – Comunicación entre procesos y dinámica, en la página
25.
Nombre:
mypid.txt
Proceso lector:
Gestor de la coherencia (padre)
Proceso escritor:
Servidor FTP – gestor de la conexión (padre)
Utilidad:
Contiene el PID del servidor FTP, para que, cuando haya una
actualización preparada, el gestor de la coherencia pueda avisarle
enviándole una interrupción software.
Formato:
\n
Nombre:
DFP.cfg
Proceso lector:
Módulo de variables globales.
Proceso escritor:
Programa de configuración.
Utilidad:
Contiene los valores de los parámetros esenciales para el
funcionamiento del sistema, que el administrador debe determinar la
primera vez que éste se ponga en funcionamiento.
El módulo de variables globales los lee, y posteriormente, dichas
variables son importadas por las partes de la aplicación que las
requieran.
Formato:
\n
\n
\n
\n
\n
\n
Nombre:
messrecv.xml
Proceso lector:
Gestor de la coherencia (padre)
Proceso escritor:
Gestor de la coherencia (padre)
Utilidad:
Almacena un mensaje XML remitido desde otro de los nodos integrantes
de la red DFP.
Cuando se completa la recepción, se hace una copia del fichero, que
utilizará el proceso hijo derivado especialmente para trabajar sobre
él. El original se borra entonces.
Formato:
Descrito en la sección 5.2 - Tipos de mensaje y su estructura, a
partir de la página 18.
Nombre:
message0 .. message999.xml
Proceso lector:
Gestor de la coherencia (hijo)
Proceso escritor:
Gestor de la coherencia (hijo)
Utilidad:
Contiene el mensaje XML que se va a enviar. Cada proceso hijo genera
un fichero de actualización diferente, e identificado mediante un
número de orden entre 0 y 999.
Formato:
Descrito en la sección 5.2 - Tipos de mensaje y su estructura, a
partir de la página 18.
Nombre:
update0.txt .. update999.txt
Proceso lector:
Servidor FTP – módulo de directorios virtuales (hijo)
Proceso escritor:
Gestor de la coherencia (hijo 0 .. hijo 999)
Utilidad:
Contienen el resultado del proceso de un mensaje de actualización de
la TDV. Cada proceso hijo genera un fichero de actualización
diferente, e identificado mediante un número de orden entre 0 y 999.
Formato:
\n
\n
\n
\n
\n
\n
\n
\n
\n
Nombre:
RC
Proceso lector:
Gestor de la coherencia (hijo) / Servidor FTP – módulo de directorios
virtuales (padre)
Proceso escritor:
Gestor de la coherencia (hijo) / Servidor FTP – módulo de directorios
virtuales (padre)
Utilidades:
Define un sencillo mecanismo de exclusión (región crítica) para evitar
que el servidor FTP y el gestor de la coherencia traten de acceder
simultáneamente a un recurso compartido (el fichero “ResponseTimes.txt”).
Formato:
-vacío-
Nombre:
tabnodsrv.txt
Proceso lector:
Servidor FTP – módulo de directorios virtuales (padre)
Proceso escritor:
Servidor FTP – módulo de directorios virtuales (padre)
Utilidad:
Contiene una lista con las direcciones IPv4 de los nodos que integran
la red DFP, y los números que identifican a los directorios especiales
en los que se almacenan sus TDVs.
Así, cuando se recibe un aviso de actualización, el servidor FTP
determina si proviene de una máquina cuya tabla de directorios ya
figura en la local, o si es necesario crear un nuevo directorio
especial para ella.
Formato:
\n
\n
Nombre:
ResponseTimes.txt
Proceso lector:
Servidor FTP – módulo de directorios virtuales (hijo)
Proceso escritor:
Gestor de la coherencia (padre)
Utilidad:
Cuando un cliente solicita una descarga remota, el servidor FTP
construye una lista con los nodos que contienen el archivo pedido, y
los ordena según su tiempo de respuesta, de menor a mayor.
Es el gestor de la coherencia el encargado de calcular periódicamente
estos tiempos, y almacenarlos en este fichero. El servidor FTP no
tiene más que leerlos. Cada uno va asociado a un nodo que se
identifica mediante su IPv4.
Formato:
\n
\n
5.6.9 ALGUNOS EJEMPLOS DE FUNCIONAMIENTO
Los ejemplos son una magnífica herramienta de ayuda a la hora de
explicar un concepto complicado. Y más aún, cuando se pretende
describir el funcionamiento de un proceso complejo, como precisamente
es un protocolo de comunicaciones.
Por ello, esta sección intenta plantear una serie preguntas que
podrían surgir ante el análisis de un sistema distribuido como el
desarrollado, y las respuestas que el protocolo DFP ofrece, a modo de
ejemplos que enriquezcan o complementen los apartados anteriores. En
cierta manera, podría entenderse como una sección de preguntas y
respuestas más frecuentes (FAQ). Al término de la misma, se ofrecen
una serie de ejemplos de pruebas de ejecución de la aplicación en un
entorno real formado por tres máquinas conectadas en red.
Este punto se organiza en párrafos con esta apariencia:
P: Texto de la pregunta. Escrita en cursiva.
R: Texto de la respuesta. Caracteres normales.
P: Un nodo recibe correctamente un mensaje, pero es especialmente
grande, y su procesado especialmente lento. Imaginemos que tal
procesado le lleva al nodo varios segundos, o incluso un minuto. Si en
ese momento se produce una actualización en otra parte de la red, el
host no podrá atenderla.
R: El sistema se ha implementado utilizando procesos paralelos. Cada
vez que se recibe un mensaje, se deriva una tarea hija que trabaja
sobre los datos obtenidos. De este modo, la parte de la aplicación
encargada de garantizar la coherencia de los datos, puede volver a
ponerse a la escucha de nuevos mensajes inmediatamente.
P: Un nodo problemático pasa mucho tiempo offline. En ese caso, su TDV
estará cada vez más obsoleta, al perderse varias actualizaciones
consecutivas.
R: No existe una cola de actualizaciones. Cada actualización
sobreescribe a la anterior, de modo que cuando el nodo problemático
vuelva a conectarse, obtendrá las últimas versiones de todas las
tablas locales de la red con un solo envío por parte de cada host.
P: Cuando un nodo que ha estado offline vuelve a conectarse, ¿cómo
sabe que podría haber actualizaciones pendientes para él?
R: Cada vez que un nodo vuelve a conectarse, transmite por multicast
un mensaje de tipo “node online”. Aprovecha las confirmaciones para
construir su tabla de nodos, y para solicitar, a las máquinas
remitentes, sus tablas locales.
P: Un Np se cae en medio del envío de su TDV actualizada.
R: Como se detalla en la sección 5.1 - Tipos de mensaje y su
estructura, página 18, los mensajes se generan en XML, y dado que este
lenguaje sigue unas normas muy estrictas, no se permiten cosas como
etiquetas que no se cierran, y errores sintácticos parecidos. Los
receptores emplean un parser de XML para garantizar que los mensajes
estén bien formados. Si se recibe alguno que no cumpla con esta
condición indispensable, simplemente, se descartará.
Respecto al propagador, cuando vuelva a arrancar, encontrará un
mensaje pendiente de envío, y lo retransmitirá, siguiendo el
procedimiento usual.
P: Una máquina no se cae, pero sí el enlace que la une con un
propagador. Desde su óptica, se tratará de un nodo “no coherente”. A
ojos de las demás, sin embargo, seguirá siendo visible y perfectamente
funcional. Cuando se restablezca esa conexión problemática… ¿cómo sabe
que puede que tenga mensajes pendientes? No es una caída del sistema:
la máquina sigue funcionando, y es posible que lo único que suceda es
que se ha cortado físicamente un cable. ¿Cómo sabe el nodo con el que
perdió el contacto, que ya vuelve a responder?
O sea, que tenemos dos nodos que funcionan perfectamente y que se
consideran, mutuamente, offline.
R: Los mensajes se envían por multicast a todos los nodos,
independientemente de que sean o no accesibles desde el propagador.
6. BREVE MANUAL DEL ADMINISTRADOR
El sistema está pensado para requerir un mínimo de mantenimiento. En
realidad, funciona de forma totalmente automática. El administrador,
por tanto, sólo debe encargarse, ocasionalmente, y de una manera
sencilla, de las tareas de:
1.
Iniciar el sistema
2.
Actualizar las tablas de directorios.
3.
Dar de baja a un nodo.
La primera vez que se ponga en marcha la aplicación, puede ser
necesario establecer los valores de algunas de las variables
esenciales. La configuración de las mismas, se almacena en el fichero
“DFP.cfg”. Dado que es imposible prever la infinidad de combinaciones
y configuraciones en cada servidor, siempre es una buena idea
flexibilizar las posibilidades de cada aplicación.
Los parámetros a los que tiene acceso el administrador, a través del
sencillo programa de configuración “configdfp”, son:
- El path a partir del cual se monta la tabla de directorios
virtuales.
El administrador sólo tiene que proporcionar un camino válido. La
aplicación ignorará el resto del disco duro (no será accesible de
ningún modo, por parte de ningún cliente; téngase en cuenta que la TDV
está en memoria, así que cualquier intento, malintencionado o
accidental, de salirse de los límites que impone el path determinado
por el administrador y acceder a otras zonas del disco duro, será
infructuoso). El programa de configuración sólo admite paths
correctos, es decir, que existan en el disco duro. Todos los ficheros
y directorios que cuelguen de él, se almacenarán en la TDV
recursivamente.
- El nombre del host local.
Simplemente, el administrador debe proporcionar al programa de
configuración el nombre de la máquina sobre la que se va a ejecutar la
aplicación. Éste es absolutamente necesario. Si la aplicación no lo
conociera, no le sería posible abrir los sockets de comunicación con
los clientes FTP.
- Puerto del servidor FTP, al que deben conectarse los clientes.
Aunque el puerto estándar para el protocolo FTP es el 21, la
aplicación utiliza uno de tres posibles, todos ellos dentro del rango
de los puertos que se conocen como “registrados” (abiertos a la
utilización por parte del usuario). Aunque el sistema está pensado
para ser, a ojos de un cliente FTP estándar, indistinguible de un
servidor ordinario, lo cierto es que en esta versión, aún
relativamente preliminar, es más razonable instalarlo en puertos
diferentes (sin descartar que, en caso de que se le apliquen mejoras
en el futuro, acabe sustituyendo completamente al servidor FTP
estándar). Estos tres puertos son: 1024, 1025 y 1026. Si uno de ellos
estuviera ocupado por otra aplicación, el administrador debe elegir
uno libre.
- Temporizador para el Ping.
Con objeto de optimizar las descargas remotas (esto es: escoger el
servidor más rápido, cuando un cliente solicita un fichero ubicado en
varios de ellos), el gestor de la coherencia calcula con cierta
periodicidad los tiempos de respuesta de todos los nodos que integran
la red. Para ello, utiliza una herramienta bien conocida: el Ping. La
librería estándar de Perl que implementa esta operación, requiere que
se especifique un tiempo máximo de espera, antes de decidir que un
nodo no responde.
En redes locales con un gran ancho de banda, los tiempos de respuesta
son del orden de los microsegundos. En redes de área amplia, como
Internet, suelen aproximarse a los 200 ó 300 milisegundos. En
cualquier caso, el mínimo tiempo de respuesta admisible por el
programa de configuración es 1 segundo, lo suficientemente holgado
como para garantizar que, en condiciones normales, la mayoría de los
nodos responderá sin problemas. El límite superior es de 10 segundos.
En principio, si una máquina no responde a un ping tras ese tiempo, se
puede concluir, casi con toda seguridad, que es inaccesible46.
Respecto a la actualización de la tabla de directorios virtuales, se
trata de un proceso automático en el que no interviene el
administrador. Su único cometido es mantener el contenido de los
directorios a partir de los cuales se genera la TDV. Es totalmente
posible, para cualquier cliente, navegar por la TDV mientras el
administrador modifica la estructura de directorios (ya que estará
actuando sobre el disco, y la tabla está almacenada en memoria),
aunque cabe la posibilidad de que se le muestren al usuario ficheros o
directorios que ya no existen. De todos modos, la actualización no
tendrá efecto hasta que la aplicación se detenga y se lance de nuevo,
para que el módulo de directorios virtuales genere la nueva TDV. Eso
sí, hay que tener algo en cuenta: NUNCA deben utilizarse directorios
con el nombre “DIRn”, (donde n es un número entero), pues son
precisamente los que emplea el servidor FTP para almacenar (en
memoria) las tablas virtuales de las máquinas remotas.
Por último, la eliminación de un nodo se consigue simplemente
ejecutando la aplicación de configuración, y respondiendo “Yes”
(nótese que la primera letra es una “Y” mayúscula) a la primera
pregunta que se plantea: “Delete node?”. Si el usuario confirma su
decisión afirmativa, e introduce correctamente la contraseña de grupo,
el host enviará a los demás un mensaje XML informándoles que se da de
baja, y que deben actualizar sus tablas de nodos en consecuencia.
- Contraseña del grupo.
Para certificar la autenticidad de los mensajes que se envían, cada
nodo aplica una firma digital sobre los mismos (que, como se describe
en la sección 5.6.6 – El gestor de la coherencia de los datos, página
125, se almacena en el elemento XML ), recurriendo al algoritmo
MD5. Ésta no tiene como finalidad identificar a una máquina concreta,
o encriptar información sensible, sino más bien demostrar que el
mensaje transmitido es semánticamente correcto (del análisis de la
sintaxis ya se encarga el parser de XML), y que proviene de un host
que pertenece al grupo multicast del sistema de FTP distribuido. Así,
el mismo mensaje siempre generará la misma firma digital, para todos
los nodos miembros de la red DFP. El nodo receptor compara la firma
digital obtenida, con una que él mismo genere sobre el mensaje en
cuestión, a la que se le añade la clave de grupo. Si ambos resultados
coinciden, es más que razonable concluir que, con una probabilidad muy
alta, el mensaje proviene de un nodo que, efectivamente, pertenece al
sistema. De lo contrario, debe rechazarse.
- Borrado del presente nodo:
Si el administrador decide que el presente nodo debe darse de baja, no
tiene más que teclear “DelNod” cuando el programa de configuración se
lo solicite. De esta manera, cuando el sistema arranque, se limitará a
enviar un mensaje de tipo “Delete node” a todos los demás integrantes
de la red.
7. RESUMEN Y CONCLUSIONES.
En un sistema distribuido, recurrir a la organización centralizada de
un “supernodo”, comporta varios problemas. Entre los más evidentes,
figura el colapso al que se abocaría el sistema si dicha máquina
controladora sufriera algún problema técnico. Sin embargo, conviene no
obviar otros más sutiles como la ineficiencia que supondría un
protocolo que se apoyara en la existencia de un solo
superadministrador que lo gestionara todo.
Este proyecto adopta un enfoque en el que cada host es independiente y
puede funcionar de forma aislada, pero cuando varios de ellos deben
integrarse para formar el sistema DFP, deben atenerse a unas reglas,
es decir, las postuladas por el protocolo de comunicaciones que se ha
desarrollado.
La intención es no favorecer a ninguno de los nodos en concreto, sino
tener a todos por iguales, capaces de desempeñar cualquiera de los
roles que dichas reglas definen.
Las ventajas son evidentes:
*
La unión de una serie de máquinas, siempre bajo la escrupulosa
observancia de las reglas trazadas por el protocolo, consigue
sumar las cualidades de cada una de ellas, tomadas por separado.
*
Cualquier cliente tiene acceso a una estructura de directorios
equivalente a la unión de las de los hosts individuales. El
aumento del espacio de almacenamiento es notable. Aún más:
ampliarlo es tan sencillo como dar de alta a un nuevo nodo por
medio de un proceso prácticamente automático, transparente a los
clientes y que apenas requiere la participación de un
administrador.
*
La estabilidad del sistema se ve reforzada significativamente.
Cualquier cliente podría navegar a través de la estructura de
directorios de un nodo que, en realidad, no estuviera en línea, ya
que todos los demás servidores contienen siempre copias de la
misma.
*
De la ventaja anterior se deriva una muy interesante: la
aceleración de las descargas de archivos, en promedio, y el
aumento de las posibilidades de obtener un fichero concreto. En un
sistema centralizado, un fallo en el servidor principal pone toda
su estructura de directorios fuera del alcance de los clientes. En
este proyecto, si un archivo se encuentra duplicado en varios
nodos (por ejemplo, a través de mirrors47), el protocolo garantiza
que, si una solicitud de descarga no puede ser satisfecha, se
redirigirá automáticamente y de forma transparente al usuario,
hacia alguno de los nodos en funcionamiento, que contengan el
mismo fichero. Además, se concede prioridad a aquellos nodos con
mejor tiempo de respuesta. El cliente no advertirá el problema en
ningún momento.
*
El mantenimiento del sistema es verdaderamente sencillo, y apenas
requiere intervención humana. Una vez que un nodo ha sido
configurado correctamente, y se ha integrado en la red de FTP
distribuido, su funcionamiento se regula automáticamente y sin la
participación del administrador, salvo para modificar la
estructura de directorios cuando sea preciso, o para dar de baja
al nodo cuando se considere oportuno.
En otro orden de cosas, sería injusto no reconocer también los
inconvenientes, limitaciones y problemas potenciales que esta
aproximación al problema de la distribución de servidores FTP puede
plantear. No es menos cierto, sin embargo, que dichos puntos débiles
prácticamente se pueden reducir al más evidente: el aumento del
consumo de recursos en general, y de memoria y ancho de banda en
particular.
Téngase en cuenta que cada nodo almacena en RAM su estructura de
directorios, más la de todos los que forman el sistema DFP. Así,
aunque el sistema es muy fácilmente escalable48, siempre deben tenerse
en cuenta lo limitado de los recursos de hardware.
Cuando se trata con servidores que contienen gran cantidad de
directorios y archivos, las tablas virtuales pueden alcanzar
perfectamente tamaños de varios megabytes. Nada que, en principio, un
servidor moderno no pueda manejar, pero estamos hablando de más de un
nodo, es decir, de más de una de esas tablas. El sistema, por tanto,
no está pensado para ejecutarse en una red que conste de un número muy
alto de hosts (podría considerarse razonable contar con unos 30 ó 40
de ellos).
La aplicación se ha probado con una red de tres máquinas (en realidad,
PC de sobremesa, de gama media-baja), y no sólo no se observaron
ningún tipo de problemas en el funcionamiento, sino que todas las
operaciones se realizaron a una velocidad más que aceptable.
Conviene recordar que este proyecto se ha diseñado para funcionar
sobre estaciones de trabajo de gran capacidad. Aunque nada impide
formar una red con ordenadores domésticos, ésta debería estar
constituida por pocos nodos, que además tendrían que compartir tablas
relativamente reducidas. Para obtener de la aplicación todo el
rendimiento que se espera de ella, hay que recurrir a máquinas muy
potentes.
8. OPCIONES ABIERTAS.
El verdadero objetivo de este proyecto es demostrar que la
distribución de un servicio de FTP es factible, mediante la
implementación de un sencillo protocolo de comunicaciones. En este
sentido, el código comentado hasta ahora podría ser susceptible de ser
mejorado y ampliado en un futuro. A continuación se presentan una
serie de posibles líneas de investigación:
*
Comprobar el funcionamiento de la aplicación en una red real,
formada por un número importante de nodos (entre 20 y 30, por
ejemplo) de gran capacidad.
*
Puede mejorarse el rendimiento de la aplicación utilizando
comunicación entre procesos y sockets Unix, en lugar de ficheros.
Además, algunas de las tablas utilizadas pueden sustituirse por
estructuras hash, para permitir accesos directos a los datos. Esta
mejora podría llevarse a cabo incluso sobre la propia TDV.
*
Sería interesante una implementación completa del protocolo FTP,
que permita la modificación de la estructura de directorios por
parte de los clientes.
*
Es conveniente comprobar la portabilidad de la aplicación. Aunque
el lenguaje Perl tiene una sólida reputación de compatibilidad
entre plataformas (no en vano, es uno de los lenguajes de Internet
más difundidos), siempre es aconsejable garantizar que el sistema
puede funcionar sin problemas en otros sistemas operativos
(Solaris, por ejemplo).
*
Quizás podría extenderse la idea a otros protocolos, como el TFTP
(Trivial FTP) o HTTP.
*
El desarrollo de herramientas de seguimiento y estadística, puede
resultar de mucha ayuda al administrador. Podría resultar muy
interesante el empleo de una interfaz gráfica sobre ellas.
*
Permitir la actualización de las TDV en línea. En la versión
actual de la aplicación, es necesario desconetar un nodo para
llevar a cabo esta tarea.
9. REFERENCIAS Y BIBLIOGRAFÍA.
[WALL] – Programming Perl, 3ª edición. Larry Wall, Tom Christiansen &
Jon Orwant. Ed. O’Reilly, 2000.
[TACK] – Utilizando Linux, 2ª edición. Tackett & Gunter. Ed. Prentice
Hall, 1997.
Perl in a nutshell, a desktop quick reference. Ellen Siever, Stephen
Spainhour & Nathan Patwardhan. Ed. O’Reilly, 1999.
Request for comments (RFC) editor homepage, en
http://www.rfc-editor.org/
Peer-to-peer working group, en http://www.peer-to-peerwg.org
RFC 768, User Datagram Protocol, en
http://www.faqs.org/rfcs/rfc768.html
Design Patterns, Pattern Languages and Frameworks, en
http://www.cs.wustl.edu/~schmidt/patterns.html
Multicast Transport Protocols, en
http://www.roads.lut.ac.uk/DS-Archive/MTP.html
Extensible Markup Language (XML), en http://www.w3.org/XML/
RFC 1321 – The MD5 Digest Algorithm, en
http://www.faqs.org/rfcs/rfc1321.html
IANA Home Page, en http://www.iana.org
1 Ver http://www.rfc-editor.org/
2 Véase http://www.peer-to-peerwg.org
3 Cabría preguntar, ¿por qué en memoria, y no en disco? La respuesta
es: por eficiencia. La gestión y actualización de una tabla ubicada en
la memoria RAM es mucho más rápida y sencilla que una equivalente,
almacenada en el disco.
4 Una mejora interesante, en el futuro, podría estar encaminada a
permitir que se transmitan sólo las zonas de la tabla que han variado.
Así, se conseguiría un uso óptimo del ancho de banda. Sin embargo,
semejante modificación no es, ni mucho menos, trivial. Todo lo
contrario.
5 “Uno de los extremos de una red de comunicaciones entre múltiples
procesos, y que funciona de un modo muy similar a un teléfono o un
buzón de correos. Lo más importante de un socket es su dirección de
red (algo así como el número de teléfono).” – [WALL; página 1002]
6 UDP es el acrónimo de User Datagram Protocol, un protocolo de
comunicaciones entre máquinas conectadas en una red que se presenta,
con frecuencia, como una alternativa al conocido protocolo de internet
TCP (Transmission Control Protocol). Más información sobre el
protocolo UDP en la RFC 768, http://www.faqs.org/rfcs/rfc768.html
7 Un patrón de diseño viene a ser un conjunto de sugerencias y
orientaciones basadas en la experiencia, pensadas para ofrecer
soluciones eficientes y contrastadas a problemas de naturaleza
similar. Más información en
http://www.cs.wustl.edu/~schmidt/patterns.html
8 En la comunicación “multicast”, hay un solo transmisor, y múltiples
receptores. Véase http://www.roads.lut.ac.uk/DS-Archive/MTP.html para
más información.
9 XML es el acrónimo de eXtended Markup Language, es decir un lenguaje
de marcas (esto es, palabras clave que se insertan en ciertos
documentos para determinar el modo en que deben presentarse al
usuario) similar al HTML. La principal diferencia entre estos estriba
en el hecho de que el HTML describe cómo debe mostrarse información en
la web, mientras que el XML describe qué es la información que se va a
mostrar. Para más información, consultar: http://www.w3.org/XML/
10 MD5 es un algoritmo de firma digital que tiene como objetivo
garantizar la integridad de los datos, mediante la inserción de un
código único de 128 bits de longitud, comparable a una “huella
dactilar”, en el sentido de que cada bloque de datos tiene la suya, y
es diferente de cualquier otra. Ver:
http://www.faqs.org/rfcs/rfc1321.html
11 En programación, un “parser” es un intérprete o analizador, que
comprueba la corrección sintáctica de un texto que se le suministra
como entrada. Son una de las partes imprescindibles en el diseño de un
compilador.
12 Más información sobre los enlaces simbólicos en [TACK]; página 358
13 Aunque ésta puede ser externa al propio fuente XML, en cuyo caso se
debe incluir una referencia al archivo o la URL en la que la DTD se
encuentra. Esa es precisamente la táctica que adopta este proyecto.
14 Conocida la definición de “multicast”, es fácil deducir el
significado de este tecnicismo: se trata de una transmisión que
involucra a un solo emisor y a un solo receptor. Antiguamente se
empleaba la expresión “punto a punto” para referirse a este tipo de
comunicación.
15 Una dirección IP es un número que identifica unívocamente al emisor
o al receptor de un paquete de datos que se envía a través de
Internet. Hasta la fecha (Marzo 2002), la versión empleada es la 4,
que define direcciones de 32 bits. En un futuro próximo, se espera que
comience a aplicarse la versión 6, con direcciones de 128 bits.
16 A las pruebas me remito: [WALL], página 434 en adelante.
17 Recordemos que este campo contendrá la cadena “localhost” si la
entrada pertinente de la TDV hace referencia a un fichero o directorio
local, y una dirección IP, si se trata de una entrada ubicada en un
nodo remoto. En cualquier caso, estas cadenas sólo figurarán en la
primera fila de la TDV de cada bloque. El resto contendrá el valor 0
(ver página 16)
18 Aunque no es la acepción exacta, una interrupción puede entenderse
como algo parecido a una “señal urgente que se envía a un proceso, de
modo que éste debe detenerse para atenderla”. Las interrupciones
software son enviadas por aplicaciones, lo que las distingue de las
hardware, enviadas por dispositivos electrónicos, a través de canales
físicos.
19 De acuerdo con [WALL], página 998, un pragma es un módulo estándar
cuyas sugerencias prácticas se reciben (y posiblemente, se ignoran) en
tiempo de compilación.
”A standard module whose practical hints and suggestions are received
(and possibly ignored) at compile time”.
20 Que se describe con todo lujo de detalles en la sección 5.6.7.- El
programa de configuración, y se complementa con la información que
puede encontrarse en la sección 6 – Breve manual del administrador.
21 Como se detalla en el apartado “Module Privacy and the Exporter” de
[WALL].
22 No confundir con los puertos “físicos” que cualquier ordenador
personal incluye de serie (paralelo, serie, USB…). En programación de
aplicaciones destinadas a utilizarse en una red, un “puerto” es un
punto de conexión lógica, con un número único asignado.
23 Aunque los métodos de comunicación, en Perl, suelen devolver la
dirección de la máquina remitente, no lo hacen con un formato legible.
Las IP se transmiten comprimidas, así que es necesario procesarlas
antes de poder imprimirlas en pantalla o en un fichero.
24 Un “handle” (también llamado a veces “descriptor”) es algo parecido
a una variable que representa a una estructura de datos más compleja,
destinada al intercambio o almacenamiento de información, como en el
caso de los sockets de comunicación o ficheros. De esta forma, las
operaciones que se apliquen sobre dicha variable (generalmente, se
trata de apertura, cierre, lectura y escritura), se estarán aplicando
también a la estructura a la que representa.
25 Un “firewall” es un conjunto de aplicaciones destinadas a evitar
que usuarios no autorizados accedan a información privada. Más
información en http://www.linux-firewall-tools.com/linux/
26 En http://www.iana.org
27 Ver la sección 5.5 - Códigos de respuesta , página 29.
28 La característica más notable de las estructuras hash es que
constan de datos accesibles, de forma inmediata (no secuencialmente),
mediante claves únicas. Con frecuencia, se comparan con diccionarios:
las claves, en ese caso, serían las palabras, y los datos que se
buscan, las definiciones de éstas.
29 Que significa “retorno de carro y nueva línea”.
30 [WALL] dedica uno de sus capítulos más densos a las expresiones
regulares (y no es para menos): “Pattern Matching”, a partir de la
página 139.
31 Véase [WALL], página 115.
32 Un “proxy” actúa como intermediario (algunos autores dirían
“embajador”) entre el cliente y un tercero. Entre los objetivos de
esta táctica se cuentan el aumento de la seguridad y de la eficiencia.
En el caso que nos ocupa, este intermediario “hace creer” al cliente
que el archivo que ha pedido está en la máquina a la que se ha
conectado.
33 Un contexto podría definirse como una zona del código fuente en la
que es visible(utilizable) un determinado grupo de variables. Suele
hablarse de contextos o entornos locales (que afectan a una región
bien delimitada) y globales (que se refieren a todo un módulo, o
incluso a toda una aplicación). En Perl, el uso de contextos es
flexible y potente… y precisamente por eso, nada trivial. No es
infrecuente sobreescribir accidentalmente el contexto de una variable,
definiéndola más de una vez dentro del mismo.
34 Un procedimiento recursivo es aquel que se define en términos de sí
mismo. Digamos que cada llamada a una rutina recursiva se hace sobre
una versión más simple del problema original, hasta alcanzar un caso
límite o “base de la recursión” en el que el proceso se detiene (de lo
contrario, se produciría una serie indefinida de llamadas). El ejemplo
más clásico es el de la función “factorial”. El factorial de un número
“n” se define como “n” multiplicado por el factorial de “n-1”. A su
vez, el factorial de “n-1” es “n-1” multiplicado por el factorial de
“n-2”, etc. El proceso se detiene en el caso base: el factorial de 1.
35 Como se puede leer en [TACK], páginas 223, 238, 359-363
36 En puridad, en Perl se evalúa como “cierta” cualquier expresión
distinta de cero, “undef” y “false”.
37 De hecho, esta es una de las ventajas del sistema de FTP
distribuido.
38 Un procedimiento bastante extendido, que se utiliza para determinar
el tiempo de respuesta de un nodo. Para explicarlo, se suele poner el
ejemplo del sonar de los submarinos (aunque, obviamente, los
fundamentos físicos no tienen nada que ver): la máquina que lo lleva a
cabo envía una serie de paquetes de datos hacia el objetivo. Éste, si
los recibe, debe responder. El nodo emisor no tiene más que calcular
el tiempo que ha transcurrido desde que se enviaron los datos hasta
que se reciba el “eco” del objetivo.
39 Claro que, si uno de los procesos se bloquea justo cuando está en
la región crítica, tendrá que ser abortado sin que llegue a borrar el
archivo “RC”, así que el recurso quedará inaccesible para todos los
demás. La posibilidad es muy pequeña (prácticamente despreciable),
pero convendría que se tuviera en cuenta, como se menciona en la
sección 7 - Opciones abiertas.
40 No confundir con el hecho de que sea el objetivo de una descarga
remota. En este caso, sólo cabe la posibilidad de que este código se
esté ejecutando en el servidor origen.
41 Las direcciones IPv6 miden 16 bytes de longitud. Las direcciones
actuales, IPv4, sólo emplean 4 bytes. Esto permite distinguir entre un
máximo de 4.294.967.296 dominios diferentes. En su momento, parecían
más que suficientes (casi uno por cada habitante del planeta), pero
una vez más, y como sucedió cuando hubo quien aseguró que los primeros
PCs nunca necesitarían más de 640 Kbytes de RAM, los hechos se
encargaron de demostrar que, cuando hay que poner alguna cota al
crecimiento de algún sector informático, más vale que sea MUY holgada.
Es de suponer que con 16 bytes, tendremos dominios libres para rato (a
no ser que algún día haya más máquinas conectadas a Internet, que
estrellas en todas las galaxias conocidas).
42 A grandes rasgos, los órdenes de eficiencia pretenden estimar el
número de operaciones que tendrá que llevar a cabo un algoritmo, para
un cierto número de elementos de entrada. Así, un orden de n cuadrado
significa que para, digamos, 100 elementos de entrada, serán
necesarias 1002 = 10.000 operaciones.
43 Un checksum viene a ser algo parecido a una firma digital, aunque
su finalidad principal, más que la de encriptar un bloque de datos, es
la de garantizar su corrección.
44 Hay que admitir que la solución propuesta no es precisamente un
prodigio de elegancia, pero es indiscutiblemente legible, y no resulta
especialmente ineficiente: téngase en cuenta que se utilizan dos
bucles consecutivos, de orden n. Peccata minuta para la capacidad de
proceso de las máquinas modernas.
45 Recuérdese que ésta sólo aparece en la primera fila de la TDV. Las
demás, para ahorrar espacio, contienen un cero en este campo.
46 A efectos prácticos, así es porque, ¿a quién le interesaría
conectar con un servidor con un retraso de 11 segundos?
47 Un mirror de un servidor es, digámoslo así, una “copia perfecta”
del mismo, en lo que respecta a los contenidos. Todos los archivos que
un cliente puede encontrar en la máquina original, estarán presentes,
y además bajo la misma estructura de directorios, en el mirror
(literalmente, “espejo”, en inglés).
48 Más que eso: es trivial. Añadir un nuevo nodo es tan sencillo como
lanzar la aplicación, tras configurar los cinco parámetros esenciales.
El proceso de alta se lleva a cabo automáticamente.
182

  • UPDATED JUNE 2007 APPENDIX M FORM OF MORTGAGE
  • JARANTOWICE 07112013 R TZ50112013 INFORMACJA O WYBORZE NAJKORZYSTNIEJSZEJ OFERTY
  • “DAMAGED CREDIT? YOU CAN STILL PURCHASE A HOME WITH
  • LEY DE REGULACIÓN DEL DERECHO DE PETICIÓN LEY NO
  • EL CUMPLEAÑOS DE CARLOS – CON GUIÓN DE KLETT
  • REMATE NO 1 2013RE000001BCCR REMATE DE EQUIPO INFORMÁTICO EL
  • HOSPITAL GENERAL DOCENTE “JULIO M ARISTEGUI VILLAMIL”MUNICIPIO CÁRDENAS COMPORTAMIENTO
  • PLAN INTERNATIONAL ALERTA DE LA NECESIDAD DE PROTECCIÓN DE
  • LOGOTIPO DEL CLIENTE MODELO DE CONTRATO DE DESARROLLO DE
  • EL CONTRATO DE IMPLANTACIÓN DE PROGRAMAS INFORMÁTICOS O SOFTWARE
  • ELECCIONES DIRECTOR DEPARTAMENTO 2021 CENSO MIEMBROS CONSEJO DEL DEPARTAMENTO
  • TEACHING OPPORTUNITY! CAC WELCOMES ARTISTS INTERESTED IN SHARING THEIR
  • DARBO SUTARTIS NR [] [] M [] []
  • PORTADA (LOS ELEMENTOS EN ROJO SON EJEMPLOS QUE DEBEN
  • INDICE DEL DOCUMENTO QUE ES FUNAREC21 2 LA BASE
  • ANEXO IV ÍNDICE QUE HABRÁ DE ACOMPAÑAR A LAS
  • MJMR VOL 25 NO 1 2014 PAGES (6368) ALDAHROUTY
  • LATVIJAS SPORTA PEDAGOĢIJAS AKADĒMIJA AKADĒMISKO DARBU IZSTRĀDE RĪGA 2008
  • REVENUE CANADA AGENCY ELECTION PURSUANT TO PARAGRAPH 861(2)(F) ITA
  • [ANNEXII TO AP(DIR SERIES) CIRCULAR NO110 OF 12062013] FORM
  • ЛОМЯНКИ 20 ЯНВАРЯ 2022 ОБРАЗЕЦ ДОГОВОРА АРЕНДЫ МАШИН (УСЛОВИЯ
  • DCCONDUCTIVITY OF A SUSPENSION OF INSULATING PARTICLES WITH INTERNAL
  • FORM NO SH4 SECURITIES TRANSFER FORM [PURSUANT TO SECTION
  • SECRETARIA DE OBRAS PUBLICAS GOBIERNO DEL ESTADO DE AGUASCALIENTES
  • SHAREHOLDERS RESOLUTION APPROVING SHARE TRANSFER COMPANY NAME FZLLC (THE
  • A LA GERENCIA DE ATENCIÓN PRIMARIA DEL AREA UNICA
  • DEPARTMENT OF AGING AND DISABILITY SERVICES (ADS) BUREAU OF
  • PROFITS AND LOSSES REPORT FOR THE 1 QUARTER OF
  • RECUERDEN LA ESTRUCTURA INTERNA DEL TP EL OBJETIVO ES
  • COMBINED MANUAL ISSUE DATE 082019 CAPITAL GAINS AND LOSSES