his 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 theWCFClient
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.).
Collapse
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:
Collapse
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:
Collapse
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. Collapse
//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:
Collapse
// [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. Collapse
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:
Collapse
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:
Collapse
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
No comments:
Post a Comment