Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# WCF Client/Server without HTTP with Callbacks Made Easy

0.00/5 (No votes)
19 Aug 2009 4  
A simple way to do callbacks that works in C# in programs

Introduction

This is an MCF server/client that allows a client to send something to the server which in turn, sends it to every other client that is connected. They are simply 2 console apps in one solution (written in VS 2005). I developed 2 stand-alone classes that each application can create. I have tested them in Form applications and across different computers and all work, but for ease, I am only including the console apps both connecting to localhost on port 8000. If you have problems connecting, then it is a firewall or network issue.

Background

I'm a hardcore C++ programmer and I have written numerous programs in DCOM. However, my company is jumping on the C# bandwagon and I have to go along. There are ways to write COM apps in C# but somebody mentioned WCF (Windows Communication Foundation) to me and I thought I'd check it out.

The learning curve is steeper than COM. I spent a week looking at numerous articles that claimed to do this but they either didn't compile, were weak in comments or just plain didn't tell you if the code was client or server code.

I'm posting this here to give everybody a clear and concise program with comments that actually compiles and WORKS!

Using the Code

Open the WCFServer solution. It contains both the client and server applications. Right click on the workspace and go to "set startup projects". Make sure that the server app is the top one and both are set to "Start". Hit F5 and it will create a single client and a single server. Then, right click on the WCFClient project and go to "Debug" and click on "start a new instance". Start as many instances of the client as you want (I have tested with 10). Type something in one client and it will be broadcasted to all of the other clients. Make sure that the reference to "System.ServiceModel" is included. If not, add it in references to both projects.

Let's get started. First look at the client code in sections (The entire class is included in the code.).

