Here is the fourth part of my quest to share with you my experience in creating cloud based viewing technologies. Last Time we took a look at how to host our viewer service on the Amazon cloud, so it could be reached from any desktop application. Today we are going to add a, let’s say, fancy – at least to me – feature: server callbacks. It is also known as push notifications. Here is a definition I gathered from the web:
Push notification, or server push, describes a style of Internet-based communication where the request for a given communication is initiated by the publisher or server. This technology aims at enhancing performances and traffic at the client side by suppressing the need for the client to poll regularly the server for updates.
The technology we are studying today is a kind of push notification, only valid in the context WCF Server – WCF Client. You can find resources about it on the web by looking for WCF Callback keywords.
First of all, I largely inspired that example from the great blog post of Barry Dorrans you can find here.
As usual, I will start by the server side implementation, expose the deployment considerations, to finish with the client side implementation and tests. The idea I wanted to illustrate is to add server callbacks to our viewer service, such as when a new model is added by the administrator console to the cloud database, reciprocally removed, each client viewer will be automatically notified without the need to run for example a polling thread on the client side. I hope the interest of such an approach looks obvious to you…
I – Implementing callback on server side
The first thing we need to define is a callback contract interface, to reuse WCF terminology. This interface will expose the methods that the clients can implement in order to receive server callbacks. We also need to specify that this interface is the CallbackContract of a specific ServiceContract. In my example, I decided to create a specific ServiceContract so things can look more organized as if it was gathered with my existing previous contract. So declaration of that stuff is as follow:
public interface IServerCallback
{
[OperationContract(IsOneWay = true)]
void OnDbModelAdded(ModelInfo modelInfo);
[OperationContract(IsOneWay = true)]
void OnDbModelRemoved(string modelId);
}
[ServiceContract(CallbackContract = typeof(IServerCallback))]
public interface IAdnCloudViewerNotification
{
[OperationContract]
bool Subscribe();
[OperationContract]
bool Unsubscribe();
}
-
My callback interface need to notify the client of two things: when a new model is added and when one is removed, so the clients can automatically update their cloud model collection without need to refresh all of it from the server.
The new IAdnCloudViewerNotification contract also provides two functionalities: being able to subscribe and unsubscribe to the server callbacks.
Here is the added implementation on the server side:
public class AdnCloudViewerSrv :
IAdnCloudViewerSrv,
IAdnCloudViewerNotification
{
// Previous implementation...
private static readonly List<IServerCallback> _subscribers =
new List<IServerCallback>();
public bool Subscribe()
{
try
{
IServerCallback callback =
OperationContext.Current.GetCallbackChannel<IServerCallback>();
if (!_subscribers.Contains(callback))
_subscribers.Add(callback);
return true;
}
catch(Exception ex)
{
return false;
}
}
public bool Unsubscribe()
{
try
{
IServerCallback callback =
OperationContext.Current.GetCallbackChannel<IServerCallback>();
if (!_subscribers.Contains(callback))
_subscribers.Remove(callback);
return true;
}
catch
{
return false;
}
}
private void DbModelAddedNotifySubscribers(ModelInfo modelInfo)
{
_subscribers.ForEach(delegate(IServerCallback callback)
{
if (((ICommunicationObject)callback).State ==
CommunicationState.Opened)
{
callback.OnDbModelAdded(modelInfo);
}
else
{
_subscribers.Remove(callback);
}
});
}
private void DbModelRemovedNotifySubscribers(string modelId)
{
_subscribers.ForEach(delegate(IServerCallback callback)
{
if (((ICommunicationObject)callback).State ==
CommunicationState.Opened)
{
callback.OnDbModelRemoved(modelId);
}
else
{
_subscribers.Remove(callback);
}
});
}
}
The only modification to the existing methods was to add the appropriate calls to DbModelAddedNotifySubscribers and DbModelRemovedNotifySubscribers when a model is added/removed, so the subscribers get notified.
-
II – Configuring WCF service for callback notifications
-
That was the easy part. We now need to deploy our service in order to test it. I followed the path I’m getting used to when testing WCF services:
-
- Start by testing the service locally using the ServiceHost functionality I talked about in the second part.
-
- Deploy the service locally in IIS and test it locally.
-
- Deploy the service on the cloud in IIS and test it from remote desktops
-
The first we are going to need in order to get WCF callback working is to choose a binding that supports duplex communication. It’s pretty obvious when we think about it: in order for the server to callback the client, it needs to have a channel available between them, so basicHttpBinding we used so far isn’t adequate.
There are two predefined bindings that supports duplex, so our choices are pretty restricted: wsDualHttpBinding and Net.Tcp binding. The following post explains very well why Net.Tcp binding is the recommended binding to use, so I won’t expand over it, just read the post:)
-
As usual all those binding settings will be configured through the Web.Config file of our service, so pay attention to the following, as a little mistake in that file can cause hours of troubles finding why your dam service doesn’t work when deployed in IIS, talking from experience!
-
I highlighted the areas of specific interest. You can see I added a netTcpBinding configuration, as well as two netTcp endpoints: one for the IAdnCloudViewerSrv contract (that’s the previous viewer functionalities) and a new one IAdnCloudViewerNotification dedicated for the callback notifications. Although not required in this example, I conserved the basicHttpBinding endpoint for future use, for example to be used by a mobile client such as an Android application. Because of that you also need to allow mutiple site binding by specifying the multipleSiteBindingEnabled=true flag.
-
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding
name="AdnCloudServicesBinding"
transferMode="Streamed"
messageEncoding="Mtom"
maxBufferSize="2147483647"
maxBufferPoolSize="2147483647"
maxReceivedMessageSize="2147483647">
<readerQuotas
maxDepth="200"
maxStringContentLength="2147483647"
maxArrayLength="2147483647"
maxBytesPerRead="2147483647"
maxNameTableCharCount="2147483647"/>
</binding>
</basicHttpBinding>
<netTcpBinding>
<binding name="TcpBinding"
hostNameComparisonMode="StrongWildcard"
sendTimeout="00:10:00"
maxReceivedMessageSize="65536"
transferMode="Buffered"
portSharingEnabled="false">
<readerQuotas
maxDepth="200"
maxStringContentLength="2147483647"
maxArrayLength="2147483647"
maxBytesPerRead="2147483647"
maxNameTableCharCount="2147483647"/>
<security mode="None">
<transport clientCredentialType="None"/>
<message clientCredentialType="None"/>
</security>
</binding>
</netTcpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="AdnCloudServicesBehavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
<dataContractSerializer maxItemsInObjectGraph="2147483647"/>
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service
behaviorConfiguration="AdnCloudServicesBehavior"
name="AdnCloudViewerService.AdnCloudViewerSrv">
<endpoint
address="mex"
binding="mexTcpBinding"
contract="IMetadataExchange"/>
<endpoint
name="WCFCHttpEndpoint"
address=""
binding="basicHttpBinding"
bindingConfiguration="AdnCloudServicesBinding"
contract="AdnCloudViewerService.IAdnCloudViewerSrv"/>
<endpoint
name="WCFNetTcpEndpoint"
bindingConfiguration="TcpBinding"
binding="netTcpBinding"
contract="AdnCloudViewerService.IAdnCloudViewerSrv"
address=""/>
<endpoint
name="WCFCallbackEndpoint"
bindingConfiguration="TcpBinding"
binding="netTcpBinding"
contract="AdnCloudViewerService.IAdnCloudViewerNotification"
address="Callback"/>
</service>
</services>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true "/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
III – Enabling IIS for net.tcp transport
-
We are a couple of steps away from being able to deploy in IIS now. The next thing you need to make sure is that you enabled the WCF Non-HTTP Activation. This is done through the Add/remove Windows Feature of the control panel:
-
-
Fire your IIS console, select the desired web site and then go to “Bindings…” in the right pane menu. Here make sure a single net.tcp binding is defined and that it matches the port defined in your web.config. For me 8000 (the default net.tcp port is 808)
-
-
Publish a deployment package for your service and deploy as described in the second post. Once deployed, make sure you add net.tcp protocol to the list of Enabled Protocols of your service. This is available in the right pane menu, once you selected the relevant service, under Advanced Settings…
-
-
IV – Implementing the client side
If you did everything as described, you should have your service running locally under IIS and should be able to communicate through net.tcp with it. Go to the client project and add a new service reference to it (eventually delete any previous existing reference to the http service). In the new service reference field add in that case “net.tcp://localhost:8000/AdnCloudViewer/AdnCloudViewerSrv.svc” your service should be found by Visual Studio. Remember to add the async wrappers option, as described in the first post.
The client should implement the callback interface and also connect to the subscribe/unsubscribe service. The relevant parts of the implementation are illustrated below:
public partial class ViewerClientForm :
Form,
IDisposable,
IAdnCloudViewerNotificationCallback
{
AdnCloudViewerSrvClient _viewerClient;
AdnCloudViewerNotificationClient _notificationClient;
Dictionary<string, TreeNode> _modelIdNodeMap;
bool ConnectService()
{
try
{
// We use the NetTcp End Point
_viewerClient = new AdnCloudViewerSrvClient("WCFNetTcpEndpoint");
//_viewerClient=new AdnCloudViewerSrvClient("WCFCHttpEndpoint");
if (_viewerClient.Endpoint != null)
{
// Callback client initialization is as follow.
// For sake of simplicity init is synchronous
// but we could very well use an asyn init as well
InstanceContext context = new InstanceContext(this);
_notificationClient = new AdnCloudViewerNotificationClient(
context,
"WCFCallbackEndpoint");
// Subscribe to notification service
_notificationClient.Subscribe();
return true;
}
return false;
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show(ex.Message);
return false;
}
}
void IAdnCloudViewerNotificationCallback.OnDbModelAdded(
ModelInfo modelInfo)
{
try
{
ModelInfo[] infos = new ModelInfo[]
{
modelInfo
};
this.Invoke(
new RefreshCloudModelsDelegate(EndRefreshCloudModels),
new object[] { infos });
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show(ex.Message);
}
}
///////////////////////////////////////////////////////////////////////
// OnDbModelRemoved server callback implementation
//
///////////////////////////////////////////////////////////////////////
public void OnDbModelRemoved(string modelId)
{
if (_modelIdNodeMap.ContainsKey(modelId))
{
_modelIdNodeMap[modelId].Remove();
_modelIdNodeMap.Remove(modelId);
}
}
}
I also implemented very much the same thing on the console side, so there could be multiple consoles and clients apps running each on different machine over the web.
When done you can deploy the service on AWS cloud the very same way we did in the previous post. You would need to make sure net.tcp is enabled on the cloud machine as we did here locally.
That’s finally time to test our callbacks: upload a model through the console, you should see it appearing on each clients connected to the internet on any remote desktop machine… ta daaa!
Comments
You can follow this conversation by subscribing to the comment feed for this post.