Introduction
Windows Communication Foundation represents the state of the art communication library in the .NET environment. The flexibility of this device is amazing, and it allows to easily remotize functions on specific services, automatically serializing all the parameters in the signature through the simple decoration of properties and objects.
WCF allows to interconnect with services through web services standard communications or custom communications by simply modifying the configuration file, supporting a huge number of transfer protocols (HTTP, net TCP, MSMQ, ...) and communication protocols (security, reliable message...).
Another major quality of the library is its extensibility: it is possible to create new protocols, behaviors that can be defined at a service level or at an endpoint level.
The implementation of a service or of a service consumer through WCF is based on three concepts that are resumed by ABC: the Address where the service answers, the Binding which is how the service answers, and the Contract which are the methods exposed by the service. This article aims to describe a component which extends WCF in terms of configuration to make a service to be discovered at runtime by a service consumer through the WS-Discovery specification.
Background
WS-Discovery is a specification in the stack of WS-* protocols within the metadata.
The WS-Discovery specification foresees that a service is characterized not by its ABC parameters, but only by the Contract and the 'scopes' that are useful information to distinguish two services having the same interface. Each service implements a scheme of scopes called ScopeMatchBy, and eventually, the scopes in URI format (Uniform Resource Identifier). A service can change its metadata, or the scopes themselves, during its life, but it has to keep track of the version through an information called MetadataVersion.
At the opening, the service introduces itself to the network through a multicast Hello message containing the above information. Similarly, at closure, it sends a multicast Bye message to the network. If the metadata or the scopes change, the service must send just a Hello message with the new MetadataVersion number.
The client looks for a service through the type (that is the contract), eventually attaching the information about scope and ScopeMatchBy. The search can be carried out in two ways: without discovery proxy, and with discovery proxy.
Without discovery proxy, the client sends a multicast message called Probe containing the search parameters (contract type, ScopeMatchBy, and scopes).
The figure above shows the multicast communication in red and the unicast communication in blue.
Every service thinking of satisfying the request answers through a unicast ProbeMatch message. If ProbeMatch information is sufficient, the client can connect to the service, asking the service metadata to collect information about the binding first. Otherwise, the client must send a new multicast message called 'Resolve', and the service must answer with a unicast ResolveMatch message, giving necessarily the missing information.
If the discovery proxy is present in the network, the communication takes place with the same messages (Probe, ProbeMatch...), but modes change from multicast to unicast towards the discovery proxy.
To remind myself how communications take place, I introduce an analogy belonging to an environment other than the IT one. I think about several students in their classroom, every time a new student arrives, he enters the room greeting aloud and saying his name (Hello multicast). Suddenly, a student needs Robert, but he doesn’t’ know him. So, he calls Robert aloud in the silent room (Probe multicast). As a consequence, all the people called Robert stand up and go towards the person who called them (ProbeMatch unicast). Well, the discovery proxy represents the teacher in the room: when someone calls Robert aloud, besides all the people called Robert, the teacher stands up, too. He goes to the student and he introduces himself, 'I’m the teacher, and the next time you need someone, come and ask me for (Hello unicast). After that, the student stands up and asks the teacher when he needs someone (Probe unicast).
In this implementation, I won’t consider the version with the discovery proxy, but as soon as I have time, I will extend my code to include the discovery proxy as well, maybe integrating the IStorageAdapter
of Roman Kiss’ WS-Transfer.
Using the code
The WS-Discovery implementation for WCF can be divided into two parts (into three parts if the discovery proxy implementation is included): a part concerning the service and the other concerning the client.
The integration of the discovery approach from the client side point of view extending the WCF configuration is problematic because WCF standard proxy works by specifying the Address Binding and Contract, while the discovery proxy needs only the Contract; other information are just discovered at runtime.
Moreover, I didn’t consider the client-side configuration aspect so important. The scopes are optional: if they aren’t specified, all services implementing the contract will satisfy the scopes request, unless particular service configurations are present.
The implementation of a discovery proxy doesn’t need to inherit from a class as per a WCF proxy, but it is enough to declare a DiscoveryClient<TChannel>
class. In fact, the Channel property allows to interact with the methods of the remote service.
DiscoveryClient<IServiceSample> proxy = new DiscoveryClient<IServiceSample>();
The constructor foresees to be able to eventually receive the scopes required for the service, and so it begins the search for the required service by sending a Probe message to the network. It is important to have an instance of the proxy as soon as possible so that at the first call of a proxy method, the service to be connected to has already been found avoiding useless waits.
When the ProbeMatch message arrives, the client can release the WaitHandle
, invoke the remote call, and return the result. If the client receives more than one ProbeMatch, the first one unlocks the semaphore, and at the moment of the call invocation, the best service is found through the virtual method GetBestMemento()
. It is probable that at the first call, the first service who sent the ProbeMatch is used, unless a sufficient time went by between instantiation and method invocation to receive all ProbeMatch before the method invocation. Once the client uses a channel, it continues to use it all its life long. By re-creating the client, all the early stored ClientMemento
remain, and so GetBestMemento
will be able to find the best candidate in a more careful way.
As already said, GetBestMemento
is a virtual function, that means that it is possible to inherit the DiscoveryClient<IServiceSample>
class and to execute the overridden method to run the custom selection logic.
ClientMemento
is the class that represents the basic information to interconnect to the service. Through the data received by the probe, it is possible to instantiate the memento that takes care of getting other possible information, as for instance the binding, through the metadata exposed by the service. If the client and the server use this discovery implementation, a shortcut is present to avoid the metadata roundtrip. The ProbeMatch message foresees the sending of the service address endpoint through the EndpointReference
XML structure (see WS-Addressing), which includes not only an address field, but also an extensibility paradigm within the ReferenceParameters
field. Within this field, the service enters all information concerning the communication binding. If there is this information, ClientM
emento doesn’t need the metadata roundtrip, and it is ready for communication.
A major quality of the client implementation is the AutomaticChangeChannelWhenFaulted
property, which allows the fault tolerance property on the services. In fact, when there are several candidate services, GetBestMemento
returns a channel towards the chosen service. If the proxy operation fails, the client automatically repeats the same operation towards another candidate at disposal, and it throws the exception only when there is no other candidate on which the operation can be executed. This client functionality is disabled by setting the property to false; as a consequence, the exception would be thrown when other candidates are present as well.
One of the main worries dealing with the client concerned with the management of personalized headers in a message sent using WS-Discovery. WCF allows the addition of personalized headers by the use of the OperationContextScope
class, which unfortunately was declared sealed
and is therefore inextensible to discovery functionalities.
IMyContract proxy = cf.CreateChannel();
using (new OperationContextScope((IClientChannel)proxy))
{
OperationContext.Current.OutgoingMessageHeaders.Add(
MessageHeader.CreateHeader("otherHeaderName",
"http://otherHeaderNs", "otherValue"));
Console.WriteLine(proxy.echo("Hello World"));
}
The problem is due to the fact that the DiscoveryClient
channel doesn’t implement the IClientChannel
interface, at least until when the real channel is created at the end of the discovery process. That is why I had to create a less elegant solution that let nevertheless bypass the problem. I’m talking about the DiscoveryOperationContextScope<TChannel>
class that allows, in a similar way, to execute the required operation.
DiscoveryClient<IMyContract> proxy = new DiscoveryClient<IMyContract>();
using (DiscoveryOperationContextScope<IMyContract> os
= new DiscoveryOperationContextScope<IMyContract>(proxy))
{
os.OutgoingHeaders.Add(MessageHeader.CreateHeader("MyHeaderName",
"" ,"MyheaderValue"));
Console.WriteLine("Output string: " + proxy.Channel.GetString("qqq"));
}
The implementation of the service part seems easier, the behavior adds to the static ServiceContext
class the information concerning the service. ServiceContext
hooks the Opened
and Closing
events of ServiceHost to send Hello and Bye messages and it continues to listen to at the multicast port to receive the Probe/Resolve messages.
A function linked to the extensibility concerns the dynamic scopes. While implementing the service, I wanted to make the scopes to be dynamically manageable to let them be changed during the service life, reminding always to myself that every metadata change has be notified to the client through a new Hello message.
Configuration of the discoverable service
The part concerning the service foresees that WS-Discovery can be set during the configuration phase through a specific Behavior and an associated extension:
<extensions>
<behaviorExtensions>
<add name="serviceDiscoverableBehavior"
type="Masieri.ServiceModel.WSDiscovery.Behaviors.DiscoveryBehaviorSection,
Masieri.ServiceModel.WSDiscovery, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=18ad931e67d285bd"
/>
</behaviorExtensions>
</extensions>
<services>
<service behaviorConfiguration="serviceDiscoverable"
name="ServiceTest.IServiceSample">
...
</service>
</services>
The discovery functionality setting can therefore be introduced without recompiling the code again but by simply introducing the settings in the configuration file. The associated behavior has to be configured with specific sections:
<behavior name="serviceDiscoverable">
<serviceMetadata
httpGetEnabled="true"
httpGetUrl="http://localhost:8080/Mex" />
<serviceDiscoverableBehavior
scopesMatchBy="http://schemas.xmlsoap.org/ws/2005/04/discovery/rfc2396">
<scopes>
<add url="http://myscope.tempuri.org/"/>
</scopes>
</serviceDiscoverableBehavior>
</behavior>
</serviceBehaviors>
The ServiceMetadata behavior has to be specified in the ServiceBehavior
setting to allow to recover the binding settings through WSDL. Actually, this setting would be unnecessary, if this discovery library were used both for the client and for the server, as a mechanism was developed to avoid the metadata roundtrip and to allow a faster communication. The automatic metadata creation by WCF is very comfortable, but not so fast: it is true that it is created the first time and then kept in memory for the following requests; that is normal in 90% of the cases, but in some architectural cases, I found the services were often instantiated and destroyed with a lot of trouble for me as a consequence. If the WSDL creator had problems, for instance, due to unknown custom protocols (I noticed it in the implementation of the SDK soap.udp), the creation could be slowed down for some seconds.
The service setting through the configuration is done, but it is possible to configure the service in a programmatic way.
Log
The log management was realized in a particularly attentive way to allow a complete integration with the WCF logs to have an exhaustive vision of the communication scenario. By the use of the configuration file, it is possible to add the System.ServiceModel.WSDiscovery
source to the System.ServiceModel
one and to make the log messages to flow together to a specific listener. The WS-Discovery messages are instead added to the System.ServiceModel.MessageLogging
source as all the WCF messages.
<system.diagnostics>
<sources>
<source name="System.ServiceModel.WSDiscovery"
switchValue="Warning, Error">
<listeners>
<add initializeData="InfoServiceDebug.e2e"
type="System.Diagnostics.XmlWriterTraceListener,
System, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
name="ServiceModel Listener"
traceOutputOptions="LogicalOperationStack, DateTime,
Timestamp, ProcessId, ThreadId, Callstack" />
</listeners>
</source>
<source name="System.ServiceModel.MessageLogging"
switchValue="Warning, Error" >
<listeners>
<clear />
<add type="System.Diagnostics.DefaultTraceListener" name="Default"
traceOutputOptions="None" />
<add initializeData="MessageLog.e2e"
type="System.Diagnostics.XmlWriterTraceListener,
System, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
name="MessageLogging Listener"
traceOutputOptions="LogicalOperationStack, DateTime, Timestamp,
ProcessId, ThreadId, Callstack" />
</listeners>
</source>
</sources>
<sharedListeners>
<add type="System.Diagnostics.DefaultTraceListener"
name="Default" />
</sharedListeners>
</system.diagnostics>
Points of interest
In this paragraph, I would like to share my architectural experiences regarding the use of WS-Discovery.
Service redundancy
The key-aspect of an architecture based on WS-Discovery is the realisation of service redundancy when possible. Some time ago, I worked on the system of the Italian railway network (RFI). Talking about used solutions, it is as various and heterogeneous as every other system of such a great dimension, but the whole system is controlled through web services. The geographical aspect of the systems, which are located in stations, compartments, and in the RFI CED (data centre) was a peculiarity to be taken in an adequate consideration, because it was unconceivable that a web service could face all the calls coming from the whole country (actually, it is possible, but really expensive!).
In such a scenario, I developed the first version of WS-Discovery to try to completely change the existing model (of course, the sources were completely rewritten and improved in a lot of aspects). Working in a team of people who work well together, we developed a solution that made several clients from the whole territory, not to interface directly with the central web service, but to find run time local providers that gave them the same information. These local providers were actually cache managers pretending to be the required service. If the client needs information coming directly from the source, it can always get it by the use of the correct scope.
Fault tolerance
Fault tolerance represents another magic aspect of WS-Discovery. When a client cannot make a call to a service, it can try with another service satisfying the same scopes criteria. This behaviour is obviously settable through the client AutomaticChangeChannelWhenFaulted
property.
do
{
try
{
OnInvoking();
object ret = method.Invoke(_lastUsedChannel, parameters);
OnInvoked();
DiscoveryLogger.Info("Method invoked successfully");
return ret;
}
catch (Exception ex)
{
DiscoveryLogger.Error("Errore nell'invocazione del servizio", ex);
lock (_lock)
{
ClientContext.Current.RemoveDiscoveredEndpoint(
Helpers.ContractDescriptionsHelper.GetContractFullName<TChannel>(), mem);
mem = GetBestMemento();
if (mem == null)
{
StartProbeProcess();
DiscoveryLogger.Warn(
@"The DiscoveryClient can't scale on another service");
if (originalException == null)
throw ex;
else
throw originalException;
}
if (originalException == null)
originalException = ex;
}
}
} while (AutomaticChangeChannelWhenFaulted);
Scopes with quality of service and dynamic change of the system
In another project developed together with the railway society, we studied a solution with dynamic scopes which represented the service QOS (Quality of service) to allow the client to hook the service whose QOS is compatible with its needs, without being too demanding, to avoid blocking all the services with better QOS. By executing the GetBestMemento
overload, the client can select the service with the most suitable QOS, while the services update the QOS according to the number of simultaneous users through the MetadataVersion mechanism.
Extension of Roman Kiss’ ESB
WS-Discovery is completely compatible with the implementation of WS-Eventing and WS-Transfer dealt with in Roman Kiss’ article: server-side, by including the discovery Behavior into the service; client-side, by banally using the DiscoveryClient<RKiss.WSEventing.IWSEventing>
class, for instance.
Dynamic load balancing
In a recent architecture for video surveillance and scene analyses systems, I implemented a service that can dynamically load some components of scene analyses which allowed, through a SubscriptionManager, to subscribe to the analyzed events. The computational work carried out by these components was high. I thought of integrating them in a server that was sufficiently scalable in several PCs, giving the identifier of the analyzed scene in the scopes. In such a way, the system can optimize the load on several machines without clustering configuration. It can dynamically distribute the load without taking care of the interconnected clients. In the case of dynamic reallocation of a component from a machine to another one, server 1 starts the host preparation process on a new machine (server 2), which sends a hello with the list of scopes of the served component when it is ready. The client, which receives the hello message, files the Memento among those at disposal. Server 1 sends a Hello with a new MetadataVersion version, where the scope of the removed component is no more present, and sends an EndSubscription message to the connected clients automatically, therefore causing the transfer to the new Memento and the new Subscribe on server 2.
History
- October 2008 - First release.