Software y Aplicaciones Web

Blog de desarrollo de software y aplicaciones web

Comentarios Recientes

Comment RSS

MSDN Home Page (Argentina)


C# Corner


AspAlliance.com

Declaración

Las opiniones en este blog se proporcionan "TAL CUAL", sin garantías,  no confieren derechos y no reflejan, necesariamente, la opinión de quienes me contratan.
Algunas cuestiones que se comentan en el blog no son reales, cualquier similitud con alguna persona viva o muerta no es más que una coincidencia, tampoco significa que necesite terapia, soy asi.

© Copyright 2007-2010

Propaganda

Este sitio implementa publicidad basada en intereses
Jul
11.
2008

  Múltiples Dominios en la Misma Aplicación

Esta publicación muestra la forma en que se puede lograr que multiples dominios o subdominios funcionen en la misma aplicación y lo más importante, que la aplicación se de cuenta y administre esta situación.

Introducción 

Un motor de blog (la aplicación) se instala en un servidior, eso significa que existe un dominio algo como www.pedro.com.ar que nos permite acceder al blog. Sin embargo se puede hacer que otro dominio como por ejemplo www.jose.com.ar apunte al mismo servidor, en consecuencia se puede acceder a la misma aplicación con uno u otro dominio, todo ese se maneja en las tablas del Domain Name System que asocian los dominios al mismo IP (teoría básica en arquitectura de redes).

Por otro lado, se el hosting lo permite se pueden crear subdominios estos pueden tener la forma de blog.pedro.com.ar o trabajo.pedro.com.ar y hacer que se acceda en el mismo servidor donde reside el dominio principal (pedro.com.ar).

La cuestión es que se necesita saber con que dominio se está accediendo para de ese modo mostrar una u otra parte de la aplicación. Caso concreto es lo que hace Blogger, los millones de blog estan en nombre_del_blog.blogspot.com; observen que el dominio principal es blogspot.com y el subdominio es el que permite acceder a uno u otro blog.

En las siguientes líneas voy a explicar como se puede hacer esto en ASP.NET.


Arquitectura de una aplicación ASP

Cuando una persona accede a un sitio tipeando la dirección del mismo en su navegador, ocurre lo siguiente:

El servidor de WEB (Internet Information Server o Apache o cualquier otro), recibe la petición (request) y como se ha instalado un servidior de páginas activas (Active Server Page - ASP), transfiere la petición a este servidor. Si la aplicación fuese una Java Server Page (JSP) ocurre lo mismo, salvando la diferncia de servidores (uno es de Microsoft y el otro puede ser de SUN)

A continuación, si es la primera vez que se accede a la aplicación se crea una instancia de la clase HttpApplication. Si la aplicación tiene un archivo Global.asax, ASP.NET crea una instancia de la clase Global.asax que es derivada de la clase HttpApplication y la utiliza para representar la aplicación. Aquí está la clave para el problema que estamos desarrollando.

Como pueden ser varios los usuarios que acceden a la aplicación, ASP.NET brinda una manera de mantener la información o estado de cada uno de ellos.

Se puede ver en el segundo diagrama como para cada cliente se instancian otras clases que mantinene la información de que se pide (HttpRequest), y que se devuelve (HttpResponse). Es importante aclarar que solamente en el primer pedido se desencadena eventos que corresponden a la inicialización de la aplicación; en los siguientes pedidos se desencadenan eventos que corresponden a la inicialización de una sesión (session), esta es la otra clave para este problema.

Importante

Es importante ver que aún cuando hay varias sesiones, existe un solo dominio de aplicación; en este dominio se instancia la clase HttpApplication tantas veces como usuarios se conectan al sitio.

Esta caracterísca es la que nos permite crear objetos estáticos que mantinen información para toda la aplicación.

Es bastante común que uno mantenga la configuración de la aplicación en un objeto de este tipo para que de ese modo todos los usuarios accedan a la misma información y además para ahorrarnos memoria del servidior.

 

Generalmente eso se hace con un código que permite acceder a una propiedad (declarada estática) que nos facilita el acceso a un campo (también estático) de manera que la primera vez que se referencia invoca al método que carga la configuración (desde la base de datos o de un archivo XML).

El código más o menos puede ser como el siguiente:


 

   1:   
   2:  public class Configuracion
   3:  {
   4:    public Configuracion()
   5:    {
   6:    }
   7:    private static Configuracion _Actual;
   8:    public static Configuracion Actual
   9:    {
  10:      get
  11:      {
  12:        if (_Actual == null)
  13:        {
  14:          _Actual = CargarConfiguracion();
  15:        }
  16:        return _Actual;
  17:      }
  18:    }
  19:    ...
  20:  }

 


Con este código siempre vamos tener acceso a un único objeto que "vive" en el dominio de la aplicación. Pueden existir millones de clientes accediendo al sitio, todos ellos accederán al mismo objecto.

Para aquellas aplicaciones como un portal que desea mostrar la cantidad de personas que están conectadas esto le viene como anillo al dedo.

Pero por ejemplo en el caso de Blogger cuando se accede a pedro.blogspot.com entramos al blog de Pedro y cuando accedemos a jose.blogspot.com entramos al blog de José; sin embargo ambos blogs estan (virtualmente) en el dominio blogspot.com (digo virtualmente porque Google utiliza mecanismos de redireccion de IP en sus granjas de servidores, eso es pura arquitectura de redes que nada tiene que ver con lo que estoy desarrollando así que no le damos importancia).


El siguiente esquema representa lo que supongo que debe tener Blogger, y es lo que vamos a desarrollar:

La consigna es mantener en el dominio de la aplicación tantas instancias de HttpApplication como usuarios se conectan, pero además se debe mantener instancias de Blog que son accedidas por diferentes usuarios.

Esto incorporar un problema de sincronización. Se debe cuidar el múltiple acceso (actualización) de información del mismo blog, en generál habrá un solo administrador por blog de modo que las propiedades de configuración solo podrán ser "modificadas" por un único proceso, sin embargo puede ocurrir que el usuario administrador acceda por medio de varias instancias de su navegador pudiendo provocar una inconsistencia en los valores de las propiedades.

Otro problema (que no lo es tanto) tiene que ver con la persistencia de la información. Al respecto, si el modelo es relacional (base de datos) basta con incorporar un identificador de blog en las tablas correspondientes y filtrar las consultas de acuerdo a dicho valor; si el modelo es plano (documentos XML) basta con almacenar los documentos en directorios diferentes para cada blog.

Finalmente el problema más grave a resolver es el que tiene que ver con la performance. Sería muy sencillo hacer que cada usuario (sesión) instancie un objeto que representa el blog al que está accediendo. Dado que la mayoría de los usarios son lectores no es aplicable instanciar el mismo objeto para todos (el único acceso es de lectura), de manera que la alternativa es compartir una instancia de blog de acuerdo al dominio que están accdiendo.

Ya expliqué que los objetos estáticos están en el ámbito del dominio de aplicación, esto quiere decir que solo hay un objeto en toda la aplicación. Consecuentemente esta aproximación no sirve, dado que necesitamos una instancia por cada blog (dominio) que se mantiene con la aplicación.

 

 

Desarrollo del Ejercicio

Como dije antes, cuando se instancia el primer objeto de la clase HttpApplication, se puede ejectura código (nuestro código) que nos permite comenzar a administrar la situación. Para estudiar la situación he desarrollado un ejercicio que cuenta cuantas sesiones se abren por cada dominio asociado en una aplicación.

Para probar este ejercicio en modo local es necesario tener el Internet Information Server habilitado en la computadora; hay que apuntar un directorio virtual a la carpeta donde está la aplicación de manera que se pueda acceder por medio de un navegador Internet Explorer o FireFox.

Obviamente la computadora tiene un nombre que se conoce como el host y para el IIS ese nombre viene a ser el "dominio", por otro lado siempre podremos acceder mediante el nombre reservado "localhost".

Como se pude ver, la máquina en donde estuve jugando se llama "icaro"  pero también se puede acceder a los "sitios" habilitados mediante  "localhost". En este ejercicio el directorio (sitio) virtual es demo.

El ejercició lo realizé con Visual Studio 2008, se trata de una Sitio Web para el Framework 2.0 (en realiadad eso no importa demasiado), pero si es importante aclarar que si van a utilizar el Visual Stduio 2005, cuando agregen la clase global de la aplicación archivo Global.asax, el 2005 no genera código asociado y tendrán que escribir todo esto entre las marcas <script runat="server"> ... </script>. Es la única diferencia que hallé.

El siguiente es el código que puse en el archivo de clase para la aplicación Global.asax.cs. 

Por costumbre (viejo programador de C), siempre que voy a utilizar cadenas que no cambian las declaro como constantes de ese modo no consumo memoria innecesaria.

