WCF: OneWay y bloqueo del cliente
07 Feb 2010Cuando me estaba preparando el MCTS sobre WCF leí una afirmación sorprendente en el Training Kit oficial de Microsoft. En él venían a decir sus autores, en una traducción más o menos libre, lo siguiente sobre el patrón de intercambio de mensajes OneWay:
Dada la naturaleza OneWay del canal, uno podría pensar que, tan pronto el consumidor envía el mensaje, éste se procesa asíncronamente y el cliente es libre de hacer otras cosas. Sin embargo, la forma en que la maquinaria de WCF funciona significa que el consumidor, de hecho, se bloquea, incluso si el mensaje es OneWay, hasta que el dispatcher entrega el mensaje a una instancia del servicio, en la forma de una llamada a un método del objeto.
Teniendo en cuenta que OneWay es un patrón “Fire-and-Forget”, no encaja mucho que el cliente tenga que mantenerse bloqueado hasta que el servidor entrega el mensaje a un objeto que implemente el contrato de servicio. Es importante recordar que alguna de las particularidades de OneWay no hacen sino resaltar esta naturaleza “Fire-and-Forget”; por ejemplo, las siguientes dos propiedades:
- Un método OneWay no soporta retornar ningún resultado, siempre será un método void. Tampoco soporta el atributo FaultContract, pues no pueden definirse errores que vaya a devolver al no tener capacidad de devolver nada.
- Un método OneWay no soporta fluir transacciones entre el cliente y el servidor, y viceversa.
No parece que lo afirmado por los autores del Training Kit encaje mucho con estas propiedades (y otras que se quedan en el tintero) de OneWay. Así que lo mejor es salir de dudas con un pequeño ejemplo, para ver la validez de dicho comentario.
La forma más sencilla de poder comprobarlo es aprovecharse de la forma en que está definido el pipeline de WCF. Son varios los puntos de extensibilidad que tiene este framework. Si colocáramos en uno de ellos una clase que retuviera la entrega del mensaje a un objeto servidor, podríamos comprobar si efectivamente el cliente sigue bloqueado. Para comprobarlo, vamos a usar un Message Inspector.
Inyectar un Message Inspector
Vamos a partir de un nuevo proyecto de librería WCF. La estructura que nos genera Visual Studio nos vale; dejaremos únicamente un método que no devuelva nada, como se ve a continuación.
/// <summary> /// One Way Service. /// </summary> [ServiceContract] public interface IService { /// <summary> /// OneWay method. /// </summary> /// <param name="value">Value to send.</param> [OperationContract(IsOneWay=true)] void SendData(int value); }
La implementación de este contrato de servicio no tiene demasiada importancia, con saber que deberíamos colocar un breakpoint al comienzo del método para saber en qué momento es realmente invocado, es suficiente.
Vamos ahora a empezar a crear el Message Inspector. Crearemos una clase que implemente la interfaz IDispatchMessageInspector e implementaremos uno de sus métodos.
/// <summary> /// My message inspector class. /// </summary> public class MyMessageInspector : IDispatchMessageInspector { public object AfterReceiveRequest( ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { // Crear un "replicador" de mensajes y usarlo para obtener una copia del mismo MessageBuffer buffer = request.CreateBufferedCopy(int.MaxValue); string messageContent = buffer.CreateMessage().GetReaderAtBodyContents().ReadOuterXml(); System.Diagnostics.Debug.WriteLine(messageContent); // Asignar una copia sin leer del mensaje en request, para que otros // componentes del pipeline de WCF puedan leerlo sin fallar. request = buffer.CreateMessage(); // Devolver null como resultado, que será lo que reciba el metodo BeforeSendReply // en el parámetro correlationState return null; } public void BeforeSendReply( ref System.ServiceModel.Channels.Message reply, object correlationState) { } }
En cuanto al método AfterReceiveRequest, se ejecuta en el camino de subida del mensaje desde la red hacia el objeto que va a servir esta petición. Queremos mostrarlo en la consola, para lo cual vamos a crear un buffer y generar una copia del mismo. Es importante usar el buffer en primer lugar, puesto que un mensaje no puede leerse dos veces. Si lo hiciéramos con el parámetro request, luego no podría leerse otra vez por otros componentes del pipeline de WC y cada petición fallaría irremediablemente.
En cuanto al método BeforeSendReply, no queremos hacer nada especial con él así que simplemente dejamos pasar el mensaje reply, sin leerlo para no tener el problema antes comentado de lecturas. Como curiosidad, decir que lo que devuelve el método AfterReceiveRequest lo recibe el método BeforeSendReply en su parámetro correlationState, para poder relacionar ambas llamadas entre sí.
Ahora que ya tenemos el Inspector, es el momento de modificar la configuración de WCF para usarlo. Para ello tenemos que crear una clase que modifique el comportamiento del endpoint sobre el que vamos a escuchar. Para ello, tendremos que implementar la interfaz IEndpointBehavior.
/// <summary> /// Custom endpoint behavior. /// </summary> public class MyEndpointBehavior : IEndpointBehavior { public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher) { endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new MyMessageInspector()); } public void Validate(ServiceEndpoint endpoint) { } }
De esta interfaz, como se puede observar, sólo nos interesa implementar el método ApplyDispatchBehavior, puesto que nuestro MessageInspector sólo va a actuar en el lado del servidor. Es importante no dejarnos ningún método con su implementación por defecto, puesto que tirar una NotImplementedException en cualquiera de ello tendría consecuencias indeseadas.
Ya sólo nos queda una última clase para tener todo el código listo. Este comportamiento personalizado para el endpoint necesitamos configurarlo de algún modo. La forma más limpia siempre es a través del fichero app.config, pero para ello necesitamos representar este comportamiento personalizable como un elemento de configuración. Esto podemos hacerlo creando una clase que herede de BehaviorExtensionElement, clase que por otra parte cargará nuestro comportamiento custom para el endpoint. El código sería algo así:
/// <summary> /// Custom behavior extension element for the custom endpoint behavior. /// </summary> public class MyBehaviorExtensionElement : BehaviorExtensionElement { public override Type BehaviorType { get { return typeof(MyEndpointBehavior); } } protected override object CreateBehavior() { return new MyEndpointBehavior(); } }
¿Sencillo, verdad? La clase simplemente representa al comportamiento personalizado que deseamos añadir como configuración. ¿Y ahora esto cómo se usaría? Primero se cargaría la clase que acabamos de definir como una extensión de los behaviors, y posteriormente se definiría como nueva configuración para un endpoint. Finalmente se cargaría en el endpoint correspondiente esta nueva configuración. Sin embargo, con las novedades que trae WCF4 en la configuración, este último paso podemos saltárnoslo, sabiendo que a partir de ese momento todos los endpoints van a tener esa extensión cargada.
<system.serviceModel> <extensions> <behaviorExtensions> <add name="myCustomEndpointBehavior" type="OneWayService.MyBehaviorExtensionElement, OneWayService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/> </behaviorExtensions> </extensions> <behaviors> <endpointBehaviors> <behavior> <myCustomEndpointBehavior/> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior> <serviceMetadata httpGetEnabled="true"/> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
Como se puede ver, en primer lugar se carga la extensión y, a continuación, se define como parte del comportamiento de los endpoints. Al no darle nombre al endpointBehavior, se cargará para todos los endpoints existentes. Por último, he activado la generación del fichero WSDL para poder generar un proxy.
Probando el inspector
Bien, suponiendo que ya tenemos el servicio arriba y un proyecto de cliente correctamente creado. Tras añadir la referencia al servicio, podemos escribir algo como lo siguiente para ver si realmente nuestro cliente se bloquea o no.
using (ServiceReference.ServiceClient proxy = new ServiceReference.ServiceClient()) { Console.WriteLine("Calling the remote server..."); proxy.SendData(int.MaxValue); Console.WriteLine("Remote server called..."); Console.ReadLine(); }
Con esto, es momento de lanzar el cliente. Deberemos tener breakpoints tanto en el método que hemos implementado del inspector, como en la clase que implementa el servicio. Si todo va bien veremos la siguiente secuencia de pasos:
- El cliente invoca al servicio usando el proxy.
- Salta el breakpoint en el MessageInspector. Si ejecutamos cualquiera de las instrucciones del inspector, con ejecución paso a paso, veremos que inmediatamente salta el breakpoint del cliente. Por lo tanto, no se está bloqueando.
- El servidor se queda esperando a que sigamos la ejecución, para llegar al objeto servidor, donde el otro breakpoint debería saltar.
He probado el mismo código con las mismas condiciones (el servicio desplegado en IIS 7.0), pero con el framework anterior y el resultado es el mismo. Parece, por tanto, que OneWay sí es realmente un patrón “Fire-and-Forget” y no bloquea a los clientes
Bibliografía:
Microsoft .NET Framework 3.5 – Windows Communication Foundation. Self-Paced Training Kit
Descargas: