Freigeben über


Sessionless duplex services? No problem. Small issues, yes; problems, no.

Duplex is neato, definitely, because among other things it allows a service to push information at clients as it sees fit. You could just have two services, and one service throws an endpoint at the other and then listens for stuff coming back, too, but there are scenarios for this and scenarios for that. Sometimes a duplex client is the perfect thing but you don't really want the session that comes with the system-provided duplex-supporting bindings used to wire up the two-way communication system. For example, WsDualHttpBinding uses a secure conversation session by default and a reliable session if you turn it on; and the NetTcpBinding surfaces a session associated with the underlying TCP connection.

The problem with these is that there are some session lifetime and creation issues that you have to manage when you use sessions. What's the timeout period? What do I do when Abort happens? And so on. Wouldn't it be nice of you could do duplex without session overhead? Well, you can. But you may want to consider why you want to before you do it. I'll wait to see when Christian thinks it might be helpful. ;-)

A duplex contract is merely a service contract in which there are some operations that specify outbound calls from the service to the client. And a duplex client, then, is merely a WCF application that also hosts a client-side listener for return calls from the service to which it is connected. A client runtime is exposed for use by the -- surprise -- ClientRuntime class. If the client is a duplex client, however, it also hosts a service runtime, exposed for inspection or modification as a DispatchRuntime object available from the ClientRuntime.CallbackDispatchRuntime property. This exposes the entire service side runtime and can be thought of as a service in your client application but for one major exception: It cannot be activated by a call from a service. It must connect to a service first, and then it listens for callbacks from that service. In the WCF system-provided duplex bindings, the information about the callback listener is transferred to the service (so that the service knows where to direct the callback invocations) using the provided session support.

 Therefore, when -- for your own twisted reasons -- you want to build a sessionless duplex application, you have to do a little extra lifting. But not that much. Here's how to build a simple duplex application that does not establish a session using the HTTP transport and a few extra gadgets. First, let's get you up and running, and then we'll post more later about how this works and how to modify it. Or maybe someone will beat me to that.

Big Fat Warning. This sample does not do security. You'd be a fool to think it does, so don't think that.

OK, onward. First, a simple duplex contract.

  [ServiceContract(
    Name = "SampleDuplexHello",
    Namespace = "https://microsoft.wcf.documentation",
    CallbackContract = typeof(IHelloCallbackContract),
    SessionMode = SessionMode.NotAllowed
  )]
  public interface IDuplexHello
  {
    [OperationContract(IsOneWay = true)]
    void Hello(string greeting);
  }

  public interface IHelloCallbackContract
  {
    [OperationContract(IsOneWay = true)]
    void Reply(string responseToGreeting);
  }

Note that the ServiceContractAttribute.SessionMode property is set to SessionMode.NotAllowed. This means that if we make a mistake and supply a sessionful binding, the application throws an exception. Here is the service implementation.

  public class DuplexHello : IDuplexHello, IDisposable
  {
    public DuplexHello()
    {
      Console.WriteLine("Service object created: " + this.GetHashCode().ToString());
    }

    public void Hello(string greeting)
    {
      Console.WriteLine("Caller sent: " + greeting);
      Console.WriteLine("Session ID: " + OperationContext.Current.SessionId);
      Console.WriteLine("Waiting two seconds before returning call.");
      // Put a slight delay to demonstrate asynchronous behavior on client.
      Thread.Sleep(2000);
     
      // Get the callback client object.
      IHelloCallbackContract caller
        = OperationContext.Current.GetCallbackChannel<IHelloCallbackContract>();

      // Fetch out the client ReplyTo and place it in the outbound To header to
      // direct the callback message.
      Uri to = new Uri(OperationContext.Current.IncomingMessageHeaders.ReplyTo.ToString());
      OperationContext.Current.OutgoingMessageHeaders.To = to;
     
      string response = "Service object " + this.GetHashCode().ToString() + " received: " + greeting;
      Console.WriteLine("Sending back: " + response);
      caller.Reply(response);
    }

    #region IDisposable Members

    public void Dispose()
    {
      Console.WriteLine("Service object destroyed: " + this.GetHashCode().ToString());
    }

    #endregion
  }