Utilizo una instancia de Dictionary, que no es otra cosa que una tabla hash o mapa que nos permite almacenar objetos y acceder a los mimos por medio de una clave, vean la documentación en MSDN. En este caso el diccionario tiene como clave un string que contiene el dominio y un objeto. Podría definir que va a ser un número pero quiero mostrar como se puede tener un objeto cualquiera y hacer un cast de acuerdo a nuestras necesidades.

Lo importante del código es que la instancia del diccionario se almacena en una colección que es accesibe desde toda la aplicación. Esta colección forma parte del Estado de la Aplicación, y tambien es un diccionario. La propiedad ApplicationDomains es la que nos facilita el acceso al diccionario con los dominios activos. Podría accederse directamente pero el código es más seguro de este modo. He publicado otra propiedad que lleva el número de sesiones que se abren mientras está funcionando la aplicación.

Luego están los metodos que en realidad son administradores de eventos, los que sirven para atrapar situaciones como el inicio y fin de la aplicación, inicio y fin de una sesión. Hay otros metodos pero no vienen al caso.

Con la intención de llevar un registro de lo que está ocurriendo, puse un metodo que graba un log en la carpeta App_Data (cuidado si no les funciona hay que otorgar derechos de escritura al usuario ASPNET).

Finalmente hay un metodo que brinda una cadena con la información de los dominios activos. 

