Contents
The modern application architecture requires a transparent mapping of the Distributed Object Oriented Design model to the physical deployed model on the Intranet and Internet Networks. The standard .NET Remoting allows to consume a remoting object using only one standard channel such as the tcp or http in the physical peer-to-peer design pattern. The .NET Remoting infrastructure doesn't have a direct mechanism to chain and re-route channels between the consumer and remote object.
This article show you how to design and implement the message sink (Router) to forward the Remoting Message (IMessage
) to the properly channel. The connectivity to the well-known remote object described by its url (uniform resource locator) address can be mapped to the logical url address and administrated in the config file or programmatically changed during the runtime. Using the logical url addresses allows to virtualize a distributed business model based on the remoting interface contracts.
Before than we will go to its implementation details, let's start it with its concept and usage. I am assuming that you have a knowledge of the .NET Remoting.
To create a proxy for the well-known remote object driven by interface contract requires to know the following:
- metadata describing the interface contract, for instance:
ITestInterface
- url address of the remote object, for instance: tcp://localhost:9090/endpointB
The Interface contract is an abstract definition between the consumer and remote object located in the shareable assembly installed in the GAC, which allows to build a distributed model based on the loosely coupled design pattern.
The second requirement, the url address represents the physical description of the peer-to-peer connectivity. This string can be hard coded or programmatically retrieved from the custom config file using the extra coding for that.
Basically, the url address consists of the information related to the remoting channel and remoting object. It's information about the message dispatching. For instance:
tcp://localhost:9090/endpointB
where:
-
tcp is a channel name to select a specified channel from the collection of the registered channels in the current AppDomain. This is a unique name, which it holds the link of all requested sinks in the channel organized into the stack. The remoting message flows through this sink stack in the order how the sinks have been registered.
-
:// specifies the channel delimiter
-
localhost:9090 specifies the destination (receiver) channel. In our example, this is a tcp standard receiver. Using the custom channel it can be different, for instance: for MSMQ channel this part might look like this: ./private$/queueName
-
/ specifies the endpoint delimiter
-
endpointB represents an object Uri address. This is a remoting endpoint address referencing the well-know remote object published by its host process. Note that the endpoint address is needed only for a last sink in the server channel. The concept of the chaining channels is based on that.
The url address is stored in the IMessage
(property Uri) based on the client request. It's a read-writeable property allows to be modified and more abstracted. In the properly channel we need a custom message sink to check its format and mapping to the physical url address required by the remoting infrastructure.
The following sinks describes these features:
The concept of the Router Client Message Sink is based on the mapping the physical url address to the unique logical name, for instance, see the following mapping:
tcp://localhost:9090/endpointB => tcp://testobject
where: testobject = localhost:9090/endpointB represents the logical url name on the client side
It's a responsibility of the router client sink to make this mapping based on the in memory knowledge base of the url addresses (urlKB). The urlKB located in the client sink provider and it can be configured during the registration service from the config file. Of course, its contents also can be updated during the runtime based on the application needs, which it will allow to reconfigure a distributed model on the fly.
The following config snippet shows a configuration of the client provider in the tcp channel included its urlKB (custom property lurl):
<clientProviders>
<!-- Message Router -->
<provider ref="router" name="TcpRouterC9092"
lurl ="endpoint=localhost:9090/endpoint,
endpointB=localhost:9090/endpointB,
test=localhost:9092/; tcp://localhost:9090/endpoint,
router9092=localhost:9092/"/>
<formatter ref="binary" />
</clientProviders>
The position of the Router Server Message Sink in the logical channel is different from its client side. The server channel is processing the client's outgoing IMessage
, therefore the router message sink can control the message workflow based on the url address.
Concept of the chaining remoting channels is shown in the following picture:
The router is driven by delimiter character ';' in the logical url address string. In this case, the IMessage
is dispatching to the next channel instead of forwarding to the next sink. Like the above client provider, the server provider also contains the urlKB to obtain the physical url address.
The following config snippet shows that:
<serverProviders>
<formatter ref="binary" />
<provider ref="router" name="TcpRouterS9092"
lurl ="router9090=tcp://localhost:9090/,
router9092=tcp://localhost:9092/,
endpoint=tcp://localhost:9090/endpoint,
endpointB=tcp://localhost:9090/endpointB,
test=tcp://localhost:9092/; tcp://localhost:9090/endpoint"/>
</serverProviders>
Based on the above concept, the connectivity to the remote object is transparent regardless of how many channels have been chained. This connectivity can be described by the unique logical name and the routers of the each channel will take care it.
Using the Router Sink requires that you install the MessageRouter.dll and RouterLogicalCallContext.dll assemblies into the GAC and the following modification of the machine.config file in the remoting section:
<channelSinkProviders>
<clientProviders>
<formatter id="soap"
type="System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider,
System.Runtime.Remoting,Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<formatter id="binary"
type="System.Runtime.Remoting.Channels.BinaryClientFormatterSinkProvider,
System.Runtime.Remoting,Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"/>
<provider id="router"
type="RKiss.MessageRouter.RouterClientSinkProvider,
MessageRouter,Version=1.0.936.36529,
Culture=neutral, PublicKeyToken=47a36cf75249d9dc"/>
</clientProviders>
<serverProviders>
<formatter id="soap"
type="System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider,
System.Runtime.Remoting,Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<formatter id="binary"
type="System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider,
System.Runtime.Remoting,Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<provider id="wsdl"
type="System.Runtime.Remoting.MetadataServices.SdlChannelSinkProvider,
System.Runtime.Remoting,Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<provider id="router" type="RKiss.MessageRouter.RouterServerSinkProvider,
MessageRouter,Version=1.0.936.36529, Culture=neutral,
PublicKeyToken=47a36cf75249d9dc"/>
</serverProviders>
</channelSinkProviders>
The MessageRouter contains two sink providers, one for the client and the other one for server. Both of them have the same id (router), which can be used to reference them in the config files.
The following features are built -in:
- RouterClientSinkProvider is used only for mapping logical url address to the physical url.
- RouterServerSinkProvider is used for mapping url addresses and re-routing the remoting message flow.
Note that the routers in the client/server channels are not tightly coupled, so they can be used separately based on the application needs.
After installation of the MessageRouter (GAC + machine.config), the router is ready to use in the .Net Remoting infrastructure like other standard providers such as soap and binary.
The following picture illustrates how to use a client router:
The remote object (published as endpointB) is fully transparent to the consumer side in the loosely coupled design pattern. The client is creating its proxy based on the unique logical url address (for instance: endpointB) in the tcp channel. The client router is mapping this address to the physical address (tcp://localhost:9090/endpointB) and forwarding to the next sink. Note that the logical url name doesn't need to be matched with the endpoint address, but it's better and readable to use it the same.
As you can see, using the client router is straightforward and configurable, special when the consumers are based on the Remoting Interface contract.
How about the situation when a deploying model needs to use more than one channels, for instances; asynchronous remoting call over internet using the Web Service and MSMQ, event driven architecture using the custom MSMQ channel, etc.? Well, in situations like those, the chaining channels is a good solution to hide all connectivity issues from the business logic.
The following picture shows the configuration issues for the chaining channels using the Server Router:
The Server Router of the Channel X-1 found the router delimiter (';') in the Uri string, which indicates to forward the IMessage
to the next channel (Channel X). The next channel in the Uri string is described by its unique logical url address, so the router will replace it by the physical one from its knowledge base (urlKB). After that, the IMessage
can be re-routed to the properly channel.
Note that the chained channels can be used in any combination of the standard and custom channels. For illustration and evaluation purposes I used chaining of the standard tcp channels.
Updating Router
Earlier, I mentioned that the knowledge base of the router sink (urlKB) can be updated during the runtime. The concept is based on using the CallContext
object which it travels with the Remoting message between the client and remote object. There is a very simple abstract definition (contract) located in the RouterLogicalCallContext.dll assembly:
namespace RKiss.MessageRouter
{
[Serializable]
public class RouterLogicalCallContext : ILogicalThreadAffinative
{
string strUrlKB;
public string UrlKB
{
get {return strUrlKB; }
set { strUrlKB = value; }
}
}
}
The following client's code snippet shows how to update the urlKB, for instance, in the TcpRouterS9092:
routerName = "TcpRouterS9092";
RouterLogicalCallContext urlkb = new RouterLogicalCallContext();
urlkb.UrlKB = "endpointB=tcp://localhost:1234/myObjectUri, endpoint=";
CallContext.SetData(routerName, urlkb);
The above code will perform updating of the endpointB entry and deleting of the endpoint entry from the urlKB of the TcpRouterS9092.
The Router Provider uses the following standard and custom properties:
ref
is the provider template being referenced (for instance: ref="router")
name
specifies the name of the provider. Each provider has an unique name which is used for proper message (for instance: name="TcpRouterC9092")
lurl
(custom property) specifies the string of the url pairs such as logical url name and its physical representative. The comma is delimiter to separate each url pairs in the lurl string (for instance: test=tcp://localhost:9092/; tcp://localhost:9090/endpoint, endpoint=tcp://localhost:9090/endpoint).
The Router design is based on re-directing the IMessage
to the first message sink of the next chained channel. The .NET Remoting infrastructure allows to chain the message sinks in the channel. Plug-in a custom message sink (Router) in the properly channel position (stack of the message sinks) and monitoring the url address is a design pattern of the chaining channels.
The following picture show that:
The incoming IMessage
in the server channel is passed trough the first sink - formatter. After that, the next sink is the Router and the IMessage
is passed to its method ProcessMessage
. There is all router logic in this method:
- Checking the
CallContext
for the router name. If the object exists, the router's knowledge base is updated based on its property (UrlKB).
- Checking the router delimiter in the url address string. If it does not exist, the standard message flow is invoked -
nextSink.ProcessMessage
method and its result is returned back to the caller.
In the case of the router delimiter, the following router logic is going to be performed:
- Replacing the logical url address from the local url knowledge base, if it's necessary.
- Searching for the next (chained) channel.
- Creating the MessageSink for this channel.
- Calling the
SyncProcessMessage
rsp. AsyncProcessMessage
method to pass the IMessage
to the next channel (first message sink).
- Return the result back to the caller.
As the above picture shows, the chaining channels is straightforward way with a minimum performance overhead. The IMessage
image has been already created by the consumer of the remote object, we just re-directed it to the other sink in the chained channel.
The behaviour of the message flow is like on the client channel, therefore the chained channel has to be registered in the same host process with the router channel. The host process, in this situation, represents a bridge process between the chained remoting channels. The chained channel doesn't need to be the same type, it can be any standard or custom channel, the .NET Remoting infrastructure will guarantee that the IMessage
will flow properly through all of them.
The implementation of the Router uses a standard message sink boilerplate (infrastructure). I will skip it this description and I am going to focus only on the parts which related to the router.
In the server router, there are two places where a router logic has to be inserted. The first place is the sink provider's constructor to initialize its knowledge base (located in the hashtable object) by values from the config file. The following code snippet shows that:
public RouterServerSinkProvider(IDictionary properties,
ICollection providerData)
{
string strLURL = "";
if(properties.Contains("name"))
m_strProviderName = Convert.ToString(properties["name"]);
if(properties.Contains("lurl"))
strLURL = Convert.ToString(properties["lurl"]);
if(strLURL != "")
{
try
{
string[] arrayUrl = strLURL.Split(new char[]{'=',','});
for(int ii=0; ii<arrayUrl.Length; ii++)
{
m_HT.Add(arrayUrl[ii].Trim(), arrayUrl[++ii].Trim());
}
}
catch(Exception ex)
{
string strWarning =
string.Format(
"{0}.RouterServerSinkProvider has problem ({1}) in the {2}.",
m_strProviderName, ex.Message, strLURL);
WriteEventLog(strWarning, EventLogEntryType.Warning);
}
}
WriteEventLog(string.Format(
"{0}.RouterServerSinkProvider has been initiated.",
m_strProviderName));
}
The second place is a sink's ProcessMessage
method. There is a logic to update an internal knowledge base (hashtable) from the CallContext
object targeted for the specified router. The rest of work is done in the private methods such as MessageDispatcher
and MessageRouter
.
public ServerProcessing ProcessMessage(
IServerChannelSinkStack sinkStack,
IMessage requestMsg, ITransportHeaders requestHeaders,
Stream requestStream, out IMessage responseMsg,
out ITransportHeaders responseHeaders,
out Stream responseStream)
{
ServerProcessing servproc = ServerProcessing.Complete;
responseHeaders = null;
responseStream = null;
if(m_Next != null)
{
object objCC = requestMsg.Properties["__CallContext"];
if(objCC != null && objCC is LogicalCallContext)
{
LogicalCallContext lcc = objCC as LogicalCallContext;
object objData = lcc.GetData(m_Provider.ProviderName);
if(objData != null && objData is RouterLogicalCallContext)
{
RouterLogicalCallContext rlcc =
objData as RouterLogicalCallContext;
if(rlcc.UrlKB == "")
{
m_Provider.ClearLogicalURL();
}
else
if(rlcc.UrlKB == "?")
{
rlcc.UrlKB = m_Provider.GetLogicalURL();
lcc.SetData(m_Provider.ProviderName, rlcc);
}
else
{
string[] arrayUrl =
rlcc.UrlKB.Split(new char[]{'=',','});
for(int ii=0; ii<arrayUrl.Length; ii++)
{
m_Provider.SetLogicalURL(
arrayUrl[ii].Trim(), arrayUrl[++ii].Trim());
}
}
}
}
responseMsg = MessageDispatcher(requestMsg);
if(responseMsg == null)
{
servproc = m_Next.ProcessMessage(sinkStack, requestMsg,
requestHeaders, requestStream,
out responseMsg, out responseHeaders, out responseStream);
}
else
if(RemotingServices.IsOneWay((
requestMsg as IMethodCallMessage).MethodBase) == true)
{
servproc = ServerProcessing.OneWay;
}
}
else
{
Trace.WriteLine(string.Format(
"{0}:RouterServerSink ProcessMessage null",
m_Provider.ProviderName));
responseMsg = null;
responseHeaders = null;
responseStream = null;
}
return servproc;
}
The MessageDispatcher
method has a responsibility to validate a primary url address. In the case of the logical address, it will perform its mapping to the physical form using the provider's knowledge base (urlKB).
private IMessage MessageDispatcher(IMessage requestMsg)
{
IMessage responseMsg = null;
if(requestMsg.Properties["__Uri"] != null)
{
string strUrl = requestMsg.Properties["__Uri"].ToString();
string[] strArrayUrlPath = strUrl.Split(';');
string[] strArrayPrimaryAddr = strArrayUrlPath[0].Split('/');
if(strArrayUrlPath.Length == 1 &&
strArrayPrimaryAddr.Length == 2)
{
}
else
{
string strObjUrl = strUrl.Remove(0,
strArrayUrlPath[0].Length + 1).TrimStart(' ');
string[] strArrayNewUrlPath =
strObjUrl.Split(new char[]{';'}, 2);
string lurl = strArrayNewUrlPath[0].Trim();
if(lurl.IndexOf("://") < 0)
{
string purl = m_Provider.GetLogicalURL(lurl);
if(purl != "" && strArrayNewUrlPath.Length == 1)
{
strObjUrl = purl;
}
else
if(purl != "" && strArrayNewUrlPath.Length == 2)
{
strObjUrl = purl + ";" + strArrayNewUrlPath[1];
}
}
responseMsg = MessageRouter(requestMsg, strObjUrl);
}
}
else
{
Exception exp = new Exception(
string.Format(
"{0}:RouterServerSink: The Uri address is null",
m_Provider.ProviderName));
responseMsg = new ReturnMessage(exp,
(IMethodCallMessage)requestMsg);
}
return responseMsg;
}
Finally, the re-routing process is implemented in the following method. The logic is very simple. First of all, the valid channel is searching and creating its first message sink based on the chained physical url address. When we have a correct message sink, the IMessage
can be passed into the sink invoking its method SyncProcessMessage
resp. AsyncProcessMessage
. The return value (respondMsg
) is sent back to the original caller.
private IMessage MessageRouter(IMessage requestMsg, string strObjUrl)
{
IMessage responseMsg = null;
requestMsg.Properties["__Uri"] = strObjUrl;
string strDummy = null;
IMessageSink iMsgSink = null;
foreach(IChannel channel in ChannelServices.RegisteredChannels)
{
if(channel is IChannelSender)
{
iMsgSink = (channel as IChannelSender).CreateMessageSink(
strObjUrl, null, out strDummy);
if(iMsgSink != null)
break;
}
}
if(iMsgSink == null)
{
string strErr = string.Format(
"{0}:RouterServerSink: A supported " +
"channel could not be found for {1}",
m_Provider.ProviderName, strObjUrl);
responseMsg = new ReturnMessage(new Exception(strErr),
(IMethodCallMessage)requestMsg);
}
else
{
if(RemotingServices.IsOneWay((requestMsg
as IMethodCallMessage).MethodBase) == true)
{
responseMsg = (IMessage)iMsgSink.AsyncProcessMessage(
requestMsg, null);
}
else
{
responseMsg = iMsgSink.SyncProcessMessage(requestMsg);
}
}
return responseMsg;
}
The client router implementation is much simpler than server one. Updating the router knowledge base and mapping the logical url address to the physical one is done the same way like in the server router. The different is only in the place where it's processing. For the client router, right place is the CreateSink
method. Note that the final url address is passed to the message sink when proxy to the remote object has been created:
public IClientChannelSink CreateSink(IChannelSender channel,
string url, object remoteChannelData)
{
IClientChannelSink Sink = null;
m_strChannelName = channel.ChannelName;
StringBuilder sbUrl = new StringBuilder(m_strChannelName);
sbUrl.Append("://");
try
{
object obj = CallContext.GetData(ProviderName);
if(obj != null && obj is RouterLogicalCallContext)
{
RouterLogicalCallContext rlcc =
obj as RouterLogicalCallContext;
if(rlcc.UrlKB == "")
{
ClearLogicalURL();
}
else
if(rlcc.UrlKB == "?")
{
rlcc.UrlKB = GetLogicalURL();
CallContext.SetData(ProviderName, rlcc);
}
else
{
string[] arrayNewUrl =
rlcc.UrlKB.Split(new char[]{'=',','});
for(int ii=0; ii<arrayNewUrl.Length; ii++)
{
SetLogicalURL(arrayNewUrl[ii].Trim(),
arrayNewUrl[++ii].Trim());
}
}
}
string[] arrayUrl = url.Split(new char[]{';'}, 2);
string lurl = arrayUrl[0].Remove(0, sbUrl.Length).Trim();
if(lurl.IndexOf('/') < 0)
{
string purl = GetLogicalURL(lurl);
if(purl != "")
{
sbUrl.Append(purl);
if(arrayUrl.Length == 2)
{
sbUrl.Append(";");
sbUrl.Append(arrayUrl[1]);
}
url = sbUrl.ToString();
}
}
object ms = m_Next.CreateSink(channel, url,
remoteChannelData);
Sink = new RouterClientSink(this, url, ms);
}
catch(Exception ex)
{
WriteEventLog(string.Format(
"{0}/{1}.CreateSink catch {2}", m_strChannelName,
m_strProviderName, ex.Message), EventLogEntryType.Error);
}
return Sink;
}
The following code snippet shows implementation of the message processing methods in the client message sink. The IMessage.Uri
property is overwritten by value m_Uri
, which represents the physical url address. The client sink just passing the IMessage
to the next sink.
public IMessageCtrl AsyncProcessMessage(IMessage msgReq,
IMessageSink replySink)
{
msgReq.Properties["__Uri"] = m_Url;
IMessageCtrl iMsgCtrl =
m_NextMsgSink.AsyncProcessMessage(msgReq,
replySink);
return iMsgCtrl;
}
public IMessage SyncProcessMessage(IMessage msgReq)
{
msgReq.Properties["__Uri"] = m_Url;
IMessage msgRsp = m_NextMsgSink.SyncProcessMessage(msgReq);
return msgRsp;
}
I created a test solution to test the client and server routers. There is a set of the following projects:
- MessageRouter - client and server routers
- RouterLogicalCallContext - abstract definition of the router call context object
- TestInterface - test interface contract
- TestObject - test remote objects
- ConsoleServer - host process to publish the test remote objects
- WindowsClient - client tester
Note that the last 4 projects have been designed and implemented only for test purposes using the "Hello world" design pattern.
- Install the following assemblies into the GAC: MessageRouter, RouterLogicalCallContext, TestInterface and TestObject
- Modify your machine.config file as I mentioned early.
- Launch the ConsoleServer.exe program
- Launch the WindowsClient.exe program
- Select the url address on the top combo box (see, the following picture)
- Click on the SayHello
- Check the response on the ConsoleServer program box
The url combo box drop menu has many different url addresses, try them to verify the configuration of the client/server routers. You would to update the client/server routers knowledge base using the msg combo box to see the mapping and routing messages on the fly.
This article described a simple solution to chain channels in .NET Remoting. Using the logical url address to described the remoting connectivity in the deployed application allows to significant improve your implementation on the client side and administrated in the config file or programmatically on the fly. The great benefit is using the client router in the application model based on the remoting interface contracts design pattern. There is no need to implement extra code to retrieve a specified url address requested by GetObject
method, the client router will take care of this mapping based on your configuration like in the case of using the new
operator. Chaining channel (standard and custom) will make transparent connectivity to the remoting objects.