In this chapter, you read about the basics of .NET Remoting. You now know the difference between MarshalByRefObjects, which allow you to call server-side
IN THIS CHAPTER, I DEMONSTRATE the key techniques you'll need to know to use .NET Remoting in your real-world applications. I show you the differences between Singleton and SingleCall objects and untangle the mysteries of client-activated objects. I also introduce you to SoapSuds.exe, which can be used to generate proxy objects containing only
As you have seen in the previous chapter's examples, there are two very different types of remote interaction between
Marshalling objects by value means to serialize their state (instance variables), including all objects referenced by instance
When passing the Customer object in the previous chapter's validation example to the server, it is serialized to XML like this:
<a1: Customer id='ref-4'> < FirstName id='ref-5'>Joe</ FirstName > < LastName id='ref-6'>Smith</ LastName > < DateOfBirth >1800-05-12T00:00:00.0000+02:00</ DateOfBirth > </a1: Customer >
This XML document will be read by the server and an exact copy of the object created.
| Note |
An important point to know about ByValue objects is that they are not
remote
objects. All methods on those objects will be executed locally (in the same context) to the caller. This also means that, unlike with MarshalByRefObjects, the compiled class has to be available to the client. You can see this in the
|
When a ByValue object holds references to other objects, those have to be either serializable or MarshalByRefObjects;
A MarshalByRefObject is a remote object that runs on the server and accepts method calls from the client. Its data is stored in the server's memory and its methods executed in the server's AppDomain. Instead of passing around a variable that points to an object of this type, in reality only a pointer-like construct-called an ObjRef-is passed around. Contrary to common pointers, this ObjRef does not contain the memory address, rather the server
Server-activated objects are somewhat comparable to classic stateless Web Services. When a client
Depending on the configuration of its objects, the server then decides whether a new instance will be created or an existing object will be reused. SAOs can be
In the following examples, you'll see the differences between these two kinds of services. You'll use the same shared interface, clientand server-side implementation of the service, and only change the object mode on the server.
The shared assembly General.dll will contain the interface to a very simple remote object that allows the storage and retrieval of stateful information in form of an int value, as shown in Listing 3-1.
Listing 3-1: The Interface Definition That Will Be Compiled to a DLL
|
|
using System; namespace General { public interface IMyRemoteObject { void setValue (int newval); int getValue(); } }
|
|
The client that is shown in Listing 3-2 provides the means for opening a connection to the server and
Listing 3-2: A Simple Client Application
|
|
using System; using System.Runtime.Remoting; using General; using System.Runtime.Remoting.Channels.Http; using System.Runtime.Remoting.Channels; namespace Client { class Client { static void Main(string[] args) { HttpChannel channel = new HttpChannel(); ChannelServices.RegisterChannel(channel); IMyRemoteObject obj = (IMyRemoteObject) Activator.GetObject( typeof(IMyRemoteObject), "http://localhost:1234/MyRemoteObject.soap"); Console.WriteLine("Client.Main(): Reference to rem. obj
acquired
"); int tmp = obj.getValue(); Console.WriteLine("Client.Main(): Original server side value: {0}",tmp); Console.WriteLine('Client.Main(): Will set value to 42"); obj.setValue(42); tmp = obj.getValue(); Console.WriteLine("Client.Main(): New server side value {0}", tmp); Console.ReadLine(); } } }
|
|
The sample client will read and output the server's original value, change it to 42, and then read and output it again.
For SingleCall objects the server will create a single object, execute the method, and destroy the object again. SingleCall objects are registered at the server using the following statement:
RemotingConfiguration.RegisterWellKnownServiceType( typeof(<YourClass>), "<URL>", WellKnownObjectMode.SingleCall );
Objects of this kind can obviously not hold any state information, as all internal variables will be discarded at the end of the method call. The reason for using objects of this kind is that they can be deployed in a very scalable manner. These objects can be located on different computers with an intermediate multiplexing/load-balancing device, which would not be possible when using stateful objects. The complete server for this example can be seen in Listing 3-3.
Listing 3-3: The Complete Server Implementation
|
|
using System; using System.Runtime.Remoting; using General; using System.Runtime.Remoting.Channels.Http; using System.Runtime.Remoting.Channels; namespace Server { class MyRemoteObject: MarshalByRefObject, IMyRemoteObject { int myvalue; public MyRemoteObject() { Console.WriteLine("MyRemoteObject.Constructor: New Object created"); } public MyRemoteObject(int startvalue) { Console.WriteLine("MyRemoteObject.Constructor: .ctor called with {0}", startvalue); myvalue = startvalue; } public void setValue(int newval) { Console.WriteLine("MyRemoteObject.setValue(): old {0} new {1}", myvalue,newval); myvalue = newval; } public int getValue() { Console.WriteLine("MyRemoteObject.getValue(): current {0}",myvalue); return myvalue; } } class ServerStartup { static void Main(string[] args) { Console.WriteLine ("ServerStartup.Main(): Server started"); HttpChannel chnl = new HttpChannel(1234); ChannelServices.RegisterChannel(chnl);
RemotingConfiguration.RegisterWellKnownServiceType( typeof(MyRemoteObject), "MyRemoteObject.soap", WellKnownObjectMode.SingleCall);
// the server will keep running until
keypress
. Console.ReadLine(); } } }
|
|
When the program is run, the output in Figure 3-1 will appear on the client.
Figure 3-1:
Client's output for a SingleCall object
What's happening is exactly what you'd expect from the previous description-even though it might not be what you'd normally expect from an object-oriented application. The reason for the server returning a value of 0 after setting the value to 42 is that your client is talking to a completely different object. Figure 3-2 shows the server's output.
Figure 3-2:
Server's output for a SingleCall object
This indicates that the server will really create one object for each call (and an additional object during the first call as well).
Only one instance of a Singleton object can exist at any given time. When receiving a client's request, the server checks its internal tables to see if an instance of this class already exists; if not, this object will be created and stored in the table. After this check the method will be executed. The server
| Note |
Singletons have an associated lifetime as well, so be sure to override the standard lease time if you don't want your object to be destroyed after some minutes. (More on this later in this chapter.) |
For registering an object as a Singleton, you can use the following lines of code:
RemotingConfiguration.RegisterWellKnownServiceType( typeof(<YourClass>), "<URL>", WellKnownObjectMode.Singleton );
The ServerStartup class in your sample server will be changed
class ServerStartup { static void Main(string[] args) { Console.WriteLine ("ServerStartup.Main(): Server started"); HttpChannel chnl = new HttpChannel(1234); ChannelServices.RegisterChannel(chnl);
RemotingConfiguration.RegisterWellKnownServiceType( typeof(MyRemoteObject), "MyRemoteObject.soap", WellKnownObjectMode.Singleton);
// the server will keep running until keypress. Console.ReadLine(); } }
When the client is started, the output will show a behavior consistent with the "normal" object-oriented way of thinking; the value that is returned is the same value you set two lines before (see Figure 3-3).
Figure 3-3:
Client's output for a Singleton object
The same is true for the server, as Figure 3-4 shows.
Figure 3-4:
Server's output for a Singleton object
An interesting thing happens when a second client is started afterwards. This client will receive a value of 42 directly after startup without your setting this value beforehand (see Figures 3-5 and 3-6). This is because only one instance exists at the server, and the instance will stay
Figure 3-5:
The second client's output when calling a Singleton object
Figure 3-6:
Server's output after the second call to a Singleton object
| Tip |
Use Singletons when you want to share data or resources between clients. |
When using either SingleCall or Singleton objects, the necessary instances will be created dynamically during a client's request. When you want to publish a certain object instance that's been precreated on the server-for example, one using a nondefault constructor-
In this case you can use RemotingServices.Marshal() to publish a given instance that behaves like a Singleton afterwards. The only difference is that the object has to already exist at the server before publication.
YourObject obj = new YourObject(<your params for constr>); RemotingServices.Marshal (obj,"YourUrl.soap");
The code in the ServerStartup class will look like this:
class ServerStartup { static void Main(string[] args) { Console.WriteLine ("ServerStartup.Main(): Server started"); HttpChannel chnl = new HttpChannel(1234); ChannelServices.RegisterChannel(chnl);
MyRemoteObject obj = new MyRemoteObject(4711); RemotingServices.Marshal(obj,"MyRemoteObject.soap");
// the server will keep running until keypress. Console.ReadLine(); } }
When the client is run, you can safely expect to get a value of 4711 on the first request because you started the server with this initial value (see Figures 3-7 and 3-8).
Figure 3-7:
Client's output when calling a published object
Figure 3-8:
Server's output when publishing the object
A client-activated object (CAO) behaves mostly the same way as does a "normal" .NET object (or a COM object). When a creation request on the client is
A client-activated object's lifetime is managed by the same lifetime service used by SAOs, as shown later in this chapter. CAOs are so-called stateful objects; an instance variable that has been set by the client can be retrieved again and will contain the correct value. [1] These objects will store state information from one method call to the other. CAOs are explicitly created by the client, so they can have distinct constructors like normal .NET objects do.
The .NET Remoting Framework can be configured to allow client-activated objects to be created like normal objects using the new operator. Unfortunately, this manner of creation has one serious drawback: you cannot use shared interfaces or base classes. This means that you either have to ship the compiled objects to your clients or use SoapSuds to extract the metadata.
As shipping the implementation to your clients is neither
In the following example, you'll use more or less the same class you did in the previous examples; it will provide your client with a setValue() and getValue() method to store and retrieve an int value as the object's state. The metadata that is needed for the client to create a reference to the CAO will be extracted with SoapSuds.exe, about which you'll read more later in this chapter.
The
Listing 3-4: A Server That Offers a Client-Activated Object
|
|
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels.Http; using System.Runtime.Remoting.Channels; namespace Server { public class MyRemoteObject: MarshalByRefObject { int myvalue; public MyRemoteObject(int val) { Console.WriteLine("MyRemoteObject.ctor(int) called"); myvalue = val; } public MyRemoteObject() { Console.WriteLine("MyRemoteObject.ctor() called"); } public void setValue(int newval) { Console.WriteLine("MyRemoteObject.setValue(): old {0} new {1}", myvalue,newval); myvalue = newval; } public int getValue() { Console.WriteLine("MyRemoteObject.getValue(): current {0}",myvalue); return myvalue; } } class ServerStartup { static void Main(string[] args) { Console.WriteLine ("ServerStartup.Main(): Server started"); HttpChannel chnl = new HttpChannel(1234); ChannelServices.RegisterChannel(chnl);
RemotingConfiguration.ApplicationName = "MyServer"; RemotingConfiguration.RegisterActivatedServiceType( typeof(MyRemoteObject));
// the server will keep running until keypress. Console.ReadLine(); } } }
|
|
On the server you now have the new startup code needed to register a channel and this class as a client-activated object. When adding a Type to the list of activated services, you cannot provide a single URL for each object; instead, you have to set the RemotingConfiguration.ApplicationName to a string value that identifies your server.
The URL to your remote object will be http://<hostname>:<port>/<ApplicationName>. What happens behind the scenes is that a general activation SAO is automatically created by the framework and published at the URL http://<hostname>:<port>/<ApplicationName>/RemoteActivationService.rem. This SAO will take the clients' requests to create a new instance and pass it on to the remoting framework.
To extract the necessary interface information, you can run the following SoapSuds command line in the directory where the server.exe assembly has been placed:
soapsuds -ia:server -nowp -oa:generated_metadata.dll
| Note |
You should perform all command-line operations from the Visual Studio command prompt, which you can bring up by selecting Start
➢
Programs
➢
Microsoft Visual Studio .NET
➢
Visual Studio .NET Tools. This command prompt sets the correct "
|
The resulting generated_metadata.dll assembly must be referenced by the client. The sample client also registers the CAO and acquires two references to (different) remote objects. It then sets the value of those objects and outputs them again, which shows that you really are dealing with two different objects.
As you can see in Listing 3-5, the activation of the remote object is done with the
new
operator. This is possible because you registered the Type as ActivatedClientType before. The runtime now
Listing 3-5: The Client Accesses the Client-Activated Object
|
|
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels.Http; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Activation; using Server; namespace Client { class Client { static void Main(string[] args) { HttpChannel channel = new HttpChannel(); ChannelServices.RegisterChannel(channel);
RemotingConfiguration.RegisterActivatedClientType( typeof(MyRemoteObject), "http://localhost:1234/MyServer");
Console.WriteLine("Client.Main(): Creating first object");
MyRemoteObject obj1 = new MyRemoteObject(); obj1.setValue(42);
Console.WriteLine("Client.Main(): Creating second object");
MyRemoteObject obj2 = new MyRemoteObject(); obj2.setValue(4711);
Console.WriteLine("Obj1.getValue(): {0}",obj1.getValue()); Console.WriteLine("Obj2.getValue(): {0}",obj2.getValue());
Console.ReadLine(); } } }
|
|
When this code sample is run, you will see the same behavior as when using local objects-the two instances have their own state (Figure 3-9). As expected, on the server two different objects are created (Figure 3-10).
Figure 3-9:
Client-side output when using CAOs
Figure 3-10:
Server-side output when using CAOs
From what you've read up to this point, you know that SoapSuds cannot extract the metadata for nondefault constructors. When your application's design relies on this functionality, you can use a factory design pattern, in which you'll include a SAO providing methods that return new instances of the CAO.
| Note |
You might also just ship the server-side implementation assembly to the client and reference it directly. But as I stated previously, this is clearly against all distributed application design principles! |
Here, I just give you a short introduction to the factory design pattern. Basically you have two classes, one of which is a
factory
, and the other is the real object you want to use. Due to constraints of the real class, you will not be able to construct it directly, but instead will have to call a method on the factory, which creates a new instance and
Listing 3-6 shows you a
Listing 3-6: The Factory Design Pattern
|
|
using System; namespace FactoryDesignPattern { class
MyClass
{ } class
MyFactory
{ public MyClass getNewInstance() { return new MyClass(); } } class
MyClient
{ static void Main(string[] args) { // creation using "new" MyClass obj1 = new MyClass(); // creating using a factory
MyFactory fac = new MyFactory(); MyClass obj2 = fac.getNewInstance();
} } }
|
|
When bringing this pattern to remoting, you have to create a factory that's running as a server-activated object (
| Note |
Distributing the implementation to the client is not only a bad choice due to deployment issues, it also makes it possible for the client
|
You have to design your factory SAO using a shared assembly which contains the interface information (or abstract base classes) which are implemented by your remote objects. This is shown in Listing 3-7.
Listing 3-7: The Shared Interfaces for the Factory Design Pattern
|
|
using System; namespace General { public interface
IRemoteObject
{ void setValue(int newval); int getValue(); } public interface
IRemoteFactory
{
IRemoteObject getNewInstance(); IRemoteObject getNewInstance(int initvalue);
} }
|
|
On the server you now have to implement both interfaces and create a startup code that registers the factory as a SAO. You don't have to register the CAO in this case because every MarshalByRefObject can be returned by a method call; the framework takes care of the necessity to remote each call itself, as shown in Listing 3-8.
Listing 3-8: The Server-Side Factory Pattern's Implementation
|
|
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels.Http; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Messaging; using General;
namespace Server
{
class MyRemoteObject: MarshalByRefObject, IRemoteObject
{ int myvalue; public MyRemoteObject(int val) { Console.WriteLine("MyRemoteObject.ctor(int) called"); myvalue = val; } public MyRemoteObject() { Console.WriteLine("MyRemoteObject.ctor() called"); } public void setValue(int newval) { Console.WriteLine("MyRemoteObject.setValue(): old {0} new {1}", myvalue,newval); myvalue = newval; } public int getValue() { Console.WriteLine("MyRemoteObject.getValue(): current {0}",myvalue); return myvalue; } }
class MyRemoteFactory: MarshalByRefObject,IRemoteFactory
{ public MyRemoteFactory() { Console.WriteLine("MyRemoteFactory.ctor() called"); }
public IRemoteObject getNewInstance()
{ Console.WriteLine("MyRemoteFactory.getNewInstance() called");
return new MyRemoteObject();
}
public IRemoteObject getNewInstance(int initvalue)
{ Console.WriteLine("MyRemoteFactory.getNewInstance(int) called");
return new MyRemoteObject(initvalue);
} }
class ServerStartup
{ static void Main(string[] args) { Console.WriteLine ("ServerStartup.Main(): Server started"); HttpChannel chnl = new HttpChannel(1234); ChannelServices.RegisterChannel(chnl);
RemotingConfiguration.RegisterWellKnownServiceType( typeof(MyRemoteFactory), "factory.soap", WellKnownObjectMode.Singleton);
// the server will keep running until keypress. Console.ReadLine(); } } }
|
|
The client, which is shown in Listing 3-9, works a little bit differently from the previous one as well. It creates a reference to a remote SAO using Activator.GetObject() , upon which it places two calls to getNewInstance() to acquire two different remote CAOs.
Listing 3-9: The Client Uses the Factory Pattern
|
|
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels.Http; using System.Runtime.Remoting.Channels.Tcp; using System.Runtime.Remoting.Channels; using General; namespace Client { class Client { static void Main(string[] args) { HttpChannel channel = new HttpChannel(); ChannelServices.RegisterChannel(channel); Console.WriteLine("Client.Main(): Creating factory");
IRemoteFactory fact = (IRemoteFactory) Activator.GetObject( typeof(IRemoteFactory), "http://localhost:1234/factory.soap");
Console.WriteLine("Client.Main(): Acquiring first object from factory");
IRemoteObject obj1 = fact.getNewInstance();
obj1.setValue(42); Console.WriteLine("Client.Main(): Acquiring second object from " + "factory");
IRemoteObject obj2 = fact.getNewInstance(4711);
Console.WriteLine("Obj1.getValue(): {0}",obj1.getValue()); Console.WriteLine("Obj2.getValue(): {0}",obj2.getValue()); Console.ReadLine(); } } }
|
|
When this sample is running, you see that the client behaves nearly identically to the previous example, but the second object's value has been set using the object's constructor, which is called via the factory (Figure 3-11). On the server a factory object is generated, and each new instance is created using the overloaded getNewInstance() method (Figure 3-12).
Figure 3-11:
Client-side output when using a factory object
Figure 3-12:
Server-side output when using a factory object
One point that can lead to a bit of confusion is the way an object's lifetime is managed in the .NET Remoting Framework. Common .NET objects are managed using a garbage collection algorithm that checks if any other object is still using a given instance. If not, the instance will be garbage collected and disposed.
When you apply this schema (or the COM way of reference counting) to remote objects, it
This constraint leads to a new kind of lifetime service: the lease-based object lifetime. Basically this means that each server-side object is associated with a lease upon creation. This lease will have a time-to-live counter (which starts at five minutes by default) that is decremented in certain intervals. In addition to the initial time, a defined amount (two minutes in the default configuration) is added to this time to live upon every method call a client places on the remote object.
When this time
When the sponsor decides that the lease will not be renewed or when the framework is unable to contact any of the registered sponsors, the object is marked as timed out and then garbage collected. When a client still has a reference to a timed-out object and calls a method on it, it will receive an exception.
To change the default lease times, you can override
InitializeLifetimeService()
in the MarshalByRefObject. In the following example, you see how to change the last CAO sample to implement a different lifetime of only ten
namespace Server { class MyRemoteObject: MarshalByRefObject, IRemoteObject {
public override object InitializeLifetimeService()
{ Console.WriteLine("MyRemoteObject.InitializeLifetimeService() called");
ILease lease = (ILease)base.InitializeLifetimeService(); if (lease.CurrentState == LeaseState.Initial)
{
lease.InitialLeaseTime = TimeSpan.FromMilliseconds(10); lease.SponsorshipTimeout = TimeSpan.FromMilliseconds(10); lease.RenewOnCallTime = TimeSpan.FromMilliseconds(10);
} return lease; } // rest of implementation ... } class MyRemoteFactory: MarshalByRefObject,IRemoteFactory { // rest of implementation } class ServerStartup { static void Main(string[] args) { Console.WriteLine ("ServerStartup.Main(): Server started");
LifetimeServices.LeaseManagerPollTime = TimeSpan.FromMilliseconds(10);
HttpChannel chnl = new HttpChannel(1234); ChannelServices.RegisterChannel(chnl); RemotingConfiguration.RegisterWellKnownServiceType( typeof(MyRemoteFactory), "factory.soap", WellKnownObjectMode.Singleton); // the server will keep running until keypress. Console.ReadLine(); } } }
On the client side, you can add a one-second delay between creation and the first call on the remote object to see the effects of the changed lifetime. You also need to provide some code to handle the RemotingException that will get thrown because the object is no longer available at the server. The client is shown in Listing 3-10.
Listing 3-10: A Client That Calls a Timed-Out CAO
|
|
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels.Http; using System.Runtime.Remoting.Channels.Tcp; using System.Runtime.Remoting.Channels; using General; namespace Client { class Client { static void Main(string[] args) { HttpChannel channel = new HttpChannel(); ChannelServices.RegisterChannel(channel); Console.WriteLine("Client.Main(): Creating factory"); IRemoteFactory fact = (IRemoteFactory) Activator.GetObject( typeof(IRemoteFactory), "http://localhost:1234/factory.soap"); Console.WriteLine("Client.Main(): Acquiring object from factory"); IRemoteObject obj1 = fact.getNewInstance(); Console.WriteLine("Client.Main(): Sleeping one second");
System.Threading.Thread.Sleep(1000);
Console.WriteLine("Client.Main(): Setting value"); try {
obj1.setValue(42);
} catch (Exception e) {
Console.WriteLine("Client.Main(). EXCEPTION \n{0}",e.Message);
} Console.ReadLine(); } } }
|
|
Running this sample, you see that the client is able to successfully create a factory object and call its
getNewInstance()
method (Figure 3-13). When calling
setValue()
on the returned CAO, the client will receive an exception
Figure 3-13:
The client receives an exception because the object has timed out.
Figure 3-14:
The server when overriding
InitializeLifetimeService()
[1]
The only exception from this rule lies in the object's lifetime, which is managed completely differently from the way it is in .NET