Archivo: Global.asax.cs 

   1:  using System;
   2:  using System.Text;
   3:   
   4:  namespace MultiDomainApplicationDemo
   5:  {
   6:    public class Global : System.Web.HttpApplication
   7:    {
   8:   
   9:      #region Keys for Application and Sessions Collection
  10:      /// <summary>
  11:      /// Estas claves son para manipular los campos en esta clase
  12:      /// Todos los campos se guardan en el Objeto Dictionary Application
  13:      /// </summary>
  14:      const string _ApplicationDomains = "_ApplicationDomains";
  15:      const string _ApplicationSessions = "_ApplicationSessions";
  16:   
  17:      #endregion
  18:   
  19:      #region Fields and Properties
  20:   
  21:      /// <summary>
  22:      /// Diccionario que mantiene el número de sesiones por cada host (dominio o subdominio)
  23:      /// </summary>
  24:      public System.Collections.Generic.Dictionary<string, object> ApplicationDomains
  25:      {
  26:        get
  27:        {
  28:          if (Application[_ApplicationDomains] == null)
  29:          {
  30:            Application[_ApplicationDomains] = new System.Collections.Generic.Dictionary<string, object>();
  31:          }
  32:          return (System.Collections.Generic.Dictionary<string, object>) Application[_ApplicationDomains];
  33:        }
  34:      }
  35:   
  36:      /// <summary>
  37:      /// Número de sesiones que se abrieron
  38:      /// </summary>
  39:      public int ApplicationSessions
  40:      {
  41:        get
  42:        {
  43:          if (Application[_ApplicationSessions] == null)
  44:          {
  45:            Application[_ApplicationSessions] = 0;
  46:          }
  47:          return (int) Application[_ApplicationSessions];
  48:        }
  49:        set
  50:        {
  51:          Application[_ApplicationSessions] = value;
  52:        }
  53:      }
  54:   
  55:   
  56:      #endregion
  57:   
  58:   
  59:      #region Application and Events Handlers
  60:      /// <summary>
  61:      /// Handles the Start event of the Application control.
  62:      /// </summary>
  63:      /// <param name="sender">The source of the event.</param>
  64:      /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
  65:      protected void Application_Start(object sender, EventArgs e)
  66:      {
  67:        Application.Lock();
  68:        ApplicationSessions = 0;
  69:        int a = ApplicationDomains.Count;    // esto es para que se cree el diccionario
  70:        Application.UnLock();
  71:   
  72:        WriteLog("Application Start", false);
  73:      }
  74:   
  75:      /// <summary>
  76:      /// Handles the Start event of the Session control.
  77:      /// </summary>
  78:      /// <param name="sender">The source of the event.</param>
  79:      /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
  80:      protected void Session_Start(object sender, EventArgs e)
  81:      {
  82:        Application.Lock();
  83:        ApplicationSessions += 1;
  84:   
  85:        string domain = this.Request.Url.Host;
  86:        if (!ApplicationDomains.ContainsKey(domain))
  87:        {
  88:          ApplicationDomains.Add(domain, 0);
  89:        }
  90:        ApplicationDomains[domain] = (int) ApplicationDomains[domain] + 1;
  91:        Application.UnLock();
  92:   
  93:        WriteLog(string.Format("Session Start in Host: {0}, Number of Sesions: {1} {2}", domain, ApplicationSessions.ToString(), ListDomains()), true);
  94:      }
  95:   
  96:      protected void Application_BeginRequest(object sender, EventArgs e)
  97:      {
  98:      }
  99:   
 100:      protected void Application_AuthenticateRequest(object sender, EventArgs e)
 101:      {
 102:      }
 103:   
 104:      protected void Application_Error(object sender, EventArgs e)
 105:      {
 106:      }
 107:   
 108:      /// <summary>
 109:      /// Handles the End event of the Session control.
 110:      /// </summary>
 111:      /// <param name="sender">The source of the event.</param>
 112:      /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
 113:      protected void Session_End(object sender, EventArgs e)
 114:      {
 115:        Application.Lock();
 116:        ApplicationSessions -= 1;
 117:        string domain = this.Request.Url.Host;
 118:        if (ApplicationDomains.ContainsKey(domain))
 119:        {
 120:          if ((int) (ApplicationDomains[domain] = (int) ApplicationDomains[domain] - 1) < 1)
 121:          {
 122:            ApplicationDomains.Remove(domain);
 123:          }
 124:        }
 125:        Application.UnLock();
 126:   
 127:        WriteLog(string.Format("Session End, Number of Sessions: {0} {1}", ApplicationSessions.ToString(), ListDomains()), true);
 128:      }
 129:   
 130:      /// <summary>
 131:      /// Handles the End event of the Application control.
 132:      /// </summary>
 133:      /// <param name="sender">The source of the event.</param>
 134:      /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
 135:      protected void Application_End(object sender, EventArgs e)
 136:      {
 137:        WriteLog("Application End", true);
 138:      }
 139:   
 140:      #endregion
 141:   
 142:   
 143:      #region Helper Methods
 144:      /// <summary>
 145:      /// Escribre una cadena de texto en el archivo de log
 146:      /// </summary>
 147:      /// <param name="strText">Cadena de texto a escribir en el log</param>
 148:      /// <param name="isAppend">Modo de escritura: true=Agrega, false=Sobreescribe</param>
 149:      void WriteLog(string strText, bool isAppend)
 150:      {
 151:        string fileName = System.Web.HttpContext.Current.Server.MapPath("~/App_Data/log.txt");
 152:        System.IO.StreamWriter writer = new System.IO.StreamWriter(fileName, isAppend);
 153:        writer.WriteLine(DateTime.Now.ToString() + ", " + strText);
 154:        writer.Close();
 155:      }
 156:   
 157:      /// <summary>
 158:      /// Genera una cadena de texto con los dominios activos
 159:      /// </summary>
 160:      /// <returns>Cadena de texto</returns>
 161:      public string ListDomains()
 162:      {
 163:        StringBuilder sb = new StringBuilder();
 164:   
 165:        sb.AppendLine("\nDominios activos: " + ApplicationDomains.Count.ToString());
 166:        foreach (string domain in ApplicationDomains.Keys)
 167:        {
 168:          sb.AppendLine(string.Format("\tDominio: {0} Sessiones: {1}", domain, ((int) ApplicationDomains[domain]).ToString()));
 169:        }
 170:        sb.AppendLine("-------------------------------------------------------");
 171:        return sb.ToString();
 172:      }
 173:   
 174:      #endregion
 175:   
 176:    }
 177:  }

Con la intención de mostar que los objetos estáticos no nos sirven, puse una clase (estupida) que publica una propiedad que puede manipularse desde toda la aplicación. El siguiente código muestra esto.

Archivo: Estatica.cs 

   1:   
   2:  /// <summary>
   3:  /// Descripción breve de Estatica
   4:  /// </summary>
   5:  public class Estatica
   6:  {
   7:    public Estatica()
   8:    {
   9:    }
  10:    private static string _Dato = string.Empty;
  11:    /// <summary>
  12:    /// Se publica esta propiedad solo para mostrar que 
  13:    /// se comparte en todo el ámbito de la aplicación
  14:    /// </summary>
  15:    public static string Dato
  16:    {
  17:      get
  18:      {
  19:        return _Dato;
  20:      }
  21:      set
  22:      {
  23:        _Dato = value;
  24:      }
  25:    }
  26:  }

Veamos la página de prueba, tiene un TextArea para mostrar el log y un mecanismo para manipular la propiedad Dato de la clase detallada anteriormente.

