|
NATs (Network
Address Translation) prevents the private IP address of your home and/or
work PC to be exposed to the big wild and bad Internet. The only thing nodes
outside of the NAT-shielded network can see is the IP address of the NAT-capable
router device. A firewall
should prevent unwanted network traffic from both inside and outside of a
network - and personal firewalls even scale this wish down to the single
computer, again at work or at home.
Cool. This is good and surely not extremely latest news for you. But this
fact and these two measurements makes building certain kinds of distributed
systems a little bit... well, tough, to say the least. Or as I would put it in
my words: "It totally sucks to build duplex communication over the Internet as
we all want to be safe and secure.".
I have been facing the wish to call from an Internet-located resource to an
enterprise- or home-located program in a secure fashion countless times. There
are tons of programs that can do this, like MSN Messenger or Skype, to name just
a few very prominent ones. So, what can we do when we want to build a
distributed solution based on Windows, based on .NET, based on WCF? Surely, we
could build everything that Messenger et. al. does on our own or just hijack
Messenger. But I think there should be some more general purpose approach which
is potentially highly interoperable (more
to come...).
WCF has this neat feature of duplex channels and callback contracts. You can
do duplex messaging in WCF over different transports, like TCP, Named Pipes and
even HTTP. In the latter case you need to set up (or better: let WCF do it for
you) an HTTP-based callback endpoint. This is either done automatically for you
(watch out for some
interesting security side effect or
you can decide which address your consuming application should listen on.
Whatever approach you use, you just cannot beat firewalls and you surely lose
the battle against NATs - so what to do?
We need some helpers, as always.
This helper shows up in the shape of the
Connectivity and the Identity
services from the BizTalk Services labs
cornucopia. We can use the Connectivity Service as a relay in order to enable
the above described callback scenario. There are already some very good
philosophical and
architectural
postings and
descriptions of the currently available services.

For duplex communication to happen, the key secret is that we do not only
register the service with the relay but also the client's base callback address.
Of course we need to model the service and consumer conversation and we decide
to go for WCF's opt-in callback contract model. Next we add a duplex-capable
binding and should be ready to go.
First, let's have a look at the major
ServiceContract for a really simple example. We just want to be able to
subscribe and unsubscribe to a service to get notifications when certain topics
occur. Yeah, I know, this is far from being a complete and production-ready
pub/sub framework - if you look for this please switch over to my friend
Juval.
[ServiceContract(
Name =
"PubSub",
Namespace =
"http://thinktecture.com/services/relay/pubsub",
ConfigurationName="PSC",
CallbackContract=typeof(IPubSubCallback))]
public interface
IPubSub
{
[OperationContract(IsOneWay=true)]
void Subscribe(string
subMessage);
[OperationContract(IsOneWay
= true)]
void Unsubscribe(string
unsubMessage);
}
For sake of completeness, this is the
CallbackContract to publish data from the service to the consumers.
[ServiceContract(
Name =
"PubSubCallback",
Namespace =
"http://thinktecture.com/services/relay/pubsub",
ConfigurationName="PSCC")]
public interface
IPubSubCallback
{
[OperationContract(IsOneWay
= true)]
void Publish(string
pubMessage);
}
So far, nothing really new and extraordinary exciting, at least if you are
familiar with basic duplex callbback contract modeling.
Second, we now need a binding that a) supports duplex communication and b) of
course can do the relaying from the BizTalk Labs Connectivity service. That
means that netTcpBinding,
netNamedPipeBinding and
wsDualHttpBinding will not work - for obvious reasons.
Following is the service-side config for our simple duplex relay sample. This
shows a little known but incredibly powerful way to make up a custom binding
with duplex capability. We use the relayTransport
from the BizTalk Services SDK and stack on top if it a binary message encoder.
Then we need a oneWay element in order to be
able to take a IDuplexSessionChannel or a
IRequestChannel and expose it as a
IOutputChannel, or conversely it can take a
IDuplexSessionChannel or a
IReplyChannel and expose it as a
IInputChannel. Last but not least we add a
compositeDuplex element.
<configuration>
<system.serviceModel>
<bindings>
<customBinding>
<binding
name="duplexRelay">
<compositeDuplex
/>
<oneWay
/>
<binaryMessageEncoding
/>
<relayTransport
/>
</binding>
</customBinding>
</bindings>
<services>
<service
name="PSS">
<endpoint
name="DuplexRelayEndpoint"
contract="PSC"
binding="customBinding"
bindingNamespace="http://thinktecture.com/services/relay/pubsub"
bindingConfiguration="duplexRelay"
/>
</service>
</services>
</system.serviceModel>
</configuration>
I will skip the service hosting code, it is merely about fiddling with the
endpoint address setup as I am runnin gthe servive and the client on the same
machine and want to dynamically have the machine name in the endpoint address so
that the relay service can register a unique address for the relaying.

Half time.
Now on to the consuming side. We similarily need to have the custom duplex
binding but now want to explicitly specify the endpoint address for the
callback. We will do this inside of the client code.
<configuration>
<system.serviceModel>
<bindings>
<customBinding>
<binding
name ="duplexRelay">
<compositeDuplex
clientBaseAddress="SeeCode"
/>
<oneWay
/>
<binaryMessageEncoding
/>
<relayTransport
/>
</binding>
</customBinding>
</bindings>
<client>
<endpoint
name="RelayEndpoint"
contract="PSC"
binding="customBinding"
bindingConfiguration="duplexRelay"
address="SeeCode"
/>
</client>
</system.serviceModel>
</configuration>
This is the code to set the client's base callback address dynamically in
code, derived from what I have shown
here.
CustomBinding binding = (CustomBinding)channelFactory.Endpoint.Binding;
CompositeDuplexBindingElement
duplex = binding.Elements.Find<CompositeDuplexBindingElement>();
Uri clientAddress =
new
Uri(
String.Format(
"net.relay://{0}/services/{1}/PubSub/callback/",
RelayBinding.DefaultRelayHostName,
GetHostName()));
duplex.ClientBaseAddress =
clientAddress;
One thing we need to make sure with the custom duplex binding it to have the
WS-Addressing ReplyTo header explicitly set on
the outgoing message.
using (new
OperationContextScope((IContextChannel)channel))
{
Console.WriteLine("Calling
Subscribe...");
OperationContext.Current.OutgoingMessageHeaders.ReplyTo
=
((IClientChannel)channel).LocalAddress;
channel.Subscribe(input);
}
Likewise, on the service side, we need to add the To
header to the outgoing message traveling to the original caller, i.e. the
client.
public void Subscribe(string
subMessage)
{
Console.WriteLine("Subscribed...:
{0}", subMessage);
IPubSubCallback callback =
OperationContext.Current.GetCallbackChannel<IPubSubCallback>();
if (callback !=
null)
{
OperationContext.Current.OutgoingMessageHeaders.To
=
OperationContext.Current.IncomingMessageHeaders.ReplyTo.Uri;
callback.Publish("Huhu!");
}
}
To make a rather long story short, this is what happens when we run both the
service and the client. We also tested this between Sri Lanka and Germany :)
Buddhike was running the
service and I the client: worked, I could reach his service and his service was
calling back into my client - everything behind firewalls and NATs.
Everything happens in a safe fashion as we need to authenticate ourselves with a
registered self-issued InfoCard for communication.

Oh, BTW, there is currently one caveat with my approach and the code: the
first time the service wants to call back into the client some human being needs
to select the appropriate card. This is by design of the current
netRelayBinding from the BizTalk Services SDK
and surely by design of CardSpace.
I am still investigating.
[Download
sample code]
|