Contents
- Introduction
- Concept and Design
-
Remoting Interface
-
Callback Custom Attribute
-
Remoting Method
- Implementation
-
RemoteCallbackLib
-
RemoteObject (A , X and WS)
-
RemoteObjectX
-
RemoteObjectWS
-
HostServer
-
HostServer.exe.config
-
RemoteWindowsForm
- Initialization
- Callback object
- Invoking the Remote Method
-
RemoteWindowsForm.exe.config
- Test
- Conclusion
Introduction
The object remoting is an integral part of the .Net Framework. It allows to call
an object located on the Enterprise network using the same programming techniques
like access to object located within the application domain. The remoting is a peer-to-peer
communication mechanism between the remote object (server) and its consumer on the
client side. The objects can be hosted in different application domains such as
Web Server, Web Service, COM+ Service, etc. The remoting call is processing
in a synchronously manner with the option of the asynchronously end notification
on the client side. This design pattern is knew as a BeginInvoke/EndInvoke
technique.
The client will receive a notification of the end remoting with the return value
of the called method. Note that the client has no state of the remote method during
this remoting process. (only finally as a returned value). This article will describe
a mechanism of the remoting callback which can be used to notify client about the
remote method state. This solution is suitable for any cases where "real-time"
processing is required. In my simple sample I will demonstrate how to make a Windows
Form more sensitive and pre-emptive during the remoting time using the Remote
Callbacks.
Concept and Design
The concept of the remoting call-back is based on the remoting delegate, where
a callback object is delegated to the remote object within the invoked method.
The following picture shows the remoting callback "plumbing" mechanism:
The remoting part is sitting between the client and server objects. From the application
point of view it represents by the proxy/stub of the remoting object, communication channel
and message format protocol. The client/server connectivity is started at
the remote object with publishing its metadata (url object) and registering the
stub object in the specified listener such as tcp or http channel and port. This
process can be done either programmatically or administratively using a config file.
The client side needs to know where the remote object metadata located and which
channel and port is dedicated for this connectivity (listener channel). Based on this information,
the client will create a proxy object of the remote object within its application domain.
Also like a server side, this process can be completed by either the hard-coding or using the config
file. Using config files to establish a "wiring" between the client/server is more
flexible solution than hard-coding.
The above picture shows two remoting objects between the client and sever
side:
- Remote object - is calling by the client
(Windows Form) to invoke any public member of the remote object. The object
can be setup for any mode such as Singleton, SingleCall or Client Activated.
Note that remoting callback mechanism doesn't need to keep an object state
(for instance: event class), it's a stateless - loosely coupled design
pattern. The connectivity are setup explicitly and transparently (the remote
object has to be published).
- Callback object
- is calling by the remote
object within the method scope. Here is a situation little bit different. The
remote callback is a private client-server contract and it is configured implicitly.
The major different is that the client creating the callback proxy and then it is
delegated to the remote object as a method argument. The callback object is
running in the Singleton mode with unlimited lease time.
Based on the physical connectivity with the remoting objects, the client can
create one common callback object for the same channel and formatter for
different remote objects. This sample using
a common callback object, where Windows Form calls concurrently three different
remote objects and passing delegate of the same callback proxy. Note that each
remote object will create own remote callback proxy based on the delegate metadata and
using the Reflection design pattern.
The following picture shows a situation on the client side using multiple
remoting objects:
This is a generic model for any number of the remoting callback objects. The
callback object is receiving a state from the remote object and passing it to the
thread pool in the "fire&forget" fashion. The worker thread then dispatching a
call to the Windows Control handler based on the cookie and senderId value.
Note that every object is serialized/de-serialized between the proxy/stub,
therefore it has to be derived from the ISerializable (or attributed).
Let's look at closely for some issues which are related with the design
implementation:
Remoting
Interface
Interface is a contract between the object and its consumer. The interfaces
allow to publish an object based on their abstract definitions. This
encapsulation is giving a more flexibility in the design implementation. The
major advantage using interfaces (or abstract class) is their loosely design pattern,
which it may play an important role in the remoting issue. It is a good design
technique to put all common abstract definitions into one assembly. In my
sample I created separate project called a ContractObject
for that issue.
The following is an abstract definition of the remote object contract:
namespace RKiss.RemoteObject
{
public delegate bool RemoteCallback(string sender, object e);
[Guid("B19A2AD2-31F2-4c6e-B5A6-24495670BE02")]
public interface IRmObject
{
string GiveMeCallback(int timeinsec, string ticketId, object wire);
string Echo(string msg);
}
[Serializable]
public class CallbackEventArgs : EventArgs
{
private string _sender;
private string _ticketId;
private object _state;
public string Sender
{
get { return _sender; }
set { _sender = value; }
}
public string TicketId
{
get { return _ticketId; }
set { _ticketId = value; }
}
public object State
{
get { return _state; }
set { _state = value; }
}
}
}
There are three parts of the metadata in its assembly: delegator, interface
and callback's EventArg
class. They can be modified based on needs of the
application. Note that interface has been attributed by Guid value to keep its
ID the same (The .Net Service will be accepted this Guid each time when object is going to
be re-registered into the COM+ catalog)
Callback Custom Attribute
For setup a config of the Remote Callback object on the client side is
suitable to use a custom attribute technique. It will allow to hide and reuse
all implementation for this private remoting object. I created a separate project
- RemoteCallbackLib
to handle this solution.
There is a RemoteCallbackAttribute
to config
any attributed object (of course derived from the
MarshalByRefObject
class) for the Remoting purpose.
The following code snippet shows its usage:
[RemoteCallback("tcp", desc="Callbacks Test")]
public CallbackClass cb = null;
private RemoteCallbackSubscriber sub = null;
The RemoteCallback attribute is activated by its subscriber - RemoteCallbackSubscriber
, which has to
be constructed during the client's initialization:
sub = new RemoteCallbackSubscriber(this);
cb.Parent = this;
Remoting Method
The method
signature of the Remoting object which wants to use the Remoting Callback
mechanism includes less two additional arguments such as the ticketId and objwire
as it is shown in the following snippet:
public string GiveMeCallback(int timeinsec, string ticketId, object objwire)
The ticketId
represents a cookie value to handle
multiple callbacks on the client side. The objwire
is a delegate object of the client's callback proxy.
Implementation
The sample solution of the Remote Callbacks implemenation is divided into
several projects:
- ContractObject - common abstract definitions (see the above)
- RemoteCallbackLib - RemoteCallback Custom Attribute (see the above)
- RemoteObjectA - classic .Net object hosted in the HostServer
- RemoteObjectX - .Net Service (COM+) hosted in the HostServer
- RemoteObjectWS - Web Service hosted in the IIS
- HostServer - console server program
- RemoteWindowsForm - Windows Form client program
also the solution included the following config files:
- HostServer.exe.config - the Remoting config info for objects hosted by
HostServer
- RemoteWindowsForm.exe.config - the config info for Remoting objects using
by this client
RemoteCallbackLib
The following code snippet shows the implementation of the RemoteCallbackAttribute
. Its design is based on the Refection of the "parent"
assembly, where located all metadata of the Callback object. The
RemoteCallbackLib
is built into the separate assembly which it can be reused by another remoting clients.
namespace RKiss.RemoteCallbackLib
{
[AttributeUsage(AttributeTargets.Field)]
public class RemoteCallbackAttribute : Attribute
{
private string _desc;
private string _protocol;
private int _port;
public string desc
{
get { return _desc; }
set {_desc = value; }
}
public string protocol
{
get { return _protocol; }
set {_protocol = value; }
}
public int port
{
get { return _port; }
set {_port = value; }
}
public RemoteCallbackAttribute() : this("tcp", 0) {}
public RemoteCallbackAttribute(string sProtocol) : this(sProtocol, 0) {}
public RemoteCallbackAttribute(string sProtocol, int iPort)
{
protocol = sProtocol;
port = iPort;
}
}
public class RemoteCallbackSubscriber
{
private Hashtable HT = Hashtable.Synchronized(new Hashtable());
private string site = "localhost";
public RemoteCallbackSubscriber(object parent)
{
Type typeParent = parent.GetType();
foreach(FieldInfo fi in typeParent.GetFields())
{
foreach(Attribute attr in fi.GetCustomAttributes(true))
{
if(attr is RemoteCallbackAttribute)
{
RemoteCallbackAttribute rca = attr as RemoteCallbackAttribute;
int port = rca.port;
if(port == 0)
{
Random rdmPort = new Random(~unchecked((int)DateTime.Now.Ticks));
port = rdmPort.Next(1, ushort.MaxValue);
}
IChannel channel = null;
if(rca.protocol == "tcp")
channel = new TcpChannel(port);
else
if(rca.protocol == "http")
channel = new HttpChannel(port);
else
throw new Exception(string.Format("The '{0}' is not recognize protocol.", rca.protocol));
ChannelServices.RegisterChannel(channel);
string nameofCallbackObject = fi.FieldType.FullName;
Type typeofCallbackObject = typeParent.Assembly.GetType(nameofCallbackObject);
RemotingConfiguration.RegisterWellKnownServiceType(
typeofCallbackObject,
nameofCallbackObject,
WellKnownObjectMode.Singleton);
string urlofCallbackObject = string.Format(@"{0}://{1}:{2}/{3}",
rca.protocol, site, port, nameofCallbackObject);
object proxyCB = Activator.GetObject(typeofCallbackObject, urlofCallbackObject);
fi.SetValue(parent, proxyCB);
HT[fi.Name] = channel;
}
}
}
}
public IChannel this [string nameofCallback]
{
get { return HT[nameofCallback] as IChannel; }
}
}
}
RemoteObject (A , X and WS)
The design pattern of the Remoting methods is the same for any of these
remoting objects. The following code snippet shows the RemoteObjectA
class derived from the
MarshalByRefObject
class and IRmObject
interface.
using RKiss.RemoteObject;
namespace RKiss.RemoteObjectA
{
public class RmObjectA : MarshalByRefObject, IRmObject
{
public RmObjectA()
{
Trace.WriteLine(string.Format("[{0}]RmObjectA activated", GetHashCode()));
}
public string Echo(string msg)
{
Trace.WriteLine(string.Format("[{0}]RmObjectA.Echo({1})", GetHashCode(), msg));
return msg;
}
public string GiveMeCallback(int timeinsec, string ticketId, object objwire)
{
RemoteCallback wire = objwire as RemoteCallback;
bool bProgress = true;
CallbackEventArgs cea = new CallbackEventArgs();
cea.TicketId = ticketId;
cea.State = "running ...";
cea.Sender = GetType().ToString();
wire(cea.Sender, cea);
while(timeinsec-- > 0 && bProgress)
{
Thread.Sleep(1000);
cea.State = timeinsec;
bProgress = wire(cea.Sender, cea);
}
cea.State = bProgress?"done":string.Format("aborted at {0}", ++timeinsec);
wire(cea.Sender, cea);
Trace.WriteLine(string.Format("[{0}]RmObjectA.GiveMeCallback({1}, {2}, {3})",
GetHashCode(), timeinsec, ticketId, wire.GetType().FullName));
return ticketId;
}
}
}
There are implementation of two methods of the
IRmObject
interface in the object. The first one -
Echo has a test purpose, the other one -
GiveMeCallback
simulated some time consuming work with notification of
the
method state using the Remoting Callback mechanism. The method state is wrapped
and serialized by the CallbackEventArgs
object.
Now I will show you only differencies in the following remote objects:
RemoteObjectX
This is a .Net Service - transactional and poolable object register into the
COM+ catalog and config as a remoting object. There are attributes for assembly
and class necessary for this object configuration in the COM+ catalog. The other
change, the remote class is
derived from the ServicedComponent
. This is a standard stuff for the .Net Service
configuration.
using RKiss.RemoteObject;
[assembly: ApplicationName("RemoteObjectX")]
[assembly: ApplicationID("026B9E80-6B07-45f0-8EBF-BD35B5D3BB77")]
[assembly: Description("Remoting COM+ test")]
[assembly: ApplicationActivation(ActivationOption.Library)]
[assembly: ApplicationAccessControl(Value = false, Authentication = AuthenticationOption.None)]
namespace RKiss.RemoteObjectX
{
[Guid("B19A2AD2-31F2-4c6e-B5A6-24495670BE02")]
[Description("Remoting Object")]
[ObjectPooling(Enabled=true,MinPoolSize=3,MaxPoolSize=64)]
[Transaction(TransactionOption.Required)]
[ConstructionEnabled(Default="server=ATZ-ROMAN;uid=sa;pwd=;database=Logger")]
[EventTrackingEnabled]
public class RmObjectX : ServicedComponent, IRmObject
{
...
}
}
Note that object has to run in the COM+ library only, that's the limitation
of the Beta2 and it will be fix it in the RTM version.
RemoteObjectWS
This is a Web Service object generated by wizard and modify for the remoting
purpose.
The following code snippet shows its boilerplate:
using RKiss.RemoteObject;
namespace RemoteObjectWS
{
public class RmObjectWS : MarshalByRefObject, IRmObject
{
public RmObjectWS()
{
...
}
#region Component Designer generated code
...
#endregion
[WebMethod]
public string Echo(string msg)
{
...
}
[WebMethod]
public string GiveMeCallback(int timeinsec, string ticketId, object objwire)
{
...
}
}
}
Note that this project has to be unzip and move it into the localhost\RemoteObjectWS
directory in prior of opening the solution.
HostServer
This is a server program to host
a remote object. It's a very simply console program to perform a configuration
of the remote object(s). There are two kinds of options for this configuration
as I mentioned earlier. The option 1 is commented.
namespace RKiss.ServerActivation
{
public class Server
{
public static int Main(string [] args)
{
try
{
RemotingConfiguration.Configure(@"..\..\HostServer.exe.config");
}
catch(Exception ex)
{
System.Console.WriteLine(ex.Message);
}
System.Console.WriteLine("Hit <enter> to exit...");
System.Console.ReadLine();
return 0;
}
}
}
HostServer.exe.config
This is
a configuration file to config the Remoting ObjectA
and ObjectX
on the Tcp
channel port# 12345. Both objects have been choose for server activatation
running as a wellknown object in the SingleCall
mode. This config file can be modified during the
deploying process to match an application environment.
<configuration>
<system.runtime.remoting>
<application name="RemoteTest">
<service>
<wellknown mode="SingleCall"
type="RKiss.RemoteObjectA.RmObjectA, RemoteObjectA"
objectUri="ObjectA" />
</service>
<service>
<wellknown mode="SingleCall"
type="RKiss.RemoteObjectX.RmObjectX, RemoteObjectX"
objectUri="ObjectX" />
</service>
<channels>
<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting"
port="12345" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
RemoteWindowsForm
This is a
client side - consumer of the Remoting objects. The Remoting client can be any
object on the Enterprise Network. This sample is using a Windows Form to
demonstrate an asynchronously invoking remote objects with callbacks to the
Windows Controls.
There are 3 major parts on the client side related to
the remoting callbacks:
Initialization
The Initialization part has the following responsibility for the remoting
process and connectivity:
- subscribing (config) a callback object as a remoting object
- creating proxy objects of the requested remote objects based on the configuration
file
- making an Echo test on the remoting objects
The following code snippet shows that:
public Form1()
{
InitializeComponent();
try
{
sub = new RemoteCallbackSubscriber(this);
cb.Parent = this;
NameValueCollection uriAddr = (NameValueCollection)ConfigurationSettings.GetConfig("Client/urlAddress");
string uriObjectA = (string)uriAddr["objectA"];
string uriObjectX = (string)uriAddr["objectX"];
string uriObjectWS = (string)uriAddr["objectWS"];
Type typeofRI = typeof(RKiss.RemoteObject.IRmObject);
roA = (IRmObject)Activator.GetObject(typeofRI, uriObjectA);
roX = (IRmObject)Activator.GetObject(typeofRI, uriObjectX);
roWS = (IRmObject)Activator.GetObject(typeofRI, uriObjectWS);
textBoxStatus.Text = roWS.Echo(roA.Echo(roX.Echo("This is an ECHO Message")));
}
catch(Exception ex)
{
textBoxStatus.Text = ex.Message;
buttonRM.Hide();
}
}
Callback object
The callback object is the remoting object for the client's remoting objects,
that's why it is derived from the MarshalByRefObject
class. The callback object
has been initiated for infinity lease time (actually its life time is depended
from the client's life time). There is one callback method - Progress with the
arguments (state) passed from the remote object. Based on this state, the DispatchEvent
helper calls a particular Windows Control. Note that callback
method is running in the "fire&forget" fashion to isolated processes and
minimize its respond time. The callback object holding a state of the
progressing and passing back to the remote object which it will allow to abort
the remoting call. This a a great feature of the remoting callbacks to make the
remote call pre-emptive.
public class CallbackClass : MarshalByRefObject
{
private Form1 _parent = null;
private bool _state = false;
public Form1 Parent { set { _parent = value; }}
public bool State { set { lock(this) { _state = value; }}}
public override Object InitializeLifetimeService()
{
ILease lease = (ILease)base.InitializeLifetimeService();
if(lease.CurrentState == LeaseState.Initial)
{
lease.InitialLeaseTime = TimeSpan.FromMinutes(0);
}
return lease;
}
public bool Progress(string sender, object e)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(DispatchEvent), e);
return _state;
}
private void DispatchEvent(object e)
{
CallbackEventArgs cea = e as CallbackEventArgs;
lock(this)
{
if(cea.TicketId == "ProgressBar")
{
if(cea.State is int)
{
_parent.TextBoxStatus.Text = cea.State.ToString();
_parent.ProgressBarStatus.PerformStep();
}
else
if(cea.State is string)
_parent.TextBoxStatus.Text = cea.State.ToString();
}
else
if(cea.TicketId == "TextBox")
{
if(_parent.TextBoxStatus.BackColor == Color.Yellow)
_parent.TextBoxStatus.BackColor = Color.White;
else
_parent.TextBoxStatus.BackColor = Color.Yellow;
}
else
if(cea.TicketId == "Button")
{
if(_parent.ButtonRM.ForeColor == Color.Magenta)
_parent.ButtonRM.ForeColor = Color.Black;
else
_parent.ButtonRM.ForeColor = Color.Magenta;
}
}
}
}
Invoking the Remote Method
This code snippet shows
invoking a method on the Remoting objects using the
asynchronous design pattern. Clicking on the button Run/Abort the following code
snippet is going to be perform:
private void buttonRM_Click(object sender, System.EventArgs e)
{
try
{
if(buttonRM.Text == "Run")
{
buttonRM.Text = "Busy";
progressBarStatus.Value = 0;
textBoxStatus.Text = string.Empty;
cb.State = true;
int timeinsec = 10;
string ticketId = "ProgressBar";
RemoteCallback wire = new RemoteCallback(cb.Progress);
AsyncCallback acbA = new AsyncCallback(asyncCallBackA);
DelegateGiveMeCallback dA = new DelegateGiveMeCallback(roA.GiveMeCallback);
IAsyncResult arA = dA.BeginInvoke(timeinsec, ticketId, wire, acbA, null);
AsyncCallback acbX = new AsyncCallback(asyncCallBackX);
DelegateGiveMeCallback dX = new DelegateGiveMeCallback(roX.GiveMeCallback);
IAsyncResult arX = dX.BeginInvoke(timeinsec, "TextBox", wire, acbX, null);
AsyncCallback acbWS = new AsyncCallback(asyncCallBackWS);
DelegateGiveMeCallback dWS = new DelegateGiveMeCallback(roWS.GiveMeCallback);
IAsyncResult arWS = dWS.BeginInvoke(timeinsec, "Button", wire, acbWS, null);
buttonRM.Text = "Abort";
}
else
if(buttonRM.Text == "Abort")
{
cb.State = false;
}
}
catch(Exception ex)
{
textBoxStatus.Text = ex.Message;
buttonRM.Text = "Run";
}
}
private void asyncCallBackA(IAsyncResult ar)
{
DelegateGiveMeCallback d = (DelegateGiveMeCallback)((AsyncResult)ar).AsyncDelegate;
object o = d.EndInvoke(ar);
buttonRM.Text = "Run";
}
private void asyncCallBackX(IAsyncResult ar)
{
DelegateGiveMeCallback d = (DelegateGiveMeCallback)((AsyncResult)ar).AsyncDelegate;
object o = d.EndInvoke(ar);
buttonRM.Text = "Run";
}
private void asyncCallBackWS(IAsyncResult ar)
{
DelegateGiveMeCallback d = (DelegateGiveMeCallback)((AsyncResult)ar).AsyncDelegate;
object o = d.EndInvoke(ar);
buttonRM.Text = "Run";
}
The design implementation is the same for any remoting object. It's starting
to create a callback delegator - RemoteCallback
for
the specified callback method (Progress). Secondly, the
AsyncCallBack
is initiated for the particular handler function (for
instance; asyncCallBackA
). This function is going
to be called at the end of the remoting call. Next step is to create a delegator
of the Remoting method for the asynchronous call. As a last step is invoking a
delegator method using the BeginInvoke
function.
Note that all calls are processing without any blocking, so the Windows
Control thread can be yielded and be ready to process any event such as callbacks,
user interface, asyncCallBacks, etc. When the remoting is done, the post-call
function (handler) is called to finish and resulting the remoting method.
RemoteWindowsForm.exe.config
This is a client configuration file. As you can see it's a different from the
server side and also from the standard one. There is no section for the
system.runtime.remoting. I created a custom section
Client\urlAddress where located an objecturi address of the remote
object which client wants to talk.
<configuration>
<configSections>
<sectionGroup name="Client">
<section name="urlAddress" type="System.Configuration.NameValueSectionHandler,System" />
</sectionGroup>
</configSections>
<Client>
<urlAddress>
<add key="objectA" value="tcp://localhost:12345/RemoteTest/ObjectA" />
<add key="objectX" value="tcp://localhost:12345/RemoteTest/ObjectX" />
<add key="objectWS" value="http://localhost/RemoteObjectWS/RemoteObjectWS.soap" />
</urlAddress>
</Client>
</configuration>
Test
Testing of the Remoting Callbacks solution needs to install properly Web Service
and .Net Service projects. The package has included a batch files to make their
installation easy. Please, look at them an change their pathname related to your
setup environment if it's necessary. After that, launch the client program (RemoteWindowsForm)
and you should see the following form:
The Echo Message has been nested trough all of three remote objects. This is
an indication that your connectivity with the remote objects are valid. Now
click the button Run to process the remoting callbacks.
As you can see, each remote object will notify its control on the Window Form
such as the progress bar, text and button colors. Pressing the button Abort the
remoting process will be aborted on the all remoting objects. After these simple
test, lunch more clients (for instance five) and repeat the above test
simultaneously for all of them. I hope everything is working well like on my
machine.
Conclusion
Using the Remoting Callback in the .Net Application
enhancing its event driven model, making more pre-emptive and realistic. As
typically example is downloading a large file or query database. In this article
I described the capability of the .Net Remoting and Reflexion which they play
very significant role in the .Net Enterprise Solutions.