miércoles, 29 de junio de 2011

Sockets en C (II)

En la entrada pasada describí (de forma relativamente informal) el modelo cliente-servidor y las funciones involucradas en la programación de Sockets bajo Linux, tanto en el caso de sockets orientados a conexión, como en el caso de sockets no orientados a conexión.

En esta entrada voy a hacer una presentación un poco más formal y voy a desarrollar un ejemplo de socket utilizando UDP. Lo aquí mostrado está basado en este tutorial y los apuntes de mis clases de Redes.

Comenzamos por describir los archivos de cabecera involucrados:

1) types.h
Contiene las definiciones de una gran cantidad de tipos de datos utilizados en llamadas al sistema, en particular, algunos utilizados en los siguientes archivos de cabecera.

2) socket.h
Incluye todas las estructuras de datos necesarias para el trabajo con sockets.

3) in.h
Contiene constantes y estructuras necesarias para trabajar con direcciones de Internet.

Entre las estructuras de datos utilizadas, es destacable sockaddr_in, cuya definición es la siguiente:

struct sockaddr_in
{
short sin_family; /* must be AF_INET */
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8]; /* Not used, must be zero */
};

Los objetos tipo sockaddr_in se utilizan par almacenar direcciones de Internet. Como veremos en el código, necesitaremos lógicamente 2 variables de este tipo: una para almacenar la dirección del servidor y una para la dirección del cliente.


El caso de estudio

Tenemos un sistema de balanceo de carga, en el que por X razón, nos interesa que el nodo director conozca en todo momento el estado de carga de los servidores reales. En una entrada pasada, estudiamos el método utilizado para el cálculo de la carga del sistema en Linux, cuyo valor podemos leer en el archivo /proc/loadavg.

Con esto en mente, lo que planeo hacer a continuación es:
1) Un proceso para los servidores reales que cada cierto tiempo envíe a través de un socket UDP su carga al nodo director.
2) Un proceso en el nodo director que esté siempre en espera de la data enviada por los servidores reales y la almacene en un log.


Procedimiento para los servidores reales

Este procedimiento es realmente sencillo, básicamente hace 3 cosas:
1) Abrir el socket UDP
2) En un proceso de n iteraciones lee el archivo /proc/loadavg y envía lo leído a través del socket
3) Envía una señal de parada al nodo remoto y cierra el socket

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define tam_buffer 100
#define NAME_FILE "///proc//loadavg"

int main( int argc, char *argv[] )
{

FILE *file_load;
char *d, line_load[tam_buffer];
double OneMin, FiveMins, FifteenMins;
int i = 0, socket_udp, tam, num_bytes, time_sleep;
struct sockaddr_in addressip;

int n_packets = 0, curr_packets = 0;

/* validation input arguments */
if( argc != 5 )
{
printf(">:ERROR: Use mode: ./realsv IP PORT TIME N_PACKETS\n");
exit( -1 );
}
time_sleep = atoi(argv[3]);
n_packets = atoi(argv[4]);

/* init socket */
addressip.sin_family = AF_INET;
addressip.sin_port = htons( atoi( argv[2] ) );
inet_aton( argv[1], &(addressip.sin_addr) );
bzero( &(addressip.sin_zero), 8 );
socket_udp = socket(AF_INET, SOCK_DGRAM, 0);

tam = sizeof(struct sockaddr);

do
{
if((file_load = fopen(NAME_FILE,"r")) == NULL )
{
fprintf(stderr,"Can't open the file, %s \n",NAME_FILE);
exit(1);
}
else
{
if(feof(file_load))
{
fclose (file_load);
break;
}

d = fgets(line_load,100,file_load);
sscanf(line_load,"%lf %lf %lf", &OneMin, &FiveMins,
&FifteenMins);
// Send load to lvs
num_bytes = sendto(socket_udp, (double *)&OneMin,
sizeof( OneMin ), 0,
(struct sockaddr *)&addressip, tam );

i++;
printf("\n>: Send No: %d", i);
printf("\n>: bytes send: %d", num_bytes);
printf("\n>: load last minute: %f \n",OneMin);
// Close the file
fclose (file_load);
sleep(time_sleep);
}
curr_packets++;
}while(curr_packets < n_packets);

// Send STOP Signal:
OneMin = -1.0;
num_bytes = sendto(socket_udp, (double *)&OneMin,
sizeof( OneMin ), 0,
(struct sockaddr *)&addressip, tam );

// Close the connection:
printf("\n>: Close conection.");
close(socket_udp);

return 0;
}

