.NET Remotingsockets.xmlSocketsweb-services.xmlServicios webintroUn poco de historia
En el desarrollo de sistemas complejos usando técnicas de orientación a objetos, una interfaz simple a nivel de bytes como la de los sockets no resulta del todo apropiada. Por eso existen distintas tecnologías mediante las cuales un objeto puede exponer sus interfaces al público para facilitar su utilización a un nivel de abstracción mayor.
En el modelo COM [Component Object Model], todos los objetos han de implementar la interfaz IUnknown mediante la cual se controla su ciclo de vida (contando las referencias existentes a un objeto se puede saber cuándo puede ser éste eliminado) y se pueden explorar sus características (consultando los interfaces que implementa o, más bien, preguntando si el objeto soporta un interfaz dado):
[uuid(00000000-0000-0000-C000-000000000046)]
interface IUnknown {
HRESULT QueryInterface (
[in] const IID iid,
[out, iid_is(iid)] IUnknown iid );
unsigned long AddRef();
unsigned long Release();
}
La forma de hacer referencia a un interfaz COM es a través de su identificador IID [Interface IDentifier], un número de 128 bits único a nivel global [GUID: Globally Unique IDentifier].
Hay que mencionar que COM es un estándar binario que ni requiere ni impide el uso de orientación a objetos en el diseño de objetos COM. De hecho, COM se limita a la especificación de interfaces y no permite herencia de implementación (lo que puede considerarse algo positivo en el desarrollo de componentes software, donde es preferible usar composición en vez de herencia), si bien sí permite herencia simple de interfaces (aunque también es cierto que esta característica tampoco se usa mucho, ya que se prefieren definir categorías que no son más que conjuntos de interfaces).
Una vez publicado una intrefaz con su IID, su especificación no puede modificarse bajo ninguna circunstancia (un componente puede implementar distintas versiones de un interfaz pero éstas se tratan en realidad como interfaces diferentes implementados por el componente, lo que, por otro lado, evita los conflictos de nombres que se producirían en un modelo orientado a objetos convencional).
DCOM [Distributed COM] se limita a ampliar el modelo COM de forma transparente mediante la utilización interna de un mecanismo de comunicación entre procesos basado en el uso de llamadas a procedimientos remotos usando un estándar binario definido por DCOM.
COM/DCOM se utiliza mediante la generación automática de proxies en el cliente y stubs en el servidor que se encargan de encapsular la comunicación entre procesos. De cara al programador, ésta se realiza de forma transparente. Los proxies y los stubs se generan automáticamente a partir de la especificación de los interfaces en MIDL [Microsoft COM Interface Definition Language], un ejemplo de la cual aparece arriba en la definición de IUnknown.
COM+ es otra extensión de COM que apareció en el año 2000 con Windows 2000 Server. Su primera versión, COM+ 1.0, integra COM con distintas tecnologías, tales como el procesamiento de transacciones (con MTS [Microsoft Transaction Server]) o el envío de mensajes asíncronos (mediante MSMQ [Microsoft Message Queue server]), entre otras. Su segunda versión, COM+ 2.0, es la plataforma .NET.
La principal innovación que supone COM+ (y, por ende, la plataforma .NET) en la evolución de los productos de Microsoft es la introducción de atributos declarativos. Estos atributos permiten separar distintos aspectos en el mismo sentido en que el desarrollo de software orientado a aspectos permite identificar y aislar asuntos compartidos por distintos componentes. Para entender esto de forma intuitiva, digamos que los aspectos sirven para centralizar código que de otra forma aparecería duplicado y esparcido por distintas partes de una aplicación (p.ej. control de acceso, serialización, sincronización, transacciones...).
appdomainsDominios de aplicación
El CLR [Common Language Runtime] de la plataforma .NET divide cada proceso en uno o varios dominios de aplicación. Dichos dominios aislan los objetos que contienen de todos los demás que queden fuera del dominio, de forma que para que dos objetos de distintos dominios se puedan comunicar es necesario utilizar "marshalling" (el mecanismo mediante el cual se empaquetan datos para su transmisión, también conocido como serialización):
image/remoting/application-domains.gif
El CLR se encarga de permitir la realización de llamadas que atraviesen los límites de un dominio, de un proceso y de una máquina. Cuando los objetos están en el mismo dominio, la llamada es local. En cualquier otro caso, la llamada es remota (de ahí lo de .NET Remoting), incluso cuando los objetos estén en el mismo proceso pero en diferente dominio. Las llamadas locales son inmediatas (como en cualquier invocación a una función en un lenguaje de programación tradicional), mientras que las llamadas remotas involucran el uso de "marshalling" a través de proxies.
El siguiente fragmento de código muestra el nombre del dominio de aplicación actual y de los assemblies que se hallan en él:
using System.Reflection;
using System.Runtime.Remoting;
...
AppDomain domain = AppDomain.CurrentDomain;
Console.WriteLine("Dominio actual: " + domain.FriendlyName);
Assembly[] loadedAssemblies = domain.GetAssemblies();
Console.WriteLine("Assemblies en el dominio actual:");
foreach (Assembly assembly in loadedAssemblies)
Console.WriteLine (assembly.FullName);
src/remoting/Dominio.cscodeCódigo fuente de ejemplo
Los dominios de aplicación constituyen unidades aisladas en el CLR. En ellos, una aplicación puede ejecutarse o detenerse sin afectar a las aplicaciones que se ejecutan en otros dominios de aplicación. De hecho, una aplicación no puede acceder directamente a los recursos que se encuentren en un dominio de aplicación distinto del suyo. Gracias a ello, un fallo en una aplicación se puede mantener confinado en los límites de un dominio de aplicación de forma que, aunque distintos dominios de aplicación estén en un mismo proceso, los demás dominios de aplicación no se verán afectados por el fallo.
contextsContextos
Los contextos definen una partición de los dominios de aplicación, de tal forma que los objetos pertenecientes a un contexto comparten las propiedades de su contexto. Los contextos en COM+ derivan de los apartamentos COM (el mecanismo mediante el cual se controla la sincronización entre hebras en COM) y de los contextos MTS (que separan objetos en función de su "dominio transaccional").
Un objeto puede ser ágil [context-agile] o estar ligado a un contexto [context-bound], lo cual viene predeterminado si el objeto deriva de la clase System.ContextBoundObject. Un objeto ágil A puede interactuar con un objeto B ligado a un contexto como si A estuviese en el mismo contexto que B. Esto es, puede llamarse a A desde cualquier contexto o dominio de aplicación libremente. Cualquier llamada que requiera pasar los límites de un contexto es interceptada y, dependiendo de las propiedades del contexto, es preprocesada, postprocesada o, simplemente, rechazada.
image/remoting/remoting-context.gifarchitectureArquitectura de .NET Remoting
Sobre la infraestructura definida por los contextos y empleando la capacidad de reflexión del CLI [Common Language Infraestructure], .NET Remoting (=CLR Object Remoting) proporciona las bases para construir una amplia variedad de estilos de comunicación: desde llamadas síncronas (como DCOM) hasta llamadas completamente asíncronas (con la posibilidad de sondeo y notificación de la terminación de la llamada), ya sea utilizando una codificación binaria sobre un canal TCP o empleando SOAP (en XML y, usualmente, sobre HTTP).
Canales
Los canales proporcionan el medio mediante el cual transmitir mensajes de extremo a extremo. Se encargan de transmitir mensajes entre dominios de aplicación, puede que en distintas máquinas conectadas a través de una red de ordenadores, entre distintos procesos concurrentes que se ejecutan en una máquina o, simplemente, entre dominios de aplicación dentro de un proceso. La plataforma .NET lleva incorporadas implementaciones de los canales para realizar la comunicación mediante sockets sobre TCP o HTTP.
image/remoting/remoting-channel.gif
Un canal normal, HTTP o TCP, no incorpora ninguna medida de seguridad a la hora de transmitir datos. Si queremos que la transmisión de datos se realice de forma segura, tendremos que crear nuestro propio canal (creando una clase que implemente los interfaces IChannelReceiver, IChannel e IChannelSender) o alojar nuestros objetos en el IIS, al cual se puede acceder usando HTTPS si lo configuramos adecuadamente.
Formateadores
Los formateadores se encargan de serializar los objetos .NET para que puedan transmitirse a través de los canales de comunicación. La plataforma .NET puede serializar los objetos en binario y en SOAP/XML. El formateador binario es más eficiente, el que emplea XML resulta más cómodo a la hora de integrar sistemas heterogéneos. Además, podemos crear formateadores específicos que se adapten a nuestras necesidades.
image/remoting/remoting-formatter.gifNota
La diferencia clave entre .NET Remoting y los Servicios Web se encuentra en la forma en que serializan los datos. Los Servicios Web emplean siempre XML, .NET Remoting puede emplear cualquier formateador que implemente la interfaz System.Runtime.Remoting.Messaging.IRemotingFormatter, como System.Runtime.Serialization.Formatters.Binary.BinaryFormatter o System.Runtime.Serialization.Formatters.Soap.SoapFormatter.
Por defecto, los canales HTTP utilizan el formateador SOAP, mientras que los canales TCP usan el formateador binario para acceder a objetos remotos, si bien todo es configurable en .NET Remoting. Cuando las llamadas remotas sólo cruzan los límites de un contexto pero se realizan dentro de un dominio de aplicación, se utiliza un canal especial CrossContextChannel que está optimizado para trabajar dentro del espacio de memoria del proceso y no requiere formateador alguno.
Marshalling
"Marshalling" es el mecanismo mediante el cual se empaquetan las llamadas entre dominios de aplicación para su transmisión (tanto el paso de parámetros como la devolución de resultados). Se puede acceder de forma remota a un objeto de dos formas diferentes:
Por valor (MarshalByValue), haciendo una copia del objeto completo, que se transmite a través del canal perdiendo el enlace entre el original y la copia. El cliente dispondrá localmente de una copia completa del objeto remoto. Esta copia, obviamente, trabajará de forma independiente con respecto a la copia remota del objeto. En otras palabras, en el momento en el que se accede a un objeto remoto por valor, el objeto deja de ser remoto.Por referencia (MarshalByRef), pasando únicamente una referencia al objeto [ObjRef] y creando un "proxy" que sirve de enlace entre el cliente y el objeto remoto. Los objetos remotos siempre residen y se ejecutan en el servidor. El cliente se comunica con el objeto remoto a través del proxy, que sólo tiene una referencia al objeto remoto.
Los objetos ágiles (independientes del contexto) se transmiten por valor si no están ligados a un dominio de aplicación, mientras que se transmiten por referencia si están asociados a un dominio de aplicación y se accede a ellos desde fuera de ese dominio. Los objetos ligados a un contexto siempre se transmiten por referencia fuera de ese contexto.
El acceso a un objeto por valor siempre será más rápido una vez que se dispone de una copia local del objeto, si bien el tiempo que se tarda en obtener inicialmente esa copia puede ser considerable si el objeto es grande. En realidad, lo único que se hace al acceder por valor a un objeto remoto es "descargar" el objeto del servidor y trabajar con él localmente. Por el contrario, al acceder por referencia a un objeto remoto, nuestra aplicación es realmente una aplicación distribuida. Esto puede ser útil cuando los objetos remotos son demasiado grandes (lo que hace prohibitiva su transmisión hasta el cliente) o cuando los objetos remotos residen en un servidor desde el cual se puede acceder a recursos no disponibles directamente desde el cliente.
Proxy
Un "proxy" [apoderado] es un objeto que actúa localmente en nombre de un objeto remoto. Desde el punto de vista del programador, el proxy acepta llamadas como si fuese el objeto real, si bien internamente lo único que hace es delegar en el objeto remoto para ejecutar las llamadas que recibe.
Cuando un cliente quiere acceder remotamente a un objeto del servidor, .NET Remoting crea automáticamente un proxy transparente que hace de servidor en el lado del cliente, de forma que el cliente trabaja con él como si del propio objeto remoto se tratase. El proxy implementa todos los métodos que aparecen en la interfaz del objeto remoto. Las llamadas que recibe las envía al objeto remoto, que es el que verdaderamente se encarga de hacer el trabajo.
Dispatcher
El "dispatcher" se sitúa al otro extremo del canal, recibe los mensajes del proxy, invoca al método real en el objeto remoto, recoge la el resultado y devuelve un mensaje de respuesta.
useUso de .NET Remoting
El modelo de activación de objetos de la plataforma .NET se parece más al de CORBA que al de COM:
Si al otro extremo no se está escuchando, no se puede realizar ninguna conexión (en COM, la activación se produce bajo demanda).No existe ningún registro a modo de páginas amarillas (como sucede en RMI, por ejemplo).Los servidores no se activan remotamente (en este sentido, .NET Remoting se parece más a los sockets TCP que a los componentes COM).Nota
En los proyectos que utilizan clases del espacio de nombres System.Runtime.Remoting es necesario agregar una referencia a la DLL System.Runtime.Remoting.dll.
Uso de dominios de aplicación dentro de un proceso
Comencemos creando un servidor ultra-simple (servidor.exe):
using System;
namespace RemotingExample
{
public class Servidor
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine ("Servidor cargado y ejecutado");
}
// Dominio en el que se ejecuta el servidor
public string Domain
{
get { return AppDomain.CurrentDomain.ToString(); }
}
}
}
Ahora intentamos usarlo desde un cliente que creamos como aplicación independiente (cliente.exe). Para que esta aplicación funcione correctamente, añadiremos una referencia al proyecto servidor.exe (algo que podemos hacer directamente desde el explorador de soluciones del Visual Studio).
Ya en el cliente, creamos un nuevo dominio de aplicación (dentro del mismo proceso) y cargamos en él el assembly servidor.exe:
using System;
using System.Reflection;
using System.Runtime.Remoting;
namespace RemotingExample
{
public class Cliente
{
[STAThread]
static void Main(string[] args)
{
AppDomain newDomain;
ObjectHandle o;
Servidor s;
// Nuevo dominio
AnewDomain = AppDomain.CreateDomain("MiNuevoDominio");
newDomain.ExecuteAssembly("Servidor.exe", null, args);
// Acceso al servidor...
o = newDomain.CreateInstance ( "Servidor",
"RemotingExample.Servidor");
s = o.Unwrap(); // ... para forzar la instanciación del objeto
Console.WriteLine(s + " @ " + s.Domain);
// Fin
Console.WriteLine("Pulse ENTER...");
Console.ReadLine();
}
}
}
CreateInstance recibe como parámetros el assembly en el que se encuentra nuestro "servidor" y el nombre completo de la clase a la que pertenece nuestro servidor.
src/remoting/RemotingError.vs2003.zipzipCódigo fuente del ejemplo: Servidor no serializable (Visual Studio 2003)src/remoting/RemotingError.vs2005.zipzipCódigo fuente del ejemplo: Servidor no serializable (Visual Studio 2005)
Al ejecutar el programa anterior, salta una excepción porque el servidor no es un objeto al que se pueda acceder desde otro dominio de aplicación.
Para que el servidor sea accesible desde otro dominio de aplicación por valor, basta con marcarlo como serializable. Como para acceder al objeto por valor hay que construir localmente una copia exacta del objeto remoto, el objeto completo ha de transmitirse a través del canal existente entre dominios de aplicación diferentes, para lo cual es imprescindible que el objeto sea serializable.
using System;
namespace RemotingExample
{
[Serializable]
public class Servidor
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine ("Servidor cargado y ejecutado");
}
// Dominio en el que se ejecuta el servidor
public string Domain
{
get { return AppDomain.CurrentDomain.ToString(); }
}
}
}
src/remoting/RemotingByValue.vs2003.zipzipCódigo fuente del ejemplo: Paso por valor (Visual Studio 2003)src/remoting/RemotingByValue.vs2005.zipzipCódigo fuente del ejemplo: Paso por valor (Visual Studio 2005)
También podríamos controlar explícitamente cómo se serializa un objeto si, en vez de limitarnos a utilizar el atributo [Serializable], implementásemos explícitamente la interfaz ISerializable.
Hacer que al objeto remoto se pueda acceder por referencia es casi tan simple. Basta con hacer que la clase Servidor derive de System.MarshalByRefObject.
using System;
namespace RemotingExample
{
public class Servidor: System.MarshalByRefObject
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine ("Servidor cargado y ejecutado");
}
// Dominio en el que se ejecuta el servidor
public string Domain
{
get { return AppDomain.CurrentDomain.ToString(); }
}
}
}
src/remoting/RemotingByRef.vs2003.zipzipCódigo fuente del ejemplo: Paso por referencia (Visual Studio 2003)src/remoting/RemotingByRef.vs2005.zipzipCódigo fuente del ejemplo: Paso por referencia (Visual Studio 2005)Acceso a objetos remotos con .NET Remoting
Una vez que ya tenemos los conocimientos básicos necesarios para crear un servidor real al que se acceda remotamente, creamos una biblioteca de clases (Servidor.dll) en la que incluimos la funcionalidad que nuestro servidor proporcionará a sus clientes:
public class Servidor: System.MarshalByRefObject
{
public Servidor()
{
Console.WriteLine("Constructor");
}
~Servidor()
{
Console.WriteLine("Destructor");
}
public string GetInfo ()
{
return AppDomain.CurrentDomain.FriendlyName;
}
}
El servidor lo tenemos que alojar en un dominio de aplicación, para lo cual creamos una aplicación en modo de consola:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Remoting.Channels.Tcp;
...
class Host
{
static void Main(string[] args)
{
ChannelServices.RegisterChannel(
new HttpChannel(8888));
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(RemotingExample.Servidor),
"Servidor",
WellKnownObjectMode.SingleCall);
Console.WriteLine(
"Servidor listo para aceptar mensajes...");
Console.WriteLine(
"Pulse INTRO para salir");
Console.ReadLine();
}
}
Esta aplicación se encarga de crear y registrar un canal, ya que .NET Remoting exige que, para que los clientes puedan acceder a los objetos remotos, éstos estén ligados a un canal cuyo nombre sea conocido por el cliente. Por tanto, lo primero que hacemos es registrar un canal a través del cual se pueda acceder al objeto de forma remota.
Acto seguido, se registran los tipos de objetos a los que se podrá acceder desde fuera del dominio de aplicación del servidor, así como su URL de acceso. Los tipos registrados serán accesibles desde el exterior mientras la aplicación que los aloja esté ejecutándose.
También hay que indicar el tipo de activación de los objetos a los que se accede de forma remota. Desde el servidor, sólo se permiten dos tipos de activación en .NET Remoting:
SingleCall: Se crea una instancia de la clase para cada llamada realizada a través del canal (comunicación sin estado, como en el protocolo HTTP).Singleton: Existe una única instancia de la clase común para todos los clientes. Este singleton sirve de gateway a la lógica de la aplicación.
Para poder hacer referencia al servidor desde el cliente, obtenemos una referencia al proxy del objeto remoto mediante una llamada al método GetObject de la clase System.Runtime.Remoting.Activator. Una vez que tenemos el proxy, podemos usar el servidor como cualquier otro objeto:
// Acceso remoto
ChannelServices.RegisterChannel(new HttpChannel());
Servidor remoto = (Servidor)Activator.GetObject (
typeof(RemotingExample.Servidor),
"http://localhost:8888/Servidor" );
Console.WriteLine( remoto.GetInfo() );
// Acceso local
Servidor local = new Servidor();
Console.WriteLine( local.GetInfo() );
El cliente, como es lógico, debe especificar el canal a través del cual se comunicará con el objeto remoto. Este objeto podría incluso ofrecer sus servicios a través de distintos canales, presumiblemente de distintos tipos. El cliente usará el canal que resulte más adecuado para comunicarse con el objeto remoto.
src/remoting/RemotingExample.vs2003.zipzipCódigo fuente del ejemplo (Visual Studio 2003)src/remoting/RemotingExample.vs2005.zipzipCódigo fuente del ejemplo (Visual Studio 2005)
Los metadatos de la clase Servidor necesarios para poder compilar el cliente los podemos obtener del propio assembly (Servidor.dll) o de la referencia web http://localhost:8888/Servidor?wsdl. A partir de esa referencia se puede crear el ensamblado requerido por el cliente sin tener que distribuir el código completo del servidor. Para lograrlo, podemos usar la utilidad soapsuds:
soapsuds -url:http://localhost:8888/Servidor?wsdl
-oa:InterfazServidor.dll
Cuando un objeto se registra en un canal para que se pueda acceder a él de forma remota, los clientes podrán acceder a todas las propiedades, métodos y variables públicas no estáticas de la clase a la que corresponda el objeto.
En el ejemplo anterior, el ciclo de vida del objeto remoto se controla en el servidor. Cuando un cliente quiere acceder al objeto remoto, el cliente obtiene un proxy y en el servidor se crea una instancia de la clase del objeto remoto para atender única y exclusivamente a una petición del cliente. El objeto remoto sólo se instancia ("activa", si usamos la terminología de .NET Remoting) cuando el cliente llama a un método del proxy. Esto nos permite ahorrar una llamada inicial a través de la red para, simplemente, crear el objeto.
Si usamos el modo de activación SingleCall, el objeto remoto se instancia para atender una única petición, tras la cual el objeto se elimina. Por tanto, esos objetos no mantenienen su estado entre distintas peticiones provenientes de un mismo cliente. Esto, que a primera vista es una seria limitación, permite construir aplicaciones distribuidas escalables, ya que el objeto sólo consume recursos del servidor temporalmente y, en el caso de usar un cluster en el servidor, la carga se puede distribuir con mayor facilidad (al no importar qué servidor atiende cada petición porque éstas son independientes unas de otras). En definitiva, este modo de activación es útil en aplicaciones que sólo requieren realizar una operación rápida e independiente del resto de la aplicación como puede ser consultar un dato concreto en una base de datos para mostrarlo en la interfaz de usuario.
Por el contrario, en el modo de activación Singleton, sólo tendremos una instancia del objeto que atenderá todas las peticiones que se reciban de distintos clientes. A diferencia de SingleCall, como el objeto existe de forma permanente, puede mantener el estado entre distintas llamadas (estado que será global para todos los clientes que accedan a él). Por ejemplo, podríamos usar este modo de activación para implementar correctamente un servidor de chat como un objeto remoto único (en vez de usar direcciones de broadcast).
Un objeto singleton es un objeto único en su tipo que proporciona un punto de acceso global a los servicios implementados por el objeto remoto. Cuando se hace una llamada al objeto remoto, sólo se creará una instancia del objeto si previamente no existe, algo difícil de lograr con DCOM, en el que los objetos remotos siempre se crean desde el cliente y hay que idear mecanismos artificiales que nos permitan comprobar si existe ya un objeto de ese tipo para no crear otro. En el caso de .NET Remoting, se simplifica la creación de "singletons", si bien su implementación seguirá siendo compleja. Los objetos remotos de este tipo tendremos que implementarlos como aplicaciones multihebra para que puedan atender concurrentemente las peticiones de múltiples clientes.
Este tipo de objetos remotos, SAO (objetos activados por el servidor), proporcionan una funcionalidad limitada porque sólo se pueden instanciar usando constructores por defecto, sin parámetros. Pero no constituyen la única opción que tenemos a nuestra disposición en .NET Remoting, como veremos a continuación
Ejercicio
Comprobar cuándo se crean y se destruyen los objetos en el servidor cuando utilizamos los modos de activación Singleton y SingleCallCuando el cliente retoma el control...
En situaciones como las vistas hasta ahora, el servidor controla cuándo se crea un objeto al que se pueda acceder de forma remota. Como no podría ser menos, dada su flexibilidad, .NET Remoting también permite que el cliente sea el que controle el ciclo de vida de los objetos a los que accede de forma remota, al más puro estilo de COM/DCOM.
Este tipo de objetos, denominados CAOs [Client activated objects], se instancian en el servidor cuando el cliente lo solicita (no se espera a que se llame a un método, como sucedía con los SAOs). Esto permite utilizar constructores con parámetros si hace falta. La instanciación/activación de un objeto CAO se realiza de la siguiente forma:
El cliente crea una instancia del objeto en el servidor mediante una solicitud de activación.El servidor crea una instancia de la clase (previamente registrada) y devuelve una referencia al objeto recién creado.El cliente utiliza esa referencia para crear un proxy mediante el cual pueda comunicarse con el objeto remoto.
Como consecuencia del proceso seguido, la instancia del objeto remoto atenderá únicamente las peticiones provenientes del cliente que creó el objeto (a diferencia de los SAOs Singleton, que son objetos compartidos entre todos los clientes). Por ejemplo, se usaría un objeto CAO cuando un cliente quiere realizar un pedido y la realización del pedido involucra ir pasando por una serie de etapas, para las cuales ha de mantenerse en todo momento el estado del pedido.
La implementación en sí de un objeto CAO no difiere de la de un objeto SAO. Tendremos que crear una clase que herede de System.MarshalByRefObject. Lo que sí tendremos que cambiar es la aplicación que aloja al objeto:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Remoting.Channels.Tcp;
...
class Host
{
static void Main(string[] args)
{
ChannelServices.RegisterChannel(new HttpChannel(8888));
RemotingConfiguration.ApplicationName = "Servidor";
RemotingConfiguration.RegisterActivatedServiceType (
typeof(RemotingExample.Servidor) );
Console.WriteLine(
"Servidor listo para aceptar mensajes...");
Console.WriteLine(
"Pulse INTRO para salir");
Console.ReadLine();
}
}
El cliente, como antes, deberá usar un canal HTTP para acceder al servidor. En esta ocasión no obstante, se ha de utilizar la llamada al método estático CreateInstance de la clase Activator. Este método devuelve una referencia a partir de la cual se puede obtener un proxy con el método Unwrap y dicho proxy lo usaremos para acceder al objeto de forma remota:
// Acceso remoto
ChannelServices.RegisterChannel(new HttpChannel());
object[] attrs =
{ new UrlAttribute("http://localhost:8888/Servidor")};
ObjectHandle handle = Activator.CreateInstance
( "Servidor", "RemotingExample.Servidor", attrs);
Servidor remoto = (Servidor) handle.Unwrap();
Console.WriteLine( remoto.GetInfo() );
En resumen, los CAOs ofrecen la máxima flexibilidad, mientras que los SAOs proporcionan una mayor escalabilidad.
"Se alquila"
En .NET Remoting, el ciclo de vida de los objetos remotos se controla mediante leasing, la realización de "contratos de alquiler", igual que en RMI o Jini en Java.
El lease determina el periodo de tiempo que el objeto estará activo en memoria antes de que el CLR lo elimine. En el caso de los objetos activados por el servidor de tipo SingleCall, estos sólo existen mientras dure una llamada a un método. En cambio, los SAOs de tipo Singleton y los CAOs vivirán en función de sus "contratos de alquiler".
Un objeto remoto puede definir su ciclo de vida redefiniendo el método InitializeLifeTimeService() de la clase base MarshalByRefObject. Cuando el objeto se crea, la duración de su contrato de alquiler se fija usando la propiedad InitialLeaseTime del contrato representado por un objeto que implementa la interfaz ILease. La duración predeterminada del contrato es de 300 segundos por defecto, aunque eso se puede modificar en la aplicación que aloja el objeto remoto (de forma global para todos los objetos de la aplicación) o en el propio objeto remoto (de forma local a cada objeto):
// En la aplicación del servidor
public class Host
{
static void Main(string[] args)
{
LifetimeServices.LeaseTime = TimeSpan.FromMinutes(1);
LifetimeServices.RenewOnCallTime = TimeSpan.FromSeconds(30);
...
}
...
}
// En el objeto remoto
public class ObjetoRemoto : MarshalByRefObject
{
...
public override Object InitializeLifetimeService()
{
ILease lease = (ILease)base.InitializeLifetimeService();
if (lease.CurrentState == LeaseState.Initial) {
lease.InitialLeaseTime = TimeSpan.FromMinutes(1);
lease.SponsorshipTimeout = TimeSpan.FromMinutes(2);
lease.RenewOnCallTime = TimeSpan.FromSeconds(30);
}
return lease;
}
...
}
Cada dominio de aplicación contiene un "gestor de alquileres" [lease manager], una hebra más, que elimina los objetos cuando sus "contratos de alquiler" expiran. El gestor de alquileres examina periódicamente los contratos para ver cuáles han caducado y eliminar los objetos correspondientes (cada 10 segundos).
Cada vez que un "patrocinador" del objeto renueva su contrato de alquiler, dicho contrato se amplía hasta el tiempo establecido por la propiedad RenewOnCallTime (120 segundos por defecto), siempre y cuando fuese a caducar antes de ese plazo. Cuando el contrato caduca, se espera el tiempo fijado por SponsorshipTimeout antes de eliminar físicamente el objeto, por si algún patrocinador desea mantener el objeto (de nuevo, 120 segundos por defecto).
El patrocinador del objeto puede ser el cliente que accede a él o cualquier otro objeto interesado en mantener al objeto remoto. Para renovar el contrato, el patrocinador debe llamar al método Renew() del contrato asociado al objeto remoto, el cual se obtiene a través de una llamada al método GetLifetimeService(). El patrocinador, además, recibirá una notificación cada vez que el lease vaya a caducar, para lo cual ha de implementar la interfaz ISponsor y registrarse como patrocinador del objeto:
remoto.GetLifeTimeService().Register ( patrocinador );
No obstante, el contrato se renueva automáticamente cada vez que se accede al objeto, por lo que usualmente no tendremos que preocuparnos por este asunto. Si el contrato llegase a caducar y el objeto remoto fuese eliminado, la llamada al método remoto generaría una excepción (en el caso de los objetos CAO) o volvería a instanciar otro objeto (en el caso de los objetos SAO Singleton, que son únicos en cualquier momento pero no siempre son los mismos).
Ficheros de configuración
En los ejemplos vistos hasta ahora, el cliente debe conocer el tipo concreto del objeto remoto que activa. Eso implica que cualquier cambio en la configuración del servidor requiere recompilar todos los clientes.
Afortunadamente, esto no es necesario porque se pueden usar ficheros de configuración XML de forma que en el código no tengamos que saber de qué tipo concreto son los objetos a los que se accede (aunque siempre deberemos ser conscientes de si el objeto remoto es un SAO SingleCall, un SAO Singleton o un CAO).
En el servidor, una vez que tengamos el fichero de configuración, podemos registrar los canales oportunos y establecer los objetos a los que se puede acceder de forma remota con una simple llamada al método Configure de la clase RemotingConfiguration:
RemotingConfiguration.Configure ("Servidor.config");
donde Servidor.config es el fichero XML con todos los datos necesarios para la configuración del servidor.
Como es lógico, en el cliente también podemos usar un fichero de configuración análogo y no tener que especificar los datos relativos a la configuración del sistema (a costa de que un usuario malintencionado pueda acceder fácilmente a esos datos, disponibles en XML). Además, en el caso del cliente, ya no tenemos que usar Activator para instanciar un objeto remoto. Bastará con crear un objeto con new y la plataforma .NET se encargará automáticamente de instanciar correctamente el objeto, independientemente de si es un objeto local o un objeto remoto:
RemotingConfiguration.Configure ("Cliente.config");
Servidor remoto = new Servidor();
Más fácil, imposible !!!
Ejemplo
Como ejemplo del uso de ficheros de configuración en .NET Remoting, crearemos un sencillo servidor al que se accederá por referencia:
using System;
using System.Net;
using System.Runtime.Remoting;
namespace RemotingExample {
public class Servidor: System.MarshalByRefObject
{
public Servidor() {
Console.WriteLine("Constructor");
}
~Servidor() {
Console.WriteLine("Destructor");
}
public string GetHost () {
return Dns.GetHostName();
}
public string GetApplication () {
return RemotingConfiguration.ApplicationName;
}
public string GetAppDomain () {
return AppDomain.CurrentDomain.FriendlyName;
}
}
}
Este servidor lo alojaremos en un proceso al que se accederá remotamente (Host.exe):
using System;
using System.Runtime.Remoting;
namespace RemotingExample
{
class Host {
static void Main(string[] args) {
RemotingConfiguration.Configure("Servidor.config");
Console.WriteLine(
"Servidor listo para aceptar mensajes...");
Console.WriteLine(
"Pulse INTRO para salir");
Console.ReadLine();
}
}
}
El fichero de configuración Servidor.config especifica el canal que se utilizará para acceder al servidor (un canal TCP en el puerto 7777) y el modo de activación del servidor (SingleCall):
Servidor.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application name="RemotingExample">
<service>
<wellknown mode="SingleCall"
type="RemotingExample.Servidor, Servidor"
objectUri="RemotingExample.rem" />
</service>
<channels>
<channel port="7777" ref="tcp" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
De forma análoga, para el cliente también necesitamos otro fichero de configuración en formato XML:
Cliente.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.runtime.remoting>
<application>
<client>
<wellknown type="RemotingExample.Servidor, Servidor"
url="tcp://localhost:7777/RemotingExample/RemotingExample.rem" />
</client>
<channels>
<channel ref="tcp" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
A partir de este fichero, el acceso remoto al servidor desde el cliente resulta trivial:
using System;
using System.Runtime.Remoting;
namespace RemotingExample
{
class Cliente
{
[STAThread]
static void Main(string[] args)
{
// Acceso remoto
RemotingConfiguration.Configure( "Cliente.config" );
Servidor remoto = new Servidor();
Console.WriteLine( remoto.GetAppDomain() );
Console.WriteLine( remoto.GetApplication() );
Console.WriteLine( remoto.GetHost() );
Console.ReadLine();
}
}
}
src/remoting/RemotingExample.config.vs2003.zipzipCódigo fuente del ejemplo (Visual Studio 2003)src/remoting/RemotingExample.config.vs2005.zipzipCódigo fuente del ejemplo (Visual Studio 2005)
NOTA: Para asegurarnos de que los ficheros de configuración se distribuyen junto con nuestros ejecutables, tenemos que comprobar que la propiedad "Copiar en el directorio de resultados" de nuestros ficheros de configuración .config esté activada.
.NET Remoting también en IIS
Usando ficheros de configuración en XML, si nuestros objetos son de tipo SAO y utilizamos canales HTTP, podemos utilizar el Internet Information Server para alojar nuestros objetos remotos, sin necesidad de crear una aplicación que haga de servidor. Si alojamos la aplicación en un directorio virtual llamado MiServicio dentro del IIS, se podrá acceder al objeto remoto a través de la siguiente URL:
http://localhost/MiServicio/Servidor.soap