Seeking WCF Duplex "TwoWay" Subscribe+Callback Example
Asked Answered
V

4

18

Renewing the bounty AGAIN because I really need to know how to get this to work, or a definitive answer about why it won't.

I've added an alternative explanation of the problem here.

Having a hell of a time getting a two-way (IsOneWay = false) WCF client-server to work in .Net 3/3.5.

After the client successfully enrolls with the service, the service's periodic Announcement() calls-back to the enrolled clients. It is now that either the client or the server hangs until the server's SendTimeout, adjusted to 2 seconds, elapses. Then the server side has a timeout exception as follows. Only then does the client user code immediately RECEIVE THE METHOD CALL and attempt to return a value. By then the client's socket is aborted and the WCF stuff fails.

It seems to me that something on the client is hanging it's local WCF queue from processing until the socket times out, but not quite early enough to cancel the local method call. But if the exception below is to be believed, the server is attempting to send an operation to http://schemas.microsoft.com/2005/12/ServiceModel/Addressing/Anonymous (inappropriate!) and is timing out. Maybe that URI is just the "Name" of the remotely connected client as WCF knows to refer to it for the purposes of the error message and it just appears to mean that it's failing to load a URI. I can't tell if the server fails first or the client fails first.

I've tried adding WCF tracing but I'm not getting much more information.

Similar sample code is here, but it must have been too much to digest. I've experimented with varations of that code.

TimeoutException 'This request operation sent to http://schemas.microsoft.com/2005/12/ServiceModel/Addressing/Anonymous did not receive a reply within the configured timeout (00:00:00).  The time allotted to this operation may have been a portion of a longer timeout.  This may be because the service is still processing the operation or because the service was unable to send a reply message.  Please consider increasing the operation timeout (by casting the channel/proxy to IContextChannel and setting the OperationTimeout property) and ensure that the service is able to connect to the client.'

Server stack trace: 
   at System.ServiceModel.Dispatcher.DuplexChannelBinder.SyncDuplexRequest.WaitForReply(TimeSpan timeout)
   at System.ServiceModel.Dispatcher.DuplexChannelBinder.Request(Message message, TimeSpan timeout)
   at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
   at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
   at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)
Vitalis answered 12/8, 2010 at 22:59 Comment(8)
This is for 3.0/3.5, not 4.0. Thanks.Vitalis
have you used the wcf traceing on both the client and the server to see if it shows up anything?Oxus
Yes, as best I can interpret the results, I only find the same exception as listed above. All orderings and messages appear correct otherwise.Vitalis
I just thought you may be hitting the WCF connection limit, (I think it defaults to 10), then after 2 minutes WCF may time out and allow another connection to be made.Oxus
No, I'm not hitting the limit. It fails the first time.Vitalis
I think (without reading to much of your post) you've missunderstood the meaning of IsOneWay. IsOneWay shouldn't be false to allow two-way communication, IsOneWay=true means that when you call the method wcf starts sending the data and then return out of the function. It dosn't care what happens at the server at all. This means that you won't get timeout, but also that the method needs to be of type void. Also, I have a working example of two-way wcf at home which I can post later today with some explanation.Europium
Nope, I don't think what you thought I thunk. I actually want a response from the connected client's code, a blocking operation on the server to ask the connected client a question. I'd love to see your example of the two-way callbacks! Thanks.Vitalis
@Europium - can you post that code example?Vitalis
O
27

Firstly get yourself a copy of Programming WCF Services, if you don't already have one.

If the client is WinForm or WPF, you need to use [CallbackBehavior(UseSynchronizationContext = false)] as otherwise the client will not process the incoming message until the UI thread enters the message loop.

Firstly a “Duplex” channel in WCF is not truly Duplex! A message from

  • Client to server
  • Can block a message the server is waiting for from the client
  • (or the other way round)

As messages are only dispatched in order on a single WCF channel. A Duplex WCF channel does NOT give you two incoming message queues. The results coming back from a “TwoWay” call are just the same as the “call” as this level of the WCF stack. Once you get your head round this a lot of the problems become clearer to understand.

If the client is WinForm or WPF, you may need to use [CallbackBehavior(UseSynchronizationContext = false)] as otherwise the client will not process the incoming message until the UI thread enters the message loop.

Some rules I found to help avoid deadlocks. (Look at my WCF questions to see the pain I had!)

The sever must never call out to a client on the same connection as a call from the same client is in process on.

And/or

The client must never call back to the server on the same connection as is used for the “callbacks” while processing a call-back.

The next time I think I will just use two contracts (and hence TCP connections) one for the callback and other for all client->server requests. Or use my own polling system, as this gave me so much pain.

Sorry I don’t have time today to write an example. Anyway most examples work for what the example is trying to do, but break down in real life for some reason to do with your application.

The best web site I know for WCF examples is Juval Lowy’s web site.

Your may also find the questions I asked about WCF on Stack Overflow useful, as I was having the same kind of problems as you.

Also spending a day or two reading all the WCF questions and answers on Stack Overflow will give a good idea of the problems to avoid.

Oxus answered 19/8, 2010 at 9:21 Comment(3)
Hi Ian, thanks for all these tips. The related SO question's code has the UseSynchronizationContext = false and it does not callback within the context of processing a call - the client enrolls in the subscriptions, then later, the server asks each client subscriber a simple question every Timer.Tick. I'll keep looking at your suggested links too.Vitalis
The correct term for "not truly duplex" is "half-duplex".Opportunist
Thank you so much for a detailed explanation!! :)Suctorial
C
4