El código se explica solo, sin embargo haré algunas notas.
  • El procedimiento toma como argumentos: la IP del nodo remoto, el puerto del nodo remoto, el tiempo (en segundos) que indica cada cuanto se debe enviar un paquete y el número de paquetes que se deben enviar.
  • Se utiliza la función sleep() como temporizador para el envío de los paquetes.
  • Se utiliza AF_INET para indicar que se trata de un socket que funciona en red.
  • Se utiliza la función htons() para evitar problemas "entendimiento" entre máquinas, pues todos los procesadores no representan los número de la misma forma (algunos lo hacen con el byte más significativo a la izquierda, otros a la derecha).
  • Es importante comprender los argumentos que se pasan a sendto().
  • Cuando ya se han enviado los n paquetes especificados por línea de comando, se rompe el bucle, se envía un paquete con -1 para que el nodo remoto (nodo director) sepa que ya hemos terminado, y finalmente se cierra el socket.

Procedimiento para el nodo director

Ahora continuaremos con el procedimiento que se debe ejecutar en el nodo director, el cual es tanto o más sencillo que el anterior. En el nodo director, simplemente estaremos a la espera de los datos enviados por el socket y mostraremos por pantalla cada vez que recibamos alguno.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define tam_load 8
#define tam_ip 20

int main(int argc,char *argv[])
{

struct sockaddr_in iprealsv;
char addressip[tam_ip];
double buffer;
int socket_udp, tam, numbytes = 1, n = 0, interval = 1;

/* validation input arguments */
if(argc != 3)
{
printf("\n>:ERROR: Use mode: ./lvsrcp PORT TIME \n");
exit( -1 );
}

/* init socket */
iprealsv.sin_family = AF_INET;
iprealsv.sin_port = htons( atoi(argv[1]) );
iprealsv.sin_addr.s_addr = INADDR_ANY;
bzero( &(iprealsv.sin_zero), 8 );

socket_udp = socket( AF_INET, SOCK_DGRAM, 0 );
tam = sizeof( struct sockaddr );

if ((bind( socket_udp, (struct sockaddr *)&iprealsv, tam )) == -1)
printf("\n>:ERROR: bind function \n");

while (1)
{
n++;
numbytes = recvfrom(socket_udp, (double *)&buffer,
sizeof(buffer), 0,
(struct sockaddr *)&iprealsv, (socklen_t *)&tam);
printf(">: Package received #: %d \n", n);
printf(">: bytes received: %d \n", numbytes);
printf(">: load real server: %f \n", buffer);
strcpy(addressip, inet_ntoa(iprealsv.sin_addr));
addressip[strlen(addressip)] = '\0';
printf(">: real_server: address IP :%s and load: %f \n", addressip, buffer);

if(buffer < 0)
break;
}

printf("\n>: Finished connection. \n");
/* close connection */
close( socket_udp );
return 0;
}

Muy sencillo el código. Simplemente abrimos el socket, tal como en el primer programa, y luego creamos un bucle infinito para esperar a que lleguen los datos. Cuando el programa recibe un paquete cuyo dato es -1, se rompe el bucle, se cierra el socket y se termina el programa.

Es destacable notar que ambas máquinas deben saber qué tipo de datos están enviando/recibiendo; en este caso el único dato que se está enviando es un valor real. Si el paquete está formado por varios datos, lógicamente tendremos que pasar por el proceso de armar el dato compuesto (seguramente en una cadena) del lado del cliente y luego desarmar el dato en el lado del servidor.


martes, 28 de junio de 2011

lunes, 27 de junio de 2011

/proc/loadavg

El archivo /proc/loadavg contiene información acerca de la carga del sistema. Específicamente, los siguientes 5 indicadores:
  1. Promedio de procesos activos en el último minuto
  2. Promedio de procesos activos en los últimos 5 minutos
  3. Promedio de procesos activos en los últimos 15 minutos
  4. Procesos que estan planificados para ejecutarse/total de procesos
  5. Process ID del proceso más recientemente ejecutado