namespace WCFClient
{
    //These are the interface declarations for the client
    [ServiceContract]
    interface IMessageCallback
    {
        //This is the callback interface declaration for the client
        [OperationContract(IsOneWay = true)]
        void OnMessageAdded(string message, DateTime timestamp);
    }
    [ServiceContract(CallbackContract = typeof(IMessageCallback))]
    public interface IMessage
    {
        //these are the interface declarations for the server.
        [OperationContract]
        void AddMessage(string message);
        [OperationContract]
        bool Subscribe();
        [OperationContract]
        bool Unsubscribe();
    }

Now at first, this didn't seem very intuitive to me (I actually worked on getting this right for several hours). Why is the actual callback function Service contract not declared as a callback and the server's ServiceContract (which aren't callbacks) declared as callbacks? I'm no MCF expert, but I think it's because Bill Gate's gang is a bunch of.....freaking geniuses? (or something else). Anyhow, that is how it has to be declared. The "isOneWay" parameter on the actual callback function keeps each client from reporting back with each callback. That can lead to locking up everything.

Now for creating the class to connect to the server:

class RCRProxy : IMessageCallback, IDisposable
    {
        IMessage pipeProxy = null;
        public bool Connect()
        {
            /*note the "DuplexChannelFactory".  This is necessary for Callbacks.
             A regular "ChannelFactory" won't work with callbacks.*/
            DuplexChannelFactory<IMessage> pipeFactory =
                  new DuplexChannelFactory<IMessage>(
                      new InstanceContext(this),
                      new NetTcpBinding(),
                      new EndpointAddress("net.tcp://localhost:8000/ISubscribe"));
            try
            {
                //Open the channel to the server
                pipeProxy = pipeFactory.CreateChannel();
                //Now tell the server who is connecting
                pipeProxy.Subscribe();
                return true;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                return false;
            }
        }
        public void Close()
        {
            pipeProxy.Unsubscribe();
        }
        //This function sends a string to the server so that it can broadcast
        // it to all other clients that have called Subscribe().
        public string SendMessage(string message)
        {
            try
            {
                pipeProxy.AddMessage(message);
                return "sent >>>>  " + message;
            }
            catch(Exception e)
            {
                return e.Message;
            }
        }
        //This is the function that the SERVER will call
        public void OnMessageAdded(string message, DateTime timestamp)
        {
            Console.WriteLine(message + ": " + timestamp.ToString("hh:mm:ss"));
        }
        //We need to tell the server that we are leaving
        public void Dispose()
        {
            pipeProxy.Unsubscribe();
        }
    }

First off, notice that it is derived from the IMessageCallbacack interface. Notice the "DuplexChannelFactory"? I tried everything to get a regular old "ChannelFactory" to work with to no avail, even though numerous callback examples showed them in their code. But again, these all seemed to be partial code solutions where this isn't.

That's it for the client class. You can put it into any program, create it and tell it to connect. For example:

namespace WCFClient
{    
    class Program
    {
        static void Main(string[] args)
        {
            RCRProxy rp = new RCRProxy();
            if (rp.Connect() == true)
            {
                string tmp = Console.ReadLine();
                while (tmp != "EXIT")
                {
                    rp.SendMessage(tmp);
                    tmp = Console.ReadLine();
                }
            }
            if(((ICommunicationObject)rp).State == CommunicationState.Opened)
                rp.Close();            
        }
    }
}

Now let's look at the SERVER class code in a couple of parts. Again, the entire class is in the downloadable project.

    //interface declarations just like the client but the callback 
    //declaration is a little different
    [ServiceContract]
    interface IMessageCallback
    {
        [OperationContract(IsOneWay = true)]
        void OnMessageAdded(string message, DateTime timestamp);
    }
    //This is a little different than the client 
    // in that we need to state the SessionMode as required or 
    // it will default to "notAllowed"
    [ServiceContract(CallbackContract = 
	typeof(IMessageCallback),SessionMode = SessionMode.Required)]
    public interface IMessage
    {
        [OperationContract]
        void AddMessage(string message);
        [OperationContract]
        bool Subscribe();
        [OperationContract]
        bool Unsubscribe();
    }
     [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]

Here the ServiceContract for the server functions have to set the SessionMode to "Required" or it will default to "NotAllowed" and the callback session will be denied.

In the line of code:

// [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]

I set the InstanceContextMode to "PerCall" so that I can capture the session information with each call. If you are transferring numerous small packets very often, you might want to change it to "PerSession". Here is the rest of the code for the SERVER class.

class RCRServer : IMessage
    {
        private static  List<IMessageCallback> subscribers = new List<IMessageCallback>();
        public ServiceHost host = null;
    
        public void Connect()
        {
            //I'm doing this next part programmatically instead of in app.cfg 
            // because I think it makes it easier to understand (and XML is stupid)
            using (ServiceHost host = new ServiceHost(
                typeof(RCRServer),
                new Uri("net.tcp://localhost:8000")))
            {
                //notice the NetTcpBinding?  This allows programs instead of web stuff
                // to communicate with each other
                host.AddServiceEndpoint(typeof(IMessage),
                  new NetTcpBinding(),
                  "ISubscribe");                
                
                try
                {
                    host.Open();
                    Console.WriteLine("Successfully opened port 8000.");
                    Console.ReadLine();
                    host.Close();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                }             
            }
        }
        
        public bool Subscribe()
        {
            try
            {
                //Get the hashCode of the connecting app and store it as a connection
                IMessageCallback callback = 
		OperationContext.Current.GetCallbackChannel<IMessageCallback>();
                if (!subscribers.Contains(callback))
                    subscribers.Add(callback);
                return true;
            }
            catch(Exception e)
            {
                Console.WriteLine(e.Message);
                return false;
            }
        }
        public bool Unsubscribe()
        {
            try
            {
                //remove any connection that is leaving
                IMessageCallback callback = 
		OperationContext.Current.GetCallbackChannel<IMessageCallback>();
                if (subscribers.Contains(callback))
                    subscribers.Remove(callback);
                return true;
            }
            catch
            {
                return false;
            }
        }
        public void AddMessage(String message)
        {
            //Go through the list of connections and call their callback function
            subscribers.ForEach(delegate(IMessageCallback callback)
            {
                if (((ICommunicationObject)callback).State == CommunicationState.Opened)
                {
                    Console.WriteLine("Calling OnMessageAdded on callback 
				({0}).", callback.GetHashCode());
                    callback.OnMessageAdded(message, DateTime.Now);
                }
                else
                {
                    subscribers.Remove(callback);
                }
            });
        }
    }

This class is derived from the IMessage interface. Be sure to keep the two straight or nothing will work. This class should probably be put into an actual "Service" that runs all of the time allowing the clients to connect. I think that all of the comments in the code for the server explain the class.

Now for creating the server class example:

namespace WCFService
{
    static class Program
    {        
        static void Main()
        {
            RCRServer server = new RCRServer();
            server.Connect();
        }
    }
}

That's all there is to it. Of course, the includes for both classes are:

using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;

Cool Stuff

Although the learning curve on MCF is a little steep, if I try to remember back 12 or 13 years to when I first learned COM, I think the learning curve is about the same. However, I couldn't have created these two programs in COM with only a few lines of code as I did here.

Conclusion

What do I think of MCF? Well, I wrote another single instance console app that fired off 10,000 messages to 10 other clients and I found it to be kind of slow. It did work, but a little slow. For that reason, I think I will stay with my raw socket classes. There is a lot more code to them but they are very fast and lots of code is job security.

I hope this helps the rest of you that are trying to figure out callbacks in MCF.

History

  • 20th August, 2009: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here