Introduction
In Part 1, we defined what a service is, and also created a simple WCF service and client to demonstrate the process of creating and consuming services under WCF. In this article, we'll start exploring some of the more advanced communication options available under WCF. All the source code for the examples presented are available in the download for Part 1.
An All-In-One Service
Let's continue by implementing an example where the service and the client both exist in the same process! Why would anyone want to do this? Well, I’ve needed it, for one. Suppose you have a module that contains several services. The services are actually DB servers that abstract and isolate access to the database. Well, one of the services might actually need the functionality that is provided by another service (in the process). Since they are all in the same housing, you could instantiate the required service (class) locally. But then, you would be accessing the service differently than other clients, and you would also be bypassing the isolation and other facilities that the ServiceHost
provides. In any case, this is just another way of doing things when the requirements are right. In a subsequent section, we’ll actually be doing the opposite, where we are creating a singleton and want only one instance of the service class. The following source shows the changes necessary to modify the LocalTimeService example.
Collapse
namespace AllInOneTimeService
{
class Client
{
public bool keepClocking = true;
LocalTimeProxy proxy = null;
public Client()
{
proxy = new LocalTimeProxy();
}
public void ClockingThread()
{
while (keepClocking)
{
Console.WriteLine(proxy.GetLocalTime());
Thread.Sleep(1000);
}
proxy.Close();
}
static void Main(string[] args)
{
Uri baseAddress = new
Uri(ConfigurationManager.AppSettings["basePipeTimeService"]);
ServiceHost serviceHost = new
ServiceHost(typeof(LocalTimeService), baseAddress);
serviceHost.Open();
Console.WriteLine("Service is running...." +
"press any key to terminate.");
Client client = new Client();
Thread thread = new Thread(new
ThreadStart(client.ClockingThread));
thread.Start();
Console.ReadKey();
client.keepClocking = false;
Thread.Sleep(2000);
serviceHost.Close();
}
}
}
After the service is started, we simply instantiate a client. Of course, this is just to demonstrate that the service can be accessed by internal and external sources. Normally, it would be one service accessing another service as a result of a client request. The only other change of note is that the config file needs to define both the service and the client endpoints. Go ahead and start this version of the service. Then, start several instances of the standalone clients. As you can see, the service can be accessed from different sources (internal and external), and they are all serviced by the same host.
Two Spigots for the Price of One
The LocalTimeService examples that we've been using only supports IPC (internal clients using named pipes). Let’s change that, and add support for TCP clients. The cool thing about this is that we won’t have to change much, it will mostly be only config file changes. The code below is the only change we need to make to the service code. We’re using the AllInOne version of the service so that you can see how we can support both types of transport at the same time. The internal client will use named pipes, while the external client will use TCP.
static void Main(string[] args)
{
Uri baseAddress = new Uri(
ConfigurationManager.AppSettings["baseTcpTimeService"]);
ServiceHost serviceHost = new ServiceHost(
typeof(LocalTimeService), baseAddress);
baseAddress = new Uri(
ConfigurationManager.AppSettings["basePipeTimeService"]);
serviceHost.AddServiceEndpoint(typeof(ILocalTime),
new NetNamedPipeBinding(), baseAddress);
serviceHost.Open();
...
}
First, we instantiate ServiceHost
, and pass the name of the endpoint that we want it to support (TCP). Then, we programmatically add a second endpoint (named pipe). All the required information is in the config file, as shown below:
Collapse
<configuration>
<appSettings>
<add key="baseTcpTimeService"
value="net.tcp://localhost:9000/LocalTimeService" />
<add key="basePipeTimeService"
value="net.pipe://localhost/LocalTimeService" />
</appSettings>
<system.serviceModel>
<services>
<service name="LocalTimeService">
<endpoint
address=""
binding="netTcpBinding"
contract="ILocalTime"
/>
</service>
</services>
<client>
<endpoint name ="LocalTimeService"
address="net.pipe://localhost/LocalTimeService"
binding="netNamedPipeBinding"
contract="ILocalTime" />
</client>
</system.serviceModel>
</configuration>
The only change that’s required on the standalone client is in the config file, as shown next:
<configuration>
<system.serviceModel>
<client>
<endpoint name ="LocalTimeService"
address="net.tcp://localhost:9000/LocalTimeService"
binding="netTcpBinding"
contract="ILocalTime" />
</client>
</system.serviceModel>
</configuration>
As we did before, go ahead and start the service, and then start several stand-alone clients. No difference, except that the external clients are using TCP to communicate with the service, while the internal clients use named pipes. Isn't that cool?! But wait, there’s more.
The Lazy Client
Let’s now consider a different service. This is a service that monitors the temperature in a reactor. It is very important that changes in the reactor temperature be detected as quickly as possible. So, each client needs to check with the service at very short intervals of time. And what would that be? Once a second, ten times a second, a hundred times a second? Clearly, if the number of clients is large, there would be a lot of wasted traffic if the temperature is not changing frequently. Alternatively, suppose the client decides to check the temperature every 100 milliseconds, there is still the possibility that the temperature may have changed a few times (up and down) in that span of time. So, if the client needed to know each change of temperature, then it would have missed a few.
So a better (or ‘more gooder’, as a friend of mine prefers) solution would be to implement a Publish/Subscribe pattern. The service (sensor) would simply monitor the temperature, and if the change happens to be above a specified threshold, it would send out a message to any client that had requested to be notified. The clients would just have to subscribe to the service and then wait for any notifications. When the notifications come, each client would do whatever it needed to do according to its role: log, alarm, control, etc. Here’s the code for a temperature sensor service:
Collapse
[ServiceContract(Session = true,
CallbackContract = typeof(ITempChangedHandler))]
public interface ITempChangedPub
{
[OperationContract(IsInitiating = true)]
void Subscribe();
[OperationContract(IsTerminating = true)]
void Unsubscribe();
}
public interface ITempChangedHandler
{
[OperationContract(IsOneWay = true)]
void TempChanged(int newTemp);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class TempSensor : ITempChangedPub
{
ArrayList subscribers = new ArrayList();
TempSimulator sim;
public TempSensor()
{
sim = new TempSimulator(this);
Thread simThread = new Thread(new
ThreadStart(sim.SimThread));
simThread.Start();
}
public void Subscribe()
{
subscribers.Add(
OperationContext.Current.GetCallbackChannel<ITempChangedHandler>());
}
public void Unsubscribe()
{
ITempChangedHandler caller =
OperationContext.Current.GetCallbackChannel<ITempChangedHandler>();
foreach (ITempChangedHandler h in subscribers)
if (h == caller)
{
subscribers.Remove(h);
break;
}
}
public void PublishTempChanged(int temp)
{
foreach (ITempChangedHandler h in subscribers)
h.TempChanged(temp);
}
public void StopSim()
{
sim.runSim = false;
Thread.Sleep(1000);
}
}
A few things to note on the above code. First, you’ll notice that the service class has been decorated with a ServiceBehavior
attribute. There are several options for this attribute, but it essentially specifies how we want to control the ‘instancing behavior’ of the service. In this situation, we specify ‘Single’ since we want only one instance of the service. It has to stick around to be able to receive the subscription messages from the clients. The ServiceBehavior
attribute has two additional parameters specified for it. First, the Session=true
indicates that the service will create and maintain a session for each client. The lifetime of the session is defined by the IsInitiating
/IsTerminating
properties of the OperationContract
attribute. So the session can be kinda controlled by the client. Let’s take another look at that.
Consider the interface defined below that might be used to control AGVs. Automated Guided Vehicles (AGVs) are essentially self guided forklifts. They have a laser system on board with which they can tell where they are within a physical environment. And then you can control them pretty much like you would control a remote control car.
[ServiceContract(Session=true)]
public interface IAGVControl
{
[OperationContract(IsInitiating=true)]
int AcquireVehicle();
[OperationContract(IsTerminating=true)]
void ReleaseVehicle();
[OperationContract]
void MoveTo(int location);
[OperationContract]
void Load();
[OperationContract]
void Unload();
}
A client would first make a call to AcquireVehicle
to establish a session (as well as get a physical AGV to manipulate). Now, the client can perform any number of operations with the vehicle. Possession of the vehicle is essentially controlled through session management! Of course, we know that with great power comes great responsibility. Once the client calls ReleaseVehicle
, there better not be any subsequent requests made.
One last point on the previous TempSensor code. You’ll notice that the OperationContract
attribute for the callback TempChanged
has the IsOneway
property set to true
. This property defines the requirement (or lack thereof) for a reply message. Even if the return value is void
, there will still be a reply message generated if the IsOneway
property is set to false
(the default). Requiring a reply message provides a mechanism to return exceptions thrown by the service back to the client. Also note that if you set IsOneway
to true
and you do have a return value, you’ll get an exception.
Now, here’s the rest of the code for the temperature service:
static void Main(string[] args)
{
TempSensor sensor = new TempSensor();
Uri baseAddress = new Uri(
ConfigurationManager.AppSettings["basePipeTempService"]);
ServiceHost serviceHost = new ServiceHost(sensor, baseAddress);
serviceHost.Open();
Console.WriteLine("Service is running...." +
"press any key to terminate.");
Console.ReadKey();
sensor.StopSim();
serviceHost.Close();
}
The only thing to note on the above is that we create the service class object and then pass a reference to it to ServiceHost
.
So, the temperature service provides an interface that lets clients subscribe and unsubscribe to temperature changes. In this case, the service will send a message to each client that has subscribed, whenever the temperature changes more than 5 degrees. There is nothing special about the subscribe/unsubscribe interface, you could call it whatever you want (in fact, you’ll need a different name if there is more than one thing that clients can subscribe to), and you can pass in parameters. For example, you might allow each client to specify at what granularity or temperature level that it wants to be notified. Of course, the service has to do a little more work, in this case, to keep track of which client wants what.
You’ll also notice that there is a second interface defined. That is the interface that the clients have to implement in order to receive notifications from the service. When the proxy gets created for the client, a ‘callback’ interface will also be included in order for the clients to be able to know what they have to implement. Here’s the proxy class for the service.
Collapse
[ServiceContract(CallbackContract=typeof(
ITempChangedPubCallback), Session=true)]
public interface ITempChangedPub
{
[OperationContract]
void Subscribe();
[OperationContract]
void Unsubscribe();
}
public interface ITempChangedPubCallback
{
[OperationContract]
void TempChanged(int newTemp);
}
public interface ITempChangedPubChannel : ITempChangedPub,
System.ServiceModel.IClientChannel
{
}
public partial class TempChangedPubProxy :
System.ServiceModel.DuplexClientBase<ITempChangedPub>,
ITempChangedPub
{
public TempChangedPubProxy(
System.ServiceModel.InstanceContext callbackInstance) :
base(callbackInstance)
{
}
public void Subscribe()
{
base.InnerProxy.Subscribe();
}
public void Unsubscribe()
{
base.InnerProxy.Unsubscribe();
}
}
The temperature service client, then, is just slightly more complicated than the client for the LocalTimeService
. The TempChanged
client needs to do two things. First, it has to instantiate a class that implements the ITempChangedPubCallback
interface so that the service can send messages back to the client. And the other thing the client needs to do is provide some kind of hosting for the callback, similar to what the ServiceHost
provides for the service. This is how the message can be detected and routed to the right place. The class that provides that functionality is the InstanceContext
class (if you look at the class definition, you'll notice it has a host
member). When we create the InstanceContext
, we pass it the handler class that will service the temperature change messages (has implemented ItempChangedPubCallback
).
class TempChangedHandler : ITempChangedPubCallback
{
public void TempChanged(int temp)
{
Console.WriteLine(temp.ToString());
}
}
static void Main(string[] args)
{
InstanceContext siteTempChangedHandler = null;
TempChangedPubProxy proxyTempChanged;
siteTempChangedHandler = new InstanceContext(new TempChangedHandler());
proxyTempChanged = new TempChangedPubProxy(siteTempChangedHandler);
proxyTempChanged.Subscribe();
Console.WriteLine("Client is running....press any key to terminate.");
Console.ReadKey();
proxyTempChanged.Unsubscribe();
}
That’s the extent of the code, except for the endpoint definition in the config file. Here, we need to indicate both the ‘client’ endpoint (the service we’re talking to) as well as an endpoint definition for the client's callback.
Collapse
<configuration>
<appSettings>
<add key="basePipeTempChangedHandler"
value="net.pipe://localhost/TempSensorHandler" />
</appSettings>
<system.serviceModel>
<services>
<service
name="TempSensorHandler">
<!-- use base address provided by host -->
<endpoint name="pipeEndpoint" address=""
binding="netNamedPipeBinding"
contract="ITempChangedPubCallback" />
</service>
</services>
<client>
<endpoint name ="TempChangedPub"
address="net.pipe://localhost/TempSensor"
binding="netNamedPipeBinding"
contract="ITempChangedPub" />
</client>
</system.serviceModel>
</configuration>
Start the service, and then start an instance of the client to test out the code. I’m curious to see if the temperatures does manage to stay around 100 over time.
Asynchronicity
Sometimes, the functionality to be provided by a service is a long process, and the client cannot afford to wait for the response. Under those circumstances, there are two different approaches to providing asynchronous access to the service. The first one is based on the .NET Asynchronous pattern where the disconnection is provided by the client side infrastructure. The second method uses a Duplex Communication pattern where the client implements a callback interface. Let’s take a look at each approach.
Consider the AGV application described previously. AGVs are very slow vehicles relative to the processing time of the system. They travel at best about 1 foot/second. So, to travel 10 feet, it would take 10 seconds. And there’s speedup, slowdown, and turning delays that also have an effect on the total time. So when a client sends a MoveTo request, it will need to wait a while before it gets back a response that the operation completed.
There is nothing special that needs to be done on the service side for asynchronous clients. All of the functionality is provided on the client side. So we’ll start with a service that implements the IAGVControl
interface described previously. Here's the code for a class we'll call AGVController.
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class AGVControl : IAGVControl
{
public int AcquireVehicle()
{
int vehicleID = 0;
return vehicleID;
}
public void ReleaseVehicle()
{
}
public void MoveTo(int location)
{
Thread.Sleep(5000);
}
public void Load()
{
}
public void Unload()
{
}
}
As you can see, there is (literally) nothing here. Except that the MoveTo
operation takes 5 seconds to complete. So, any method calling MoveTo
won’t return right away. The instancing behavior for the class is specified as PerSession
. That means, ServiceHost
will create an instance of AGVControl
for each client session. And the session lifetime was defined with the interface using the IsInitiating
/IsTerminating
attributes. Compile AGVControl
into a service DLL so that we can generate a proxy using svcutil. We’ll describe the proxy generation when we build the asynchronous client.
As usual, we need to build a host for the service. There is nothing different from the previous examples, so here it is.
static void Main(string[] args)
{
Uri baseAddress =
new Uri(ConfigurationManager.AppSettings["basePipeAGVCtlr"]);
ServiceHost serviceHost =
new ServiceHost(typeof(AGVControl), baseAddress);
serviceHost.Open();
Console.WriteLine("Service is running....press any key to terminate.");
Console.ReadKey();
serviceHost.Close();
}
<configuration>
<appSettings>
<add key="basePipeAGVCtlr" value="net.pipe://localhost/AGVControl" />
</appSettings>
<system.serviceModel>
<services>
<service name="AGVController.AGVControl">
<endpoint
address=""
binding="netNamedPipeBinding"
contract="AGVController.IAGVControl"
/>
</service>
</services>
</system.serviceModel>
</configuration>
Compile and start the service to make sure everything is OK. Now, let’s turn our attention towards the client where all the magic takes place. Once again, the first thing we need is a proxy in order for the client to be able to do anything.
One of the options for svcutil is to generate asynchronous methods to match the Asynchronous pattern. Essentially, this means generating two method signatures for each service method, one to begin the asynchronous operation and one to end. The other item that’s required for the Asynchronous pattern is a callback method that will be called when the asynchronous operation has completed. That’s when we get the results from the service operation.
So, execute svcutil against the AGVControl service DLL that we created above. This will generate the ‘xsd’ and ‘wsdl’ files, as we have seen previously. Then, re-run svcutil, but this time, specify the *.xsd and *.wsdl files that were just created, and also include the ‘/a’ option. Svcutil will generate a proxy, with asynchronous signatures for each method that the service exposes.
Since svcutil is really just a little helper utility, I decided to do some editing on the resulting file. I know that the only method that takes a long time is the MoveTo
method (I know that because I coded the delay). So, I only want to implement that one as an asynchronous operation, all others are to remain as synchronous calls. Here’s the edited version of the proxy:
Collapse
[ServiceContract]
public interface IAGVControl
{
[OperationContract]
int AcquireVehicle();
[OperationContract]
void Load();
[OperationContract(AsyncPattern = true)]
System.IAsyncResult BeginMoveTo(int location,
System.AsyncCallback callback, object asyncState);
void EndMoveTo(System.IAsyncResult result);
[OperationContract]
void ReleaseVehicle();
[OperationContract]
void Unload();
}
public interface IAGVControlChannel :
IAGVControl, System.ServiceModel.IClientChannel
{
}
public partial class AGVControlProxy :
System.ServiceModel.ClientBase<IAGVControl>,
IAGVControl
{
public AGVControlProxy()
{
}
public int AcquireVehicle()
{
return base.InnerProxy.AcquireVehicle();
}
public void Load()
{
base.InnerProxy.Load();
}
public System.IAsyncResult BeginMoveTo(int location,
System.AsyncCallback callback, object asyncState)
{
return base.InnerProxy.BeginMoveTo(location,
callback, asyncState);
}
public void EndMoveTo(System.IAsyncResult result)
{
base.InnerProxy.EndMoveTo(result);
}
public void ReleaseVehicle()
{
base.InnerProxy.ReleaseVehicle();
}
public void Unload()
{
base.InnerProxy.Unload();
}
}
You’ll notice that the MoveTo
operation has been replaced with two methods, BeginMoveTo
and EndMoveTo
. There are also a couple of extra parameters that were added to the BeginMoveTo
method (in addition to the location parameter that was defined for MoveTo
). Those are required in order to support the Asynchronous pattern. The first parameter is a callback delegate where we will specify what method needs to be called when the operation completes. The second parameter is a state parameter, and it’s usual to pass the proxy object so that the end operation can be called.
OkeeDokee, we are ready to build a client that will exercise the AGVControl service. This time, we’ll build a Windows Forms client, to make the UI more conducive to what we want. The figure below shows the sample application that is included in the download. We are using the shell application, simply to demonstrate the asynchronous operation, so there's not much code included.
All that we want to show here is that the MoveTo
operation is indeed disconnected from the actual processing that is taking place at the service. To demonstrate that, the client code creates a separate thread that will update the status bar while the service is doing its thing and we are waiting for a reply (complete source is in the download for Part 1).
Collapse
public partial class Form1 : Form
{
AGVControlProxy proxy = null;
bool gotVehicle = false;
bool gotResponse = false;
...
private void btnMoveTo_Click(object sender, EventArgs e)
{
if (txtPosition.Text.Length > 0)
{
int position = System.Convert.ToInt32(txtPosition.Text);
proxy.BeginMoveTo(position, MoveCallback, proxy);
gotResponse = false;
Thread thread = new Thread(new ThreadStart(WaitingThread));
thread.Start();
}
}
private void MoveCallback(IAsyncResult ar)
{
((AGVControlProxy)ar.AsyncState).EndMoveTo(ar);
gotResponse = true;
}
private void WaitingThread()
{
int waitCount = 1;
while (gotResponse == false)
{
toolStripStatusLabel1.Text =
"Waiting..." + waitCount.ToString();
waitCount++;
Thread.Sleep(100);
}
toolStripStatusLabel1.Text = "Got response";
}
...
}
Compile the client, and make sure you’ve got the service app running. Depress the Acquire button, and then the MoveTo button. You’ll see the status bar being updated until a response is received from the service. The callback method is what controls the termination of the thread so you also know when that occurred.
That's it for now. In Part 3 (the last one!), we'll complete the asynchronous examples by implementing the same functionality as above but utilizing a Duplex communication pattern. We will also look at the fourth transport type, message-queueing, and where that may be used.
Al Alberto
|
Click here to view Al Alberto's online profile.
|