Los 3 primeros indicadores, son en los que nos centraremos en esta entrada. Ellos representan los promedios aproximados exponenciales de la carga del sistema. Mientras mayores sean estos valores, más cargado se encuentra la máquina. Estos promedios son los mostrados por diversos comandos de Linux como uptime, w, top y procinfo.


Forma de cálculo

La fórmula para calcular el valor actual de la carga es la de medias móviles amortiguadas exponencialmente, utilizada ampliamente en Estadística y Economía:

load(t) = load(t-1) * EXP_M + (1 - EXP) * x

En donde:
  • x = número de procesos activos (procesos que están en cola para ser ejecutados)
  • EXP_M = 1/exp(5seg/(60seg*m)), siendo m = 1, 5 y 15 (minutos)
Para 1 minuto:
EXP_1 = 1 / exp(5 seg/1 min) = 1/exp(1/12) =~ 0.91

Para 5 minutos:
EXP_5 = 1 / exp(5 seg/5 min) = 1/exp(1/60) =~ 0.98

Para 15 minutos:
EXP_15 = 1 / exp(5 seg/15 min) = 1/exp(1/180) =~ 0.99

El valor de 5seg involucrado en el cálculo de EXP_M tiene que ver con la frecuencia de cálculo, que es de 5HZ, lo cual implica que la función de cálculo se ejecuta cada 5000ms o 5seg.

Cuanto más grande es EXP_M, la historia "permanece" por más tiempo. Se puede decir que la historia tiene un "valor de decaimiento" de 0.91 para el primer caso, 0.98 para el segundo y 0.99 para el tercero.

Una buena discusión del uso de medias móviles ponderadas exponencialmente y su comparación con otro tipo de medias, se puede leer aquí.


Implementación en Linux

En realidad, estando el kernel de Linux ampliamente optimizado, en vez de representar los valores de EXP_M en punto flotante, se representan en punto fijo, pues los cálculos se realizan en el espacio de kernel y no en espacio de usuario.

La función de cálculo de la carga se encuentra definida en <kernel/timer.c> y su prototipo es:
static inline void calc_load(unsigned long ticks)
Esta función, internamente hace uso de la macro CALC_LOAD(load, exp, n), definida en <linux/sched.h&gt.

La función que imprime los valores de carga en /proc/loadavg se encuentra definida en linux/fs/proc/proc_misc.c, y su prototipo es:
static int loadavg_read_proc(char *page, char **start, off_t off,
                 int count, int *eof, void *data) 
Esta función está constantemente ejecutándose en el sistema, y es así como cada vez que ejecutamos el comando uptime (por ejemplo) podemos acceder a estos valores.


Más información

No es mucha la información que se consigue sobre este tema en la red. Sin embargo dejo un par de enlaces útiles:

BULMA: ¿Cómo se calculas los promedios de loadavg en Linux?. link
TeamQuest: UNIX Load Average. link

jueves, 23 de junio de 2011

Creando un repositorio SVN (Linux)




En esta entrada voy a explicar paso a paso, como crear un repositorio subversion. En mi caso particular, instalo el repositorio en la máquina que hace de servidor en mi casa (en la cual muchas veces codifico), la cual tiene Ubuntu 11.04, y a la cual me voy a conectar para hacer commits/updates desde al menos otras 2 máquinas frecuentemente.

Requisitos

Asumo que se tiene un servidor Apache instalado, puesto que utilizaremos este servidor web para conectarnos via http a nuestros repositorios.

Paso a paso

1) Instalar subversion y el módulo svn para apache.

En mi caso, subversion ya venía instalado por defecto cuando instalé Ubuntu, sin embargo, de no estarlo, es lo primero que debemos hacer:

sudo aptitude install subversion

Luego, instalar el módulo svn para apache:

sudo aptitude install libapache2-svn

2) Grupos y permisología

Esto no es estrictamente necesario, pero 100% recomendable por cuestiones de seguridad. Crearemos un grupo, al cual agregaremos nuestro usuario y el usuario de Apache (www-data):

sudo addgroup svn
sudo usermod -a -G svn marco
sudo usermod -a -G svn www-data

