Seeking WCF Duplex “TwoWay” Subscribe+Callback Example

拈花ヽ惹草 提交于 2019-11-29 21:48:52
Ian Ringrose

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.

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.

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!