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
{
[ServiceContract]
interface IMessageCallback
{
[OperationContract(IsOneWay = true)]
void OnMessageAdded(string message, DateTime timestamp);
}
[ServiceContract(CallbackContract = typeof(IMessageCallback))]
public interface IMessage
{
[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()
{
DuplexChannelFactory<IMessage> pipeFactory =
new DuplexChannelFactory<IMessage>(
new InstanceContext(this),
new NetTcpBinding(),
new EndpointAddress("net.tcp://localhost:8000/ISubscribe"));
try
{
pipeProxy = pipeFactory.CreateChannel();
pipeProxy.Subscribe();
return true;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return false;
}
}
public void Close()
{
pipeProxy.Unsubscribe();
}
public string SendMessage(string message)
{
try
{
pipeProxy.AddMessage(message);
return "sent >>>> " + message;
}
catch(Exception e)
{
return e.Message;
}
}
public void OnMessageAdded(string message, DateTime timestamp)
{
Console.WriteLine(message + ": " + timestamp.ToString("hh:mm:ss"));
}
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.
[ServiceContract]
interface IMessageCallback
{
[OperationContract(IsOneWay = true)]
void OnMessageAdded(string message, DateTime timestamp);
}
[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:
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()
{
using (ServiceHost host = new ServiceHost(
typeof(RCRServer),
new Uri("net.tcp://localhost:8000")))
{
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
{
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
{
IMessageCallback callback =
OperationContext.Current.GetCallbackChannel<IMessageCallback>();
if (subscribers.Contains(callback))
subscribers.Remove(callback);
return true;
}
catch
{
return false;
}
}
public void AddMessage(String message)
{
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