Lo siguiente que debemos crear es el archivo de acceso al repositorio. Así crearemos todos los usuarios que accederán al repositorio:
sudo htpasswd -c /etc/apache2/dav_svn.passwd marco
sudo htpasswd /etc/apache2/dav_svn.passwd otro_usuario

Obsérvese que a partir del segundo usuario, no se pasa el parámetro -c, puesto que ya dav_svn.passwd se encuentra creado. Por cada usuario nos pedirá una contraseña, la cual no tiene por qué coincidir con la contraseña del correspondiente usuario en el sistema (en caso de existir).

3) Modificamos el archivo de configuración

Como root, debemos editar el archivo /etc/apache2/mods-enabled/dav_svn.conf para especificar nuestros datos de configuración. El archivo se explica solo, sin embargo dejo las líneas que me interesan:

<Location /svn>

DAV svn

SVNParentPath /var/lib/svn

AuthType Basic
AuthName "Subversion Repository"
AuthUserFile /etc/apache2/dav_svn.passwd

<LimitExcept GET PROPFIND OPTIONS REPORT>

Require valid-user
</LimitExcept>

</Location>


Ahí especifico la ruta por la cual quiero acceder a los repositorios (en este caso http://servidor/svn), la ruta física donde se almacenarán todos los repositorios (/var/lib/svn), digo que quiero utilizar autenticación básica y especifico la ruta de dav_svn.passwd.

4) Reiniciar el servidor Apache.

sudo /etc/init.d/apache2 stop
sudo /etc/init.d/apache2 start


5) Creamos el directorio padre para nuestros repositorios
sudo mkdir /var/lib/svn

6) Agregar proyectos

sudo svnadmin create /var/lib/svn/myproject
sudo svn mkdir -m "crear la estructura" file:///var/lib/svn/myproject/trunk file:///var/lib/svn/myproject/tags file:///var/lib/svn/myproject/branches

sudo chown -R www-data:svn /var/lib/svn/myproject/
sudo chmod -R g+rws /var/lib/svn/myproject/

7) Comenzar a trabajar

Entramos al directorio donde tengamos los archivos actuales de nuestro proyecto (puede ser en la misma máquina u otra) y hacemos un checkout:

svn co http://server.name/svn/myproject/trunk .

o en el caso de que estemos en la misma máquina, podemos utilizar:

svn co http://localhost/svn/myproject/trunk .

Agregamos lo que tengamos que agregar (por ejemplo con un svn add *) y hacemos el import inicial:

sudo svn import -m "iniciando" . file:///var/lib/svn/myproject/trunk


Para hacer el import inicial, utilicé "file:///", esto fue porque estaba trabajando en el servidor directamente. Si en cambio hubiese estado en otra de las máquinas de la red, debí utilizar http y la dirección IP del servidor.

Y eso es todo. Así como creamos el proyecto "myproject" podemos crear todos los que queramos. El resto es simplemente trabajar y utilizar los comandos svn que utilizamos habitualmente.


Más información

doc.ubuntu-es: Subversion

martes, 21 de junio de 2011

scp con ejemplos

Más que a modo de tutorial o algo así, esta entrada la escribo a modo de recordatorio para mí mismo :)

scp significa secure cp, es un comando Unix/Linux para realizar copias seguras de archivos entre sistemas/usuarios. Es seguro, porque utiliza el protocolo ssh, es decir, que la información se transmite de forma encriptada.

La sintaxis es la siguiente:

scp [[from_user@]from-host:]source-file [[to_user@]to-host:][destination-file]

En donde:
  • from_user es el usuario con el que transmitirá el(los) archivo(s).
  • from-host es el nombre o dirección IP de la máquina desde la cual se enviará el(los) archivo(s).
  • source-file es el(los) archivo(s) o directorio(s) que se quieren copiar. En caso de ser un directorio, se debe especificar la opción -r para copiar todo el contenido del mismo.
  • to_user es el usuario que recibirá el(los) archivo(s).
  • to-host es el nombre o dirección IP de la máquina a la cual se copiará el(los) archivo(s).
  • destination-file es el nombre que recibirá el archivo copiado en el destino.