If you've built a duplex application before, the only interesting code here is the section in which we find the incoming ReplyTo header and assign that value to the outgoing To header.

      // Fetch out the client ReplyTo and place it in the outbound To header to
      // direct the callback message.
      Uri to = new Uri(OperationContext.Current.IncomingMessageHeaders.ReplyTo.ToString());
      OperationContext.Current.OutgoingMessageHeaders.To = to; 

With the default sessionful bindings, you don't have to take this step. But without those sessions to help out, you do. If this application structure is important to you, you'd probably handle this wiring procedure in a combination extensible object and IDispatchMessageInspector. Here we simply do this in code.

 Now the tricky part. I'll just tell you how to build the binding and get back to discussing this later. We're going to layer a CompositeDuplexBindingElement over a OneWayBindingElement on top of a HttpTransportBindingElement. Oh yeah, we need an encoder, too. OK, let's use the default TextMessageEncodingBindingElement for that. I find config easier for this than code, so we'll assemble this binding using the CustomBinding. It looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service
        name="Microsoft.WCF.Documentation.DuplexHello"
        behaviorConfiguration="mex">
        <host>
          <baseAddresses>
            <add baseAddress="https://localhost:8080/DuplexHello"/>
          </baseAddresses>
        </host>
        <endpoint
          address=""
          binding="customBinding"
          bindingConfiguration="duplexNoSession"
          contract="Microsoft.WCF.Documentation.IDuplexHello"
         />
        <endpoint
          address="mex"
          binding="mexHttpBinding"
          contract="IMetadataExchange"
        />
      </service>
    </services>
    <bindings>
      <customBinding>
        <binding name="duplexNoSession">
          <compositeDuplex />
          <oneWay/>
          <textMessageEncoding />
          <httpTransport />
        </binding>
      </customBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="mex" >
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Note that here I enable metadata using the ServiceMetadataBehavior. Downloading and creating the client remains pretty simple. Now, create a host:

      // Create a ServiceHost for the service type and use the base address from configuration.
      using (ServiceHost serviceHost = new ServiceHost(typeof(DuplexHello)))
      {
        try
        {
          // Open the ServiceHostBase to create listeners and start listening for messages.
          serviceHost.Open();

and you're running. Let's get to the client, and then I'll sign off for now. Use Svcutil.exe to generate client code. Once you do that, the trick to the client is that you must create a new OperationContextScope to set the outbound ReplyTo header. Then, pass the local address of the underlying channel to the ReplyTo header and invoke the client.

      // Picks up configuration from the config file.
      InstanceContext callbackInstanceContext = new InstanceContext(this);
      SampleDuplexHelloClient wcfClient = new SampleDuplexHelloClient(callbackInstanceContext);
      try
      {
        using (OperationContextScope opScope = new OperationContextScope(wcfClient.InnerDuplexChannel))
        {
          // Add explicit replyto for the other side to pick up.
          OperationContext.Current.OutgoingMessageHeaders.ReplyTo
            = wcfClient.InnerChannel.LocalAddress;
 

In duplex clients you need to hold the application domain open to listen for callbacks. Without a session to fault, if a client vanishes the service will not know about it until a callback fails. The upside, however, is that the channel is not like to fault. Typically, if a callback fails, your service can simply ignore that result or retry the call on the same channel later. This is a handy way of getting services to support eventish behavior for client applications without worrying unduly about the channel lifetime and infrastructure. The complete client application follows. I learned most of this to write documentation for duplex applications but I was guided mainly by Dr. Nick and Kenny Wolf, who have written in their blogs about pieces and helped me understand how they go together to make this work. If I have to repost to clarify or modify what I've said, it's not their fault. :-) More soon.

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;

namespace Microsoft.WCF.Documentation
{
  public class Client : SampleDuplexHelloCallback
  {
    AutoResetEvent waitHandle;

    public Client()
    {
      waitHandle = new AutoResetEvent(false);
    }

    public void Run()
    {
      // Picks up configuration from the config file.
      InstanceContext callbackInstanceContext = new InstanceContext(this);
      SampleDuplexHelloClient wcfClient = new SampleDuplexHelloClient(callbackInstanceContext);
      try
      {
        using (OperationContextScope opScope = new OperationContextScope(wcfClient.InnerDuplexChannel))
        {
          // Add explicit replyto for the other side to pick up.
          OperationContext.Current.OutgoingMessageHeaders.ReplyTo
            = wcfClient.InnerChannel.LocalAddress;

          Console.ForegroundColor = ConsoleColor.White;
          Console.WriteLine("Enter a greeting to send and press ENTER: ");
          Console.Write(">>> ");
          Console.ForegroundColor = ConsoleColor.Green;
          string greeting = Console.ReadLine();
          Console.ForegroundColor = ConsoleColor.White;
          Console.WriteLine("Called service with: \r\n\t" + greeting);
          wcfClient.Hello(greeting);
          Console.WriteLine("Execution passes service call and the client waits.");
          this.waitHandle.WaitOne();
          Console.ForegroundColor = ConsoleColor.Red;
          Console.WriteLine("Set was called.");
        }
      }
      catch (TimeoutException timeProblem)
      {
        Console.WriteLine("The service operation timed out. " + timeProblem.Message);
        // Sessionful channels should abort the channel unless it is a
        // specified fault. This duplex sample uses a datagram channel, which should be
        // reusable.
        wcfClient.Close();
      }
      catch (CommunicationException commProblem)
      {
        Console.WriteLine("There was a communication problem. " + commProblem.Message);
        // Sessionful channels should abort the channel unless it is a
        // specified fault. This duplex sample uses a datagram channel, which should be
        // reusable.
        wcfClient.Close();
      }
      finally
      {
        Console.ResetColor();
        Console.Write("Press ");
        Console.ForegroundColor = ConsoleColor.Red;
        Console.Write("ENTER");
        Console.ResetColor();
        Console.Write(" to exit...");
        Console.ReadLine();
      }
    }
   
    public static void Main()
    {
      Client client = new Client();
      client.Run();
    }

    public void Reply(string response)
    {
      Console.WriteLine("Received output.");
      Console.WriteLine("\r\n\t" + response);
      this.waitHandle.Set();
    }
  }
}

Comments

  • Anonymous
    October 19, 2006
    Two posts ago I wrote the following post about how to build a duplex service and client that does NOT

  • Anonymous
    May 15, 2007
    Desde el punto de vista de la escalabilidad, en teoria, es mejor crear y destruir el proxy, si es que

  • Anonymous
    December 28, 2009
    I don't think it is possible for this to work with any binding using a client that is behind NAT or a firewall unless they are reconfigured. If this is not true I am missing something and would love some clarification.

  • Anonymous
    December 29, 2009
    Hi Mushroom Hunter! No, you're right -- as is, a client that is expecting inbound messages -- that is, messages that will arrive on a different network connection but which are correlated with a specific client proxy instance by WCF so as to appear like an event or notification -- will not receive them if they are behind a firewall and/or a NAT router. Tough problem, yes? NO! At least, not now that you have the AppFabric ServiceBus, which solves EXACTLY this problem, among others. I'll post an example of how to use Service Bus with WCF soon, but the SDK has several examples. Go here: http://msdn.microsoft.com/en-us/library/ee732537.aspx. The short version is this: The Service Bus (and the Access Control Service, which it trusts as a claims provider by default) enable you to build a WCF service AND WCF client (in fact, you can use any client that supports a REST stack) that can communicate without requiring anyone to open any inbound ports or alter NAT router tables. It's an amazingly useful technology. Go have a look.