only for RuBoard |
Now that the server is running, it's time to talk about building the client. The million dollar question is how the client will get the type information for ServerInfo . Directly referencing the ServerInfo assembly is one option, but not a realistic one, so it will not be considered . It doesn't take an active imagination to foresee the problems that can occur. Instead, this section examines a few techniques for consuming remote objects that allow the implementation to change without affecting the client.
The .NET Framework SDK ships with a utility called SoapSuds.exe that can generate a proxy object the client can use. SoapSuds.exe will even generate the source code for the proxy (but only in C#). Just point it to the URL of the remote object, link to the client shown in Example 9-7, and run. The HTTPchannel must be specified along with the port that was given to the server.
The following command creates a proxy object (with C# source) to the Singleton version of ServerInfo :
soapsuds -url:http://192.168.1.100:1970/RemotingTest/ ServerInfo.rem?wsdl -oa:ServerInfoProxy.dll -gc
'Remote Object Client ' 'Compile: 'vbc /t:exe /r:system.dll /r:System.Runtime.Remoting.dll '/r:ServerInfoProxy.dll remoteclient.vb ' Imports System Imports ObjectServerSpace Public Class Client Public Shared Sub Main( ) Try Dim si As New ServerInfo( ) If Not (si Is Nothing) Then Console.WriteLine(si.MachineName) Console.WriteLine(si.IPAddress) Console.WriteLine(si.GetMachineTime) Console.WriteLine("{0}MB Free", si.GetAvailableMemory) Console.WriteLine("Processor Used: {0}", si.GetProcessorUsed) Console.WriteLine("Calls made: {0}", si.CallCount) End If Catch e As Exception Console.WriteLine("Type: {0}", e.GetType( ).FullName) Console.WriteLine(e.Message) End Try Console.WriteLine("Hit ENTER to continue...") Console.ReadLine( ) End Sub End Class
si looks like a local instance of ServerInfo . However, when the example is run (as shown in Figure 9-3), the statistics for the server machine are returned. The mystery is solved when the source code output from SoapSuds is examined. The namespace, class name , and methods are the same as ServerInfo , but the similarity ends there.
If the client is run successively, the call count is steadily incremented, proving that the object is in fact a Singleton. However, Singleton objects do not live forever. When created, server-side objects are associated with a lease , which is a countdown to destruction. By default, the lease time is 5 minutes. If a call is not made during this time, the object is garbage collected. Otherwise, when the lease expires , it is renewed, by default, for another 2 minutes.
After running the client, walk away for a while (get a peanut butter and jelly sandwich or something) and come back. When the client is run again, the call count should be back down to 5, indicating that a new instance of the Singleton was activated.
You can change the lease defaults by overriding the InitializeLifetimeService method in the ServerInfo class. To make the Singleton live forever, just return Nothing :
' Inherited from MarshalByRefObject Public Overrides Function InitializeLifetimeService( ) As Object Return Nothing End Function
Optionally, the lease time can be changed. Here, the initial lease is set to 10 minutes, with a renewal time of 20 seconds:
Public Overrides Function InitializeLifetimeService( ) As Object Dim lease As ILease = CType(MyBase.GetLifetimeService, ILease) If lease.CurrentState = LeaseState.Initial Then lease.InitialLeaseTime = TimeSpan.FromMinutes(10) lease.RenewOnCallTime = TimeSpan.FromSeconds(20) End If Return lease End Function
In Example 9-7, ServerInfo is a server-activated object. This means the server is responsible for the lifetime of the object; it determines when the object is no longer used. Each instance of the client will make calls on the same ServerInfo object.
In contrast, you can specify that a remote object be client-activated , meaning the client will have a say in determining an object's lifetime. To illustrate the concept, two changes must be made.
First, the server configuration file must be changed to allow ServerInfo to be client-activated. An <activated> element that supplies the client-activated type and the assembly in which it is located must be added within <service> , as the following code illustrates:
'RemoteService.exe.config <service> <wellknown.../> <wellknown.../> <wellknown.../> <activated type="ObjectServerSpace.ServerInfo,ServerInfo"/> <service>
The addition of the <activated> tag does not mean that the server will create client-activated types; it means that a client can now specify what kind of activation it wants (client or server).
The client must also be modified to register the type as client-activated, as follows (registration can occur anywhere , but the Main procedure is an obvious possibility):
Imports System.Runtime.Remoting . . . RemotingConfiguration.RegisterActivatedClientType( _ GetType(ServerInfo), "tcp://192.168.1.100:1969/RemotingTest") Dim si As New ServerInfo( )
RegisterActivatedType takes the client-activated type and a URL to the type's location. Note the location does not contain the object URI, only the path to the object.
The client-activated object is still created on the server, but the reference held is unique to the client. Running multiple instances of the client shows that the call count returned by the object is 5 for each instance. When the client terminates, the object is garbage collected as if it were local. However, the CAO still has a lease. If the client did not terminate (and if the object is still in scope) and the lease expired , the object would be garbage collected just like an SAO.
Using SoapSuds to generate proxy assemblies has two shortcomings. First, it doesn't work if the object's constructor takes a parameter (it accepts only default constructors). Second, the server address is hardcoded into the proxy, which makes distribution more difficult. Changes on the server side would require the client to regenerate the proxy.
To get around this problem, you can use an object factory on the server. The idea behind this design pattern is that two interfaces are defined; the first interface (the factory) defines a method that returns the second (the object):
Public Interface IObjectFactory Function CreateObject( ) As IRemoteObject End Interface Public Interface IRemoteObject 'Interface methods correspond to remote object End Interface
The interfaces are kept in a separate assembly that is then shared between the client and the server. The remote object assembly resides on the server and is implemented like this:
Public Class ObjectFactory : Inherits MarshalByRefObject Implements IObjectFactory Public Function CreateObject( ) As IRemoteObject _ Implements IServerInfoFactory.CreateServerInfo Return New RemoteObject( ) End Function End Class Public Class RemoteObject : Inherits MarshalByRefObject Implements IRemoteObject ' Methods corresponding to IRemoteObject End Class
The server configuration file exposes only the ObjectFactory class (which is implemented as a Singleton), preventing clients from establishing a dependency on RemoteObject . New functionality (or implementation of existing methods) can be added to the remote assembly without affecting existing clients.
Example 9-8 contains the object factory interfaces for a new version of ServerInfo .
'vbc /t:library /r:system.dll ServerInterfaces.vb Imports System Imports System.Net Namespace ServerInterfaces Public Interface IServerInfoFactory Function CreateServerInfo( ) As IServerInfo End Interface Public Interface IServerInfo Function GetMachineTime( ) As DateTime Function GetProcessorUsed( ) As Single Function GetAvailableMemory( ) As Long ReadOnly Property IPAddress( ) As IPAddress ReadOnly Property MachineName( ) As String End Interface End Namespace
The ServerInfo class must now be changed in order to implement IServerInfo ; the new version is shown in Example 9-9. In addition to referencing ServerInterfaces.dll , the object factory class must be implemented to return instances of the ServerInfo class.
'vbc /t:library /r:System.dll '/r:ServerInterface.dll ServerInfo.vb Imports System Imports System.Diagnostics Imports System.Net Imports System.Threading 'Private Data Members Imports ServerInterfaces Namespace ObjectServerSpace Public Class ServerInfoFactory : Inherits MarshalByRefObject Implements IServerInfoFactory Public Function CreateServerInfo( ) As IServerInfo _ Implements IServerInfoFactory.CreateServerInfo Dim si As New ServerInfo( ) Return si End Function End Class Public Class ServerInfo : Inherits MarshalByRefObject Implements IServerInfo Public Sub New( ) 'Init data members End Sub 'Shared method Public Function GetMachineTime( ) As DateTime _ Implements IServerInfo.GetMachineTime 'Implementation End Function 'Get % of process currently in use Public Function GetProcessorUsed( ) As Single _ Implements IServerInfo.GetProcessorUsed 'Implementation End Function 'Get MBytes of free memory Public Function GetAvailableMemory( ) As Long _ Implements IServerInfo.GetAvailableMemory 'Implementation End Function Public ReadOnly Property MachineName( ) As String _ Implements IServerInfo.MachineName 'Implementation End Property Public ReadOnly Property IPAddress( ) As IPAddress _ Implements IServerInfo.IPAddress 'Implementation End Property End Class End Namespace
The Windows Service does not need to be recompiled, because it does not directly reference ServerInfo.dll or ServerInterfaces.dll . However, the configuration file must be changed to expose a well-known Singleton object entry for the factory class (see Example 9-10). Generally, if there are entries for the object type returned by the factory in the config file, they can be removed.
<configuration> <system.runtime.remoting> <application name="RemotingTest"> <service> <wellknown mode="Singleton " type="ObjectServerSpace.ServerInfoFactory,ServerInfo " objectUri="ServerInfoFactory.rem" /> <!--not needed <wellknown mode="Singleton" type="ObjectServerSpace.ServerInfo,ServerInfo" objectUri="ServerInfo.rem" /> <wellknown mode="SingleCall" type="ObjectServerSpace.ServerInfo,ServerInfo" objectUri="ServerInfo2.rem" />--> </service> </application> </system.runtime.remoting> </configuration>
ServerInfo.dll and ServerInterfaces.dll should be moved to the same directory as the service.
There are also several small changes to the client. First, add a reference to ServerInterfaces.dll . Then, remove the reference to ServerInfoProxy.dll .
Getting an interface is the factory's purpose. However, since interfaces cannot be created, another mechanism has to be used to obtain the remote object, which, unfortunately , means no transparent activation (e.g., creating an instance using Dim like a normal object).
Activator.GetObject can be called to return an interface reference to an existing server-side object. This method can be used in place of the old ServerInfo declaration:
'Replace this 'Dim si As New ServerInfo( ) 'with this Dim factoryObj As Object = _ Activator.GetObject(GetType(IServerInfoFactory), _ " tcp://192.168.1.100:1969/RemotingTest/ServerInfoFactory.rem") Dim factory As IServerInfoFactory = CType(factoryObj, IServerInfoFactory) Dim si As IServerInfo = factory.CreateServerInfo( )
The first parameter is the interface typein this case, IServerInfoFactory . The second is the URL of the object as defined in the server config file, including the object URI. The returned Object can be typecast to the factory interface. Note, though, that the proxy does not transmit anything over the network until a method is called. Here, that is during CreateServerInfo .
only for RuBoard |