Además se pueden especificar las siguientes opciones:
  • -p para preservar los tiempos de acceso y modificación, así como los permisos del archivo original.
  • -q para no mostrar la barra de progreso de la transferencia.
  • -r para copiar recursivamente el contenido del directorio origen.
  • -v para mostrar mensaje de depuración.

Ejemplo 1

scp imagen001.jpg 192.168.1.105:imagen001-copy.jpg

Copia el archivo imagen001.jpg del directorio actual, al directorio home del mismo usuario, en la máquina 192.168.1.105 bajo el nombre imagen001-copy.jpg

Ejemplo 2

scp 192.168.1.105:imagen001-copy.jpg ./imagen001-copy2.jpg

Copia el archivo imagen001-copy.jpg del directorio home en la máquina 192.168.1.105, del usuario logueado, al directorio actual, bajo el nombre imagen001-copy2.jpg

Ejemplo 3

scp archivo1.c marco1@192.168.122.130:/home/marco1/programas/

Copia el archivo archivo1.c del directorio actual, al directorio /home/marco1/programas/, utilizando el usuario marco1.

Ejemplo 4

scp -r marco1@192.168.122.130:/home/marco1/codigo/ marco2@192.168.122.131:/home/marco2/codigo/

Copia recursivamente el contenido del directorio /home/marco1/codigo del usuario marco1, en la máquina 192.168.122.130 hacia el directorio /home/marco2/codigo/ del usuario marco2, en la máquina 192.168.122.131.

Más información



lunes, 20 de junio de 2011

Sockets en C (I)


Como parte de mi trabajo de tesis de maestría, debo establecer un mecanismo de comunicación entre varias máquinas (en una red local) que forman un cluster de balanceo de carga. En el nodo director (el encargado de repartir la carga) debo monitorear constantemente el estado de carga de los servidores reales.

En el nodo director debe estar ejecutándose siempre un proceso que recopile cierta información de los servidores reales, mientras que en estos, debe estar ejecutándose un proceso que capture y envíe la información al nodo director.

¿Cómo comunicar estos dos procesos a través de una LAN? La respuesta es simple: Sockets. Un socket es una interfaz de entrada-salida que permite comunicar dos procesos que pueden estar ejecutándose en el mismo sistema o en sistemas diferentes.

En términos prácticos, un socket no es más que un archivo, sobre el cual se pueden utilizar las funciones read() and write() del lenguaje C.

Los sockets pueden clasificarse en 2, de acuerdo al tipo de conexión utilizada:
  • Sockets stream: orientados a conexión, comunicación confiable, utilizan el protocolo TCP.
  • Sockets datagram: no orientados a conexión, comunicación no confiable, utilizan el protocolo UDP.
Nota: Existe un tercer tipo en esta clasificación, los sockets raw, que dan acceso a protocolos de bajo nivel, y no serán tomados en cuenta en lo sucesivo.


El modelo cliente-servidor

Bajo el problema planteado al principio (el cluster de balanceo de carga), se puede apreciar claramente el modelo cliente-servidor, característico de los sockets: el servidor es el proceso que reside en el nodo director del cluster y que está constantemente recibiendo y procesando información (está escuchando, a espera de recibir la data) y el cliente es el proceso que reside en los servidores reales y que cada cierto tiempo contacta al servidor para enviarle sus datos.

Normalmente en un modelo cliente-servidor, el servidor generalmente envía data a los servidores reales, a petición de estos. En este caso, por la definición del problema, esto no se realizará.


¿Qué se necesita?

Para poder establecer la conexión entre cliente y servidor, se necesitan 3 elementos:
  • Protocolo de comunicación: ambos procesos deben hablar el mismo idioma; el protocolo como ya se dijo puede ser TCP o UDP, dependiendo de si se requiere confiabilidad o no.
  • Dirección IP del servidor: evidentemente, dentro de la red, para que un cliente pueda alcanzar al servidor debe conocer la dirección IP de este.
  • Puerto: identifica al proceso en la otra máquina.

Modelo cliente-servidor orientado a conexión

Para los sockets tipo stream, el modelo cliente-servidor es el siguiente:

En el servidor, lo primero que se hace es abrir el socket, mediante la función socket(int dominio, int tipo, int protocolo). Luego se informa al sistema operativo que se ha abierto el socket, mediante la función bind(int descriptor, (struct sockaddr *) direccion, socklen_t sizeof(direccion)). Posteriormente se informa al sistema operativo que el proceso está listo para recibir solicitudes, mediante la función listen(int descriptor, int max_clientes). Finalmente, la función accept(int descriptor, (struct sockaddr *) direccion, socklen_t sizeof(direccion)) acepta las solicitudes de conexión entrantes, es decir, reserva los recursos necesarios para manejar la conexión.

En el cliente, se comienza también abriendo el socket, mediante la función socket(int dominio, int tipo, int protocolo). Luego se solicita la conexión con el servidor, mediante la función connect(int descriptor, (struct sockaddr *) direccion, socklen_t sizeof(direccion)).

En sistemas Unix/Linux, una vez establecida la conexión, cliente y servidor pueden intercambiar información, mediante las funciones write() y read(), las mismas funciones de escritura/lectura utilizadas normalmente en C. En Windows, se utilizan las funciones equivalentes send() y recv().

Tanto cliente como servidor pueden utilizar la función close(int descriptor) (la misma utilizada para cerrar archivos) para terminar la comunicación y cerrar el socket.


Modelo cliente-servidor no orientado a conexión

Para la creación de sockets tipo datagram, el modelo cliente-servidor es:

El modelo es similar al anterior, con la salvedad de que no se establece una conexión. En el servidor, una vez que el socket se ha asociado con un puerto con la función bind(), queda activado para recibir/responder mensajes, mediante las funciones recvfrom() y sendto(), respectivamente. De igual forma, en el cliente no es necesario solicitar una conexión, sino que una vez abierto el socket, se puede enviar/recibir data.

A diferencia de write() y read(), en las funciones recvfrom() y sendto() se debe especificar el destinatario, pues no hay ninguna conexión previamente establecida.


Más información

Voy a dejar hasta acá esta entrada, para en la siguiente entrar de lleno con un par de ejemplos y explicar algunos detalles propios de la programación, pues teóricamente ya no hay mucho que decir.

En la red se consigue información casi ilimitada sobre el tema. Dejo algunos links:
  • Linux HowTos: C/C++ Sockets Tutorial (inglés). link
  • Programación de sockets en C de Unix/Linux (español). link
  • Sockets UDP en C para Linux (español). link

sábado, 4 de junio de 2011

Desistalando/Instalando PostgreSQL


Ayer pasé un buen rato tratando de que mi servidor local de PostgreSQL 8.4 reaccionara. Postgres es la segunda aplicación que más uso en Windows (o al menos eso dice mi menú de inicio), pero un buen día, así sin más, al intentar conectarme via PgAdminIII al servidor local, me salió el siguiente mensaje: "Server doesn't listen".


El problema que tenía era básicamente que no arrancaba el servidor. Intenté arrancarlo manualmente y nada. Después de intentar unas cuantas cosas infructuosamente y para hacer el cuento corto, encontré el problema: postmaster.pid. Por alguna extraña razón que ignoro, este archivo se corrompió y era el que impedía que mi servidor arrancara correctamente. Lo eliminé, reinicié y ¡listo! Problema resuelto.

Como el cuento se hizo realmente corto, aprovecho para comentar algo sobre la desinstalación/instalación de Postgres; proceso que repetí unas cuantas veces mientras le buscaba solución al problema.

La situación: tengo que desinstalar postgres, pero en mi servidor local tengo varias bases de datos sin respaldo reciente. El temor que tenía era, por supuesto, perder la data, pero afortunadamente la gente de Postgres entiende que esto puede pasar y NO elimina el contenido del subdirectorio data, que se encuentra dentro del directorio donde tengamos instalado el programa. Al finalizar la desinstalación, el wizard muestra un mensaje como el siguiente:


Ya con la tranquilidad que da no haber perdido la data, sólo queda eliminar el usuario postgres. Esto lo hacemos desde una cónsola de MS-DOS, abierta como Administrador:


Y así, ya tendremos desinstalado postgres y eliminado el usuario postgres del sistema. Podremos reinstalar. Durante el wizard de instalación, si seleccionamos el antiguo directorio data, Postgres utilizará los mismos archivos de configuración (postgresql.conf, pg_hba.conf, etc) y podremos utilizar las mismas bases de datos que antes.