Archivo: Default.aspx 

   1:  <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="MultiDomainApplicationDemo._Default" %>
   2:   
   3:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   4:   
   5:  <html xmlns="http://www.w3.org/1999/xhtml" >
   6:  <head runat="server">
   7:    <title>Multi Domain Application Demo</title>
   8:  </head>
   9:  <body>
  10:    <form id="form1" runat="server">
  11:      <div style="text-align: center">
  12:        <h2>
  13:          Estado de la aplicación
  14:        </h2>
  15:        <br />
  16:        <asp:TextBox ID="txtLogFile" runat="server" TextMode="MultiLine" ReadOnly="true" Columns="100" Rows="20" ></asp:TextBox>
  17:        <br />
  18:        <div style="float:right;">
  19:          <asp:Button ID="btnRefresh" runat="server" Text="Actualizar" OnClick="btnRefresh_Click" />
  20:        </div>
  21:        <div style="clear:both;"></div>
  22:        <h2>
  23:          Veamos una propiedad estática
  24:        </h2>
  25:        Valor actual:&nbsp;<asp:Label ID="lblDato" runat="server"></asp:Label><br />
  26:        Nuevo valor:&nbsp;<asp:TextBox ID="txtDato" runat="server"></asp:TextBox>&nbsp;&nbsp;
  27:        <asp:Button ID="btnDato" runat="server" Text="Cambiar" OnClick="btnDato_Click" />
  28:      </div>
  29:    </form>
  30:  </body>
  31:  </html>

A continuación está el código asociado a la página, observen que solamente se carga el archivo log y en el otro método se actualiza la propiedad Dato de la clase utilitaria.

Archivo: Default.aspx.cs 

   1:  using System;
   2:   
   3:  namespace MultiDomainApplicationDemo
   4:  {
   5:    public partial class _Default : System.Web.UI.Page
   6:    {
   7:      protected void Page_Load(object sender, EventArgs e)
   8:      {
   9:        if (!IsPostBack)
  10:        {
  11:          LoadLogFile();
  12:        }
  13:        lblDato.Text = Estatica.Dato;
  14:      }
  15:   
  16:      void LoadLogFile()
  17:      {
  18:        string fileName = System.Web.HttpContext.Current.Server.MapPath("~/App_Data/log.txt");
  19:        string fileData = string.Empty;
  20:   
  21:        if (fileName != null && System.IO.File.Exists(fileName))
  22:        {
  23:          fileData = System.IO.File.ReadAllText(fileName, System.Text.Encoding.Default);
  24:        }
  25:        else
  26:        {
  27:          fileData = "No existe el archivo " + fileName;
  28:        }
  29:        txtLogFile.Text = fileData;
  30:      }
  31:   
  32:      protected void btnRefresh_Click(object sender, EventArgs e)
  33:      {
  34:        LoadLogFile();
  35:      }
  36:      protected void btnDato_Click(object sender, EventArgs e)
  37:      {
  38:        Estatica.Dato = lblDato.Text = txtDato.Text + " " + DateTime.Now.ToString();
  39:      }
  40:    }
  41:  }

Como las ideas se califican de "buenas ideas" solamente cuando se realizan, con los integrantes de las céluas Jujuy ASP NET, THE ASP Berries y TeamDJ.Net vamos a utilizar estos conceptos en el desarrollo de AnOtherBlog que es como el nombre lo dice otro motor de blog pero éste tiene la característica que es para aprender y practicar.

Este ejercicio solamente muestra una cuenta de sesiones abiertas por cada dominio asociado a la aplicación, pero del mismo modo que mantenemos un número en el diccionario podremos mantener una instancia de cualquier clase de se defina. Todavía hay que analizar y definir estrategias de performance y devolución de memoria no utilizado, recordemos que el diccionario esta en el dominio de la aplicación y cuando no hay mas sesiones vinculadas a un dominio debería liberarse la memoria asignada.

Para finalizar les dejo el código completo del ejercicio: MultiDomainApplicationDemo.zip (11,55 kb)

Espero que les sirva







Comments (3) -

abel Mexico

Sunday, May 17, 2009 6:16 AM

abel

Como puedo lograrlo en APACHE? PHP.
snif

jack

Friday, July 17, 2009 8:18 PM

jack

Hey very nice blog!! Man .. Beautiful .. Amazing .. I have bookmarked your blog also...

jtentor

Friday, July 17, 2009 8:31 PM

jtentor

Thanks

Add comment




  Country flag
biuquote
  • Comment
  • Preview
Loading