Assuming the client is a WinForms application, you should make the handling of the callback independent from the rest of the application by using Ian's suggestion plus delegating the work to be done on the UI thread if needed. For example, if the server wants to notify the client of something, such as change the text of a label, you could do the following:

[CallbackBehavior(UseSynchronizationContext = false)]
internal class ServiceCallback : IServiceCallback
{
    ChangeMainFormLabel(string text)
    {
        frmMain.Instance.BeginInvoke(new Action()(() => frmMain.Instance.lblSomething.Text = text));
    }
}

(Instance is a static property that returns the single instance of frmMain and lblSomething is some Label that the server would like to change.) This method will return immediately and free the server from waiting for the client's UI, and the UI will be updated as soon as it's free to do so. And best of all, no deadlocks since no one is waiting for anyone.

Conjunctiva answered 22/8, 2010 at 9:34 Comment(3)
It is a Console app. I've tried running the main loop on a worker thread, but it didn't seem to change anything. Does all that fit with your ideas?Vitalis
@uosɐſ: If it's a console app, all you have to do is use UseSynchronizationContext = false. Just remember that the methods on the callback object will be executed on a different thread, so synchronize any access to shared objects. If you post a code sample, it might help us pinpoint the problem you're having.Conjunctiva
Here is the code sample that has the synccontext=false as you suggest. (I have it in the original post but I understand that it's hard to identity)Vitalis
E
0

Sorry, I totally forgot the example (:-$).

Here is my code for the server:
ISpotifyServer.cs

[ServiceContract(CallbackContract = typeof(ISpotifyCallback))]
public interface ISpotifyService
{
    [OperationContract(IsOneWay = true)]
    void Login(string username, string password);
}

ISpotifyCallback.cs

[ServiceContract]
public interface ISpotifyCallback
{
    [OperationContract(IsOneWay = true)]
    void OnLoginComplete();

    [OperationContract(IsOneWay = true)]
    void OnLoginError();
}

Program.cs

class Program
{
    static void Main(string[] args)
    {

        using (ServiceHost host = new ServiceHost(typeof(SpotifyService)))
        {
            host.Open();

            Console.WriteLine("Service running.");
            Console.WriteLine("Endpoints:");

            foreach (ServiceEndpoint se in host.Description.Endpoints)
                Console.WriteLine(se.Address.ToString());


            Console.ReadLine();

            host.Close();
        }
    }
}

AppData.xml

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MetadataEnabledBehavior">
          <serviceMetadata />
          <serviceDebug includeExceptionDetailInFaults="True"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service behaviorConfiguration="MetadataEnabledBehavior" name="SpotiServer.SpotifyService">
        <host>
          <baseAddresses>
            <add baseAddress="net.tcp://localhost:9821" />
          </baseAddresses>
        </host>
        <clear />
        <endpoint address="spotiserver" binding="netTcpBinding"
            name="TcpEndpoint" contract="SpotiServer.ISpotifyService"
            listenUriMode="Explicit">
          <identity>
            <dns value="localhost"/>
            <certificateReference storeName="My" storeLocation="LocalMachine"
                x509FindType="FindBySubjectDistinguishedName" />
          </identity>
        </endpoint>
        <endpoint address="mex" binding="mexTcpBinding" contract="IMetadataExchange" />
      </service>
    </services>
  </system.serviceModel>
</configuration>

And for the client:
Program.cs

class Program
{
    static void Main(string[] args)
    {
        InstanceContext context = new InstanceContext(new CallbackHandler());

        String username;
        String password;

        Console.Write("Username: ");
        username = Console.ReadLine();

        Console.WriteLine("Password: ");
        password = ReadPassword();

        SpotiService.SpotifyServiceClient client = new SpotiService.SpotifyServiceClient(context);
        client.Login(username, password);

        Console.ReadLine();
    }

    private static string ReadPassword()
    {
        Stack<string> passbits = new Stack<string>();
        //keep reading
        for (ConsoleKeyInfo cki = Console.ReadKey(true); cki.Key != ConsoleKey.Enter; cki = Console.ReadKey(true))
        {
            if (cki.Key == ConsoleKey.Backspace)
            {
                //rollback the cursor and write a space so it looks backspaced to the user
                Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
                Console.Write(" ");
                Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
                passbits.Pop();
            }
            else
            {
                Console.Write("*");
                passbits.Push(cki.KeyChar.ToString());
            }
        }
        string[] pass = passbits.ToArray();
        Array.Reverse(pass);
        return string.Join(string.Empty, pass);
    }
}

I think that's about all. I of cause also have an implementation of the interfaces, that (on the client-side) prints the result to the console, and on the serverside runs "OnLoginComplete" if user-name and password is correct, otherwise runs "OnLoginError". Let me know if it doesn't work or if you need help setting it all up.

Europium answered 14/9, 2010 at 23:41 Comment(3)
Hi Alxandr. I appreciate the time you've take to post this, but the IsOneWay=true is the problem. I've been successful with such IsOneWay=true examples. It's the IsOneWay=false that is the challenge and the aim of this question.Vitalis
Are you using sockets or http?Europium
And it dosn't work just removing the IsOneWay on the Login-rutine?Europium

© 2022 - 2024 — McMap. All rights reserved.